@@ -3,14 +3,24 @@ import type {
33 OrchestrationV2ProviderGoal ,
44 ThreadId ,
55} from "@t3tools/contracts" ;
6- import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell" ;
6+ import type { EnvironmentThreadShell , ThreadStructure } from "@t3tools/client-runtime/state/shell" ;
77import {
88 presentPendingBackgroundWork ,
99 presentProviderGoal ,
1010} from "@t3tools/client-runtime/state/thread-execution" ;
1111import { childThreadInputsTitle } from "@t3tools/client-runtime/state/threads" ;
12- import { GitBranchIcon , MessageCircleIcon , ShieldQuestionIcon , TargetIcon } from "lucide-react" ;
12+ import { formatDuration } from "@t3tools/shared/orchestrationTiming" ;
13+ import * as DateTime from "effect/DateTime" ;
14+ import {
15+ CircleDashedIcon ,
16+ GitBranchIcon ,
17+ MessageCircleIcon ,
18+ ShieldQuestionIcon ,
19+ TargetIcon ,
20+ TerminalIcon ,
21+ } from "lucide-react" ;
1322import { Fragment } from "react" ;
23+ import { tickTextContent } from "~/lib/tickTextContent" ;
1424import { cn } from "~/lib/utils" ;
1525import { Button , InlineButton } from "../ui/button" ;
1626import { Tooltip , TooltipPopup , TooltipTrigger } from "../ui/tooltip" ;
@@ -99,6 +109,7 @@ export function goalNowItem(input: {
99109export function backgroundWorkNowItem ( input : {
100110 readonly threadId : ThreadId ;
101111 readonly tasks : ReadonlyArray < OrchestrationV2PendingBackgroundTask > ;
112+ readonly turnItems : ThreadStructure [ "turnItems" ] ;
102113 readonly stopDisabled : boolean ;
103114 readonly stopping : boolean ;
104115 readonly onStop : ( ) => Promise < void > ;
@@ -107,51 +118,67 @@ export function backgroundWorkNowItem(input: {
107118 const { onOpenThread } = input ;
108119 const presentation = presentPendingBackgroundWork ( input . tasks ) ;
109120 if ( presentation === null ) return null ;
121+ const rows = presentation . items . map ( ( task ) => {
122+ // Same key derivePendingBackgroundWork gives turn-item tasks; subagent items carry progress.
123+ const item = input . turnItems . findLast (
124+ ( candidate ) => ( candidate . nativeItemRef ?. nativeId || candidate . id ) === task . taskId ,
125+ ) ;
126+ const subagent = item ?. type === "subagent" ? item : undefined ;
127+ return {
128+ task,
129+ activity : subagent ?. progress ?. replace ( / \s + / gu, " " ) . trim ( ) || null ,
130+ startedAtMs : item ?. startedAt ? DateTime . toEpochMillis ( item . startedAt ) : null ,
131+ updatedAtMs : subagent ? DateTime . toEpochMillis ( subagent . updatedAt ) : 0 ,
132+ childThreadId : task . childThreadId ?? subagent ?. childThreadId ?? undefined ,
133+ } ;
134+ } ) ;
135+ const [ only ] = rows . length === 1 ? rows : [ ] ;
136+ const newest = rows
137+ . filter ( ( row ) => row . activity )
138+ . toSorted ( ( left , right ) => right . updatedAtMs - left . updatedAtMs ) [ 0 ] ;
139+ const isCommand = ( row : ( typeof rows ) [ number ] ) => row . task . kind !== "subagent" ;
140+ const verb = presentation . waiting ? "Waiting on" : "Running" ;
141+ const onlyThreadId = only ?. childThreadId ;
142+ const startedAtMs = only ?. startedAtMs ?? null ;
110143 return {
111144 id : `background-work:${ input . threadId } ` ,
112- icon : (
113- < span
114- className = { cn (
115- "size-1.5 rounded-full" ,
116- presentation . waiting ? "bg-foreground" : "bg-muted-foreground" ,
117- ) }
118- aria-hidden = "true"
119- />
145+ icon : rows . every ( isCommand ) ? < TerminalIcon /> : < CircleDashedIcon /> ,
146+ title : only ? (
147+ < >
148+ { verb } < span className = { cn ( isCommand ( only ) && "font-mono" ) } > { only . task . label } </ span >
149+ </ >
150+ ) : (
151+ presentation . title
152+ ) ,
153+ description : only ? (
154+ only . activity
155+ ) : newest ? (
156+ `${ newest . task . label } : ${ newest . activity } `
157+ ) : rows . every ( isCommand ) ? (
158+ < span className = "font-mono" > { rows . map ( ( row ) => row . task . label ) . join ( ", " ) } </ span >
159+ ) : (
160+ rows . map ( ( row ) => row . task . label ) . join ( ", " )
120161 ) ,
121- title : presentation . title ,
122- description :
123- presentation . items . length === 1 && presentation . items [ 0 ] ?. childThreadId === undefined
124- ? undefined
125- : presentation . items . map ( ( item , index ) => {
126- const childThreadId = item . childThreadId ;
127- const separator = index < presentation . items . length - 1 ? "," : null ;
128- return (
129- < Fragment key = { item . taskId } >
130- { index > 0 ? " " : null }
131- { childThreadId === undefined ? (
132- < >
133- { item . label }
134- { separator }
135- </ >
136- ) : (
137- < span className = "whitespace-nowrap" >
138- < InlineButton
139- tone = "muted"
140- aria-label = { `${ presentation . items . length === 1 ? "Show" : "Open" } subagent ${ item . label } ` }
141- onClick = { ( ) => onOpenThread ( childThreadId ) }
142- >
143- { presentation . items . length === 1 ? "Show" : item . label }
144- </ InlineButton >
145- { separator }
146- </ span >
147- ) }
148- </ Fragment >
149- ) ;
150- } ) ,
151162 actions : (
152- < InlineButton disabled = { input . stopDisabled } onClick = { ( ) => void input . onStop ( ) } >
153- { input . stopping ? "Stopping..." : "Stop" }
154- </ InlineButton >
163+ < >
164+ { startedAtMs === null ? null : (
165+ < span
166+ ref = { tickTextContent (
167+ ( nowMs ) => formatDuration ( Math . max ( 1_000 , nowMs - startedAtMs ) ) ,
168+ true ,
169+ ) }
170+ className = "tabular-nums"
171+ />
172+ ) }
173+ { onlyThreadId === undefined ? null : (
174+ < InlineButton tone = "muted" onClick = { ( ) => onOpenThread ( onlyThreadId ) } >
175+ Open
176+ </ InlineButton >
177+ ) }
178+ < InlineButton disabled = { input . stopDisabled } onClick = { ( ) => void input . onStop ( ) } >
179+ { input . stopping ? "Stopping..." : "Stop" }
180+ </ InlineButton >
181+ </ >
155182 ) ,
156183 } ;
157184}
0 commit comments