diff --git a/ui-tui/src/app/useMainApp.ts b/ui-tui/src/app/useMainApp.ts
index 262b400fa38..064d64ad597 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 d3d702355b9..a6862027c0a 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 3d40f6f8513..24426efe630 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 1107edfce7b..a990b06d0e8 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 00000000000..6194311cb15
--- /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 a459ec5a8a4..60fc4b76baa 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 }]