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:
2026-10-09 17:24:14 +07:00
parent fa156d6a59
commit 96e4d28f7c
25 changed files with 589 additions and 208 deletions
+23 -10
View File
@@ -10,6 +10,7 @@ const assert = require("node:assert/strict");
const path = require("node:path");
const fs = require("node:fs");
const os = require("node:os");
const { execFileSync } = require("node:child_process");
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "pty-run-test-"));
process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db");
@@ -80,7 +81,7 @@ describe("pty-run", () => {
});
try {
const t0 = Date.now();
const child = spawn("sh", ["-c", argv[2]], {
const child = spawn("sh", ["-c", argv[2], "sh", os.tmpdir()], {
env: { ...process.env, SHELL: "/bin/echo", PATH: `${bin}:${process.env.PATH}` },
});
let out = "";
@@ -168,7 +169,7 @@ describe("pty-run", () => {
const respawn = calls.find((c) => c[0] === "respawn-pane");
assert.ok(respawn, "expected the idle shell to be replaced by the argv");
const argv = respawn.slice(respawn.indexOf("--") + 1);
assert.equal(argv[0], "claude");
assert.deepEqual(argv.slice(3, 6), ["sh", "/tmp/repo", "claude"]);
assert.deepEqual(argv.slice(-2), ["--resume", "abc12345"]);
});
@@ -507,21 +508,32 @@ describe("pty-run", () => {
}
/** The CLI's own command line: what tmux passes after the `--`, less the
* `sh -c '<wait for a client>; exec "$@"' sh` wrapper spawnRun puts first. */
* `sh -c '<cd; wait for a client>; exec "$@"' sh <cwd>` wrapper spawnRun
* puts first. */
function paneArgv(newSessionCall) {
const argv = newSessionCall.slice(newSessionCall.indexOf("--") + 1);
assert.deepEqual([argv[0], argv[1], argv[3]], ["sh", "-c", "sh"]);
assert.match(argv[2], /session_attached.*exec "\$@"$/);
return argv.slice(4);
assert.match(argv[2], /^cd -- "\$1".*session_attached.*exec "\$@"$/);
return argv.slice(5);
}
it("runs a Windows pane's command itself — no sh there — and a Terminal as tmux's default shell", () => {
const argv = ["C:\\Users\\u\\.local\\bin\\claude.exe", "--model", "opus"];
assert.deepEqual(pty.paneArgv(argv, "win32"), argv);
assert.deepEqual(pty.paneArgv(null, "win32"), []);
assert.equal(pty.paneArgv(argv, "linux")[0], "sh");
assert.deepEqual(pty.paneArgv(argv, "linux").slice(-3), argv);
assert.match(pty.paneArgv(null, "linux")[2], /exec "\$SHELL" -l$/);
assert.deepEqual(pty.paneArgv(argv, "C:\\w", "win32"), argv);
assert.deepEqual(pty.paneArgv(null, "C:\\w", "win32"), []);
assert.equal(pty.paneArgv(argv, "/w", "linux")[0], "sh");
assert.deepEqual(pty.paneArgv(argv, "/w", "linux").slice(-3), argv);
assert.match(pty.paneArgv(null, "/w", "linux")[2], /exec "\$SHELL" -l$/);
});
it("starts a pane in its folder even where tmux skips -c (issue #69)", () => {
// A tmux server whose own cwd was deleted (a pruned plugin cache dir)
// ignores `-c` and hands every pane that deleted cwd.
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pane-cwd-"));
const argv = pty.paneArgv(["pwd"], dir, "linux");
const out = execFileSync(argv[0], argv.slice(1), { cwd: "/", encoding: "utf8" });
assert.equal(fs.realpathSync(out.trim()), fs.realpathSync(dir));
fs.rmSync(dir, { recursive: true });
});
it("hands a long prompt to the pane through a file, never tmux's argv (issue #68)", (t) => {
@@ -578,6 +590,7 @@ describe("pty-run", () => {
[
...call.slice(call.indexOf("--") + 2, call.indexOf("--") + 4),
"sh",
os.tmpdir(),
"printf",
"%s|",
"x y",