11import { For , Show , createSignal } from "solid-js"
2- import { Copy , Split , Trash2 , Undo } from "lucide-solid"
2+ import { Copy , ExternalLink , Split , Trash2 , Undo } from "lucide-solid"
33import type { MessageInfo , ClientPart } from "../types/message"
44import { partHasRenderableText } from "../types/message"
55import type { MessageRecord } from "../stores/message-v2/types"
@@ -8,6 +8,7 @@ import { copyToClipboard } from "../lib/clipboard"
88import { useI18n } from "../lib/i18n"
99import { showAlertDialog } from "../stores/alerts"
1010import { deleteMessagePart } from "../stores/session-actions"
11+ import { isTauriHost } from "../lib/runtime-env"
1112
1213interface MessageItemProps {
1314 record : MessageRecord
@@ -45,6 +46,15 @@ export default function MessageItem(props: MessageItemProps) {
4546
4647 const messageParts = ( ) => props . parts
4748
49+ // User messages can temporarily include synthetic helper parts (e.g. tool traces / file reads).
50+ // We only want to display the primary prompt text for the user message; other synthetic text
51+ // parts should be hidden.
52+ const primaryUserTextPartId = ( ) => {
53+ if ( ! isUser ( ) ) return null
54+ const firstText = messageParts ( ) . find ( ( part ) => part ?. type === "text" ) as { id ?: string } | undefined
55+ return typeof firstText ?. id === "string" ? firstText . id : null
56+ }
57+
4858 const fileAttachments = ( ) =>
4959 messageParts ( ) . filter ( ( part ) : part is FilePart => part ?. type === "file" && typeof ( part as FilePart ) . url === "string" )
5060
@@ -96,7 +106,8 @@ export default function MessageItem(props: MessageItemProps) {
96106 }
97107
98108 if ( url . startsWith ( "file://" ) ) {
99- window . open ( url , "_blank" , "noopener" )
109+ // Local filesystem URLs are not reliably downloadable from the message stream.
110+ // We hide the download action for these chips.
100111 return
101112 }
102113
@@ -373,6 +384,7 @@ export default function MessageItem(props: MessageItemProps) {
373384 messageType = { props . record . role }
374385 instanceId = { props . instanceId }
375386 sessionId = { props . sessionId }
387+ primaryUserTextPartId = { primaryUserTextPartId ( ) }
376388 onRendered = { props . onContentRendered }
377389 />
378390 ) }
@@ -399,17 +411,20 @@ export default function MessageItem(props: MessageItemProps) {
399411 < img src = { attachment . url } alt = { name } class = "h-5 w-5 rounded object-cover" />
400412 </ Show >
401413 < span class = "truncate max-w-[180px]" > { name } </ span >
402- < button
403- type = "button"
404- onClick = { ( ) => void handleAttachmentDownload ( attachment ) }
405- class = "attachment-download"
406- aria-label = { t ( "messageItem.attachment.downloadAriaLabel" , { name } ) }
407- >
408- < svg class = "h-3 w-3" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" >
409- < path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2" />
410- < path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M8 12l4 4 4-4m-4-8v12" />
411- </ svg >
412- </ button >
414+ < Show when = { ! attachment . url ?. startsWith ( "file://" ) } >
415+ < button
416+ type = "button"
417+ onClick = { ( ) => void handleAttachmentDownload ( attachment ) }
418+ class = "attachment-download"
419+ aria-label = { t ( "messageItem.attachment.downloadAriaLabel" , { name } ) }
420+ title = { t ( "messageItem.attachment.downloadAriaLabel" , { name } ) }
421+ >
422+ < svg class = "h-3 w-3" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" >
423+ < path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2" />
424+ < path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M8 12l4 4 4-4m-4-8v12" />
425+ </ svg >
426+ </ button >
427+ </ Show >
413428
414429 < button
415430 type = "button"
0 commit comments