fix(tui): stabilize live todo progress

This commit is contained in:
Brooklyn Nicholson 2026-04-26 15:55:38 -05:00
parent 1566f1eecc
commit f5552f92e2
14 changed files with 256 additions and 86 deletions

View file

@ -28,10 +28,6 @@ const TranscriptPane = memo(function TranscriptPane({
return (
<>
<Box flexDirection="column" flexShrink={0}>
<LiveTodoPanel />
</Box>
<ScrollBox flexDirection="column" flexGrow={1} flexShrink={1} ref={transcript.scrollRef} stickyScroll>
<Box flexDirection="column" paddingX={1}>
{transcript.virtualHistory.topSpacer > 0 ? <Box height={transcript.virtualHistory.topSpacer} /> : null}
@ -73,6 +69,10 @@ const TranscriptPane = memo(function TranscriptPane({
</Box>
</ScrollBox>
<Box flexDirection="column" flexShrink={0} paddingX={1}>
<LiveTodoPanel />
</Box>
<NoSelect flexShrink={0} marginLeft={1}>
<TranscriptScrollbar scrollRef={transcript.scrollRef} t={ui.theme} />
</NoSelect>

View file

@ -10,6 +10,7 @@ import type { Theme } from '../theme.js'
import type { ActiveTool, DetailsMode, Msg, SectionVisibility } from '../types.js'
import { Md } from './markdown.js'
import { TodoPanel } from './todoPanel.js'
import { ToolTrail } from './thinking.js'
export const MessageLine = memo(function MessageLine({
@ -35,6 +36,10 @@ export const MessageLine = memo(function MessageLine({
const activityMode = sectionMode('activity', detailsMode, sections, detailsModeCommandOverride)
const thinking = msg.thinking?.trim() ?? ''
if (msg.kind === 'trail' && msg.todos?.length) {
return <TodoPanel t={t} todos={msg.todos} />
}
if (msg.kind === 'trail' && (msg.tools?.length || tools.length || thinking)) {
return thinkingMode !== 'hidden' || toolsMode !== 'hidden' || activityMode !== 'hidden' ? (
<Box flexDirection="column">

View file

@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react'
import { memo } from 'react'
import type { AppLayoutProgressProps } from '../app/interfaces.js'
import { useTurnSelector } from '../app/turnStore.js'
import { toggleTodoCollapsed, useTurnSelector } from '../app/turnStore.js'
import { $uiState } from '../app/uiStore.js'
import type { DetailsMode, Msg, SectionVisibility } from '../types.js'
@ -105,8 +105,9 @@ export const StreamingAssistant = memo(function StreamingAssistant({
export const LiveTodoPanel = memo(function LiveTodoPanel() {
const ui = useStore($uiState)
const todos = useTurnSelector(state => state.todos)
const collapsed = useTurnSelector(state => state.todoCollapsed)
return <TodoPanel t={ui.theme} todos={todos} />
return <TodoPanel collapsed={collapsed} onToggle={toggleTodoCollapsed} t={ui.theme} todos={todos} />
})
interface StreamingAssistantProps {

View file

@ -11,35 +11,52 @@ const rowColor = (t: Theme, status: TodoItem['status']) => {
return tone === 'active' ? t.color.cornsilk : tone === 'body' ? t.color.statusFg : t.color.dim
}
export const TodoPanel = memo(function TodoPanel({ t, todos }: { t: Theme; todos: TodoItem[] }) {
export const TodoPanel = memo(function TodoPanel({
collapsed = false,
onToggle,
t,
todos
}: {
collapsed?: boolean
onToggle?: () => void
t: Theme
todos: TodoItem[]
}) {
if (!todos.length) {
return null
}
const done = todos.filter(todo => todo.status === 'completed').length
return (
<Box flexDirection="column" marginBottom={1}>
<Text color={t.color.dim}>
<Text color={t.color.amber}> </Text>
<Text bold color={t.color.cornsilk}>
Todo
</Text>{' '}
<Text color={t.color.statusFg} dim>
({todos.filter(todo => todo.status === 'completed').length}/{todos.length})
<Box onClick={onToggle}>
<Text color={t.color.dim}>
<Text color={t.color.amber}>{collapsed ? '▸ ' : '▾ '}</Text>
<Text bold color={t.color.cornsilk}>
Todo
</Text>{' '}
<Text color={t.color.statusFg} dim>
({done}/{todos.length})
</Text>
</Text>
</Text>
<Box flexDirection="column" marginLeft={2}>
{todos.map(todo => {
const tone = todoTone(todo.status)
const color = rowColor(t, todo.status)
return (
<Text color={color} dim={tone === 'dim'} key={todo.id}>
<Text color={color}>{todoGlyph(todo.status)} </Text>
{todo.content}
</Text>
)
})}
</Box>
{!collapsed && (
<Box flexDirection="column" marginLeft={2}>
{todos.map(todo => {
const tone = todoTone(todo.status)
const color = rowColor(t, todo.status)
return (
<Text color={color} dim={tone === 'dim'} key={todo.id}>
<Text color={color}>{todoGlyph(todo.status)} </Text>
{todo.content}
</Text>
)
})}
</Box>
)}
</Box>
)
})