| 1 | # Motion contract |
| 2 | |
| 3 | Central motion policy for the underwater TUI lives in |
| 4 | `crates/tui/src/tui/motion/`. |
| 5 | |
| 6 | ## Modes |
| 7 | |
| 8 | | Mode | Decorative ambient | Status spinner | Streaming | |
| 9 | |------|--------------------|----------------|-----------| |
| 10 | | `Full` | yes | animated braille | steady ~60 FPS display clock (16 ms); catch-up is STAGED, not live — see note below | |
| 11 | | `Reduced` | no | static calm glyph | **same** display clock — not a slow typewriter; no catch-up bursts | |
| 12 | | `Still` | no | static chevron | state-change redraws; stream still coalesces on the display clock | |
| 13 | |
| 14 | Provider SSE deltas are **input**, never animation timing. |
| 15 | `StreamDisplayClock` (`tui/streaming`) coalesces them; `FrameRequester` |
| 16 | coalesces decorative frame wakes. The main `ui` poll loop remains the only |
| 17 | `terminal.draw` emitter — do not add a competing animation loop. |
| 18 | |
| 19 | ## Integration |
| 20 | |
| 21 | - Derive `MotionPolicy::from_settings(low_motion, fancy_animations, force_reduced)`. |
| 22 | - Spinners: prefer `MotionPolicy::spinner_glyph` / `spinner_presentation`; the |
| 23 | frame table stays in `tui/spinner.rs`. |
| 24 | - Streaming: `stream_display_clock.set_allow_catch_up(policy.allows_catch_up_bursts())`. |
| 25 | - Working/phase chrome above the composer (TUI-DOG-008) must stay truthful under |
| 26 | Reduced/Still — calm redraws, not decorative spin. |
| 27 | |
| 28 | ## One-shot phase transitions |
| 29 | |
| 30 | - A successful turn records the first history index owned by that turn. Tool |
| 31 | and agent receipts keep their final geometry and ordering while a bounded |
| 32 | 70 ms stagger briefly dims then settles each row. Reduced/Still skip the |
| 33 | treatment and show the final receipts immediately. |
| 34 | - Ombre depth takes the typed `ShellPhase` as an input. Working leans subtly |
| 35 | deeper, verification leans toward the live surface ink, and waiting, |
| 36 | approval, and failure return the exact static base ramp. |
| 37 | - When an empty-water shell enters Working, fish follow one deterministic |
| 38 | 800 ms flee-and-return arc keyed to `turn_started_at`. It never loops; |
| 39 | waiting, approval, stopped/error, and reduced-motion states remain still. |
| 40 | - These treatments never add/remove transcript rows, change hitboxes, or use |
| 41 | provider delta timing as an animation clock. |
| 42 | |
| 43 | ## Honesty note: catch-up is staged, not wired |
| 44 | |
| 45 | `note_delta_with_backlog` and the catch-up thresholds exist and are tested, |
| 46 | but every production drain site currently calls `note_delta` (queued = 1), so |
| 47 | Full-motion catch-up never actually fires and Full/Reduced stream at the same |
| 48 | steady clock. Do not describe catch-up as live behavior until the real queue |
| 49 | depth/oldest-age metrics are fed in at the `ui.rs` drain sites |
| 50 | (TUI-DOG-017 follow-up). |
| 51 | |
| 52 | A second, unrelated "adaptive chunking" policy (`streaming/chunking.rs`, plus a |
| 53 | `LineBuffer` newline gate) was deleted in v0.9.4: it could only ever decide |
| 54 | "drain everything available", and both `LineBuffer` constructors bypassed the |
| 55 | gate. A commit beat now unconditionally flushes everything received since the |
| 56 | previous beat. Newline-boundary safety for partial code fences is owned by the |
| 57 | incremental markdown parser (`ParseState::commit_complete_lines`), which is |
| 58 | where it is actually in force. |
| 59 |