# PROP-037: `vibe tree` — the interactive TUI application {#root}

@status:impl/done

[p01] @fact:status-line **Status: IMPLEMENTED** (requirements authored 2026-07-15, owner-commissioned;
TREE-TUI-PLAN v0.2 executed against them, verified against the tree 2026-07-25
by the spec-actualization campaign — the visual language matches byte for byte
and the shipped F-key map is the one this Spec 2 text defines); **revised
2026-07-15 (Spec 2)** onto the action system: the TUI is a `Surface` on
`vibe-actions` (PROP-039), every command is an addressed action (§13), Search
Everywhere (§7.3) is promoted from a stub to a shipped feature, and the i18n
mechanism (§1.6) is now real. Extends
[PROP-036 §2.11](PROP-036-package-tree.xml#tui) (the analyzer's TUI sketch) into
a full application contract. @status:impl/done

[p02] @fact:related **Related:** [PROP-036](PROP-036-package-tree.xml) (the `vibe tree` analyzer +
the `PackageTree` model this app renders); **[PROP-039](../vibe-actions/PROP-039-action-system.xml)
(the `vibe-actions` action system this TUI is built on — §13)** + its design-doc
[`vibevm/vibespecs/design/action-system.xml`](../../design/action-system.xml); [core-ai-native
discipline](`spec://org.vibevm.ai-native/core-ai-native`) (§11). Plan (archived):
`legacy-spec/terraforms/TREE-TUI-PLAN-v0.1.md`. @status:spec/done

[p03] @fact:language-line **Language:** the shipped UI is **English** (§1.6); the **i18n mechanism ships**
(PROP-039 §8, §13.4) with English the only mandatory-complete locale — no
non-English catalogues are shipped now (§12). @status:spec/done

[p04] @fact:GRANULAR-ADDRESSABLE This contract is deliberately **granular and addressable** (owner directive):
every feature is its own `{#anchor}` REQ, cited by the code via `specmark`, so a
large feature set stays a map rather than a tangle. A REQ is the unit of work. @status:spec/done

## 0. What "finished" means for this subsystem {#done-definition}

[p05] @fact:DONE-IS-THINNESS-NOT-FEATURE-COUNT **Owner ruling, 2026-08-06: this subsystem is finished when it is a
THIN SURFACE** — when every capability it presents lives in a library and
the TUI only renders. Not when a list of screens is complete, not when a
suppression count reaches zero. @status:spec/done

[p06] @fact:THE-TEST-IS-DELETION-ON-PAPER **The test:** delete the TUI on paper. If anything but presentation and
input handling is lost, the split is wrong and the work is not done. This is
`flow:omnichannel`'s own test
(`spec://org.vibevm.world/omnichannel/flows/omnichannel/OMNICHANNEL-PROTOCOL#thinness`),
applied to the surface that most invites logic to accumulate in it. @status:spec/done

[p07] @fact:WHY-THIS-BOUNDARY-AND-NOT-THE-OTHERS **Why this boundary and not the obvious ones**, recorded because three
alternatives were measured and offered on the same day. A *screen list* has a
visible end but requires deciding which screens should exist — it answers
"how much" rather than "what for". A *suppression count* (27 carry no reason
today, of 53 at the perimeter below) is hygiene: all 27 could close without
the subsystem moving a step. *Manual tests* — and the TUI is the one surface
automated tests cannot really exercise — are worth having and their number
grows with the screens, so they define no end. Thinness is the only one of
the four that a new screen does not move. @status:spec/done

[p08] @fact:MANUAL-TESTS-ALREADY-EXIST-FOR-THIS-SURFACE **Correction, 2026-08-06.** The
line above said there were none. There are three, and two are this subsystem's:
`vibevm/vibespecs/manual-tests/MT-02-vibe-tree-tui.xml` (the tree TUI's visual sign-off) and
`MT-03-vibe-prefs-tui.xml`, beside `MT-01-vibe-tree.xml` for the plain renderer.
The rejection of manual-tests-as-boundary never depended on the count and still
holds; the parenthetical was simply false about a file three directories away —
the exact shape this campaign exists to find. @status:impl/done

[p09] @fact:WHAT-THE-MEASUREMENT-FOUND The measurement that framed the question: **63 files, 18 426 lines, 258
tests inside the subsystem.** It is not a sketch, and its problem was never
incompleteness — it was an undefined edge. @status:spec/done

[p10] @fact:THE-PERIMETER-OF-THAT-MEASUREMENT **The perimeter, so the number is
reproducible instead of merely quoted** *(added 2026-08-06)*: the two `tui/`
trees — `crates/vibe-cli/src/commands/tree/tui/**` and
`crates/vibe-cli/src/commands/prefs/tui/**`. It deliberately excludes
`commands/tree/`'s own model and build code, which is the library half the
deletion test is about, and that exclusion is why the figure is the right one
to audit against. Both numbers this section quotes reproduce at exactly this
perimeter and at no neighbouring one: `commands/tree/` alone gives 52 files and
13 117 lines, and adding all of `prefs/tui` to it gives 72 and 20 372. A number
whose perimeter is unstated is a number the next reader re-derives wrongly —
this one was recovered by measuring five candidate perimeters. @status:impl/done

[p11] @fact:THE-LAYERING-LAW-IS-THE-SAME-LAW This is the §1 layering law seen from outside: *vibevm logic never leaks
into the app* is thinness stated inward, and the deletion test is how it is
checked from without. They are one rule with two readings, not two rules. @status:spec/done

[p12] @fact:THE-AUDIT-RAN-AND-THE-TWO-CLAUSES-DISAGREE **The audit ran on
2026-08-06, over the perimeter this section fixes, and its result is a split
verdict — because this section's definition has two clauses and the subsystem
satisfies one of them.** The deletion test passes: no domain capability lives
only inside the perimeter, so deleting the TUI on paper loses presentation,
input handling, the surface's own persisted UI state (`vibe.tree.*`) and
TUI-specific features, and nothing else. Version comparison, dependency
resolution, manifest validation and package-state computation are absent from
the perimeter entirely — the app consumes them through `PackageTree`, which is
§1.2's one seam working as written. @status:impl/done

[p13] @fact:WHAT-FAILS-IS-ONLY-RENDERS-NOT-THE-DELETION-TEST **What fails is the
other clause: «the TUI only renders».** The settings persist path is written
**three times** inside the perimeter — `prefs/tui/form/lifecycle.rs:78-89` and
`prefs/tui/form/provenance_edit.rs:52` re-implement the scope gate, and
`tree/tui/settings.rs:547` re-implements the dotted-path mutation — while
`vibe-settings` already ships both and the CLI already uses them
(`crates/vibe-settings/src/cli/mod.rs:231-244` and its `set_dotted` at `:247`).
Nothing is lost if the TUI is deleted, so the deletion test is silent about it;
the work is nonetheless done twice, which is what «only renders» forbids. @status:impl/done

[p14] @fact:THE-CONTRAST-IS-INSIDE-THE-SAME-PERIMETER **The contrast that makes this
a defect rather than a matter of taste sits in the same subsystem.** The lint
modal calls the library's `validate` (`crates/vibe-settings/src/schema/validate.rs:130`)
instead of copying it, and `diff_from_default` is likewise called rather than
re-written. The surface therefore already knows how to be thin in two places
and is thick in a third — so the fix is a known shape, not a new design. @status:impl/done

[p15] @fact:THE-REMAINING-WORK-IS-BOUNDED-AND-NAMED **The one bounded piece of work
that stood between this subsystem and finished is done (2026-08-20):** the
three persist copies collapsed onto the library seam — B-077's landing gave
`vibe-settings` the public `check_writable` + `set_in_layer` pair and every
TUI write goes through them; the thinness audit's sole surviving violation is
closed. What remains around the perimeter is not contract work but hygiene:
the `#[allow(dead_code)]` shadow (the 2026-08-03-04 audit row), drained by
the owner's «построить до конца» directive — each site wired, deleted, or
carrying a recorded reason. @status:impl/done

[p16] @fact:TWO-CANDIDATES-WERE-WEIGHED-AND-CLEARED **Two candidates were weighed and
deliberately cleared, recorded so they are not re-opened as new findings.** The
filter/shape/order pipeline (`flatten.rs`, `shape.rs`, `sort.rs`) is
display-shaping over a graph that stays in the library, and §1.3 assigns
filtering, ordering and flattening to the app's own Model layer — legitimate
today, though a second tree-shaped consumer would turn it into a lift candidate.
The `vibe.tree.*` parse functions are the wire format of the surface's **own**
keys, so they are own-state rather than leaked domain. @status:impl/done

[p17] @fact:ONE-MEASUREMENT-OF-THIS-SECTION-IS-SOFTER-THAN-IT-READS **A correction to
this section's own numbers, produced by the audit that reproduced them.** All
four reproduce exactly — 63 files, 18 426 lines, 258 tests, 53 suppressions of
which 27 carry no reason — but «carries no reason» means «no `//` comment on the
same line`». Several of the 27 carry their reason on the line above
(`theme/mod.rs:41-47`, `ui/mod.rs:80-91`, `ui/button.rs:50,57`), so under a
softer reading the figure is smaller. The strict reading is the one that
reproduces, and it is the one this section means. @status:impl/done

## 1. Architecture — layers {#architecture}

- [p18] @fact:layers-intro The application separates, as fixed layers, (a) the vibevm data it renders, (b)
  its own model + UI state, (c) the reusable view components + their styling, and
  (d) the event/control logic. @status:spec/done
- @fact:LAYERING-LAW The one-line law: **styling never leaks into
  control or app logic, and vibevm logic never leaks into the app.** @status:spec/done

### 1.1 The four layers {#layers}

[p19] @fact:FOUR-LAYERS REQ. The code is organized as four layers with one-directional dependencies
(each may depend only on those above it): @status:spec/done

1. [p20] @fact:LAYER-BACKEND **vibevm backend** — `vibe-core` / `vibe-workspace` / `vibe-spec` (the
   `PackageTree` builder, PROP-036). Unowned by this app. @status:spec/done
2. @fact:LAYER-MODEL **Model** — the app's data: the borrowed/owned `PackageTree` plus all UI
   state (§1.3). Pure data + logic-on-data; **no rendering, no event types**. @status:spec/done
3. @fact:LAYER-VIEW **View** — the component library (§2) and the theme (§2.2). Renders Model +
   component state into a ratatui buffer; **no app control flow**. @status:spec/done
4. @fact:LAYER-CONTROLLER **Controller** — event routing, the keymap registry (§5.1), the modal stack
   (§6), and the actions that mutate the Model (§1.5). @status:spec/done

[p21] @fact:MVC-CYCLE The MVC cycle: event → Controller (routes to the focused component / top modal,
produces an Action) → Action mutates Model → View renders Model + component
state through the Theme. @status:spec/done

### 1.2 The vibevm boundary {#backend-boundary}

[p22] @fact:BACKEND-BOUNDARY-REQ REQ. The app consumes vibevm through exactly one seam — the `PackageTree` model
(PROP-036) and a small read interface over it. No view or controller code
reaches into `vibe-core` / `vibe-workspace` / `vibe-spec` types directly; if the
app needs a vibevm fact, it is exposed as a method/field on the app Model's
projection of `PackageTree`. This keeps the analyzer free to evolve without
dragging the UI with it. @status:spec/done

### 1.3 Model {#model}

[p23] @fact:MODEL-REQ REQ. The Model owns: the analyzed `PackageTree` (read-only vibevm data); and the
**UI state** — the active display mode (§4), the sort/shape configuration *per
context* (§3.2, §7.2), the fold set + selection + horizontal pan, the active tab,
the sub-table block order, and the loaded settings (§9). The Model exposes
queries and mutating methods; it never renders and never names a key or a
crossterm event. Model logic (filtering, ordering, flattening) is unit-tested
without a terminal. @status:spec/done

### 1.4 View — components + theme {#view}

[p24] @fact:VIEW-REQ REQ. All on-screen output is produced by the component library (§2). A component
renders itself from its own small state plus the Model and the `Theme`; a
component never reads keys or mutates the Model. The **Theme** (§2.2) is the sole
source of colors, styles, spacing, and glyphs — the "CSS" — so a restyle touches
only the theme, never component logic. @status:spec/done

### 1.5 Controller — events, keymap, modals, actions {#controller}

[p25] @fact:CONTROLLER-REQ REQ. The Controller receives each terminal event and routes it: to the **top
modal** if the modal stack (§6) is non-empty, else to the focused base-screen
component, else to the global keymap. Routing is driven by the **mode-aware
keymap registry** (§5.1), never by an ad-hoc `match` scattered across the app.
An event resolves to an **Action** (a typed intent — e.g. `OpenModeMenu`,
`ToggleFold`, `Quit`); Actions mutate the Model. A terminal resize always
requests a repaint (already fixed; the repaint law: rat-salsa repaints only on
`Control::Changed`). @status:spec/done

### 1.6 English-only, i18n-ready {#i18n}

[p26] @fact:I18N-READY-REQ REQ. All user-facing strings are English and live behind a single string
accessor (a `strings` module / a `label(key)` indirection), so a future
localization layer swaps the source without touching call sites. No localization
is shipped now (§12); the indirection is the only present obligation. @status:spec/done

## 2. The component library {#components}

[p27] @fact:component-library-intro A reusable, vibevm-owned widget library — the abstraction that stops "a million
implementations of the same control" from accumulating. @status:spec/done

### 2.1 Component strategy — wrap, extend, or invent {#component-strategy}

[p28] @fact:COMPONENT-STRATEGY REQ. Components are built in this order of preference: **(1) wrap** a `rat-widget`
widget behind our `ui::` API + Theme (Menu, Button, TextInput, scrolling,
Tabbed, popup, MsgDialog — reuse, do not reinvent); **(2) extend** in
rat-widget's idiom when a needed widget is not in rat-widget — author it as
though continuing the rat-widget family, but on the vibevm side; **(3) invent**
on bare `ratatui-core` only as a last resort, when the need is unthinkable within
rat-widget's ideology. Every component has exactly one implementation behind one
`ui::` facade; call sites never touch `rat_widget::` directly. @status:spec/done

### 2.2 The theme {#theme}

[p29] @fact:THEME-REQ REQ. A single `Theme` value carries every color, text style, border style,
spacing, and glyph the components use (selection highlight, window chrome, group
frames, the `[x]` close glyph, subheaders, disabled/enabled button, etc.).
Components take `&Theme`; no component hard-codes a `Color`/`Style`. The theme is
terminal light/dark aware where it matters. The theme is the TUI's "CSS"
(§1.4): a restyle touches only the theme. @status:spec/done

[p30] @fact:visual-language-note The visual language is **a first-class part of this contract**, not an
afterthought. The lore (aesthetics, rationale, the full tables the eye reads)
lives in the design doc
[`vibevm/vibespecs/design/tui-visual-language.xml`](../../design/tui-visual-language.xml);
the normative REQs the code is traceable to are the four anchors below. @status:spec/done

#### 2.2.1 Palette tokens {#palette-tokens}

[p31] @fact:PALETTE-TOKENS-REQ REQ. Colour reaches a component only through a **`Palette`** — a data-driven
mapping from **semantic role tokens** to `Color`. The role set is exactly:
`base`, `surface0`, `surface1`, `muted`, `subtext`, `text`, `accent`, `love`,
`gold`, `foam`, `rose`, `selection`, `border`, `paper`, `button_on`,
`button_off`. No component names a `Color` literal; it names a role. @status:spec/done

[p32] @fact:FIVE-PALETTES REQ. Five palettes ship, each a complete role→`Color` mapping. **Rosé Pine**
(the cosmic-violet look already in `theme.rs`) is **canonical-locked**: its
eleven `Color::Rgb` values are preserved exactly (a snapshot test pins them).
**Catppuccin Mocha, Macchiato, Frappé** (dark) and **Catppuccin Latte** (light)
are the canonical Catppuccin values (`accent`←mauve, `love`←red, `gold`←yellow,
`foam`←teal, `rose`←pink, `muted`←overlay0, `subtext`←subtext0). The canonical
hex values for all five are normative in the design doc §3. @status:spec/done

[p33] @fact:PALETTE-IS-LIGHT REQ. A palette carries an `is_light` flag; `selection` is composed (`accent`
ground + `base` text, bold), `border` = `muted`, `paper` = `surface0`, and the
`paper`/`selection` rendering inverts against `is_light` so a light theme's
detail card reads correctly. The active palette is a `Model` field, persisted
through the settings system (§9) and overridable at the CLI/env. @status:spec/done

#### 2.2.2 Glyph vocabulary {#glyph-vocabulary}

[p34] @fact:GLYPH-CONSTANTS REQ. Every glyph is a constant on the `Theme`, never a hardcoded string at a
call site. The fold indicator is `▾`/`▸` (not `+`/`-`); the DAG re-occurrence
marker is `↩` (not `(*)`); the on/off flags are `●`/`○` (not `x`/`.`). Tree
connectors stay `│├└─`; the frame stays rounded `╭╮╰╯`; the close affordance is
`✕`; the bar indicator uses block elements `▁▂▃▄▅▆▇█` (or braille). The full
replacement table is normative in the design doc §2. @status:spec/done

[p35] @fact:NO-ASCII-SEMANTIC REQ. In the primary UI (Tier ≥ 1) there are no `+`/`-`/`*`/`#`/`x`/`.` used as
**semantic** glyphs. Those ASCII characters appear only behind the Tier 0
fallback (§2.2.3). @status:spec/done

#### 2.2.3 Rendering tiers {#rendering-tiers}

[p36] @fact:FOUR-TIERS REQ. Rendering degrades through four tiers — **3** (truecolor: full RGB, rounded
frames, braille/blocks), **2** (256-colour: palette quantised to the 6×6×6 cube,
rounded, blocks), **1** (16 ANSI: role→ANSI mapping, rounded-or-square frames,
blocks), **0** (explicitly dumb — `TERM=linux` (the Linux VT) or `TERM=dumb`:
ANSI mono, ASCII `+-|` frames, `#` indicators). @status:spec/done

[p37] @fact:TIER-DETECTION REQ. Tier detection is a **pure function** over the environment —
`detect_tier(colorterm: Option<&str>, term: Option<&str>) -> Tier` (`$COLORTERM`
first, then `$TERM`; `crossterm` exposes no colour-count API). The TUI reads the
env once at launch in a sanctioned spot and feeds the values in; the detected
tier is overridable through the settings system (§9). **The default is Tier 3**:
anything not explicitly dumb (an unset or empty `TERM`/`COLORTERM`, or a generic
`TERM=xterm`) is assumed truecolor, because every incumbent terminal renders
truecolor and several (notably on Windows) do not advertise the capability via
env at all — defaulting to Tier 3 keeps a modern terminal colourful instead of
degrading it to mono. The lower tiers are the **fallback** (the degradation
path), reached only when the environment explicitly advertises a lower
capability (a 256-colour `TERM`, or an explicitly dumb `TERM=linux`/`dumb`);
Tier 0 is never reached from an unset env. A genuinely limited terminal is
otherwise overridden via `vibe.tree.tier`. @status:spec/done

[p38] @fact:TIER-PROJECTION REQ. Degradation is a **projection**: one `Theme` is built for Tier 3 and
projected onto the detected tier (roles quantised / ANSI-mapped / ASCII-fallback).
One source of truth, many projections — never bespoke per-tier rendering in a
component. @status:spec/done

#### 2.2.4 Window aesthetics {#window-aesthetics}

[p39] @fact:WINDOW-AESTHETICS-REQ REQ. A window (§2.3) is not a fallback: it composes a solid `base` panel, a
rounded frame stroked in `border`, a title rendered as an `accent`-coloured chip,
interior padding, an optional shadow (so the panel reads as raised), and a
top-right `[✕]` close affordance. Even at Tier 1 the frame + filled panel +
title chip carry the "floating panel" reading without truecolor. @status:spec/done

#### 2.2.5 Spacing & rhythm {#spacing}

[p40] @fact:SPACING-REQ REQ. The theme's **spacing** (§2.2) is normative, not decorative: a component
never jams its content against a stroke, and a row of controls is **centre-
aligned in its area**, never left-jammed. The `ui::` library owns the spacing
vocabulary — three constants that define the rhythm, so a re-space touches one
place: @status:spec/done

- [p41] @fact:PAD-CONSTANTS **`PAD_X` = 2, `PAD_Y` = 1 — interior padding.** Inside every window/dialog
  frame, content is inset from the border: a horizontal margin (`PAD_X`) each
  side and a vertical margin (`PAD_Y`) top and bottom — a blank row under the
  title and a blank row above the base. This is the "interior padding" §2.2.4
  requires; `ui::inner_pad` applies it. A window whose body sits on the frame is
  a violation. @status:spec/done
- @fact:GUTTER-CONSTANT **`GUTTER` = 1 — group gutter.** A control inside a `Group` frame (§2.6) is
  inset from that frame's stroke by the gutter, and its selection highlight bar
  is inset with it, so neither the label nor the bar touches the border. @status:spec/done
- @fact:RHYTHM-RULE **Rhythm.** Sibling controls are separated — framed groups by a blank row
  between them, inline hint/footer runs by a `•` separator with surrounding
  space — so the eye reads structure, not a wall of glyphs. @status:spec/done

[p42] @fact:spacing-lore-note The values are lore-explained in the design doc §6 `#spacing-rhythm`; the footer
(§5.2) is the canonical inline application. @status:spec/done

### 2.3 Window / Panel {#window}

[p43] @fact:WINDOW-COMPONENT REQ. A `Window` component: a bordered, titled region drawn over a cleared rect,
with an optional title and an optional top-right `esc [x]` close affordance
(§8). It is the base of every modal and the card. It composes child components
and lays them out; it owns no app logic. @status:spec/done

### 2.4 Menu — the dropdown list {#menu}

[p44] @fact:MENU-COMPONENT REQ. A `Menu` component: a centered, window-framed list of options; `↑`/`↓` move
the highlight (the highlighted item is styled), `Enter` confirms the highlighted
option, `Esc` cancels. This is the canonical "dropdown menu" design referenced
throughout this contract (F3 mode menu §7.1; sort options §7.2). @status:spec/done

### 2.5 Button {#button}

[p45] @fact:BUTTON-COMPONENT REQ. A `Button` component: a labelled, focusable control; the focused button is
highlighted; `Enter` activates it. Buttons participate in a window's focus order
(arrow / Tab navigation). Used by dialogs (OK, Save, Cancel). @status:spec/done

### 2.6 Group / Fieldset {#group}

[p46] @fact:GROUP-COMPONENT REQ. A `Group` component: a visually delimited cluster of child components
(color / padding / a border frame), with an optional group name rendered at the
frame's top-right corner. Groups give a multi-setting dialog its visual
structure (§7.2). @status:spec/done

### 2.7 RadioGroup {#radio-group}

[p47] @fact:RADIO-GROUP-COMPONENT REQ. A `RadioGroup` component: a `Group` whose children are mutually-exclusive
options; exactly one is selected; `↑`/`↓` move, `Enter`/`Space` selects. Backs
single-choice settings (the sub-table block order, §7.2; the copy format/dest,
§10.2). @status:spec/done

### 2.8 TextField {#text-field}

[p48] @fact:TEXT-FIELD-COMPONENT REQ. A `TextField` component: a single-line editable text input (the file-path
entry, §10.5). Simple for now (a later REQ enriches it); wraps rat-widget's text
input where possible (§2.1). @status:spec/done

### 2.9 Card / Form {#card}

[p49] @fact:CARD-COMPONENT REQ. A `Card` component: a `Window` laid out as a labelled vertical form — see
§8 for its full design contract (background/foreground, bold headers, line
spacing, wrapping, per-line copy). @status:spec/done

### 2.10 The standard "Coming Soon" modal {#coming-soon}

[p50] @fact:COMING-SOON-MODAL REQ. A single reusable `ComingSoon` modal — a `Window` titled with the feature
name, a "coming soon" body, and an `OK` button (`Enter`/`Esc` closes). It is the
standard placeholder for **every** not-yet-implemented feature (F1 Search
Everywhere §7.3; PNG export §10.4; any future stub). Wiring a feature's entry
point to `ComingSoon` is how a feature is "reserved" before it is built. @status:spec/done

## 3. The Tree widget + filter pipeline {#tree-widget}

[p51] @fact:tree-abstraction-intro The core reusable abstraction: one tree widget, fed by a configurable pipeline. @status:spec/done

### 3.1 The Tree widget {#tree}

[p52] @fact:TREE-WIDGET-REQ REQ. A single `Tree` component renders a forest of `PackageTree` nodes — the
`│├└` glyphs, the `+`/`-` expand indicator, the load/`T`/`C`/`S` columns, the
`(*)` DAG dedup + cycle-guard (PROP-036 §2.12), selection highlight, vertical
scroll, and horizontal pan. **Every mode (§4) renders through this one widget** —
there are no bespoke flat-list renderers. Fold state and selection are the
widget's (per-instance) state. @status:spec/done

### 3.2 The filter / shape pipeline {#tree-filters}

[p53] @fact:FILTER-PIPELINE REQ. Data reaches the `Tree` widget only after passing a configurable
**pipeline**: `PackageTree` → (filter: which packages) → (shape: how they form a
forest) → (order: sibling ordering) → the widget's row list. The pipeline is
data-driven and testable in isolation; a mode (§4) is a pipeline configuration,
not new rendering code. The three tree *shapes* (§3.3) and the orderings (§7.2)
are pipeline settings, selectable by the user and persisted (§9). @status:spec/done

### 3.3 The three tree shapes {#tree-shapes}

[p54] @fact:THREE-SHAPES REQ. The pipeline offers three shapes, selectable per context on the F2 sort menu
(§7.2), **default = (a)**: @status:spec/done

- [p55] @fact:SHAPE-MEMBERS-ROOTS **(a) members-as-roots + full subtrees** — each package in the filter set is a
  forest root shown with its entire dependency subtree (cross-type deps
  included); DAG dedup via `(*)`. @status:spec/done
- @fact:SHAPE-LOAD-FOREST **(b) load-type forest** — a package is a root only if no other member of the
  same filter set depends on it; children are its same-set dependencies
  (cross-set deps omitted). @status:spec/done
- @fact:SHAPE-PRUNED **(c) pruned tree** — the tree from the declared roots, keeping only branches
  that reach a member of the filter set. @status:spec/done

[p56] @fact:shapes-validate All three are pipeline configurations over §3.2 — implementing them validates
that the filter/shape abstraction is right. @status:spec/done

## 4. Modes {#modes}

[p57] @fact:THREE-MODES REQ. `vibe tree` has three display modes, each a Tree-widget (§3.1) configuration
(§3.2) — none is a flat list: @status:spec/done

### 4.1 Tree mode {#mode-tree}

[p58] @fact:MODE-TREE-REQ REQ. One tree over the whole package set (the current default). Filter = all;
shape + order per §3.2/§7.2. @status:spec/done

### 4.2 Sub-tables mode {#mode-subtables}

[p59] @fact:MODE-SUBTABLES-REQ REQ. **Several trees stacked vertically** — one Tree instance per effective-load
partition (`static` / `dynamic` / `no-boot`), each under a subheader, in the
user-chosen block order (§7.2). Each block is a full tree (per §3.3), not a flat
list. @status:spec/done

### 4.3 Tabs mode {#mode-tabs}

[p60] @fact:MODE-TABS-REQ REQ. **One tree per tab** — a tab bar (`Static` / `Dynamic` / `No-boot`); the
active tab shows that partition's tree (per §3.3). `Shift`+`←`/`→` switches tabs
(§5.3); plain arrows navigate the active tab's tree. @status:spec/done

### 4.4 Mode selection {#mode-select}

[p61] @fact:MODE-SELECT-REQ REQ. The active mode is chosen from the F3 menu (§7.1) — not a bare cycle key.
The active mode is shown in the status line and persisted (§9). @status:spec/done

## 5. The keymap {#keymap}

### 5.1 The mode-aware registry {#keymap-registry}

[p62] @fact:KEYMAP-REGISTRY-REQ REQ. A single **keymap registry** maps each context (base mode, and each modal)
to the set of actions available there, each with its key(s) and its footer
label. The footer (§5.2) renders **only** the actions valid in the current
context — an action unavailable in a mode (e.g. sub-table block reorder in tree
mode) is absent from both the keymap and the footer. Routing (§1.5) consults the
registry; no context re-implements dispatch. @status:spec/done

### 5.2 The F-key scheme + footer {#keys}

[p63] @fact:F-KEY-SCHEME REQ. Primary actions are on function keys: `F1` Search Everywhere (§7.3), `F2`
sort menu (§7.2), `F3` mode menu (§7.1), `F4` settings (opens the `vibe prefs`
settings UI — PROP-041), `F6` copy / `Shift+F6` copy-settings (§10.2). The footer
lists the active keys with short labels; **`Shift` is written as `↑`** (e.g.
`Shift+F6` → `↑F6`) to keep hints short. @status:spec/done

[p64] @fact:FOOTER-TWO-ROWS REQ. The footer renders as **two centred rows** (§2.2.5 `#spacing`): the F-key
command row (`F1`…`F6`) above, the navigation + `Enter`/`Esc` row below, each a
run of `key desc` pairs separated by a dim `•` and **centred under the screen**.
Only the keys valid in the current context appear. A single left-aligned run
jamming every hint onto one line is a violation — the footer carries visual
rhythm like the rest of the surface (§2.2.5). @status:spec/done

### 5.3 Tree interaction keys {#tree-keys}

[p65] @fact:TREE-KEYS-REQ REQ. Within a tree: `↑`/`↓` move selection (+ scroll), `←`/`→` pan horizontally,
`Space` folds/unfolds the selected node, `Enter` opens the detail card (§8).
`Shift`+`←`/`→` switches **tabs app-wide** — the tree's display tabs (in tabs
mode) and the Search Everywhere category tabs (§7.3); plain `←`/`→` stay
tree-pan (which is why tab-switch takes Shift). These direct keys are exempt
from the F-key scheme (they are navigation, not commands). @status:spec/done

### 5.4 Focus groups + Tab Order {#focus-groups}

[p66] @fact:FOCUS-GROUPS-REQ REQ. A screen's controls are partitioned into **focus groups** — cohesive
clusters the user steps between with `Tab` (and `Shift+Tab` backwards). Within a
group the group's own keys act (arrows move a radio selection, typing edits a
field, `Enter` activates a button). A screen with more than one focus group
declares a **Tab Order** — the explicit sequence `Tab` cycles — per screen; a
single-group screen has no Tab Order (`Tab` is inert there). `Tab` never switches
display tabs (that is `Shift+←`/`Shift+→`, §5.3) and never activates a control
(that is `Enter`). @status:spec/done

