feat(tui): per-section visibility for the details accordion

Adds optional per-section overrides on top of the existing global
details_mode (hidden | collapsed | expanded).  Lets users keep the
accordion collapsed by default while auto-expanding tools, or hide the
activity panel entirely without touching thinking/tools/subagents.

Config (~/.hermes/config.yaml):

    display:
      details_mode: collapsed
      sections:
        thinking: expanded
        tools:    expanded
        activity: hidden

Slash command:

  /details                              show current global + overrides
  /details [hidden|collapsed|expanded]  set global mode (existing)
  /details <section> <mode|reset>       per-section override (new)
  /details <section> reset              clear override

Sections: thinking, tools, subagents, activity.

Implementation:

- ui-tui/src/types.ts             SectionName + SectionVisibility
- ui-tui/src/domain/details.ts    parseSectionMode / resolveSections /
                                  sectionMode + SECTION_NAMES
- ui-tui/src/app/uiStore.ts +
  app/interfaces.ts +
  app/useConfigSync.ts            sections threaded into UiState
- ui-tui/src/components/
  thinking.tsx                    ToolTrail consults per-section mode for
                                  hidden/expanded behaviour; expandAll
                                  skips hidden sections; floating-alert
                                  fallback respects activity:hidden
- ui-tui/src/components/
  messageLine.tsx + appLayout.tsx pass sections through render tree
- ui-tui/src/app/slash/
  commands/core.ts                /details <section> <mode|reset> syntax
- tui_gateway/server.py           config.set details_mode.<section>
                                  writes to display.sections.<section>
                                  (empty value clears the override)
- website/docs/user-guide/tui.md  documented

Tests: 14 new (4 domain, 4 useConfigSync, 3 slash, 3 gateway).
Total: 269/269 vitest, all gateway tests pass.
This commit is contained in:
Brooklyn Nicholson 2026-04-24 02:34:32 -05:00
parent 6051fba9dc
commit 78481ac124
16 changed files with 478 additions and 70 deletions

View file

@ -1,7 +1,7 @@
import { NO_CONFIRM_DESTRUCTIVE } from '../../../config/env.js'
import { dailyFortune, randomFortune } from '../../../content/fortunes.js'
import { HOTKEYS } from '../../../content/hotkeys.js'
import { nextDetailsMode, parseDetailsMode } from '../../../domain/details.js'
import { isSectionName, nextDetailsMode, parseDetailsMode, SECTION_NAMES } from '../../../domain/details.js'
import type {
ConfigGetValueResponse,
ConfigSetResponse,
@ -10,7 +10,7 @@ import type {
} from '../../../gatewayTypes.js'
import { writeOsc52Clipboard } from '../../../lib/osc52.js'
import { configureDetectedTerminalKeybindings, configureTerminalKeybindings } from '../../../lib/terminalSetup.js'
import type { DetailsMode, Msg, PanelSection } from '../../../types.js'
import type { DetailsMode, Msg, PanelSection, SectionName } from '../../../types.js'
import type { StatusBarMode } from '../../interfaces.js'
import { patchOverlayState } from '../../overlayStore.js'
import { patchUiState } from '../../uiStore.js'
@ -57,7 +57,8 @@ export const coreCommands: SlashCommand[] = [
sections.push(
{
rows: [
['/details [hidden|collapsed|expanded|cycle]', 'set agent detail visibility mode'],
['/details [hidden|collapsed|expanded|cycle]', 'set global agent detail visibility mode'],
['/details <section> [hidden|collapsed|expanded|reset]', 'override one section (thinking/tools/subagents/activity)'],
['/fortune [random|daily]', 'show a random or daily local fortune']
],
title: 'TUI'
@ -140,7 +141,7 @@ export const coreCommands: SlashCommand[] = [
{
aliases: ['detail'],
help: 'control agent detail visibility',
help: 'control agent detail visibility (global or per-section)',
name: 'details',
run: (arg, ctx) => {
const { gateway, transcript, ui } = ctx
@ -154,9 +155,14 @@ export const coreCommands: SlashCommand[] = [
}
const mode = parseDetailsMode(r?.value) ?? ui.detailsMode
patchUiState({ detailsMode: mode })
transcript.sys(`details: ${mode}`)
const overrides = SECTION_NAMES
.filter(s => ui.sections[s])
.map(s => `${s}=${ui.sections[s]}`)
.join(' ')
transcript.sys(`details: ${mode}${overrides ? ` (${overrides})` : ''}`)
})
.catch(() => {
if (!ctx.stale()) {
@ -167,10 +173,46 @@ export const coreCommands: SlashCommand[] = [
return
}
const mode = arg.trim().toLowerCase()
const tokens = arg.trim().toLowerCase().split(/\s+/)
// Per-section override: `/details <section> <mode>`
if (tokens.length >= 2 && isSectionName(tokens[0])) {
const section = tokens[0] as SectionName
const action = tokens[1] ?? ''
if (action === 'reset' || action === 'clear' || action === 'default') {
const { [section]: _drop, ...rest } = ui.sections
patchUiState({ sections: rest })
gateway
.rpc<ConfigSetResponse>('config.set', { key: `details_mode.${section}`, value: '' })
.catch(() => {})
transcript.sys(`details ${section}: reset`)
return
}
const sectionMode = parseDetailsMode(action)
if (!sectionMode) {
return transcript.sys('usage: /details <section> [hidden|collapsed|expanded|reset]')
}
patchUiState({ sections: { ...ui.sections, [section]: sectionMode } })
gateway
.rpc<ConfigSetResponse>('config.set', { key: `details_mode.${section}`, value: sectionMode })
.catch(() => {})
transcript.sys(`details ${section}: ${sectionMode}`)
return
}
// Global mode (existing behavior).
const mode = tokens[0] ?? ''
if (!DETAIL_MODES.has(mode)) {
return transcript.sys('usage: /details [hidden|collapsed|expanded|cycle]')
return transcript.sys(
'usage: /details [hidden|collapsed|expanded|cycle] or /details <section> [hidden|collapsed|expanded|reset]'
)
}
const next = mode === 'cycle' || mode === 'toggle' ? nextDetailsMode(ui.detailsMode) : (mode as DetailsMode)