TUI Config Interaction Standard — T418 Tier-1 Design Doc
Status: design artifact only — no code built. Tiers 2-3 (shared primitives + migration + settings screen) are TTY+decision-gated and not included here.
Source cross-checked: src/tui/, src/core/config.ts, USAGE.md.
1. The North Star Pattern (Claude Code /config as reference)
Claude Code's /config command is the interaction model that every Cyberine TUI interactive surface should converge on. It is not being mimicked for mimicry's sake; it is the right model because it matches the terminal UX genre Cyberine already inhabits (Ink TUI, no mouse by default, keyboard-first) and because users who run Cyberine alongside Claude Code already know the pattern.
1.1 Structural elements
The pattern has five elements that always appear together:
-
Tab navigation across categories. The top of the view shows a horizontal tab bar: category labels separated by spaces, the active one highlighted with the accent color. Tab/Shift+Tab or h/l cycles categories; the rest of the view is scoped to the active category.
-
Type-to-filter search box. A single-line input at the top of the list area (below the tab bar). Typing narrows the list to rows whose label or value contains the query as a subsequence. The search box always has focus when the overlay is open; navigation keys (Up/Down/j/k) move the cursor without leaving the search box.
-
Two-column aligned list. Each row has a fixed-width left column (label, left-aligned, muted color) and a right column (current value, left-aligned, accent color for non-default values). The gutter symbol (existing
SYM.gutter) marks the selected row. Both columns share the same monospace grid so values across all rows line up vertically. -
Keyboard-driven row editors. Pressing Enter on a selected row opens an inline editor appropriate for that row's type:
- Toggle (bool): Enter flips the value immediately; the right column updates in place; no confirmation.
- Enum picker (fixed set): the right column opens a compact inline list of valid options; Up/Down selects; Enter confirms; Esc cancels.
- Value editor (free text or number): the row switches to an inline text input pre-filled with the current value; Enter saves; Esc cancels. Editing is always modal within the row — no separate screen.
-
Footer key-hint bar. The existing
KeyHintscomponent (src/tui/components/KeyHints.tsx) renders the contextual hints at the bottom of the screen. While the config/settings overlay is active, the hints reflect only the active interaction state: idle list showsTab categories /jk navigate Enter edit Esc back; while editing a toggle/enum showsEnter confirm Esc cancel; while in a text editor showsEnter save Esc cancel Ctrl+U clear.
1.2 Layout sketch (character-cell, 80-col example)
[Fleet] [Settings] [Providers] <- tab bar (ACCENT = active)
filter: _ <- search box
------------------------------------------
> fleet compete <- selected row (SYM.gutter + accent value)
permission default
model deepseek/deepseek-v4-pro
thinking on
theme dark
------------------------------------------
Tab categories /jk navigate Enter edit Esc back <- KeyHints footer1.3 Rendering stack constraints
Every component in the Cyberine TUI runs inside Ink 5 + React 18 (type: module, tsx). All sizes are in character cells. The pattern must respect:
- No emoji anywhere (global code-style rule).
- BMP-only glyphs — same contract as the mascot.
- The
KeyHintscomponent already knows how to render aHint[]list and truncate on narrow terminals. - The
FuzzyPickercomponent (src/tui/components/FuzzyPicker.tsx) already implements the type-to-filter search box and the edge-scroll windowed list. The settings view is NOT a FuzzyPicker (items are not searched for selection, they are browsed for editing), but the search box and windowing logic can be reused or cloned.
1.4 Primitive shapes needed for Tier 2
A builder of shared TUI primitives should produce:
TabbedView: renders the tab bar; acceptstabs: { id: string; label: string }[]+activeTab: string+onSwitch: (id) => void. UsesuseInputfor Tab/Shift+Tab and h/l. Wraps whatever content the caller renders below it.SearchableList<T>: wraps the FuzzyPicker search-box + windowed-list pattern but with edit semantics: items are not selected-to-dismiss, they are selected-to-edit. Acceptsitems: T[],getLabel: (T) => string,getValue: (T) => string,renderRow: (T, selected) => JSX,onEdit: (T) => void,onClose: () => void. Maintains internal query state and filter. NoonSelectfor dismissal —EntercallsonEdit.KeyHintFooter: a thin wrapper aroundKeyHintsthat accepts acontext: 'idle' | 'editing-toggle' | 'editing-enum' | 'editing-value'prop and renders the right hint set for each state. Eliminates the per-surface hint array duplication.- Row-level editors (rendered inline, not as overlays):
ToggleEditor: renderson | offwith the current value highlighted; Enter flips and auto-confirms.EnumEditor: renders a compact vertical list of valid values; Up/Down selects; Enter confirms.ValueEditor: a single-line text input (wrapsuseTextInput); pre-filled with current value; Enter saves; Ctrl+U clears; Esc cancels.
2. Interactive Surface Inventory
Every interactive surface in the current Cyberine TUI, with its current interaction style and its target mapping.
2.1 Fleet Hub (src/tui/app.tsx) — cyberine agents / npm run tui
Surface 1: Runs list (fleet list, main view)
- Current: arrow keys / j/k navigate a flat or grouped list of runs; Enter opens detail; no search by default.
- Currently has: text filter overlay (
/key opensFuzzyPickerin filter mode), folder picker overlay (nopensFuzzyPicker), view-mode cycle (Ctrl+S), grouping toggle. - Target mapping: the runs list itself does not need the config pattern (it is a task monitor, not a settings UI). The existing
FuzzyPickerapproach for the run filter is already correct. The folder picker is also correct. No migration needed.
Surface 2: Folder picker (FuzzyPicker overlay, n key)
- Current:
FuzzyPickeroverlay with search box, windowed list, footer hints. Pattern already matches the type-to-filter + footer-hint target. - Target mapping: compliant. No change needed.
Surface 3: Run filter (FuzzyPicker overlay, / key)
- Current:
FuzzyPickeroverlay, same as folder picker. - Target mapping: compliant. No change needed.
Surface 4: Quick reply bar (Space on a run, QuickReply component)
- Current: single-line text input (
useTextInput), footer hint bar (QUICKREPLY_HINTS). Simple text-entry, no navigation. - Target mapping: not a settings surface; stays as a direct text input. No migration.
Surface 5: Steer input (SteerInput component)
- Current: single-line text input with
Ctrl+Vpaste; footer hint bar (STEER_HINTS). - Target mapping: same as Quick reply — direct text input, not a settings surface.
Surface 6: Compose panel (new session, composeActive state)
- Current: text input for the initial task; footer hint bar (
COMPOSE_HINTS). - Target mapping: direct task input, not a settings surface.
Surface 7: Chats view (chat session list, listSource === 'chats')
- Current: j/k navigation over chat sessions; Enter resumes;
Ctrl+Xdeletes; hint bar (CHATS_HINTS). - Target mapping: this is a browsable list like the runs list; not a settings surface. The two-column pattern (session title left, age right) is already in use in
ChatSessionList. No migration needed for the list itself, but the column-alignment convention from the config pattern should be adopted as a style norm.
Surface 8: Session browser (global, sessionBrowserOpen, s key from hub)
- Current: j/k navigation, folder headers, session rows, persistent bottom bar, footer hint bar (
SESSION_BROWSER_HINTS). - Target mapping: browsable list + action bar; not a settings surface. The folder-header / session-row two-column layout should adopt the label-left / value-right alignment norm from the config pattern (folder name left, session count right).
Surface 9: Hermes hub welcome (hubWelcomeOpen, WelcomeBanner)
- Current: read-only display of available adapters, judges, workflows. No keyboard-driven editing.
- Target mapping: informational surface, not interactive. No migration.
Surface 10: Help overlay (HelpOverlay, ? key)
- Current: full-screen keybinding reference; any key closes. Two-column layout (key left, label right) — this layout already follows the target norm.
- Target mapping: read-only; compliant already. No migration.
Surface 11: Worker peek (WorkerPeek, Enter on a worker row in detail)
- Current: scrollable log output;
Ctrl+U/Ctrl+Dscroll; footer hint bar (HINTS.peek). - Target mapping: log viewer, not a settings surface. No migration.
Surface 12: Diff viewer (DiffViewer, d key in run detail)
- Current: scrollable diff output; j/k,
Ctrl+U/Ctrl+D, g/G scroll; footer hints (HINTS.diff). - Target mapping: read-only viewer. No migration.
Surface 13: Workflow monitor (WorkflowMonitor)
- Current: read-only workflow-step graph; Esc back; hint bar.
- Target mapping: read-only viewer. No migration.
2.2 Chat REPL (src/tui/ChatApp.tsx) — cyberine chat
Surface 14: Main chat input (InputBox component)
- Current: full-width text input with cursor, history (Up/Down), paste collapse,
@-mention expansion, double-Esc clear, Shift+Enter newline. Footer: hint bar fromChatStatusLine. - Target mapping: primary text-entry surface, not a settings surface. Already has good UX. No migration.
Surface 15: /config command (inline text command, no TUI overlay)
- Current: typed as
/config [<key> <value>]in the chat input. Bare form prints current values as a system message. With key+value sets one setting (fleet, permission, model, thinking, theme) and prints a confirmation. This is a text-only interaction — no keyboard navigation, no visual list. - Target mapping: this surface is the PRIMARY CANDIDATE for migration to the north-star pattern. A future
settingsscreen (tier 2-3) should replace or augment this with the tab nav + two-column list + inline editor pattern. The/configtext command should remain as a power-user shortcut even after the visual settings screen exists, because it is scriptable and muscle-memory-friendly. - Current managed keys:
fleet,permission,model,thinking,theme. These map directly to the config-knob inventory in section 3.
Surface 16: /model picker (ModelPicker component, FuzzyPicker-backed)
- Current: fuzzy picker overlay listing all models from the model catalog; current model marked with
(current); arrows+Enter pick; Esc cancels. - Target mapping: already matches the type-to-filter + footer-hint pattern. This is the canonical implementation of the enum-picker element for a large open-ended set. The
ModelPickerrenders 2-col rows (model id left, context-window + notes right) — this is the right layout. No migration needed; use as a reference for how to build the provider/model picker in the settings screen.
Surface 17: Session picker (SessionPicker component, FuzzyPicker-backed)
- Current: fuzzy picker overlay for
cyberine chat --resume; session title left, id+age right. - Target mapping: already correct two-column alignment. No migration.
Surface 18: Question prompt (QuestionPrompt component)
- Current: multiple-choice question rendered inline in the transcript; Up/Down or number key selects; Enter confirms.
- Target mapping: this is the inline enum-picker element applied to planner-generated questions. The pattern is correct. The keyboard nav (Up/Down + Enter) should be the same as the
EnumEditorprimitive in the settings pattern.
Surface 19: /mode and /xmode commands (inline text)
- Current:
/modewith no arg shows current mode + cycle instructions; with an arg sets the permission mode. Shift+Tab cycles through modes. - Target mapping: mirrors
/config permission. Will be subsumed by the settings screen's permission-mode row in tier 2. The Shift+Tab cycle is the toggle/enum-cycle shorthand that should be preserved.
3. Config-Knob Inventory (Settings Screen Spec for Tier 2)
All knobs that a future persistent settings screen would expose, cross-checked against src/core/config.ts (the Config interface and the config object initialization block) and the env-var table in USAGE.md.
3.1 TUI appearance
| Display label | Env var | Type | Default | Read site |
|---|---|---|---|---|
| Theme | CYBERINE_THEME | enum picker (dark | cyan | claude-dark | light | ansi) | dark | src/tui/theme.ts line 27 (resolveVariant()); THEME_NAMES exported |
| Mascot variant | CYBERINE_MASCOT | enum picker (dna-infinity | infinity-wave | infinity-3d) | dna-infinity | src/tui/brand/mascot.ts |
| Mascot animate | CYBERINE_MASCOT_ANIMATE | toggle (on/off) | auto (on locally, off over SSH) | src/tui/brand/mascot.ts |
| Alt-screen (fleet) | CYBERINE_ALTSCREEN | toggle (on/off) | on for fleet, off for chat | src/tui/app.tsx line 344; src/tui/ChatApp.tsx |
| Terminal bell | CYBERINE_BELL | toggle (on/off) | on | src/tui/app.tsx line 336 |
| OS notifications | CYBERINE_NOTIFY | toggle (on/off) | on | src/tui/app.tsx line 338 |
| Mouse mode (chat) | CYBERINE_MOUSE | toggle (on/off) | on (disable with CYBERINE_MOUSE=0) | src/tui/ChatApp.tsx |
| Wheel lines | CYBERINE_WHEEL_LINES | value (integer 1-10) | 3 | src/tui/ChatApp.tsx |
| Kitty keyboard | CYBERINE_KITTY_KEYBOARD | toggle (on/off) | off | src/tui/ChatApp.tsx |
3.2 Chat REPL settings
| Display label | Env var / config key | Type | Default | Read site |
|---|---|---|---|---|
| Fleet mode (execution mode) | CYBERINE_MODE (runtime: executionModeRef) | enum picker (compete | swarm | research | deep | smart | rush) | compete | src/tui/ChatApp.tsx ExecutionMode type; /config fleet handler |
| Permission mode | CYBERINE_CHAT_PERMISSION_MODE (runtime: permissionMode) | enum picker (default | acceptEdits | plan | auto | bypassPermissions | dontAsk) | default | src/core/config.ts config.defaultPermissionMode; ChatApp line 806 |
| Planner model | CYBERINE_CHAT_MODEL (runtime: activeModel) | value (free text, validated against catalog) | first resolved provider | src/core/config.ts config.chatModel; ChatApp /config model handler |
| Planner provider | CYBERINE_CHAT_PROVIDER | value (provider id from registry) | first resolved provider | src/core/config.ts config.chatProvider |
| Show thinking | CYBERINE_CHAT_THINKING (runtime: showThinking) | toggle (on/off) | on | src/tui/ChatApp.tsx line 696 (setShowThinkingGlobal); /config thinking handler |
| Chat max tokens | CYBERINE_CHAT_MAX_TOKENS | value (integer) | 8192 | src/core/config.ts config.chatMaxTokens |
| Context window hint | CYBERINE_CHAT_CONTEXT_WINDOW | value (integer) | 65536 | src/core/config.ts config.chatContextWindow |
| Model cycle list | CYBERINE_CHAT_MODELS | value (comma-separated model ids) | (unset) | src/tui/ChatApp.tsx Ctrl+T handler |
| User memory | CYBERINE_USER_MEMORY | toggle (on/off) | on | src/store/userMemory.ts; USAGE.md line 4699 |
| Status line command | CYBERINE_STATUSLINE_CMD | value (shell command string) | (unset) | src/tui/hooks/useStatuslineCmd.ts |
| Persona | CYBERINE_PERSONA | enum picker (cyberine | none) | cyberine | src/core/config.ts config.persona |
3.3 Dispatch settings
| Display label | Env var | Type | Default | Read site |
|---|---|---|---|---|
| Build fleet | CYBERINE_BUILD_FLEET | value (comma-separated worker ids) | (unset) | src/core/config.ts config.buildFleet |
| Route mode | CYBERINE_ROUTE | enum picker (auto | off) | auto | src/core/config.ts config.routeMode |
| Default dispatch mode | CYBERINE_MODE | enum picker (same as fleet mode above) | (unset = no preset) | src/core/config.ts config.mode |
| Loop max tokens | LOOP_MAX_TOKENS | value (integer) | 16000 | src/core/config.ts config.loopMaxTokens |
| Loop max tool rounds | LOOP_MAX_TOOL_ROUNDS | value (integer) | 25 | src/core/config.ts config.loopMaxToolRounds |
| Task trusted | CYBERINE_TASK_TRUSTED | toggle (on/off) | on | src/core/config.ts config.taskTrusted |
| Host sandbox mode | HOST_SANDBOX_MODE | enum picker (auto | seatbelt | bubblewrap | docker | gvisor | podman | none) | auto | src/core/config.ts config.hostSandboxMode |
| Daily spend cap | CYBERINE_DAILY_USD_CAP | value (number, USD) | 10 | src/core/config.ts config.dailySpendCapUsd |
| Smart router | CYBERINE_SMART_ROUTER | toggle (on/off) | off | src/core/config.ts config.smartRouter |
3.4 Worker mode
| Display label | Env var | Type | Default | Read site |
|---|---|---|---|---|
| Worker mode | CYBERINE_WORKER_MODE | enum picker (code | architect | ask | debug | custom ids) | (unset = no override) | src/core/config.ts config.workerMode; src/core/personas/registry.ts |
| OpenCode model | OPENCODE_MODEL | value (model id) | deepseek/deepseek-v4-pro | src/core/config.ts config.openCodeModel |
| OpenCode agent | OPENCODE_AGENT | enum picker (plan | build) | plan | src/core/config.ts config.openCodeAgent |
3.5 Advanced / CYBERINE_HOME
| Display label | Env var | Type | Default | Read site |
|---|---|---|---|---|
| CYBERINE_HOME | CYBERINE_HOME | value (directory path) | ~/.cyberine | src/core/config.ts line 21 |
| Plans dir | CYBERINE_PLANS_DIR | value (directory path) | ~/.cyberine/plans/<project> | src/core/config.ts config.plansDir |
| Exec backend | CYBERINE_EXEC_BACKEND | enum picker (local | docker | ssh) | local | src/core/config.ts config.execBackend |
| Trust gate | CYBERINE_TRUST_GATE | toggle (on/off) | on | src/core/config.ts config.trustGateEnabled |
| Rules | CYBERINE_RULES | toggle (on/off) | on | src/core/config.ts config.rulesEnabled |
| Repo map | CYBERINE_REPO_MAP | toggle (on/off) | on | src/core/config.ts config.repoMapEnabled |
| Instruction files | CYBERINE_INSTRUCTION_FILES | toggle (on/off) | on | src/core/config.ts config.instructionFilesEnabled |
| Context providers | CYBERINE_CONTEXT_PROVIDERS_ENABLED | toggle (on/off) | on | src/core/config.ts config.contextProvidersEnabled |
4. Tab Category Proposal for the Tier-2 Settings Screen
Based on the knob inventory above, a four-tab layout covers all knobs without overflow:
- Chat — fleet mode, permission mode, planner model/provider, thinking, max tokens, context window, persona, model cycle list.
- Appearance — theme, mascot variant, mascot animate, alt-screen, bell, OS notifications, mouse, wheel lines, kitty keyboard, status line command.
- Dispatch — build fleet, route mode, loop max tokens/rounds, sandbox mode, daily spend cap, task trusted, smart router, exec backend.
- Advanced — CYBERINE_HOME, plans dir, trust gate, rules, repo map, instruction files, context providers, worker mode, OpenCode model/agent, user memory.
The /config command in ChatApp manages a sub-set of "Chat" tab knobs today (fleet, permission, model, thinking, theme). The visual settings screen should present all four tabs; /config remains as a shorthand for the Chat tab only.
5. Cross-Check Statement
Every knob name, env var, default, and read site in section 3 was verified directly against:
- The
Configinterface type definitions insrc/core/config.ts(lines 26-612). - The
configobject initialization block insrc/core/config.ts(lines 627-882). - The
/configcommand handler insrc/tui/ChatApp.tsx(lines 2068-2201) for the five currently managed keys. - The env-var table in
USAGE.md(lines 4699-4763 and surrounding sections). src/tui/theme.tsforCYBERINE_THEMEandTHEME_NAMES.src/tui/app.tsxforCYBERINE_BELL,CYBERINE_NOTIFY,CYBERINE_ALTSCREEN.
No knob name was invented from memory. Any knob that appears only in USAGE.md without a corresponding config.ts entry is documented with its USAGE.md location.
6. Tier 2-3 Scope (NOT in this run)
The following work is gated on a real TTY session and a scope decision from the user. It is documented here so the next agent picks up from a clear spec, not from the ground.
- Build the shared primitives:
TabbedView,SearchableList,KeyHintFooter,ToggleEditor,EnumEditor,ValueEditor. - Wire a
/settingsslash command (orskey from a future settings entry point) that renders the four-tab settings overlay on top of the chat REPL. - Migrate the five existing
/configkeys (fleet, permission, model, thinking, theme) into the Chat tab of the new settings screen, keeping/configas an alias. - Add persistence: the settings screen writes changed values via
upsertEnvKeys(the same mechanism/config themeand/config modelalready use) to~/.cyberine/.env. - Migrate the fleet hub's folder-picker and run-filter overlays to use
SearchableListonce it exists (backward-compatible; same behavior, shared implementation). - Visual verification on a real TTY after each primitive is built (no snapshot tests — project rule).