docs/tui.md
Rama main · réplica de lectura del repositorio original.
<!-- authority:non-specification -->
# The Exomem terminal UI (`exomem tui`)
A persistent, keyboard-first surface over the same product commands the CLI,
MCP, and REST doors expose. One long-lived process (no per-command Python
startup), guided access to capture, recall, review, adoption, packs, and
diagnostics — and an honest face for the engine's own signals: warming lanes,
degraded retrieval, structured errors with remediations.
The TUI is a thin client. Every knowledge read and write goes through the
unified command registry via the same invocation seam as the CLI
(`src/exomem/product_invoke.py`): identical coercion, vault resolution, owner
principal, writer lease, and governance egress. It adds no second storage or
retrieval path and never writes vault files directly.
## Install
The UI stack is an optional extra so lean and server installs never carry it:
uv sync --extra tui # source checkout
pip install 'exomem[tui]' # package install
Without the extra, `exomem tui` prints a one-line install hint and exits
non-zero. Snapshot/interaction tests additionally need the dev group:
uv sync --extra tui --group tui-dev
Supported platforms: Linux, WSL, macOS, and Windows Terminal (Textual renders
on all four). An interactive terminal is required — a piped invocation exits
with status 2.
## Launch
exomem tui # uses $EXOMEM_VAULT_PATH
exomem tui --vault /path/to/vault
With no resolvable vault, first run opens.
## First run is a ledger
Setup is a short conversation on one screen. Each answer collapses into a line
you can read back — `✓ vault created ~/Exomem — 28 files, plain markdown` —
and the active question sits below the lines already recorded. Four steps:
vault, packs, a first capture, and asking for what you just captured. Under two
minutes, and the last step is the product demo built from your own words.
Three promises the screen keeps:
- **Nothing is written until a line says so.** Creating a vault shows exactly
what will be created — the folder, its governed layer, the file count — and
writes only after you choose it.
- **`esc` rewinds one line.** Lines that recorded a write are pinned: you
cannot un-create a folder, and the screen says so instead of pretending.
- **Everything but the vault is skippable** (`s`), and skipping leaves its own
line. "Skip for now" on the first question lands on a working, vault-less
Home where every path stays reachable.
Pointing "Create a fresh vault" at a folder that already holds a Knowledge Base
connects to it rather than failing. Choosing "Scan a folder first" inserts a
read-only report before any decision.
First run never reopens once a vault is connected.
## Screens
| Key (from Home) | Screen | What it is |
|---|---|---|
| 1 | Continue | local continuation checkpoints from the client hooks, rendered as copyable resume packets |
| 2 | Ask | measured recall with evidence — results cite source pages; deep-context packet copyable; degradation announced at the results |
| 3 | Capture | thought (immutable source) or insight (governed note); title derived from the first line; no folder decisions |
| 4 | Review | the Epistemic Inbox queue with the exact supported triage: dismiss, snooze (dated), reopen; deeper flows point at the Review Studio |
| 5 | Adopt | read-only scan of any folder (works pre-init); write modes are separate confirmed steps; originals never rewritten |
| 6 | Packs | multi-select over the built-in catalog, persisted like `setup` does; packs guide interpretation, never folder structure |
| 7 | Status | doctor preflight, resources, warm state, hooks, install provenance — each warning with its remediation |
| 8 | Settings | compute mode (quiet/normal/performance, persisted), theme toggle, vault identity |
Ask presents *measurements*, not generated prose: Exomem is a pure substrate —
the server measures, your model reasons. The copyable context packet exists
precisely to hand that evidence to a reasoning model.
## How to read the screen
Everything that happened reads as a **receipt**: a glyph, a word, then what
actually happened — `● ready`, `▲ warming`, `✓ saved`, `✗ not saved`. Status is
always a glyph *and* a word, so nothing depends on color; `NO_COLOR` and
ASCII-only terminals lose nothing but hue.
Amber marks live state only — the current step, the `▸ retrieved` header, live
wikilinks in a preview, the cursor, focus, and the selected row's `▌` bar. It
is never decoration and never an error.
Anything that fails, comes back empty, or runs degraded renders the same way:
what happened, a dim line stating what was and was not changed, then a list of
next actions you can select. No screen dead-ends, and no traceback reaches the
terminal.
## Key map
Global: `ctrl+p` command palette · `?` help overlay · `u` refresh this screen ·
`esc` back (guarded — non-empty input is never silently discarded) · `ctrl+q`
quit (confirmed). Home: `1`–`8` open screens · `enter` opens the highlighted
row. Lists: arrows and `j/k` · `enter` opens · the selected row carries `▌`.
Ask: `enter` submit · `u` re-run · `esc` steps back one layer (results → the
query, which is kept → back) · `e` evidence preview · `y` copy context packet ·
`w` save an insight. Capture: `ctrl+s` save · `tab` cycle thought/insight ·
`e` edit the derived title · `esc` back with your typing kept. Review: `enter`
context · `d` dismiss · `s` snooze · `o` reopen · `v` cycle state view. Adopt:
`m` save manifest · `c` copy as sources (both confirmed). First run: `esc`
rewind one line · `s` skip an optional step.
## Governed write-back
Saving an insight (`w` on Ask, or Capture's insight kind) writes through
`create_note`. Two governed rules are handled for you, visibly:
- every compiled note needs at least one semantic unit — the editor pre-fills
an editable `## Observations` line, and if a draft has none, one is appended
restating the title;
- a new page without a qualifying typed relation needs an explicit relation
disposition — the TUI runs the validate→commit draft flow and records your
confirmation as a `reviewed_none` disposition with an audit reason. Connect
the note properly later via `exomem connect` or the Review Studio.
## Local vs Hosted
This TUI operates the local engine in-process. A Hosted/remote connection is
not part of this surface today; `exomem doctor --profile remote` and the
connector docs cover remote posture, and the Status screen shows local install
provenance and the managed-service version when one is configured.
## Limitations
- Long-lived process, no file watcher: out-of-band edits (e.g. Obsidian)
refresh lexical search via mtime checks, but the semantic sidecar drifts
until a server or `exomem index` pass. The Ask screen's refresh action
(`u`) clears in-process caches.
- Binary media import (`/upload`, `media.process`) is not in the TUI yet —
capture text, or use adoption for markdown folders.
- Review depth: proposal-first flows (supersede, compile, accept relations)
deliberately live in the browser Review Studio; the TUI links to it (the
Studio requires the http transport to be serving).
- Cooperative cancellation: a cancelled ask stops affecting the UI
immediately; the underlying computation may finish in the background.
## Troubleshooting
- `exomem tui` exits 2 with "needs an interactive terminal": stdin/stdout is
piped or redirected — run it in a real terminal.
- Install hint on launch: the `tui` extra is missing — `uv sync --extra tui`.
- "partial results — still warming": the background warm hasn't finished;
results are lexical-only until it does. `exomem warm` pre-loads models
explicitly.
- Ask shows skipped lanes on a lean install: expected — without the
`embeddings` extra retrieval runs keyword/BM25 only, and the TUI disables
the model lanes at startup so this is not reported as degradation.
- Writes reporting `MUTATION_BUSY`/`WRITER_LEASE_REQUIRED`: another process
(usually the running server) holds the mutation boundary — retry after it
finishes; reads stay available.
## Running the TUI tests
uv run --frozen --extra tui --group tui-dev python -m pytest tests/tui tests/test_tui_entry.py -q
Snapshot goldens live in `tests/tui/__snapshots__/` and render exclusively
from the deterministic fake backend with synthetic paths (the committed SVGs
are scanned by the public-artifact privacy gate — never regenerate them
against a real vault). Intentional regeneration:
uv run --frozen --extra tui --group tui-dev python -m pytest tests/tui/test_snapshots.py --snapshot-update
To review layout as a character grid without a terminal (development aid; it
drives the same deterministic fake):
uv run --extra tui python scripts/tui_frames.py home first-run ask
The lean matrix (no `tui` extra) skips `tests/tui` entirely; `tests/test_tui_entry.py`
and `tests/test_product_invoke.py` run everywhere.