From cf70bdcceb899c1663422f10515bbc40db529b25 Mon Sep 17 00:00:00 2001 From: Jason O'Neal Date: Mon, 18 May 2026 23:18:51 -0400 Subject: [PATCH] fix(webchat): scope attachment button input --- CHANGELOG.md | 1 + ui/src/ui/views/chat.test.ts | 20 ++++++++++++++++++++ ui/src/ui/views/chat.ts | 16 +++++++++++++--- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bca5ddb6ac..548f56d6b6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ Docs: https://docs.openclaw.ai ### Fixes - WebChat: summarize internal message-tool source replies so tool cards no longer duplicate the visible reply body. (#84773) Thanks @jason-allen-oneal. +- WebChat: scope the visible attachment button to its own composer file input so clicking Upload reliably opens the file picker. (#83952, fixes #47983) Thanks @jason-allen-oneal. - Gateway: preserve deferred lifecycle-error cleanup across later non-terminal events so provider timeouts can persist failed session state instead of leaving sessions stuck running. (#85256, fixes #63819) Thanks @samzong. - Agents/subagents: report tool-only child progress during timeout summaries instead of showing no visible output. - Telegram/ACP: preserve explicit `:topic:` conversation suffixes when inbound ACP targets do not carry a separate thread id. diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index 0e6d22cac12..f9466aaf5e7 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -1070,6 +1070,26 @@ describe("chat attachment picker", () => { expect(getChatAttachmentDataUrl(attachments[0])).toBe(`data:image/png;base64,${base64}`); }); + it("opens the scoped file input from the visible attach button", () => { + const container = renderChatView(); + const input = requireElement( + container, + ".agent-chat__file-input", + "attachment file input", + ) as HTMLInputElement; + const attachButton = requireElement( + container, + `[aria-label="${t("chat.composer.attachFile")}"]`, + "attach button", + ) as HTMLButtonElement; + const clickInput = vi.spyOn(input, "click").mockImplementation(() => undefined); + + attachButton.click(); + + expect(attachButton.type).toBe("button"); + expect(clickInput).toHaveBeenCalledTimes(1); + }); + it("accepts and previews non-video file attachments", async () => { const onAttachmentsChange = vi.fn(); const container = renderChatView({ onAttachmentsChange }); diff --git a/ui/src/ui/views/chat.ts b/ui/src/ui/views/chat.ts index c6b76124f28..36f1daf1ed3 100644 --- a/ui/src/ui/views/chat.ts +++ b/ui/src/ui/views/chat.ts @@ -427,6 +427,17 @@ function focusComposerFromChrome(event: MouseEvent, connected: boolean) { ?.focus({ preventScroll: true }); } +function clickComposerFileInput(event: MouseEvent) { + const target = event.currentTarget; + if (!(target instanceof HTMLElement)) { + return; + } + target + .closest(".agent-chat__input") + ?.querySelector(".agent-chat__file-input") + ?.click(); +} + function restoreHistoryCaret(target: HTMLTextAreaElement, direction: "up" | "down") { requestAnimationFrame(() => { if (document.activeElement !== target) { @@ -1562,10 +1573,9 @@ export function renderChat(props: ChatProps) {