From cf3002664b109b4d1993976b0585bf952f40db74 Mon Sep 17 00:00:00 2001 From: alt-glitch Date: Fri, 12 Jun 2026 09:17:25 +0530 Subject: [PATCH] =?UTF-8?q?opentui(v6):=20HERMES=5FTUI=5FDIAGNOSTICS=20mas?= =?UTF-8?q?ter=20switch=20=E2=80=94=20gate=20/mem,=20/heapdump=20+=20windo?= =?UTF-8?q?w-stats=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regular users get zero diagnostic surface by default: /mem and /heapdump disappear from /help and completion, and invoking them prints the one-line enable hint (relaunch with HERMES_TUI_DIAGNOSTICS=1) instead of executing — an enable switch, not a secret. With the switch on, the commands work as before and HERMES_TUI_WINDOW_STATS defaults on (still individually settable either way). Full env-flag ledger (master switch / user config / dev tuning / internal plumbing) in docs/opentui-env-flags.md. 672 tests exit 0. --- docs/opentui-env-flags.md | 57 +++++++++++++++++++++ ui-opentui/src/logic/env.ts | 15 ++++++ ui-opentui/src/logic/slash.ts | 32 +++++++++--- ui-opentui/src/test/slash.test.ts | 38 ++++++++++++++ ui-opentui/src/test/utilityCommands.test.ts | 13 +++++ ui-opentui/src/view/transcript.tsx | 7 +-- 6 files changed, 152 insertions(+), 10 deletions(-) create mode 100644 docs/opentui-env-flags.md diff --git a/docs/opentui-env-flags.md b/docs/opentui-env-flags.md new file mode 100644 index 00000000000..aa5b658c0a3 --- /dev/null +++ b/docs/opentui-env-flags.md @@ -0,0 +1,57 @@ +# OpenTUI env flags — the consolidated ledger + +Every environment variable the OpenTUI TUI reads (grep-verified 2026-06-12), +classified by who should ever touch it. The design rule shipped with this doc: +**regular users see zero diagnostic surface by default; one master switch +(`HERMES_TUI_DIAGNOSTICS=1`) turns all of it on when needed.** + +## 1. The master switch + +| var | default | effect | +|---|---|---| +| `HERMES_TUI_DIAGNOSTICS` | **off** | Enables the diagnostic slash commands (`/mem`, `/heapdump`) — hidden from `/help` and completion otherwise, and invoking them while off prints the enable hint rather than executing. Also flips the *default* of `HERMES_TUI_WINDOW_STATS` to on. Not a secret — support flows are "relaunch with `HERMES_TUI_DIAGNOSTICS=1`". | + +## 2. User-facing configuration (fine to document publicly) + +| var | default | effect | +|---|---|---| +| `HERMES_TUI_ENGINE` | auto (`opentui` if Node≥26.3 + built, else `ink`) | Engine pick; also `display.tui_engine` in config.yaml. | +| `HERMES_TUI_MOUSE` | on (launcher sets it) | Mouse support (wheel scroll, selection, click-to-expand). | +| `HERMES_TUI_MAX_MESSAGES` | ceiling | Scrollback rows kept in the TUI. Can LOWER the ceiling, never raise: 3000 with windowing, 1000 with windowing off (handle-table safety). | +| `HERMES_TUI_TOOL_OUTPUT_LINES` | unlimited | Cap expanded tool-output lines (set a number to restore a cap). | +| `HERMES_TUI_COMPOSER_ROWS` | default rows | Composer height. | + +## 3. Escape hatches & tuning (dev-facing, individually settable) + +| var | default | effect | +|---|---|---| +| `HERMES_TUI_WINDOWING` | **on** | `0` = bit-exact pre-windowing renderer (every row mounts; cap clamps back to 1000). The A/B + regression escape hatch. | +| `HERMES_TUI_WINDOW_IDLE_MS` | ~1000 | Idle-measure pulse cadence (the spacer-exactness march). Test knob. | +| `HERMES_TUI_WINDOW_STATS` | = `HERMES_TUI_DIAGNOSTICS` | Exposes live/peak mounted-row counters (`globalThis.__hermesTuiWindowStats`) for bench/live-attach reads. | +| `HERMES_TUI_LOG_LEVEL` / `HERMES_TUI_LOG_FILE` | engine defaults | Logging verbosity/destination (`/logs` reads the ring buffer regardless). Deliberately independent of the master switch — support often wants logs without the full diag surface. | + +## 4. Internal plumbing (set by the launcher/bench/tests — humans never set these) + +| var | set by | effect | +|---|---|---| +| `HERMES_PYTHON`, `HERMES_PYTHON_SRC_ROOT`, `HERMES_CWD` | launcher / bench | Which gateway python + repo root + cwd the TUI spawns against (the bench's fake-gateway seam). | +| `HERMES_TUI_ACTIVE_SESSION_FILE` | launcher/bench | Session handoff file. | +| `HERMES_TUI_RESUME`, `HERMES_TUI_PROMPT`, `HERMES_TUI_FAKE` | launcher/tests | Resume-at-boot, seeded prompt, fake-mode. | +| `HERMES_TUI_RPC_TIMEOUT_MS`, `HERMES_TUI_STARTUP_TIMEOUT_MS` | tests/CI | Protocol timeouts. | +| (`ui-tui` only) `HERMES_TUI_MEMSAMPLE_FD/MS` | bench | Ink fd-3 node sampler. | + +## How the pieces compose (the support script) + +- Regular user, normal day: zero flags, zero diagnostic commands visible. +- "My TUI feels heavy" support flow: `HERMES_TUI_DIAGNOSTICS=1 hermes` → `/mem` + for the live numbers, `/heapdump` for a snapshot to attach, window stats + exposed for `bench/live-attach.sh ` to read. +- Developer profiling: same master switch + the individual knobs + (`HERMES_TUI_WINDOWING=0` A/B, `WINDOW_IDLE_MS` tuning) as needed. +- Anything in section 4 appearing in a user-facing doc is a bug. + +Gating implementation: `logic/env.ts` (`diagnosticsEnabled()`), +`logic/slash.ts` (`DIAGNOSTIC_COMMANDS` — dispatch hint, help + completion +filtering), `view/transcript.tsx` (stats default). Tests: +`slash.test.ts` (gating both states), `utilityCommands.test.ts` (commands +themselves, gate enabled suite-wide). diff --git a/ui-opentui/src/logic/env.ts b/ui-opentui/src/logic/env.ts index 362d2704503..e43861e5801 100644 --- a/ui-opentui/src/logic/env.ts +++ b/ui-opentui/src/logic/env.ts @@ -15,6 +15,21 @@ export function envFlag(value: string | undefined, fallback: boolean): boolean { return fallback } +/** + * The diagnostics master switch — `HERMES_TUI_DIAGNOSTICS` (default OFF). + * + * Gates the developer/profiling surface a regular user should never trip + * over: the diagnostic slash commands (`/mem`, `/heapdump`) and the default + * for `HERMES_TUI_WINDOW_STATS` (which can still be set individually). It is + * an enable switch, not a secret: anyone CAN set it (support flows say + * "relaunch with HERMES_TUI_DIAGNOSTICS=1"), it just keeps the day-to-day + * surface clean. Read per call so tests (and long-lived processes whose + * wrapper mutates env before launch) see the live value. + */ +export function diagnosticsEnabled(): boolean { + return envFlag(process.env.HERMES_TUI_DIAGNOSTICS, false) +} + /** * Parse `HERMES_TUI_TOOL_OUTPUT_LINES` (a TUI-only env var — deliberately NOT * a config.yaml knob): how many output lines an expanded tool body shows. diff --git a/ui-opentui/src/logic/slash.ts b/ui-opentui/src/logic/slash.ts index 1b69ed8e059..2b448a5bdb6 100644 --- a/ui-opentui/src/logic/slash.ts +++ b/ui-opentui/src/logic/slash.ts @@ -11,6 +11,7 @@ * (exec/plugin → system · alias → re-dispatch · skill/send → submit a turn · * prefill → notice). Long output routes to the pager (Phase 5a). */ +import { diagnosticsEnabled } from './env.ts' import { DETAILS_SECTIONS, DETAILS_USAGE, type DetailsMode, nextDetailsMode, parseDetailsMode } from './details.ts' import { formatBytes, memReport, performHeapdump } from './diagnostics.ts' import { formatSpawnTree, formatSpawnTreeList, readSpawnTreeEntries } from './replay.ts' @@ -149,7 +150,12 @@ function present(ctx: SlashContext, title: string, text: string): void { else ctx.pushSystem(text) } -const CLIENT_HELP = [ +/** Process-diagnostic commands — hidden behind `HERMES_TUI_DIAGNOSTICS` + * (logic/env.ts). Regular users never see them; support flows enable them + * with one env var. Keep this set in sync with the `(diag)` lines below. */ +const DIAGNOSTIC_COMMANDS = new Set(['mem', 'heapdump']) + +const CLIENT_HELP_LINES = [ '/help — list commands', '/model [name] — switch model (picker if bare)', '/copy [n] — copy the last (or n-th) response', @@ -160,12 +166,17 @@ const CLIENT_HELP = [ '/compact [on|off|toggle] — compact transcript spacing', '/details [hidden|collapsed|expanded|cycle] — tool/reasoning detail', '/replay [n|path] — inspect an archived spawn tree', - '/mem — live memory stats', - '/heapdump — write a V8 heap snapshot', + '/mem — live memory stats (diag)', + '/heapdump — write a V8 heap snapshot (diag)', '/logs — recent engine log lines', '/quit, /exit — quit', '(other /commands run on the gateway)' -].join('\n') +] + +function clientHelp(): string { + const lines = diagnosticsEnabled() ? CLIENT_HELP_LINES : CLIENT_HELP_LINES.filter(l => !l.includes('(diag)')) + return lines.join('\n') +} type ClientHandler = (arg: string, ctx: SlashContext) => void | Promise @@ -631,9 +642,9 @@ const CLIENT: Record = { // Prefer the live catalog; fall back to the client list if it's unavailable. try { const cat = await ctx.request('commands.catalog', {}) - ctx.pushSystem(renderCatalog(cat) || CLIENT_HELP) + ctx.pushSystem(renderCatalog(cat) || clientHelp()) } catch { - ctx.pushSystem(CLIENT_HELP) + ctx.pushSystem(clientHelp()) } }, logs: (_arg, ctx) => ctx.openPager('Logs', ctx.logTail().join('\n') || '(log empty)'), @@ -643,7 +654,8 @@ const CLIENT: Record = { /** The registered client-command names (catalog introspection — tests/menus). */ export function clientCommandNames(): string[] { - return Object.keys(CLIENT).sort() + const names = Object.keys(CLIENT) + return (diagnosticsEnabled() ? names : names.filter(n => !DIAGNOSTIC_COMMANDS.has(n))).sort() } /** Render the gateway `commands.catalog` into a help block (loose-typed read). @@ -701,6 +713,12 @@ export async function dispatchSlash(input: string, ctx: SlashContext): Promise { expect(p.system).toContain('done') }) }) + +describe('diagnostic command gating (HERMES_TUI_DIAGNOSTICS)', () => { + const KEY = 'HERMES_TUI_DIAGNOSTICS' + const prev = process.env[KEY] + afterEach(() => { + if (prev === undefined) delete process.env[KEY] + else process.env[KEY] = prev + }) + + test('OFF (default): /mem and /heapdump respond with the enable hint, not the command', async () => { + delete process.env[KEY] + const p = makeCtx(async () => ({})) + await dispatchSlash('/mem', p.ctx) + await dispatchSlash('/heapdump', p.ctx) + expect(p.system[0]).toContain('HERMES_TUI_DIAGNOSTICS=1') + expect(p.system[1]).toContain('HERMES_TUI_DIAGNOSTICS=1') + expect(p.calls).toHaveLength(0) // never reached the gateway ladder either + }) + + test('OFF: the diagnostic names are absent from clientCommandNames()', () => { + delete process.env[KEY] + const names = clientCommandNames() + expect(names).not.toContain('mem') + expect(names).not.toContain('heapdump') + expect(names).toContain('logs') // non-diagnostic neighbors stay + }) + + test('ON: /mem executes (live memory stats), names are listed', async () => { + process.env[KEY] = '1' + expect(clientCommandNames()).toContain('mem') + expect(clientCommandNames()).toContain('heapdump') + const p = makeCtx(async () => ({})) + await dispatchSlash('/mem', p.ctx) + const out = [...p.system, ...p.paged.map(x => x.text)].join('\n') + expect(out).toMatch(/rss|heap/i) + }) +}) diff --git a/ui-opentui/src/test/utilityCommands.test.ts b/ui-opentui/src/test/utilityCommands.test.ts index cd6e03bc3bf..93c4edf38d3 100644 --- a/ui-opentui/src/test/utilityCommands.test.ts +++ b/ui-opentui/src/test/utilityCommands.test.ts @@ -22,6 +22,19 @@ import { formatSpawnTree, formatSpawnTreeList, readSpawnTreeEntries } from '../l import { clientCommandNames, dispatchSlash, type SlashContext } from '../logic/slash.ts' import type { Part } from '../logic/store.ts' +// The utility commands under test are DIAGNOSTIC commands — gated behind +// HERMES_TUI_DIAGNOSTICS (logic/env.ts). This suite tests the commands +// themselves, so enable the gate for the whole file (gating behavior has its +// own tests in slash.test.ts). +const PREV_DIAG = process.env.HERMES_TUI_DIAGNOSTICS +beforeEach(() => { + process.env.HERMES_TUI_DIAGNOSTICS = '1' +}) +afterEach(() => { + if (PREV_DIAG === undefined) delete process.env.HERMES_TUI_DIAGNOSTICS + else process.env.HERMES_TUI_DIAGNOSTICS = PREV_DIAG +}) + // /heapdump must not write a REAL multi-MB snapshot per test run — stub the V8 // seam; the path/mkdir plumbing still runs for real (under a temp HERMES_HOME). vi.mock('node:v8', () => ({ writeHeapSnapshot: vi.fn((path?: string) => path ?? 'unnamed.heapsnapshot') })) diff --git a/ui-opentui/src/view/transcript.tsx b/ui-opentui/src/view/transcript.tsx index 35b6930e51a..bb519183c74 100644 --- a/ui-opentui/src/view/transcript.tsx +++ b/ui-opentui/src/view/transcript.tsx @@ -88,7 +88,7 @@ import type { BoxRenderable, ScrollBoxRenderable } from '@opentui/core' import { useRenderer } from '@opentui/solid' import { createComputed, createMemo, createSelector, createSignal, For, on, onCleanup, onMount, Show } from 'solid-js' -import { envFlag } from '../logic/env.ts' +import { diagnosticsEnabled, envFlag } from '../logic/env.ts' import type { Message, SessionStore } from '../logic/store.ts' import { computeWindow, @@ -234,9 +234,10 @@ export function Transcript(props: { store: SessionStore }) { if (!streaming) estimates.set(key, estimate) return estimate } - // DEV stats exposure for the bench (HERMES_TUI_WINDOW_STATS): the live + // DEV stats exposure for the bench (HERMES_TUI_WINDOW_STATS — defaults to + // the HERMES_TUI_DIAGNOSTICS master switch; settable individually): the live // current/peak mounted-row counters from logic/window.ts. - if (envFlag(process.env.HERMES_TUI_WINDOW_STATS, false)) { + if (envFlag(process.env.HERMES_TUI_WINDOW_STATS, diagnosticsEnabled())) { ;(globalThis as unknown as Record)['__hermesTuiWindowStats'] = windowRowStats() }