fix: keep historical thinking collapsed in tui
(cherry picked from commit 354999388a4e465b6b7efa46cdc61d8d5b22b35f)
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user