# Interactive TUI
`mind probe` launches an interactive terminal UI: a browsable, searchable view of
every source and item, and the interactive front end for the rest of the CLI
(meld, learn, sync, upgrade, config). The non-interactive catalog listing
(cli.md, CLI-80..85) remains available behind an opt-out. Built on `ratatui` +
`crossterm`.
This document is the spec for that mode. Verbs it drives are defined in cli.md;
the lock it takes per action is defined in storage.md (STO-40, STO-41).
## Entry and modes
- `TUI-1` `mind probe` with no opt-out launches the interactive TUI. It is the
primary interactive entry point. Launching the TUI requires a TTY on stdout.
(Bare `mind`, with no subcommand, does NOT launch the TUI; see TUI-2.)
- `TUI-2` `probe` falls back to the non-interactive catalog listing (CLI-80..85)
when any of these holds: `--no-tui` is given, `--json` is given, or stdout is not
a TTY (piped or redirected). The `query`, `--kind`, and `--source` arguments
apply in both modes: in the listing they filter it (CLI-80, CLI-83); in the TUI
they seed the initial search and filter state. Bare `mind` (no subcommand) is
unchanged and does not launch the TUI.
- `TUI-71` `probe` also falls back to the non-interactive listing (extending
TUI-2's conditions) when the output mode is Unicode-hostile: `--ascii` is given,
or the active locale is not UTF-8 (`LC_ALL`/`LC_CTYPE`/`LANG`, first set wins;
none set is treated as non-UTF-8). The TUI draws Unicode box-drawing glyphs, so
a run that asked for ASCII output, or a terminal whose locale cannot render
them, must not silently launch it. This is distinct from `NO_COLOR`, which does
NOT force the fallback: the TUI still launches and renders monochrome (TUI-65).
- `TUI-3` (removed, superseded by TUI-54) `-n` was formerly the short form of
`--no-tui` on `probe`. Removed to free `-n` globally for `--dry-run` (CLI-163).
`--no-tui` is now long-only; see TUI-54.
## Browse tree
- `TUI-10` The view has two top-level groups: **Installed** and **Available**.
Each is an independently collapsible tree. A third **Unmanaged** group appears
below them when the agent home holds items mind did not install (UNM-6).
- `TUI-11` Under each group the hierarchy is source -> kind (skills, agents,
rules) -> item -> detail. Left/Right (and Space) collapse/expand structural nodes
(source and kind buckets); Enter opens the details dialog on a source or item
(TUI-26) and toggles a group header or kind bucket. Collapse state for structural
nodes is tracked in a per-node collapsed
set distinct from the item-detail expansion set, so a source's items can be
hidden and re-shown independently of item detail. Expanding an item shows its
description and frontmatter, and for a skill its file tree. Navigation is
keyboard-driven (move, expand, collapse, page, jump to search).
- `TUI-12` **Installed** is the manifest (manifest.json): installed items grouped
source -> kind -> item, each showing effective name, source, and short commit,
matching what `recall` reports (CLI-70).
- `TUI-13` **Available** aggregates, de-duplicated: (a) catalog items of melded
sources that are not installed; (b) not-yet-melded sources suggested by the
registry (TUI-31); and (c) ad-hoc sources the user enters (TUI-30). A melded
source's items are known from its catalog; a not-yet-cloned Available source is
shown as a collapsed node whose items are populated by a preview on expand
(TUI-30).
- `TUI-14` A search box filters the visible tree by case-insensitive substring
over item name and description (consistent with CLI-85), across both groups, and
composes with the active kind and source filters. Clearing the search restores
the full tree.
- `TUI-15` The TUI polls the on-disk registry (sources.json) and manifest
(manifest.json) on a short interval (about once a second), under a brief shared
lock (TUI-25), so changes made by another `mind` process or a direct edit appear
without a manual reload. A refresh preserves the current selection, expansion,
and search state, and is skipped while a mutating action holds the lock. Catalog
contents are re-scanned when the melded source set changes or after a sync, not
on every tick.
- `TUI-16` The list keeps the highlighted row within the middle
two-thirds of the visible area: as the selection moves, the view scrolls to
maintain a margin of about one-sixth of the visible height above and below the
highlight, so the highlight does not reach the top or bottom edge while there are
more rows to scroll in that direction. Near the start or end of the list the
highlight may sit at the edge, since there is nothing further to scroll. The
margin is derived from the current viewport height, so it adapts to terminal size
(TUI-42).
## Actions (CLI parity)
Each action invokes the same verb the CLI exposes, against the same registry,
manifest, and store.
- `TUI-20` Install the selected Available item (`learn`, CLI-30); uninstall the
selected Installed item (`forget`, CLI-40). Installing on a higher node selects
in bulk, so the user need not name each item: a Source installs every available
item from it (`learn '<source>#*'`), a kind bucket every item of that kind
(`<source>#<kind>:*`), and the Available group everything (`learn '*'`). The
selection flows through the same closure/confirm path, and already-installed
items are skipped (DEP-23).
- `TUI-21` Meld the selected/entered source (`meld`, CLI-10); unmeld a melded
source (`unmeld`, CLI-20). The TUI's unmeld uninstalls the source's installed
items (the `--forget` purge, CLI-22) by default; this is a destructive action
and is confirmed before applying (TUI-24).
- `TUI-22` Sync all or the selected source (`sync`, CLI-50); upgrade pending or the
selected item(s) (`upgrade`, CLI-60), showing the same deltas and confirming
before applying (CLI-61).
- `TUI-23` View and manage agent homes (`config lobes` list / add / remove,
CLI-111..113).
- `TUI-24` Every mutating action confirms before applying; destructive actions
(`forget`, `unmeld --forget`) require an explicit confirmation. Results and
errors are shown inline; a `MindError` is surfaced in the UI, not printed to a
hidden stderr. The verb's own stdout is captured for the duration of the action
so it cannot corrupt the alternate screen; a one-line summary of it is shown in
the status bar. After a successful mutation the affected tree refreshes.
- `TUI-25` The TUI holds no lock while idle. It acquires the global lock
(storage.md) only for the duration of a single operation: a shared lock while
loading or refreshing state, an exclusive lock for each mutating action
(STO-40, STO-41), releasing it immediately after. A running TUI therefore never
blocks another `mind` invocation for longer than one operation.
- `TUI-26` Pressing Enter on a source or item opens a details dialog: a centered
overlay (TUI-42) describing the node and listing the actions valid for it as a
selectable list, each run through the normal confirm-and-execute path (TUI-24).
For an item it shows its kind, source, the commit when installed, and the
description, and offers Install when the item is not installed, else Forget (an
unmanaged item offers Forget with the not-managed warning, UNM-5). For a source
it shows its name and installed/available item counts, and offers Install all
available items, Uninstall all installed items, and Unmeld; an action is omitted
when it would do nothing (no Install-all with nothing available, no Uninstall-all
with nothing installed). j/k move the highlight, Enter or y runs the highlighted
action, and Esc, q, or n dismisses without acting. An Install action arms the
same dependency-closure preview as the direct `i` action (DEP-40). The dialog is
an additional entry point and does not remove the direct action keys (TUI-20..22).
On a group header, kind bucket, or suggested source there is no dialog, so Enter
keeps its TUI-11 toggle/preview; expansion is also on Space and Left/Right.
- `TUI-50` An item node is expandable to show its dependencies. Space (and
Left/Right) on an item node toggles a dependency subtree: its direct
dependencies (DEP-4, the union of `{{ns:}}` and `requires` edges) appear as child
nodes, each itself expandable, so the user walks the graph in place. It is
cycle-safe: a dependency that would revisit an ancestor on the current path is
shown as a marked back-edge and not expanded again (DEP-22). This extends the
TUI-11 expansion (which toggled only source and kind buckets) to item nodes. The
dependency children are a view of the graph, distinct from each item's own
canonical line under its source -> kind bucket.
- `TUI-51` Pressing Enter on a dependency child node (TUI-50) moves the cursor to
that dependency's canonical item line (its node under its source -> kind bucket),
expanding any collapsed ancestors needed to reveal it, rather than opening the
details dialog. Enter on a normal item line keeps the TUI-26 details dialog. So
Enter on a dependency navigates to the real item, where its own actions and
dependency subtree are then available.
## Preview and registry (browsing the not-yet-melded)
- `TUI-30` Melding a hand-entered repo spec (the `m` action; any form `meld`
accepts, CLI-11) runs the interactive `meld` directly: the TUI suspends to the
normal terminal (TUI-44) and runs `meld` (CLI-10) so the clone and every prompt
-- the namespace prompt (CLI-24), the install-hook disclosure (HOOK-20), the
install-items confirmation (CLI-23), and any SSH passphrase or host-key prompt
(TUI-45) -- behave exactly as they do from the CLI. No preview is pre-cloned: the
source is already named. This is the interactive form of `meld`.
- `TUI-31` The Available registry of suggested, not-yet-melded sources is the
union of the `[discover].sources` entries declared by all melded sources
(DSC-38), de-duplicated by URL and excluding sources already melded. Expanding a
registry entry shallow-clones it to a temporary preview area and shows its
catalog tree under Available without registering it; confirming promotes the
preview to a real meld (the suspended interactive flow, TUI-44/TUI-21) and
declining discards the temp clone.
## Terminal handling
- `TUI-40` The TUI enters and leaves the alternate screen / raw mode cleanly and
restores the terminal on normal exit, on error, and on panic, so a crash never
leaves the terminal in a broken state.
- `TUI-41` Quitting leaves no partial state: every mutation was already committed
per-action under the lock (TUI-25), so there is nothing to roll back on exit.
- `TUI-42` Rendering is responsive to the terminal size: the status and key-hint
lines wrap to the available width (growing to a bounded number of rows), and
every centered overlay (the confirm modal, the meld and lobe-path input
dialogs, the lobes modal) is clamped to the terminal width and height. Content
is never cut off the right edge or pushed off screen on a narrow terminal, so
there is no minimum-width requirement. The TUI may use Unicode (box drawing,
geometric node markers) for presentation; the ASCII-only convention applies to
written prose, not the interface.
- `TUI-43` Ctrl-C is a force-exit available from every mode (the search box, the
spec-input and lobe-path inputs, and the modals), not only the normal-mode `q`.
It is intercepted before mode routing, so a `Char('c')` is never entered as
text. One Ctrl-C arms and shows a hint; a second consecutive Ctrl-C exits, so a
single accidental Ctrl-C while typing does not quit. Any other key disarms.
- `TUI-44` Actions whose verbs may prompt interactively run with the TUI
suspended: `mind` leaves raw mode and the alternate screen, runs the verb on the
normal terminal so its prompts read stdin and write stdout exactly as the CLI
does (never captured, never blocked behind raw mode), then restores the alternate
screen and redraws. After the verb the user presses Enter to return, so the
verb's output is readable before the browser redraws. The verbs that suspend are
`meld` (install-hook disclosure HOOK-20, install confirm CLI-23, SSH passphrase
TUI-45) and `unmeld` (uninstall-hook prompt HOOK-54). Non-prompting mutations
(learn/forget/sync/upgrade) instead run with stdout captured (TUI-24) and do not
suspend.
- `TUI-52` When forgetting a single installed item that other installed items
depend on (DEP-60), the TUI surfaces the warning in the confirmation
description -- listing the dependent keys -- before the user confirms. The
action still proceeds on confirmation; the TUI does not block the removal.
This mirrors the CLI's DEP-60 warning, adapted to the TUI's confirm-then-act
flow (TUI-24) rather than a stdin prompt.
- `TUI-45` While the TUI holds the terminal, every `git` child runs
non-interactively (`GIT_TERMINAL_PROMPT=0` and an ssh `BatchMode=yes` wrapper),
so an auth-required remote -- a private SSH repo whose key needs a passphrase, or
an unknown host key -- fails fast with an error surfaced inline (TUI-24) instead
of hanging the UI on a hidden prompt the user cannot see or answer. The suspended
interactive meld (TUI-44) restores interactive git for its duration, so that same
passphrase or host-key prompt works on the normal terminal. This is why a typed
spec (TUI-30) can meld a private SSH source while a background preview, sync, or
upgrade of one fails fast rather than freezing.
- `TUI-53` The source details dialog (TUI-26) shows the namespace the
source will install under (its effective prefix: the consumer `--namespace` alias,
else `[source].prefix`, else none; NS-1). When none of the source's items are
installed, the namespace is editable in the dialog (an input field), and the
edited value is persisted as the source's display prefix (`alias`) before the
Install-all action runs (NS-30). This edits the display prefix only, not the
source's identity alias (STO-58), so it never renames the source or relocates
its clone. When any of the source's items are installed the namespace is shown
read-only with a note that it is locked until those items are forgotten (NS-30,
CLI-161).
- `TUI-54` `probe --no-tui` is long-only; its former short `-n` is removed (CLI-164,
CLI-163). `mind probe --no-tui` is the only accepted form; `mind probe -n` is now
a usage error.
- `TUI-63` An installed row carries a `stale` flag, computed at the snapshot
boundary (data.rs) the same way the CLI's CLI-75 outdated check is computed at
its three call sites: the source content hash no longer matches the recorded
manifest hash, or the effective name has changed (a rename, e.g. a prefix
change). A stale row shows a trailing drift marker (`\u{2191}`, ASCII `^` under
TUI-65) matching the CLI's CLI-155 glyph, and its details dialog (TUI-26) adds
an `out of date` status line. The upgrade confirm (`u`, TUI-22) is no longer
blind: instead of a bare "Upgrade all pending items?", it lists every stale
item by key and source (mirroring the per-item delta the CLI's `upgrade`
reports before applying, DEP-40), or states explicitly that nothing is out of
date when the stale set is empty, so the user always confirms a specific,
named set of changes rather than an opaque bulk action.
- `TUI-64` Pressing `?` in normal mode opens a keymap help overlay listing every
binding grouped by category (navigation, actions, general); any key closes it,
intercepted ahead of normal-mode routing so it never leaks into search or an
action. The bottom hint line (HINTS) additionally names `/` (search), `h/l`
(collapse/expand), paging, and `?` (help), which it previously omitted
alongside the always-abbreviated action keys.
- `TUI-65` The TUI honors the same output-capability signals the CLI's
`render::OutputCtx` already resolves once at startup (CLI-150, CLI-151,
CLI-154): `NO_COLOR` (any value) disables every `fg`/`bg` color in the tree
rows, selection highlight, modal borders, and status/hint text -- a monochrome
style set that keeps BOLD/DIM/REVERSED (video attributes, not colors) so
structure and selection stay visible without emitting color codes. A non-UTF-8
locale (the same `detect_utf8_locale` check) disables the Unicode geometric
markers, disclosure triangles, and highlight symbol in favor of an ASCII
fallback (`+`/`o`/`*`/`-`/`?`/`^`/`>` for node kinds, `v`/`>` for disclosure,
`>` for the highlight symbol and the dialog action marker), so a non-UTF-8
terminal never renders mojibake in place of a box-drawing or geometric glyph.
- `TUI-67` Text measurement for wrapping and modal sizing (TUI-42) is in display
columns (`unicode-width`), not a raw char count, so a line containing a wide
CJK/emoji character is not under-counted and does not wrap or size later than
it actually would on screen. A truncated item description in a tree row
(`truncate`, 50-char budget) carries a trailing `...` marker, so a cut
description reads as "more text exists here" rather than looking like the
description simply ends there; untruncated text is returned unchanged.
- `TUI-68` An empty Installed or Available group shows a call-to-action row
instead of a bare, unexplained blank list, using the same wording the CLI's
plain listing already uses (CLI-187) so the message is consistent across
surfaces: `no sources melded; run \`mind meld <owner/repo>\` to add one` when
no source is melded at all, or `no items match '<query>'` when a search
matched nothing. A group that is empty for a legitimate reason (sources are
melded, nothing has been installed yet, and no search is active) gets no
synthetic row -- that state needs no explanation.
- `TUI-69` Esc on a settled search filter (non-empty, not focused -- the user
already submitted it with Tab/Enter, TUI-14) is a two-step clear: the first
Esc in normal mode arms the clear and shows a status hint instead of wiping
the filter immediately, and only a second CONSECUTIVE Esc clears it. Any
other intent in between (moving the selection, an action key, and so on)
disarms the pending clear, so a later, unrelated Esc is treated as a fresh
first press rather than completing an old one. Esc still clears immediately
in the two cases where that is the expected, unsurprising behavior: while
actively typing in the search box (`search_focused`, bailing out of entry),
and when there is no active filter to protect (nothing to arm).
- `TUI-70` The status/error line's row budget scales with the terminal height
rather than a flat 3-row ceiling: a chained `MindError` can run several
sentences, and clamping it to 3 wrapped rows regardless of terminal size cut
it off with no way to read the rest. A fixed reserve (the search bar, a
minimum tree row, a minimum hint row) is always held back, so a very long
message still cannot starve the tree pane on a short terminal, but a taller
terminal shows correspondingly more of a long message.
- `TUI-62` The lobes action (TUI-23) shares its implementation with the CLI:
`ActionKind::LobeAdd` dispatches to `commands::lobe_add`, the same function
`mind config lobes add <path>` calls, with `force` hardcoded to `false`. So the
CLI's guarantees around it apply to the TUI path with no separate
implementation: the resolved lobe directory is created before the config entry
is written (HARN-15), already-installed items of admitted kinds are backfilled
into it unconditionally (HARN-7, HARN-17), and a pre-existing foreign file at a
backfill target is reported as a failure -- naming the `--force` remedy -- and
never clobbered, because the TUI's call site never passes `force`.
## Security
- `TUI-60` All source-derived strings entering the TUI snapshot model --
item names, descriptions, source names, and any other catalog-controlled text --
are normalized through `strip_ansi` (the same sanitization applied at the CLI's
display sites, DSC-69 / MKT-9) before being stored in the `Snapshot`. This
prevents terminal injection from catalog-controlled content on the TUI's
default interactive surface, including during destructive-action confirms.
- `TUI-61` The stdout-capture file `with_captured_stdout` uses to keep a mutating
action's verb output off the alternate screen (TUI-24) is created with the same
exclusive-create scheme as the `evolve` download/auth-config temp files
(STO-45, STO-61): an unpredictably-named, mode-0700 temp directory
(`mktemp_dir_prefixed("mind-tui-capture")`, `create_dir` not `create_dir_all`)
holding a file opened with `create_new(true)` and mode 0600. A predictable path
opened with plain `create(true).truncate(true)` (the pre-fix behavior) lets a
local attacker who pre-creates that path as a symlink get the symlink target
truncated and overwritten the next time any user on the host runs a capturing
TUI action, and `remove_file` afterward unlinks only the symlink, leaving the
target's damage in place. `create_new` refuses to open through a pre-existing
path (symlink or otherwise), so the write never follows an attacker-planted
symlink.