feat: v2.7.8 — Artifacts hub, clone related repos as extra folders, quieter resume prompt

- Artifacts (/artifacts): claude.ai pages sessions publish (captured from the
  Artifact tool's hook), pinned workspace files, skills and plugins, each
  kept as a snapshot; shared ones appear in a Hub and a skill or plugin
  installs into the viewer's own account (server/lib/artifacts.js,
  /api/artifacts, docs/plans/artifacts/).
- Workspace extra folders: clone one or more related repos beside the
  workspace and add them in one step (POST /api/lanes/:id/sources/clone).
- A pane that ended by itself while tmux kept running is no longer offered
  for resume (tmux.serverStartedAt in reconcileOrphans).
- Changes panel: a folder that is not a git repo opens on Files, and its
  files open from there.
- Plus the parallel sessions' work in this tree: File Explorer button in the
  panel header, user delete, RSA SSH keys, chat/terminal and pet fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 16:04:03 +07:00
parent e94038481e
commit 22de5009ff
79 changed files with 4617 additions and 231 deletions
+30
View File
@@ -150,6 +150,36 @@ describe("dashboard-runs", () => {
assert.deepEqual(offered(), []);
});
it("does not offer a pane that ended by itself while its tmux server ran on", () => {
const { db } = require("../db");
db.prepare("INSERT INTO lanes (id, title, cwd) VALUES (21, 'w', '/j')").run();
const session = db.prepare(
"INSERT INTO sessions (id, name, status, cwd) VALUES (?, ?, 'active', '/j')"
);
for (const id of ["q-codex", "q-signal", "q-crash", "q-reboot"]) session.run(id, id);
db.prepare(
"INSERT INTO events (session_id, event_type, data) VALUES ('q-signal', 'SessionEnd', ?)"
).run(JSON.stringify({ reason: "other" }));
start("agenthub-lane-21", { laneId: 21, sessionId: "q-codex", tool: "codex" });
start("agenthub-lane-21-1", { laneId: 21, sessionId: "q-signal" });
start("agenthub-lane-21-2", { laneId: 21, sessionId: "q-crash" });
start("agenthub-lane-21-3", { laneId: 21, sessionId: "q-reboot", tool: "codex" });
// The host's tmux has run since long before; lane 21-3's server is gone.
const since = (id) => (id === "agenthub-lane-21-3" ? null : 0);
runs.reconcileOrphans((id) => !id.startsWith("agenthub-lane-21"), since);
const status = (id) => runs.getRun(id).status;
assert.equal(status("agenthub-lane-21"), "abandoned", "Codex quit by the user");
assert.equal(status("agenthub-lane-21-1"), "abandoned", "closed, tmux ran on");
assert.equal(status("agenthub-lane-21-2"), "interrupted", "Claude never said it ended");
assert.equal(status("agenthub-lane-21-3"), "interrupted", "died with its server");
// A server newer than the run restarted under it: interrupted too.
start("agenthub-lane-21-4", { laneId: 21, sessionId: "q-codex", tool: "codex" });
runs.reconcileOrphans((id) => !id.startsWith("agenthub-lane-21"), () => Date.now());
assert.equal(status("agenthub-lane-21-4"), "interrupted");
});
it("gives a hookless tool's session the lane of the run it started in", () => {
const { db } = require("../db");
start("agenthub-lane-7-1", {