mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
refactor(tui): /clean pass on per-section visibility plumbing
- domain/details: extract `norm()`, fold parseDetailsMode + resolveSections into terser functional form, reject array values for resolveSections - slash /details: destructure tokens, factor reset/mode into one dispatch, drop DETAIL_MODES set + DetailsMode/SectionName imports (parseDetailsMode + isSectionName narrow + return), centralize usage strings - ToolTrail: collapse 4 separate xxxSection vars into one memoized `visible` map; effect deps stabilize on the memo identity instead of 4 primitives
This commit is contained in:
@@ -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, SectionName } from '../../../types.js'
|
||||
import type { Msg, PanelSection } from '../../../types.js'
|
||||
import type { StatusBarMode } from '../../interfaces.js'
|
||||
import { patchOverlayState } from '../../overlayStore.js'
|
||||
import { patchUiState } from '../../uiStore.js'
|
||||
@@ -38,7 +38,11 @@ const flagFromArg = (arg: string, current: boolean): boolean | null => {
|
||||
return null
|
||||
}
|
||||
|
||||
const DETAIL_MODES = new Set(['collapsed', 'cycle', 'expanded', 'hidden', 'toggle'])
|
||||
const RESET_WORDS = new Set(['reset', 'clear', 'default'])
|
||||
const CYCLE_WORDS = new Set(['cycle', 'toggle'])
|
||||
const DETAILS_USAGE =
|
||||
'usage: /details [hidden|collapsed|expanded|cycle] or /details <section> [hidden|collapsed|expanded|reset]'
|
||||
const DETAILS_SECTION_USAGE = 'usage: /details <section> [hidden|collapsed|expanded|reset]'
|
||||
|
||||
export const coreCommands: SlashCommand[] = [
|
||||
{
|
||||
@@ -150,9 +154,7 @@ export const coreCommands: SlashCommand[] = [
|
||||
gateway
|
||||
.rpc<ConfigGetValueResponse>('config.get', { key: 'details_mode' })
|
||||
.then(r => {
|
||||
if (ctx.stale()) {
|
||||
return
|
||||
}
|
||||
if (ctx.stale()) return
|
||||
|
||||
const mode = parseDetailsMode(r?.value) ?? ui.detailsMode
|
||||
patchUiState({ detailsMode: mode })
|
||||
@@ -164,59 +166,38 @@ export const coreCommands: SlashCommand[] = [
|
||||
|
||||
transcript.sys(`details: ${mode}${overrides ? ` (${overrides})` : ''}`)
|
||||
})
|
||||
.catch(() => {
|
||||
if (!ctx.stale()) {
|
||||
transcript.sys(`details: ${ui.detailsMode}`)
|
||||
}
|
||||
})
|
||||
.catch(() => !ctx.stale() && transcript.sys(`details: ${ui.detailsMode}`))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const tokens = arg.trim().toLowerCase().split(/\s+/)
|
||||
const [first, second] = 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 (second && isSectionName(first)) {
|
||||
const reset = RESET_WORDS.has(second)
|
||||
const mode = reset ? null : parseDetailsMode(second)
|
||||
|
||||
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
|
||||
if (!reset && !mode) {
|
||||
return transcript.sys(DETAILS_SECTION_USAGE)
|
||||
}
|
||||
|
||||
const sectionMode = parseDetailsMode(action)
|
||||
const { [first]: _drop, ...rest } = ui.sections
|
||||
|
||||
if (!sectionMode) {
|
||||
return transcript.sys('usage: /details <section> [hidden|collapsed|expanded|reset]')
|
||||
}
|
||||
|
||||
patchUiState({ sections: { ...ui.sections, [section]: sectionMode } })
|
||||
patchUiState({ sections: mode ? { ...rest, [first]: mode } : rest })
|
||||
gateway
|
||||
.rpc<ConfigSetResponse>('config.set', { key: `details_mode.${section}`, value: sectionMode })
|
||||
.rpc<ConfigSetResponse>('config.set', { key: `details_mode.${first}`, value: mode ?? '' })
|
||||
.catch(() => {})
|
||||
transcript.sys(`details ${section}: ${sectionMode}`)
|
||||
transcript.sys(`details ${first}: ${mode ?? 'reset'}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Global mode (existing behavior).
|
||||
const mode = tokens[0] ?? ''
|
||||
const next = CYCLE_WORDS.has(first ?? '') ? nextDetailsMode(ui.detailsMode) : parseDetailsMode(first)
|
||||
|
||||
if (!DETAIL_MODES.has(mode)) {
|
||||
return transcript.sys(
|
||||
'usage: /details [hidden|collapsed|expanded|cycle] or /details <section> [hidden|collapsed|expanded|reset]'
|
||||
)
|
||||
if (!next) {
|
||||
return transcript.sys(DETAILS_USAGE)
|
||||
}
|
||||
|
||||
const next = mode === 'cycle' || mode === 'toggle' ? nextDetailsMode(ui.detailsMode) : (mode as DetailsMode)
|
||||
|
||||
patchUiState({ detailsMode: next })
|
||||
gateway.rpc<ConfigSetResponse>('config.set', { key: 'details_mode', value: next }).catch(() => {})
|
||||
transcript.sys(`details: ${next}`)
|
||||
|
||||
@@ -707,34 +707,39 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
trail?: string[]
|
||||
activity?: ActivityItem[]
|
||||
}) {
|
||||
const thinkingSection = sectionMode('thinking', detailsMode, sections)
|
||||
const toolsSection = sectionMode('tools', detailsMode, sections)
|
||||
const subagentsSection = sectionMode('subagents', detailsMode, sections)
|
||||
const activitySection = sectionMode('activity', detailsMode, sections)
|
||||
const visible = useMemo(
|
||||
() => ({
|
||||
thinking: sectionMode('thinking', detailsMode, sections),
|
||||
tools: sectionMode('tools', detailsMode, sections),
|
||||
subagents: sectionMode('subagents', detailsMode, sections),
|
||||
activity: sectionMode('activity', detailsMode, sections)
|
||||
}),
|
||||
[detailsMode, sections]
|
||||
)
|
||||
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
const [openThinking, setOpenThinking] = useState(thinkingSection === 'expanded')
|
||||
const [openTools, setOpenTools] = useState(toolsSection === 'expanded')
|
||||
const [openSubagents, setOpenSubagents] = useState(subagentsSection === 'expanded')
|
||||
const [deepSubagents, setDeepSubagents] = useState(subagentsSection === 'expanded')
|
||||
const [openMeta, setOpenMeta] = useState(activitySection === 'expanded')
|
||||
const [openThinking, setOpenThinking] = useState(visible.thinking === 'expanded')
|
||||
const [openTools, setOpenTools] = useState(visible.tools === 'expanded')
|
||||
const [openSubagents, setOpenSubagents] = useState(visible.subagents === 'expanded')
|
||||
const [deepSubagents, setDeepSubagents] = useState(visible.subagents === 'expanded')
|
||||
const [openMeta, setOpenMeta] = useState(visible.activity === 'expanded')
|
||||
|
||||
useEffect(() => {
|
||||
if (!tools.length || (toolsSection !== 'expanded' && !openTools)) {
|
||||
if (!tools.length || (visible.tools !== 'expanded' && !openTools)) {
|
||||
return
|
||||
}
|
||||
|
||||
const id = setInterval(() => setNow(Date.now()), 500)
|
||||
|
||||
return () => clearInterval(id)
|
||||
}, [toolsSection, openTools, tools.length])
|
||||
}, [openTools, tools.length, visible.tools])
|
||||
|
||||
useEffect(() => {
|
||||
setOpenThinking(thinkingSection === 'expanded')
|
||||
setOpenTools(toolsSection === 'expanded')
|
||||
setOpenSubagents(subagentsSection === 'expanded')
|
||||
setOpenMeta(activitySection === 'expanded')
|
||||
}, [thinkingSection, toolsSection, subagentsSection, activitySection])
|
||||
setOpenThinking(visible.thinking === 'expanded')
|
||||
setOpenTools(visible.tools === 'expanded')
|
||||
setOpenSubagents(visible.subagents === 'expanded')
|
||||
setOpenMeta(visible.activity === 'expanded')
|
||||
}, [visible])
|
||||
|
||||
const cot = useMemo(() => thinkingPreview(reasoning, 'full', THINKING_COT_MAX), [reasoning])
|
||||
|
||||
@@ -877,7 +882,7 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
// override means "I don't want to see meta at all", so respect it.
|
||||
|
||||
if (detailsMode === 'hidden') {
|
||||
if (activitySection === 'hidden') {
|
||||
if (visible.activity === 'hidden') {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -900,13 +905,13 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
// hidden sections stay hidden so the override is honoured.
|
||||
|
||||
const expandAll = () => {
|
||||
if (thinkingSection !== 'hidden') setOpenThinking(true)
|
||||
if (toolsSection !== 'hidden') setOpenTools(true)
|
||||
if (subagentsSection !== 'hidden') {
|
||||
if (visible.thinking !== 'hidden') setOpenThinking(true)
|
||||
if (visible.tools !== 'hidden') setOpenTools(true)
|
||||
if (visible.subagents !== 'hidden') {
|
||||
setOpenSubagents(true)
|
||||
setDeepSubagents(true)
|
||||
}
|
||||
if (activitySection !== 'hidden') setOpenMeta(true)
|
||||
if (visible.activity !== 'hidden') setOpenMeta(true)
|
||||
}
|
||||
|
||||
const metaTone: 'dim' | 'error' | 'warn' = activity.some(i => i.tone === 'error')
|
||||
@@ -920,7 +925,7 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
{spawnTree.map((node, index) => (
|
||||
<SubagentAccordion
|
||||
branch={index === spawnTree.length - 1 ? 'last' : 'mid'}
|
||||
expanded={subagentsSection === 'expanded' || deepSubagents}
|
||||
expanded={visible.subagents === 'expanded' || deepSubagents}
|
||||
key={node.item.id}
|
||||
node={node}
|
||||
peak={spawnPeak}
|
||||
@@ -938,7 +943,7 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
render: (rails: boolean[]) => ReactNode
|
||||
}[] = []
|
||||
|
||||
if (hasThinking && thinkingSection !== 'hidden') {
|
||||
if (hasThinking && visible.thinking !== 'hidden') {
|
||||
panels.push({
|
||||
header: (
|
||||
<Box
|
||||
@@ -951,7 +956,7 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
}}
|
||||
>
|
||||
<Text color={t.color.dim} dim={!thinkingLive}>
|
||||
<Text color={t.color.amber}>{thinkingSection === 'expanded' || openThinking ? '▾ ' : '▸ '}</Text>
|
||||
<Text color={t.color.amber}>{visible.thinking === 'expanded' || openThinking ? '▾ ' : '▸ '}</Text>
|
||||
{thinkingLive ? (
|
||||
<Text bold color={t.color.cornsilk}>
|
||||
Thinking
|
||||
@@ -971,7 +976,7 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
</Box>
|
||||
),
|
||||
key: 'thinking',
|
||||
open: thinkingSection === 'expanded' || openThinking,
|
||||
open: visible.thinking === 'expanded' || openThinking,
|
||||
render: rails => (
|
||||
<Thinking
|
||||
active={reasoningActive}
|
||||
@@ -986,7 +991,7 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
})
|
||||
}
|
||||
|
||||
if (hasTools && toolsSection !== 'hidden') {
|
||||
if (hasTools && visible.tools !== 'hidden') {
|
||||
panels.push({
|
||||
header: (
|
||||
<Chevron
|
||||
@@ -998,14 +1003,14 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
setOpenTools(v => !v)
|
||||
}
|
||||
}}
|
||||
open={toolsSection === 'expanded' || openTools}
|
||||
open={visible.tools === 'expanded' || openTools}
|
||||
suffix={toolTokensLabel}
|
||||
t={t}
|
||||
title="Tool calls"
|
||||
/>
|
||||
),
|
||||
key: 'tools',
|
||||
open: toolsSection === 'expanded' || openTools,
|
||||
open: visible.tools === 'expanded' || openTools,
|
||||
render: rails => (
|
||||
<Box flexDirection="column">
|
||||
{groups.map((group, index) => {
|
||||
@@ -1045,7 +1050,7 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
})
|
||||
}
|
||||
|
||||
if (hasSubagents && !inlineDelegateKey && subagentsSection !== 'hidden') {
|
||||
if (hasSubagents && !inlineDelegateKey && visible.subagents !== 'hidden') {
|
||||
// Spark + summary give a one-line read on the branch shape before
|
||||
// opening the subtree. `/agents` opens the full-screen audit overlay.
|
||||
const suffix = spawnSpark ? `${spawnSummaryLabel} ${spawnSpark} (/agents)` : `${spawnSummaryLabel} (/agents)`
|
||||
@@ -1063,19 +1068,19 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
setDeepSubagents(false)
|
||||
}
|
||||
}}
|
||||
open={subagentsSection === 'expanded' || openSubagents}
|
||||
open={visible.subagents === 'expanded' || openSubagents}
|
||||
suffix={suffix}
|
||||
t={t}
|
||||
title="Spawn tree"
|
||||
/>
|
||||
),
|
||||
key: 'subagents',
|
||||
open: subagentsSection === 'expanded' || openSubagents,
|
||||
open: visible.subagents === 'expanded' || openSubagents,
|
||||
render: renderSubagentList
|
||||
})
|
||||
}
|
||||
|
||||
if (hasMeta && activitySection !== 'hidden') {
|
||||
if (hasMeta && visible.activity !== 'hidden') {
|
||||
panels.push({
|
||||
header: (
|
||||
<Chevron
|
||||
@@ -1087,14 +1092,14 @@ export const ToolTrail = memo(function ToolTrail({
|
||||
setOpenMeta(v => !v)
|
||||
}
|
||||
}}
|
||||
open={activitySection === 'expanded' || openMeta}
|
||||
open={visible.activity === 'expanded' || openMeta}
|
||||
t={t}
|
||||
title="Activity"
|
||||
tone={metaTone}
|
||||
/>
|
||||
),
|
||||
key: 'meta',
|
||||
open: activitySection === 'expanded' || openMeta,
|
||||
open: visible.activity === 'expanded' || openMeta,
|
||||
render: rails => (
|
||||
<Box flexDirection="column">
|
||||
{meta.map((row, index) => (
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import type { DetailsMode, SectionName, SectionVisibility } from '../types.js'
|
||||
|
||||
const MODES = ['hidden', 'collapsed', 'expanded'] as const
|
||||
export const SECTION_NAMES: readonly SectionName[] = ['thinking', 'tools', 'subagents', 'activity']
|
||||
|
||||
export const SECTION_NAMES = ['thinking', 'tools', 'subagents', 'activity'] as const
|
||||
|
||||
// Activity panel = ambient meta (gateway hints, terminal-parity nudges,
|
||||
// background-process notifications). Hidden out of the box because tool
|
||||
// failures already render inline on the failing tool row — the panel itself
|
||||
// is noise for typical use. Opt back in via `display.sections.activity` or
|
||||
// `/details activity collapsed`.
|
||||
const SECTION_DEFAULTS: SectionVisibility = { activity: 'hidden' }
|
||||
|
||||
const THINKING_FALLBACK: Record<string, DetailsMode> = {
|
||||
collapsed: 'collapsed',
|
||||
@@ -9,66 +17,36 @@ const THINKING_FALLBACK: Record<string, DetailsMode> = {
|
||||
truncated: 'collapsed'
|
||||
}
|
||||
|
||||
export const parseDetailsMode = (v: unknown): DetailsMode | null => {
|
||||
const s = typeof v === 'string' ? v.trim().toLowerCase() : ''
|
||||
const norm = (v: unknown) => String(v ?? '').trim().toLowerCase()
|
||||
|
||||
return MODES.find(m => m === s) ?? null
|
||||
}
|
||||
export const parseDetailsMode = (v: unknown): DetailsMode | null =>
|
||||
MODES.find(m => m === norm(v)) ?? null
|
||||
|
||||
export const isSectionName = (v: unknown): v is SectionName =>
|
||||
typeof v === 'string' && (SECTION_NAMES as readonly string[]).includes(v)
|
||||
|
||||
export const resolveDetailsMode = (d?: { details_mode?: unknown; thinking_mode?: unknown } | null): DetailsMode =>
|
||||
parseDetailsMode(d?.details_mode) ??
|
||||
THINKING_FALLBACK[
|
||||
String(d?.thinking_mode ?? '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
] ??
|
||||
'collapsed'
|
||||
parseDetailsMode(d?.details_mode) ?? THINKING_FALLBACK[norm(d?.thinking_mode)] ?? 'collapsed'
|
||||
|
||||
// Build a SectionVisibility from a free-form `display.sections` config blob.
|
||||
// Skips keys that aren't recognized section names or don't parse to a valid
|
||||
// mode — partial overrides are intentional, missing keys fall through to the
|
||||
// global details_mode at render time.
|
||||
export const resolveSections = (raw: unknown): SectionVisibility => {
|
||||
const out: SectionVisibility = {}
|
||||
// Build SectionVisibility from a free-form blob. Unknown section names and
|
||||
// invalid modes are dropped silently — partial overrides are intentional, so
|
||||
// missing keys fall through to SECTION_DEFAULTS / global at lookup time.
|
||||
export const resolveSections = (raw: unknown): SectionVisibility =>
|
||||
raw && typeof raw === 'object' && !Array.isArray(raw)
|
||||
? (Object.fromEntries(
|
||||
Object.entries(raw as Record<string, unknown>)
|
||||
.map(([k, v]) => [k, parseDetailsMode(v)] as const)
|
||||
.filter(([k, m]) => !!m && isSectionName(k))
|
||||
) as SectionVisibility)
|
||||
: {}
|
||||
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return out
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
|
||||
const mode = parseDetailsMode(v)
|
||||
|
||||
if (mode && isSectionName(k)) {
|
||||
out[k] = mode
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Built-in per-section defaults applied when the user has no explicit
|
||||
// override. The activity panel (gateway hints, terminal-parity nudges,
|
||||
// background-process notifications) is hidden out of the box — it's noise
|
||||
// for the typical day-to-day user, who only cares about thinking + tools +
|
||||
// streamed content. Tool failures still surface inline on the failing tool
|
||||
// row; this default only suppresses the ambient meta feed.
|
||||
//
|
||||
// Opt back in with `display.sections.activity: collapsed` (under chevron)
|
||||
// or `expanded` (always open) in `~/.hermes/config.yaml`, or live with
|
||||
// `/details activity collapsed`.
|
||||
const SECTION_DEFAULTS: SectionVisibility = { activity: 'hidden' }
|
||||
|
||||
// Resolve the effective mode for one section: explicit override wins,
|
||||
// then the SECTION_DEFAULTS fallback, then the global details_mode.
|
||||
// Single source of truth — every render site that needs to know "is this
|
||||
// section open by default" calls this.
|
||||
// Effective mode for one section: explicit override → SECTION_DEFAULTS → global.
|
||||
// Single source of truth for "is this section open by default / rendered at all".
|
||||
export const sectionMode = (
|
||||
name: SectionName,
|
||||
global: DetailsMode,
|
||||
sections?: SectionVisibility
|
||||
): DetailsMode => sections?.[name] ?? SECTION_DEFAULTS[name] ?? global
|
||||
|
||||
export const nextDetailsMode = (m: DetailsMode): DetailsMode => MODES[(MODES.indexOf(m) + 1) % MODES.length]!
|
||||
export const nextDetailsMode = (m: DetailsMode): DetailsMode =>
|
||||
MODES[(MODES.indexOf(m) + 1) % MODES.length]!
|
||||
|
||||
Reference in New Issue
Block a user