@@ -542,6 +542,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
542542 row . message . role === "assistant" &&
543543 ( ( ) => {
544544 const messageText = row . message . text || ( row . message . streaming ? "" : "(empty response)" ) ;
545+ const copyText = row . message . text . trim ( ) . length > 0 ? row . message . text : null ;
545546 return (
546547 < >
547548 { row . showCompletionDivider && (
@@ -554,106 +555,115 @@ export const MessagesTimeline = memo(function MessagesTimeline({
554555 </ div >
555556 ) }
556557 < div className = "min-w-0 px-1 py-0.5" >
557- < ChatMarkdown
558- text = { messageText }
559- cwd = { markdownCwd }
560- isStreaming = { Boolean ( row . message . streaming ) }
561- />
562- { ( ( ) => {
563- const turnSummary = turnDiffSummaryByAssistantMessageId . get ( row . message . id ) ;
564- if ( ! turnSummary ) return null ;
565- const checkpointFiles = turnSummary . files ;
566- const summaryStat = summarizeTurnDiffStats ( checkpointFiles ) ;
567- const changedFileCountLabel = String ( checkpointFiles . length ) ;
568- const allDirectoriesExpanded =
569- allDirectoriesExpandedByTurnId [ turnSummary . turnId ] ?? true ;
570- const isFileSectionCollapsed =
571- collapsedFileSectionsByTurnId [ turnSummary . turnId ] ?? false ;
572- return (
573- < div className = "mt-2 rounded-lg border border-border/80 bg-card/45 p-2.5" >
574- < div className = "flex items-center justify-between gap-2" >
575- { checkpointFiles . length > 0 ? (
576- < button
577- type = "button"
578- className = "group flex items-center gap-1.5 text-[10px] uppercase tracking-[0.12em] text-muted-foreground/65 transition-colors duration-150 hover:text-muted-foreground/90"
579- onClick = { ( ) => onToggleFileSection ( turnSummary . turnId ) }
580- >
581- < ChevronRightIcon
582- aria-hidden = "true"
583- className = { cn (
584- "size-3 shrink-0 transition-transform duration-150" ,
585- ! isFileSectionCollapsed && "rotate-90" ,
586- ) }
587- />
588- < span > Changed files ({ changedFileCountLabel } )</ span >
589- { hasNonZeroStat ( summaryStat ) && (
590- < >
591- < span className = "mx-1" > •</ span >
592- < DiffStatLabel
593- additions = { summaryStat . additions }
594- deletions = { summaryStat . deletions }
558+ < div className = "flex items-start gap-2" >
559+ < div className = "min-w-0 flex-1" >
560+ < ChatMarkdown
561+ text = { messageText }
562+ cwd = { markdownCwd }
563+ isStreaming = { Boolean ( row . message . streaming ) }
564+ />
565+ { ( ( ) => {
566+ const turnSummary = turnDiffSummaryByAssistantMessageId . get ( row . message . id ) ;
567+ if ( ! turnSummary ) return null ;
568+ const checkpointFiles = turnSummary . files ;
569+ const summaryStat = summarizeTurnDiffStats ( checkpointFiles ) ;
570+ const changedFileCountLabel = String ( checkpointFiles . length ) ;
571+ const allDirectoriesExpanded =
572+ allDirectoriesExpandedByTurnId [ turnSummary . turnId ] ?? true ;
573+ const isFileSectionCollapsed =
574+ collapsedFileSectionsByTurnId [ turnSummary . turnId ] ?? false ;
575+ return (
576+ < div className = "mt-2 rounded-lg border border-border/80 bg-card/45 p-2.5" >
577+ < div className = "flex items-center justify-between gap-2" >
578+ { checkpointFiles . length > 0 ? (
579+ < button
580+ type = "button"
581+ className = "group flex items-center gap-1.5 text-[10px] uppercase tracking-[0.12em] text-muted-foreground/65 transition-colors duration-150 hover:text-muted-foreground/90"
582+ onClick = { ( ) => onToggleFileSection ( turnSummary . turnId ) }
583+ >
584+ < ChevronRightIcon
585+ aria-hidden = "true"
586+ className = { cn (
587+ "size-3 shrink-0 transition-transform duration-150" ,
588+ ! isFileSectionCollapsed && "rotate-90" ,
589+ ) }
595590 />
596- </ >
591+ < span > Changed files ({ changedFileCountLabel } )</ span >
592+ { hasNonZeroStat ( summaryStat ) && (
593+ < >
594+ < span className = "mx-1" > •</ span >
595+ < DiffStatLabel
596+ additions = { summaryStat . additions }
597+ deletions = { summaryStat . deletions }
598+ />
599+ </ >
600+ ) }
601+ </ button >
602+ ) : (
603+ < div className = "flex items-center gap-1.5 text-[10px] uppercase tracking-[0.12em] text-muted-foreground/65" >
604+ < EyeIcon className = "size-3 shrink-0" />
605+ < span > Diff available</ span >
606+ </ div >
597607 ) }
598- </ button >
599- ) : (
600- < div className = "flex items-center gap-1.5 text-[10px] uppercase tracking-[0.12em] text-muted-foreground/65" >
601- < EyeIcon className = "size-3 shrink-0" />
602- < span > Diff available</ span >
608+ < div className = "flex items-center gap-1.5" >
609+ < Button
610+ type = "button"
611+ size = "xs"
612+ variant = "outline"
613+ onClick = { ( ) => onOpenTurnDiff ( turnSummary . turnId ) }
614+ >
615+ Open diff
616+ </ Button >
617+ { checkpointFiles . length > 0 && ! isFileSectionCollapsed && (
618+ < Button
619+ type = "button"
620+ size = "xs"
621+ variant = "outline"
622+ onClick = { ( ) => onToggleAllDirectories ( turnSummary . turnId ) }
623+ >
624+ { allDirectoriesExpanded ? "Collapse all" : "Expand all" }
625+ </ Button >
626+ ) }
627+ </ div >
603628 </ div >
604- ) }
605- < div className = "flex items-center gap-1.5" >
606- < Button
607- type = "button"
608- size = "xs"
609- variant = "outline"
610- onClick = { ( ) => onOpenTurnDiff ( turnSummary . turnId ) }
611- >
612- Open diff
613- </ Button >
614629 { checkpointFiles . length > 0 && ! isFileSectionCollapsed && (
615- < Button
616- type = "button"
617- size = "xs"
618- variant = "outline"
619- onClick = { ( ) => onToggleAllDirectories ( turnSummary . turnId ) }
620- >
621- { allDirectoriesExpanded ? "Collapse all" : "Expand all" }
622- </ Button >
630+ < div className = "mt-1.5" >
631+ < ChangedFilesTree
632+ key = { `changed-files-tree:${ turnSummary . turnId } ` }
633+ turnId = { turnSummary . turnId }
634+ files = { checkpointFiles }
635+ allDirectoriesExpanded = { allDirectoriesExpanded }
636+ resolvedTheme = { resolvedTheme }
637+ cwd = { markdownCwd }
638+ onOpenTurnDiff = { onOpenTurnDiff }
639+ />
640+ </ div >
641+ ) }
642+ { checkpointFiles . length === 0 && (
643+ < p className = "mt-1.5 text-xs text-muted-foreground/75" >
644+ Open the diff to inspect changes when the file summary is unavailable.
645+ </ p >
623646 ) }
624647 </ div >
625- </ div >
626- { checkpointFiles . length > 0 && ! isFileSectionCollapsed && (
627- < div className = "mt-1.5" >
628- < ChangedFilesTree
629- key = { `changed-files-tree:${ turnSummary . turnId } ` }
630- turnId = { turnSummary . turnId }
631- files = { checkpointFiles }
632- allDirectoriesExpanded = { allDirectoriesExpanded }
633- resolvedTheme = { resolvedTheme }
634- cwd = { markdownCwd }
635- onOpenTurnDiff = { onOpenTurnDiff }
636- />
637- </ div >
638- ) }
639- { checkpointFiles . length === 0 && (
640- < p className = "mt-1.5 text-xs text-muted-foreground/75" >
641- Open the diff to inspect changes when the file summary is unavailable.
642- </ p >
648+ ) ;
649+ } ) ( ) }
650+ < p className = "mt-1.5 text-[10px] text-muted-foreground/30" >
651+ { formatMessageMeta (
652+ row . message . createdAt ,
653+ row . message . streaming
654+ ? formatElapsed ( row . durationStart , nowIso )
655+ : formatElapsed ( row . durationStart , row . message . completedAt ) ,
656+ timestampFormat ,
657+ resolvedLocale ,
643658 ) }
659+ </ p >
660+ </ div >
661+ { copyText && (
662+ < div className = "flex shrink-0 items-start pt-0.5" >
663+ < MessageCopyButton text = { copyText } label = "response" />
644664 </ div >
645- ) ;
646- } ) ( ) }
647- < p className = "mt-1.5 text-[10px] text-muted-foreground/30" >
648- { formatMessageMeta (
649- row . message . createdAt ,
650- row . message . streaming
651- ? formatElapsed ( row . durationStart , nowIso )
652- : formatElapsed ( row . durationStart , row . message . completedAt ) ,
653- timestampFormat ,
654- resolvedLocale ,
655665 ) }
656- </ p >
666+ </ div >
657667 </ div >
658668 </ >
659669 ) ;
0 commit comments