From cf8439263ae4d85102176f1bc170624faec6ebed Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sun, 26 Apr 2026 15:33:01 -0500 Subject: [PATCH] fix(tui): keep todo pinned outside transcript --- ui-tui/src/app/useMainApp.ts | 6 +++++- ui-tui/src/components/appLayout.tsx | 4 ++-- ui-tui/src/lib/liveLayout.test.ts | 2 +- ui-tui/src/lib/liveLayout.ts | 2 +- ui-tui/src/lib/messages.test.ts | 23 +++++++++++++++++++++++ ui-tui/src/lib/messages.ts | 13 +++++++++++++ 6 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 ui-tui/src/lib/messages.test.ts diff --git a/ui-tui/src/app/useMainApp.ts b/ui-tui/src/app/useMainApp.ts index 262b400fa3..064d64ad59 100644 --- a/ui-tui/src/app/useMainApp.ts +++ b/ui-tui/src/app/useMainApp.ts @@ -16,6 +16,7 @@ import type { } from '../gatewayTypes.js' import { useGitBranch } from '../hooks/useGitBranch.js' import { useVirtualHistory } from '../hooks/useVirtualHistory.js' +import { appendTranscriptMessage } from '../lib/messages.js' import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js' import { terminalParityHints } from '../lib/terminalParity.js' import { buildToolTrailLine, sameToolTrailGroup, toolTrailLabel } from '../lib/text.js' @@ -198,7 +199,10 @@ export function useMainApp(gw: GatewayClient) { [selection] ) - const appendMessage = useCallback((msg: Msg) => setHistoryItems(prev => capHistory([...prev, msg])), []) + const appendMessage = useCallback( + (msg: Msg) => setHistoryItems(prev => capHistory(appendTranscriptMessage(prev, msg))), + [] + ) const sys = useCallback((text: string) => appendMessage({ role: 'system', text }), [appendMessage]) diff --git a/ui-tui/src/components/appLayout.tsx b/ui-tui/src/components/appLayout.tsx index d3d702355b..a6862027c0 100644 --- a/ui-tui/src/components/appLayout.tsx +++ b/ui-tui/src/components/appLayout.tsx @@ -28,10 +28,10 @@ const TranscriptPane = memo(function TranscriptPane({ return ( <> + + - - {transcript.virtualHistory.topSpacer > 0 ? : null} {transcript.virtualRows.slice(transcript.virtualHistory.start, transcript.virtualHistory.end).map(row => ( diff --git a/ui-tui/src/lib/liveLayout.test.ts b/ui-tui/src/lib/liveLayout.test.ts index 3d40f6f851..24426efe63 100644 --- a/ui-tui/src/lib/liveLayout.test.ts +++ b/ui-tui/src/lib/liveLayout.test.ts @@ -4,6 +4,6 @@ import { liveTailOrder } from './liveLayout.js' describe('liveTailOrder', () => { it('keeps todo before transcript and assistant live output', () => { - expect(liveTailOrder()).toEqual(['todo', 'history', 'assistant']) + expect(liveTailOrder()).toEqual(['todo', 'scroll-history', 'assistant']) }) }) diff --git a/ui-tui/src/lib/liveLayout.ts b/ui-tui/src/lib/liveLayout.ts index 1107edfce7..a990b06d0e 100644 --- a/ui-tui/src/lib/liveLayout.ts +++ b/ui-tui/src/lib/liveLayout.ts @@ -1 +1 @@ -export const liveTailOrder = () => ['todo', 'history', 'assistant'] as const +export const liveTailOrder = () => ['todo', 'scroll-history', 'assistant'] as const diff --git a/ui-tui/src/lib/messages.test.ts b/ui-tui/src/lib/messages.test.ts new file mode 100644 index 0000000000..6194311cb1 --- /dev/null +++ b/ui-tui/src/lib/messages.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest' + +import { appendTranscriptMessage } from './messages.js' + +describe('appendTranscriptMessage', () => { + it('merges adjacent tool-only shelves into one transcript row', () => { + const out = appendTranscriptMessage( + [{ kind: 'trail', role: 'system', text: '', tools: ['Terminal("one") ✓'] }], + { kind: 'trail', role: 'system', text: '', tools: ['Terminal("two") ✓'] } + ) + + expect(out).toEqual([{ kind: 'trail', role: 'system', text: '', tools: ['Terminal("one") ✓', 'Terminal("two") ✓'] }]) + }) + + it('does not merge tool shelves across thinking text', () => { + const out = appendTranscriptMessage( + [{ kind: 'trail', role: 'system', text: '', thinking: 'plan', tools: ['Terminal("one") ✓'] }], + { kind: 'trail', role: 'system', text: '', tools: ['Terminal("two") ✓'] } + ) + + expect(out).toHaveLength(2) + }) +}) diff --git a/ui-tui/src/lib/messages.ts b/ui-tui/src/lib/messages.ts index a459ec5a8a..60fc4b76ba 100644 --- a/ui-tui/src/lib/messages.ts +++ b/ui-tui/src/lib/messages.ts @@ -1,4 +1,17 @@ import type { Msg, Role } from '../types.js' +const isToolShelf = (msg: Msg | undefined) => + Boolean(msg?.kind === 'trail' && !msg.text && !msg.thinking?.trim() && msg.tools?.length) + +export const appendTranscriptMessage = (prev: Msg[], msg: Msg): Msg[] => { + if (isToolShelf(msg) && isToolShelf(prev.at(-1))) { + const last = prev.at(-1)! + + return [...prev.slice(0, -1), { ...last, tools: [...(last.tools ?? []), ...(msg.tools ?? [])] }] + } + + return [...prev, msg] +} + export const upsert = (prev: Msg[], role: Role, text: string): Msg[] => prev.at(-1)?.role === role ? [...prev.slice(0, -1), { role, text }] : [...prev, { role, text }]