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>