Skip to content

fix(tui): stabilize matrix rainfall + pin reveal letters to real drops - #49

Merged
edwin-zvs merged 7 commits into
mainfrom
matrix-rain-stable-epoch
May 19, 2026
Merged

edwin-zvs merged 7 commits into
mainfrom
matrix-rain-stable-epoch

Conversation

@edwin-zvs

Copy link
Copy Markdown
Contributor

Summary

Two visual bugs in the matrix-rain panel:

  1. Field reset on intensity change. update_matrix_rain_intensity was shifting matrix_rain_foreground_epoch forward every frame intensity rose. Each column's head = ((now − epoch)/cell_ms + …) % cycle reads the shared epoch, so the shift teleported every drop on screen — looking like the whole rainfall restarted in place.
  2. Reveal letters appearing before the drop got there. The old rain_pass_elapsed_ms predicted head-arrival times using a phase formula (offset = (seed >> 8) % cycle) that didn't match the formula foreground_rain_frame actually used (crossing = epoch + threshold·RAMP_UP), so the predicted pin time had no relation to where a real drop was. Compounded by the epoch jumps from Add in-app text selection for TUI copy #1.

What changed

  • Stable epoch. Stop shifting foreground_epoch. Set once at app start; column clocks are strictly monotonic.
  • Threshold-free foreground_rain_frame. Drop position is now ((now − epoch) / cell_ms + seed_phase) % cycle. No teleports. A seed-derived phase keeps columns staggered.
  • Activity gates registration, not position. A column only registers a new drop into matrix_rain_active_drops when its head is at the top (≤ 1) and activity ≥ threshold. Rising intensity adds drops at the top of their next cycle — no mid-screen pop-ins. Falling intensity lets already-registered drops finish their fall.
  • Live letter pinning. The main loop captures each column's current foreground head into drop_heads: Vec<Option<i16>>. render_matrix_reveal walks the reveal's letters and pins each the first frame its column's head reaches the letter's row. RevealWord grows a pin_state: Vec<Option<u64>> + a pin_letter setter; MatrixRain::active_reveals_mut lets the renderer latch pins frame-by-frame. Once all letters pin, the existing hold + fade timer starts. Letters whose column never activates stay invisible until the reveal expires (12 s) — better than appearing fake-pinned.
  • Removed rain_pass_elapsed_ms and its test (function is gone). Reveal pinning is observational, not analytic.

Test plan

  • cargo test -p agentd-cli --release — 108 unit + 1 integration pass; 2 new foreground_rain_frame_* tests (heads advance by exactly 1 cycle step per cell_ms ⇒ never teleport; columns are phase-staggered).
  • Run agent, watch the matrix-rain pane while a codex/claude session is mid-turn → reveal letters should appear one by one as drops pass them, never ahead of the drop.
  • Spin up multiple busy sessions to drive intensity up → drops in already-falling columns stay where they are; new columns only start at the top.
  • Let activity decay → falling drops complete their descent and disappear off the bottom; nothing vanishes mid-screen.

edwin-zvs added 7 commits May 18, 2026 21:47
Two visual bugs in the matrix-rain panel:

1. **Field reset on intensity change.** Every frame intensity rose,
   `update_matrix_rain_intensity` shifted `foreground_epoch` forward.
   Since each column's `head = ((now - epoch)/cell_ms + …) % cycle`
   reads the shared epoch, the shift teleported every drop on screen
   — looking like the entire rainfall restarted in place.

2. **Reveal letters appearing before the drop arrived.** The old
   `rain_pass_elapsed_ms` predicted head-arrival times using a
   different phase formula (`offset = (seed >> 8) % cycle`) than the
   one `foreground_rain_frame` actually used, so the predicted
   "pinned_at" had no relationship to where a real drop was.

Fixes:

- Stop shifting `foreground_epoch`. It's set once at app start and
  the per-column clock is strictly monotonic — drops never teleport.
- Drop the `threshold` param from `foreground_rain_frame`. Position
  is now a pure function of `(now − epoch)`, `seed`, `speed`,
  `cycle`, with a stable seed-derived phase so columns stay
  staggered.
- Move activity gating to drop registration: a column only registers
  a new drop when `head ≤ 1` *and* `activity ≥ threshold`. A rising
  intensity adds columns at the *top* of their next cycle (no
  mid-screen pop-ins); a falling intensity lets already-registered
  drops finish their fall before disappearing.
- Replace `rain_pass_elapsed_ms` with live pinning. The main render
  loop captures each column's current foreground head into
  `drop_heads: Vec<Option<i16>>`. `render_matrix_reveal` walks the
  reveal's letters and pins each the first frame its column's head
  reaches the letter's row. Once all letters are pinned, the
  short hold + fade timer starts. Letters whose columns never
  activate stay unrendered until the reveal expires.
- Add `pin_state: Vec<Option<u64>>` to `RevealWord` + a
  `pin_letter` setter + a `MatrixRain::active_reveals_mut` iter
  so the renderer can latch pins frame-by-frame.

Tests:
- Delete `rain_pass_elapsed_ms_waits_for_natural_column_pass`
  (function removed).
- Rewrite the `foreground_rain_frame_*` tests for the new
  threshold-free signature: one asserts heads advance by exactly 1
  cycle step per `cell_ms` (never teleport), the other asserts
  distinct columns aren't all in phase.
…pin window

Three related fixes to the matrix-rain reveals after live testing:

