From f8a5a1c12eff981b4161fb05d0f6347a388b48aa Mon Sep 17 00:00:00 2001 From: Laodouuu <1257029425@qq.com> Date: Tue, 4 Aug 2026 23:31:34 +0800 Subject: [PATCH] fix(web): align uploaded attachments with user messages (#185) Co-authored-by: Yaowei Zheng Co-authored-by: Claude Fable 5 --- .../features/chat/attached-files-banner.tsx | 6 ++-- .../web/src/features/chat/message-item.tsx | 33 +++++++++++++++---- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/web/src/features/chat/attached-files-banner.tsx b/packages/web/src/features/chat/attached-files-banner.tsx index 909df1b..88a8c53 100644 --- a/packages/web/src/features/chat/attached-files-banner.tsx +++ b/packages/web/src/features/chat/attached-files-banner.tsx @@ -3,8 +3,8 @@ * server appends aren't shown verbatim, they collapse into a single line reading * "Attached files: a.pdf, b.csv" (paperclip icon + static text, no navigation — the files live * in the session scratchpad and the model opens them by path); the body text around them is - * rendered as usual by the caller. Same shape as SkillsBanner, so the two notices a message - * can carry read as one family. + * rendered as usual by the caller. It keeps the same visual language as message-level notices; + * the caller owns its user-side alignment and timestamp footer. */ import { S } from "../../lib/strings"; import { attachmentFileName } from "../../lib/attachments"; @@ -22,7 +22,7 @@ export function AttachedFilesBanner({ files }: { files: string[] }) { // into a paragraph-tall block above the message. The full list stays reachable as a title.

{label} diff --git a/packages/web/src/features/chat/message-item.tsx b/packages/web/src/features/chat/message-item.tsx index 2f3a196..209db82 100644 --- a/packages/web/src/features/chat/message-item.tsx +++ b/packages/web/src/features/chat/message-item.tsx @@ -217,10 +217,6 @@ export function MessageItem({ item, ctx }: { item: ChatItem; ctx: StreamRenderCo <> {scheduled && } {skills && } - {/* Files uploaded with this message: named above the bubble, like the other - message-level notices — the bytes live in the session scratchpad, the model - opens them by path (goal mode never gets here: it takes text and images only). */} - {files.length > 0 && } {text && (

@@ -236,6 +232,19 @@ export function MessageItem({ item, ctx }: { item: ChatItem; ctx: StreamRenderCo />
)} + {/* Files uploaded with this message: shown below the text in the same user-side + container and with the same timestamp footer as uploaded images. The bytes live in + the session scratchpad, where the model opens them by path (goal mode never gets + here: it takes text and images only). */} + {files.length > 0 && ( +
+ + +
+ )} {images.map((src, i) => (
@@ -262,8 +271,15 @@ export function MessageItem({ item, ctx }: { item: ChatItem; ctx: StreamRenderCo // Images sent with the message live inside the same chip: `item.images` on a vision // model (delivered as image messages right behind the text), or restored from the // [attached image: …] path lines without one — the same two shapes a user_text bubble - // handles, so both render identically here. - const { text: steerText, images: steerImages } = splitAttachments(item.text); + // handles, so both render identically here. Files ride steering only as + // [attached file: …] rows (there is no item.files channel) and collapse into the same + // banner a full prompt uses, kept inside the chip — a files-only steering would + // otherwise render as an empty chip with the filenames lost. + const { + text: steerText, + images: steerImages, + files: steerFiles, + } = splitAttachments(item.text); const shown = [...steerImages, ...(item.images ?? [])]; return (
@@ -292,6 +308,11 @@ export function MessageItem({ item, ctx }: { item: ChatItem; ctx: StreamRenderCo ))}
)} + {steerFiles.length > 0 && ( +
+ +
+ )}