perf: faster plugin updates, half-size first bundle, images from the chat attach on Windows

- Chat view: an attached image's path is pasted on its own after the text
  (promptForPane). claude turns a paste that is only an image path into
  [Image #n], but left a Windows path inside a paste of text as a plain path.
  Other files and @-mentions stay in the text as before.
- Plugin bootstrap: depsHash covers only what npm installs (no release
  version), so a version bump no longer reinstalls runtime deps; the client's
  installed tree is kept while its manifests are unchanged (no npm ci per
  update); the user-side build runs vite only (tsc -b cost ~25 s / ~900 MB);
  clientBuild is recorded only after a build that did not fail, so a failed
  one is retried; npm output goes to runtime/npm-install.log.
- Server: a port held by another program ends with a clear message and exit 1
  instead of an uncaught 'error' stack trace.
- Client: the onboarding wizard is lazy and rendered only before onboarding;
  usePrefersReducedMotion moved to hooks/ so PetSettings no longer pulls the
  Sagi chat/pane stack into the entry chunk (884 KB -> 431 KB, gzip 268 -> 133 KB).
This commit is contained in:
2026-10-16 10:05:03 +07:00
parent a58e95a957
commit ecbbb4dd8f
17 changed files with 313 additions and 89 deletions
@@ -267,7 +267,7 @@ describe("PaneChat — model, effort and permission mode", () => {
renderChat();
fireEvent.click(screen.getByRole("button", { name: /Opus/ }));
fireEvent.click(await screen.findByRole("menuitemradio", { name: /Sonnet/ }));
expect(submitToRun).toHaveBeenCalledWith("agenthub-lane-1", "/model sonnet");
expect(submitToRun).toHaveBeenCalledWith("agenthub-lane-1", "/model sonnet", []);
});
it("shows the mode the pane shows, and asks the server to step it", async () => {
@@ -430,7 +430,7 @@ describe("PaneChat — the unsent prompt", () => {
expect(screen.getByPlaceholderText("Write a message…")).toHaveValue("half typed");
expect(screen.getByText("a.ts")).toBeInTheDocument();
fireEvent.click(screen.getByTestId("pane-chat-send"));
expect(submitToRun).toHaveBeenCalledWith("agenthub-lane-1", "half typed @src/a.ts");
expect(submitToRun).toHaveBeenCalledWith("agenthub-lane-1", "half typed @src/a.ts", []);
await waitFor(() => expect(localStorage.getItem("agenthub.chat.draft.s1")).toBeNull());
});
@@ -462,7 +462,8 @@ describe("PaneChat — the unsent prompt", () => {
fireEvent.click(screen.getByTestId("pane-chat-send"));
expect(submitToRun).toHaveBeenCalledWith(
"agenthub-lane-1",
'read @"/home/u/My Docs/spec.md" @/home/u/b.ts'
'read @"/home/u/My Docs/spec.md" @/home/u/b.ts',
[]
);
});
@@ -490,7 +491,7 @@ describe("PaneChat — the unsent prompt", () => {
expect(box).toHaveValue("hello");
confirmDialog.mockResolvedValueOnce(true);
fireEvent.click(screen.getByTestId("pane-chat-send"));
await waitFor(() => expect(submitToRun).toHaveBeenCalledWith("agenthub-lane-1", "hello"));
await waitFor(() => expect(submitToRun).toHaveBeenCalledWith("agenthub-lane-1", "hello", []));
});
it("sends without asking once the session has started", async () => {
@@ -499,7 +500,7 @@ describe("PaneChat — the unsent prompt", () => {
renderChat();
fireEvent.change(screen.getByPlaceholderText("Write a message…"), { target: { value: "hi" } });
fireEvent.click(screen.getByTestId("pane-chat-send"));
await waitFor(() => expect(submitToRun).toHaveBeenCalledWith("agenthub-lane-1", "hi"));
await waitFor(() => expect(submitToRun).toHaveBeenCalledWith("agenthub-lane-1", "hi", []));
expect(confirmDialog).not.toHaveBeenCalled();
});
});
@@ -24,6 +24,7 @@ import {
ModelPicker,
PASTE_AS_FILE_CHARS,
PromptEditor,
promptForPane,
promptWithImages,
type PromptImage,
} from "../RunSetup";
@@ -586,3 +587,23 @@ describe("ModelMenu", () => {
expect(onChange).toHaveBeenCalledWith("claude-sonnet-5-5");
});
});
describe("promptForPane — images apart, every other file in the text", () => {
it("keeps @-mentions and other files in the text, and takes images out", () => {
// A Windows path inside a paste of text stayed a plain path in claude; a
// paste that is only the path becomes [Image #n].
const images: PromptImage[] = [
{ path: "C:\\Users\\u\\.claude\\agenthub\\drops\\f326fc666d3bdd92-image.png", name: "image.png" },
{ path: "/data/drops/0123456789abcdef-log.txt", name: "log.txt" },
{ path: '@"/home/u/My Docs/a.PNG"', name: "a.PNG" },
{ path: "/data/drops/0123456789abcdef.JPG", name: "b.jpg" },
];
expect(promptForPane(" look ", images)).toEqual({
text: 'look /data/drops/0123456789abcdef-log.txt @"/home/u/My Docs/a.PNG"',
files: [
"C:\\Users\\u\\.claude\\agenthub\\drops\\f326fc666d3bdd92-image.png",
"/data/drops/0123456789abcdef.JPG",
],
});
});
});