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
@@ -291,6 +291,39 @@ describe("getUpdatesStatus — release drives update_available", () => {
assert.match(status.manual_command, /git merge --ff-only v1\.6\.0/);
});
it("does not offer a fast-forward when HEAD holds commits the newer release lacks", async () => {
// The release squashed the local wip commits into a new one: neither side
// contains the other, so `merge --ff-only` could only fail.
const repo = makeVersionedRepo(tmpDir, "diverged", "1.5.0", null);
git(repo, ["checkout", "-b", "rel"]);
fs.writeFileSync(
path.join(repo, "package.json"),
JSON.stringify({ name: "fx", version: "1.6.0" })
);
git(repo, [...IDENT, "commit", "-am", "release"]);
git(repo, ["tag", "v1.6.0"]);
git(repo, ["checkout", "main"]);
fs.writeFileSync(path.join(repo, "wip.txt"), "wip");
git(repo, [...IDENT, "add", "."]);
git(repo, [...IDENT, "commit", "-m", "wip"]);
const status = await getUpdatesStatus(repo, {
skipFetch: true,
latestRelease: {
ok: true,
tag: "v1.6.0",
name: "1.6.0",
url: "u",
published_at: null,
notes: null,
},
});
assert.equal(status.update_available, true);
assert.equal(status.situation, "diverged_from_release");
assert.doesNotMatch(status.manual_command, /--ff-only/);
assert.match(status.situation_note, /v1\.6\.0/);
});
it("is up to date — not a dev build — when local version equals the release and no commits are ahead", async () => {
const repo = makeVersionedRepo(tmpDir, "level", "1.5.0", "v1.5.0");
const status = await getUpdatesStatus(repo, {