diff --git a/ui-tui/src/lib/liveProgress.test.ts b/ui-tui/src/lib/liveProgress.test.ts index d10e1bb9a1f..141fb7acdc2 100644 --- a/ui-tui/src/lib/liveProgress.test.ts +++ b/ui-tui/src/lib/liveProgress.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { appendToolShelfMessage, isTodoDone } from './liveProgress.js' +import { appendToolShelfMessage, canHoldToolShelf, isTodoDone, mergeToolShelfInto } from './liveProgress.js' describe('isTodoDone', () => { it('only treats non-empty all-completed/cancelled lists as done', () => { @@ -16,6 +16,23 @@ describe('isTodoDone', () => { }) }) +describe('tool shelf helpers', () => { + it('recognizes contextual thinking shelves as holders', () => { + expect(canHoldToolShelf({ kind: 'trail', role: 'system', text: '', thinking: 'plan' })).toBe(true) + expect(canHoldToolShelf({ kind: 'trail', role: 'system', text: '', tools: ['one ✓'] })).toBe(true) + expect(canHoldToolShelf({ role: 'assistant', text: 'done' })).toBe(false) + }) + + it('merges source rows into an existing shelf', () => { + expect( + mergeToolShelfInto( + { kind: 'trail', role: 'system', text: '', thinking: 'plan', tools: ['one ✓'] }, + { kind: 'trail', role: 'system', text: '', tools: ['two ✓'] } + ) + ).toEqual({ kind: 'trail', role: 'system', text: '', thinking: 'plan', tools: ['one ✓', 'two ✓'] }) + }) +}) + describe('appendToolShelfMessage', () => { it('merges adjacent tool shelves into one contextual shelf', () => { const merged = appendToolShelfMessage([{ kind: 'trail', role: 'system', text: '', tools: ['one ✓'] }], { diff --git a/ui-tui/src/lib/liveProgress.ts b/ui-tui/src/lib/liveProgress.ts index 62f741633ce..9666e4312c1 100644 --- a/ui-tui/src/lib/liveProgress.ts +++ b/ui-tui/src/lib/liveProgress.ts @@ -6,9 +6,14 @@ export const isTodoDone = (todos: readonly TodoItem[]) => export const isToolShelfMessage = (msg: Msg | undefined) => Boolean(msg?.kind === 'trail' && !msg.text && !msg.thinking?.trim() && msg.tools?.length) -const canHoldToolShelf = (msg: Msg | undefined) => +export const canHoldToolShelf = (msg: Msg | undefined) => Boolean(msg?.kind === 'trail' && !msg.text && (msg.thinking?.trim() || msg.tools?.length)) +export const mergeToolShelfInto = (target: Msg, source: Msg): Msg => ({ + ...target, + tools: [...(target.tools ?? []), ...(source.tools ?? [])] +}) + export const appendToolShelfMessage = (prev: readonly Msg[], msg: Msg): Msg[] => { if (!isToolShelfMessage(msg)) { return [...prev, msg] @@ -20,7 +25,7 @@ export const appendToolShelfMessage = (prev: readonly Msg[], msg: Msg): Msg[] => if (canHoldToolShelf(candidate)) { const next = [...prev] - next[index] = { ...candidate!, tools: [...(candidate!.tools ?? []), ...(msg.tools ?? [])] } + next[index] = mergeToolShelfInto(candidate!, msg) return next }