@@ -62,6 +62,7 @@ import {
6262} from "./userMessageTerminalContexts" ;
6363import { InlineDiffPreview } from "./InlineDiffPreview" ;
6464import { FileChangeCard } from "./FileChangeCard" ;
65+ import { ExplorationCard } from "./ExplorationCard" ;
6566
6667const MAX_VISIBLE_WORK_LOG_ENTRIES = 6 ;
6768
@@ -73,6 +74,7 @@ const DIFF_PREVIEW_MAX_HEIGHT = 260;
7374const DIFF_HUNK_SPACING = 8 ;
7475const AGENT_GROUP_HEADER_HEIGHT = 32 ;
7576const FILE_CHANGE_CARD_COLLAPSED_HEIGHT = 64 ;
77+ const EXPLORATION_CARD_COLLAPSED_HEIGHT = 36 ;
7678
7779type TimelineEntry = ReturnType < typeof deriveTimelineEntries > [ number ] ;
7880type TimelineMessage = Extract < TimelineEntry , { kind : "message" } > [ "message" ] ;
@@ -105,6 +107,13 @@ type TimelineRow =
105107 createdAt : string ;
106108 entry : TimelineWorkEntry ;
107109 }
110+ | {
111+ kind : "exploration" ;
112+ id : string ;
113+ createdAt : string ;
114+ entries : TimelineWorkEntry [ ] ;
115+ isLive : boolean ;
116+ }
108117 | { kind : "working" ; id : string ; createdAt : string | null } ;
109118
110119interface TimelineRowContentProps {
@@ -213,6 +222,8 @@ const TimelineRowContent = memo(function TimelineRowContent({
213222
214223 { row . kind === "file-change" && < FileChangeCard diffPreviews = { row . entry . diffPreviews ?? [ ] } /> }
215224
225+ { row . kind === "exploration" && < ExplorationCard entries = { row . entries } isLive = { row . isLive } /> }
226+
216227 { row . kind === "message" &&
217228 row . message . role === "user" &&
218229 ( ( ) => {
@@ -564,6 +575,9 @@ export const MessagesTimeline = memo(function MessagesTimeline({
564575 let pendingWork : TimelineWorkEntry [ ] = [ ] ;
565576 let pendingWorkFirstId : string | null = null ;
566577 let pendingWorkFirstCreatedAt : string | null = null ;
578+ let pendingExploration : TimelineWorkEntry [ ] = [ ] ;
579+ let pendingExplorationFirstId : string | null = null ;
580+ let pendingExplorationFirstCreatedAt : string | null = null ;
567581 let cursor = index ;
568582
569583 const flushPendingWork = ( ) => {
@@ -580,6 +594,21 @@ export const MessagesTimeline = memo(function MessagesTimeline({
580594 }
581595 } ;
582596
597+ const flushPendingExploration = ( ) => {
598+ if ( pendingExploration . length > 0 ) {
599+ nextRows . push ( {
600+ kind : "exploration" ,
601+ id : pendingExplorationFirstId ! ,
602+ createdAt : pendingExplorationFirstCreatedAt ! ,
603+ entries : pendingExploration ,
604+ isLive : false ,
605+ } ) ;
606+ pendingExploration = [ ] ;
607+ pendingExplorationFirstId = null ;
608+ pendingExplorationFirstCreatedAt = null ;
609+ }
610+ } ;
611+
583612 while ( cursor < timelineEntries . length ) {
584613 const current = timelineEntries [ cursor ] ;
585614 if ( ! current || current . kind !== "work" ) break ;
@@ -590,13 +619,22 @@ export const MessagesTimeline = memo(function MessagesTimeline({
590619
591620 if ( isFileChange ) {
592621 flushPendingWork ( ) ;
622+ flushPendingExploration ( ) ;
593623 nextRows . push ( {
594624 kind : "file-change" ,
595625 id : current . id ,
596626 createdAt : current . createdAt ,
597627 entry : current . entry ,
598628 } ) ;
629+ } else if ( isExplorationEntry ( current . entry ) ) {
630+ flushPendingWork ( ) ;
631+ if ( pendingExploration . length === 0 ) {
632+ pendingExplorationFirstId = current . id ;
633+ pendingExplorationFirstCreatedAt = current . createdAt ;
634+ }
635+ pendingExploration . push ( current . entry ) ;
599636 } else {
637+ flushPendingExploration ( ) ;
600638 if ( pendingWork . length === 0 ) {
601639 pendingWorkFirstId = current . id ;
602640 pendingWorkFirstCreatedAt = current . createdAt ;
@@ -606,6 +644,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
606644 cursor += 1 ;
607645 }
608646 flushPendingWork ( ) ;
647+ flushPendingExploration ( ) ;
609648 index = cursor - 1 ;
610649 continue ;
611650 }
@@ -634,6 +673,16 @@ export const MessagesTimeline = memo(function MessagesTimeline({
634673 }
635674
636675 if ( isWorking ) {
676+ for ( let i = nextRows . length - 1 ; i >= 0 ; i -- ) {
677+ const r = nextRows [ i ] ;
678+ if ( ! r ) break ;
679+ if ( r . kind === "exploration" ) {
680+ nextRows [ i ] = { ...r , isLive : true } ;
681+ break ;
682+ }
683+ if ( r . kind === "message" || r . kind === "proposed-plan" ) break ;
684+ }
685+
637686 nextRows . push ( {
638687 kind : "working" ,
639688 id : "working-indicator-row" ,
@@ -719,6 +768,7 @@ function estimateRowHeight(
719768 if ( row . kind === "proposed-plan" ) return estimateTimelineProposedPlanHeight ( row . proposedPlan ) ;
720769 if ( row . kind === "working" ) return 40 ;
721770 if ( row . kind === "file-change" ) return FILE_CHANGE_CARD_COLLAPSED_HEIGHT ;
771+ if ( row . kind === "exploration" ) return EXPLORATION_CARD_COLLAPSED_HEIGHT ;
722772 return estimateTimelineMessageHeight ( row . message , { timelineWidthPx } ) ;
723773}
724774
@@ -954,6 +1004,28 @@ function workToneIcon(tone: TimelineWorkEntry["tone"]): {
9541004 } ;
9551005}
9561006
1007+ const EXPLORATION_LABEL_RE =
1008+ / ^ ( R e a d | S e a r c h ( e d ) ? | G l o b ( b e d ) ? | G r e p ( p e d ) ? | L i s t ( e d ) ? | F i n d | F o u n d | V i e w ( e d ) ? | I n s p e c t ( e d ) ? ) \b / i;
1009+
1010+ function isExplorationEntry ( entry : TimelineWorkEntry ) : boolean {
1011+ if ( entry . requestKind === "file-change" || entry . requestKind === "command" ) return false ;
1012+ if (
1013+ entry . itemType === "file_change" ||
1014+ entry . itemType === "command_execution" ||
1015+ entry . itemType === "web_search"
1016+ )
1017+ return false ;
1018+ if ( entry . command ) return false ;
1019+ if ( ( entry . diffPreviews ?. length ?? 0 ) > 0 ) return false ;
1020+ if ( entry . agentGroup ) return false ;
1021+
1022+ if ( entry . requestKind === "file-read" ) return true ;
1023+ if ( entry . itemType === "image_view" ) return true ;
1024+
1025+ const heading = ( entry . toolTitle ?? entry . label ) . trim ( ) ;
1026+ return EXPLORATION_LABEL_RE . test ( heading ) ;
1027+ }
1028+
9571029function workToneClass ( tone : "thinking" | "tool" | "info" | "error" ) : string {
9581030 if ( tone === "error" ) return "text-rose-300/50 dark:text-rose-300/50" ;
9591031 if ( tone === "tool" ) return "text-muted-foreground/70" ;
0 commit comments