diff --git a/packages/ui/src/components/message-item.tsx b/packages/ui/src/components/message-item.tsx index 2f8eb26e..e69ba652 100644 --- a/packages/ui/src/components/message-item.tsx +++ b/packages/ui/src/components/message-item.tsx @@ -45,6 +45,15 @@ export default function MessageItem(props: MessageItemProps) { const messageParts = () => props.parts + // User messages can temporarily include synthetic helper parts (e.g. tool traces / file reads). + // We only want to display the primary prompt text for the user message; other synthetic text + // parts should be hidden. + const primaryUserTextPartId = () => { + if (!isUser()) return null + const firstText = messageParts().find((part) => part?.type === "text") as { id?: string } | undefined + return typeof firstText?.id === "string" ? firstText.id : null + } + const fileAttachments = () => messageParts().filter((part): part is FilePart => part?.type === "file" && typeof (part as FilePart).url === "string") @@ -372,6 +381,7 @@ export default function MessageItem(props: MessageItemProps) { messageType={props.record.role} instanceId={props.instanceId} sessionId={props.sessionId} + primaryUserTextPartId={primaryUserTextPartId()} onRendered={props.onContentRendered} /> )} diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 3f8405d4..4f2ff2aa 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -13,9 +13,12 @@ interface MessagePartProps { messageType?: "user" | "assistant" instanceId: string sessionId: string + // For user messages, keep the primary prompt text visible even when synthetic (optimistic). + // Other synthetic text parts (tool traces, read outputs, etc.) should be hidden. + primaryUserTextPartId?: string | null onRendered?: () => void } - export default function MessagePart(props: MessagePartProps) { + export default function MessagePart(props: MessagePartProps) { const { isDark } = useTheme() const { preferences } = useConfig() @@ -28,8 +31,19 @@ interface MessagePartProps { const shouldHideTextPart = () => { const part = props.part if (!part || part.type !== "text") return false - // Keep optimistic user prompts visible; hide synthetic assistant text. - return Boolean((part as any).synthetic) && props.messageType !== "user" + + const isSynthetic = Boolean((part as any).synthetic) + if (!isSynthetic) return false + + // Keep optimistic user prompts visible; hide other synthetic user helper parts. + if (props.messageType === "user") { + const primaryId = props.primaryUserTextPartId + if (!primaryId) return false + return part.id !== primaryId + } + + // Hide synthetic assistant text. + return true } diff --git a/packages/ui/src/lib/prompt-placeholders.ts b/packages/ui/src/lib/prompt-placeholders.ts index 25fe12f6..79d26804 100644 --- a/packages/ui/src/lib/prompt-placeholders.ts +++ b/packages/ui/src/lib/prompt-placeholders.ts @@ -1,4 +1,4 @@ -import type { Attachment } from "../types/attachment" +import type { Attachment, FileSource } from "../types/attachment" export function resolvePastedPlaceholders(prompt: string, attachments: Attachment[] = []): string { if (!prompt) { @@ -12,7 +12,7 @@ export function resolvePastedPlaceholders(prompt: string, attachments: Attachmen // IMPORTANT: avoid rewriting plain `@mentions` or email addresses. const fileAttachmentPaths = new Set( attachments - .filter((a) => a.source.type === "file") + .filter((a): a is Attachment & { source: FileSource } => a.source.type === "file") .map((a) => a.source.path), )