diff --git a/ui-tui/src/components/messageLine.tsx b/ui-tui/src/components/messageLine.tsx index f2c0241ff..a73a4f01d 100644 --- a/ui-tui/src/components/messageLine.tsx +++ b/ui-tui/src/components/messageLine.tsx @@ -1,7 +1,7 @@ import { Ansi, Box, NoSelect, Text } from '@hermes/ink' import { memo } from 'react' -import { SECTION_NAMES, sectionMode } from '../domain/details.js' +import { sectionMode } from '../domain/details.js' import { LONG_MSG } from '../config/limits.js' import { userDisplay } from '../domain/messages.js' import { ROLE } from '../domain/roles.js' @@ -21,13 +21,19 @@ export const MessageLine = memo(function MessageLine({ sections, t }: MessageLineProps) { - if (msg.kind === 'trail' && msg.tools?.length) { - // Per-section overrides win over the global mode, so don't pre-empt on - // `detailsMode === 'hidden'` — only skip when EVERY section is hidden, - // matching ToolTrail's own internal short-circuit. - const anyVisible = SECTION_NAMES.some(s => sectionMode(s, detailsMode, sections) !== 'hidden') + // Per-section overrides win over the global mode, so resolve each section + // we might consume here once and gate visibility on the *content-bearing* + // sections only — never on the global mode. A `trail` message feeds Tool + // calls + Activity; an assistant message with thinking/tools metadata + // feeds Thinking + Tool calls. Gating on every section would let + // `thinking` (expanded by default) keep an empty wrapper alive when only + // `tools` is hidden — exactly the empty-Box bug Copilot caught. + const thinkingMode = sectionMode('thinking', detailsMode, sections) + const toolsMode = sectionMode('tools', detailsMode, sections) + const activityMode = sectionMode('activity', detailsMode, sections) - return anyVisible ? ( + if (msg.kind === 'trail' && msg.tools?.length) { + return toolsMode !== 'hidden' || activityMode !== 'hidden' ? ( @@ -56,7 +62,10 @@ export const MessageLine = memo(function MessageLine({ const { body, glyph, prefix } = ROLE[msg.role](t) const thinking = msg.thinking?.trim() ?? '' - const showDetails = detailsMode !== 'hidden' && (Boolean(msg.tools?.length) || Boolean(thinking)) + + const showDetails = + (toolsMode !== 'hidden' && Boolean(msg.tools?.length)) || + (thinkingMode !== 'hidden' && Boolean(thinking)) const content = (() => { if (msg.kind === 'slash') { diff --git a/ui-tui/src/types.ts b/ui-tui/src/types.ts index eb8053610..3fdb39b82 100644 --- a/ui-tui/src/types.ts +++ b/ui-tui/src/types.ts @@ -116,11 +116,11 @@ export type Role = 'assistant' | 'system' | 'tool' | 'user' export type DetailsMode = 'hidden' | 'collapsed' | 'expanded' export type ThinkingMode = 'collapsed' | 'truncated' | 'full' -// Per-section overrides on top of the global DetailsMode. Each missing key -// falls back to the global mode; an explicit value overrides for that one -// section only — so users can keep the accordion collapsed by default while -// auto-expanding tools, or hide the activity panel entirely without touching -// thinking/tools/subagents. +// Per-section overrides for the agent details accordion. Resolution order +// at lookup time is: explicit `display.sections.` → built-in +// SECTION_DEFAULTS → global `details_mode`. Today the built-in defaults +// expand `thinking`/`tools` and hide `activity`; `subagents` falls through +// to the global mode. Any explicit value still wins for that one section. export type SectionName = 'thinking' | 'tools' | 'subagents' | 'activity' export type SectionVisibility = Partial>