fix(frontend): open file chooser from attachment button (#8018)

This commit is contained in:
Wenchao An 2026-11-09 09:57:11 +08:00 committed by GitHub
parent 0d04322d41
commit 97746ad725
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 55 additions and 7 deletions

View File

@ -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.

View File

@ -3281,7 +3281,6 @@ export function InputBox({
</PromptInputButton>
</Tooltip>
<AddAttachmentsButton
onOpen={openMentions}
className="px-2!"
disabled={composerLocked}
uploadLimits={uploadLimits}
@ -3853,9 +3852,7 @@ function AddAttachmentsButton({
className,
disabled,
uploadLimits,
onOpen,
}: {
onOpen?: () => 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()}
>
<PaperclipIcon className="size-3" />
</PromptInputButton>

View File

@ -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,
}) => {

View File

@ -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<HTMLInputElement>('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<HTMLInputElement>('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();