feat(desktop): animate the terminal splash banner - #4541
Merged
Conversation
Two independent colour waves over the existing banner geometry: the honeycomb field and the `buzz term` wordmark, each with its own direction, period, wavelength and intensity. The field wave is a PROJECTION onto a direction vector, not a per-axis sweep — phase(cell) = (x*dx + y*dy)/wavelength - t*speed — normalized in VISUAL space, because terminal cells are ~2:1 (8.4x17 CSS px). Without the aspect correction a "45 degree" direction renders at ~63 degrees. Default travels bottom-right to top-left. Both layers are tunable from one config block per layer at the top of terminalBannerWave.ts (FIELD_WAVE / WORDMARK_WAVE), each knob commented with its unit, what "bigger" does, and a measured contrast-safe range. The gates run at the documented extremes. Colour is a phase-INDEPENDENT lookup table, built once per palette. Calling the solver per cell per frame costs 42.7ms at 112x46 and 203ms at 228x90 — dead on arrival by 2.6-12x, because the field's contrast pin is a 28-step bisection per cell. The table is keyed on sampled (layer, hue, contrast) only, so steady-state frames are a lookup and a lerp, and the lookup path holds no strings: the tables are parsed to numeric mirrors at build time and blended in RGB. Measured 5.3x faster than blending via the hex helper (13.6ms -> 2.6ms of lookup per frame at 228x90), with the intermediate quantization reproduced exactly so output bytes do not move — verified byte-identical over 20.4M lookups x 62 themes, with the oracle's own negative control confirming it can fail. Accessibility and invariants, all gated: - prefers-reduced-motion takes the static painter call — the shipped banner byte for byte, not a paused animation parked on some phase. - The wordmark holds WCAG 4.5:1 on every frame. Interpolating between samples that sit exactly ON the floor dips UNDER it (contrast is convex along an sRGB chord; measured 4.4766 on solarized-light), so the blend is re-lifted with the same operator that placed the endpoints. - The field keeps its monotonic 2.60 -> 1.04 decay and the bevels stay phase-invariant: hue and the contrast axis are separated, so the wave cannot brighten the rim or deform the chassis. - Smoothness is measured in CIEDE2000, per theme, relative to that theme's own static worst step. Euclidean sRGB scores hue rotation at constant lightness as harshly as a brightness cliff and reversed the verdict on 20 themes. Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
… header
The module header still quoted Tyler's ORIGINAL directive ("right to left")
and summarized the field as "right-to-left across the whole viewport". The
config comment, the default vector `{x:-1, y:-1}`, and the direction gate all
implement the SUPERSEDING amendment (originates bottom-right, travels
top-left) — so the file's stated contract contradicted its shipped behavior,
and the header is what the next reader trusts first.
Now the amendment is quoted and marked authoritative, the original is kept
and marked superseded (so the history stays readable), and the field summary
names the actual axis with a pointer to FIELD_WAVE.direction.
Comment-only. Every changed line is a docblock line; the byte oracle's
digest is unchanged at COMBINED 96cb28f6...b87d over 20.4M lookups x 62
themes, tsc rc=0, biome clean on the file, desktop suite 4011/4011.
Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
The docstring on `cyclicRampPosition` claimed the palette fold made the wave
reverse direction every half-cycle. That claim was FALSE, and it cost a
review round: a reviewer read it, took it as the author's statement of the
code's behavior, and blocked the PR for back-and-forth motion the code does
not produce. The test comment reused the same false premise to justify
sampling direction only over phase 0.1 -> 0.2.
The motion was always correct. Writing the field as
hue(x, phase) = A(G(p(x) + phase))
with `p` affine in position, `G` the fold and `A` affine-monotone, phase
enters `G` only through the sum `p(x) + phase`, so
hue(x + d*u, phase + D) = hue(x, phase) for d = D * span / wavelength
exactly, at every phase, for ANY `G`. The fold shape sets the spatial
profile and cannot touch the direction of motion. Measured: rigid-shift
+3.13px per +0.01 phase at every phase including the alleged turn at 0.5 and
the wrap (worst residual 1.2e-5, predicted 3.128), crest travel monotone
0/200 backward steps, net travel exactly one spatial period. What the fold
actually does is make a fixed CELL's colour oscillate, which is what every
travelling wave does to a point it passes over — mistaking that for a
pattern reversal is what put the sentence in the file.
Production behavior is unchanged: the diff to terminalBannerWave.ts is
comment-only, and the byte oracle is identical at COMBINED 96cb28f6...b87d
across all 62 themes, so the WCAG 4.5 and dE00 certificates transfer.
The gate now sweeps the whole cycle — including the fold turn and the wrap,
the two places the old one avoided — with two instruments (rigid-translation
fit bounded inside one spatial period, and branch-continuous crest tracking)
and two positive controls it must kill: phase folded in TIME (the motion the
false comment described) and negated phase (a sign error).
Widening it exposed two real defects in the gate itself, both fixed here:
- it derived its measurement axis from `FIELD_WAVE.direction`, so flipping
that config rotated the instrument with the bug and survived. The axis is
now anchored to the spec, with a separate assertion that the config still
matches it.
- it called `makeCellHue` with a hand-made phase and never went through
`phaseAt`, so a time-fold injected into the real clock path was invisible.
Every arm now runs the whole shipped chain.
Mutation battery 8/8 killed with sha256-verified restore, re-run after
formatting: aspect-neutral, aspect-inverted, travel-sign, field-direction,
wordmark-sign, real-reversal, nonuniform-rate, wavelength-dropped. Desktop
suite 4012/4012, tsc clean, biome clean.
Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
The pre-push branch-skew hook correctly refused a tree CI will never test: origin/main advanced over desktop/src/testing/e2eBridge.ts, which this branch also carries via its base. Merging the PR's own base rather than origin/main directly: 4c257c8 already contains current main (5e0efb0) AND Max's resolution of that exact e2eBridge.ts overlap, certified with Desktop Smoke E2E (1) green. Re-resolving main's conflict here would duplicate that work and risk diverging from it. Merge-only, no rebase, no force. merge-tree reported 0 conflicts. Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> * origin/max/tui-renderer: fix(desktop): disambiguate provider API key labels and annotate mint key (#4406) fix(desktop): make OpenAI key re-enterable after first save in card mint dialog (#4140) fix(config-bridge): add harness-definition env tier and fix equal-value model override (#3580) test(desktop): prove channel repair boundary Polish mobile composer and messaging UI (#3918) ci(linux): enable mesh-llm feature in Linux release and canary builds (#4524) test(desktop): arm validation error before read test(desktop): atomically start channel validation fix(desktop): retain terminal focus on viewport click test(desktop): isolate deferred channel read fix(desktop): stop the terminal fade clobbering the app surface's compositor hint Revert "Merge PR #4523: close channel validation latch race" test(desktop): close channel validation latch race
## What When editing an agent, show where it runs. The edit dialog previously showed nothing about the backend; the "Where to run" section only existed in the create flow. This adds a read-only **Run on** section to `AgentInstanceEditDialog`: - **Local agents:** "This computer". - **Provider agents (e.g. Kubernetes):** the provider id plus its saved config rows — context, namespace, image, resources, etc. — with labels humanized from the stored keys and rows in provider-schema order (locators first, request/limit pairs adjacent, alphabetical spillover for unknown providers). - Copy states these are the settings **saved at creation** and that the run location can't be changed afterwards (a new agent is required). ## Design decisions (from thread review with @wren + @sami) - **No provider probe on edit.** `info` is executable work, and its schema reflects the plugin *today* (including a freshly generated random namespace default) — not what this agent was deployed with. The stored record is the only honest source. - **Saved settings, not effective settings.** Optional fields a record omits (e.g. `service_account`) are defaulted by the provider at deploy time; we render only what was persisted and never synthesize today's defaults. - **Safe rendering of opaque provider config.** Values render as safe scalars only; arrays/objects degrade to a summary row (React throws on object children — a hand-edited record must not crash the dialog). Falsy-but-present values (`0`, `false`) render honestly. Secret-shaped keys are redacted using the same word-split heuristic as the create-time `validate_provider_config` gate — one definition of "looks like a secret". The gate already blocks such keys on every app write path; display-side redaction is screenshot hygiene and covers hand-edited records. - **`backendAgentId` intentionally excluded:** deploy-time runtime state written on start, not saved creation intent. - **Read-only, no form state.** The backend is immutable post-create (`UpdateManagedAgentRequest` has no backend field), so the section renders straight from `agent.backend` with no reset effect. - `ADVANCED_FIELDS_MOTION_TRANSITION` was duplicated in both agent dialogs; hoisted to `agentConfigOptions` (also keeps the edit dialog inside the file-size ratchet). ## Testing - Unit contract for `summarizeRunOn` (9 tests): scalar honesty incl. `0`/`false`, structured-value fallback, secret redaction fail-safe, preferred ordering with spillover, key humanization. - Playwright spec (4 tests, registered in the smoke project): kubernetes agent with the exact eight-key record a real create flow persisted, local agent, blox agent (`workstation_name`), and redacted secret-shaped keys from a hypothetical future provider. - `pnpm typecheck`, `pnpm check`, full `pnpm test` (3937 pass) green at this head. - Live screenshots posted in the originating Buzz thread. --------- Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
branch-skew again: main advanced during the ~9min pre-push hook run and now overlaps desktop/playwright.config.ts, which this branch carries via its base merge. Merging main so the pushed tree is one CI will actually test. Merge-only, no rebase, no force. merge-tree reported 0 conflicts. My own commits still touch only the 7 desktop/src/features/terminal files. Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> * origin/main: feat(desktop): show saved Run on settings when editing an agent (#4539)
## Summary - show relevant unread threads and active agents when hovering a channel - keep channel-level unread emphasis separate from thread activity dots - make activity rows navigate to the thread and remove demo-only data ## Test plan - `just ci` (all stages passed except the final duplicate native check, which ran out of disk after its earlier clippy pass) - `cd desktop && pnpm exec playwright test tests/e2e/channel-activity-popover.spec.ts --project=smoke` --------- Signed-off-by: kenny lopez <klopez4212@gmail.com>
**Category:** fix **User Impact:** Users can save password-protected identity backups directly to protected macOS folders such as Downloads. **Problem:** Signed macOS builds could not save a portable `.ncryptsec` backup to Downloads because the atomic writer created an unauthorized sibling temporary file. This surfaced as an “Operation not permitted” error after the user completed backup creation. **Solution:** Portable exports now write only to the exact path authorized by the native Save panel, sync and verify the saved bytes, and refuse to truncate an existing backup. Buzz’s app-managed backup retains its atomic writer and durability guarantees. <details> <summary>File changes</summary> **desktop/src-tauri/src/commands/export_util.rs** Clarifies that secret exports use a dedicated writer compatible with native Save-panel authorization. **desktop/src-tauri/src/commands/identity.rs** Routes portable NIP-49 exports through the Save-panel-compatible writer while preserving canonical app state. **desktop/src-tauri/src/key_backup.rs** Adds an exclusive-create portable writer with owner-only permissions, disk sync, byte verification, and cleanup on failure. Keeps the existing atomic writer for app-managed backups. **desktop/src-tauri/src/key_backup_tests.rs** Covers portable export permissions, absence of sibling files, and preservation of existing backups. </details> ## Reproduction steps 1. Install a signed macOS build containing this change. 2. Open **Settings → Profile → Private key → Create backup** and complete backup creation. 3. Save a fresh `identity.ncryptsec` file into `~/Downloads` and confirm Buzz reports success. 4. Open and verify the saved backup with its password. 5. Repeat the save using an existing filename and confirm Buzz preserves the existing file and asks for a new filename. ## Verification - Full desktop Tauri suite: 2,049 passed, 14 ignored - Diagnostic suite: 3 passed - Focused backup coverage: 30 passed - Tauri clippy (`--all-targets -D warnings`), Rust formatting, and `git diff --check`: passed - Push hooks: org safety, branch skew, and desktop Tauri checks passed Signed-production Downloads smoke remains required after merge because the signing workflow is restricted to `main`. Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
## Summary - move **Channel templates** from Communities to Personal settings - always expose the template picker in New Channel, using **None** as the no-template value - create a channel template directly from the picker and select it on return - preview the selected template's current visibility, canvas, agents, and teams - order the channel-creation controls as **Type / Visibility / Template** and mark Template **Optional** - cover populated and empty libraries, inline creation, selection, visibility overrides, mixed agent/team inventory, field order, optional labeling, and settings navigation in Playwright ## Validation Validated at desktop-only tip `76442270c88aa1d533ddca5de9f87cd615183919` with a clean worktree: - focused channel-template Playwright: 2/2 passed - Type / Visibility / Template ordering and muted Optional treatment visually inspected in the replacement screenshot - `git diff --check origin/main...HEAD` passed - PR diff contains exactly nine Desktop files and no Mobile files The pre-push hook was bypassed only for the corrected history push because the inherited Mobile test `keeps follow mode off while a tall newest message stays visible` passes in Linux CI but fails on macOS because its offscreen-child mounting assertion is platform-sensitive. No Mobile code or tests are changed by this PR. ## Screenshot  Originating Buzz channel: `efba7343-e147-48b7-a2aa-15a5f04abc57` --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
…aned Node (#4382) This PR fixes two Windows-specific install failures: Windows Defender blocking the bare `irm|iex` PowerShell install command, and managed Node shims pointing at a version-bumped (now-absent) Node directory. The Defender block (Trojan:Win32/Commando.A!ml) fires before PowerShell runs and is not clearable via Allow. The Node orphaning means shims in the managed npm prefix resolve but fail at runtime with 'node not recognized' because they reference the deleted old Node path. - Replace all three Windows CLI install commands (Goose, Claude, Codex) with a two-step shape — `Invoke-RestMethod` to a named temp file, then execute — to eliminate the dropper signature; a new `windows_install_command!` macro in `discovery/windows_install.rs` generates all three strings at compile time so the shape cannot drift between runtimes - `$ErrorActionPreference='Stop'` aborts on download failure instead of falling through to a missing-file exit-0; `exit $LASTEXITCODE` propagates the vendor script's own exit code - Add `probe_node(executable, expected_version, timeout)` as a bounded seam: stdout goes to a temp file (not a pipe) so no exit path can block on an inherited handle; the child runs in its own process group on Unix so an unconditional group SIGKILL on every exit path terminates all descendants; on Windows `taskkill /T /F` provides the same tree-wide cleanup; `managed_node_runtime_ready()` is a thin wrapper that resolves the managed Node path and calls the seam - Add `resolve_adapter_path()` in `managed_node.rs`: resolves the candidate first, then calls `should_invalidate_adapter()` — a pure predicate that returns `true` only when the resolved path is under `buzz_managed_npm_bin_dir()` AND the managed Node runtime is orphaned; external adapters outside the managed prefix are always preserved Note: CI cannot reproduce the Defender block (no live Defender ML classifier). Proof of fix is structural — the command shape no longer matches the dropper signature. Canary validation on a real Windows machine with Defender enabled is the definitive check. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## The bug buzz-agent emitted its `usage_update` notification in exactly one place: after `ctx.run()` returned. Until that moment a turn's token counters lived only in the prompt task's stack frame. **A turn killed mid-flight reported nothing at all** — the provider had already billed every round it completed, and no consumer ever saw any of it. That is not a corner case for anything that ends a turn on a clock. It is the normal case for a long-horizon benchmark run that relaunches its agent between phases. ## How big Measured against a provider's own billing ledger over one run's window: | | provider ledger | what we recorded | |---|---|---| | the relaunched lead seat | $485 / 348M tok | $98.99 / 90.3M tok | | the two seats that were not relaunched | $29.90 / 856M | $25.81 / 765M — reconciles | 97% of that run's usage rows came back all zeros, against 1–4% for comparable runs that never relaunch. In one 450-phase trial exactly 7 phases recorded any usage — and each of those carries 177k–437k input tokens, a whole session's worth landing in the one phase that happened to end gracefully. Worth being precise about what was *not* wrong, since both were plausible and both were checked: - **Not pricing.** The rates were verified against the provider's endpoints API and match what we charge. - **Not a truncation bug.** The usage files were intact and internally consistent. The tokens were never captured in the first place. ## The fix The run loop now emits a session-cumulative `usage_update` after every usage-bearing provider response, so an interrupted turn has reported everything but its single in-flight request. - **Emitting more than once per turn is already part of the contract.** buzz-acp's `UsageTracker` advances its committed baseline only at publish time, and goose behaves the same way — which is why the tracker was written to tolerate it. - **The turn-start session baseline is snapshotted into `RunCtx`** so the mid-turn figure stays *session*-cumulative. A turn-local number would be discarded by a high-water-mark consumer and lose the turn entirely; there is a test for exactly that. - **Snapshot by value, not a session handle.** The loop reports once per round, and taking the sessions lock on each would serialise concurrent sessions behind one another's provider round-trips. Nothing else advances those counters while the turn holds `busy`, so it cannot go stale. - **One shared `wire::usage_update_payload`** for both call sites, so the mid-turn and end-of-turn shapes cannot drift. A drift there would present as tokens silently vanishing, which is the failure this reporting exists to prevent. ## Why not a SIGTERM handler That was the obvious shape and it does not work. At signal time the counters are not sitting anywhere a handler could reach — they are in the turn's stack frame, and the value the handler would need has not been folded into the session yet. Making usage durable *during* the turn is what actually fixes it; once it is, a handler adds nothing beyond the in-flight request, whose cost is unknown until its response lands. ## Tests - `usage_is_reported_after_each_round_not_only_at_turn_end` — two rounds; asserts the **first** notification carries round 1's counts alone, proving it went out before round 2 returned. - `mid_turn_usage_includes_earlier_turns` — a mid-turn report must be session-cumulative, not turn-local. buzz-agent 18/18 on the `fake_llm` suite, 382 unit. `cargo fmt` / `clippy` / `cargo check --workspace --all-targets` clean. ## Scope Agent-side only, against `main`. The matching harness change — settling usage on the timeout path, which was skipped on the reasoning that an incomplete turn has nothing to flush — is **#4553**, against the benchmark branch, since that harness does not exist on `main`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Atish Patel <atish@squareup.com> Co-authored-by: Claude Code <noreply@anthropic.com>
## Summary - document exact-head trusted approval as the only desktop tagging authorization - explicitly require `desktop_ref=desktop-v<version>` for the internal desktop handoff - replace the stale `squareup/sprout-releases` repository name with `squareup/buzz-releases` ## Audit coverage Compared `block/buzz` release documentation and automation with `squareup/buzz-releases` `main` (`5b09e5c5d71c80a0849a33458f4e45695df515d7`), including its README, agent guide, Buildkite field hint, desktop validator, release validation tests, and protected updater promotion instructions. ## Validation - `bash scripts/test-release-ref-contract.sh` - `git diff --check origin/main...HEAD` Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Summary - replace a platform-dependent mounted-`RichText` assertion with the production follow-mode boundary predicate - retain the jump-to-latest assertion as the visible consequence of follow mode remaining off - leave production behavior and desktop PR #4549 unchanged ## Why `ScrollablePositionedList` may keep an offscreen item mounted within cache extent on macOS while Linux does not. Mounting therefore does not establish whether reversed-list item 0 is at the latest boundary. The replacement reads the list's public `itemPositionsNotifier` and applies the same `index == 0 && abs(itemLeadingEdge) < 0.01` contract used by `message_list.dart`. ## Validation At commit `bc88617e61d8e9edf8fea832baa8d918163ee212` on macOS with repo Flutter 3.41.7: - `cd mobile && ../bin/flutter test` — 1088 passed, 1 skipped - `cd mobile && ../bin/flutter analyze` — no issues - pre-push `mobile-test` and `branch-skew` hooks — passed Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
…ealed The banner's rAF loop was gated on `welcomeVisible` alone. That is not the question "is the splash on screen": this substrate is mounted unconditionally by AppShell on every route and merely CSS-concealed in Buzz mode (`.buzz-terminal-substrate` is `position:absolute; inset:0`), and `welcomeVisible` starts `true` and only clears on terminal INPUT. So for anyone who never opened the terminal the loop ran forever behind the entire app, repainting a canvas nobody can see. Measured in the channel view with the terminal never opened: 120 splash rAF/s indefinitely, versus 0.0/s with the loop neutered to a single paint. It showed up first as CI, not as a visual bug — Desktop Smoke E2E shard 2 failed on three `empty-edit-delete.spec.ts` cases that never touch the terminal. Both parents pass those cases in 2.4-3.3s while both of my heads timed out at 8s+, 9/9 attempts, same spec blob; a green-vs-green control ran 0.93-0.95 against runner variance while my head ran 1.11-1.20 across 177 shared tests. A uniform ~20% slowdown of unrelated specs is what a hidden 120/s repaint loop looks like. The gate is `owner === "terminal"`, and `owner` joins the dependency array so the cleanup arm actually cancels on the transition. `enabled` is explicitly NOT the gate: it is `available && Boolean(active)` where `available` is `isTauri()`, so it is permanently false in the browser (gating the tests green while real users still pay) and true-while-concealed in the app once a session is auto-created on channel open. It is wrong in both opposite directions. Reduced-motion, colour, and geometry paths are untouched: `terminalBannerColor.ts`, `terminalBannerPainter.ts`, `terminalBanner.ts`, and `terminalBannerWave.ts` are byte-identical to the certified head, so the WCAG/dE00 certificates transfer — re-measured rather than assumed. Byte oracle COMBINED 96cb28f6...b87d unchanged across all 62 per-theme digests, with two in-scope controls (HUE_BUCKETS, CONTRAST_BUCKETS) each moving the digest to prove the oracle still sees. Gate: a five-arm unit test driving real owner transitions `buzz -> terminal -> buzz -> terminal` with `enabled` at its default `true`, so arm 1 is the production-shaped state that used to animate. Arm 3 retains the scheduled handle and asserts cancellation, then advances a manual frame clock to prove no hidden paint and no successor callback — a cancelled frame and a frame that was never scheduled are different failures. Arm 5 pins the invariant the `owner`-only gate leans on (`owner === "terminal"` implies `enabled`), which is true by construction today but otherwise unpinned. Five mutants killed: guard deleted, `owner` omitted from deps, cleanup removed, loop disabled everywhere, and `owner` -> `enabled` in a production-shaped state. Desktop suite 4096/4096, tsc clean, biome clean. Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Required by the branch-skew pre-push hook: main changed three files this branch also touches (desktop/playwright.config.ts, desktop/src/app/AppShell.tsx, desktop/src/testing/e2eBridge.ts), so local checks would otherwise have run on a tree CI never tests. One conflict, in AppShell.tsx, and it is a union: ours adds the `useTerminalContext` import for the terminal lane, theirs adds `markAllReadSources` to the existing `AppShell.helpers` import. Both symbols are kept and both are used (`markAllReadSources` at :382, `TerminalBootstrap` at :743). Non-empty base, so the resolution is semantic rather than a whitespace splice. Note for readers comparing against main: `TerminalBootstrap` is absent from main's AppShell.tsx and from main's desktop/src entirely — the terminal feature is not yet merged and lives only on the lane, so the apparent "main removed it" diff direction is main lacking the lane's addition, not a removal to preserve. Merged tree built and run, not merge-tree alone: tsc rc=0, biome clean on the resolved file, desktop suite 4110/4110. Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
tlongwell-block
pushed a commit
that referenced
this pull request
Aug 3, 2026
Conflict: desktop/src/app/AppShell.tsx — union resolution keeping both sides' imports (ours: useTerminalContext; theirs: markAllReadSources). Resolution is byte-identical to the union already certified on PR #4541 (blob 4a25215e…). TerminalBootstrap mount at AppShell retained: the terminal feature is absent from main's tree, so the diff direction is main lacking the lane's addition, not a removal. Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
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.
Sub-PR of #4347. Base
max/tui-renderer@d8a4d63a8. Merge commits only — no rebase, no force.Tyler's ask: slow waves of buzz theme colours across the honeycomb lattice, plus a separate slow wave on the
buzz termwordmark travelling left-to-right. Eva's amendment (evt5c2182c3): the field wave originates bottom-right and travels top-left, as a projection onto a direction vector.What this does
Two independent waves over the existing banner geometry — different directions, different periods. Geometry and glyphs are untouched; this only supplies a
hueaxis and the colour table that makes it affordable.Field wave is a projection, not a per-axis sweep:
normalized in visual space, because terminal cells are ~2:1 (8.4×17 CSS px). Without the aspect correction a "45°" direction renders at ~63°. The direction gate asserts the measured travel angle in visual space, not grid space.
Tunability is one config block per layer at the top of
terminalBannerWave.ts(FIELD_WAVE,WORDMARK_WAVE) —direction,seconds,wavelength,intensity, each commented with its unit, what "bigger" does, and a measured contrast-safe range. Gates run at the documented extremes via awaves?override, so no module state is mutated to test them.Invariants, all gated
bannerColordirectly.hueand the contrast axis are separated, so a moving wave cannot brighten the rim or make the chassis appear to deform.Perf
No per-cell-per-frame
ramp(). Colour is a phase-independent LUT built once per palette; steady-state frames are a lookup plus a lerp.REGIME: Chromium 148.0.7778.96, DPR 2, macOS, headless, throttled (1 paint per rAF tick, vsync ON), paired arms, 5 reps × 120 frames, buzz-dark, cell 8.4×17 CSS px, per-frame paint only. Magnitude is rig-local; only existence of an effect replicates.
LUT fill, one-time per palette: 128 hue × 24 contrast buckets, median 66.2 ms.
µs/frame. Two controls, because one can't interpret the animated arm:
flat= one colour for every cell (Chromium short-circuits the fillStyle setter → glyph raster alone);pre= distinct precomputed colour per cell → raster + the per-cell CSS colour parse any multi-colour banner pays.anim−preis the honest LUT cost;anim−flatwould charge the animation for parse work the static path pays too.The animated banner is 11–36x cheaper than the static banner already on main. The static path solves
ramp()per cell — a ≤200-iteration lift loop plus a 28-step bisection — so shipped static paint is 44–211 ms/frame. It only survives because it paints once. The precompute is a large improvement to the banner's existing cost, not just a budget for the animation.A defect the decomposition caught
The first receipt had 228×90 at 101.6% of a 60Hz budget, 80% of it my own LUT lookup. Cause:
mix()re-parses hex per call andhex2rgb(b)sits inside its per-channel map, so a bilinear field lookup cost ~12 string parses + 3 hex formats per cell per frame. Fix: parse the tables to numeric mirrors once at build time and blend in RGB — 13.6 ms → 2.6 ms of lookup at 228×90.The intermediate
rgb2hexrounding is load-bearing, not incidental: the old nested-mixpath quantized its intermediate blend before the outer blend consumed it, and the WCAG/dE00 certificates were measured against those bytes. The new path reproduces that rounding at the same point, verified byte-identical over 20.4M lookups × 62 themes (COMBINED 96cb28f6…). The oracle's own negative control — dropping only the intermediate quantization — moves the digest tod5be7e54…, so it can fail.Verification
tsc --noEmitclean; biome clean on every file this PR touches.sampleHead(hue)→sampleHead(t), which killed the wordmark wave entirely while every gate stayed green — the hue function and the table were each tested in isolation but nothing asserted the painter composes them. Not an equivalent mutant: head cells have 49 distinctt.git merge-treevs Mari's fix(desktop): retain terminal focus on viewport click #4528 (b76a9d605): 0 conflicts — we both touchTerminalSubstrate.tsx, so textual cleanliness isn't enough. I built the merged tree (20502735…) and ran it:tscrc=0 and the full suite 4011/4011. Her change is JSX at ~L517, mine is an effect at ~L343.Wren reviews at exact SHA
8d97ffde44e738dff10193f75cda693cf9fbc514. Max integrates.