# crates/tui — agent guidance

Scope: the TUI, the runtime engine embedded in it, and everything a user sees.
Read the repo-root `AGENTS.md` first. Current flakes and known debt are in
the `codewhale-ops` repo, not here.

## The shell grammar (do not regress it)

The default shell is the underwater system (`src/tui/underwater.rs`, `ocean.rs`,
`widgets/`, `views/`). Its contract:

- **One owner per fact.** Route/mode/permission/context live in the header;
  Tasks/To-do in the top strip; receipts and the single live row in the
  transcript; phase/cost/detail keys in the footer. Never restate a fact in a
  second place.
- **One live row.** Settled receipts are still; only the active row and the
  footer phase mark move. Decorative motion exists only in empty idle water and
  stops the instant the user types or anything needs attention.
- **Phase is typed.** `ShellPhase::from_app` derives idle/typing/working/
  waiting/approval/done/failed from real app state. Never invent state in a
  renderer; never compare English strings to detect state — use the enums.
- **Treatment is typed.** `OceanTreatment` (ombre/flat/classic) parses once from
  settings. Every treatment keeps ambient life; appearance and motion
  (`low_motion`, `fancy_animations`) are independent axes.
- **Footer notices go through the toast system** (`push_status_toast` /
  `active_status_toast`), never the legacy `status_message` sink: toasts carry
  level + TTL, errors hold sticky, acknowledgements expire.
- **Compact tiers shed chrome, not content.** At small sizes a room drops
  titles/captions/spacers before the object the user opened it to manipulate,
  and bodies budget from the footer's *wrapped* height (`wrapped_footer_lines` /
  `action_footer_lines`).
- **Rows are objects.** Anything selectable has a hitbox recorded at render
  time, keyboard + mouse parity, and visible focus. Destructive controls arm
  before they fire.

## Localization

Every user-visible string goes through `tr(locale, MessageId::…)` — no hardcoded
English in render paths. Glyphs (`▸ · ▾ ─`), key names (`Enter`, `Alt+?`), and
commands (`/fleet setup`) are composed in code, never embedded in translations.
Adding a string is a four-part change: see `locales/AGENTS.md`.

## Verification

```sh
cargo test -p codewhale-tui --lib --locked             # library unit suite
cargo test -p codewhale-tui --tests --locked           # every crates/tui/tests/ target
cargo clippy --workspace --all-targets --locked -- -D warnings
```

Narrower reruns of the slow acceptance targets, once `--tests` has told you
which one moved:

```sh
cargo test -p codewhale-tui --test pty qa_pty --locked
cargo test -p codewhale-tui --test pty release_runtime_qa --locked
cargo test -p codewhale-tui --test pty terminal_matrix_qa --locked
```

**`--lib` and `--tests` are disjoint target sets.** `crates/tui/tests/` holds
two dozen process-level acceptance targets that a `--lib` run never compiles,
let alone executes, so a green `cargo test -p codewhale-tui --bin codewhale-tui`
says nothing about them. `adaptive_evidence_acceptance` sat red across two
releases for exactly that reason: every routine command anyone ran was a unit-only
run, and only `cargo test --workspace` reached it. Run both, or run the
workspace gate.

Run clippy with `--all-targets`: `--bin` alone skips test targets and lets lints
reach CI.

Real-terminal QA gotchas, learned the hard way:

- The local tmux **server** may carry `NO_COLOR=1` and `TERM=dumb` from old VHS
  runs — launch panes with `env -u NO_COLOR` or all color QA silently lies. tmux
  also force-enables the low-motion overlay; prove full motion with
  `TMUX`/`TMUX_PANE` removed.
- Scripted PTY input: one Enter on the slash menu both accepts the highlighted
  match and runs it. A scripted second Enter lands *inside* whatever modal just
  opened. Send one key, wait, capture.
- Judge motion from repeated captures diffed over time, never single
  screenshots. Layout gates: 40x12, 60x16, 80x24, 100x32, 140x40.
- `CODEWHALE_TUI_DEBUG=1` writes per-frame diff sizes to
  `~/.codewhale/logs/tui-render.log`. Streaming should be tens of cells per
  frame; a multi-thousand-cell frame is only acceptable on a genuine layout
  transition.
