diff --git a/ui-tui/src/__tests__/messageLine.test.ts b/ui-tui/src/__tests__/messageLine.test.ts
new file mode 100644
index 00000000000..b330bbd2374
--- /dev/null
+++ b/ui-tui/src/__tests__/messageLine.test.ts
@@ -0,0 +1,19 @@
+import { describe, expect, it } from 'vitest'
+
+import { shouldShowResponseSeparator } from '../components/messageLine.js'
+
+describe('shouldShowResponseSeparator', () => {
+ it('separates assistant response text from visible details', () => {
+ expect(shouldShowResponseSeparator({ role: 'assistant', text: 'final', thinking: 'plan' }, true)).toBe(true)
+ })
+
+ it('does not add a response separator without details or body text', () => {
+ expect(shouldShowResponseSeparator({ role: 'assistant', text: 'final' }, false)).toBe(false)
+ expect(shouldShowResponseSeparator({ role: 'assistant', text: ' ', thinking: 'plan' }, true)).toBe(false)
+ })
+
+ it('does not add response separators to non-assistant transcript rows', () => {
+ expect(shouldShowResponseSeparator({ role: 'user', text: 'prompt' }, true)).toBe(false)
+ expect(shouldShowResponseSeparator({ role: 'system', text: 'note' }, true)).toBe(false)
+ })
+})
diff --git a/ui-tui/src/__tests__/virtualHeights.test.ts b/ui-tui/src/__tests__/virtualHeights.test.ts
index b93df65d72a..37cb9c009ce 100644
--- a/ui-tui/src/__tests__/virtualHeights.test.ts
+++ b/ui-tui/src/__tests__/virtualHeights.test.ts
@@ -32,6 +32,45 @@ describe('virtual height estimates', () => {
)
})
+ it('accounts for the response separator when assistant details are visible', () => {
+ const msg: Msg = { role: 'assistant', text: 'ok', thinking: 'plan' }
+
+ expect(estimatedMsgHeight(msg, 80, { compact: false, details: true })).toBe(
+ estimatedMsgHeight(msg, 80, { compact: false, details: false }) + 3
+ )
+ })
+
+ it('does not account for a response separator without visible details', () => {
+ const msg: Msg = { role: 'assistant', text: 'ok' }
+
+ expect(estimatedMsgHeight(msg, 80, { compact: false, details: true })).toBe(
+ estimatedMsgHeight(msg, 80, { compact: false, details: false })
+ )
+ })
+
+ it('honors per-section visibility when estimating response separators', () => {
+ const thinkingOnly: Msg = { role: 'assistant', text: 'ok', thinking: 'plan' }
+ const toolsOnly: Msg = { role: 'assistant', text: 'ok', tools: ['Tool A'] }
+
+ expect(
+ estimatedMsgHeight(thinkingOnly, 80, {
+ compact: false,
+ details: true,
+ thinkingVisible: false,
+ toolsVisible: true
+ })
+ ).toBe(estimatedMsgHeight(thinkingOnly, 80, { compact: false, details: false }))
+
+ expect(
+ estimatedMsgHeight(toolsOnly, 80, {
+ compact: false,
+ details: true,
+ thinkingVisible: true,
+ toolsVisible: false
+ })
+ ).toBe(estimatedMsgHeight(toolsOnly, 80, { compact: false, details: false }))
+ })
+
it('reserves two extra rows for the inter-turn separator on non-first user messages', () => {
const msg: Msg = { role: 'user', text: 'follow-up question' }
const base = estimatedMsgHeight(msg, 80, { compact: false, details: false })
diff --git a/ui-tui/src/app/useMainApp.ts b/ui-tui/src/app/useMainApp.ts
index 4d7ab8926ba..ad2348d0dce 100644
--- a/ui-tui/src/app/useMainApp.ts
+++ b/ui-tui/src/app/useMainApp.ts
@@ -252,7 +252,10 @@ export function useMainApp(gw: GatewayClient) {
return `${thinking}:${tools}`
}, [ui.detailsMode, ui.detailsModeCommandOverride, ui.sections])
- const detailsVisible = detailsLayoutKey !== 'hidden:hidden'
+ const [thinkingDetailsMode, toolsDetailsMode] = detailsLayoutKey.split(':')
+ const thinkingDetailsVisible = thinkingDetailsMode !== 'hidden'
+ const toolsDetailsVisible = toolsDetailsMode !== 'hidden'
+ const detailsVisible = thinkingDetailsVisible || toolsDetailsVisible
const userPromptWidth = composerPromptWidth(ui.theme.brand.prompt)
const heightCacheKey = `${ui.sid ?? 'draft'}:${cols}:${userPromptWidth}:${ui.compact ? '1' : '0'}:${detailsLayoutKey}`
@@ -281,10 +284,21 @@ export function useMainApp(gw: GatewayClient) {
estimatedMsgHeight(virtualRows[index]!.msg, cols, {
compact: ui.compact,
details: detailsVisible,
+ thinkingVisible: thinkingDetailsVisible,
+ toolsVisible: toolsDetailsVisible,
userPrompt: ui.theme.brand.prompt,
withSeparator: virtualRows[index]!.msg.role === 'user' && firstUserIdx >= 0 && index > firstUserIdx
}),
- [cols, detailsVisible, firstUserIdx, ui.compact, ui.theme.brand.prompt, virtualRows]
+ [
+ cols,
+ detailsVisible,
+ firstUserIdx,
+ thinkingDetailsVisible,
+ toolsDetailsVisible,
+ ui.compact,
+ ui.theme.brand.prompt,
+ virtualRows
+ ]
)
const syncHeightCache = useCallback(
diff --git a/ui-tui/src/components/messageLine.tsx b/ui-tui/src/components/messageLine.tsx
index 4d1481373ab..2a7f0bbba23 100644
--- a/ui-tui/src/components/messageLine.tsx
+++ b/ui-tui/src/components/messageLine.tsx
@@ -109,6 +109,8 @@ export const MessageLine = memo(function MessageLine({
const showDetails =
(toolsMode !== 'hidden' && Boolean(msg.tools?.length)) || (thinkingMode !== 'hidden' && Boolean(thinking))
+ const showResponseSeparator = shouldShowResponseSeparator(msg, showDetails)
+
const content = (() => {
if (msg.kind === 'slash') {
return {msg.text}
@@ -195,6 +197,17 @@ export const MessageLine = memo(function MessageLine({
)}
+ {showResponseSeparator && (
+
+
+ └─
+
+
+ Response
+
+
+ )}
+
@@ -208,6 +221,9 @@ export const MessageLine = memo(function MessageLine({
)
})
+export const shouldShowResponseSeparator = (msg: Msg, showDetails: boolean): boolean =>
+ msg.role === 'assistant' && showDetails && /\S/.test(msg.text)
+
interface MessageLineProps {
cols: number
compact?: boolean
diff --git a/ui-tui/src/lib/virtualHeights.ts b/ui-tui/src/lib/virtualHeights.ts
index 874f8a1b8dc..4ae2ee3f734 100644
--- a/ui-tui/src/lib/virtualHeights.ts
+++ b/ui-tui/src/lib/virtualHeights.ts
@@ -1,6 +1,6 @@
+import { TERMUX_TUI_MODE } from '../config/env.js'
import type { Msg } from '../types.js'
-import { TERMUX_TUI_MODE } from '../config/env.js'
import { transcriptBodyWidth } from './inputMetrics.js'
const hashText = (text: string) => {
@@ -72,11 +72,15 @@ export const estimatedMsgHeight = (
{
compact,
details,
+ thinkingVisible = details,
+ toolsVisible = details,
userPrompt = '',
withSeparator = false
}: {
compact: boolean
details: boolean
+ thinkingVisible?: boolean
+ toolsVisible?: boolean
userPrompt?: string
withSeparator?: boolean
}
@@ -111,7 +115,17 @@ export const estimatedMsgHeight = (
}
if (details) {
- h += (msg.tools?.length ?? 0) + wrappedLines(msg.thinking ?? '', bodyWidth)
+ const hasVisibleTools = toolsVisible && Boolean(msg.tools?.length)
+ const hasVisibleThinking = thinkingVisible && /\S/.test(msg.thinking ?? '')
+ const hasVisibleDetails = hasVisibleTools || hasVisibleThinking
+
+ if (hasVisibleDetails) {
+ h += (hasVisibleTools ? (msg.tools?.length ?? 0) : 0) + (hasVisibleThinking ? wrappedLines(msg.thinking ?? '', bodyWidth) : 0)
+
+ if (msg.role === 'assistant' && /\S/.test(msg.text)) {
+ h += 2
+ }
+ }
}
if (msg.role === 'user' || msg.kind === 'diff') {