| 1 | # Tideline → Ratatui translation spec |
| 2 | |
| 3 | **Status:** spec + scaffolding. Not merged, no PR opened. |
| 4 | **Branch:** `design/tideline-ratatui-translation-20260828` (worktree |
| 5 | `cw-main-0912-integrator-20260828`), based on `origin/main` `dde9cdb044cf7625b78eeb31e53a479ae07b16f2` |
| 6 | (rebase-past #5699/#5700 verified). Branch point ancestry: `e5a6046cb` → #5699 → #5700. |
| 7 | **Implemented & green:** topbar (`crates/tui/src/tui/topbar.rs`) + golden-buffer |
| 8 | harness, 6/6 via `./scripts/dev-test.sh tui topbar` (11,329 skipped). |
| 9 | |
| 10 | Authority order used: the three approved screenshots > this document's |
| 11 | prose > the recovered motion sketch (motion language) > `tideline-redesign.html` |
| 12 | (interaction-registry idea only, superseded on layout). |
| 13 | |
| 14 | --- |
| 15 | |
| 16 | ## 1. What the three approved screens actually contain |
| 17 | |
| 18 | Cell-inventory read of the references (startup, work+fleet, settings/appearance): |
| 19 | |
| 20 | - **Topbar (all three).** One row: `CODEWHALE` wordmark; contextual |
| 21 | segments (`run …`, `fleet …`, `3/4 whales`, `model …`, `theme …`, |
| 22 | `Settings / Appearance`, `folder …`); pinned right = `context NN% ▰▰▱▱▱` + |
| 23 | full clock. Segment set varies per screen; brand/meter/clock never move. |
| 24 | - **Startup.** Centered hero: "What are we working on?", one dim |
| 25 | subtitle; `QUICK ACTIONS` band with 3 rows (icon · label · description · |
| 26 | command + `›`); a 4-column option strip (New worktree / Chat only / Theme / |
| 27 | Help); whale-outline composer; footer with route · cost · keys. |
| 28 | - **Work + Fleet.** Left rail (RUNS / WHALES / FLEET / WORK / CONTEXT, then |
| 29 | help/settings, `«` collapse); receipt stream (user + assistant turns, a |
| 30 | `├──/└──` pod-formation tree, state-marked receipt rows with timestamps and |
| 31 | receipt counts, an indented conclusion block, a legend row); `FLEET LEDGER` |
| 32 | table (WHALE/ASSIGNMENT/STATE/ELAPSED/RECEIPTS/LAST UPDATE, selected row |
| 33 | marker `▶`); composer; footer with cost and keys. |
| 34 | - **Settings.** 3 panes: category rail (Appearance → Advanced + help/file/ |
| 35 | feedback); theme list (13 rows: 4 mode rows + 9 presets, selected row boxed |
| 36 | + `✓`) and MOTION (OPTIONAL) toggles; live Deepsea preview pane replaying a |
| 37 | transcript + mini ledger + composer, with translucent whale silhouettes in |
| 38 | the background; footer with 6 key bindings. |
| 39 | |
| 40 | ## 2. HTML lies — resolutions (non-negotiable) |
| 41 | |
| 42 | | In the reference | Why it cannot ship | Decision | |
| 43 | |---|---|---| |
| 44 | | Composer drawn as a stroked whale outline | No bezier strokes; only box-drawing glyphs | **Rounded border.** `╭─╮│╰╯` border (dim at rest, Info on focus); the old fluke cap is retired and no brand glyph is hand-drawn into the composer. The send `↑` becomes a 3-cell hitbox `[↑]` right-aligned inside the border. The terminal never approximates the canonical raster mark. | |
| 45 | | Translucent whale silhouettes behind text (Deepsea preview) | No alpha; painting over text destroys it | **Empty-cells-only compositing**, the `ambient_life.rs` rule verbatim: write only cells that are open water (`is_open_water` + `TEXT_CLEARANCE_ROWS = 1` clearance from `occupied_text_bounds`). Eviction order when water is scarce: bubbles first, then fish school, then jellyfish; the whale cameo is evicted last (highest identity value). Caustic-style tinting stays bg-only on `cell.symbol() == " "`. Deepsea ambient runs only under `MotionMode::Full`. | |
| 46 | | SVG icons (plug, clock, folder, palette, chat) | Not renderable | One glyph per action, added to `glyphs.rs` with declared ASCII fallbacks via `ascii_fallback`: plug `⌁`→`+`, resume `↺`→`<`, folder `▤`→`=`, palette `◐`→`*`, chat `◌`→`o`, worktree `⑂`→`y`, help `?`→`?` (identity). Each is 1 cell, no wide glyphs. | |
| 47 | | Ledger cells wrapping to two lines | Table columns are exact integer cells | Fixed column widths + per-column truncation: WHALE 10 (never truncates — names are short by contract), ASSIGNMENT = remainder (truncate with `…`, never wrap), STATE 12 (glyph + word), ELAPSED 8, RECEIPTS 8, LAST UPDATE 8 (`HH:MM:SS`). **At 80 columns** the rail is hidden and ledger sheds to `WHALE │ ASSIGNMENT │ STATE` — ELAPSED, RECEIPTS, LAST UPDATE drop in that order before ASSIGNMENT loses cells. | |
| 48 | | Meter bars `▰▱`, tree `├── └──`, state marks `●○✓!` | Fine — they are glyphs | Keep. Declared ASCII fallbacks already in `glyphs::ascii_fallback` (`▰→#`, `▱→-`, `├→+`, `└→+`, `●→.`, `○→.`, `✓→Y`, `!→!`). Verified single-width in the ascii-safe golden test. | |
| 49 | |
| 50 | **Rule restated:** if it cannot be expressed as cells, it is not in the design. |
| 51 | Nothing else in the three screens requires non-cell expression. |
| 52 | |
| 53 | ## 3. Lineage — what each component replaces (frame.rs slot map) |
| 54 | |
| 55 | The live shell renders ten slots (`tui/ui/frame.rs` header split ~:818, body |
| 56 | constraints ~:928). The references collapse the bottom into one footer: |
| 57 | |
| 58 | | Slot today | Disposition | |
| 59 | |---|---| |
| 60 | | header (`underwater::render_header`) | **Replaced** by `topbar::Topbar` (implemented here). Facts survive: mode/permission chips move to the footer activity segment; route/model stays a topbar segment. | |
| 61 | | 0 work strip (`work_surface::render`) | **Extends** — becomes the rail's WORK group (owned by #5699; do not fight their files). | |
| 62 | | 1 chat (+rail via `split_chat`) | **Extends** — receipt stream + fleet ledger attach to the transcript column; rail reuses `work_surface::split_chat`. | |
| 63 | | 2 workflow panel | **Kept unchanged** (drill-in above composer). | |
| 64 | | 3 pending input preview | **Merged into the composer** as a one-row crumb above the input line — the reference shows queued messages as composer content, not a band. | |
| 65 | | 4 background-work chip | **Deleted as a band**; the fact moves to the topbar `fleet n/m` segment and the rail WORK group (one surface owns each fact). | |
| 66 | | 5 session boot receipt | **Deleted as a band**; boot lines become ordinary transcript receipts. | |
| 67 | | 6 activity band | **Merged into the footer** (left half: phase chip + echolocation + cost). | |
| 68 | | 7 composer | **Extends** — rounded border + `[↑]` hitbox; composer authority logic untouched. | |
| 69 | | 8 identity band | **Merged into the footer** (right half: depth line + key legend). `phase_strip::render_identity` is the merge target; `render_footer` delegates today already. | |
| 70 | |
| 71 | Orphaned facts, each with exactly one home: cost/token ledger → footer; |
| 72 | boot receipts → transcript; background-work → topbar fleet segment + WORK rail; |
| 73 | permission/mode chips → footer activity segment; session metrics detail → |
| 74 | `/cost` (the sketch's rule: the ledger row moves behind `/cost`). |
| 75 | |
| 76 | ## 4. Founder directive resolutions |
| 77 | |
| 78 | 1. **Blue stage vs `Color::Reset`.** Smallest honest resolution: Dark and |
| 79 | Light keep `Color::Reset` (the host-terminal inheritance rule and sketch |
| 80 | principle 1 are load-bearing). The blue stage ships as **Deepsea, promoted |
| 81 | to a first-class one-keystroke toggle** (topbar Theme segment opens the |
| 82 | picker with Deepsea preselected; the theme strip on startup), plus a |
| 83 | blue-biased ink ramp for Dark so the stage reads cool even on Reset. |
| 84 | **Tradeoff named, not papered over:** a user on Dark who wants the painted |
| 85 | field without Deepsea's underwater life cannot have it — with no alpha, |
| 86 | "inherit host background" and "painted blue field" are mutually exclusive; |
| 87 | anything else is a third theme pretending not to be one. |
| 88 | 2. **Continuous activity vs "no ambient loops".** Resolution: *state is always |
| 89 | shown; decoration is opt-in.* The footer's phase segment (echolocation chip |
| 90 | + phase word + live detail) is persistent in every phase, matching |
| 91 | `phase_strip::activity_height()`'s existing always-reserved row. Ambient |
| 92 | life (fish/jelly/bubbles/cameo) stays `MotionMode::Full`-gated. Nothing |
| 93 | new loops for decoration. |
| 94 | 3. **Mature and original.** One surface owns each fact (§3 map). The braille |
| 95 | spinner is **retired** in favor of echolocation (`<·>` family) at the same |
| 96 | 420 ms cadence; `spinner.rs` constants remain for non-Tideline call sites |
| 97 | until the migration slice deletes them. |
| 98 | 4. **Motion gating + still frames.** Every motion below declares a still frame |
| 99 | that carries the same information and rides `MotionMode` |
| 100 | (`MotionPolicy::from_settings(low_motion, fancy_animations, …)`), wall-clock |
| 101 | keyed, never frame-count keyed. |
| 102 | |
| 103 | ## 5a. Component inventory |
| 104 | |
| 105 | Data sources are real `App` fields/methods (verified at HEAD). "Hitbox" = |
| 106 | where the `Rect` is stored for `mouse_ui` (existing pattern: |
| 107 | `viewport.last_*_area` / `work_surface.hitboxes` / `launch.row_areas`). |
| 108 | |
| 109 | | Component | What it does | States | Data source | Replaces | Owning file | Keys | Mouse hitbox | Golden name | |
| 110 | |---|---|---|---|---|---|---|---|---| |
| 111 | | Topbar | One-row status surface | per-screen segment set; hover; shed | `effective_route_identity_display()`, run/fleet summaries, `context_budget` pct, injected clock | `underwater::render_header` | `tui/topbar.rs` ✅ | Tab⇄, Enter activate | brand/menu + per-segment rects → `viewport.last_topbar_hitboxes` | `topbar_{startup,work,settings}_{w}x{h}` ✅ | |
| 112 | | Hero (startup) | Centered prompt + subtitle | first-run vs returning | `LaunchState`, `workspace_session_count` | `render_launch_screen` | `tui/underwater.rs` | — | none | `startup_{w}x{h}` | |
| 113 | | Quick actions | 3 command rows | selected/hover/disabled (no model) | `LaunchAction`, provider state | launch menu rows | `tui/underwater.rs` + `mouse_ui.rs:441` | ↑/↓, Enter, Esc | row rects (exists) | `startup_*` | |
| 114 | | Option strip | 4 columns (worktree/chat/theme/help) | hover/selected | `LaunchState` | launch options row | same | Tab, Enter | 4 col rects | `startup_*` | |
| 115 | | Rail | Left column, 5 groups + collapse | expanded/collapsed/focused | `WorkSurfaceState`, `subagent_cache`, run list, git status | work strip + `sidebar` remnants | `tui/work_surface/` (#5699 territory) | Tab, ↑/↓, Enter, `«` | `WorkHitbox{WorkRowId,row_y}` (exists) | `work_{w}x{h}` | |
| 116 | | Receipt stream | Turn + receipt rows, pod tree | streaming/settled; selected | `history` cells, pod formation receipt | transcript rail | `tui/history.rs`, `work_surface/render` | ↑/↓, Enter inspect | row rects (transcript click path) | `work_*` | |
| 117 | | Fleet ledger | Whale table | row selected; state per whale | `subagent_cache` + worker runtime states | workflow-panel duplicate | `tui/work_surface/panels.rs` | ↑/↓, Enter/click inspect | row rects → inspector | `ledger_{w}x{h}` | |
| 118 | | Theme list | 13 themes + motion toggles | selected/preview/applying | `ThemeId`, `ocean_treatment`, `low_motion`, `fancy_animations` | `theme_picker.rs` | `tui/theme_picker.rs`, `views/` | ↑/↓, Enter preview/apply | row rects | `settings_{w}x{h}` | |
| 119 | | Live preview | Projection of a real screen in chosen theme | mirrors screen state; never a second store | same render fns, `TestBackend`-style projection into the pane | settings preview | `tui/views/` settings | — | none (passive) | `settings_*` | |
| 120 | | Settings rail | 8 categories + meta rows | selected | `ConfigView` | `ConfigView` nav | `tui/views/mod.rs` | ↑/↓, Tab | category rects | `settings_*` | |
| 121 | | Composer | Input with rounded border + send hitbox | focus, pending crumb, approval-replaced | `ComposerState`, pending preview | composer_ui/chrome (extends) | existing composer files | Enter, ⇧Enter, Esc | `[↑]` submit rect; border focus click | `composer_{w}x{h}` | |
| 122 | | Footer | One band: phase·cost (left), depth line·keys (right) | per-phase ink; 80% warn | `SessionState` cost, phase, `context_budget` | slots 6+8 merged | `tui/phase_strip.rs` | — | depth segment → context inspector | `footer_{w}x{h}` | |
| 123 | | Notifications inbox | Attention rows (gold ◆) | unread/read; per-kind | `status_toasts`/`sticky_status` → typed records | toast soup | `tui/notifications.rs` | Enter, `r`, Esc | row rects | `notifications_{w}x{h}` | |
| 124 | |
| 125 | No component lacks lineage; the two deletions (slots 4, 5) and two merges |
| 126 | (3, 6+8) are stated above. |
| 127 | |
| 128 | ## 5b. Layout contract (copy-pasteable) |
| 129 | |
| 130 | Screen shell — all three screens: |
| 131 | |
| 132 | ```rust |
| 133 | let [topbar, stage, footer] = Layout::vertical([ |
| 134 | Constraint::Length(1), // topbar (implemented) |
| 135 | Constraint::Min(1), // stage: the screen |
| 136 | Constraint::Length(1), // footer: phase·cost · depth·keys (merged 6+8) |
| 137 | ]).areas(area); |
| 138 | ``` |
| 139 | |
| 140 | Startup stage: |
| 141 | |
| 142 | ```rust |
| 143 | let [hero, rule_a, quick, rule_b, strip, spacer] = Layout::vertical([ |
| 144 | Constraint::Percentage(38), // hero: heading + subtitle |
| 145 | Constraint::Length(1), // wave rule `⋯ ∼∼∼ ⋯` (dim, static) |
| 146 | Constraint::Length(3 + 2), // QUICK ACTIONS: label row + 3 rows + margins |
| 147 | Constraint::Length(1), // wave rule |
| 148 | Constraint::Length(3), // option strip: 4 columns × 2 rows |
| 149 | Constraint::Min(1), |
| 150 | ]).areas(stage); |
| 151 | // options strip: |
| 152 | let [wt, chat, theme, help] = Layout::horizontal([ |
| 153 | Constraint::Ratio(1, 4); 4]).areas(strip); |
| 154 | // composer+footer below stage per screen shell (composer Length(4) incl. border) |
| 155 | ``` |
| 156 | |
| 157 | Work + Pod stage: |
| 158 | |
| 159 | ```rust |
| 160 | let rail_w = if width >= 120 { 22 } else if width >= 100 { 16 } else { 0 }; |
| 161 | let [rail, main] = Layout::horizontal([Constraint::Length(rail_w), Constraint::Min(1)]) |
| 162 | .areas(stage); |
| 163 | let [stream, ledger, composer] = Layout::vertical([ |
| 164 | Constraint::Min(6), // receipt stream |
| 165 | Constraint::Length(1 + 1 + rows.min(4)), // POD LEDGER: header + rows |
| 166 | Constraint::Length(4), // composer |
| 167 | ]).areas(main); |
| 168 | // ledger columns at main width w (rows are 1 line, never wrap): |
| 169 | let cols = if w >= 130 { |
| 170 | [Length(10), Min(8), Length(12), Length(8), Length(8), Length(8)] // +LAST UPDATE |
| 171 | } else if w >= 110 { |
| 172 | [Length(10), Min(8), Length(12), Length(8), Length(8)] // +RECEIPTS |
| 173 | } else { |
| 174 | [Length(10), Min(8), Length(12)] // core three |
| 175 | }; |
| 176 | ``` |
| 177 | |
| 178 | Settings stage (3-pane): |
| 179 | |
| 180 | ```rust |
| 181 | if width >= 100 { |
| 182 | let [nav, form, preview] = Layout::horizontal([ |
| 183 | Constraint::Length(18), Constraint::Min(30), Constraint::Percentage(38), |
| 184 | ]).areas(stage); |
| 185 | } else { |
| 186 | let [nav, form] = Layout::horizontal([Constraint::Length(16), Constraint::Min(20)]) |
| 187 | .areas(stage); // preview sheds |
| 188 | } |
| 189 | ``` |
| 190 | |
| 191 | **Shed order (global, what collapses first):** ① topbar Theme segment → |
| 192 | ② topbar Workspace → ③ topbar Whales → ④ Pod → ⑤ Run/breadcrumb → |
| 193 | ⑥ settings preview pane → ⑦ rail (100 → 0) → ⑧ ledger time columns |
| 194 | (LAST UPDATE → RECEIPTS → ELAPSED) → ⑨ clock sheds date prefix → |
| 195 | ⑩ option strip 4→2 columns. Transcript and composer never shed below their |
| 196 | existing floors (`MIN_CHAT_HEIGHT = 3`, `MIN_COMPOSER_HEIGHT = 2` + border). |
| 197 | Topbar ①–⑤ and ⑨ are implemented and tested. |
| 198 | |
| 199 | ## 5c. Golden buffers |
| 200 | |
| 201 | 12 files, cell-exact, under `crates/tui/src/tui/goldens/`: |
| 202 | `topbar_{startup,work,settings}_{80x24,100x30,120x32,160x40}.txt` ✅ |
| 203 | (`BLOCKER_SIZES` from `views/status_picker.rs:423`). The remaining 8 names in |
| 204 | §5a are reserved; each lands with its component. Re-bless: |
| 205 | |
| 206 | ```sh |
| 207 | CODEWHALE_BLESS_GOLDENS=1 ./scripts/dev-test.sh tui topbar |
| 208 | ``` |
| 209 | |
| 210 | The harness (`topbar/tests.rs`) renders through `TestBackend`, dumps |
| 211 | `Buffer` cell symbols, and asserts byte-exact equality; a missing golden |
| 212 | fails the test unless blessing. Goldens are the design contract — a visual |
| 213 | change that cannot show as a golden diff did not happen. |
| 214 | |
| 215 | ## 5d. Color contract |
| 216 | |
| 217 | Semantic roles only — `ChromeInk` (17 variants, `palette/grammar.rs:49`) via |
| 218 | `chrome_style`; **no hex in widgets**, presets stay in `palette/`. Grammar |
| 219 | families obey `STATUS_BAR_COLOR_GRAMMAR.md`: |
| 220 | |
| 221 | | Element | ChromeInk | Family | |
| 222 | |---|---|---| |
| 223 | | Wordmark | `Attention` (gold) | Cognition — restrained text identity | |
| 224 | | Segment labels / separators / clock | `Metadata` / `MetadataDim` / `MetadataHint` | Metadata | |
| 225 | | Route · model · run | `Identity` | Identity | |
| 226 | | Pod (live) / `3/4` / context meter / theme name | `Active` / `Info` | Active / Identity | |
| 227 | | `not connected` / ≥80% meter | `Waiting` / `Attention` | Cognition | |
| 228 | | Receipt working / done / caution / failed | `Active` / `Outcome` / `Attention` / `Failure` | Active/Outcome/Cognition/Failure | |
| 229 | | Ledger `failed` (preview error row) | `Failure` | Failure — red stays failure-only | |
| 230 | |
| 231 | Resolution paths: truecolor presets → ANSI-256 → ANSI-16 injective role |
| 232 | matrix (`palette/adapt.rs:598`, `role.ansi16()`), ANSI-16 backgrounds drop to |
| 233 | `Color::Reset` (`adapt_bg`). Nonempty `NO_COLOR` selects a distinct monochrome |
| 234 | depth: foreground, background, and underline colors become `Color::Reset`, |
| 235 | while bold, underline, reverse, and selection symbols remain. ASCII rendering |
| 236 | is independent. Dark/Light/Deepsea all resolve through the same output adapter. |
| 237 | |
| 238 | ## 5e. Motion table (wall-clock keyed; `MotionMode`-gated; still frames carry the facts) |
| 239 | |
| 240 | | Motion | Trigger state | Cadence | Cell footprint | Still frame | Gate | Mechanism | |
| 241 | |---|---|---|---|---|---|---| |
| 242 | | Echolocation | turn active (any phase) | 420 ms × 6 frames (`widgets/header.rs:13`) | 7 cells, band; 1 cell chip; never shifts adjacent text | static `<·>` (chip: `·`) | Reduced freezes frame 0; Still static | pure fn of `(turn_started_at, Instant::now)` beside `header_status_indicator_frame`; replaces braille `spinner.rs` frames | |
| 243 | | Spout | turn success, once (suppressed if next turn <400 ms) | one-shot 900 ms, ease-out, droplets +60–110 ms | 1 col × 3 rows above activity band's right edge; ≤4 cells | skipped — `surfaced ✓ …` summary line carries it | `Full` only | `surfaced_at: Instant` + `FrameRequester::request_at` for the 900 ms window, direct `Buffer` writes (`paint_marks` technique); failure surfaces flat `✗`, no motion | |
| 244 | | Bioluminescent tail | tokens streaming | decay 1.5 s (existing), 12 graphemes | ≤12 cells restyled, no new draws | frozen settled brightness (current behavior) | Reduced/Still keep freeze semantics | extend `hot_tail.rs`: keep `HOT_TAIL_GRAPHEMES=12`, `breath_luminance` period; add accent-tint step truecolor, bold-only ANSI-16 | |
| 245 | | Depth line | token count changes only (turn boundaries + stream ticks) | no private clock | ≤16 cells in footer right | identical (static per count) | always visible, all modes | hand-rolled span builder (not `Gauge`): `▁▂▄▆∿▄▄▄▄ 61%`; ≥80% cap `▲` Attention + `surface soon — /compact` | |
| 246 | | Idle breath | idle, nothing else moving | 4.2 s cosine luminance, 1 cell | header bit `·` dim→bright→dim | dim `·` | `Full` only | same cell, luminance-only function of `Instant`; wave rules `⋯ ∼∼∼ ⋯` are static `Span`s | |
| 247 | |
| 248 | Cadence law: everything is a function of `Instant`/wall clock (echolocation |
| 249 | reuses the 420 ms heartbeat), never a frame counter; `FrameRequester` stays |
| 250 | the only additional-frame source and the poll loop the only draw emitter. |
| 251 | |
| 252 | ## 6. Clickability contract (the grokbuild bar) |
| 253 | |
| 254 | Every selectable target follows the registry discipline from |
| 255 | `tideline-redesign.html` ("Every navigates"): visible focus, recorded hitbox, |
| 256 | keyboard + mouse parity, an inspect action, no decorative glyph asks to be |
| 257 | clicked. Mechanically this repo already has the pattern — `WorkHitbox` |
| 258 | storage, `viewport.last_workflow_panel_area`/`last_workflow_cancel_area`, |
| 259 | `launch.row_areas`, `sidebar_hover.sections` row matching |
| 260 | (`mouse_ui.rs:270,441,695`). This spec adds **one** new registry: |
| 261 | `viewport.last_topbar_hitboxes: Vec<(TopbarSegmentId, Rect)>` filled by |
| 262 | `topbar::topbar_hitboxes` at render (same shapes as painted cells — asserted |
| 263 | by test) and consumed in `mouse_ui` exactly like the workflow-panel cancel |
| 264 | zone. Hover = value ink brightens + underline (tested); press = the segment's |
| 265 | action (`Model→` provider inspector, `Pod→` ledger, `Context→` context |
| 266 | inspector, `Theme→` theme picker, `Run→` run dashboard, `Brand→` menu). |
| 267 | Keyboard parity: Tab cycles topbar → rail → stream → ledger → composer; |
| 268 | Enter activates the focused target; Esc backs out. Ledger rows, quick |
| 269 | actions, theme rows, and option-strip tiles reuse the row-rect pattern above — |
| 270 | no second mouse system is introduced. |
| 271 | |
| 272 | ## 7. Moment map (craft lens; surprise budget ≤3 per screen) |
| 273 | |
| 274 | Ladder per screen — enter → orient → hover → press → action begins → waiting |
| 275 | → completion → failure → recovery → exit — with the moments that must feel |
| 276 | authored, everything else staying calm: |
| 277 | |
| 278 | - **Startup.** *Enter:* hero breathes once (idle breath) — the quietest |
| 279 | "I'm here". *Waiting (connect):* echolocation on the model segment; the |
| 280 | word `not connected` itself stays still. *Completion (first connect):* |
| 281 | **spout** — the one payoff moment; suppressed if work resumes instantly. |
| 282 | *Failure:* flat `✗` + the model segment says what to do next (microcopy: |
| 283 | "no route yet — /connect"), never "Error". *Exit into work:* composer |
| 284 | keeps focus — no modal re-orientation. Surprise budget: spout and hero |
| 285 | breath; composer focus changes only its border and send affordance. |
| 286 | - **Work + Pod.** *Orient:* the pod-formation tree draws its `├──└──` rows in |
| 287 | one ≤600 ms top-down reveal, then is forever still (continuity: the tree is |
| 288 | the same object in the ledger below). *Waiting:* receipt rows carry typed |
| 289 | state marks (`●○✓!`) + echolocation only on the live row — no duplicated |
| 290 | status anywhere (footer owns phase, ledger owns whales, stream owns events). |
| 291 | *Press:* ledger row `▶` + inspect opens beside the evidence, never |
| 292 | replaces it. *Failure:* the caution/failed receipt is Attention/Failure ink |
| 293 | **plus a word** — color never invents state. Surprise budget: formation |
| 294 | reveal, the legend row that teaches the marks in place. |
| 295 | - **Settings.** *Orient:* the live preview is a real projection of the real |
| 296 | renderers — what you see is what ships. *Press (apply theme):* one depth- |
| 297 | line ripple across the preview's context meter, one-shot, ≤16 cells — |
| 298 | change you can feel without a transition festival. *Hover:* theme rows |
| 299 | preview on hover-delay (300 ms) only in `Full` motion. *Recovery:* Esc |
| 300 | restores the pre-preview theme with no flicker (projection, not mutation). |
| 301 | Surprise budget: the apply ripple, Deepsea's silent ambient life (empty- |
| 302 | cells-only, opt-in, settles under low motion). |
| 303 | |
| 304 | Accessibility is non-negotiable throughout: every motion above has a still |
| 305 | frame; NO_COLOR retains meaning in glyph/intensity, and ANSI-16 adds its role hues; every |
| 306 | target is keyboard-reachable with visible focus; red stays failure-only. |
| 307 | |
| 308 | ## 8. Acceptance status |
| 309 | |
| 310 | - ✅ Topbar implemented end-to-end: deterministic widget, shed order, hitboxes, |
| 311 | ascii-safe, meter warn ink, hover — **6/6 tests green** via |
| 312 | `./scripts/dev-test.sh tui topbar` (6 run, 11,329 skipped; nextest). |
| 313 | - ✅ 12/12 topbar goldens written and asserted at the four blocker sizes. |
| 314 | - ✅ Spec covers every region of all three screens with lineage (§5a), real |
| 315 | constraint lists (§5b), semantic color only (§5d), gated wall-clock motion |
| 316 | with still frames (§5e), clickability registry (§6), moment map (§7). |
| 317 | - **Partial (named):** the remaining components (hero, quick actions, option |
| 318 | strip, rail, receipt stream, ledger, theme list, live preview, settings |
| 319 | rail, composer restyle, footer merge, notifications) are specified but not |
| 320 | implemented; their 8 reserved golden names are empty until each lands. |
| 321 | Wiring `topbar` into `frame.rs` is deliberately **not** done here — it |
| 322 | collides with live writers #5698/#5699 and belongs to the first landing |
| 323 | slice after their bases settle. |
| 324 | |
| 325 | Not authorized / not done: no merge, no PR, no deploy, no `BASE_PROMPT` or |
| 326 | `motion/ethos.rs` edits, no billing, no writes into other agents' worktrees. |
| 327 |