fix: keep historical thinking collapsed in tui

(cherry picked from commit 354999388a4e465b6b7efa46cdc61d8d5b22b35f)
This commit is contained in:
LeonSGP43
2026-06-08 19:23:20 +08:00
committed by Teknium
parent 7f84de2775
commit b587681afc
7 changed files with 119 additions and 4 deletions

View File

@@ -128,6 +128,81 @@ describe('MessageLine', () => {
expect(renderedLine).toContain('Ψ > Okay')
})
it('keeps historical thinking blocks collapsed by default', () => {
const stdout = new PassThrough()
const stdin = new PassThrough()
const stderr = new PassThrough()
let output = ''
Object.assign(stdout, { columns: 80, isTTY: false, rows: 24 })
Object.assign(stdin, { isTTY: false })
Object.assign(stderr, { isTTY: false })
stdout.on('data', chunk => {
output += chunk.toString()
})
const instance = renderSync(
React.createElement(MessageLine, {
cols: 80,
msg: { kind: 'trail', role: 'system', text: '', thinking: 'step one\nstep two' },
t: DEFAULT_THEME
}),
{
patchConsole: false,
stderr: stderr as NodeJS.WriteStream,
stdin: stdin as NodeJS.ReadStream,
stdout: stdout as NodeJS.WriteStream
}
)
instance.unmount()
instance.cleanup()
const rendered = stripAnsi(output)
expect(rendered).toContain('Thinking')
expect(rendered).not.toContain('step one')
expect(rendered).not.toContain('step two')
})
it('keeps live thinking blocks expanded while streaming', () => {
const stdout = new PassThrough()
const stdin = new PassThrough()
const stderr = new PassThrough()
let output = ''
Object.assign(stdout, { columns: 80, isTTY: false, rows: 24 })
Object.assign(stdin, { isTTY: false })
Object.assign(stderr, { isTTY: false })
stdout.on('data', chunk => {
output += chunk.toString()
})
const instance = renderSync(
React.createElement(MessageLine, {
cols: 80,
liveDetails: true,
msg: { kind: 'trail', role: 'system', text: '', thinking: 'step one\nstep two' },
t: DEFAULT_THEME
}),
{
patchConsole: false,
stderr: stderr as NodeJS.WriteStream,
stdin: stdin as NodeJS.ReadStream,
stdout: stdout as NodeJS.WriteStream
}
)
instance.unmount()
instance.cleanup()
const rendered = stripAnsi(output)
expect(rendered).toContain('Thinking')
expect(rendered).toContain('step one')
expect(rendered).toContain('step two')
})
})
describe('upsert', () => {

View File

@@ -82,6 +82,28 @@ describe('virtual height estimates', () => {
).toBe(estimatedMsgHeight(toolsOnly, 80, { compact: false, details: false }))
})
it('treats historical thinking blocks as collapsed unless explicitly expanded', () => {
const msg: Msg = { role: 'assistant', text: 'ok', thinking: 'line 1\nline 2\nline 3' }
expect(
estimatedMsgHeight(msg, 80, {
compact: false,
details: true,
thinkingExpanded: false,
thinkingVisible: true,
toolsVisible: false
})
).toBeLessThan(
estimatedMsgHeight(msg, 80, {
compact: false,
details: true,
thinkingExpanded: true,
thinkingVisible: true,
toolsVisible: 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 })

View File

@@ -353,6 +353,10 @@ export function useMainApp(gw: GatewayClient) {
const [thinkingDetailsMode, toolsDetailsMode] = detailsLayoutKey.split(':')
const thinkingDetailsVisible = thinkingDetailsMode !== 'hidden'
const toolsDetailsVisible = toolsDetailsMode !== 'hidden'
const historyThinkingExpanded =
thinkingDetailsVisible && (ui.detailsModeCommandOverride || ui.sections.thinking === 'expanded')
const detailsVisible = thinkingDetailsVisible || toolsDetailsVisible
const userPromptWidth = composerPromptWidth(ui.theme.brand.prompt)
const heightCacheKey = `${ui.sid ?? 'draft'}:${cols}:${userPromptWidth}:${ui.compact ? '1' : '0'}:${detailsLayoutKey}`
@@ -390,6 +394,7 @@ export function useMainApp(gw: GatewayClient) {
}),
virtualRows[index]!.msg
),
thinkingExpanded: historyThinkingExpanded,
thinkingVisible: thinkingDetailsVisible,
toolsVisible: toolsDetailsVisible,
userPrompt: ui.theme.brand.prompt,
@@ -399,6 +404,7 @@ export function useMainApp(gw: GatewayClient) {
cols,
detailsVisible,
firstUserIdx,
historyThinkingExpanded,
thinkingDetailsVisible,
toolsDetailsVisible,
ui.compact,

View File

@@ -34,6 +34,7 @@ export const MessageLine = memo(function MessageLine({
detailsMode = 'collapsed',
detailsModeCommandOverride = false,
isStreaming = false,
liveDetails = false,
msg,
prev,
sections,
@@ -81,6 +82,7 @@ export const MessageLine = memo(function MessageLine({
<ToolTrail
commandOverride={detailsModeCommandOverride}
detailsMode={detailsMode}
preferExpandedThinking={liveDetails}
reasoning={thinking}
reasoningAlwaysVisible={msg.isMoaReference}
reasoningTokens={msg.thinkingTokens}
@@ -245,6 +247,7 @@ export const MessageLine = memo(function MessageLine({
<ToolTrail
commandOverride={detailsModeCommandOverride}
detailsMode={detailsMode}
preferExpandedThinking={liveDetails}
reasoning={thinking}
reasoningTokens={msg.thinkingTokens}
sections={sections}
@@ -300,6 +303,7 @@ interface MessageLineProps {
detailsMode?: DetailsMode
detailsModeCommandOverride?: boolean
isStreaming?: boolean
liveDetails?: boolean
msg: Msg
// The block rendered directly above this one. Drives the group-boundary
// lead gap (see domain/blockLayout.ts::hasLeadGap). Undefined at the top of

View File

@@ -76,6 +76,7 @@ export const StreamingAssistant = memo(function StreamingAssistant({
detailsModeCommandOverride={detailsModeCommandOverride}
isStreaming={block.isStreaming}
key={block.key}
liveDetails
msg={block.msg}
prev={prev}
sections={sections}

View File

@@ -678,6 +678,7 @@ export const ToolTrail = memo(function ToolTrail({
commandOverride = false,
detailsMode = 'collapsed',
outcome = '',
preferExpandedThinking = false,
reasoningActive = false,
reasoning = '',
reasoningAlwaysVisible = false,
@@ -695,6 +696,7 @@ export const ToolTrail = memo(function ToolTrail({
commandOverride?: boolean
detailsMode?: DetailsMode
outcome?: string
preferExpandedThinking?: boolean
reasoningActive?: boolean
reasoning?: string
// MoA reference blocks (see Msg.isMoaReference) stay visible even when
@@ -721,6 +723,9 @@ export const ToolTrail = memo(function ToolTrail({
[commandOverride, detailsMode, sections]
)
const thinkingDefaultExpanded =
visible.thinking === 'expanded' && (preferExpandedThinking || commandOverride || sections?.thinking === 'expanded')
const [now, setNow] = useState(() => Date.now())
// Local toggles own the open state once mounted. Init from the resolved
// section visibility so default-expanded sections (thinking/tools) render
@@ -735,7 +740,7 @@ export const ToolTrail = memo(function ToolTrail({
// label. This only affects the initial mount value; the re-sync effect
// below deliberately does NOT re-apply it, so a manual collapse still
// sticks (see the no-OR-at-effect-time warning above, #14968).
const [openThinking, setOpenThinking] = useState(visible.thinking === 'expanded' || reasoningAlwaysVisible)
const [openThinking, setOpenThinking] = useState(thinkingDefaultExpanded || reasoningAlwaysVisible)
const [openTools, setOpenTools] = useState(visible.tools === 'expanded')
const [openSubagents, setOpenSubagents] = useState(visible.subagents === 'expanded')
const [deepSubagents, setDeepSubagents] = useState(visible.subagents === 'expanded')
@@ -766,11 +771,11 @@ export const ToolTrail = memo(function ToolTrail({
return
}
setOpenThinking(visible.thinking === 'expanded')
setOpenThinking(thinkingDefaultExpanded)
setOpenTools(visible.tools === 'expanded')
setOpenSubagents(visible.subagents === 'expanded')
setOpenMeta(visible.activity === 'expanded')
}, [visible])
}, [thinkingDefaultExpanded, visible])
const cot = useMemo(() => thinkingPreview(reasoning, 'full', THINKING_COT_MAX), [reasoning])

View File

@@ -74,6 +74,7 @@ export const estimatedMsgHeight = (
details,
leadGap = false,
thinkingVisible = details,
thinkingExpanded = thinkingVisible,
toolsVisible = details,
userPrompt = '',
withSeparator = false
@@ -81,6 +82,7 @@ export const estimatedMsgHeight = (
compact: boolean
details: boolean
leadGap?: boolean
thinkingExpanded?: boolean
thinkingVisible?: boolean
toolsVisible?: boolean
userPrompt?: string
@@ -124,7 +126,7 @@ export const estimatedMsgHeight = (
if (hasVisibleDetails) {
h +=
(hasVisibleTools ? (msg.tools?.length ?? 0) : 0) +
(hasVisibleThinking ? wrappedLines(msg.thinking ?? '', bodyWidth) : 0)
(hasVisibleThinking ? (thinkingExpanded ? wrappedLines(msg.thinking ?? '', bodyWidth) : 1) : 0)
if (msg.role === 'assistant' && /\S/.test(msg.text)) {
h += 2