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
@@ -434,6 +434,45 @@ describe("PaneChat — the unsent prompt", () => {
await waitFor(() => expect(localStorage.getItem("agenthub.chat.draft.s1")).toBeNull());
});
// A drag from outside the dashboard (a file manager, VS Code, a selection)
// carries only text: the terminal beneath pastes it, so the chat takes it too.
const textDrop = (data: Record<string, string>) => {
const drop = new Event("drop", { bubbles: true, cancelable: true });
Object.defineProperty(drop, "dataTransfer", {
value: { types: Object.keys(data), getData: (t: string) => data[t] ?? "", files: [] },
});
act(() => {
screen.getByTestId("pane-chat").dispatchEvent(drop);
});
return drop;
};
it("attaches the local files of a dropped file:// list", async () => {
renderChat();
const drop = textDrop({
"text/uri-list": "file:///home/u/My%20Docs/spec.md\r\nfile:///home/u/b.ts",
"text/plain": "/home/u/My Docs/spec.md",
});
expect(drop.defaultPrevented).toBe(true);
expect(await screen.findByText("spec.md")).toBeInTheDocument();
expect(screen.getByText("b.ts")).toBeInTheDocument();
fireEvent.change(screen.getByPlaceholderText("Write a message…"), {
target: { value: "read" },
});
fireEvent.click(screen.getByTestId("pane-chat-send"));
expect(submitToRun).toHaveBeenCalledWith(
"agenthub-lane-1",
'read @"/home/u/My Docs/spec.md" @/home/u/b.ts'
);
});
it("inserts dropped plain text into the prompt", () => {
renderChat();
const drop = textDrop({ "text/plain": "src/lib/x.ts" });
expect(drop.defaultPrevented).toBe(true);
expect(screen.getByPlaceholderText("Write a message…")).toHaveValue("src/lib/x.ts");
});
// Issue #73: before the session starts the CLI may be asking in the terminal
// (trust this folder? — "No, exit" preselected); an Enter sent from here
// would answer it, so a send asks first.