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
+42
View File
@@ -3186,6 +3186,48 @@ describe("GET /api/sessions/:id/file", () => {
fs.rmSync(dir, { recursive: true, force: true });
assert.equal((await fetch(url(report))).status, 404);
});
it("serves a file the session's Bash or reply named as a whole path (issue #76)", async () => {
const sid = "named-file-" + Date.now();
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ah-named-file-"));
const report = path.join(dir, "REPORT_ssh_ec2.md");
const out = path.join(dir, "out.txt");
const chart = path.join(dir, "chart.png");
const stem = path.join(dir, "REPORT");
const unread = path.join(dir, "locked.md");
fs.writeFileSync(report, "# Audit");
fs.writeFileSync(out, "generated");
fs.writeFileSync(chart, "PNGBYTES");
fs.writeFileSync(stem, "stem");
fs.writeFileSync(unread, "locked");
fs.chmodSync(unread, 0o000);
await post("/api/hooks/event", { hook_type: "SessionStart", data: { session_id: sid } });
await post("/api/hooks/event", {
hook_type: "PostToolUse",
data: {
session_id: sid,
tool_name: "Bash",
tool_input: { command: `python gen.py -o ${out} && plot ${chart} ${unread}` },
tool_response: { stdout: "done", stderr: "" },
},
});
await post("/api/hooks/event", {
hook_type: "Stop",
data: { session_id: sid, last_assistant_message: `Báo cáo: \`${report}\`.` },
});
const url = (p) => `/api/sessions/${sid}/file?path=${encodeURIComponent(p)}`;
assert.deepEqual((await fetch(url(report))).body, { text: "# Audit", truncated: false });
assert.deepEqual((await fetch(url(out))).body, { text: "generated", truncated: false });
const img = await fetch(`/api/sessions/${sid}/image?path=${encodeURIComponent(chart)}`);
assert.equal(img.status, 200);
// Only a WHOLE path: `<dir>/REPORT` is a prefix of the named report, not named itself.
assert.equal((await fetch(url(stem))).status, 404);
// Named, but its account could not read it (root sees through the mode).
if (process.getuid?.() !== 0) assert.equal((await fetch(url(unread))).status, 404);
fs.chmodSync(unread, 0o600);
fs.rmSync(dir, { recursive: true, force: true });
});
});
describe("GET /api/sessions/:id/image", () => {