fix(tui): chevrons re-toggle even when section default is expanded

Recovers the manual click on the details accordion: with #14968's new
SECTION_DEFAULTS (thinking/tools start `expanded`), every panel render
was OR-ing the local open toggle against `visible.X === 'expanded'`.
That pinned `open=true` for the default-expanded sections, so clicking
the chevron flipped the local state but the panel never collapsed.

Local toggle is now the sole source of truth at render time; the
useState init still seeds from the resolved visibility (so first paint
is correct) and the existing useEffect still re-syncs when the user
mutates visibility at runtime via `/details`.

Same OR-lock cleared inside SubagentAccordion (`showChildren ||
openX`) — pre-existing but the same shape, so expand-all on the
spawn tree no longer makes inner sections un-collapsible either.
This commit is contained in:
Brooklyn Nicholson 2026-04-24 12:22:20 -05:00
parent 62c14d5513
commit f5e2a77a80

View file

@ -392,6 +392,9 @@ function SubagentAccordion({
const hasTools = item.tools.length > 0 const hasTools = item.tools.length > 0
const noteRows = [...(summary ? [summary] : []), ...item.notes] const noteRows = [...(summary ? [summary] : []), ...item.notes]
const hasNotes = noteRows.length > 0 const hasNotes = noteRows.length > 0
// `showChildren` only seeds the recursive `expanded` prop for nested
// subagents — it MUST NOT be OR-ed into the local section toggles, or
// expand-all permanently locks the inner chevrons open.
const showChildren = expanded || deep const showChildren = expanded || deep
const noteColor = statusTone === 'error' ? t.color.error : statusTone === 'warn' ? t.color.warn : t.color.dim const noteColor = statusTone === 'error' ? t.color.error : statusTone === 'warn' ? t.color.warn : t.color.dim
@ -414,13 +417,13 @@ function SubagentAccordion({
setOpenThinking(v => !v) setOpenThinking(v => !v)
} }
}} }}
open={showChildren || openThinking} open={openThinking}
t={t} t={t}
title="Thinking" title="Thinking"
/> />
), ),
key: 'thinking', key: 'thinking',
open: showChildren || openThinking, open: openThinking,
render: childRails => ( render: childRails => (
<Thinking <Thinking
active={item.status === 'running'} active={item.status === 'running'}
@ -447,13 +450,13 @@ function SubagentAccordion({
setOpenTools(v => !v) setOpenTools(v => !v)
} }
}} }}
open={showChildren || openTools} open={openTools}
t={t} t={t}
title="Tool calls" title="Tool calls"
/> />
), ),
key: 'tools', key: 'tools',
open: showChildren || openTools, open: openTools,
render: childRails => ( render: childRails => (
<Box flexDirection="column"> <Box flexDirection="column">
{item.tools.map((line, index) => ( {item.tools.map((line, index) => (
@ -488,14 +491,14 @@ function SubagentAccordion({
setOpenNotes(v => !v) setOpenNotes(v => !v)
} }
}} }}
open={showChildren || openNotes} open={openNotes}
t={t} t={t}
title="Progress" title="Progress"
tone={statusTone} tone={statusTone}
/> />
), ),
key: 'notes', key: 'notes',
open: showChildren || openNotes, open: openNotes,
render: childRails => ( render: childRails => (
<Box flexDirection="column"> <Box flexDirection="column">
{noteRows.map((line, index) => ( {noteRows.map((line, index) => (
@ -528,14 +531,14 @@ function SubagentAccordion({
setOpenKids(v => !v) setOpenKids(v => !v)
} }
}} }}
open={showChildren || openKids} open={openKids}
suffix={`d${item.depth + 1} · ${aggregate.descendantCount} total`} suffix={`d${item.depth + 1} · ${aggregate.descendantCount} total`}
t={t} t={t}
title="Spawned" title="Spawned"
/> />
), ),
key: 'subagents', key: 'subagents',
open: showChildren || openKids, open: openKids,
render: childRails => ( render: childRails => (
<Box flexDirection="column"> <Box flexDirection="column">
{children.map((child, i) => ( {children.map((child, i) => (
@ -718,6 +721,13 @@ export const ToolTrail = memo(function ToolTrail({
) )
const [now, setNow] = useState(() => Date.now()) 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
// open on first paint; the useEffect below re-syncs when the user mutates
// visibility at runtime via /details. NEVER OR these against
// `visible.X === 'expanded'` at render time — that locks the panel open
// and silently breaks manual chevron clicks for default-expanded
// sections (regression caught after #14968).
const [openThinking, setOpenThinking] = useState(visible.thinking === 'expanded') const [openThinking, setOpenThinking] = useState(visible.thinking === 'expanded')
const [openTools, setOpenTools] = useState(visible.tools === 'expanded') const [openTools, setOpenTools] = useState(visible.tools === 'expanded')
const [openSubagents, setOpenSubagents] = useState(visible.subagents === 'expanded') const [openSubagents, setOpenSubagents] = useState(visible.subagents === 'expanded')
@ -960,7 +970,7 @@ export const ToolTrail = memo(function ToolTrail({
}} }}
> >
<Text color={t.color.dim} dim={!thinkingLive}> <Text color={t.color.dim} dim={!thinkingLive}>
<Text color={t.color.amber}>{visible.thinking === 'expanded' || openThinking ? '▾ ' : '▸ '}</Text> <Text color={t.color.amber}>{openThinking ? '▾ ' : '▸ '}</Text>
{thinkingLive ? ( {thinkingLive ? (
<Text bold color={t.color.cornsilk}> <Text bold color={t.color.cornsilk}>
Thinking Thinking
@ -980,7 +990,7 @@ export const ToolTrail = memo(function ToolTrail({
</Box> </Box>
), ),
key: 'thinking', key: 'thinking',
open: visible.thinking === 'expanded' || openThinking, open: openThinking,
render: rails => ( render: rails => (
<Thinking <Thinking
active={reasoningActive} active={reasoningActive}
@ -1007,14 +1017,14 @@ export const ToolTrail = memo(function ToolTrail({
setOpenTools(v => !v) setOpenTools(v => !v)
} }
}} }}
open={visible.tools === 'expanded' || openTools} open={openTools}
suffix={toolTokensLabel} suffix={toolTokensLabel}
t={t} t={t}
title="Tool calls" title="Tool calls"
/> />
), ),
key: 'tools', key: 'tools',
open: visible.tools === 'expanded' || openTools, open: openTools,
render: rails => ( render: rails => (
<Box flexDirection="column"> <Box flexDirection="column">
{groups.map((group, index) => { {groups.map((group, index) => {
@ -1072,14 +1082,14 @@ export const ToolTrail = memo(function ToolTrail({
setDeepSubagents(false) setDeepSubagents(false)
} }
}} }}
open={visible.subagents === 'expanded' || openSubagents} open={openSubagents}
suffix={suffix} suffix={suffix}
t={t} t={t}
title="Spawn tree" title="Spawn tree"
/> />
), ),
key: 'subagents', key: 'subagents',
open: visible.subagents === 'expanded' || openSubagents, open: openSubagents,
render: renderSubagentList render: renderSubagentList
}) })
} }
@ -1096,14 +1106,14 @@ export const ToolTrail = memo(function ToolTrail({
setOpenMeta(v => !v) setOpenMeta(v => !v)
} }
}} }}
open={visible.activity === 'expanded' || openMeta} open={openMeta}
t={t} t={t}
title="Activity" title="Activity"
tone={metaTone} tone={metaTone}
/> />
), ),
key: 'meta', key: 'meta',
open: visible.activity === 'expanded' || openMeta, open: openMeta,
render: rails => ( render: rails => (
<Box flexDirection="column"> <Box flexDirection="column">
{meta.map((row, index) => ( {meta.map((row, index) => (