[p67] @fact:FOCUS-MARKED REQ. The focused group is visually marked (the theme's selection/accent), so the
user sees where `Tab` will land next. Example Tab Orders: the F2 sort menu (§7.2)
cycles its radio groups — "Sort by" → "Shape" (→ "Block order" in sub-tables
mode); a settings form (PROP-041 §4) cycles its field groups; the quit-confirm
dialog (§7.4) cycles OK → Cancel. @status:spec/done

## 6. The modal stack {#modal-stack}

[p68] @fact:MODAL-STACK-REQ REQ. Modals form a **stack**: opening a modal pushes it; it draws over
everything below (each lower layer still visible as backdrop); input goes only to
the top modal; `Esc` pops the top modal (returning to the one beneath). A feature
may push a modal over a modal — e.g. copy-settings (§10.2) → file-path (§10.5) is
a depth-2 stack. The base screen is the bottom of the stack; `Esc` at the bottom
triggers quit-confirm (§7.4), it does not pop. @status:spec/done

## 7. Menus & dialogs {#dialogs}

### 7.1 F3 — the mode menu {#f3-mode-menu}

[p69] @fact:F3-MODE-MENU-REQ REQ. `F3` opens a `Menu` (§2.4) of the display modes (§4). Selecting one switches
the mode. This replaces any bare mode-cycle key. @status:spec/done

### 7.2 F2 — the sort menu {#f2-sort-menu}

[p70] @fact:F2-SORT-MENU-REQ REQ. `F2` opens a dialog whose content depends on the active mode: @status:spec/done

- [p71] @fact:sort-tree-tabs **tree & tabs modes** — one `RadioGroup` (§2.7): `alphabetical` / `topological`
  (the sibling order, §3.2). One group; no group chrome needed. @status:spec/done
- @fact:sort-subtables **sub-tables mode** — two `Group`s (§2.6): a "sort" group (`alphabetical` /
  `topological`) and a "block order" group (a `RadioGroup` over the static /
  dynamic / no-boot block orderings). Group names sit at each frame's top-right. @status:spec/done

[p72] @fact:SORT-SHAPE-HERE The tree **shape** (§3.3) is also chosen here (a further group), per context.
Choices persist (§9). @status:spec/done

### 7.3 F1 — Search Everywhere {#f1-search}

[p73] @fact:F1-SEARCH-REQ REQ. `F1` opens the **Search Everywhere** window — the `vibe-actions` Search
Everywhere engine (PROP-039 §10), in the IntelliJ IDEA idiom: a hybrid **"All"**
tab that searches everything, plus **per-category tabs** that narrow it
(`Tab`/`Shift+Tab` cycle; the "All" tab carries a category checkbox filter). Three
providers ship (PROP-039 §10.4): @status:spec/done

- [p74] @fact:PROVIDER-PACKAGES **Packages** — by name (`PackageProvider` over the `PackageTree`); selecting
  reveals the package in the tree. @status:spec/done
- @fact:PROVIDER-CARDS **Cards** — inside **every field** of the package detail cards (§8)
  (`PackageFieldProvider`: name, version, kind, license, load-type, origin, path,
  deps, diagnostics…); selecting opens the card focused on that field. @status:spec/done
- @fact:PROVIDER-ACTIONS **Actions** — all `vibe.tree` actions (§13.5) by address, **name, and
  description** (`ActionProvider`); selecting **invokes** the action in place (a
  command performs and closes; a toggle stays open). Disabled actions render greyed
  with their "why disabled" reason and their keybinding. @status:spec/done

[p75] @fact:SEARCH-ENGINE-NOTES Matching falls back to the **name/description** lane when nothing matches by id or
another field (PROP-039 §10.3). Results render through one normalized row (icon ·
primary · a right-aligned keybinding · group). This **supersedes** the reserved
`ComingSoon` stub. A future `StructureProvider` (AI-Native specmap nodes) plugs into
the same engine with no TUI change. @status:spec/done

### 7.4 Escape — quit with confirmation {#quit-confirm}

[p76] @fact:QUIT-CONFIRM-REQ REQ. At the base screen, `Esc` opens a confirm dialog ("Really quit?") with
`Yes`/`No` buttons; `Yes`/`Enter`-on-Yes quits, `No`/`Esc` returns. The
confirmation exists because loading may be slow and `Esc` is used pervasively in
modals — an accidental single `Esc` must not discard the session. @status:spec/done

## 8. The detail card {#detail-card}

[p77] @fact:DETAIL-CARD-REQ REQ. `Enter` on a package opens the detail **Card** (§2.9) — a real form, not a
glued text blob: @status:spec/done

- [p78] @fact:card-paper a light panel background with dark text (a "paper" card), distinct from the
  tree beneath; @status:spec/done
- @fact:card-headers **bold** field headers; blank-line / padded spacing between fields; @status:spec/done
- @fact:card-close an `esc [x]` close affordance drawn as a pseudographic cross in the top-right
  corner; @status:spec/done
- @fact:CARD-WRAP-COPY long values **wrap** (never truncate) and each wrapped value line can be
  **copied to the clipboard** (a per-line copy affordance); @status:spec/done
- @fact:card-esc `Esc` / the `[x]` closes it. @status:spec/done

[p79] @fact:CARD-CONTENT The card's content is the package detail from PROP-036 §2.11 (name, group,
version, kind, load, transitive + why, condition, STATIC.md, source, hash,
dependencies, boot path). @status:spec/done

