diff --git a/apps/desktop/src/app/agents/index.tsx b/apps/desktop/src/app/agents/index.tsx index 2fcf4a9273..176d7035c5 100644 --- a/apps/desktop/src/app/agents/index.tsx +++ b/apps/desktop/src/app/agents/index.tsx @@ -1,5 +1,5 @@ import { useStore } from '@nanostores/react' -import { useEffect, useMemo, useState } from 'react' +import { type ReactNode, useEffect, useMemo, useState } from 'react' import { useElapsedSeconds } from '@/components/chat/activity-timer' import { ActivityTimerText } from '@/components/chat/activity-timer-text' @@ -8,12 +8,14 @@ import { FadeText } from '@/components/ui/fade-text' import { Activity, AlertCircle, + CheckCircle2, Layers3, Loader2, type LucideIcon, RefreshCw, Sparkles } from '@/lib/icons' +import { useEnterAnimation } from '@/lib/use-enter-animation' import { cn } from '@/lib/utils' import { $desktopActionTasks, buildRailTasks, type RailTask, type RailTaskStatus } from '@/store/activity' import { $previewServerRestart } from '@/store/preview' @@ -59,19 +61,29 @@ const RAIL_ICON: Record = { success: Sparkles } -const STATUS_GLYPH: Record = { - completed: '✓', - failed: '✗', - interrupted: '■', - queued: '○', - running: '●' -} +// Mirrors statusGlyph() in tool-fallback.tsx so subagent rows speak the +// same visual vocabulary as the chat tool blocks. +function statusGlyph(status: SubagentStatus): ReactNode { + if (status === 'running' || status === 'queued') { + return ( + + ) + } -const STREAM_GLYPH: Record = { - progress: '·', - summary: '✓', - thinking: '💭', - tool: '●' + if (status === 'failed' || status === 'interrupted') { + return + } + + return ( + + ) } const STREAM_TONE: Record = { @@ -81,6 +93,26 @@ const STREAM_TONE: Record = { tool: 'text-foreground/85' } +function streamGlyph(entry: SubagentStreamEntry): ReactNode { + if (entry.isError) { + return + } + + if (entry.kind === 'tool') { + return + } + + if (entry.kind === 'summary') { + return + } + + if (entry.kind === 'thinking') { + return + } + + return +} + interface AgentsViewProps { initialSection?: AgentsSection onClose: () => void @@ -287,17 +319,35 @@ function DelegationGroup({ group, nowMs }: { group: RootGroup; nowMs: number }) ) } -function StreamLine({ active, entry }: { active: boolean; entry: SubagentStreamEntry }) { +function StreamLine({ + active, + entry, + parentRunning, + rowKey +}: { + active: boolean + entry: SubagentStreamEntry + parentRunning: boolean + rowKey: string +}) { + const enterRef = useEnterAnimation(parentRunning, `subagent-stream:${rowKey}`) const isMono = entry.kind === 'tool' const tone = entry.isError ? 'text-destructive' : STREAM_TONE[entry.kind] return ( -
- {STREAM_GLYPH[entry.kind]} +
+ {streamGlyph(entry)} {entry.text} {active ? ( - + ) : null}
@@ -310,6 +360,7 @@ function SubagentRow({ node, depth = 0, nowMs }: { node: SubagentNode; depth?: n const durationSeconds = typeof node.durationSeconds === 'number' ? Math.max(0, Math.round(node.durationSeconds)) : elapsed const [open, setOpen] = useState(() => running || depth < 2) + const enterRef = useEnterAnimation(true, `subagent-row:${node.id}`) useEffect(() => { if (running) setOpen(true) @@ -327,61 +378,52 @@ function SubagentRow({ node, depth = 0, nowMs }: { node: SubagentNode; depth?: n ].filter(Boolean) return ( -
0 && 'pl-4')}> +
0 && 'pl-4')} + data-slot="tool-block" + ref={enterRef} + > {visibleRows.length > 0 ? ( -
+
{visibleRows.map((entry, i) => ( ))}
) : null} {open && fileLines.length > 0 ? ( -
+

Files

{fileLines.slice(0, 8).map(line => (

@@ -397,7 +439,7 @@ function SubagentRow({ node, depth = 0, nowMs }: { node: SubagentNode; depth?: n ) : null} {node.children.length > 0 ? ( -

+
{node.children.map(child => ( ))}