Repository navigation
fix(tui): stabilize matrix rainfall + pin reveal letters to real drops - #49
Merged
Merged
Conversation
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two visual bugs in the matrix-rain panel:
update_matrix_rain_intensitywas shiftingmatrix_rain_foreground_epochforward every frame intensity rose. Each column'shead = ((now − epoch)/cell_ms + …) % cyclereads the shared epoch, so the shift teleported every drop on screen — looking like the whole rainfall restarted in place.rain_pass_elapsed_mspredicted head-arrival times using a phase formula (offset = (seed >> 8) % cycle) that didn't match the formulaforeground_rain_frameactually 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
foreground_epoch. Set once at app start; column clocks are strictly monotonic.foreground_rain_frame. Drop position is now((now − epoch) / cell_ms + seed_phase) % cycle. No teleports. A seed-derived phase keeps columns staggered.matrix_rain_active_dropswhen its head is at the top (≤ 1) andactivity ≥ 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.drop_heads: Vec<Option<i16>>.render_matrix_revealwalks the reveal's letters and pins each the first frame its column's head reaches the letter's row.RevealWordgrows apin_state: Vec<Option<u64>>+ apin_lettersetter;MatrixRain::active_reveals_mutlets 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.rain_pass_elapsed_msand 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 newforeground_rain_frame_*tests (heads advance by exactly 1 cycle step percell_ms⇒ never teleport; columns are phase-staggered).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.