11'use client'
22
33import { useEffect , useLayoutEffect , useMemo , useRef , useState } from 'react'
4- import { ChevronDown , cn , Expandable , ExpandableContent , OverflowText } from '@sim/emcn'
4+ import { ChevronDown , cn , Expandable , ExpandableContent , OverflowText , Wrench } from '@sim/emcn'
55import { ShimmerText } from '@/components/ui'
66import { isBrowserAgentAvailable } from '@/lib/browser-agent/transport'
7+ import { Terminal as TerminalTool } from '@/lib/mothership/generated/tool-catalog-v1'
78import { RETIRED_BROWSER_REQUEST_TAKEOVER_ID } from '@/lib/mothership/tools/retired-tools'
8- import { getToolDisplayTitle } from '@/lib/mothership/tools/tool-display'
9+ import { getToolDisplayTitle , getToolStatusDisplayTitle } from '@/lib/mothership/tools/tool-display'
910import { useSmoothText } from '@/hooks/use-smooth-text'
1011import { type ToolCallData , ToolCallStatus } from '../../../../types'
1112import { getAgentIcon , isToolDone } from '../../utils'
@@ -40,16 +41,14 @@ interface AgentGroupProps {
4041 items : AgentGroupItem [ ]
4142 isDelegating ?: boolean
4243 isStreaming ?: boolean
43- /** This group is the latest section in its parent sequence (drives collapse). */
44- isCurrentSection ?: boolean
4544 /** The subagent lane is still open (no subagent_end yet) — i.e. actively running. */
4645 isLaneOpen ?: boolean
4746}
4847
4948function toolStatusTitle ( tool : ToolCallData ) : string {
5049 // Raw tool names must never surface — derive a human title when no display
5150 // title was resolved upstream.
52- return tool . displayTitle || getToolDisplayTitle ( String ( tool . toolName ?? '' ) , undefined )
51+ return tool . displayTitle || getToolDisplayTitle ( String ( tool . toolName ?? '' ) , tool . params )
5352}
5453
5554/**
@@ -69,12 +68,18 @@ function collectGroupTools(items: AgentGroupItem[]): ToolCallData[] {
6968 return tools
7069}
7170
72- /** True when any row in this group (or a nested one) is waiting on a permission decision . */
73- function hasAwaitingApproval ( items : AgentGroupItem [ ] ) : boolean {
71+ /** Blocking decisions must stay visible even when the surrounding log is collapsed . */
72+ function hasBlockingInteraction ( items : AgentGroupItem [ ] ) : boolean {
7473 return items . some ( ( item ) => {
75- if ( item . type === 'tool' ) return item . data . status === ToolCallStatus . awaiting_approval
76- // Text rows carry no tool calls, so only nested groups need recursing into.
77- return item . type === 'agent_group' ? hasAwaitingApproval ( item . group . items ) : false
74+ if ( item . type === 'tool' ) {
75+ return (
76+ item . data . status === ToolCallStatus . awaiting_approval ||
77+ ( item . data . toolName === TerminalTool . id &&
78+ item . data . status === ToolCallStatus . executing &&
79+ item . data . params ?. operation === 'handoff' )
80+ )
81+ }
82+ return item . type === 'agent_group' ? hasBlockingInteraction ( item . group . items ) : false
7883 } )
7984}
8085
@@ -136,39 +141,41 @@ export function AgentGroup({
136141 items,
137142 isDelegating = false ,
138143 isStreaming = false ,
139- isCurrentSection = false ,
140144 isLaneOpen = false ,
141145 error,
142146} : AgentGroupProps ) {
143- const AgentIcon = getAgentIcon ( agentName )
144147 const isMainAgent = agentName === 'mothership'
145- // Collapsed status line: the latest tool call, always in its RUNNING
146- // phrasing — it never flips to the completed rewrite (that lives in the
147- // expanded log). Work delegated further down bubbles up, so a group whose
148- // own turn is idle still narrates what its nested agent is doing rather
149- // than freezing on its last own tool. With several tools running at any
150- // depth, the most recently started wins and the rest become "+ n"; between
151- // rounds the last tool's title stays frozen; a closed lane shows the bare
152- // name.
148+ /** Main groups summarize their tool log; subagents retain their named live status. */
153149 const status = useMemo ( ( ) => {
154- if ( isMainAgent || ! isLaneOpen ) return undefined
150+ if ( ! isMainAgent && ! isLaneOpen ) return undefined
155151 const tools = collectGroupTools ( items )
156152 const running = tools . filter ( ( tool ) => tool . status === ToolCallStatus . executing )
157- if ( running . length > 0 ) {
158- const latest = running . reduce ( ( newest , tool ) =>
159- ( tool . startedAt ?? 0 ) >= ( newest . startedAt ?? 0 ) ? tool : newest
160- )
161- const title = toolStatusTitle ( latest )
162- return running . length > 1 ? `${ title } + ${ running . length - 1 } ` : title
153+ const latest = running . length
154+ ? running . reduce ( ( newest , tool ) =>
155+ ( tool . startedAt ?? 0 ) >= ( newest . startedAt ?? 0 ) ? tool : newest
156+ )
157+ : tools . at ( - 1 )
158+ if ( ! latest ) return undefined
159+ return {
160+ toolName : latest . toolName ,
161+ title : isMainAgent
162+ ? getToolStatusDisplayTitle ( toolStatusTitle ( latest ) , latest . status , latest . toolName )
163+ : toolStatusTitle ( latest ) ,
164+ additionalCount : isMainAgent ? tools . length - 1 : Math . max ( 0 , running . length - 1 ) ,
163165 }
164- const last = tools . at ( - 1 )
165- return last ? toolStatusTitle ( last ) : undefined
166166 } , [ isLaneOpen , isMainAgent , items ] )
167+ const AgentIcon = isMainAgent
168+ ? getAgentIcon ( status ?. toolName ?? '' , Wrench )
169+ : getAgentIcon ( agentName )
167170 const headerText = error
168- ? `${ agentLabel } — Failed`
169- : status
170- ? `${ agentLabel } — ${ status } `
171- : agentLabel
171+ ? isMainAgent
172+ ? 'Tool call failed'
173+ : `${ agentLabel } — Failed`
174+ : isMainAgent
175+ ? ( status ?. title ?? 'Working' )
176+ : status
177+ ? `${ agentLabel } — ${ status . title } `
178+ : agentLabel
172179 const hasItems = items . length > 0
173180 const resolved = isAgentGroupResolved ( items )
174181 const browserAgentAvailable = isBrowserAgentAvailable ( )
@@ -178,24 +185,16 @@ export function AgentGroup({
178185 const isWorking =
179186 ! activeBrowserTakeover && ( ( isDelegating && ! resolved ) || ( isStreaming && isLaneOpen ) )
180187
181- // SUBAGENT groups never auto-expand: the collapsed row IS the live view —
182- // label plus latest running tool title. Expanding is a deliberate user
183- // action; only a pending permission prompt or a browser hand-back forces
184- // one open. The MAIN lane ("Sim") is not a delegation card: its narration
185- // and tool calls are the turn itself, so it keeps the original live-expand
186- // behavior (open while streaming/current, settles when superseded).
187- const autoExpanded = isMainAgent && isStreaming && ( isCurrentSection || isLaneOpen || ! resolved )
188- const [ manualExpanded , setManualExpanded ] = useState < boolean | null > ( null )
188+ /** Keep every log collapsed until opened, except for blocking user interactions. */
189+ const [ manualExpanded , setManualExpanded ] = useState ( false )
189190 const [ expandedTakeoverId , setExpandedTakeoverId ] = useState < string | null > ( null )
190191 // An outstanding permission prompt overrides a manual collapse: the turn
191192 // cannot proceed until it is answered, so hiding it would deadlock the chat
192193 // with nothing on screen to explain why.
193194 const expanded =
194- hasAwaitingApproval ( items ) ||
195+ hasBlockingInteraction ( items ) ||
195196 nestedBrowserTakeover ||
196- ( activeBrowserTakeover
197- ? expandedTakeoverId === activeBrowserTakeover . id
198- : ( manualExpanded ?? autoExpanded ) )
197+ ( activeBrowserTakeover ? expandedTakeoverId === activeBrowserTakeover . id : manualExpanded )
199198
200199 const toggleExpanded = ( ) => {
201200 if ( activeBrowserTakeover ) {
@@ -211,6 +210,7 @@ export function AgentGroup({
211210 < button
212211 type = 'button'
213212 onClick = { toggleExpanded }
213+ aria-expanded = { expanded }
214214 className = 'group/agent flex w-full min-w-0 cursor-pointer items-center gap-2 text-left'
215215 >
216216 < div className = 'flex size-[16px] shrink-0 items-center justify-center' >
@@ -221,6 +221,12 @@ export function AgentGroup({
221221 ) : (
222222 < OverflowText label = { headerText } className = 'text-[var(--text-body)] text-sm' />
223223 ) }
224+ { status && status . additionalCount > 0 && (
225+ < span className = 'shrink-0 text-[var(--text-secondary)] text-sm' >
226+ { ' + ' }
227+ { status . additionalCount }
228+ </ span >
229+ ) }
224230 < ChevronDown
225231 className = { cn (
226232 'size-[14px] shrink-0 text-[var(--text-icon)] opacity-0 transition-[transform,opacity] duration-150 group-hover/agent:opacity-100 group-focus-visible/agent:opacity-100' ,
@@ -271,7 +277,6 @@ export function AgentGroup({
271277 items = { item . group . items }
272278 isDelegating = { item . group . isDelegating }
273279 isStreaming = { isStreaming }
274- isCurrentSection = { idx === items . length - 1 }
275280 isLaneOpen = { item . group . isOpen }
276281 error = { item . group . error }
277282 />
0 commit comments