Commit Graph
2124 Commits
Author SHA1 Message Date
8342dfcc58 chore(release): release Buzz Desktop version 0.5.5 (#4809)
## Buzz Desktop release v0.5.5

- **Frozen main:** `25a9cf1be6d245fbd7373cb1160dbc790baf5bd5`
- **Reviewed candidate:** `8380c1f8ead8816bcf1f4ea9f66aa08e2441b15a`
- **Previous desktop release:** `desktop-v0.5.4`
- **Proposed immutable tag:** `desktop-v0.5.5`

This PR must be **squash merged** only after the Desktop Release
Candidate check passes. The branch must remain based directly on current
`main`; stale base, payload drift, incomplete notes, or an unauthorized
merge produce no tag.

The checked-in changelog accounts for every non-merge commit in the
release range. Publication remains bound to the immutable candidate tag.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
desktop-v0.5.5
2026-08-04 16:54:16 -07:00
25a9cf1be6 feat: paste composer text without formatting (#4801)
## Summary

- handle Cmd+Shift+V on macOS and Ctrl+Shift+V on Windows/Linux in the
message composer
- read plain text through the native Tauri/arboard clipboard path in
packaged builds, with a browser-only Clipboard API fallback
- re-enter ProseMirror's paste pipeline with populated `text/plain`
clipboard data so selection, undo, multiline behavior, and paste
observers remain intact
- cover both platform mappings with rendered composer E2E tests that
assert the native command path

## Testing

- `pnpm test` — 4,286 passed
- `pnpm check`
- `pnpm typecheck`
- `pnpm exec playwright test composer-selection-formatting.spec.ts
--project=smoke` — 26 passed
- `cargo check --manifest-path desktop/src-tauri/Cargo.toml --workspace
--all-targets --target aarch64-apple-darwin`
- `just desktop-tauri-test` — 2,206 core tests plus integration and
doc-test groups passed
- full pre-push hooks passed

## Manual verification

Physical packaged-app clipboard verification remains recommended on
macOS, Windows, and Linux. The automated E2E uses mocked Tauri IPC but
asserts the native `read_clipboard_text` command is invoked.

Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-04 16:16:14 -07:00
WesandGitHub 79c52166cf Revert "chore(release): release Buzz Desktop version 0.5.5" (#4808)
Reverts block/buzz#4800
2026-08-04 16:09:33 -07:00
a0ed13de14 chore(release): release Buzz Desktop version 0.5.5 (#4800)
## Buzz Desktop release v0.5.5

- **Frozen main:** `4a2305170eef565bf1836e2859247e67c030f8af`
- **Reviewed candidate:** `2d03d37b05b68186b2caad9da79080032be3ac72`
- **Previous desktop release:** `desktop-v0.5.4`
- **Proposed immutable tag:** `desktop-v0.5.5`

This PR must be **squash merged** only after the Desktop Release
Candidate check passes. The branch must remain based directly on current
`main`; stale base, payload drift, incomplete notes, or an unauthorized
merge produce no tag.

The checked-in changelog accounts for every non-merge commit in the
release range. Publication remains bound to the immutable candidate tag.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
2026-08-04 16:00:29 -07:00
4a2305170e fix: reauthenticate databricks model discovery (#4008)
## Summary
- preserve Databricks catalog 401 responses as authentication failures
and retry discovery exactly once after silently refreshing the rejected
bearer
- preserve runtime OAuth recovery: when discovery has no usable OAuth
credential, `session/new` succeeds with only the trimmed configured
model so the first `session/prompt` can run the existing browser PKCE
flow
- reject a rejected configured `DATABRICKS_TOKEN` with actionable,
non-interactive guidance; static credentials cannot recover through PKCE
- use the configured-model fallback for non-auth discovery failures
without caching failed or fallback catalogs, so later sessions retry
discovery
- keep known Databricks v2 models only for authenticated empty-catalog
responses and mark their provenance
- resolve discovery before MCP spawn or session registration, preventing
failed discovery from leaking resources or consuming session capacity
- permit serialized interactive PKCE only from the explicit saved-agent
model picker; passive draft discovery never opens a browser

## Runtime flow
1. OAuth discovery attempts cached credentials and silent refresh
without opening a browser.
2. If no usable OAuth bearer exists, `session/new` advertises only the
configured model and succeeds.
3. The first `session/prompt` uses `TokenSource::bearer()`, which may
launch browser PKCE.
4. A later session retries discovery and caches only the authenticated
catalog.

## Regression coverage
- rejected-but-locally-fresh OAuth bearer performs one refresh and one
catalog retry
- OAuth mode with no cached token allows `session/new` and returns
exactly the trimmed configured model
- the OAuth fallback is not cached; a later authenticated session
retries discovery and caches the returned catalog
- rejected static tokens still reject `session/new`
- failed discovery does not consume the sole session slot or spawn the
supplied MCP process
- Desktop interactive/passive auth intent, static-token redaction, and
authenticated empty-catalog provenance

## Verification
- `cargo test -p buzz-agent`
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib
commands::agent_models`
- `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml
--all-targets -- -D warnings`
- `cargo fmt --all -- --check`
- `git diff --check`
- full pre-push hooks

## Review
Adversarial review found and drove fixes for session/MCP resource
leakage, duplicate concurrent PKCE flows, sensitive error propagation,
incorrect 403 reauthentication, missing discovery-level coverage,
passive browser launch, and the Desktop file-size ratchet. The final
follow-up preserves the existing prompt-time OAuth flow while retaining
static-token rejection and pre-allocation discovery ordering.

---------

Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-04 15:20:49 -07:00
WesandGitHub 8faf09f9ae Revert "chore(release): release Buzz Desktop version 0.5.5" (#4797)
Reverts block/buzz#4788
2026-08-04 22:16:48 +00:00
a1d78f2959 feat: Buzz entity links — rich preview cards + in-app navigation for repos, PRs, and issues (#4695)
## Summary

Gives Buzz-hosted git entities the same "GitHub-style" chat experience
GitHub links already get: rich preview cards, real titles, and
click-through — except clicks navigate **in-app** to the Projects view
instead of a browser.

- **Spec**: `docs/buzz-entity-links.md` — link scheme, slices, and
deferred work (`buzz://project`, OS deep links, web routes).
- **Canonical `buzz://` deep links**: new
`desktop/src/shared/lib/entityLink.ts` with builders + strict parser for
`buzz://pr?id=…&owner=…&d=…`, `buzz://issue?…`, and
`buzz://repo?owner=…&d=…`, mirrored by a Rust module
(`crates/buzz-cli/src/links.rs`) with a shared golden-format test so the
two implementations can't drift.
- **Preview cards**: `linkPreview.ts` recognizes `buzz://` entity links
*and* HTTPS relay clone URLs (`{origin}/git/<pubkey>/<repo>`, the shape
agents paste today). Both normalize onto the canonical `buzz://` href,
so the two spellings of a repo dedupe to one `Buzz`-provider card
(`BuzzMark` logo) rendered by `link-preview-attachment.tsx`.
- **Title enrichment**: PR/issue cards fetch the real subject from the
relay event (`subject` tag or first content line) via
`useResolvedLinkPreviews.ts`; the cache is community-scoped and reset in
`resetCommunityState()`.
- **In-app navigation**: clicking a card or inline anchor (including
HTTPS relay clone URLs whose origin matches the active relay) routes to
the canonical `30617:<owner>:<d>` coordinate via `goProject()`
(`markdown/entityLinks.tsx`). **Merge dependency: #4671 must merge
first** — route resolution for `30617:` coordinates is implemented on
that branch (`feat/multi-repository-projects`). Entity-link and
external-anchor logic were extracted out of `markdown.tsx` to stay under
the file-size ratchet.
- **Agent side**: `buzz pr open`, `buzz issues create`, and `buzz repos
create` now return a ready-made `link` field (omitted when the relay
returns `accepted: false`), and `base_prompt.md` instructs agents to
paste it verbatim when announcing work.

## Test plan

- [x] Desktop unit tests: pass, including new `entityLink.test.mjs` and
`linkPreview.test.mjs` coverage (golden formats, malformed-link
rejection, clone-URL/`buzz://` dedupe, origin-gated anchor behavior,
label-must-win invariant, cache epoch)
- [x] Rust: `cargo test -p buzz-cli` golden-format test +
accepted/rejected link guard assertions, clippy + fmt clean
- [x] Biome + `tsc --noEmit` clean; pre-push hooks
(desktop-tauri-checks, rust-tests, desktop-test) pass
- [ ] Manual: paste a relay clone URL and a `buzz://pr` link in a
channel — verify one card each, real PR title, and in-app navigation to
the Projects view

Related: [#4671](https://github.com/block/buzz/pull/4671)

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-04 17:54:14 -04:00
4c665aeac3 fix(desktop): serialize tray channel actions for frontend (#4762)
## Summary
- serialize native `openChannel` tray actions with the camelCase field
names consumed by the TypeScript frontend
- prevent a valid tray channel ID from becoming `/channels/undefined`
- add a Rust serialization contract test covering the complete frontend
payload shape


### Root cause
`TrayAction` renamed the enum variant to `openChannel`, but its struct
fields still serialized as `channel_id` and `community_generation`. The
frontend reads `action.channelId`, so tray navigation called
`goChannel(undefined)`.


### Testing
- manually verified the corrected runtime payload and tray navigation
before removing temporary logging
- `just desktop-ci`
- pre-push hooks (desktop checks/tests, Tauri checks, and Rust tests)

---------

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-04 21:43:21 +00:00
b948c54792 chore(release): release Buzz Desktop version 0.5.5 (#4788)
## Buzz Desktop release v0.5.5

- **Frozen main:** `383d9e1eafd569b44b9c835200dba69ef7cec9dc`
- **Reviewed candidate:** `ac589061ef1009f55384536e483cfe9b1260697b`
- **Previous desktop release:** `desktop-v0.5.4`
- **Proposed immutable tag:** `desktop-v0.5.5`

This PR must be **squash merged** only after the Desktop Release
Candidate check passes. The branch must remain based directly on current
`main`; stale base, payload drift, incomplete notes, or an unauthorized
merge produce no tag.

The checked-in changelog accounts for every non-merge commit in the
release range. Publication remains bound to the immutable candidate tag.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
2026-08-04 14:42:01 -07:00
e30db7028f feat(projects): support multiple repositories (#4671)
## Summary
- adopt the finalized NIP-MP project model so one project can enumerate
and switch between multiple NIP-34 repositories
- add project and repository navigation, activity summaries,
existing-repository attachment, and repository access-channel management
- preserve privacy-safe activation provenance for agent-authored
patches, pull requests, issues, and associated commits

## Test plan
- [x] Run desktop typecheck and unit tests
- [x] Run focused NIP-MP, repository access, and provenance tests
- [x] Run Rust formatting and desktop lint checks
- [x] Run the complete pre-push suite after merging current `main`
- [ ] Manually verify project creation, repository attachment,
switching, and access repair on staging
- [ ] Manually verify public-channel and private-agent origin labels on
newly created Git activity

Related: [#4695](https://github.com/block/buzz/pull/4695)

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-04 17:30:12 -04:00
383d9e1eaf fix(ci): make desktop cache test version agnostic (#4791)
## Summary

- derive the current desktop package version in the release cache-key
contract test
- mutate that version in both `Cargo.toml` and `Cargo.lock` instead of
assuming `0.5.4`
- prevent desktop release version bumps from failing generic CI

## Context

PR #4788 bumped Desktop to `0.5.5`, exposing the hard-coded fixture. The
dedicated release candidate check passed, while generic CI failed with
`desktop version changed cache key`.

## Verification

- pre-commit hooks passed
- pre-push hooks passed
- CI will validate the full contract

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-04 14:22:26 -07:00
7bcfe7e0a1 fix(desktop): widen post-Enter timeouts in empty-edit-delete spec (#4792)
## Summary

Increases three Playwright assertion timeouts in
`tests/e2e/empty-edit-delete.spec.ts` from 5s to 10s to fix a
shard-composition flake introduced by PR #4694.

## Root Cause

PR #4694 added `huddle-transcription.spec.ts` (477 lines, 22+ tests) to
the Desktop Smoke E2E suite, shifting shard 2 composition so that
`empty-edit-delete` now runs with significantly more accumulated browser
state. The three affected assertions all wait for a React state update
triggered by pressing Enter in edit mode:

- `alertdialog` becoming visible after an empty edit (tests 1 and 2)
- `edit-target` hiding after a successful non-empty edit (test 3)

These transitions go through the React scheduler. In isolation they
complete in milliseconds. In a loaded headless shard with accumulated GC
pressure, the 5s window became insufficient — test 3 failed 3/3 times in
CI run
[30946444168](https://github.com/block/buzz/actions/runs/30946444168)
with `edit-target` still visible after Enter.

No product code is changed. The empty-edit-delete flow is correct and
untouched by #4694. This is a test-environment timing adjustment only.

## What Changed

- `tests/e2e/empty-edit-delete.spec.ts` — three `{ timeout: 5_000 }` →
`{ timeout: 10_000 }` for the post-Enter React-update waits

## Validation

- `just desktop-check` — passed
- `just desktop-test` — 4194 passed, 0 failed

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-04 17:08:29 -04:00
65f7a10035 fix(desktop): wait for terminal frame before splash (#4781)
## Summary

- keep the first-open Buzz Term splash pending until the active PTY
delivers its first frame
- retrigger the splash effect when that readiness gate changes
- cover the real bootstrap path so startup latency cannot consume the
animation invisibly

## Verification

- Wes manually verified the first-open animation in the worktree
- `pnpm --dir desktop typecheck`
- `pnpm --dir desktop test` — 4,195 passed
- `pnpm exec biome check src/features/terminal/TerminalBootstrap.tsx
src/features/terminal/TerminalSubstrate.tsx
src/features/terminal/TerminalBootstrap.test.mjs`
- pre-push hooks — branch skew, desktop check, and 4,195 desktop tests
passed

The repository-wide `pnpm --dir desktop check` still reports
pre-existing diagnostics in `personaCatalogRelay.test.mjs` and
`terminal.css`; the three changed files pass Biome directly.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-04 13:50:20 -07:00
8b8d86c5d2 fix(desktop): integer-align custom reaction emoji (#4779)
## Summary

- remove the fractional half-pixel translation from custom reaction
emoji
- preserve the existing 28px reaction pill, 14×14 glyph box, and
`object-fit: contain`
- add real-app Playwright coverage for integer centering and non-square
intrinsic dimensions

### Related issue

None found. Follow-up to the Buzz emoji-warp investigation.

### Testing

- `cd desktop && pnpm exec playwright test
tests/e2e/custom-emoji.spec.ts --project=smoke` (15 passed)
- `cd desktop && pnpm test` (4,171 passed)
- `cd desktop && pnpm lint` (passed; two pre-existing informational
`useTemplate` diagnostics)
- `cd desktop && pnpm typecheck` (passed)
- `cd desktop && pnpm exec biome check
src/features/messages/ui/MessageReactions.tsx
tests/e2e/custom-emoji.spec.ts` (passed)

Independent review also mutation-tested the regression coverage by
restoring the half-pixel transform and confirming the new test fails. No
after screenshot is included because the patch preserves dimensions and
fixes subpixel raster alignment; the real-app test asserts the mechanism
directly.

Validated at `bc95969b21b58d83b7f94de4ad25e499e52b35fb`.

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz>
2026-08-04 20:38:19 +00:00
ce3cf3cd25 Polish Huddle voice controls (#4694)
## Summary

- add a visible Stop control for interrupting agent speech
- make push-to-talk available by default while preserving manual mute
controls
- refine agent management, muted audio states, drawer layering, and
return navigation
- suppress duplicate notification sounds for Huddle messages

## Why

Huddles could trap users behind long agent speech, hide useful agent
controls, and leave temporary Huddle state visible after the call. The
drawer also regressed when the terminal substrate began painting behind
the rounded app surface.

## Validation

- `just desktop-ci`
- focused Huddle Playwright coverage for the drawer, speech
interruption, agent picker, and leave navigation

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-04 13:07:50 -07:00
5179726737 fix(local-archive): default both archive settings to enabled (#4750)
## Overview

Both local archive settings — "Archive my agents' observer frames" (kind
24200) and "Archive my agents' turn metrics" (kind 44200) — previously
defaulted to OFF in OSS builds, controlled by build-time env vars. This
had an irreversible cost: observer frames are ephemeral (not stored by
the relay), so any missed events are permanently unrecoverable. This PR
makes both settings default to enabled for all builds and removes the
build-time flag machinery entirely.

## What changed

### Rust

- `observer_archive_default_enabled()` — returns `true` unconditionally;
removed `option_env!("BUZZ_DESKTOP_BUILD_OBSERVER_ARCHIVE_DEFAULT")`
check and `nest_is_dev()` runtime fallback.
- `agent_metric_archive_default_enabled()` — returns `true`
unconditionally; removed
`option_env!("BUZZ_DESKTOP_BUILD_AGENT_METRIC_ARCHIVE_DEFAULT")` check
and its OSS-build test.
- `build.rs` — removed both `rerun-if-env-changed` declarations
(`BUZZ_BUILD_OBSERVER_ARCHIVE_DEFAULT`,
`BUZZ_BUILD_AGENT_METRIC_ARCHIVE_DEFAULT`) and the two baked-env
emitting blocks.

### Build / CI

- `Justfile` — removed `desktop-tauri-test-compiled-flags` recipe (the
dual-compile test machinery).
- `.github/workflows/ci.yml` — removed the "Desktop Tauri compiled-flag
verification" CI step.

### TypeScript

- `useObserverArchiveSeed.ts` — removed `observerArchiveDefaultEnabled`
dep from `ObserverArchiveSeedDeps` and the `policyOn` gate in
`reconcileObserverArchive`; the function now unconditionally calls
`mergeSaveSubscriptionKinds`.
- `useAgentMetricArchiveSeed.ts` — removed
`agentMetricArchiveDefaultEnabled` dep from `AgentMetricArchiveSeedDeps`
and the `defaultOn` flag-check path in `maybeSeed`; the
`hasExplicitChoice` guard is preserved as the sole gate against
re-seeding.
- `LocalArchiveSettingsCard.tsx` — removed `policy` prop,
`observerPolicy` state, and `observerArchiveDefaultEnabled` fetch from
`ObserverArchiveSection`; toggle is now always enabled (just `toggling`
disables it); removed the stale "Always on for internal builds" copy
branch; removed the `observerPolicy !== false` guard from
`handleObserverToggle`.
- `tauriArchive.ts` — updated JSDoc on both default-enabled functions to
reflect always-true.
- `e2eBridge.ts` — changed both mock defaults from `?? false` to `??
true` so E2E tests without an explicit mock override exercise the real
default behavior.

### Tests

- `useObserverArchiveSeed.test.mjs` — replaced `policyOn` dep with
direct merge dep; updated `test_oss_policy_off_no_merge` →
`test_reconcile_always_seeds_24200`; all cancellation, identity-switch,
and ordering tests adapted.
- `useAgentMetricArchiveSeed.test.mjs` — removed `defaultOn` dep and
`test_oss_build_does_not_seed`; updated
`test_internal_build_unset_seeds_*` → `test_default_enabled_*`;
`hasExplicitChoice` guard tests unchanged.

## Preservation of explicit opt-outs

Users who have previously toggled the setting off are unaffected:

- `useAgentMetricArchiveSeed` skips seeding when
`hasExplicitChoice(pubkey)` returns true (localStorage-persisted per
identity).
- Observer archive reconciliation now unconditionally calls
`mergeSaveSubscriptionKinds`, but a user who already deleted the
subscription can turn it off via the Settings toggle, which calls
`removeSaveSubscriptionKind` — this is the existing explicit opt-out
path, and the toggle is now always enabled (not locked by a policy
flag).

## Result

- No `BUZZ_BUILD_*_ARCHIVE_DEFAULT` /
`BUZZ_DESKTOP_BUILD_*_ARCHIVE_DEFAULT` references remain outside
CHANGELOG/history.
- Desktop node tests: 4168 pass, 0 fail.
- `just desktop-tauri-check`: clean.
- `just desktop-tauri-test`: all pass.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-04 16:02:34 -04:00
7bee84da82 fix(mobile): stop oversized read-state retry loop (#4595)
## Summary

- stop retrying remote read-state publishes after the local replacement
blob exceeds NIP-44's 65,535-byte plaintext limit
- preserve every local read marker and leave existing relay state
untouched rather than truncating remote state
- keep incoming remote read-state available while suppressing further
invalid publishes for the manager lifetime

## Why

A repaired/reconnecting relay exposed a 1,404-context read-state on iOS.
The app repeatedly serialized and attempted to encrypt that structurally
oversized blob while reconnect catch-up work was running, saturating
Flutter's debug UI isolate and making channel navigation take roughly
ten seconds.

This is intentionally fail-closed and behavior-preserving: local read
behavior continues, but remote publishing pauses until the manager is
recreated. No protocol or persisted-data format changes.

## Verification

- `flutter test` — 1,093 passed, 1 skipped
- `flutter analyze` — no issues
- pre-push `mobile-test` and `branch-skew` hooks passed at
`0b6423c5d4d583194f0bbe69662912133b9ae1ef`
- independent review by Princess Donut: no blocking findings;
compatibility-safe and correctly fail-closed

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-04 12:37:20 -07:00
e5efd04705 fix(desktop): close reconnect gaps that previously required CMD+R (#4737)
> Opened by Brain (agent) on behalf of @wesbillman.

## Problem

Users report the desktop app doesn't reliably reconnect and can wedge in
states where only CMD+R (or a full restart) restores connectivity
(thread `c2205e2b` in #desktop-reconnecting).

Pinky's empirical light-switch matrix (real `buzz-relay`, SIGTERM/1012 +
SIGKILL × 1s/45s/3min, at `f18a9cb10`) passed 4/4 — the backoff state
machine recovers cleanly from ordinary relay loss. That isolates the
user-stuck states to four special cases a reload resets but the auto
flow never did.

## Fixes

| Gap | Change |
|---|---|
| **G1** — recovery rode solely on the backoff timer (max 30s),
throttled by WKWebView in occluded/background windows; nothing fired on
network return or wake | New `useRelayResumeTriggers`: `online`, window
focus, and visibility→visible call `preconnect()` when the session is
`reconnecting`/`stalled`, rate-limited to one attempt per 5s
(`relayResumeTriggerPolicy.ts`). Deliberately inert for the terminal
`disconnected` state. |
| **G2** — any AUTH `OK false` latched the session terminal forever,
though the relay also rejects for transient causes (duplicate-AUTH
"already authenticated" race, ±60s clock skew, fail-closed allowlist DB
errors) | New `AuthOkTracker` (`relayAuthPolicy.ts`): "already
authenticated" resolves as success; transient rejections retry with
normal backoff; latch only on `restricted:` or after 3 consecutive
rejections. |
| **G3** — an `auth-required:` CLOSED (REQ racing AUTH after reconnect)
permanently deleted the live subscription with no UI signal — frozen
channel while state reads "connected" | Reclassified `auth-required:` as
retryable in `relayClosedPolicy.ts`. Genuinely terminal classes
(`restricted:`, `invalid:`, …) still delete. Can't loop: a truly
unauthenticated session latches terminal at the connection level. |
| **G4** — `useRelayAutoHeal` observed the 2s-debounced connection hook,
so sub-2s flaps never triggered the heal even though `resetConnection`
had already rejected every in-flight query | Auto-heal now observes the
raw connection-state emitter. The existing 15s heal rate-limit still
guards against flap storms. |

Each fix is a colocated pure-policy module + unit tests, matching the
existing `relayReconnectPolicy`/`relayClosedPolicy` pattern.

## Validation

- Full desktop unit suite: **4151 pass, 0 fail** (at branch tip, `pnpm
-C desktop test`)
- `pnpm -C desktop typecheck` and `pnpm -C desktop check` clean
(file-size ratchet respected — `relayClientSession.ts` net −2 lines
despite the tracker wiring)
- Evidence trail: `RESEARCH/DESKTOP_RECONNECT_CMDR_GAP_AUDIT.md`
(audit), `RESEARCH/DESKTOP_RECONNECT_LIGHT_SWITCH_RESULTS.md` (Pinky's
matrix)

## Not covered / follow-ups

- Native macOS sleep-wake was not automated (would kill the harness
session); G1's focus trigger is the mechanism that covers wake in
practice, but a manual sleep-wake verification on a real build is
worthwhile.
- G3 terminal-CLOSED classes (`restricted:` etc.) still silently delete
subs with no UI signal — surfacing that is a separate UX decision.
- Stall-watchdog latency (60s idle + 10s check) left unchanged; G1
triggers largely mask it.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: npub1gjuws2a2dc8z2nszprtg7v6u9q7ffeah3hgl5yx45jwn7y7aqs6s5e9xj6 <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz>
Co-authored-by: npub1yxv5wk0u0fh6dwt925wntn7h397jvteyj4r87ttcd9xae7n2t3lqqj9jmm <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
Co-authored-by: npub1gjuws2a2dc8z2nszprtg7v6u9q7ffeah3hgl5yx45jwn7y7aqs6s5e9xj6 <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz>
2026-08-04 18:33:42 +00:00
cb4a73e17d Dock Buzz Term within channel workspace (#4724)
## Summary

- replace Buzz Term's full-app takeover with a resizable bottom dock
inside the channel content surface
- add a discoverable channel-header button plus hide and
maximize/restore controls
- create PTYs lazily and keep separate, persistent terminal workspaces
per channel
- capture immutable channel/thread context on every terminal session

## Multiple-channel behavior

The dock is a single surface, but its tabs are partitioned by channel.
Switching channels swaps to that channel's sessions without terminating
background PTYs; returning restores them. New tabs capture the currently
visible channel/thread context.

## Verification

At commit `7ca087f8e08c80528387684364a65bf4ccd6315f`:

- `pnpm --dir desktop typecheck`
- `pnpm --dir desktop test` — 4,129 passed
- pre-push repository hooks — desktop check/test, Tauri checks, terminal
Rust suites all passed

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: kenny lopez <klopez4212@gmail.com>
2026-08-04 11:10:47 -07:00
Jemiah WestermanandGitHub bc9e6528a7 perf(relay): index channel-id lookups and skip trace-only reads (#4647)
## Problem

`SELECT id, community_id FROM channels WHERE id = ANY($1) AND deleted_at
IS NULL` is the top **Load by waits (AAS)** on the Buzz Postgres writer.
Two independent causes compound, and both are fixed here.

### 1. No index can serve it

`channels` is `PRIMARY KEY (community_id, id)`, and every secondary
index leads with `community_id`:

| Index | Columns |
|---|---|
| *(primary key)* | `(community_id, id)` |
| `idx_channels_nip29_group` | `(community_id, nip29_group_id)` |
| `idx_channels_dm_hash` | `(community_id, participant_hash)` |
| `idx_channels_community_type` | `(community_id, channel_type)` |
| `idx_channels_community_visibility` | `(community_id, visibility)` |
| `idx_channels_created_by` | `(community_id, created_by)` |
| `idx_channels_ttl_expiry` | `(ttl_deadline)` *(partial)* |

The two tenant-independent lookups carry **no `community_id` predicate**
— deliberately:

- `Db::communities_of_channels` — `WHERE id = ANY($1) AND deleted_at IS
NULL`
- `Db::community_of_channel` — `WHERE id = $1 AND deleted_at IS NULL`

That independence is load-bearing, not an oversight: projecting a row's
*true* owning community regardless of the fetch query's `WHERE` clause
is what makes `Inv_NonInterference` non-vacuous. If the fetch ever
dropped its tenant scoping, this lookup would still report the real
label and the checker would catch the mismatch.

But a composite btree is only usable when its leading column is
constrained, so neither query can use the primary key, and nothing else
leads with `id`. **Both sequentially scan `channels` on every call.**

### 2. In production the result is discarded

Both call sites feed `record_read_message_rows` /
`record_read_by_id_rows`, which call `tracer.record(...)`. Production
binds `NoopTracer` (`crates/buzz-relay/src/state.rs`), whose `record`
body is empty.

The existing guard tests `trace_state`, which is `Some` for every
well-formed request — it only goes `None` on malformed pubkey bytes. So
the scan ran on the hot read path and its output was dropped. This is
the classic eager-argument bug: `log.debug("..." + expensiveCall())`
with no `isDebugEnabled()` check.

### 3. Multiplied per filter

The non-search call site sits **inside the phase-3 per-filter loop**, so
a `REQ` carrying N filters performed N sequential scans of `channels`
before responding.

## Changes

**`Tracer::enabled()`** — a capability check on the trait (the
`isDebugEnabled()` of this seam), defaulting to `true`. `NoopTracer`
overrides it to `false`, and both emitters in `req.rs` now gate on it,
skipping the trace-only DB read entirely in production.

**`migrations/0027_channels_id_lookup_index.sql`**

```sql
CREATE INDEX IF NOT EXISTS idx_channels_id_live
    ON channels (id) INCLUDE (community_id)
    WHERE deleted_at IS NULL;
```

- `INCLUDE (community_id)` — both queries select exactly `(id,
community_id)`, so this is covering and can be served index-only.
- Partial on `deleted_at IS NULL` — matches both predicates exactly,
excludes soft-deleted history, and lets Postgres skip the recheck.
- **Not `UNIQUE`.** `id` alone is *not* unique in this table —
`command_executor.rs` documents that `community_of_channel(channel_id)`
is ambiguous because the same channel id can appear under more than one
community. A unique index would encode a false constraint and fail to
build on any database already holding such a pair.

Worth keeping the index even though fix #1 removes the production
caller: it still runs under conformance, and `community_of_channel` has
the same problem on its own paths.

**`schema/schema.sql`** — mirrored, since a test asserts desired-state
parity.

## Conformance is unchanged

This is the part worth reviewing closely. Under a real tracer
`enabled()` returns `true` and **every emit happens exactly as before**
— the gate only skips *building* emit inputs when nothing observes them,
never an emit that would otherwise have been made. The coverage-breach
guard stays non-vacuous.

`CountingTracer` forwards `enabled()` to its inner tracer rather than
inheriting the `true` default. Both directions matter and both fail
silently:

- inheriting `true` over a `NoopTracer` would keep the overhead this PR
removes;
- hardcoding `false` over a live tracer would suppress the emits whose
absence `EmitGuard` reports as `ImplBug` — masking real breaches behind
expected ones.

Covered by a new regression test,
`counting_tracer_delegates_enabled_to_inner`, which asserts delegation
in both directions.

## Verification

- `cargo check -p buzz-conformance -p buzz-relay` — clean
- `cargo clippy --all-targets` — clean, zero warnings
- `cargo test -p buzz-conformance` — 6/6
- `cargo test -p buzz-relay --lib conformance` — 11/11
- `cargo test -p buzz-db --lib migration` — 7/7
- `just test-unit` (pre-push) — green

Migration-count assertions in `crates/buzz-db/src/migration.rs` were
bumped 26 → 27, with content assertions for 0027 following the existing
per-migration pattern (including a guard that it never becomes
`UNIQUE`).

## Open questions for reviewers

1. **Lock strategy.** Built *without* `CONCURRENTLY`, following
migration 0004's precedent, because sqlx runs each migration inside a
transaction and `CREATE INDEX CONCURRENTLY` cannot run in one. This
takes a brief `SHARE` lock on `channels` (blocks writes, not reads) —
small relative to `events`, but an operator preferring zero
write-blocking can pre-build it by hand and `IF NOT EXISTS` makes the
migration a no-op. I could not confirm whether sqlx 0.9 supports a `--
no-transaction` directive; if it does, that may be preferable.

2. **Diagnosis is static.** This comes from reading the source, not from
`EXPLAIN` against the live database. Worth confirming with `EXPLAIN
(ANALYZE, BUFFERS)` on the writer before/after — that also sizes the win
by revealing the real table size and row counts.

3. **Expected impact** scales with average filters-per-`REQ`, which I
did not measure. `pg_stat_statements` ordered by `total_exec_time` would
confirm this query drops off the top and show whether anything else is
scanning the same way.

Signed-off-by: Jemiah Westerman <jemiah@squareup.com>
2026-08-04 13:59:54 -04:00
0c33a8a55f fix(agents): canonicalize stale persona harness pins (#4631)
Replace the stale `agent_command_override` drop logic in
`apply_persona_snapshot` with a three-tier canonical command resolver.

## What this fixes

The old code dropped a create-time harness pin when the persona switched
to a different runtime, but it had two failure modes:

1. **Preset harnesses invisible.** `known_acp_runtime_exact()` only
searches `KNOWN_ACP_RUNTIMES` (builtins). Preset harnesses such as
OpenClaw live in `PRESET_HARNESSES`, so the destination lookup returned
`None` and the outer `if let` branch never executed — a Goose→OpenClaw
persona switch left the stale Goose override in place, keeping the agent
running Goose instead of OpenClaw.

2. **Pin-side canonical resolution incomplete.** The pin was resolved by
`known_acp_runtime()`, which searches by id/command/alias and returns a
`&KnownAcpRuntime` entry correctly. However, if the *pin* named an alias
(e.g. `claude-code-acp`) and the *destination* was a preset harness
absent from builtins, the outer guard still failed for the same reason
as (1). The alias regression test pins the requirement that the
canonical resolver must handle both sides: alias pins must be recognised
and drops must fire when the destination is a known preset.

## How it works now

`canonical_harness_command(input)` accepts any form a stored override
can take — bare command, alias, path prefix, or runtime id — and
resolves it to the harness primary command through three tiers:

1. **Builtins** — `KNOWN_ACP_RUNTIMES`, matched by id/command/alias.
2. **Static presets** — `PRESET_HARNESSES`, matched by id or normalised
command.
3. **Loaded registry** — custom/preset definitions loaded at runtime.

`command_for_runtime_id` (id-only input, same three tiers) replaces the
two-step `known_acp_runtime_exact`/`lookup_loaded_harness_by_id` pattern
in `record_agent_command`, `effective_agent_command`, and
`try_record_agent_command`, adding the static preset tier so preset
harnesses resolve correctly even without a warm registry.

## Changed files

- `discovery/presets.rs` — `preset_command_for_id`,
`command_for_runtime_id`, `canonical_harness_command`
- `discovery.rs` — re-export new functions; make
`normalize_command_identity` `pub(crate)`; refactor three
command-resolution functions to use `command_for_runtime_id`
- `custom_harnesses.rs` — `loaded_harness_registry` visibility `fn` →
`pub(super)` (needed by `canonical_harness_command`)
- `persona_events.rs` — replace two-step
`known_acp_runtime_exact`/`known_acp_runtime` + pointer comparison with
canonical-command comparison
- `persona_events/stale_pin_tests.rs` (new) — four regression tests:
Goose→OpenClaw drop, OpenClaw→Goose drop, claude-code-acp alias→OpenClaw
drop, same-harness path keep
- `persona_events/tests.rs` — `sample_record`/`sample_persona` exposed
as `pub(super)` for the new test module

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
2026-08-04 13:52:35 -04:00
klopez4212andGitHub e1287c92cc Refine community invite links (#4734)
## Summary

- Separate direct invites from link sharing with a labeled divider.
- Show the generated invite URL inline with truncation and a copy
control.
- Use shared loading feedback and a restrained copy-status resize.

## Validation

- `pnpm -C desktop exec playwright test
tests/e2e/invite-link-copy.spec.ts
tests/e2e/invites-settings-screenshots.spec.ts` (7 passed)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-04 10:39:09 -07:00
0afeac8a7c feat(desktop): persist sidebar observed-unread across webview reload (#3976)
## Problem

`Command+R` (webview reload) wipes the two in-memory refs driving
sidebar channel unread badges: `observedUnreadEventsByChannelRef` and
`latestByChannelRef`. The boot catch-up REQ can only fetch events newer
than each channel's NIP-RS frontier, so thread replies that arrived
before the frontier was passively advanced (the common case) are never
re-discovered.

Inbox is unaffected because it rebuilds candidates from a relay feed
query and checks fine-grained `thread:`/`msg:` markers. The sidebar
badge path lacks an equivalent recovery mechanism.

## Solution

Persist the sidebar's per-event candidate set to localStorage as a
disposable, versioned projection cache
(`buzz-observed-unread.v1:<relay>:<pubkey>`) and hydrate it on boot
before the catch-up REQ runs.

### New files

**`observedUnreadStorage.ts`** — storage module for the cache:
- Keyed
`buzz-observed-unread.v1:<normalizedRelayUrl>:<normalizedPubkey>`
(relay-scoped to prevent cross-community leakage, matching
`threadActivityStorage`)
- Stores validated per-event `ObservedUnreadEvent` rows;
`latestByChannel` is derived at hydration — no divergent dual aggregate
- Age pruning (7d = `READ_STATE_HORIZON_SECONDS`), per-channel cap
(1000), global cap (5000) across all channels in a scope bucket
- Payload `updatedAt` for LRU ordering; registered in
`PURE_CACHE_KEY_PREFIXES` for 2 MiB eviction budget
- Field-level validation on decode; write failure is non-fatal
(session-only degradation)
- Snapshot-owning timers: `scheduleObservedUnreadWrite` deep-clones the
events map at schedule time — a late A-scope timer can never read B's
mutable refs or write under B's key

**`useObservedUnreadPersistence.ts`** — hook that owns all persistence
lifecycle:
- Scope fence: `normalized pubkey + normalized relay` identity;
`isScopeLoaded()` callback guards both projection (`rawUnread`) and
every **observed-cache mutation** (`recordUnreadEvent`, `removeChannel`,
`clearAll`) before touching refs or storage. Note: stale-scope calls to
`markChannelRead`/`markAllChannelsRead` can still affect
`forcedUnreadRef` and NIP-RS markers, which are pre-existing on `main`
and deferred to the NIP-RS arc (see Deferred below).
- Synchronous `pagehide` flush closes the Cmd+R timing gap
(`useReloadShortcut.ts` reloads within 500ms of teardown, before the
1-second debounce fires)
- Identity-reset effect: flushes old scope, resets refs, hydrates from
storage, stamps loaded scope — all atomic; cleanup flushes on unmount
- `clearAll` cancels the pending timer, resets both in-memory refs, and
clears storage in a single transactional operation; `removeChannel`
deletes the channel from both refs and replaces any pending snapshot
with the current full map — never cancel-without-replacement, preserving
sibling-channel events on reload
- Marker-prune effect on `readStateVersion`: evaluates each retained
event with `observedUnreadEventReadAt()` (the same evaluator used by the
projection memo) and removes covered events, rederiving per-channel
latest — never clears a whole channel for a single thread/msg marker
- Returns a stable `useMemo`-wrapped API object keyed on actual deps so
unrelated re-renders do not restart the catch-up REQ
- `isScopeLoaded` is a `useCallback` (not a memoized boolean) — always
reads the ref at call time, never stale

### Modified files

**`useUnreadChannels.ts`** — hook integration:
- Calls `useObservedUnreadPersistence` with all persistence wired
through the returned API
- `rawUnread`: `isScopeLoaded()` guard suppresses A-scope refs from
projecting under B
- `recordUnreadEvent`: `isScopeLoaded()` fence before touching refs;
schedules a debounced write on each successful record
- `markChannelRead` clearObserved path: calls `removeChannel` so the
cleared state survives reload
- `markAllChannelsRead`: delegates to the owner's fenced `clearAll` —
the parent does not reset the observed refs directly; `clearAll` owns
the transactional clear of both refs and storage, preventing a stale
scope-A callback from corrupting scope B

**`localStorageQuota.ts`** — registers `buzz-observed-unread.v1:` in
`PURE_CACHE_KEY_PREFIXES`

## Design constraints

The cache is a **disposable projection**: versioned key, read-through
only, safe to delete wholesale. It does not touch `ReadStateManager`,
marker semantics, or `forcedUnreadStore`. Zero overlap with the NIP-RS
manual mark-read/unread protocol work in progress in another channel;
migration path when that lands is "stop reading the key."

## Test coverage

**`observedUnreadStorage.test.mjs`** covers storage primitives:
- Key normalization, relay-scoped isolation, round-trip correctness
- Age-prune and per-channel cap on read and write; global cap across
channels
- `deriveLatestByChannel` correctness
- Thread-marker prune leaves sibling thread events persisted and lit
- Scope-isolation state machine: A rows visible in A, absent in B,
restored on A again; late A-scope write does not overwrite B's bucket
- Malformed structures/fields, relay/pubkey isolation, quota failure
degradation

**`useObservedUnreadPersistence.test.mjs`** exercises the real hook via
`createRoot` + `act`:
- pagehide flush: event recorded within debounce window survives reload
(headline regression)
- Unmount with pending write flushes before teardown
- `clearAll` cancels pending debounce so no resurrection after reload
- `removeChannel` replaces pending snapshot so sibling channel B
survives reload (two-channel repro)
- Marker prune: thread and channel markers prune covered events; sibling
channels survive
- `isScopeLoaded` returns false before identity-reset effect commits,
true after
- A→B scope switch: pending A-timer is cancelled by flush, A data
persisted synchronously (hydration round-trip)
- Stale `clearAll` from scope A rejects after scope B loads
(observed-cache scope fence)
- Stale `removeChannel` from scope A rejects after scope B loads
(observed-cache scope fence)
- API object identity stable across unrelated re-renders (catch-up
stability)

**`useUnreadChannels.test.mjs`** exercises the full parent-to-owner seam
with real hook mounts:
- Stale `markChannelRead` from scope A does not corrupt B's observed
bucket after flush
- Stale `markAllChannelsRead` from scope A does not overwrite B's bucket
after flush

## Deferred

Issues deferred to the NIP-RS arc (`#unread-messages-ux`) or future
hardening — not regressions introduced by this PR:

- **Stale-scope `forcedUnreadRef` / `markContextRead` exposure**: a
stale scope-A `markChannelRead` or `markAllChannelsRead` still deletes
B's `forcedUnreadRef` entries and advances B's NIP-RS markers via
`markContextRead` before the observed-cache fence rejects. This is
pre-existing on `origin/main` (identical shape at lines 316/330). Fix
requires touching `forcedUnreadStore` and marker paths — out of scope
for Fix A. Deferred to the NIP-RS work.
- **`isScopeLoaded` empty-scope hardening**: `isScopeLoaded()` returns
`true` when `pubkey` and `relay` are empty strings (no active session).
A guard could assert non-empty identity before stamping scope-loaded.
Low risk in practice since the hook is only mounted after auth, but
could be tightened.
- **Catch-up batch scheduling**: `handleChannelMessage` and the catch-up
loop each clone the full events map per event via
`scheduleObservedUnreadWrite`. For channels with large backlogs this
produces O(n) snapshot clones per catch-up batch. A batch-schedule API
(single snapshot at end of batch) would reduce allocations. Not
observable in normal use; deferred as a performance optimization.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-04 12:44:24 -04:00
f86dfc5883 feat(desktop): surface config diff in restart-required badge (#3637)
The "Restart required" badge reports that an agent's running config has
drifted from its spawn-time config, but never says what changed. This
ships the full feature: a typed Rust diff engine and a TS/UI layer that
renders it at every badge site.

## Rust core (spawn-snapshot diff engine)

Replaces the lossy `u64` `spawn_config_hash` with a typed
`SpawnConfigSnapshot`. The snapshot is stamped from the already-resolved
command/env/config values immediately before `spawn()`, closing the race
window where a mid-spawn config edit would suppress the badge.

`SpawnConfigSnapshot::canonical()` is the single JSON projection shared
by the badge and the diff. Drift is `to_value(stamped) !=
to_value(current)`; the diff is a generic leaf walk over those same two
values, so badge-on and diff-non-empty are structurally guaranteed.
Adding a snapshot field reaches the UI with no code change to the diff
engine — `mutation_table_covers_every_serialized_field` fails CI if a
new field arrives without a mutation row.

`eligible_restart_diff(persona_orphaned, Option<TrackedSpawnState>)`
returns the final vector — snapshot walk entries plus a synthetic
`adapter_availability` entry. It returns empty for an orphaned instance
(spawning one would fail) and for agents with no tracked spawn state
(never stamped, can never have drifted). `needs_restart =
!restart_diff.is_empty()` derives from that vector and nothing else.

Redaction policy (`policy_for(path)`) is shared by the wire diff and the
snapshot's manual `Debug` via `is_safe_to_reveal()` from
`managed_agents::env_vars` as the single authority for env-key masking:

| Policy | Paths | Rendering |
|---|---|---|
| `Text` | `system_prompt`, `team_instructions` | character counts only
|
| `MaskedBare` | `args`, `relay_url` | `••••`, no suffix |
| `MaskedSuffix` | non-allowlisted `env.*` | `••••` + last 4 chars when
longer than 8 |
| `Plain` | allowlisted `env.*` (`BUZZ_AGENT_THINKING_EFFORT`,
`BUZZ_AGENT_PROVIDER`, `BUZZ_AGENT_MODEL`, `DATABRICKS_HOST/MODEL`) and
everything else | verbatim |

Default-deny: every env key not in the explicit allowlist stays masked.
`is_safe_to_reveal()` is the single allowlist authority for both the
baked-env display and the diff.

`restart_diff` is omitted from the wire when empty
(`skip_serializing_if`).

## TypeScript / UI layer

New `restartDiff.ts` module defines `RestartDiffEntry`, `RestartChange`,
`JsonValue`; `tauri.ts` and `types.ts` re-export and add `restart_diff`
/ `restartDiff` fields (Rust omission → `restartDiff: []`).

**`RestartDiffBadge`** — hover tooltip capped at 6 entries + "and N
more", `asChild` span trigger (never inside a `<button>`), auto-restart
blurb below the diff list (on/off variant from `autoRestartEnabled`
prop; same `AUTO_RESTART_ON_BLURB` / `AUTO_RESTART_OFF_BLURB` constants
shared with the Runtime-tab banner). **`RestartDiffList`** renders the
full uncapped list for the Runtime-tab banner with `tooltip`/`inline`
presentation variants for correct foreground in both surfaces.

**`ManagedAgentRow` B4 fix** — badge moved to a sibling `div` of the row
expansion button; tooltip trigger has no `button` ancestor.

**`UnifiedAgentsSection`** — both badge sites render
`<RestartDiffBadge>` instead of a raw `<Badge>`, with
`autoRestartEnabled` threaded from `agent.autoRestartOnConfigChange`.

**Side-panel fix** — `RestartDiffBadge` rendered tab-independently in
the `ProfileSummaryView` hero area (was Runtime-tab only — root cause of
the ~50% inconsistency Will reported). Hero badge is `self-center` in
the flex column. `ProfileRuntimeTabContent` early-return checks
`needsRestart` so the banner is never dropped when all other content is
empty. Auto-restart blurb in the Runtime-tab banner uses the shared
constants.

## Wire shape

```jsonc
"restart_diff": [
  { "field": "model",              "change": { "kind": "value",  "before": "gpt-5", "after": "claude-4" } },
  { "field": "system_prompt",      "change": { "kind": "text",   "before_chars": 1234, "after_chars": 1410 } },
  { "field": "env.OPENAI_API_KEY", "change": { "kind": "masked", "before": "••••bc12", "after": "••••xyz9" } },
  { "field": "env.BUZZ_AGENT_THINKING_EFFORT", "change": { "kind": "value", "before": "medium", "after": "high" } }
]
```

`added`/`removed` occur only for dynamic-map keys; nullable struct
fields always serialize as `null`; arrays are atomic leaves (`args`,
never `args.0`).

## Tests

**Rust** — 1902 passing: snapshot mutation coverage, diff entry
serialization, allowlist-aware env masking
(`allowlisted_env_key_shows_plain_value`,
`allowlisted_env_key_is_case_insensitive`,
`non_allowlisted_env_key_stays_masked`),
`unstamped_agent_yields_no_badge_and_no_entries` (both orphan values),
`summary_without_drift_omits_restart_diff_from_the_wire`,
`unstamped_availability_is_not_drift`. Clippy clean, fmt clean.

**TypeScript** — `needs-restart-screenshots.spec.ts`: 11 E2E cases
registered in the smoke project — all three badge sites, tooltip +
keyboard focus, DOM no-button-ancestor assertion, 6+1 truncation,
uncapped Runtime list, unknown field humanisation, side-panel badge on
default Info tab, inactive/friendly-error Runtime opening path.

Consolidates [#3652](https://github.com/block/buzz/pull/3652)

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-04 12:21:59 -04:00
klopez4212andGitHub 540b58920c Polish sidebar unread hierarchy (#4573)
## Summary

- Keep selected sidebar rows regular by default; manually unread rows
become bold immediately.
- Apply a clearer dark-mode hierarchy: standard inactive rows at 75%,
muted rows at 45%, and unread rows at full emphasis.
- Keep hover text color stable while retaining the selected-row and
unread cues.

## Validation

- `pnpm typecheck`
- `pnpm build:e2e`
- Playwright: sidebar badge and channel-mute coverage

## Screenshots

Posted in the PR comments.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-04 15:39:45 +00:00
Luke TornquistandGitHub d0d4acd4fa fix(desktop): show cached display names on startup (#3317)
## Why

Buzz restores cached channels and messages before profile lookups
complete. On launch, that briefly exposes pubkey-derived labels in place
of familiar display names.

## What

- Persist a bounded, relay-scoped cache of last-known display names,
NIP-01 names, and NIP-05 handles
- Seed batch profile queries from those labels immediately, while
keeping them stale so the existing relay request revalidates them
- Keep cached data presentation-only: avatars and ownership metadata are
not persisted or used to seed profile-detail caches
- Remove cleared or missing profiles, purge a relay's labels when its
community is removed, and include the cache in local-storage quota
recovery
- Add unit coverage for parsing, bounds, eviction, malformed data, and
cleared profiles
- Add an E2E regression that delays the relay profile response and
verifies the cached name is rendered first

## Risk Assessment

Low. The cache is disposable, capped at 1,000 entries per relay, scoped
by normalized relay URL, and always revalidated. It contains only public
label fields and does not restore avatars, agent ownership, or
authorization state.

## Verification

- `just ci`
- `pnpm typecheck`
- `pnpm test` — 3,727 passed
- `pnpm exec playwright test tests/e2e/channels.spec.ts --grep "cached
profile labels"` — passed

Generated with Codex
2026-08-04 11:36:42 -04:00
56003ebf98 docs(acp): explain per-channel session model in base prompt (#4729)
## Overview

Agents running in Buzz have no built-in awareness that each channel is
an isolated conversation context. When a human mentions work "you" are
doing in another channel, the current session can misread this as its
own active context and try to coordinate, re-plan, or take ownership of
it — causing confusion and wasted turns.

## What changed

Added a `## Session Model` section to
`crates/buzz-acp/src/base_prompt.md`, inserted immediately after the
opening paragraph and before `## Buzz CLI`. The section explains:

- Each channel is a separate session; multiple sessions of the same
agent identity may be active simultaneously.
- Sessions share core memory, workspace, and relay — but not
conversation context or in-flight reasoning.
- Cross-channel work belongs to the owning session by default; the
current session may take it over only when the human explicitly requests
it.

No runtime code changes. Base prompt only.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-04 10:50:27 -04:00
0542bc8b95 docs(nip-am): normative amendment — cache SHOULD/MUST + pricingIdentity + consumer cost guidance (#4632)
Amends `docs/nips/NIP-AM.md` with three normative publisher-behavior
changes per the cleared Usage v2 plan (plan v3, D4 + D2').

## Changes

### 1. Cache emission semantics (D4)

Replaces the unconditional `MAY` with qualified obligations:

- Publishers SHOULD emit `cacheReadTokens` / `cacheWriteTokens` when the
provider exposes a cache component.
- Publishers MUST preserve an explicit zero when the provider reports
zero.
- Publishers MUST omit the field (never null or fabricated zero) when
that component is unavailable to the publisher — including when the
provider supports it but the harness does not surface it.

An explicit carve-out in both the JSON comment block and the
Numeric-validity prose exempts these fields from the payload-wide null
guidance. Omission is the only valid representation for an unavailable
cache component.

### 2. Optional `pricingIdentity` field (D2')

Adds an optional, non-nullable `pricingIdentity` object (`authority`,
`model`, `cacheClass`), defined as billing authority — distinct from the
transport `Provider` enum.

- `authority` is a registered billing-namespace identifier: exact
lowercase hostname, no scheme, no path, no trailing slash. Registered
values: `api.anthropic.com`, `api.openai.com`, `openrouter.ai`. The set
extends only by NIP amendment. Pricing lookup is an exact string match
on `(authority, model)`.
- Present only when the publisher can prove applicability: direct
official-endpoint connections prove via the actually-requested resolved
model; other routes MUST receive response-supplied authoritative billing
identity.
- MUST omit for custom/overridden base URLs, gateways (unless the
gateway is the named billing authority), unresolved aliases, and turns
where usage contributions carry more than one billing identity
(including identity-bearing mixed with unresolved).
- `cacheClass` is omitted (not null) when not applicable.
- `pricingIdentity` is optional but not nullable — omission is the only
absence representation.
- The existing `model` field retains its non-billing semantics
(configured/session model) and is never overloaded.
- Consumers MUST treat omission as "price unknown" and MUST NOT infer a
price from the session `model` field.

### 3. Consumer cost guidance (D4)

- Consumers MAY recompute cost estimates using the billing identity and
a pricing manifest.
- Consumers MUST retain the provenance of any cost value (e.g.
`manifest-estimated`, `wire-reported`).
- Consumers MUST NOT merge manifest-estimated and wire-reported costs
into an unlabeled total.

Manifest-vs-wire display preference is application policy and
deliberately excluded from this NIP.

## Scope

Doc-only. Single file: `docs/nips/NIP-AM.md`.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-04 10:40:15 -04:00
klopez4212andGitHub d0af845a1d Remove blur from Welcome composer guidance (#4691)
## Summary
- Keep the Welcome composer prompt above the dock blur so it stays
readable.
- Remove blur from the prompt and persona-motion paths.
- Cover the crisp, correctly layered banner in the onboarding browser
test.

## Validation
- `pnpm -C desktop exec biome check
src/features/channels/ui/WelcomeComposerBanner.tsx
tests/e2e/onboarding.spec.ts`
- `pnpm -C desktop build:e2e`
- `pnpm -C desktop exec playwright test tests/e2e/onboarding.spec.ts
--grep "finishing onboarding creates starter channels and focuses
welcome-everyone for a new member" --project=integration`

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-04 07:14:09 -07:00
klopez4212andGitHub a5bf3c5ae1 Refine desktop timeline activity presentation (#4582)
## Summary

- Make channel join/leave activity use the selected inline avatar-stack
treatment.
- Group related membership activity for one hour and preserve
profile/overflow-name interactions.
- Restore the virtualized day-divider handoff and align the sticky date
behavior with the message timeline.

## Validation

- `pnpm check`
- `pnpm test`
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml`
- Visual desktop screenshot captured with seeded membership activity

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-04 07:11:16 -07:00
f18a9cb106 Defer desktop media uploads until send (#4522)
## Summary

- send desktop messages immediately while media uploads continue in
background state across channel navigation
- show immediate progress above the composer and keep Jump to latest
above it
- report the real media stages as Preparing, Processing, Converting,
Uploading, and Finishing
- use Buzz's shared spinner during local media work, then switch to the
real percentage when byte transfer begins
- animate phase-label and status-suffix changes without overlap or
layout jumps
- keep cancel, progress fill, message publication, and community-reset
behavior coordinated with the background task
- use raw Tauri IPC for large browser files so renderer-side byte
serialization does not block initial feedback

## Why

Desktop previously blocked sending while attachments uploaded in the
composer. Large videos could also pause the renderer before progress
appeared, and the progress pill said Uploading while native media
processing was still underway. This makes the initial response immediate
and describes the work actually happening.

## Validation

- `cd desktop && pnpm check`
- `cd desktop && pnpm typecheck`
- `cd desktop && pnpm test` (3,931 passed)
- `cd desktop && pnpm exec vite build --mode e2e`
- `cd desktop && pnpm exec playwright test
tests/e2e/file-attachment.spec.ts --project=smoke` (11 passed)
- focused native media tests (80 passed)
- native Clippy with all targets and features
- pre-push native suite (2,107 passed, 14 ignored; 3 diagnostics passed)

Updated phase snapshots are included in the PR comments.

Split from #4512 so the desktop and mobile changes can be reviewed
independently.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-04 06:59:43 -07:00
iroiro147andGitHub ddcf0aef9f fix(desktop): stop clipping focus ring on channel intro action cards (#2392) (#4374)
## What

Fixes #2392 — the action cards in the empty-channel intro ("Create
agent", "Add people") had their `focus-visible` ring clipped by the
surrounding scroll container.

## Root cause

The cards sit in a `flex ... overflow-x-auto pb-1` row. Setting
`overflow-x` (without `overflow-y`) makes the browser compute
`overflow-y: auto` as well, so the container clips anything painted
outside its padding box — including the cards' `focus-visible:ring-2`
box-shadow. With only `pb-1` padding, the top/left/right of the ring
were cut off when Tabbing to a card.

## Change

`desktop/src/features/messages/ui/ChannelIntroBlock.tsx` — `pb-1` →
`p-1` on the action-cards scroll container, reserving 4px on all four
sides so the focus ring renders fully inside the scroll container's
padding box.

- 1 file, 1 line. No behavior change for mouse users or layout.

## Verification

- `pnpm typecheck` — clean
- `pnpm exec biome check src/features/messages/ui/ChannelIntroBlock.tsx`
— clean
- `pnpm check:file-sizes` — clean
- Desktop unit suite — **3906/3906 pass**

Signed-off-by: Sarthak Singh <sarthak.singh@juspay.in>

Signed-off-by: Sarthak Singh <sarthak.singh@juspay.in>
2026-08-04 06:50:29 -07:00
feccf4eabc Polish mobile inbox and media flows (#4512)
## Summary

- make mobile unread state visible with bold channel names, an animated
Inbox badge, and swipe-to-toggle Inbox rows
- add directional transitions for top-level mobile navigation
- let mobile send while media uploads, with cancellable progress UI
- normalize iOS and Android video uploads, attach poster frames, and
improve native video playback

## Validation

- `just mobile-check`
- `just mobile-test`
- `cargo test -p buzz-media`
- Pixel smoke test
- iPhone smoke test

Desktop background uploads moved to #4522 so the two platforms can be
reviewed independently.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: leader <71e9f2c44a6932b6772caaaccda1911d010463c3e2c6c40410b8329956046801@buzz.block.builderlab.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
mobile-v0.8.0-rc.2
2026-08-04 00:05:13 -07:00
631b05c883 feat: ship Buzz Term (#4347)
## Summary

- ship **Buzz Term** end to end: the terminal engine/runtime, mounted
desktop substrate, and user-visible naming
- add Quinn's tape-deck-inspired banner: a beveled chassis filled by the
`buzz term` wordmark, surrounded by a complete-hex field
- derive the wordmark's three-stop sweep from each theme's terminal
palette so primary, secondary, and accent roles remain visibly distinct
across all 62 shipped themes, including light themes
- paint the banner once on its own pointer-transparent canvas; PTY
rendering beneath it remains unchanged

## Banner behavior

- uses the renderer's shared `8.4 × 17` cell metrics and production
aspect ratio `2.0238`
- regenerates only for viewport/theme changes; palette switches repaint
correctly while the banner is visible
- dismisses on non-empty output from the active terminal session; empty
output and inactive sessions do not dismiss it
- fails closed below **70 columns** rather than squeezing or clipping
the wordmark
- adds **8 lines** to `terminalRenderer.ts` for shared cell metrics and
**zero lines inside `paint()`**

## Screenshots

| Buzz (light) | Buzz Dark |
|---|---|
| ![Buzz Term — Buzz
light](https://buzz.block.builderlab.xyz/media/fe4c50c1cd03645bff8f3cff588353618fd3004f320ab360165b6cb48f041eb6.png)
| ![Buzz Term — Buzz
Dark](https://buzz.block.builderlab.xyz/media/e5731f40fe020070c0b327287b4b0b6f0103adde852d0f6451ad66a0ff9d14bb.png)
|

| Kanagawa Lotus (light) | Red |
|---|---|
| ![Buzz Term — Kanagawa
Lotus](https://buzz.block.builderlab.xyz/media/260b211fe7bd232eb7faa4ec34c99baee5fed380556427c64495ceed3396a8c5.png)
| ![Buzz Term —
Red](https://buzz.block.builderlab.xyz/media/42a90830824683a0054e24649c58564134af831607736d823992181b792087d3.png)
|

Additional production-aspect finals:
[Vesper](https://buzz.block.builderlab.xyz/media/9ca6514b63f8cfb2107a85ca46f16a940c0883848e6fbc718e411af94aa13100.png),
[Min
Dark](https://buzz.block.builderlab.xyz/media/f67bd2970e5d64ffb07b1ae78ab58c847e6ebc23e7e7a48e067eb024dba64ec8.png),
and [Dark
Plus](https://buzz.block.builderlab.xyz/media/290fee08924f37d064abc687ecf3e9526ab05b87e8e56d610f23048949793dbe.png).

The screenshot harness was checked against the shipped painter at this
exact head: all **2,541 draw calls** matched on color, glyph, x, and y;
four deliberate divergence controls fired.

## Verification at `98ebc8f9048bd5f0ceb7e843b67874d642f0b7fd`

- desktop tests: **3,946 / 3,946**
- TypeScript: clean
- checks: pass (two pre-existing informational `useTemplate` notices
only)
- integration/e2e: PASS (independent exact-SHA lane; artifacts recorded
in the originating Buzz thread)
- artifact/dead-path sweep: clean
- redteam G1–G7: PASS
  - all six named banner emitter-deletion mutants die
- independent handwritten five-row full-wordmark fixture kills Quinn's
seven-mutant battery, including a one-pixel glyph change
- real `112 × 46` canvas-rect dismissal tests separately cover active
non-empty, active empty, and inactive non-empty output
- layer-drop and zero-draw painter mutants die; z-order and
pointer-events verified
  - CI's `tsc && vite build` includes all three banner modules
- performance at DPR 2 (worst-case measured envelope):
- one-time content paint: **~0.7–0.8 ms**, paid only when the banner is
built or its palette changes
- busy compositor, CSS `1277 × 697`, backing `2554 × 1394`: **470–497
µs/frame** for the full banner (**2.82–2.98%** of a 60 Hz frame)
- busy compositor, CSS `1920 × 1080`, backing `3840 × 2160`:
**1,139–1,212 µs/frame** (**6.83–7.27%**)
- empty, one-glyph, and full-banner controls converge: compositor cost
follows backing-layer area and DPR rather than painted-cell count
- in the actual idle welcome state, cost is below both vsync-clamped
rigs' resolution; it is not claimed as zero
- **Pane cross-rig spread: resolved at matched loop rate.** Two
independent rigs initially differed 2.3× (58–68 vs 136 µs/Mpx of backing
store; pane, CSS 1277×697 / backing 2554×1394, DPR 2). The cause of
*that* spread is rAF loop rate: the higher figure came from a
free-running loop at ~1600fps. Throttled to ~200–236fps, both rigs read
58–68 µs/Mpx (1.25–1.44% of a 60Hz frame). The busy-composite figures
quoted above remain the **unthrottled worst case** and are conservative
by ~2.3× at the pane. Not established: the mechanism and sign of
free-running distortion (one rig under-charges ~15%, the other
over-charges 2.3×), and the 1080p figure has not been re-measured
throttled.
- the layer paints only on generation/theme/resize and dismisses on
first non-empty active-session output, so the measurable busy cost is a
short-lived worst case rather than a persistent PTY paint-path tax

## Follow-ups in this PR

These are intentionally subsequent commits after the certified
static-banner head, not claims about `98ebc8f90`:

1. close the compositor metrology: remeasure the 1080p point throttled
and characterize the opposite-sign free-running rAF distortion, with
each measurement regime stated
2. add Tyler's animated honeycomb color waves, gated by
`prefers-reduced-motion`, a full 62-theme phase-sweep contrast check,
and DPR-2 per-tick performance certification
3. land the already-proven mounted theme-switch regression probe from
`RESEARCH/BUZZ_TERM_G3A_PROBE/`
4. bound the slow/hang-shaped G1-c mutant `waitFor`
5. optionally trim the generator to its ink bounding box, reducing the
minimum viewport from 70 to 62 columns

---------

Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Co-authored-by: npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6 <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@buzz.block.builderlab.xyz>
Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
2026-08-03 21:51:17 -04:00
b42b093613 feat(mobile): sync per-group channel sorting (#4231)
**Category:** improvement
**User Impact:** Mobile users can sort each channel group by recent
activity or A–Z, with their choices synchronized with desktop.
**Problem:** Desktop supports persistent per-group channel sorting, but
mobile shows the same groups without equivalent controls or shared
preferences. The earlier mobile attempt coupled sorting to unsafe
dirty-state behavior that could overwrite newer cross-client changes.
**Solution:** Add mobile sorting controls and encrypted NIP-78
synchronization using the existing desktop `channel-sort` contract,
while retaining ordinary whole-blob last-write-wins behavior. Local
state is scoped by identity and normalized relay, startup closes
fetch/subscription gaps, and both clients use the same deterministic
ordering rules.

<details>
<summary>File changes</summary>

**desktop/src/features/sidebar/lib/channelSortPreference.test.mjs**
Updates ordering coverage for the deterministic, cross-client A–Z
comparison rule.

**desktop/src/features/sidebar/lib/channelSortPreference.ts**
Aligns desktop channel-name collation with mobile so synchronized
preferences produce the same visible order.

**mobile/lib/features/channels/channel_sort/channel_sort_manager.dart**
Adds encrypted relay synchronization with safe startup gap handling,
clock checks, and ordinary last-write-wins conflicts.

**mobile/lib/features/channels/channel_sort/channel_sort_provider.dart**
Scopes sort state to the active identity and community lifecycle.

**mobile/lib/features/channels/channel_sort/channel_sort_storage.dart**
Defines the desktop-compatible payload, relay-scoped cache and
migration, cleanup, and shared ordering behavior.

**mobile/lib/features/channels/channels_page.dart**
Connects sort state to the channel page.

**mobile/lib/features/channels/channels_page/body.dart**
Applies each selected order to Starred, custom groups, Channels, and
DMs.

**mobile/lib/features/channels/channels_page/sections.dart**
Adds checked Recent and A–Z actions using the existing anchored-popover
UI.


**mobile/test/features/channels/channel_sort/channel_sort_manager_test.dart**
Covers payload adoption, encrypted publication, conflicts, timestamps,
retries, and cleanup.


**mobile/test/features/channels/channel_sort/channel_sort_storage_test.dart**
Covers parsing, relay isolation, migration, cleanup, and ordering modes.

**mobile/test/features/channels/channels_page_test.dart**
Verifies the group controls expose both choices.

</details>

### Reproduction steps

1. Open the mobile channel list with populated built-in and custom
groups.
2. Open a group menu and choose **Sort: Recent**; confirm active
channels move to the top.
3. Choose **Sort: A–Z**; confirm deterministic alphabetical ordering
returns.
4. Repeat for Starred, a custom group, Channels, and DMs.
5. Open desktop with the same identity and community and confirm each
synchronized preference.
6. Switch communities and confirm cached preferences do not bleed across
relays.

### Screenshots

Approved `live` custom-section flow with `research` kept offscreen.

| Recent selected | A–Z result | A–Z selected |
|---|---|---|
| ![live custom section with Recent
selected](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4231/live-recent-selected.png)
| ![live custom section sorted
A–Z](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4231/live-az-result.png)
| ![live custom section with A–Z
selected](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4231/live-az-selected.png)
|

### Validation

- Mobile `flutter analyze` — clean
- Focused mobile sort and channel-page suites — 37/37 passed
- Desktop full suite — 3906/3906 passed
- Mobile full suite — 1034 passed, 1 skipped, 1 unrelated baseline
failure reproduced at `ac4fa13b8`

<!-- Originating Buzz channel: 2a16a2bb-6fd3-4d69-8182-2afcb21b2d14 -->

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
2026-08-03 18:02:09 -07:00
d5da74e4e0 feat(mobile): add channel scroll navigation (#4239)
**Category:** improvement
**User Impact:** Mobile readers can jump directly to their oldest unread
message and return to the latest message with compact directional
controls.
**Problem:** Opening an active channel at its newest message makes it
easy to miss where unread conversation began, while moving back through
history lacks a lightweight route to the live edge.
**Solution:** Capture the channel's unread boundary when it opens, offer
an accessible up-chevron beneath the app bar to reach that stable
target, then reveal the inverse down-chevron at the bottom whenever the
reader is away from latest. Deep links retain precedence, and
live-follow, pagination, composer resizing, and explicit scroll
ownership continue to use the existing timeline behavior.

<details>
<summary>File changes</summary>

**mobile/lib/features/channels/channel_detail_page.dart**
Captures the channel's read state at open time and passes a stable
unread snapshot into the timeline before the normal deferred read update
advances it.

**mobile/lib/features/channels/channel_detail_page/message_list.dart**
Adds mutually exclusive oldest-unread and latest navigation, with
accessible icon controls positioned at opposite edges of the message
surface while preserving existing follow and deep-link behavior.

**mobile/test/features/channels/channel_detail_page_test.dart**
Covers the unread target, compact inverse controls, accessible tooltips,
and placement beneath the frosted app bar.

</details>

## Reproduction steps

1. Open a Flutter mobile channel that has unread messages without
entering through a message or thread deep link.
2. Confirm an up-chevron appears directly below the channel app bar
while the timeline remains at latest.
3. Tap the up-chevron and confirm the timeline scrolls to the oldest
message that was unread when the channel opened.
4. Confirm the unread control is replaced by a down-chevron at the
bottom of the timeline.
5. Tap the down-chevron and confirm the timeline returns to latest and
resumes following new messages.

## Screenshots

| At latest — up-chevron to oldest unread | Away from latest —
down-chevron to latest |
|---|---|
| ![Up-chevron beneath the mobile channel app
bar](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4239/buzz-mobile-scroll-to-oldest-unread.png)
| ![Down-chevron above the mobile channel
composer](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4239/buzz-mobile-scroll-to-latest.png)
|

_Real iPhone 17 Pro Simulator captures from the neutral
`buzz-mobile-scroll-to` channel._

Originating Buzz thread:
`buzz://message?channel=5b16c478-22d8-4ddd-951a-6036e19b81ff&id=6a78af32d7ac6f531b182c4e70dd5a04c503a2dab2ce2c0c74b2c6baa5921741&thread=6a78af32d7ac6f531b182c4e70dd5a04c503a2dab2ce2c0c74b2c6baa5921741`

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
2026-08-03 17:48:41 -07:00
b29c8cdaa4 feat(desktop): redesign the Huddle experience (#4281)
## Summary

- open Huddles in a focused companion window with a clean handoff back
to the in-app drawer and backing channel
- redesign the participant film strip, sidebar control, transcript
surface, and themed shell treatment
- preserve microphone and device control across windows, start agent
voice on the first reply, and show agent speaking activity in the film
strip
- give each agent a distinct session voice, beginning with the
configured default, plus compact per-agent text-to-speech and voice
controls
- enroll only agents explicitly mentioned or deliberately added through
an agent panel into the live Huddle roster
- keep temporary Huddle channels out of the sidebar unless the user
explicitly brings one into the main app
- remove Huddle-only avatar policy badges and filter short silence or
noise segments before speech-to-text posts

## Why

The previous flow exposed the temporary channel as product UI, obscured
who was present or speaking, and split transcript and audio state
between the main and companion windows. This keeps backing channels as
implementation details unless a user explicitly brings a Huddle into the
app, while sharing the live conversation and audio lifecycle across both
surfaces. Agent participants now join only after an explicit invitation,
distinct voices make multi-agent Huddles easier to follow, and short
microphone noise no longer becomes stray transcript messages.

## Validation

- `pnpm check`
- `pnpm build:e2e`
- `pnpm exec playwright test tests/e2e/huddle-transcription.spec.ts
--project=smoke` (13 passed)
- Huddle sidebar visibility unit coverage (4 passed)
- focused managed-agent and persona-mention E2E coverage (2 passed)
- `pnpm test` (3,910 passed)
- `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml
--all-targets -- -D warnings`
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml` (2,093
passed, 14 ignored; 3 diagnostics passed)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-03 17:16:09 -07:00
ede8d22dd5 feat(mobile): bring channel menus to desktop parity (#3940)
## Overview

**Category:** improvement  
**User Impact:** Mobile users can now access consistent channel and DM
actions from both the channel list and conversation header.
**Problem:** Mobile channel menus exposed a narrower, inconsistent set
of actions than desktop, and the available actions differed by entry
point.
**Solution:** This change introduces one reusable action sheet with a
clear quick-action hierarchy, role-aware lifecycle controls,
confirmations for consequential actions, and a deliberately narrower DM
menu.

## Changes

<details>
<summary>File changes</summary>

**mobile/lib/features/channels/channel_actions_sheet.dart**  
Adds the shared channel and DM action-sheet experience used by both
entry points, including Star/Unstar and Read/Unread quick actions for
channels, section movement, mute, management, inline copy actions,
guarded lifecycle actions, confirmations, and a compact DM menu without
quick actions.

**mobile/lib/features/channels/channel_detail_page.dart**  
Routes the header ellipsis through the shared action sheet so the
in-channel menu matches the channel-list experience, including for DMs.

**mobile/lib/features/channels/channel_management_provider.dart**  
Adds archive and delete operations using the desktop-compatible relay
event kinds and refreshes channel state after completion.

**mobile/lib/features/channels/channels_page.dart**  
Makes the shared channel action-sheet entry point available to the
channel-list implementation.

**mobile/lib/features/channels/channels_page/channel_tile.dart**  
Replaces the tile-specific long-press menu with the reusable action
sheet while preserving read state and section context.

**mobile/test/features/channels/channel_actions_sheet_test.dart**  
Covers action hierarchy, owner/admin/member capability guards, loading
and failure states, DM narrowing with no quick-action row, and inline
copy actions.

**mobile/test/features/channels/channel_detail_page_test.dart**  
Updates channel-header flows to exercise management through the new
shared action sheet.

**mobile/test/features/channels/channel_management_provider_test.dart**
Verifies archive and delete event tags stay compatible with desktop
behavior.

</details>

## Reproduction Steps

1. Run the mobile app and open a populated channel list.
2. Long-press a regular channel and verify the Star/Unstar and
Read/Unread quick actions appear above Move to section…, Mute, Manage,
Copy channel name, and Copy channel ID.
3. Choose either copy action and verify it copies the expected value.
4. Open a channel, tap the header ellipsis, and verify the same action
sheet appears.
5. As an admin or owner, verify Archive appears; as an owner, verify
Delete also appears. Confirm that lifecycle actions require
confirmation.
6. Long-press or open the header menu for a DM and verify it has no
quick-action row and starts with Mute, followed by Copy channel name and
Copy channel ID.

## Screenshots

### Channel menu

| Regular channel — Mark Unread | DM — no quick actions | Archive
confirmation |
|---|---|---|
| ![Regular channel actions with Mark
Unread](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3940/final-regular-channel-mark-unread.png)
| ![DM actions without quick
actions](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3940/final-dm-no-quick-actions.png)
| ![Archive
confirmation](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3940/final-archive-confirmation.png)
|

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
2026-08-03 16:23:55 -07:00
985cdcc6ea feat(agents): model-tuning parity in global Agent Defaults editor (#4578)
## Overview

The global Agent Defaults surface (Settings card, defaults modal,
onboarding) exposed structured controls for Effort but left Max Output
Tokens, Context Limit, and Max Rounds as raw env vars. Per-agent dialogs
had structured numeric fields but only for `isBuzzAgentRuntime` —
incorrectly excluding Goose. This PR unifies numeric-tuning capability
across all surfaces, fixes a pre-existing dual-editor defect, and adds
full test coverage.

## What changed

### Phase 1 — Catalog projection

- Add `max_rounds_env_var` to `KnownAcpRuntime` in `runtime_metadata.rs`
(`Some("BUZZ_AGENT_MAX_ROUNDS")` for buzz-agent, `None` elsewhere).
- Project all three numeric env-var fields (`max_tokens_env_var`,
`context_limit_env_var`, `max_rounds_env_var`) end-to-end:
`AcpRuntimeCatalogEntry` Rust struct, TS `types.ts`,
`RawAcpRuntimeCatalogEntry` + `fromRawAcpRuntimeCatalogEntry` in
`tauri.ts`, and the e2e mock bridge (`withMockRuntimeConfigMetadata`).

### Phase 2 — Field model

- `deriveAgentConfigFieldModel` now derives `maxOutputTokens` /
`contextLimit` / `maxRounds` descriptors from catalog-projected fields.
- `structuredEnvKeys(descriptors)` — exported helper that takes the
**rendered** descriptor set (not the whole model). Hidden keys follow
what is actually rendered per surface: global hides effort + all three
numeric keys for buzz-agent / two for Goose; per-agent buzz-agent hides
effort + three numeric keys; per-agent Goose hides only its two numeric
keys. `BUZZ_AGENT_THINKING_EFFORT` stays a visible generic env row
per-agent because no effort control renders there.

### Phase 3 — UI

- Extract `NumericTuningFields` from `buzzAgentModelTuningFields.tsx` as
a shared descriptor-driven component (`descriptors`, `envVars`,
`inheritedEnvVars`, `onEnvVarChange`). Kind-specific minima:
`NUMERIC_KIND_MIN` map (`maxOutputTokens`/`contextLimit`: 1,
`maxRounds`: 0) applied to `<input min>`.
- **Global surface** (`AgentConfigFields.tsx`): deduplicate the
previously duplicated Advanced env-editor block; render
`NumericTuningFields` below the env editor when descriptors exist;
`hiddenKeys` and `bakedGenericRows` exclusions use `structuredEnvKeys`
so structured keys are never double-rendered. Under 1000 lines.
- **Per-agent surfaces** (`EditAgentAdvancedFields`,
`PersonaAdvancedFields`): replace `isBuzzAgentRuntime` as the
numeric-field gate with `deriveNumericDescriptors(selectedRuntime)` from
`agentConfigCore`; hidden keys come from
`structuredEnvKeys(numericDescriptors)` — the same rendered descriptor
set, no local rebuilding (fixes pre-existing dual-editor defect).
Catalog status carried as `RuntimeCatalogStatus` (`loading | ready |
error`); both error and loading withhold structured controls and leave
saved values visible as generic rows, making error distinguishable from
"runtime not capable" (`ready` + no runtime).
- **Dialogs** (`AgentDefinitionDialog`, `AgentInstanceEditDialog`,
callers): `AgentDefinitionDialog` accepts `runtimeCatalogStatus?:
"loading" | "ready" | "error"` (replaces separate
`runtimesLoading`/`runtimesError` booleans); all call sites —
`AgentManagementDialogs`, `AgentsView`, `RequestedAgentCreateDialogs`,
`UserProfilePersonaDialogs` — compute and pass the status.

### Phase 4 — Tests

- `buildRecord` exported from `EnvVarsEditor.tsx` as a pure `(nextRows,
value, requiredKeys, hiddenKeys) => Record<string, string>` helper for
isolation testing.
- **17 new node tests** in `agentConfigCore.test.mjs`:
`deriveNumericDescriptors` (all three fields, partial, undefined
runtime, matches field-model subset); `structuredEnvKeys` per surface
including discriminating Goose per-agent effort-key invariant;
`NUMERIC_KIND_MIN` values.
- **4 new node tests** in `EnvVarsEditor.test.mjs`: hidden tuning key
preserved through generic row edits; runtime-switch then generic edit
(derives both descriptor sets, asserts new-runtime hidden key survives
`buildRecord` via `hiddenKeys` and old-runtime key survives via generic
rows); baked numeric key excluded via `filterBakedGenericRows` with
`numericTuningPlaceholder` assertion; clearing a structured override —
`numericTuningPlaceholder` verifies placeholder text.
- **5 new Playwright tests** in `agent-numeric-tuning.spec.ts` (added to
smoke project `testMatch`): global numeric fields visible for
buzz-agent; global: non-capable runtime hides numeric controls; Goose
per-agent shows `Inherit (16384)` after saving global value through the
UI; delayed catalog: saved values visible as generic rows while loading
then structured controls appear after settle; failed catalog: saved
values remain visible as generic rows (never the "unsupported" empty
state).

## Result

- buzz-agent global defaults: Max output tokens, Context limit, Max
rounds as structured inputs with `Inherit (N)` placeholders from baked
env.
- Goose global defaults: Max output tokens, Context limit as structured
inputs.
- A Goose global value surfaces as `Inherit (<value>)` in the per-agent
Goose edit dialog.
- No structured key is editable in two places on any surface; no
persisted key has zero editors.
- No `runtime.id === "buzz-agent"` comparison decides numeric-field
visibility anywhere — capability flows catalog →
`AcpRuntimeCatalogEntry` → field model → UI.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-03 18:09:24 -04:00
klopez4212andGitHub 027a74a61c Polish Share Compute settings (#3735)
## Summary
- Refresh Share Compute with the shared agent-style model controls.
- Reveal sharing details and advanced options only while sharing.
- Remove the preview-only mesh API path.

## Validation
- `pnpm check`
- `pnpm test`
- `pnpm exec playwright test tests/e2e/mesh-compute.spec.ts`

Snapshots are attached in a follow-up comment.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-03 14:12:21 -07:00
7981597848 fix(reactions): wrap long popover names (#3834)
**Category:** fix
**User Impact:** Long custom emoji names now stay contained inside
reaction popovers and remain fully readable.

**Problem:** An unbroken custom emoji name could force a reaction
popover beyond its intended maximum width and overflow the message view.

**Solution:** Give the reaction popover a definite 288px width and allow
the complete emoji name to wrap within it without truncation or
ellipsis. Short names retain the same content and interaction behavior.

<details>
<summary>File changes</summary>

**desktop/src/features/messages/ui/MessageReactions.tsx**
Bounds the reaction popover width and allows long names to break across
lines while preserving the full shortcode.

**desktop/tests/e2e/reaction-names.spec.ts**
Covers fixed width, full text preservation, and wrapping for the maximum
supported colon-wrapped reaction name, with deterministic seeded Picsum
visual fixtures and explicit image-load waits.

</details>

## Reproduction Steps

1. Open a message with a custom emoji reaction whose name is 64
characters.
2. Hover or focus the reaction pill to open its details popover.
3. Confirm the popover remains 288px wide and the complete name wraps
within it without ellipsis.
4. Open a short-name reaction and confirm its popover remains readable
and unchanged in behavior.

## Screenshots

| Before | After |
| --- | --- |
| ![Maximum-length name
before](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3834/max-length-before-picsum.png)
| ![Maximum-length name
after](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3834/max-length-after-picsum.png)
|

**Short-name regression check**

![Short reaction
name](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3834/short-name-after-picsum.png)

## Verification

- `pnpm test` in `desktop`: 3,858 passed
- Focused reaction-name E2E with seeded Picsum captures: 2 passed
- Desktop checks and commit hooks passed

Originating Buzz channel: `f2ec9671-d78e-4cde-894c-9f4c458c7f1f`

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
2026-08-03 14:09:46 -07:00
d4a4570b97 fix(desktop): clarify inherited agent parallelism (#4010)
## Summary

- show an unambiguous `App default (10)` inherited state for parallelism
in create and edit forms
- explain that blank inherits the app default and suppress create-form
number steppers that could silently set `1`
- align the E2E mint fallback with production while preserving explicit
input → definition → app-default precedence

## Why

The forms displayed `1` even though an untouched field is omitted and
desktop minting materializes `10`. The create-form spinner could also
turn blank/inherited into an explicit `1` with one click while leaving
the field looking nearly unchanged.

## Testing

- `pnpm test` (desktop: 3,886 passed)
- `pnpm typecheck` (desktop)
- `pnpm check` (desktop)
- pre-push `desktop-check` and `desktop-test`

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-03 13:47:52 -07:00
5c98932c59 feat(desktop): make onboarding model defaults skippable (#3968)
**Category:** improvement
**User Impact:** Users can skip default model configuration during
onboarding and finish it later in Settings → Agents.

**Problem:** Requiring model defaults during onboarding can block users
who are not ready to choose a harness, provider, or model. Skipping also
needs to leave existing configuration untouched rather than persisting
partial selections.

**Solution:** Stage onboarding edits locally and persist them only when
users choose Next or Back. A delayed Skip action advances without any
configuration write, while a footer hint points users to the settings
location for completing setup later.

<details>
<summary>File changes</summary>

**desktop/src/features/onboarding/ui/DefaultConfigStep.tsx**
Adds the skip action and future-settings hint, and makes model
configuration transactional so Skip discards staged changes while Next
and Back preserve the intended save behavior.

**desktop/src/testing/e2eBridge.ts**
Exposes model-config setter call counts so tests can distinguish a true
zero-write skip from a write-and-rollback implementation.

**desktop/tests/e2e/onboarding-agent-defaults.spec.ts**
Covers skipping during loading and after staged edits, verifies zero
persistence calls, and confirms Next and Back still commit changes.

</details>

## Reproduction steps

1. Start fresh onboarding and continue through harness setup to
**Configure your default model settings**.
2. Change the selected harness or model, then choose **Skip for now**.
3. Confirm onboarding advances to **Join or create a community** and the
prior global model configuration remains unchanged.
4. Return through onboarding and confirm **Next** saves the staged
selection; confirm **Back** also preserves staged changes before
returning.
5. Confirm the footer says model defaults can be configured later in
**Settings → Agents**.

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
2026-08-03 13:05:47 -07:00
e1f6da7c42 ci: add guarded desktop release cache prewarm (#4575)
## Summary

Gate 1 only for desktop release caching:

- replaces canary `rust-cache` use with explicit exact-key
`actions/cache/restore` + `save`
- computes keys after `cargo update --workspace`, including platform,
target, Rust toolchain, Cargo manifests/locks, profile/features, and
native-toolchain inputs
- normalizes only the desktop package version so a trusted `main` canary
can warm an otherwise identical release tag
- excludes Tauri bundle directories, so installers and signed artifacts
are never cached
- adds a restore-only `cache-proof-*` tag workflow that fails unless tag
scope sees the exact default-branch cache
- adds contract tests that enforce no release-workflow cache change in
Gate 1

`release.yml` is intentionally unchanged. A cache miss remains the
current cold canary build; the release path cannot be affected by
merging this PR.

## Validation

- `scripts/test-desktop-release-cache-key.sh`
- `scripts/test-desktop-release-cache-workflow.sh`
- `scripts/test-release-ref-contract.sh`
- Ruby YAML parse of all four changed workflows
- `git diff --check`
- pre-push `branch-skew`

## Post-merge proof plan

1. Run each canary cold on trusted `main`, recording cache size/save
time and fresh artifact inventory.
2. Run each canary warm, requiring the exact-key hit and recording
restore/build time.
3. Create a disposable `cache-proof-*` tag at that same trusted `main`
SHA and dispatch **Desktop release cache tag-scope proof** from the tag.
4. Do not begin Gate 2 or modify `release.yml` unless the exact
tag-scope restore succeeds and cache transfer economics are favorable.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-03 13:03:26 -07:00
ce56e34411 fix(mobile): recover stale relay sessions (#4372)
### Summary

Fixes [this
issue](buzz://message?channel=e62570dd-33ad-42c5-b92b-75f2689f9694&id=b726c366abfe62429ee3cdcd34d0c0fb98c33c3ea053480585bed71745412b56):
> I often don’t see my bot responses until after I post. they’re usually
time stamped correctly so I think it’s just a refresh issue?

### What changed?

Buzz Mobile now reconnects relay sessions after the app has remained
backgrounded beyond the existing 5-second grace period, even when the
session still reports a stale `connected` state. This makes resume
recovery independent of whether iOS runs the grace timer before or after
delivering `resumed`.

Reconnection is now based on elapsed background time rather than a
direct socket-health probe.
- If the app was backgrounded for at least the 5-second grace period,
the socket is presumed dead and the session reconnects regardless of
reported status.
- If it was backgrounded for less than that, a reported `connected`
status is still trusted.

In the sub-5-second window the socket is either genuinely alive, which
is the common case for a momentary background, or it is dead and the
client ping detects it within the two-interval worst case described
below. That is now a degraded-latency path, not a silent-forever path.

The mobile relay socket now uses `IOWebSocketChannel.connect` with a
30-second `pingInterval`. An unanswered ping closes the Dart socket
through the existing disconnect and reconnect path.

Detection takes up to two ping intervals, so about 60 seconds worst
case, not 30. One interval of idleness elapses and a ping is sent, then
a second interval elapses with no pong and the socket closes. Any
inbound pong restarts the first stage, so the clock measures idleness
rather than running on a fixed cadence.

### Why?

Buzz iOS can sometimes stop showing new bot or agent responses after a
phone has been locked for 5 to 10 minutes. When the user later posts a
message, the missing responses can appear all at once. iOS may suspend
Buzz before the short delayed cleanup that would normally close its
connection has a chance to run. Before this change, Buzz trusted the
resulting stale healthy status on resume and skipped reconnecting, so
the missing responses stayed hidden until a later post exposed the dead
connection.

A state-machine test with a stubbed connection reproduced this reported
pattern and showed that it matches this failure mode: the failed post
triggered a reconnect that fetched the missing messages. The same test
also checked the other candidate explanation, the bug tracked in
[#3053](https://github.com/block/buzz/pull/3053), where the relay has
closed the app's subscription. That state does not produce the pattern.
Posting succeeds and the user's own message appears, but nothing looks
for the missed messages, so they stay hidden. The test confirmed that
the missed messages were still available to fetch in that state, so the
missing step was a trigger to fetch them. This was not an end-to-end
reproduction on an iOS device or a live relay.

The new resume check covers the normal lock and unlock path. If the app
was backgrounded for less than the 5-second grace period, it still
trusts a connection marked as healthy. A dead connection in that window
is instead detected by the ping check, which can take up to about 60
seconds but prevents the app from remaining silently stuck. The ping
only runs while iOS is running the app, so it does not detect a
connection that died during suspension; the resume check owns the lock
and unlock path.

A pre-existing path also runs the same resume handling when network
connectivity returns while the app is already in the foreground. Because
the app was not backgrounded, this change does not alter that path,
which still trusts a connection marked as healthy and relies on the
slower ping check.

Recovery from a subscription that the relay explicitly closes remains in
[#3053](https://github.com/block/buzz/pull/3053), and the two changes
overlap in one file. Changes to how missed messages are backfilled or
replayed are out of scope.

### How is it tested?

Full mobile suite at base and head. Both runs have the same known
macOS-host-only failure in `ChannelDetailPage keeps follow mode off
while a tall newest message stays visible` at line 1053:

- Base: 1,021 passed, 1 skipped, 1 failed
- Head: 1,025 passed, 1 skipped, 1 failed

Added tests:

-
[`relay_session_test.dart`](https://github.com/block/buzz/tree/main/mobile/test/shared/relay/relay_session_test.dart):
long-background resume reconnect and within-grace control
-
[`relay_socket_liveness_test.dart`](https://github.com/block/buzz/tree/main/mobile/test/shared/relay/relay_socket_liveness_test.dart):
silent-peer disconnect and idle-but-healthy control

Mutation checks confirm that removing elapsed-background resume recovery
fails with one socket instead of two, and removing `pingInterval` leaves
the silent peer connected. Restored production code passes both
mutations' regression tests and the healthy idle control.

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz>
2026-08-03 12:28:50 -07:00
651f637275 chore(release): release Buzz Desktop version 0.5.4 (#4562)
## Buzz Desktop release v0.5.4

- **Frozen main:** `6de85fe31d781122756aecf954bae7d357a56b9a`
- **Reviewed candidate:** `5836cb8f0af478ed3ee3bc6464a20fa4cc91303f`
- **Previous desktop release:** `desktop-v0.5.3`
- **Proposed immutable tag:** `desktop-v0.5.4`

This PR must be **squash merged** only after the Desktop Release
Candidate check passes. The branch must remain based directly on current
`main`; stale base, payload drift, incomplete notes, or an unauthorized
merge produce no tag.

The checked-in changelog accounts for every non-merge commit in the
release range. Publication remains bound to the immutable candidate tag.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
mobile-v0.8.0-rc.1 desktop-v0.5.4
2026-08-03 10:33:19 -07:00
6de85fe31d test(mobile): assert follow boundary semantics (#4559)
## 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>
2026-08-03 09:56:14 -07:00
44fa1e8e3a docs(release): align desktop handoff instructions (#3988)
## 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>
2026-08-03 09:43:10 -07:00
09c86c56e5 fix: report agent usage per provider round, not once per turn (#4545)
## 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>
2026-08-03 12:42:42 -04:00
80315ac1a6 fix(desktop): harden Windows installs against Defender block and orphaned 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>
2026-08-03 09:37:44 -07:00