## 9. Settings persistence {#settings}

[p80] @fact:SETTINGS-PERSISTENCE REQ. UI state persists under `~/.vibe/tree/` as one or more JSON files. Saved:
the active mode (§4.4), the per-context sort + shape + block-order choices (§7.2,
§3.3). On TUI launch the settings are loaded and the UI restored to them; a
missing/corrupt file falls back to defaults (never a hard error). Writing is
atomic; the directory is created if absent. (This is the machine-global
`~/.vibe/` config root, distinct from the project's `.vibe/`.) @status:spec/done

## 10. The copy system {#copy}

### 10.1 Per-screen copy providers {#copy-providers}

[p81] @fact:COPY-PROVIDERS-REQ REQ. Each screen (tree/sub-tables/tabs; the card) supplies a **copy provider** —
an object that knows how to serialize *what is currently shown* into a copy
format. A tree provider serializes the tree with its current display options; the
card provider serializes the card's fields. Copy is "what I see is what I copy":
in tabs mode only the active tab is serialized. @status:spec/done

### 10.2 F6 copy / Shift+F6 copy-settings {#copy-flow}

[p82] @fact:COPY-FLOW-REQ REQ. `F6` copies the current screen straight to the clipboard as text (the
provider's default format). `Shift+F6` opens the **copy-settings** modal: a
`RadioGroup` for **format** (Markdown / PNG — and later PlantUML / Mermaid) and a
`RadioGroup` for **destination** (clipboard / file). Confirming with destination
= file pushes the file-path modal (§10.5) over it (§6 stack). @status:spec/done

### 10.3 Markdown export {#copy-markdown}

[p83] @fact:MARKDOWN-EXPORT-REQ REQ. Markdown is a real serialization, not a screen-buffer scrape: the tree
renders as a pseudographic tree inside a fenced block (or a nested list); the
card renders as well-formatted Markdown (headers + fields), not a raw text dump. @status:spec/done

### 10.4 PNG export {#copy-png}

[p84] @fact:PNG-RESERVED REQ. PNG export (a rasterized tree image; a designed package info-card image) is
**reserved** — selecting PNG opens the `ComingSoon` modal (§2.10) until the
rasterization (font + image crates) is built. Named non-goal-for-now (§12). @status:spec/done

### 10.5 Destination — clipboard vs file {#copy-dest}

[p85] @fact:COPY-DEST-REQ REQ. Clipboard writes via the platform clipboard. File destination pushes a
modal with a `TextField` (§2.8) path entry plus `Save` and `Cancel` `Button`s
(§2.5): arrow/Tab focus, `Enter` on a button acts, `Esc` cancels back to
copy-settings. The path field is plain text for now (a later REQ enriches it). @status:spec/done

## 11. AI-Native Rust discipline {#discipline}

[p86] @fact:DISCIPLINE-REQ REQ. All code implementing this contract follows the AI-Native Rust discipline
(`spec://org.vibevm.ai-native/core-ai-native`): cells with single registration
points and no sibling-cell coupling; `specmark::scope!` on every file citing the
governing PROP-037 anchor; per-fn `#[spec(implements = "spec://…#…")]` where a fn
implements a specific REQ; `anyhow` at the command edge, typed errors citing REQs
in library layers, no `unwrap`/`expect` in domain logic; the `≤600`-line file
budget; and green `conform` + `specmap` at every commit. Each REQ anchor here is
an addressable target the code traces to — that traceability is what keeps the
feature set navigable. @status:spec/done

## 12. Non-goals {#non-goals}

- [p87] @fact:NG-LOCALIZATION **Non-English localization content** — the i18n *mechanism* ships (PROP-039 §8,
  §13.4); no non-English catalogues are shipped now, and English is the only
  mandatory-complete locale. @status:spec/done
- @fact:NG-PNG **PNG export** — reserved behind `ComingSoon` (§10.4) until the rasterization
  spike lands. @status:spec/done
- @fact:NG-AIUI **The AIUI surface itself** — not built now; this TUI is its prototype (§13.1,
  PROP-039 §11.3). The Search-Everywhere `StructureProvider` (AI-Native specmap
  nodes) is likewise reserved (§7.3) — the same engine, added later. @status:spec/done
- @fact:NG-PLANTUML **PlantUML / Mermaid copy formats** — later additions to §10.2. @status:spec/done
- @fact:NG-SETTINGS-UI **A settings UI** — settings are edited via the menus (§7) and persisted (§9);
  no dedicated settings editor screen. @status:spec/done
- @fact:NG-NON-TTY **Non-tty operation** — `--json` / `--plain` (PROP-036) remain the machine and
  fallback surfaces; this contract governs the interactive TUI only. @status:spec/done

## 13. Built on the action system (`vibe-actions`, PROP-039) {#action-system}

[p88] @fact:action-system-intro This TUI is the **first consumer** and the **prototype surface** of the action
system (PROP-039). This section adapts the architecture above onto it and is
**authoritative** where it upgrades an earlier section. @status:spec/done

### 13.1 The TUI is a Surface; the Model is the serialisable view {#as-surface}

[p89] @fact:AS-SURFACE-REQ REQ. The TUI is a `Surface` (PROP-039 §11.1) over `vibe-actions`; it owns rendering
and event capture and nothing of the action core. The Model (§1.3) is the source of
the **serialisable `ModelView`** (PROP-039 §11.2) — focus, the modal stack, the
visible rows, the active tab/mode, the selection, and the set of enabled actions
with their reasons. No rendering type leaks into the Model. This makes the TUI the
prototype that proves the **AIUI** (PROP-039 §11.3): the same tree can later be
driven headless with no change to this crate's model/controller. @status:spec/done

### 13.2 Commands are addressed actions {#as-actions}

[p90] @fact:AS-ACTIONS-REQ REQ. Every TUI command is a `vibe-actions` **Action** (PROP-039 §3) in the group
**`vibe.tree`**, addressed `action://vibe.tree/<name>`, carrying a mandatory
human-readable **name + description** (§13.4), a typed param schema, and a typed
enablement over a `TreeCtx` snapshot (the mode, the selection, the active tab). This
**upgrades §1.5's "typed intent enum"**: the Controller resolves an event to an
`ActionAddr` and calls `invoke` (PROP-039 §7.1); it no longer switch-matches a local
enum. @status:spec/done

### 13.3 The keymap binds keys to addresses {#as-keymap}

[p91] @fact:AS-KEYMAP-REQ REQ. The mode-aware keymap (§5.1) binds each key/chord, per context, to an
`(action://vibe.tree/<name>, params)` (PROP-039 §9). The footer label (§5.2) is the
action's **name**; the footer lists exactly the actions **enabled** in the current
context (their enablement predicate, PROP-039 §6.2). Tree navigation keys (§5.3)
remain direct (navigation, not commands). @status:spec/done

### 13.4 i18n is real {#as-i18n}

[p92] @fact:AS-I18N-REQ REQ. This **upgrades §1.6** from "i18n-ready" to the shipped `vibe-actions` i18n
(PROP-039 §8): every action and UI string is an address-keyed catalogue entry
(`action.vibe.tree.<name>.name` / `.description`) with an inline English default;
the resolved label keeps `{value, original_en}` so Search Everywhere (§7.3) matches
the English text under any locale. English is the only mandatory-complete locale and
is checked by the legibility gate (PROP-039 §8.4). @status:spec/done

### 13.5 The action catalogue {#action-catalogue}

[p93] @fact:ACTION-CATALOGUE-REQ REQ. The `vibe.tree` actions at ship — each an addressed Action with a name +
description; the **key is its default binding** (the map, not the identity): @status:spec/done

[p94]
| Address (`action://vibe.tree/…`) | Key | Name | Description |
| --- | --- | --- | --- |
| @fact:ROW-ACT-SEARCH `search.everywhere` @status:spec/done | @fact:ROW-ACT-SEARCH-KEY `F1` @status:spec/done | @fact:ROW-ACT-SEARCH-NAME Search Everywhere @status:spec/done | @fact:ROW-ACT-SEARCH-DESCRIPTION Search packages, cards, and actions; run a found action. @status:spec/done |
| @fact:ROW-ACT-SORT `sort` @status:spec/done | @fact:ROW-ACT-SORT-KEY `F2` @status:spec/done | @fact:ROW-ACT-SORT-NAME Sort & shape… @status:spec/done | @fact:ROW-ACT-SORT-DESCRIPTION Choose the ordering and tree shape for the current view. @status:spec/done |
| @fact:ROW-ACT-MODE `mode.set` @status:spec/done | @fact:ROW-ACT-MODE-KEY `F3` @status:spec/done | @fact:ROW-ACT-MODE-NAME Switch mode… @status:spec/done | @fact:ROW-ACT-MODE-DESCRIPTION Switch between tree, sub-tables, and tabs display. @status:spec/done |
| @fact:ROW-ACT-COPY `copy` @status:spec/done | @fact:ROW-ACT-COPY-KEY `F6` @status:spec/done | @fact:ROW-ACT-COPY-NAME Copy @status:spec/done | @fact:ROW-ACT-COPY-DESCRIPTION Copy the current screen (Markdown) to the clipboard or a file. @status:spec/done |
| @fact:ROW-ACT-COPY-SETTINGS `copy.settings` @status:spec/done | @fact:ROW-ACT-COPY-SETTINGS-KEY `↑F6` @status:spec/done | @fact:ROW-ACT-COPY-SETTINGS-NAME Copy settings… @status:spec/done | @fact:ROW-ACT-COPY-SETTINGS-DESCRIPTION Choose the copy format and destination. @status:spec/done |
| @fact:ROW-ACT-FOLD `fold.toggle` @status:spec/done | @fact:ROW-ACT-FOLD-KEY `Space` @status:spec/done | @fact:ROW-ACT-FOLD-NAME Fold / unfold @status:spec/done | @fact:ROW-ACT-FOLD-DESCRIPTION Fold or unfold the selected node. @status:spec/done |
| @fact:ROW-ACT-CARD `card.open` @status:spec/done | @fact:ROW-ACT-CARD-KEY `Enter` @status:spec/done | @fact:ROW-ACT-CARD-NAME Open details @status:spec/done | @fact:ROW-ACT-CARD-DESCRIPTION Open the detail card for the selected package. @status:spec/done |
| @fact:ROW-ACT-TABS `tab.next` / `tab.prev` @status:spec/done | @fact:ROW-ACT-TABS-KEY `↑→` / `↑←` @status:spec/done | @fact:ROW-ACT-TABS-NAME Next / previous tab @status:spec/done | @fact:ROW-ACT-TABS-DESCRIPTION Move between tabs in tabs mode. @status:spec/done |
| @fact:ROW-ACT-QUIT `quit` @status:spec/done | @fact:ROW-ACT-QUIT-KEY `Esc` (base) @status:spec/done | @fact:ROW-ACT-QUIT-NAME Quit @status:spec/done | @fact:ROW-ACT-QUIT-DESCRIPTION Leave `vibe tree` (with confirmation). @status:spec/done |

[p95] @fact:CATALOGUE-ENUMERABLE REQ. This catalogue is the **enumerable** source for both the footer and the Search
Everywhere `ActionProvider` (§7.3): adding a command means registering an Action
(address + name + description + enablement), which then appears in the footer, the
keymap, and Search Everywhere with no further wiring (PROP-039 §4.3, §12.2). @status:spec/done

### 13.6 What this leaves to PROP-039 {#as-boundary}

[p96] @fact:AS-BOUNDARY-REQ REQ. The action core — address, registry, params, context, invoke, i18n, the keymap
resolver, the Search Everywhere engine + provider trait, and the Surface seam — is
owned by `vibe-actions` (PROP-039); this contract owns only the **vibe.tree
specifics**: the concrete actions (§13.5), the three providers' key/resolve/navigate
(§7.3), the `TreeCtx` shape, the F-key map, the theme, and the TUI Surface. The
layering law of §1 is extended: **the action core never leaks into the TUI, and TUI
rendering never leaks into the action core.** @status:spec/done

