feat(tui): collapse completed todo panel on turn end

This commit is contained in:
Brooklyn Nicholson
2026-04-26 16:24:15 -05:00
parent cb7cfba6de
commit 2259eac49e
6 changed files with 22 additions and 4 deletions

View File

@@ -93,7 +93,13 @@ describe('createGatewayEventHandler', () => {
onEvent({ payload: { text: 'done' }, type: 'message.complete' } as any)
expect(getTurnState().todos).toEqual([])
expect(appended).toContainEqual({ kind: 'trail', role: 'system', text: '', todos })
expect(appended).toContainEqual({
kind: 'trail',
role: 'system',
text: '',
todoCollapsedByDefault: true,
todos
})
})
it('keeps the current todo list visible when the next message starts', () => {

View File

@@ -26,6 +26,7 @@ describe('turnStore live progress helpers', () => {
kind: 'trail',
role: 'system',
text: '',
todoCollapsedByDefault: true,
todos: [
{ content: 'prep', id: 'prep', status: 'completed' },
{ content: 'serve', id: 'serve', status: 'completed' }

View File

@@ -49,12 +49,13 @@ export const archiveTodosAtTurnEnd = () => {
return []
}
const done = isTodoDone(state.todos)
const msg: Msg = {
kind: 'trail',
role: 'system',
text: '',
todos: state.todos,
...(isTodoDone(state.todos) ? {} : { todoIncomplete: true })
...(done ? { todoCollapsedByDefault: true } : { todoIncomplete: true })
}
patchTurnState({ todoCollapsed: false, todos: [] })

View File

@@ -38,7 +38,14 @@ export const MessageLine = memo(function MessageLine({
const thinking = msg.thinking?.trim() ?? ''
if (msg.kind === 'trail' && msg.todos?.length) {
return <TodoPanel incomplete={msg.todoIncomplete} t={t} todos={msg.todos} />
return (
<TodoPanel
defaultCollapsed={msg.todoCollapsedByDefault}
incomplete={msg.todoIncomplete}
t={t}
todos={msg.todos}
/>
)
}
if (msg.kind === 'trail' && (msg.tools?.length || tools.length || thinking)) {

View File

@@ -14,12 +14,14 @@ const rowColor = (t: Theme, status: TodoItem['status']) => {
export const TodoPanel = memo(function TodoPanel({
collapsed,
defaultCollapsed = false,
incomplete = false,
onToggle,
t,
todos
}: {
collapsed?: boolean
defaultCollapsed?: boolean
incomplete?: boolean
onToggle?: () => void
t: Theme
@@ -28,7 +30,7 @@ export const TodoPanel = memo(function TodoPanel({
// Fallback local state for archived todos in transcript where there's no
// external controller. Live TodoPanel passes collapsed+onToggle from the
// turn store so clicks still work there.
const [localCollapsed, setLocalCollapsed] = useState(false)
const [localCollapsed, setLocalCollapsed] = useState(defaultCollapsed)
const isControlled = typeof collapsed === 'boolean'
const effectiveCollapsed = isControlled ? collapsed : localCollapsed

View File

@@ -118,6 +118,7 @@ export interface Msg {
tools?: string[]
todos?: TodoItem[]
todoIncomplete?: boolean
todoCollapsedByDefault?: boolean
}
export type Role = 'assistant' | 'system' | 'tool' | 'user'