fix: v2.6.1 — panes start in their folder again after an update (#69), Changes panel stacks every repo, clickable paths in chat
- #69: tmux applies a pane's `-c` only while its server's own cwd exists. The server was forked from the dashboard's cwd (the plugin cache dir), outlives updates, and the old cache dir is pruned — after that every new pane started in a deleted directory, where `claude` died at once ("run not found") and a shell printed getcwd errors. tmux calls now run from `/`, and the pane's sh wrapper cds into its folder itself, which also heals a tmux server that is already stuck. - Changes panel: a workspace's extra folders are sections stacked under the workspace folder (VS Code Source Control style), with Close repository / Close other repositories on a header's right-click. - Chat: URLs and absolute paths are links; Claude Code's `[Image: source:]` line folds into its turn; uploads under `<data dir>/drops/` are listed. - Update check: a checkout holding commits a newer release lacks reports `diverged_from_release` instead of offering a fast-forward that can only fail.
This commit is contained in:
+21
-12
@@ -81,20 +81,29 @@ const WAIT_FOR_CLIENT =
|
||||
const CLIENT_WAIT_MS = 2000;
|
||||
|
||||
/**
|
||||
* A pane's tmux command: `argv` behind {@link WAIT_FOR_CLIENT}, or — with no
|
||||
* `argv` — the login shell behind it. Windows has no `sh` (a WinGet tmux runs
|
||||
* native programs), and an `sh -c` there died at once and took the session
|
||||
* with it, so it gets the command itself, or nothing for tmux's own default
|
||||
* shell.
|
||||
* `sh` that moves into `$1` itself. tmux applies `-c` only while its server's
|
||||
* own cwd still exists: the server is forked from the dashboard's cwd — a
|
||||
* plugin cache dir, deleted by `pruneRetiredPluginRoots` after an update the
|
||||
* server outlives — and from then on every pane started in that deleted dir,
|
||||
* where `claude` dies at once and a shell prints `getcwd` errors (issue #69).
|
||||
*/
|
||||
const IN_CWD = 'cd -- "$1" 2>/dev/null; shift;';
|
||||
|
||||
/**
|
||||
* A pane's tmux command, run in `cwd`: `argv` behind {@link WAIT_FOR_CLIENT},
|
||||
* or — with no `argv` — the login shell behind it. Windows has no `sh` (a
|
||||
* WinGet tmux runs native programs), and an `sh -c` there died at once and
|
||||
* took the session with it, so it gets the command itself, or nothing for
|
||||
* tmux's own default shell.
|
||||
* ponytail: no wait for the attach on Windows, so a pane may first draw at
|
||||
* 80×24 (issue #55); port the wait to PowerShell if that shows.
|
||||
*/
|
||||
function paneArgv(argv, platform = process.platform) {
|
||||
function paneArgv(argv, cwd, platform = process.platform) {
|
||||
if (platform === "win32") return argv || [];
|
||||
// argv rides as sh's positional parameters, never inside the script text.
|
||||
// cwd and argv ride as sh's positional parameters, never inside the script text.
|
||||
return argv
|
||||
? ["sh", "-c", `${WAIT_FOR_CLIENT} exec "$@"`, "sh", ...argv]
|
||||
: ["sh", "-c", `${WAIT_FOR_CLIENT} exec "$SHELL" -l`];
|
||||
? ["sh", "-c", `${IN_CWD} ${WAIT_FOR_CLIENT} exec "$@"`, "sh", cwd, ...argv]
|
||||
: ["sh", "-c", `${IN_CWD} ${WAIT_FOR_CLIENT} exec "$SHELL" -l`, "sh", cwd];
|
||||
}
|
||||
const CODEX_SANDBOXES = new Set(["read-only", "workspace-write", "danger-full-access"]);
|
||||
const CODEX_APPROVALS = new Set(["on-request", "never"]);
|
||||
@@ -614,7 +623,7 @@ function spawnRun(args) {
|
||||
|
||||
if (!tmux.hasSession(id)) {
|
||||
codexPool?.place();
|
||||
tmux.newSession({ name: id, cwd, argv: paneArgv(paneCmd), env });
|
||||
tmux.newSession({ name: id, cwd, argv: paneArgv(paneCmd, cwd), env });
|
||||
record();
|
||||
schedulePaste();
|
||||
} else if (SHELL_COMMANDS.has(tmux.paneCommand(id) || "")) {
|
||||
@@ -625,7 +634,7 @@ function spawnRun(args) {
|
||||
// still answered 200. Run the argv in the pane the user already sees
|
||||
// instead of erroring or opening a second session.
|
||||
codexPool?.place();
|
||||
tmux.sendCommand(id, paneCmd, env, cwd);
|
||||
tmux.sendCommand(id, paneArgv(paneCmd, cwd), env, cwd);
|
||||
record();
|
||||
schedulePaste();
|
||||
}
|
||||
@@ -677,7 +686,7 @@ function spawnShell({ laneId, slot = 0, cwd }) {
|
||||
const paneEnv = member ? require("./run-as").memberPaneEnv(member, env) : env;
|
||||
// Started once the pane has its real size (WAIT_FOR_CLIENT, issue #55), as
|
||||
// the login shell tmux would have run (tmux sets SHELL in the pane).
|
||||
tmux.newSession({ name: id, cwd, argv: paneArgv(null), env: paneEnv });
|
||||
tmux.newSession({ name: id, cwd, argv: paneArgv(null, cwd), env: paneEnv });
|
||||
dashboardRuns?.recordRun({
|
||||
id,
|
||||
sessionId: null,
|
||||
|
||||
+3
-1
@@ -38,7 +38,9 @@ const { execFile, execFileSync } = require("node:child_process");
|
||||
// try/catch and treats failure as "no session" — piping tmux's own
|
||||
// "error connecting to ..." noise to the process's stderr just spams the
|
||||
// journal on every poll while no tmux server is running yet.
|
||||
const EXEC_OPTS = { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] };
|
||||
// cwd "/": a tmux server is forked from the call that starts it and keeps
|
||||
// that cwd; once it is deleted, tmux ignores every pane's `-c` (issue #69).
|
||||
const EXEC_OPTS = { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"], cwd: "/" };
|
||||
let execImpl = (args, opts = EXEC_OPTS) => execFileSync("tmux", args, opts);
|
||||
|
||||
// Linux: the tmux server lives in the cgroup of whatever started it — the
|
||||
|
||||
@@ -698,6 +698,16 @@ async function getUpdatesStatus(gitRoot = DEFAULT_ROOT, options = {}) {
|
||||
`This dashboard runs from the Claude Code plugin cache (marketplace '${pluginInstall.marketplace}'), ` +
|
||||
`so it can't update itself. Run the commands above, then '/reload-plugins' in Claude Code — ` +
|
||||
`or just run '/agenthub-update', which does both and rebuilds the runtime.`;
|
||||
} else if (release.ok && releaseIsNewer && commitsAheadOfRelease > 0) {
|
||||
// HEAD carries commits the release lacks (e.g. local wip that the release
|
||||
// squashed into a new commit): `merge --ff-only` can only fail, and how to
|
||||
// reconcile is the user's call, so fetch the tag and say why.
|
||||
situation = "diverged_from_release";
|
||||
manualParts = [`cd "${root}"`, `git fetch ${canonicalRemote} --tags`];
|
||||
situationNote =
|
||||
`Your checkout has ${commitsAheadOfRelease} commit(s) that release ${release.tag} doesn't contain, ` +
|
||||
`so it can't be fast-forwarded. If they're already in the release, move onto it with ` +
|
||||
`'git reset --keep ${release.tag}'; otherwise merge or rebase onto ${release.tag}.`;
|
||||
} else if (release.ok && releaseIsNewer) {
|
||||
// Move onto the release TAG rather than the branch tip: the tag is what
|
||||
// was published, and a fast-forward keeps the tracking branch intact (a
|
||||
|
||||
Reference in New Issue
Block a user