Skip to content

feat(desktop): animate the terminal splash banner - #4541

Merged
tlongwell-block merged 15 commits into
max/tui-rendererfrom
sami/tui-splash-waves
Aug 3, 2026
Merged

feat(desktop): animate the terminal splash banner#4541
tlongwell-block merged 15 commits into
max/tui-rendererfrom
sami/tui-splash-waves

Conversation

@tlongwell-block

Copy link
Copy Markdown
Collaborator

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 term wordmark travelling left-to-right. Eva's amendment (evt 5c2182c3): 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 hue axis and the colour table that makes it affordable.

Field wave is a projection, 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.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 a waves? override, so no module state is mutated to test them.

Invariants, all gated

  • prefers-reduced-motion takes the static painter call — the shipped banner byte for byte, not a paused animation. Those differ: a stopped loop parks on whatever phase it halted at. Gated against an independent oracle that walks the geometry and computes expected colours with bannerColor directly.
  • 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 at hue 0.2188. The blend is re-lifted with the same operator that placed the endpoints. Raising bucket count only shrinks the dip asymptotically; lifting is exact.
  • Field keeps its monotonic 2.60 → 1.04 decay and bevels stay phase-invariant. hue and the contrast axis are separated, so a moving wave cannot brighten the rim or make the chassis appear to deform.
  • Smoothness in CIEDE2000, per theme, relative to that theme's own static worst step (+1 JND). Euclidean sRGB scores hue rotation at constant lightness as harshly as a brightness cliff and reversed the verdict on 20 themes — worst excess is 0.47 at defaults, <0.62 through wavelength 2.0, and 8.92 (24 themes failing) at 4.0, so the gate bites.
  • Seam: a cyclic phase cannot drive an open 3-stop ramp — a raw wrap put a 182.5 sRGB edge on screen once per period. A palindrome fold fixes it, so direction is only defined inside a half-cycle and the direction gate measures a crest centroid.

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.

grid glyphs flat pre static anim anim−pre anim %60Hz
112×46 1973 512 768 44138 1467 699 8.80%
152×63 3957 993 1502 94988 2733 1232 16.40%
228×90 8893 2123 3254 210804 5831 2577 34.98%

µ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−pre is the honest LUT cost; anim−flat would 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 and hex2rgb(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 rgb2hex rounding is load-bearing, not incidental: the old nested-mix path 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 to d5be7e54…, so it can fail.

Verification

  • Full desktop suite 4011/4011 pass, 0 fail; tsc --noEmit clean; biome clean on every file this PR touches.
  • Mutation battery 9/9 killed. It found three gate defects, all fixed: a vacuous reduced-motion gate that compared the static path against itself; an unasserted intensity clamp; and 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 distinct t.
  • git merge-tree vs Mari's fix(desktop): retain terminal focus on viewport click #4528 (b76a9d605): 0 conflicts — we both touch TerminalSubstrate.tsx, so textual cleanliness isn't enough. I built the merged tree (20502735…) and ran it: tsc rc=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.

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>
@tlongwell-block
tlongwell-block requested a review from a team as a code owner August 3, 2026 14:56
npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc and others added 14 commits August 3, 2026 11:02
… 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

![New Channel with Type, Visibility, and optional
Template](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4549/create-channel-type-visibility-template.png)

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>
@tlongwell-block
tlongwell-block merged commit 105cac0 into max/tui-renderer Aug 3, 2026
32 checks passed
@tlongwell-block
tlongwell-block deleted the sami/tui-splash-waves branch August 3, 2026 18:10
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.

6 participants