2b0b4a2191
Under display.sections.thinking: collapsed, the TUI now keeps the LIVE reasoning panel open while reasoning streams and collapses it the moment the reasoning phase ends (first tool call, final answer, or new turn). Previously 'collapsed' meant the panel was always collapsed — including the currently-streaming reasoning — and there was no way to get 'expanded while live, collapsed when done'. This makes 'collapsed' an auto preference: - turnController tags the open reasoning segment isLiveReasoning and seals the tag in endReasoningPhase/closeReasoningSegment - streamingAssistant passes reasoningActive only to the live segment, so sealed reasoning segments from earlier phases stay collapsed - ToolTrail auto-opens while reasoningActive under collapsed mode; expanded/hidden/MoA-reference semantics are unchanged Adds thinkingLiveCollapse.test.tsx covering open-on-stream, close-on- finish (including mid-turn rerender), and the expanded-mode no-op. (cherry picked from commit 6ef4ef77d3a0837c8ef5c5dac3de74f352bd637d)
121 lines
4.1 KiB
TypeScript
121 lines
4.1 KiB
TypeScript
import { useStore } from '@nanostores/react'
|
|
import { memo } from 'react'
|
|
|
|
import type { AppLayoutProgressProps } from '../app/interfaces.js'
|
|
import { toggleTodoCollapsed, useTurnSelector } from '../app/turnStore.js'
|
|
import { $uiState } from '../app/uiStore.js'
|
|
import { blockRenders } from '../domain/blockLayout.js'
|
|
import { appendToolShelfMessage } from '../lib/liveProgress.js'
|
|
import type { ActiveTool, DetailsMode, Msg, SectionVisibility } from '../types.js'
|
|
|
|
import { MessageLine } from './messageLine.js'
|
|
import { TodoPanel } from './todoPanel.js'
|
|
|
|
const groupedSegments = (segments: Msg[]): Msg[] =>
|
|
segments.reduce<Msg[]>((acc, msg) => appendToolShelfMessage(acc, msg), [])
|
|
|
|
interface LiveBlock {
|
|
isStreaming?: boolean
|
|
key: string
|
|
msg: Msg
|
|
tools?: ActiveTool[]
|
|
}
|
|
|
|
export const StreamingAssistant = memo(function StreamingAssistant({
|
|
cols,
|
|
compact,
|
|
detailsMode,
|
|
detailsModeCommandOverride,
|
|
prevMsg,
|
|
progress,
|
|
sections
|
|
}: StreamingAssistantProps) {
|
|
const ui = useStore($uiState)
|
|
const streamSegments = useTurnSelector(state => state.streamSegments)
|
|
const streamPendingTools = useTurnSelector(state => state.streamPendingTools)
|
|
const streaming = useTurnSelector(state => state.streaming)
|
|
const activeTools = useTurnSelector(state => state.tools)
|
|
const showStreamingArea = Boolean(streaming)
|
|
|
|
if (!progress.showProgressArea && !showStreamingArea && !activeTools.length) {
|
|
return null
|
|
}
|
|
|
|
// Flatten the live area into one ordered list so each block's leading gap
|
|
// can be derived from the block directly above it — including the boundary
|
|
// back into settled history (prevMsg). Tracking the predecessor rather than
|
|
// the live text is what keeps the streaming block from jumping when it
|
|
// flushes into a settled segment.
|
|
const blocks: LiveBlock[] = groupedSegments(streamSegments).map((msg, i) => ({ key: `seg:${i}`, msg }))
|
|
|
|
if (activeTools.length) {
|
|
blocks.push({ key: 'active-tools', msg: { kind: 'trail', role: 'system', text: '' }, tools: activeTools })
|
|
}
|
|
|
|
if (showStreamingArea) {
|
|
blocks.push({
|
|
isStreaming: true,
|
|
key: 'streaming',
|
|
msg: { role: 'assistant', text: streaming, ...(streamPendingTools.length && { tools: streamPendingTools }) }
|
|
})
|
|
} else if (streamPendingTools.length) {
|
|
blocks.push({ key: 'pending-tools', msg: { kind: 'trail', role: 'system', text: '', tools: streamPendingTools } })
|
|
}
|
|
|
|
const detailsCtx = { commandOverride: detailsModeCommandOverride, detailsMode, sections }
|
|
let prev = prevMsg
|
|
|
|
return (
|
|
<>
|
|
{blocks.map(block => {
|
|
const node = (
|
|
<MessageLine
|
|
cols={cols}
|
|
compact={compact}
|
|
detailsMode={detailsMode}
|
|
detailsModeCommandOverride={detailsModeCommandOverride}
|
|
isStreaming={block.isStreaming}
|
|
key={block.key}
|
|
liveDetails
|
|
msg={block.msg}
|
|
prev={prev}
|
|
reasoningActive={block.msg.isLiveReasoning === true}
|
|
sections={sections}
|
|
t={ui.theme}
|
|
{...(block.tools ? { tools: block.tools } : {})}
|
|
/>
|
|
)
|
|
|
|
// Advance the grouping predecessor only past blocks that actually
|
|
// paint, so a trail hidden by /details stays transparent here too
|
|
// (active tools live in the prop, so fold them into the check).
|
|
const checkMsg = block.tools?.length ? { ...block.msg, tools: block.tools.map(tool => tool.name) } : block.msg
|
|
|
|
if (blockRenders(checkMsg, detailsCtx)) {
|
|
prev = block.msg
|
|
}
|
|
|
|
return node
|
|
})}
|
|
</>
|
|
)
|
|
})
|
|
|
|
export const LiveTodoPanel = memo(function LiveTodoPanel() {
|
|
const ui = useStore($uiState)
|
|
const todos = useTurnSelector(state => state.todos)
|
|
const collapsed = useTurnSelector(state => state.todoCollapsed)
|
|
|
|
return <TodoPanel collapsed={collapsed} onToggle={toggleTodoCollapsed} t={ui.theme} todos={todos} />
|
|
})
|
|
|
|
interface StreamingAssistantProps {
|
|
cols: number
|
|
compact?: boolean
|
|
detailsMode: DetailsMode
|
|
detailsModeCommandOverride: boolean
|
|
prevMsg?: Msg
|
|
progress: AppLayoutProgressProps
|
|
sections?: SectionVisibility
|
|
}
|