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:
@@ -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.
|
||||
|
||||
@@ -520,9 +520,15 @@ describe("TerminalView", () => {
|
||||
"application/x-agenthub-attach": '{"path":"@src/a.ts","name":"a.ts"}',
|
||||
"text/plain": "src/a.ts",
|
||||
};
|
||||
const dataTransfer = { types: Object.keys(data), files: [], getData: (k: string) => data[k] ?? "" };
|
||||
const dataTransfer = {
|
||||
types: Object.keys(data),
|
||||
files: [],
|
||||
getData: (k: string) => data[k] ?? "",
|
||||
};
|
||||
fireEvent.dragEnter(screen.getByTestId("terminal-view"), { dataTransfer });
|
||||
expect(screen.getByTestId("terminal-drop")).toHaveTextContent("Drop to paste into the terminal");
|
||||
expect(screen.getByTestId("terminal-drop")).toHaveTextContent(
|
||||
"Drop to paste into the terminal"
|
||||
);
|
||||
fireEvent.drop(screen.getByTestId("terminal-view"), { dataTransfer });
|
||||
expect(pasteMock).toHaveBeenCalledWith("src/a.ts ");
|
||||
expect(screen.queryByTestId("terminal-drop")).toBeNull();
|
||||
@@ -582,6 +588,24 @@ describe("TerminalView", () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
// ^Z suspended claude: the pane fell to its shell and the session looked
|
||||
// closed. Ctrl+Z is undo in a browser, so it sends Ctrl+_ instead.
|
||||
it("turns Ctrl+Z into undo (Ctrl+_) and never sends ^Z, which would suspend the session", () => {
|
||||
inputMock.mockClear();
|
||||
render(<TerminalView runId="r1" wsBaseUrl="ws://x" />);
|
||||
const handler = keyHandlerMock.mock.calls[0]![0]! as (e: KeyboardEvent) => boolean;
|
||||
const ev = new KeyboardEvent("keydown", { key: "z", ctrlKey: true, cancelable: true });
|
||||
expect(handler(ev)).toBe(false);
|
||||
expect(ev.defaultPrevented).toBe(true);
|
||||
expect(inputMock).toHaveBeenCalledWith("\x1f", true);
|
||||
expect(inputMock).not.toHaveBeenCalledWith("\x1a", expect.anything());
|
||||
// Other chords with Z are left to xterm (Ctrl+Shift+Z, plain z).
|
||||
expect(handler(new KeyboardEvent("keydown", { key: "Z", ctrlKey: true, shiftKey: true }))).toBe(
|
||||
true
|
||||
);
|
||||
expect(handler(new KeyboardEvent("keydown", { key: "z" }))).toBe(true);
|
||||
});
|
||||
|
||||
// Safari + macOS's own Vietnamese Telex: the first letter is an insertText,
|
||||
// every later key REPLACES the word — the event order captured on a Mac.
|
||||
it("forwards Safari Telex's word replacements as backspaces plus the new tail", () => {
|
||||
|
||||
Reference in New Issue
Block a user