diff --git a/README.md b/README.md index 81f0383682..3d2a5e9d5d 100644 --- a/README.md +++ b/README.md @@ -2649,6 +2649,8 @@ When the agent asks for clarification, the Web UI shows the structured response Unsent Web UI composer drafts survive page reloads and switching between conversations within the same browser tab. Drafts are isolated by user, agent, and conversation, include the selected skill and explicit conversation references, and are cleared once a send is accepted. Project documents already attached to the thread also survive reloads; browser-local uploads and quoted message excerpts are not persisted. +The composer's paperclip button opens the local file chooser directly; the `@` button opens the reference picker for skills, project files, conversations, and plugins. + If an attachment upload reports `success: false`, the Web UI stops the send even when the HTTP response is 200. The upload error is shown and the composer keeps its draft and attachments for correction or retry. Files already accepted by a partially successful upload remain on the server; this does not roll them back. Document conversion reads the live PDF-converter setting and parses documents in a background worker, keeping configuration-file checks off the Gateway event loop. diff --git a/frontend/src/components/workspace/input-box.tsx b/frontend/src/components/workspace/input-box.tsx index bc04b9f6ff..f59aef0a77 100644 --- a/frontend/src/components/workspace/input-box.tsx +++ b/frontend/src/components/workspace/input-box.tsx @@ -3281,7 +3281,6 @@ export function InputBox({ void; className?: string; disabled?: boolean; uploadLimits?: UploadLimits; @@ -3876,7 +3873,7 @@ function AddAttachmentsButton({ className={cn("px-2!", className)} data-testid="add-attachments-button" disabled={disabled} - onClick={onOpen ?? (() => attachments.openFileDialog())} + onClick={() => attachments.openFileDialog()} > diff --git a/frontend/tests/e2e/chat.spec.ts b/frontend/tests/e2e/chat.spec.ts index c6e83eae4f..21213b5761 100644 --- a/frontend/tests/e2e/chat.spec.ts +++ b/frontend/tests/e2e/chat.spec.ts @@ -1210,6 +1210,37 @@ test.describe("Chat workspace", () => { }); }); + test("opens local files directly from the attachment button and keeps references under @", async ({ + page, + }) => { + await page.goto("/workspace/chats/new"); + const fileChooserPromise = page.waitForEvent("filechooser"); + await page.getByTestId("add-attachments-button").click(); + const fileChooser = await fileChooserPromise; + await expect(page.getByTestId("mention-picker")).toBeHidden(); + await fileChooser.setFiles({ + name: "local-report.txt", + mimeType: "text/plain", + buffer: Buffer.from("Local attachment"), + }); + await expect( + page.locator("form").getByText("local-report.txt"), + ).toBeVisible(); + + await page.getByTestId("mention-button").click(); + const picker = page.getByTestId("mention-picker"); + await expect(picker).toBeVisible(); + await expect( + picker.getByRole("option", { name: "Upload a file" }), + ).toBeAttached(); + await expect( + picker.getByRole("group", { name: "Skills", exact: true }), + ).toBeVisible(); + await expect( + page.locator("form").getByText("local-report.txt"), + ).toBeVisible(); + }); + test("shows gateway upload limits on the attachment entry point", async ({ page, }) => { diff --git a/frontend/tests/unit/components/workspace/input-box-mentions.dom.test.tsx b/frontend/tests/unit/components/workspace/input-box-mentions.dom.test.tsx index a30b408348..4faf6191c2 100644 --- a/frontend/tests/unit/components/workspace/input-box-mentions.dom.test.tsx +++ b/frontend/tests/unit/components/workspace/input-box-mentions.dom.test.tsx @@ -333,7 +333,7 @@ describe("mention selection focus", () => { '[contenteditable="true"]', )!; act(() => focusReferenceAt(editor, 2)); - const picker = screen.getByTestId("add-attachments-button"); + const picker = screen.getByTestId("mention-button"); act(() => picker.focus()); holdAnimationFrames(); fireEvent.click(picker); @@ -630,11 +630,29 @@ describe("unified composer mentions", () => { enterMention(container, "@"); expect(screen.queryByRole("option", { name: "Writer brief" })).toBeNull(); }); - it("the attachment button opens the same picker", () => { - renderComposer(); + it("the attachment button opens the file input directly", () => { + const { container } = renderComposer(); + const fileInput = + container.querySelector('input[type="file"]')!; + const click = rs.spyOn(fileInput, "click"); fireEvent.click(screen.getByTestId("add-attachments-button")); + expect(click).toHaveBeenCalledTimes(1); + expect(screen.queryByTestId("mention-picker")).toBeNull(); + }); + it("the mention button keeps the reference picker and its upload action", () => { + const { container } = renderComposer(); + const fileInput = + container.querySelector('input[type="file"]')!; + const click = rs.spyOn(fileInput, "click"); + fireEvent.click(screen.getByTestId("mention-button")); + expect(click).not.toHaveBeenCalled(); expect(screen.getByTestId("mention-picker")).toBeTruthy(); + expect( + screen.getByRole("option", { name: "research Research a topic" }), + ).toBeTruthy(); expect(screen.getByRole("option", { name: "Upload a file" })).toBeTruthy(); + fireEvent.click(screen.getByRole("option", { name: "Upload a file" })); + expect(click).toHaveBeenCalledTimes(1); }); it("does not select upload automatically for an unknown query", () => { const { container } = renderComposer();