1. **Vertical reveal mode.** A horizontal word needs every column
   under it to fire a drop through one specific row — at low fleet
   intensity that mostly never happens and the word never completes.
   Add a vertical orientation where the word stacks top-to-bottom in
   a single column; one drop pass pins the whole word.

   Orientation policy (picked at queue time from the current
   intensity):
   - `intensity < 0.5`: every reveal is vertical.
   - `intensity ≥ 0.5`: ~20% horizontal, ~80% vertical.

   `RevealWord` grows `orientation` + lazily-resolved `(col, row)`
   so layout doesn't shift if intensity changes mid-reveal. The
   resolved position is locked on the first render frame, so
   already-pinned letters stay aligned. `observe_event`,
   `observe_pty_activity`, `observe_tool_decision` now take
   `intensity` so the queueing path can pick orientation.

2. **Random per-cycle column activation.** Each column used to have
   a fixed seed-derived `threshold`; columns with high thresholds
   were *permanently* dark at low intensity. Replaced with a
   per-cycle random roll keyed on `frame.key` (which already
   contains `seed + cycle_index`): every column now has the same
   probability `activity` of firing a drop each cycle, scattering
   the lit columns instead of leaving fixed dead ones. The roll is
   stable within one cycle so the decision doesn't flicker.
   `foreground_column_threshold` + `nearest_active_column` are gone.

3. **Tighter pin window.** Horizontal reveal letters were pinning
   any time the column's head was *anywhere* ≥ target_y; on a
   sparse frame that meant a letter could latch based on a head
   already many rows past, appearing "out of nowhere" with no drop
   nearby. Pin now requires `head ≥ target_y && head − target_y <
   tail` — only while the drop's bright body is currently covering
   the cell. Same window applies to vertical reveals.

Tests:
- `pick_orientation_is_vertical_only_below_50_intensity` /
  `pick_orientation_can_choose_horizontal_above_50_intensity`.
- Existing reveal tests updated to the new `queue` signature
  (explicit `RevealOrientation`) and the new `observe_*`
  signatures (explicit `intensity`).
…/50 orientation

Per feedback after live testing:

1. **Vertical reveal looks like a normal drop, with non-random
   letters.** Drop the pin/hold/fade pipeline for vertical reveals.
   Now we just build a per-column `row → letter` overlay from active
   vertical reveals, and the main column loop swaps the random drop-
   body glyph for the word's letter at the matching rows. Same
   `rain_style` as the rest of the drop body — no highlight, no
   held-after-pass. Once the drop's tail moves off the cell the
   letter disappears, exactly like any other body character.

2. **50/50 split at every intensity.** Drop the `intensity < 0.5
   → vertical only` rule. Both orientations show up at low fleet
   activity now that per-cycle random column activation
   (previous commit) means every column has a chance to fire even
   at small intensities.

`render_matrix_reveal_vertical` is gone; vertical render lives
inline in `render_matrix_rain` via the two new helpers
`resolve_vertical_reveal_positions` and `build_vertical_letter_overlay`.
Horizontal reveal path (pin + fade) is unchanged — words there are
meant to read, so they still latch and hold briefly.
…nsity ≥ 0.5

Two coupled tweaks after live testing:

1. **Horizontal only at intensity ≥ 0.5.** Below that, even with
   per-cycle random column activation, ~25% of attempts left a
   horizontal word half-pinned because not every column under it
   fired a drop within 12 s. Lock to vertical there. At ≥ 0.5
   stay 50/50.

2. **Vertical letters are bold.** Same drop color (no extra
   highlight, same `rain_style`) but the cell gets
   `Modifier::BOLD` when it's painting a word letter instead of a
   random charset glyph. Just enough weight for the eye to pick
   the word out of the random tail chars without breaking the
   "looks like a normal droplet" aesthetic.

Tests updated for the orientation policy split (vertical-only
below 0.5, mixed at ≥ 0.5).
Bold (option B) was hard to see against the matrix rain. Replaced
with a hue shift: vertical-reveal letter cells now blend from
`matrix_dim` to `theme.accent_alt` (teal) instead of `theme.accent`
(green). Same head→tail shade ramp as a normal drop, no extra bold,
just a different green→teal endpoint — visible enough to follow the
word, still in the matrix palette so it doesn't read as a separate
UI layer.

New helpers `rain_letter_style` (teal variant of `rain_style`) and
`compute_drop_shade` (so the letter cell computes its own head/tail
shade independently of the surrounding column loop's `shade` var).
The accent_alt teal was too close in brightness to the rain head.
Swap the bright endpoint for `theme.text` — the same near-white
pale green used everywhere else in the TUI for plain readable text.
Vertical letters now end up clearly brighter than the brightest
rain drop while still matching the matrix palette via the same
`matrix_dim` dim end.
Horizontal words need an active drop in *every* column under the
word at the right row to finish pinning — at low/medium intensity
the 12 s lifetime often expired with the word half-written. Bump
horizontal duration to 18 s so each column has more cycles to fire
without overstaying its welcome. Vertical reveals keep the 12 s
duration since one drop pass is enough.
@edwin-zvs
edwin-zvs merged commit 1cf5e32 into main May 19, 2026
1 check passed
edwin-zvs added a commit that referenced this pull request May 19, 2026
Resolved conflicts:
  - crates/cli/src/ui.rs: took upstream's reveal-pin-on-real-drops algorithm
    (#47, #49) and reapplied the SessionEvent::ContextCompacted arms in
    format_event_body + short_event_label. Conflicts were only there
    because cargo fmt churn from this branch touched lines near the
    upstream rewrite.
  - crates/adapter-zarvis/src/provider/codex_oauth.rs: new provider
    introduced by #46 needed a Content::Summary arm matching the other
    providers — emits the SUMMARY_WIRE_PREFIX'd text as a user input
    message in the Responses-API shape.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant