TUI Code Architecture

High-integrity architecture specifications, Zustand store configurations, and connection state machines of the Hoox Terminal UI.

This page

The Hoox Terminal UI (TUI) is a full-screen, keyboard-driven operations center built with OpenTUI, React 19, and Zustand. This document covers package layout, state, data flows, and crash recovery for engineers.

Package: packages/tui (@hoox-sh/hoox-tui)
Entry: src/main.tsx · Root: src/app.tsx · CLI launcher: hoox tui


🏗️ Architectural Blueprint

Diagram

Rendering…

Directory map

packages/tui/src/
├── main.tsx                 # createCliRenderer (TUI_FPS / TUI_MOUSE env)
├── app.tsx                  # shell, session, SSE kickoff (views via view-registry)
├── components/
│   ├── views/               # 16 views (+ dashboard/, config-editor/ subpanels)
│   ├── layout/              # sidebar, statusbar
│   ├── shared/              # palette, crash screen, error boundary, …
│   └── ui/                  # dialog / toast wrappers
├── services/
│   ├── cli-bridge/          # typed hoox subprocess bridge
│   ├── hoox-path-service.ts # $HOME/.hoox paths
│   └── tui-storage.ts       # file-backed JSON (no localStorage in Bun)
├── hooks/                   # keyboard, polling, renderer-ref
└── stores/*.test.ts         # unit tests (stores live in hoox-shared)

Stores are not under packages/tui/src/stores for production code — they live in @hoox-sh/hoox-shared:

StorePathResponsibility
UIpackages/shared/src/stores/ui-store.tsactiveView, sidebar, modal, palette
Servicepackages/shared/src/stores/service-store.tsworkers, trades, logs, alerts, connection, SSE
Configpackages/shared/src/stores/config-store.tstheme (dark-only in TUI), refresh interval, notifications, shortcuts

Session persistence: $HOME/.hoox/.tui-state/session.json via shared restoreSession / saveSession. All 16 ViewId values are valid for restore; unknown IDs fall back to dashboard.

Navigation registry & colors

  • View factories, sidebar labels, keyboard shortcuts, and palette view commands are defined in packages/tui/src/view-registry.tsx.
  • Semantic status colors (ConnectionStatusColor, WorkerStatusColor, LogLevelColor, AlertSeverityColor) live in @hoox-sh/hoox-shared (packages/shared/src/colors.ts). Do not invent local status→hex maps in views.
  • Shared chrome: ViewHeader + Panel (cool focus border). Settings uses dark-only theme (light is not implemented) and shows mode/host/transport/auth via getSettingsConnectionSnapshot() — never secret values.

Test doubles (no polluting mock.module)

Installed once from packages/tui/src/test-setup.ts (preload):

DoubleModuleControl
CLIcli-bridge-test-double.tscliBridgeDouble / resetCliBridgeDouble() (includes pyneHealthCheck)
HTTP + SSEnetwork-test-double.tssetMockApiData / setMockApiFailure / emitSseEvent

View tests must not call mock.module for cli-bridge, shared stores, api-client, or sse. Override methods on the doubles instead. Edge topology fixtures use HOOX_GRAPH_METADATA_PATH (temp file) rather than mocking fs.


🗺️ View registry (16)

Must stay aligned with ViewId in packages/shared/src/types.ts, sidebar items, view-registry.tsx, and command palette entries:

ViewIdComponentPrimary data source
dashboardDashboardViewfetchWorkers, monitorStatus, checkFix, agentHealthCheck, pyneHealthCheck, kill-switch
workersWorkersOverviewstore workers + CLI deploy/logs/repair
worker-detailWorkerDetailselectedWorkerId, configShow, workerLogs
trade-monitorTradeMonitortradeStream (SSE streamTrades)
logs-viewerLogsViewerlogs (SSE streamLogs + CLI fetch)
service-managerServiceManagerdeploy/repair/rebuild/kill-switch
config-editorConfigEditorfilesystem + configValidate
setup-wizardSetupWizardwizard steps + deployAll
settingsSettingsViewconfig store + connection snapshot + checkSetup / checkFix
queue-depthQueueDepthViewmonitorQueueDepth
kv-viewerKvViewerconfigKvList / configKvGet
secrets-viewerSecretsViewerconfigSecretsList (names only)
ai-chatAiChatViewagentChatStream SSE
db-queryDbQueryViewvalidateReadOnlySql + dbQuery
edge-topologyEdgeTopologymonorepo graph-metadata.json
worker-settingsWorkerSettingsViewdashboard.jsonc / worker settings via CLI

🌐 Local vs remote mode

hoox tui resolves an API base URL and forwards it to the child process:

LaunchModeHOOX_API_URLStatus bar
hoox tuilocalHOOX_API_URL env or http://localhost:8787[LOCAL] localhost:8787
hoox tui --remoteremoteresolveGatewayUrl() (HOOX_GATEWAY_URL / CF account + wrangler subdomain)[REMOTE] <gateway-host>
hoox tui --api-url <url>remoteexplicit URL (strips trailing /; overrides --remote)[REMOTE] <host>

Env vars set on the TUI process: HOOX_API_URL, HOOX_TUI_MODE (local | remote), optional HOOX_API_TOKEN.
Shared api-client / sse read HOOX_API_URL + HOOX_API_TOKEN at import time.

Security posture (operator plane)

LayerStatus
Client Bearer (HOOX_API_TOKEN)Sent as Authorization: Bearer … by TUI HTTP/SSE via operator transport profile
Access service token headersAuto-attached when CF_ACCESS_CLIENT_ID + CF_ACCESS_CLIENT_SECRET are set
Transport profileHOOX_TRANSPORT=public|access|mtls|tunnel (mtls/tunnel reserved; headers like public until certs land)
Fail-closed remote launchYes — remote requires Bearer, Access env, or --allow-insecure
Server management routes/v1/* on gateway with requireOperatorAuth (OPERATOR_API_KEY preferred, else INTERNAL_API_KEY)
Public livenessGET /health remains unauthenticated (no secrets)
Cloudflare Access / mTLS / tunnel edgePut Access on a mgmt hostname; mTLS + tunnel are Enterprise / private-ingress follow-ups

Server secret: wrangler secret put OPERATOR_API_KEY (value must match client HOOX_API_TOKEN).

Client paths (TUI): GET /v1/workers, SSE /v1/trades/stream, /v1/logs/stream, auth probe GET /v1/health.

Prefer a dedicated management hostname (e.g. mgmt.example.com) separate from TradingView® /webhook ingress — see Zero Trust.

Auth token UX (remote)

SurfaceBehavior
CLI launchFails closed without Bearer or Access env (unless --allow-insecure)
CLI flags--token <value> sets HOOX_API_TOKEN for the child only (never logged)
Access envCF_ACCESS_CLIENT_ID + CF_ACCESS_CLIENT_SECRET satisfy the launch gate
Escape hatch--allow-insecure remote without credentials (not recommended)
Status barRemote without token → compact AUTH?; 401/403 → AUTH!
ToastsMode-aware connect / lost / reconnect; auth missing & auth failed messages
StartupREMOTE never uses CLI fallback (gateway HTTP only); LOCAL may fall back to cliBridge.monitorStatus()
Config file~/.hoox/config.json written 0600 (may hold apiToken)

Dev logging

File-backed only (never stdout — would corrupt the alternate screen):

hoox tui --debug
# or: HOOX_DEBUG=1 / TUI_DEBUG=1
# → $HOME/.hoox/.tui-state/debug.log  (JSON lines; secret-like keys + Bearer/env patterns redacted)

Wired at startup (main.tsx), connection path (app.tsx), and CLI bridge execs.


🛜 Dual-channel + SSE

Startup sequence (AppRoot)

  1. Restore session → set view / sidebar
  2. fetchWorkers() (HTTP) → on failure only, cliBridge.monitorStatus() (CLI fallback)
  3. Fire-and-forget streamTrades() + streamLogs() (SSE; silent if API down). Store retains abort handles; stopStreams() tears them down (re-subscribe replaces prior subscriptions).

CLI bridge (src/services/cli-bridge)

All mutating / diagnostic operator actions go through the hoox binary with timeout, abort tags, and structured CliErrorDetails sunk to the service store for the status bar expand panel.

Notable methods: deployAll, deployWorker, repairWorker, rebuild, checkHealth, checkFix, checkSetup, monitorStatus, monitorKillSwitch, monitorQueueDepth, workerLogs, configShow, configValidate, configKvList, configKvGet, configSecretsList, dbQuery, agentHealthCheck, pyneHealthCheck.

Dashboard surfaces agent health (agentHealthCheck) and PYNE edge health (pyneHealthCheckPyneHealthSection).
checkHealthFix exists on the bridge but is unused by views (dashboard/settings use checkFix).


🛡️ Crash protection

Layer 1 — per-view ErrorBoundary

Every primary view is wrapped. Render failures show Retry without killing sidebar/status.

Layer 2 — CrashRecoveryApp

Process uncaughtException / unhandledRejection → CrashScreen:

  • Restart — remount AppRoot
  • Safe Mode — remount with safeMode
  • Report Bug — write $HOME/.hoox/.tui-state/crash.log

🧪 Verification

cd packages/tui
bun run typecheck
bun test --preload ./src/test-setup.ts
# from monorepo root:
bun run test:tui

E2E smoke (test/e2e/smoke.test.ts) requires an interactive TTY; otherwise it skips with a clear reason.


📊 Graph integration

  • Runtime topology view reads graph-metadata.json (repo root), resolved from CWD walk-up or package-relative path.
  • Code graph (graph.json / graph.dot) is produced by bun run graph (scripts/extract-graph.ts).
  • All 16 view exports are present as function nodes under packages/tui:…/views/… (plus dashboard subpanels).
  • Regenerate after large refactors: bun run graph (~25s). Metadata should include every worker under workers/*.

Known limitations (honest)

AreaLimitation
Worker Detail DOsNames inferred from durableObjectCount + worker name; no DO listing API
Worker Detail configFalls back to demo-ish keys when configShow empty
Trade/log live feedRequires HTTP API + SSE endpoints; offline TUI uses CLI snapshots only
usePolling hookExported but unused — views poll via effects / intervals
CLI flags--fps / --no-mouse set TUI_FPS / TUI_MOUSE; renderer reads them in main.tsx
Graph freshnessFull graph.json can lag submodules until bun run graph is re-run

Tip

OpenTUI <text> children must be strings (or text nodes) — never nest <text> inside <text>. Prefer sibling <text> inside a row <box>.

Next steps