diff --git a/ui-tui/src/app.tsx b/ui-tui/src/app.tsx index a5a0d1296b..e70644ea6f 100644 --- a/ui-tui/src/app.tsx +++ b/ui-tui/src/app.tsx @@ -338,6 +338,7 @@ export function App({ gw }: { gw: GatewayClient }) { const [secret, setSecret] = useState(null) const [picker, setPicker] = useState(false) const [reasoning, setReasoning] = useState('') + const [reasoningActive, setReasoningActive] = useState(false) const [reasoningStreaming, setReasoningStreaming] = useState(false) const [statusBar, setStatusBar] = useState(true) const [lastUserMsg, setLastUserMsg] = useState('') @@ -394,6 +395,7 @@ export function App({ gw }: { gw: GatewayClient }) { clearTimeout(reasoningStreamingTimerRef.current) } + setReasoningActive(true) setReasoningStreaming(true) reasoningStreamingTimerRef.current = setTimeout(() => { reasoningStreamingTimerRef.current = null @@ -401,13 +403,14 @@ export function App({ gw }: { gw: GatewayClient }) { }, REASONING_PULSE_MS) }, []) - const clearReasoningStreaming = useCallback(() => { + const endReasoningPhase = useCallback(() => { if (reasoningStreamingTimerRef.current) { clearTimeout(reasoningStreamingTimerRef.current) reasoningStreamingTimerRef.current = null } setReasoningStreaming(false) + setReasoningActive(false) }, []) useEffect( @@ -589,6 +592,7 @@ export function App({ gw }: { gw: GatewayClient }) { }, [pushActivity, rpc, sid]) const idle = () => { + endReasoningPhase() setTools([]) setTurnTrail([]) setBusy(false) @@ -1342,6 +1346,7 @@ export function App({ gw }: { gw: GatewayClient }) { case 'message.start': setBusy(true) + endReasoningPhase() setReasoning('') setActivity([]) setTurnTrail([]) @@ -1416,7 +1421,7 @@ export function App({ gw }: { gw: GatewayClient }) { case 'tool.start': pruneTransient() - clearReasoningStreaming() + endReasoningPhase() setTools(prev => [ ...prev, { id: p.tool_id, name: p.name, context: (p.context as string) || '', startedAt: Date.now() } @@ -1507,7 +1512,7 @@ export function App({ gw }: { gw: GatewayClient }) { case 'message.delta': pruneTransient() - clearReasoningStreaming() + endReasoningPhase() if (p?.text && !interruptedRef.current) { buf.current = p.rendered ?? buf.current + p.text @@ -1528,7 +1533,6 @@ export function App({ gw }: { gw: GatewayClient }) { idle() setReasoning('') - clearReasoningStreaming() setStreaming('') if (inflightPasteIdsRef.current.length) { @@ -1586,7 +1590,20 @@ export function App({ gw }: { gw: GatewayClient }) { break } }, - [appendMessage, dequeue, newSession, pushActivity, pushTrail, send, sys] + [ + appendMessage, + bellOnComplete, + dequeue, + endReasoningPhase, + newSession, + pruneTransient, + pulseReasoningStreaming, + pushActivity, + pushTrail, + send, + sys, + stdout + ] ) onEventRef.current = onEvent @@ -2484,6 +2501,7 @@ export function App({ gw }: { gw: GatewayClient }) { activity={busy ? activity : []} busy={busy && !streaming} reasoning={reasoning} + reasoningActive={reasoningActive} reasoningStreaming={reasoningStreaming} t={theme} thinkingMode={hasReasoning ? thinkingMode : 'truncated'} diff --git a/ui-tui/src/components/thinking.tsx b/ui-tui/src/components/thinking.tsx index 4a1f4c7207..1ed03f8104 100644 --- a/ui-tui/src/components/thinking.tsx +++ b/ui-tui/src/components/thinking.tsx @@ -58,7 +58,17 @@ function Detail({ color, content, dimColor }: DetailRow) { // ── Streaming cursor ───────────────────────────────────────────────── -function StreamCursor({ active = false, color, dimColor }: { active?: boolean; color: string; dimColor?: boolean }) { +function StreamCursor({ + color, + dimColor, + streaming = false, + visible = false +}: { + color: string + dimColor?: boolean + streaming?: boolean + visible?: boolean +}) { const [on, setOn] = useState(true) useEffect(() => { @@ -67,21 +77,23 @@ function StreamCursor({ active = false, color, dimColor }: { active?: boolean; c return () => clearInterval(id) }, []) - return ( + return visible ? ( - {active && on ? '▍' : ' '} + {streaming && on ? '▍' : ' '} - ) + ) : null } // ── Thinking (pre-tool fallback) ───────────────────────────────────── export const Thinking = memo(function Thinking({ + active = false, mode = 'truncated', reasoning, streaming = false, t }: { + active?: boolean mode?: ThinkingMode reasoning: string streaming?: boolean @@ -108,12 +120,12 @@ export const Thinking = memo(function Thinking({ {preview} - + - ) : streaming ? ( + ) : active ? ( - + ) : null} @@ -127,6 +139,7 @@ type Group = { color: string; content: ReactNode; details: DetailRow[]; key: str export const ToolTrail = memo(function ToolTrail({ busy = false, thinkingMode = 'truncated', + reasoningActive = false, reasoning = '', reasoningStreaming = false, t, @@ -136,6 +149,7 @@ export const ToolTrail = memo(function ToolTrail({ }: { busy?: boolean thinkingMode?: ThinkingMode + reasoningActive?: boolean reasoning?: string reasoningStreaming?: boolean t: Theme @@ -157,7 +171,7 @@ export const ToolTrail = memo(function ToolTrail({ const reasoningTail = thinkingPreview(reasoning, thinkingMode, THINKING_COT_MAX) - if (!busy && !trail.length && !tools.length && !activity.length && !reasoningTail) { + if (!busy && !trail.length && !tools.length && !activity.length && !reasoningTail && !reasoningActive) { return null } @@ -247,16 +261,16 @@ export const ToolTrail = memo(function ToolTrail({ content: ( <> {reasoningTail} - + ), dimColor: true, key: 'cot' }) - } else if (reasoningStreaming && groups.length && thinkingMode === 'collapsed') { + } else if (reasoningActive && groups.length && thinkingMode === 'collapsed') { detail({ color: t.color.dim, - content: , + content: , dimColor: true, key: 'cot' }) @@ -276,7 +290,13 @@ export const ToolTrail = memo(function ToolTrail({ return ( {busy && !groups.length && ( - + )} {!busy && !groups.length && reasoningTail && (