Commit Graph

1463 Commits

Author SHA1 Message Date
Hardworking Honey
2a116bfb9e fix(agents): canonicalize portable env keys
Co-authored-by: Atish Patel <atish@squareup.com>
Signed-off-by: Atish Patel <atish@squareup.com>
2026-08-06 10:12:11 -05:00
Brainy Bumble
8e4358b314 fix(agents): validate catalog thinking effort before it becomes environment
Catalog sharing (f00abc6c) restores a persona event's `thinking_effort` into a
harness environment variable, but the value crosses a trust boundary that the
code did not check. A catalog persona event is authored by a stranger and its
content is plaintext, so `thinking_effort` is untrusted input on the way into a
child process's environment.

`portable_config_for_import` only rejected the empty string, and it inserted the
effort *after* `portable_env_for_import` had already run `validate_user_env_keys`
— so the value skipped even the generic env-value rules that every UI-entered
value passes. A crafted event could put a NUL byte, an arbitrary multi-word
string, or a value past `MAX_ENV_VALUE_BYTES` into `CLAUDE_CODE_EFFORT_LEVEL` or
`CODEX_CONFIG`. The new test fails on the previous commit at the NUL case.

Effort is a closed enum, so validate it as one: `portable_effort_tier` accepts
only the seven tiers Buzz recognizes (trimmed, lowercased) and drops anything
else. This mirrors `BUZZ_AGENT_THINKING_EFFORT_VALUES` in
`ui/buzzAgentConfig.ts`. Which tiers a given harness *offers* stays a UI concern;
this is the trust boundary, so it is default-deny rather than per-harness.

The same filter now applies on the publish side in
`thinking_effort_from_runtime_env`: that value is broadcast to a public relay in
plaintext, and an unrecognized local string is not something to publish.

Co-authored-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz>
Co-authored-by: Atish Patel <atish@squareup.com>
Signed-off-by: Atish Patel <atish@squareup.com>
2026-08-06 10:12:11 -05:00
Hardworking Honey
0e88804303 Include thinking effort in catalog personas
Co-authored-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz>
Co-authored-by: Atish Patel <atish@squareup.com>
Signed-off-by: Atish Patel <atish@squareup.com>
2026-08-06 10:12:11 -05:00
Brainy Bumble
c525113452 fix(agents): store thinking effort where each harness actually reads it
The effort control this PR added rendered for Claude Code and Goose but wrote
to `BUZZ_AGENT_THINKING_EFFORT` — a variable neither harness reads, and which
nothing translates at spawn (`runtime_metadata_env_vars` handles only model and
provider). Picking "high" on a Claude agent saved successfully and reached
nothing: a silent no-op.

The bug survived review because the effort descriptor's `currentPersistence`
was pinned to that one key for every runtime while `targetApplication` carried
the real one, and the divergence was documented as deliberate legacy pending a
migration that never landed. The unit fixture built `runtime("claude")` with
`thinking_env_var: null`, so no test ever saw a real catalog entry.

`currentPersistence` and `targetApplication` are now identical by construction:
`deriveEffortEnvDescriptor` reads the catalog's declared target and returns
either `{kind:"envVar",key}` (CLAUDE_CODE_EFFORT_LEVEL, GOOSE_THINKING_EFFORT,
BUZZ_AGENT_THINKING_EFFORT) or `{kind:"structuredJsonEnv",envKey,jsonKey}`
(CODEX_CONFIG/model_reasoning_effort), with the JSON target winning when a
harness declares both. `readEffortEnvValue`/`updateEffortEnvValue` dispatch on
the shape, so no call site branches on a runtime id and JSON writes still merge
rather than clobber unrelated CODEX_CONFIG properties. There is no translation
step left that can drop an edit. The now-unreachable `acpConfigOption` /
`unavailable` persistences and the `ownedByModelId` omission reason are removed
rather than left as dead alternatives.

The option set is narrowed by the harness's implied provider instead of always
offering the buzz-agent list: `CLAUDE_CODE_EFFORT_LEVEL=none` resolves to
"unset" in the 2.1.222 binary, so offering "none" on Claude would be another
silent no-op. Provider-locked harnesses (Claude, Codex) offer only their
provider's valid levels; harnesses whose provider the user picks (Goose) keep
the full list and are narrowed on surfaces that know the provider.
`implicitEffortProvider` lives in the core module, not in a component, per
rule 1 of the feature AGENTS.md.

Claude and Goose now render the same above-the-env-vars select Codex has, on
the per-agent and persona surfaces, with the owned key hidden from the generic
editor so each value has exactly one editor. A pre-existing stale
`BUZZ_AGENT_THINKING_EFFORT` on a Claude or Goose agent is not silently
dropped: no control claims it, so it stays a visible, deletable generic row.

Tests now apply the real per-runtime catalog targets and assert Claude/Goose
write their own key and never `BUZZ_AGENT_THINKING_EFFORT`, that Codex persists
as `structuredJsonEnv`, that a harness with no target omits effort as
`unsupportedByHarness`, and that a provider-locked harness offers only levels
that harness accepts. The e2e mock bridge advertises the same catalog fields,
so specs can no longer pass against a mock that claims a supporting harness
supports nothing.

Co-authored-by: Atish Patel <atish@squareup.com>
Signed-off-by: Atish Patel <atish@squareup.com>
2026-08-06 10:12:10 -05:00
Brainy Bumble
00c6a92c19 feat(agents): make thinking effort configurable and portable in exports
Codex thinking effort was unreachable from Buzz, and neither the agent nor the
team JSON export carried effort or any environment configuration. Fix both, and
fix the pre-existing bug that made the live-ACP effort tier dead code.

Root cause of "there is no way to set Codex thinking effort": `reader.rs`
matched session config options by `category == "effort"`, but no real adapter
publishes that category. `codex-acp` 1.1.9 and `claude-agent-acp` 0.58.1 both
emit `category == "thought_level"` (config ids `reasoning_effort` and `effort`
respectively). The `AcpConfigOption` tier therefore never fired for any runtime
and every agent fell through to `RespawnWithEnvVar` — or to `ReadOnly` for
Codex, which had no thinking env var at all. Match `thought_level` and read the
adapter-advertised `config_id` instead of hardcoding `"effort"`, the way
`find_model_config_id` already does for the model field.

Codex effort is settable via environment after all: the `codex-acp` adapter
Buzz spawns ignores CLI args entirely and reads its configuration from the
`CODEX_CONFIG` JSON env var. Probing the real adapter over ACP,
`-c model_reasoning_effort=low` left `reasoning_effort` at `high`, while
`CODEX_CONFIG={"model_reasoning_effort":"low"}` took effect. Declare that
target in the runtime catalog as `thinking_config_json_env_var` +
`thinking_config_json_key` so no per-runtime branch is needed in the UI or the
snapshot code, and `build_codex_config_env` already deep-merges it into the
generated network overlay so the value reaches the child process.

Exports now carry a first-class scalar `thinking_effort` plus an allowlisted
`portable_env`. `CODEX_CONFIG` itself is deliberately not allowlisted: it can
carry `model_providers.*` auth headers, so only the extracted effort scalar is
projected. Import re-applies the allowlist and the normal env validation, so a
hand-edited snapshot cannot inject arbitrary environment, and the first-class
effort field wins when it conflicts with `portable_env`. New fields are
`#[serde(default, skip_serializing_if)]`, so v1 snapshots still decode and
clean records emit no new keys.

Several files were at the desktop file-size ratchet, so the new code is split
into sibling modules rather than growing capped files: the runtime table, the
catalog entry type (Rust and TS), the raw catalog bridge, and three test
fixture/test files.

Co-authored-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz>
Co-authored-by: Atish Patel <atish@squareup.com>
Signed-off-by: Atish Patel <atish@squareup.com>
2026-08-06 10:12:10 -05:00
Cameron Hotchkies
9213090f60 test(desktop): await thread scroll anchor (#3174)
## Why
The focus/split E2E test could capture the thread root before its
programmatic middle-thread scroll had settled, then incorrectly report a
scroll-restoration failure.

## What
- Poll until the requested middle-thread scroll position is applied
- Require the captured anchor to intersect the thread viewport and
differ from the root
- Preserve the existing focus-to-split-to-focus viewport assertions

## Risk Assessment
Low — test-only synchronization change with no production behavior
changes.

## References
- Original failure:
https://github.com/block/buzz/actions/runs/30231271427/job/89870533541
- Buzz thread:
buzz://message?channel=12dd513d-45fd-48ff-80ac-8596d2fcc9d3&id=87ce6024b4bf74bfac2fa75d9f7bbbcc8f8fe2df460afe534152c495929f51ba
- Reproduced confidence: 20 consecutive targeted passes, full spec pass,
`just desktop-ci`, and `just ci`

Generated with Codex

Signed-off-by: npub1x4hk035p3p9q39a3fcrd2fe30lpkrhr5dwe0cqzzjphxyyh8m0gsq4vqap <356f67c681884a0897b14e06d527317fc361dc746bb2fc0042906e6212e7dbd1@buzz.block.builderlab.xyz>
Co-authored-by: npub1x4hk035p3p9q39a3fcrd2fe30lpkrhr5dwe0cqzzjphxyyh8m0gsq4vqap <356f67c681884a0897b14e06d527317fc361dc746bb2fc0042906e6212e7dbd1@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
2026-08-06 15:04:39 +00:00
klopez4212
480c41ebf1 Improve desktop mobile pairing flow (#5024)
## Summary
- add stable three-step guidance to desktop mobile pairing
- move code confirmation inline and show animated completion states
- preserve pairing reset behavior and reduced-motion support

## Test plan
- `pnpm --dir desktop check`
- `pnpm --dir desktop exec tsc --noEmit`
- `pnpm --dir desktop exec playwright test
tests/e2e/mobile-pairing-qr.spec.ts --project=smoke`
- pre-push desktop suite: 4,387 tests passed

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Fizz <50a12680c76f1a52c0b7af8dbb17e02c583227c290fb93b9a3defb456114223f@buzz.block.builderlab.xyz>
2026-08-06 07:45:21 -07:00
Wes
5babb97ca3 feat(desktop): show selected community in rail (#5000)
## Summary
- add a persistent vertical pill beside the active community
- keep the selected state visually distinct from unread dots and mention
badges
- preserve the existing `aria-current` selection semantics

## Screenshot
![Selected community
indicator](https://d24qwcpro867f5.cloudfront.net/repos/block/buzz/prs/5000/selected-community-indicator-v2.png)

## Test plan
- `pnpm exec biome check src/features/sidebar/ui/CommunityRail.tsx
tests/e2e/community-rail.spec.ts`
- `pnpm test` (4,387 passed)
- `pnpm build:e2e && pnpm exec playwright test
tests/e2e/community-rail.spec.ts --project=smoke` (20 passed)
- pre-push hooks: desktop check and 4,387 desktop tests passed on
`c1e80c66d12f73c4eb5c03a19e932439b14caf2d`

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-06 07:39:36 -07:00
Wes
19b41e9c8e fix(desktop): stop rate-limited reconnect backfill from tearing down the authenticated socket (#4990)
## Problem

Users on v0.5.5 report "Can't reach the relay" toggling with brief
"connected" flashes (field reports; also the macOS confirmation in
#4908). #4737 closed the stuck-reconnect gaps; this is the opposite
failure: the client redials fine, but then kills its own healthy socket.

Mechanism (all on `main`):

1. AUTH succeeds → session emits `connected`
(`relayClientSession.ts:583`), then awaits `replayLiveSubscriptions()`.
2. Paged channel backfill issues history REQs
(`relayReconnectReplay.ts`, page limit 500).
3. A `CLOSED rate-limited:` on a **history** REQ arms the rate-limit
gate but still rejects the history promise
(`relayClosedRecovery.ts:38-51`).
4. The rejection escapes `replayLiveSubscriptions()` →
`resetConnection()` tears down the authenticated socket.
5. Reconnect → AUTH OK → replay rate-limited again → loop. Each
iteration re-spends the rate-limit budget, so the loop is
self-sustaining.

## Fix

Contain backfill failures inside the replay. Each subscription's paged
backfill now retries behind the rate-limit gate up to
`PAGE_REPLAY_MAX_ATTEMPTS` (3), then degrades to live-only **for this
connection**. Socket health no longer depends on backfill success.
Nothing is lost: the replay cursor (`lastSeenCreatedAt`) only advances
on delivered events, so the next reconnect replays the same missed
window.

## Red/green proof

- Commit 1 (Pinky): e2e injecting `CLOSED rate-limited:` into the
mid-replay history REQ — **red on main** (expected 1 reconnect dial,
observed 2; connected-flash then teardown).
- Commit 2 (this fix): same test **green unchanged** — one dial, state
stays `connected` through the rate-limit hint plus the next backoff
window.

Why existing coverage missed it: the prior rate-limit e2e pre-armed the
gate *before* replay (replay politely waits), and the CLOSED-injection
test targeted a *live* subscription (which has its own retry path).
Nobody injected back-pressure from the history REQ itself.

## Verification

- `pnpm test`: 4374/4374 pass.
- `playwright test tests/e2e/relay-reconnect.spec.ts`: 14/14 pass,
including the new spec.
- `tsc --noEmit` clean; Biome clean on touched files (pre-existing
warnings on main in `personaCatalogRelay.test.mjs` / `terminal.css`
untouched).

## Not addressed here (follow-ups from the same field reports)

- AUTH terminal latch is too aggressive for relay-internal `error:`
rejections (3 strikes during a relay bad window → stuck until
click/relaunch; #4908).
- Server-side: `relay.drainJitterMs` (#4542) defaults to 0 — enabling it
on the hosted relay removes the deploy thundering herd that triggers
these rate-limit storms.

---------

Signed-off-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz>
Signed-off-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
2026-08-06 07:39:17 -07:00
Wes
96ae141763 fix(desktop): skip native notifications outside app bundles (#5004)
## Summary

- require the macOS process to be running from an actual `.app` bundle
before initializing `UNUserNotificationCenter`
- keep the existing bundle-identifier requirement
- cover packaged, case-insensitive `.app`, raw `target/debug`, and
extensionless paths

## Why

PR #4799 guarded native notification initialization with
`NSBundle.mainBundle.bundleIdentifier != nil`. Tauri embeds a bundle
identifier in raw development executables, so `tauri dev` passed that
guard and `UNUserNotificationCenter.current()` raised an uncaught
`NSInternalInconsistencyException` because LaunchServices had no bundle
proxy.

## Validation

- focused macOS notification tests: 6 passed
- direct raw debug executable no longer raises the notification-center
exception
- pre-commit formatting hook passed
- pre-push package checks passed on pushed commit
`f29a6664d2a863e7b8aa527f6149fd00b183e4de`

The first push attempt hit an unrelated timing-test failure in
`relay_admission::tests::concurrent_429_extends_the_window_for_parked_waiters`;
its focused rerun passed, and the complete pre-push package suite passed
on the next push.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-05 21:53:25 -07:00
Wes
e2796d4a89 fix(desktop): virtualize channel member lists (#4991)
## Summary

- virtualize the unfiltered channel member roster instead of eagerly
mounting every member card
- retain the existing member search/add flow and archived-member
behavior
- cover a 500-member roster, bounded mounted rows, and scrolling to the
final member in E2E

## Cause

The members sidebar rendered every active member card at once. On large
channels this mounted hundreds or thousands of avatars, profile/presence
consumers, menus, and DOM rows, blocking the renderer even though
fetching the roster itself is fast.

## Testing

- `pnpm typecheck`
- `pnpm exec biome check src/features/channels/ui/MembersSidebar.tsx
tests/e2e/channels.spec.ts`
- `pnpm build:e2e`
- `pnpm exec playwright test tests/e2e/channels.spec.ts --grep 'members
sidebar (virtualizes large channel rosters|can invite relay-authorized
agents|can invite and remove managed agents|collapses same-persona
managed agents)'` (4 passed)
- pre-push: `desktop-check`, full `desktop-test` (4,371 passed),
branch-skew

Implemented by Carl on Wes's behalf.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-05 19:42:04 -07:00
Tom Brow
16cc3de6d6 fix(desktop): enforce owner-only access in internal builds (#4053)
## Problem

Managed agents in internal Buzz builds should answer only their owner.
Previously, an agent could keep a broader access setting and respond to
other people, which did not match the access policy for internal builds.

This PR makes owner-only access effective for every managed agent in
internal builds and makes that restriction clear in the Desktop UI. Open
source builds remain configurable.

## Changes

- Enforce owner-only access when any managed agent starts or is deployed
from an internal build.
- Show the agent access control as locked to **Only me** in Desktop,
with an explanation of why it cannot be changed.
- Keep Welcome teammates working under the same rule without triggering
unnecessary restarts.
- Leave open source build behavior unchanged. This changes effective
runtime access without rewriting stored or relay-advertised settings.

The companion [#4064](https://github.com/block/buzz/pull/4064) explains
the restriction in-thread when someone without access mentions an agent.

The enforcement will remain inactive in shipped builds until
[squareup/buzz-releases#74](https://github.com/squareup/buzz-releases/pull/74)
marks internal releases during the build.

## Screenshots

| Before | After |
| --- | --- |
| ![Editable agent access control before the
change](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4053/4053-before.png)
| ![Agent access locked to Only me in an internal
build](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4053/4053-after-v2.png)
|

## Tests

Added coverage for:

- Runtime enforcement for [locally run
agents](e0165f52b5/desktop/src-tauri/src/managed_agents/runtime/tests.rs (L196))
and [deployed
agents](e0165f52b5/desktop/src-tauri/src/commands/agents_tests.rs (L510)).
- The [current-build deployment
path](e0165f52b5/desktop/src-tauri/src/commands/agents_tests.rs (L455)),
[invalid stored
access](e0165f52b5/desktop/src-tauri/src/managed_agents/access_policy.rs (L98)),
and the [local startup
guard](e0165f52b5/desktop/src-tauri/src/managed_agents/env_vars/tests.rs (L149)).
- Consistent enforcement across [both agent
backends](e0165f52b5/desktop/src-tauri/src/managed_agents/access_policy.rs (L112)).
- Welcome teammates created as [locally
run](e0165f52b5/desktop/src/features/onboarding/welcomeGuide.test.mjs (L384))
or
[deployed](e0165f52b5/desktop/src/features/onboarding/welcomeGuide.test.mjs (L393))
agents, including
[access-only](e0165f52b5/desktop/src/features/onboarding/welcomeKickoff.test.mjs (L202))
and
[runtime-related](e0165f52b5/desktop/src/features/onboarding/welcomeKickoff.test.mjs (L225))
restart behavior.

The full Desktop Rust and JavaScript suites, type checks, formatting,
clippy, and file-size checks passed. Playwright E2E was not run.

---

Originated from Buzz channel
[buzz-agent-control](buzz://channel?id=cf5dada7-e26a-4887-ae41-b3bd5f42d3b2).
Supersedes #2537.

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Signed-off-by: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz>
Co-authored-by: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz>
Co-authored-by: Amp <amp@ampcode.com>
2026-08-05 19:58:24 -06:00
Taylor Ho
5677e4ca05 test(desktop): match attachment button label (#4993)
**Category:** fix
**User Impact:** Pull requests can once again pass the Desktop smoke
test suite.
**Problem:** The inbox attachment-edit smoke test still looked for the
composer's former “Attach image” label after the shared action was
renamed to “Attach file,” causing shard 3 and the aggregate Desktop CI
job to fail on every PR.
**Solution:** Update the stale accessible-name selector to match the
current composer control while preserving the test's media-tag coverage.

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

**desktop/tests/e2e/inbox-edit.spec.ts**
Updates the attachment button selector to use the current accessible
label so the existing attachment-edit regression test reaches the
behavior it is meant to verify.

</details>

## Reproduction steps

1. Build the Desktop E2E application with `pnpm -C desktop build:e2e`.
2. Run `cd desktop && pnpm exec playwright test --project=smoke
tests/e2e/inbox-edit.spec.ts -g "editing an immediate attachment reply
preserves its media tags"`.
3. Confirm the test locates the “Attach file” control and passes.

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 18:46:29 -07:00
Tyler
4da7264d90 fix(acp): pace observer telemetry at 1/s with per-channel batch envelopes (#4917)
## Problem

Observer telemetry is the noisiest client of the relay: the old pacer
(167ms spacing + 90/min rolling cap) let a busy session bill up to 6
events/second against the owner's message quota, and the rolling cap
silently *dropped* frames once exceeded.

Ruling from the rate-limiting investigation thread (channel
`826fc99b-1472-40e7-a529-6b9db8943b8c`): pace at 1/s, always emit,
minimal PR.

**Review round 1 (Max, Sami)** found the first cut wrong in three ways —
tick burst (all pending frames per tick), startup burst (`interval`
fires at t=0), and per-channel quota arithmetic. All fixed and
mutation-verified in round 1.

**Review round 2 (Sami, Max)** found two more against the round-1 head:
1. **Drain-rate collapse (Sami, blocker):** front-run-only packing meant
a frame held ONE event whenever channels interleaved — measured 275 B/s
vs 63.5 KB/s, so an ordinary 2-channel session fell minutes behind with
zero drops and no warning. Silent unbounded latency.
2. **Coalescer byte-cap bypass (Max):** chunks pending in
`ObserverChunkCoalescer` were unbounded and outside the 4 MiB cap — 500
distinct-`messageId` 50KB chunks retained ~48MB with `pending_bytes ==
0` and zero drops.

**Review round 3 (Max)** found the drop accounting undercounted merged
chunks: a coalescer entry that merged N same-`messageId` chunks counted
as **1** in `dropped_events` when evicted (50 merged 1KB chunks evicted
→ counter read 1, 49 generated events unaccounted). Fixed: accounting is
now denominated in **source (generated) observer events** end to end —
each pending entry tracks how many chunks it absorbed, eviction charges
that count, and the count survives flush into the publish FIFO.

**Review round 4 (Sami, Max)** found three more against the round-3
head:
1. **Coalescer byte undercount (both, independently):** a pending merged
entry retains its first chunk's text **twice** until flush — once inside
the serialized event skeleton and once in the extracted text accumulator
— but was charged only `serialized_len`, so true retention overshot the
4 MiB cap ~2× (measured 8.3 MB). Fixed: `push_pending` charges
`serialized_len(&event) + text.len()`.
2. **Cap regressions asserted the accumulator against itself,** which is
how the undercount hid. All three cap tests now assert on independently
**walked** retained bytes (`serialized_len` per FIFO entry +
`serialized_len + text.len()` per coalescer entry), with a secondary
`accumulator >= walked` sanity check. Reverting the fix makes them fail
at exactly 8,328,386 / 8,328,272 bytes.
3. **FIFO-arm source accounting was implemented but untested (Sami M13;
Max reproduced at `cc9333b7c` with 102/151):** the round-3 regression
only evicted a merged entry while still in the coalescer. New test
forces a merged entry (50×1KB, `source_events=50`) through flush into
the publish FIFO, then evicts it from there — mutating the FIFO eviction
to `dropped += 1` fails with the reviewers' exact numbers (102 vs 151).

**Review round 5 (Sami 9/9/9, Max 9/9/9)** — production judged
merge-safe by both; remaining items are tests only, all landed at
`63d821620`:
1. **The walker instrument was itself unverified (Sami M17–M20; Max
independently confirmed the `return 0` mutant survives):** every cap
test asks `walked_retained_bytes()` only for `<= CAP`, so a blinded
walker passes everything — and paired with a reverted `push_pending` fix
the two mutations cancel, hiding exactly the 8.3 MB overshoot it exists
to detect. New two-sided pin: the walker must SEE the first chunk's text
twice, and must agree with the accumulator EXACTLY while both stores are
non-empty. Kills M17, M18, M19, M20.
2. **Two pre-existing snapshot-clone siblings (Sami D5b/D5c;
byte-identical at merge-base `7334ad1e1` — not this PR's regression, but
the PR made the class visible):** aliasing the inner turns map leaks a
post-save turn into the snapshot; aliasing the inner tombstones map
leaks a post-save terminal that blocks a legitimate post-restore
resurrection. Two isolation tests with in-test controls — all three
inner-map clones in `saveActiveAgentTurnsForCommunity` are now pinned.

## Change

**Harness (`crates/buzz-acp`)**
- **Global pacer: AT MOST ONE relay frame per second**, regardless of
channel count or backlog size. `interval_at(now + 1s)` restores the
no-startup-burst property; `MissedTickBehavior::Skip` is now pinned by a
paused-time test (a stalled tick arm fires one catch-up frame, not one
per missed deadline). At 1 frame/s telemetry spends ≤60/min of the
shared 120/min quota; `OBSERVER_PUBLISH_TICK` documents the tradeoff as
the knob.
- **`ObserverPublishQueue` with gather-packing:** events wait as
byte-accounted events (FIFO). `next_frame()` packs the front event's
channel **gathered queue-wide in FIFO order** — frames never mix
channels, and each channel's events keep their FIFO order, but
cross-channel frame order MAY differ from arrival order. That is what
keeps the drain rate in **bytes per slot** (one ~64KB frame/s) instead
of front-run-length events per slot. **Null-channel events
(`agent_panic`-class) are packing barriers** nothing gathers across, so
causally-global events keep exact order against every channel.
- **One byte cap over BOTH stores:** the event FIFO and the coalescer's
pending chunk buffer count against the 4 MiB budget together; eviction
is oldest-first across both (queue front, then coalescer front —
structural age order) with accounting (warn + counter). A
high-cardinality chunk flood is bounded exactly like a plain event
flood. Coalescer entries are charged their **true** retention
(`serialized_len + text.len()` — the first chunk's text lives in both
the serialized skeleton and the extracted accumulator until flush).
- **Shutdown is not a burst bypass:** paced one-frame-per-tick drain
until empty.

**Desktop**
- `unwrapObserverBatch` expands envelopes on the live relay path and
archive-ingest seam (round 1, unchanged).
- **`activeAgentTurnsStore` watermark re-keyed per (agent, channel)**
with a dedicated null-channel bucket: the per-agent `(timestamp, seq)`
gate would silently skip a delayed channel's frames as stale under
gather-packing's intentional cross-channel reorder. Safe because every
turn-mutating path is channel-scoped by the event's own `channelId`
(endTurn's null-turnId fallback matches `turn.channelId`; resurrectTurn
keys on `event.channelId`), so per-channel serialization preserves each
guard the per-agent gate provided. The tombstone-cap justification is
rewritten for the new keying (worst case for an evicted tombstone is a
ghost badge the prune reaps — bounded cosmetic staleness, not
corruption). Community-switch save/restore deep-clones the nested map.
Other per-agent maps stay agent-keyed: the clock offset is a running
minimum (order-insensitive); turns/tombstones mutate only through
channel-scoped paths.

## Version skew — old desktop + new harness

Gather-packing *intentionally* emits cross-channel-reordered frames. An
**old desktop** (per-agent watermark) against a **new harness** will
silently skip a delayed channel's turn-state events as stale — working
badges on that channel can go stale/missing until its next fresh event.
Transcript and archive are unaffected (the transcript store sorts +
rebuilds on out-of-order arrival; the archive is per-channel by
construction). Ship desktop and harness together; skew degrades badges
only, not data at rest.

## Throughput ceiling — "lossless" is qualified

Sustained lossless rate is what fits in one ~64KB frame per second, now
genuinely in bytes under interleaving:

| event payload | events per frame | sustained ceiling |
|---|---|---|
| 100 B | 250 | 250 ev/s |
| 500 B | 99 | 99 ev/s |
| 2 KB | 30 | 30 ev/s |
| 10 KB | 6 | 6 ev/s |

With C channels producing concurrently, publish slots round-robin
between them: per-channel drain is ~64KB/C per second and the 4 MiB
burst budget (~64s single-channel) shortens accordingly. Beyond budget,
oldest-first drops **with accounting** — visible, designed loss.

**Accounting semantics:** `dropped_events` counts SOURCE (generated)
observer events, not retained entries — evicting a coalesced entry that
merged N chunks charges N. On the published side, a merged entry ships
all N sources' text in ONE event, so the reconciliation invariant is
`ingested == dropped_events + Σ source_events over published events`
(for unmerged events, source_events = 1).

## Verification

At `63d821620d3513505e8766ac691a8002f9d4a96f` (this head; `git rev-parse
HEAD` matched in the same shell as every run), rustc 1.95.0:
- `cargo test -p buzz-acp`: **689 lib + 9 integration, 0 failed** —
regressions: interleaved 2-channel drain packs into ≤4 frames not 200
slots; null-channel barrier; queue-wide gather with within-channel FIFO;
distinct-key 50KB chunk flood bounded by the cap with event-level
accounting (published + dropped == ingested, survivors newest);
paused-time `MissedTickBehavior::Skip` pin (verified to fail under
`Burst`: 3 frames vs 1); merged-key eviction accounts every absorbed
source chunk in BOTH arms — coalescer-side (Max's round-3 probe) and
post-flush FIFO-side (Sami M13 / Max's round-4 probe: fails 102 vs 151
under `+= 1`). All three cap tests assert on independently walked
retained bytes, not the accumulator (verified to fail without the
`+text.len()` fix: 8,328,386 / 8,328,272 vs 4 MiB); the walker itself is
pinned two-sided against the accumulator (all four blinding mutants
M17–M20 verified to fail it, including the walker+fix cancellation
pair).
- `cargo clippy -p buzz-acp --all-targets -- -D warnings` clean, `cargo
fmt --check` clean
- Desktop: `tsc --noEmit` clean; node tests **4366 passed, 0 failed** —
snapshot-clone family fully pinned: watermark aliasing (round 4), turns
aliasing and tombstone aliasing (round 5, pre-existing gaps; each mutant
verified to fail exactly its target test with an in-test control). Prior
rounds: cross-channel reorder processed, cross-channel-delayed
null-turnId `turn_error` evicts only its own channel's turn, null-bucket
replay idempotency, same-channel stale/duplicate still skipped,
watermark survives community-switch save/restore
- All pre-push hooks green at the pushed commit (branch-skew,
desktop-check, desktop-test, rust-tests, desktop-tauri-checks)

Part of the rate-limiting fix stack; independent of
`eva/rate-limit-fixes` by design (separate minimal PR per Tyler's
ruling).

---------

Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
2026-08-05 21:32:21 -04:00
Jordan Mecom
a7ea86cdcf fix(desktop): enable the content security policy (#4614)
This change enables a Tauri content security policy that limits
executable content to the packaged application and does not allow inline
scripts.

Relay, media, asset, and Tauri IPC schemes remain available for desktop
compatibility. The policy contains the impact of a future renderer
injection; it does not itself remove an injection bug.

## Testing

- `git diff --check origin/main...codex/security-desktop-csp`
- Rebased onto `origin/main` at `5c98932`
- Full CI pending

Originating Buzz thread:
`buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1`

---------

Signed-off-by: Jordan Mecom <jm@squareup.com>
Signed-off-by: Eli Foster <efoster@squareup.com>
Co-authored-by: Eli Foster <efoster@squareup.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 16:36:36 -07:00
Tom Brow
eb6a37569d fix(desktop): enable message editing in Inbox (#2198)
### What changed?

Inbox detail now gives the current user's messages the same
ownership-gated Edit action as channel view. Editing reuses the existing
composer and mutation flow, preserves attachment metadata, and refreshes
structural overlays so the edited content appears immediately.

Foreign authors' messages remain non-editable, including grouped Inbox
conversations whose selected event is not the representative item.

| Own Inbox message exposes **Edit message**. | Saving the edit updates
the Inbox detail immediately. |
| --- | --- |
| ![Before: Edit message action in Inbox
detail](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/2198/inbox-edit-before.png)
| ![After: edited Inbox message
content](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/2198/inbox-edit-after.png)
|

### Why?

Inbox rows did not pass an edit handler into the shared message action
bar, so a user's own messages could be edited from channel view but not
from Inbox detail.

### How is it tested?

Desktop checks, unit tests, and the full local CI gate passed. The
focused Inbox Playwright regression passed 3 consecutive runs and covers
current-user edit/save, foreign and archived-channel denial, and
attachment preservation when a just-sent reply is edited before its
relay echo arrives.

Added tests:

-
[`inbox-edit.spec.ts`](https://github.com/block/buzz/blob/inbox-message-edit-action/desktop/tests/e2e/inbox-edit.spec.ts)
-
[`inboxViewHelpers.test.mjs`](https://github.com/block/buzz/blob/inbox-message-edit-action/desktop/src/features/home/lib/inboxViewHelpers.test.mjs)

*🤖 This PR was authored [with an
agent](buzz://message?channel=7f2d7e02-f4d5-4fb0-a426-0ca60ed3a1c3&id=c09ee04d18399b90296c3f932d22ab0377fa05f7e690ec7b08c36483ee633fbb).*

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Signed-off-by: npub1ft62tztwwm2x9xamk25smmuaj4sfckdkldksruf2x2jwqalffkrq0g7arr <4af4a5896e76d4629bbbb2a90def9d95609c59b6fb6d01f12a32a4e077e94d86@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1ft62tztwwm2x9xamk25smmuaj4sfckdkldksruf2x2jwqalffkrq0g7arr <4af4a5896e76d4629bbbb2a90def9d95609c59b6fb6d01f12a32a4e077e94d86@sprout-oss.stage.blox.sqprod.co>
2026-08-05 17:00:23 -06:00
Taylor Ho
005fe54d02 fix(desktop): outline the selected community (#4969)
**Category:** improvement
**User Impact:** Selected communities now use a clear offset outline
without tinting or covering their icon.

**Problem:** The selected community state replaced the icon surface with
an accent fill, obscuring image icons and changing the tile's content
treatment. Hover also changed the fill, text color, shape, and opacity,
making navigation states visually jumpy.

**Solution:** Preserve each community tile's neutral surface and content
while using a primary CSS outline for selection and a lighter outline
for hover. The transparent outline offset leaves the space around image
edges unpainted, and adjusted spacing prevents neighboring outlines from
colliding.

<img width="200" height="152" alt="Screen Recording 2026-08-05 at 3 23
32 PM"
src="https://github.com/user-attachments/assets/5c25b1c0-4be8-41c4-8f1d-ad0010310c92"
/>


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

**desktop/src/features/sidebar/ui/CommunityRail.tsx**
Replaces selected and hover fills with offset outlines, keeps icon
presentation stable across states, and adjusts rail and tooltip spacing
for the new outline geometry.

**desktop/tests/e2e/community-rail.spec.ts**
Covers the shared active/inactive surface, radius, text color, opacity,
and outline behavior, including hover invariants.

</details>

## Reproduction steps

1. Run the desktop app with two or more communities.
2. Give the active community an image icon.
3. Confirm the active icon keeps its original image and receives a 2px
primary outline with a transparent 2px gap.
4. Hover another community and confirm only a lighter outline appears;
its fill, text color, opacity, and corner radius remain unchanged.
5. Switch communities and confirm the outline follows the active
community.

## Screenshots

**Full desktop context**

![Selected community outline in the desktop
app](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4969/selected-community-full.png)

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 22:44:07 +00:00
Taylor Ho
24c7995740 fix(desktop): clamp thread panel to channel surface (#4965)
**Category:** fix
**User Impact:** Expanded thread panels now stay fully visible within
the desktop channel area instead of being cut off.

**Problem:** The resize handler clamped the thread panel against the
full window width, even though the panel renders inside a narrower
channel surface. On a 1720px window, this allowed a 1160px requested
width where only 1111px could render, leaving persisted and visible
geometry out of sync.

**Solution:** Clamp resizing against the measured channel-surface width
so the stored width matches what the layout can render while preserving
the minimum 300px main pane.

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

**desktop/src/features/channels/ui/ChannelScreen.tsx**
Passes the measured channel-surface width into the thread-panel sizing
hook.

**desktop/src/shared/hooks/useThreadPanelWidth.ts**
Clamps drag-resize updates against the available channel width instead
of the full viewport.

**desktop/tests/e2e/threadpane-ultrawide.spec.ts**
Adds a 1720px regression proving the requested and rendered panel widths
match, while retaining the ultrawide expansion case.

</details>

### Reproduction steps

1. Open a channel thread in the desktop app at a 1720×900 window size.
2. Drag the thread panel's left resize handle toward the left edge to
expand it as far as possible.
3. Confirm the panel remains fully bounded inside the channel surface
and the main channel pane remains at least 300px wide.
4. Reload the channel and confirm the persisted expanded width renders
without clipping.

### Testing

- `pnpm --dir desktop build:e2e`
- `pnpm --dir desktop exec playwright test
tests/e2e/threadpane-ultrawide.spec.ts` — 2 passed
- Push hooks: `desktop-check` and `desktop-test` passed
- `git diff --check origin/main..HEAD`

### Screenshot

![Expanded thread panel remains bounded at
1720×900](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4965/threadpane-expanded-after-fix.png)

### Related issue

None found.

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 15:40:40 -07:00
Taylor Ho
cda33978b1 style(messages): increase username contrast (#4948)
**Category:** improvement
**User Impact:** Message usernames are now bolder, making it easier to
distinguish who said what at a glance.

**Problem:** Usernames and surrounding message metadata had too little
visual separation, which made message headers slower to scan.
**Solution:** Increase the shared message-author label from semibold to
bold while preserving its existing size, spacing, and interaction
behavior.

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

**desktop/src/features/messages/ui/MessageHeader.tsx**
Raises the shared message-author font weight so standard and system
message usernames gain consistent visual contrast.

</details>

## Reproduction steps

1. Open a channel containing messages from multiple people or agents.
2. Compare each message username with its timestamp and message body.
3. Confirm the username renders in bold while the surrounding typography
and layout remain unchanged.

## Screenshots

| Before | After |
| --- | --- |
| ![Message usernames
before](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4948/before-message-usernames.png)
| ![Message usernames
after](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4948/after-message-usernames.png)
|

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 15:39:07 -07:00
iroiro147
d42d60d64e fix(desktop): rename generic attachment action from 'Attach image' to 'Attach file' (#2381) (#4304)
Fixes #2381.

## What was broken

The message composer's paperclip accepts generic attachments — images,
videos, PDFs, archives, and any other supported file — but its tooltip
and accessible name still read **"Attach image"**. Sighted users might
reasonably believe the control is image-only, and screen-reader users
get an incomplete description of what the button does.

## The fix

Rename the accessible name and tooltip text on the generic composer
paperclip in `MessageComposerToolbar.tsx`:

- `aria-label` — `"Attach image"` → `"Attach file"`
- `<TooltipContent>` — `"Attach image"` → `"Attach file"`

Plus update the 12 affected Desktop e2e selectors across five spec files
to reference the new accessible name:

- `desktop/tests/e2e/file-attachment.spec.ts` (2 selectors)
- `desktop/tests/e2e/spoiler.spec.ts` (2)
- `desktop/tests/e2e/composer-image-draw.spec.ts` (2)
- `desktop/tests/e2e/image-attachment-gallery.spec.ts` (4)
- `desktop/tests/e2e/video-attachment.spec.ts` (2)

## Scope (per the issue)

The feedback screenshot dialog
(`desktop/src/features/settings/ui/SendFeedbackDialog.tsx`) is
**unchanged** — that dialog itself is image-only, so its "Attach image"
wording is accurate. This PR only touches the generic composer control.

## Test plan

- All **105** unit tests in
`desktop/src/features/messages/ui/*.test.mjs` pass locally.
- Verified no remaining `"Attach image"` string outside the
intentionally preserved feedback dialog:
  ```sh
  grep -rn '"Attach image"' desktop/
  # → only hits in SendFeedbackDialog.tsx
  ```
- The six e2e specs are only exercised in CI; the selector updates are
mechanical and verified by grep to reference the new a11y name.

## Blast radius

- **Files touched**: `MessageComposerToolbar.tsx` (two strings); five
e2e spec files (12 selector updates).
- **User-facing behaviour**: one tooltip + one screen-reader name
change; no functional or visual changes otherwise.
- **No API or state change.**

## Out of scope

- The feedback dialog's "Attach image" wording — kept per the issue's
own "Scope" guidance.
- Any i18n plumbing — Buzz Desktop doesn't currently localize these
strings.

Signed-off-by: Sarthak Singh <sarthak.singh@juspay.in>
Signed-off-by: Ravneet Arora <rarora@squareup.com>
Co-authored-by: Ravneet Arora <rarora@squareup.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 15:16:32 -07:00
Taylor Ho
2ea9385015 fix(reactions): support max-length custom emoji (#3833)
**Category:** fix
**User Impact:** Custom emoji with valid 64-character names can now be
used as reactions without errors.

**Problem:** Buzz accepted 64-character custom emoji names during
registration, but rejected them as reactions after the required
surrounding colons made the payload 66 characters. Validation also
differed between desktop, SDK, relay, and storage boundaries.

<img width="554" height="47" alt="image"
src="https://github.com/user-attachments/assets/4013452f-210e-4dd3-9003-f45ff3b28dc8"
/>

**Solution:** Keep the product limit at 64 ASCII characters for custom
emoji names, enforce it consistently when emoji sets are registered, and
allow only valid matching custom reaction payloads up to 66 characters.
Widen the reaction projection to preserve the wrapped payload while
retaining the existing 64-character limit for ordinary reactions.

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

**crates/buzz-sdk/src/builders.rs**
Defines the shared custom emoji boundaries and covers accepted
64-character and rejected 65-character shortcodes.

**crates/buzz-relay/src/handlers/ingest.rs**
Validates emoji-set shortcodes and permits 66-character reactions only
when they are valid colon-wrapped custom emoji with a matching tag.

**crates/buzz-db/src/event.rs**
Adds storage regression coverage for maximum-length custom emoji
reactions.

**crates/buzz-db/src/migration.rs**
Verifies the reaction column migration is applied correctly.

**desktop/src/shared/api/customEmoji.ts**
Enforces the existing 64-character shortcode maximum during desktop
normalization and registration/import.

**desktop/src/shared/api/customEmoji.test.mjs**
Covers the desktop shortcode boundary.

**migrations/0027_long_reaction_payloads.sql**
Widens stored reaction payloads to 66 characters for the two required
surrounding colons.

**schema/schema.sql**
Keeps the desired schema aligned with the migration.

</details>

## Reproduction Steps

1. Register or import a custom emoji whose ASCII shortcode is exactly 64
characters.
2. Select that emoji as a reaction to a message.
3. Confirm the reaction publishes, persists, and renders without an
error.
4. Attempt to register a 65-character shortcode and confirm it is
rejected.
5. Publish an ordinary or malformed reaction over 64 characters and
confirm the relay rejects it.

## Verification

- `cargo test -p buzz-sdk`: 243 passed
- `cargo test -p buzz-db`: 94 passed, 152 Postgres-required tests
ignored
- `pnpm test` in `desktop`: 3,859 passed
- `cargo test -p buzz-relay`: 795 passed, 9 existing
Postgres-unavailable failures, 35 ignored; new reaction boundary tests
pass directly
- `cargo fmt --all -- --check`
- `git diff --check`

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-05 21:02:57 +00:00
Taylor Ho
719f9730d4 feat(desktop): allow leaving your final community (#3621)
**Category:** improvement
**User Impact:** People can leave their final Buzz community and return
to **Join or create a community** without losing their signed-in
identity.

**Problem:** Buzz Desktop blocked people from leaving when only one
community remained. Its existing remove action also changed local
configuration without ending relay membership.

**Solution:** Allow the final community to be left. Buzz now asks the
relay to end membership, removes the community locally only after
acceptance, and returns the person to the community selector while
keeping their identity signed in. If other communities remain, Buzz
switches to one of them. Relay rejection or timeout keeps the community
in place and shows an actionable retry error.

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

**desktop/src/features/communities/leaveCommunity.ts**
Adds signed kind 28936 publishing for active and inactive community
relays with actionable timeout handling.

**desktop/src/features/communities/leaveCommunity.test.mjs**
Covers event shape, relay selection, acceptance gating, rejection,
timeout messaging, and cleanup.

**desktop/src/features/communities/useCommunities.tsx**
Allows final-community removal and clears community-specific storage
without touching identity.

**desktop/src/features/communities/resolveCommunityRemoval.test.mjs**
Covers final, active, and inactive community removal state transitions.

**desktop/src/app/useCommunityNavigationTransitions.ts**
Gates local removal on relay acceptance and routes to a fallback
community or setup selector.

**desktop/src/app/AppShell.tsx**
Passes the asynchronous leave operation through shell entry points.

**desktop/src/features/communities/ui/EditCommunityDialog.tsx**
Replaces the local-only remove action with a pending-aware Leave
Community action that retains actionable errors.

**desktop/src/features/communities/ui/CommunitySwitcher.tsx**
Enables leaving the final community and carries the asynchronous
callback.

**desktop/src/features/sidebar/ui/AppSidebar.tsx**
Carries the asynchronous leave callback through sidebar props.

**desktop/src/features/sidebar/ui/CommunityRail.tsx**
Enables leaving the final community from rail settings.

**desktop/src/features/sidebar/ui/SidebarProfileCard.tsx**
Carries the asynchronous leave callback through profile community
settings.

**desktop/src/testing/e2eBridge.ts**
Teaches the mock relay to accept NIP-43 leave events.

**desktop/tests/e2e/community-rail.spec.ts**
Updates leave interactions and verifies final-community setup
navigation, storage cleanup, and identity preservation.

</details>

### Reproduction steps

1. Run Buzz Desktop with a signed-in identity and one joined community.
2. Open Community settings and choose **Leave Community**.
3. Confirm the app shows **Join or create a community** and the existing
identity remains signed in.
4. Repeat with two communities and confirm leaving the active one
switches cleanly to the remaining community.
5. Reject or withhold the relay `OK` response and confirm the community
remains configured with an actionable error in the dialog.

### Test plan

- `pnpm check`
- `pnpm build`
- `pnpm test` (3,913 passing)
- `pnpm build:e2e && pnpm exec playwright test
tests/e2e/community-rail.spec.ts --grep "final community"`


<img width="557" height="316" alt="image"
src="https://github.com/user-attachments/assets/b628182f-cba5-451d-ae4b-bee8d8dd19aa"
/>

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: npub14ndfusear8wdpe4kss8h7juc7wjk78atnqzf63zvppcpneknv4sq6x9370 <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: npub14ndfusear8wdpe4kss8h7juc7wjk78atnqzf63zvppcpneknv4sq6x9370 <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-05 14:02:43 -07:00
Kalvin C
7334ad1e16 fix(desktop): route macos notification clicks (#4799)
## Summary
- deliver macOS notifications through `UNUserNotificationCenter`
- route notification clicks to the referenced channel or thread through
the existing frontend activation path
- preserve click targets across cold startup and frontend remounts with
a small process-wide activation queue
- keep Linux notification activation behavior unchanged

## Architecture
A single `UNUserNotificationCenterDelegate` is installed during Tauri
setup. Each notification stores its navigation target in `userInfo`. On
click, Rust queues the target before emitting a wake-up event; the
frontend atomically drains the queue and dispatches the existing
notification action. The queue is the source of truth, which prevents
cold-start loss and duplicate delivery.

## Validation
Verified at `a81241611d617becf7640bee6fe56b5cdb4d0fab`:
- Biome format/check and lint
- TypeScript typecheck
- repository and desktop-Tauri `cargo fmt --check`
- repository and desktop-Tauri Clippy with `-D warnings`
- full pre-push desktop tests and Tauri workspace checks/tests
- desktop production build

Manual macOS validation passed: after explicitly ad-hoc signing the
local bundle with `xyz.block.buzz.app`, the operator confirmed real
Notification Center delivery and click navigation.

<details>
<summary>Local macOS test procedure</summary>

Tauri's generated ad-hoc signing identifier is not accepted by
`UNUserNotificationCenter`. Re-sign the local bundle with its bundle
identifier and keep other Buzz copies closed:

```bash
just desktop-release-build
APP="$HOME/.cache/cargo-target/aarch64-apple-darwin/release/bundle/macos/Buzz.app"
codesign --force --deep --sign - \
  --identifier xyz.block.buzz.app \
  --entitlements desktop/src-tauri/Entitlements.plist \
  "$APP"
codesign --verify --deep --strict --verbose=2 "$APP"
pkill -x buzz-desktop || true
open -n "$APP"
```

</details>

Buzz channel: `55e2bfca-1b38-48fb-9dc2-584d400501f3`

---------

Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-05 13:29:41 -07:00
Taylor Ho
43cced308d feat(desktop): sync themes per community (#3653)
**Category:** new-feature
**User Impact:** Users can keep a distinct Appearance scheme for each
community and restore it on another desktop signed in with the same
identity.

**Problem:** A single global theme makes it harder to distinguish among
communities, and local-only preferences do not follow a user to another
device. **Solution:** Save each community's stable theme, accent, and
system-following selection as private encrypted relay state, backed by a
responsive local cache and guarded against switch races, invalid future
records, and relay failures.

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

**desktop/src/app/App.tsx**
Mounts the community-scoped theme controller inside the active community
lifecycle.

**desktop/src/features/settings/lib/appearanceScopeCopy.test.mjs**
Covers active-community and fallback labels used to explain Appearance
scope.

**desktop/src/features/settings/lib/appearanceScopeCopy.ts**
Builds a safe, trimmed label for the currently active community.

**desktop/src/features/settings/ui/SettingsPanels.tsx**
Clarifies which Appearance controls are per-community and which apply
globally when multiple communities exist.

**desktop/src/shared/constants/kinds.ts**
Defines the NIP-78 application-data event kind used for theme
preferences.

**desktop/src/shared/theme/CommunityThemeController.tsx**
Coordinates cached appearance, encrypted relay retrieval, live updates,
reconnect behavior, and safe community switching.

**desktop/src/shared/theme/ThemeProvider.tsx**
Exposes a single appearance application path so synchronized preferences
use the existing renderer and persistence behavior.

**desktop/src/shared/theme/communityThemePreference.test.mjs**
Covers contract validation, user/relay isolation, malformed records,
cache failures, and switch-race decisions.

**desktop/src/shared/theme/communityThemePreference.ts**
Defines the versioned stable preference contract, safe defaults, local
cache keys, and persistence guards.

**desktop/src/shared/theme/communityThemeSync.test.mjs**
Covers relay absence, unreadable records, unavailability, seeding
safety, and teardown of pending writes.

**desktop/src/shared/theme/communityThemeSync.ts**
Encrypts theme preferences to the user, publishes and retrieves NIP-78
state, and handles ordering and lifecycle safety.

</details>

### Reproduction steps

1. Join at least two communities and open **Settings → Appearance**.
2. Choose a different theme, accent, or system-following mode in each
community.
3. Switch between the communities and verify each one restores its own
scheme without overwriting the other.
4. Sign in on another desktop with the same Nostr identity, join the
same community, and verify its saved scheme is restored from that
community's relay.
5. Disconnect the relay, change Appearance, and verify the UI remains
responsive and the local fallback is retained.

### Screenshots / demos
<img width="1733" height="948" alt="image"
src="https://github.com/user-attachments/assets/5afeabaa-0def-482c-9b87-8a880ee0a467"
/>


<img width="700" height="412" alt="Screen Recording 2026-07-29 at 4 39
52 PM"
src="https://github.com/user-attachments/assets/d58da329-aec5-4324-a4b2-cbcb2702a81a"
/>

---------

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-05 13:18:48 -07:00
Will Pfleger
6c40ce394f feat(desktop): cap OpenClaw agent parallelism at 5 (#4019)
OpenClaw connects to a single shared Gateway daemon. Spawning the
default 10 ACP workers per agent is both resource-expensive and
architecturally wrong — each worker opens a separate gateway connection.
Tyler's ruling: cap at 5, lower if needed.

## Contract

Store the requested value (1–32) verbatim at every persistence and wire
boundary. Apply `effective = min(requested, harness_cap)` only at the
four enforcement points:

| Boundary | Implementation |
|---|---|
| Local spawn | `BUZZ_ACP_AGENTS` env var in child `Command` |
| Remote deploy | `launch.policy_env["BUZZ_ACP_AGENTS"]` + legacy
`parallelism` field |
| Restart badge | `SpawnConfigSnapshot.parallelism` stores effective
value; the diff surface displays what actually runs |
| UI copy | Amber hint when requested > cap; no `max` attribute, no
save-path clamp |

`BUZZ_ACP_AGENTS` is added to `RESERVED_ENV_KEYS` — the Desktop resolves
the effective value into `policy_env`; a user-supplied override in `env`
would bypass the cap and is silently stripped.

## Changes

**`managed_agents/parallelism.rs`** (new) — policy core:
- `OPENCLAW_MAX_PARALLELISM = 5`
- `harness_max_parallelism(command)` — keyed on
`normalize_command_identity` so path prefixes, `.exe` suffixes, and
other cosmetic differences are ignored
- `effective_parallelism(command, value)` — identity for uncapped
harnesses
- `acp_agents_value(command, parallelism)` — `env("BUZZ_ACP_AGENTS", …)`
helper

**`runtime.rs`** — spawn clamp: `BUZZ_ACP_AGENTS =
acp_agents_value(effective_command, record.parallelism)`

**`agents_deploy.rs`** — deploy egress clamp: `build_deploy_payload`
resolves `effective_parallelism` once from `descriptor.command`; both
`launch.policy_env["BUZZ_ACP_AGENTS"]` and the legacy top-level
`parallelism` field use that value — the two are always consistent
regardless of stale `record.agent_command` pins

**`spawn_snapshot.rs`** — `from_inputs` stores
`effective_parallelism(&descriptor.command, record.parallelism)` in the
`parallelism` field. Over-cap edits that don't change the pool (e.g. 10
→ 8, both clamp to 5 on OpenClaw) produce equal snapshots; cap crossings
(8 → 3) produce different snapshots.

**`AcpRuntimeCatalogEntry.max_parallelism: Option<u32>`** — derived from
the static definition command, not the probed `entry.command` (which may
be `null` for unavailable entries), so unavailable OpenClaw entries
still carry the cap. Propagated through all four catalog constructors
(builtin discovery, preset catalog construction, custom discovery,
custom-save response), IPC types
(`RawAcpRuntimeCatalogEntry.max_parallelism`), and the frontend catalog
type.

**UI** — `EditAgentAdvancedFields` and `PersonaAdvancedFields` show an
amber hint when `selectedRuntime.maxParallelism` is set and the current
value exceeds it. Cap and label come from the catalog entry — no
hardcoded 5 in TS. No `max` attribute on inputs; the input stays
`type="text"` with 1–32 copy.

**Docs** — `docs/remote-agents.md`: `BUZZ_ACP_AGENTS` moved from the
deliberately-non-reserved section to reserved; new contract documented.
`desktop/src/features/agents/AGENTS.md`: command-keyed execution policy
documented as the sanctioned second metadata source feeding the catalog
projection.

## Tests

**Rust** (`parallelism.rs`):
- `policy_table` — `harness_max_parallelism` and `effective_parallelism`
across all openclaw variants and uncapped harnesses
- `acp_agents_value_openclaw_above_cap_is_capped` — spawn-env seam
- `override_direction_*` — both override directions (openclaw runtime +
goose override; goose runtime + openclaw override)
- `summary_persona_inherited_*` — live persona wins over stale
`agent_command`
- `snapshot_export_carries_requested_definition_parallelism` — requested
value travels wire/sync unchanged

**Rust** (`spawn_snapshot/tests.rs`):
- `openclaw_above_cap_parallelism_snapshots_equal` — stored 10 vs 8,
both clamp to 5 → snapshots equal
- `openclaw_cap_crossing_parallelism_snapshots_differ` — 8 (clamps to 5)
vs 3 → snapshots differ

**Rust** (`discovery/presets.rs`):
- `openclaw_preset_unavailable_carries_max_parallelism` /
`openclaw_preset_available_carries_max_parallelism` — catalog metadata
present with `command: null` and with a resolved path

**Rust** (`agents_deploy.rs`):
- `launch_block_openclaw_over_cap_policy_env_is_capped` — direct
`launch.policy_env` seam
-
`deploy_payload_json_stale_goose_record_live_openclaw_descriptor_both_capped`
— stale `record.agent_command=goose`, live descriptor=openclaw: both
fields cap to 5
-
`deploy_payload_json_stale_openclaw_record_live_goose_descriptor_both_uncapped`
— stale `record.agent_command=openclaw`, live descriptor=goose: both
fields pass through requested
- `deploy_payload_json_explicit_openclaw_override_both_capped` —
explicit `agent_command_override=openclaw`: both fields cap to 5

**Rust** (`persona_events/stale_pin_tests.rs`):
- `apply_persona_snapshot_goose_to_custom_harness_drops_stale_goose_pin`
— custom-direction stale-pin drop (builtin pin → loaded custom harness
via `update_loaded_harness_registry`)

**TypeScript** (`agentParallelism.test.mjs`):
- `parallelismCapHint` — at/below cap (null), above cap (hint includes
label and cap value), singular form for cap=1, uncapped harness (null)

**TypeScript** (`tauri.test.mjs`):
- `fromRawAcpRuntimeCatalogEntry` round-trips `max_parallelism` →
`maxParallelism`; absent when `undefined`

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-05 16:09:14 -04:00
klopez4212
f2ce575b62 Fix media attachment actions (#4849)
## Summary

- Upload photos immediately while keeping videos queued for background
upload.
- Move image annotation and video spoiler actions to thumbnail hover
overlays.
- Preserve the image editor's existing Draw and Spoiler controls.

### Snapshots

#### Image annotation overlay

![Image annotation
overlay](https://raw.githubusercontent.com/block/buzz/87d7e1b1a0774ffef7a1a6cba03baffd63e11bd4/pr-4849--01-image-annotation-overlay.png)

#### Image editor controls

![Image editor
controls](https://raw.githubusercontent.com/block/buzz/87d7e1b1a0774ffef7a1a6cba03baffd63e11bd4/pr-4849--02-image-editor-controls.png)

## Testing

- `pnpm typecheck`
- `pnpm check`
- Focused attachment, drawing, and spoiler smoke tests
- Pre-push desktop tests (4,286 passing)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Honey <47c18026466e670a1618fd7de0ef32b9ff75d6e0b5ccf255d13c8c3d674ed115@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-05 12:17:48 -07:00
klopez4212
2034e693a8 fix(desktop): remove join API token control (#4897)
## Summary

Remove the nonfunctional API-token option from the existing-community
join flow.

## Validation

- Focused Playwright join-flow coverage
- Add-community screenshot coverage

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-05 11:56:14 -07:00
Wes
014562c063 fix(desktop): allow shared agent mentions (#4913)
## Summary

- admit relay-discovered agents to autocomplete when their response
policy authorizes the viewer
- require authorization in the exact active stream/forum channel for
mentions, while keeping community-wide discovery for member invitation
- fail closed for relay-only agents in DMs and unresolved composer
contexts
- re-authorize cached autocomplete rows after policy/channel changes so
stale agent suggestions cannot leak back in
- preserve managed-agent behavior and explicitly reject stale
agent-marked channel members absent from both live directories

## Validation

- `pnpm --dir desktop test` — 4,288 passed
- `pnpm --dir desktop typecheck`
- `pnpm --dir desktop check`
- `pnpm --dir desktop build:e2e`
- focused Playwright mention matrix — 12 passed
- focused Playwright member-invitation matrix — 2 passed
- pre-push hooks after rebase to current `origin/main` — desktop check
and 4,288 tests passed
- independent correctness/privacy re-review cleared with no remaining
blocker

## Related competing PRs

This supersedes or overlaps #2333, #3056, #4242, #4137, #2314, #4058,
and #2605. This version adds exact-channel authorization, fail-closed
DM/context handling, cached-row reauthorization, forum coverage,
outbound mention-tag coverage, explicit stale-member coverage, and
add-member discovery coverage.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-05 11:15:11 -07:00
Jordan Mecom
efe1893dd3 fix(channels): restrict private-channel invitations (#4612)
This change requires an active owner or administrator for third-party
additions to private channels. The relay validator and transactional
database authority enforce the same rule, including removed-member
reactivation and role-change paths.

Idempotent self-target behavior remains available, while ordinary
members can no longer extend private-channel access to another identity.

## Testing

- `git diff --check
origin/main...codex/security-private-channel-invite-authority`
- Rebased onto `origin/main` at `5c98932`
- Full CI pending

Originating Buzz thread:
`buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1`

---------

Signed-off-by: Jordan Mecom <jm@squareup.com>
Signed-off-by: Eli Foster <efoster@squareup.com>
Co-authored-by: Eli Foster <efoster@squareup.com>
2026-08-05 10:47:00 -07:00
Will Pfleger
6dbc946512 fix(desktop): make missing-command error actionable for released builds (#4802)
User-facing error for missing ACP harness commands has been pointing
released-build users to run `cargo build --release --workspace` and read
TESTING.md — both dead ends for anyone not building from source.

Updated message acknowledges that antivirus software can quarantine
bundled binaries and provides practical remediation steps. Preserves
pointer to TESTING.md for source builds.

Fixes issue context from #4491.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub16v54tttfqacx9ycvc3k0ut0npj564ahcuajzy6qjvh57ntmsf4uq4806j2 <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
2026-08-05 09:51:15 -04:00
Wes
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>
2026-08-04 16:54:16 -07:00
Kalvin C
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
Wes
79c52166cf Revert "chore(release): release Buzz Desktop version 0.5.5" (#4808)
Reverts block/buzz#4800
2026-08-04 16:09:33 -07:00
Wes
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
Kalvin C
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
Wes
8faf09f9ae Revert "chore(release): release Buzz Desktop version 0.5.5" (#4797)
Reverts block/buzz#4788
2026-08-04 22:16:48 +00:00
thomaspblock
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
Kalvin C
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
Wes
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
thomaspblock
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
Will Pfleger
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
Wes
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
Kalvin C
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
klopez4212
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
Will Pfleger
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
Wes
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
Wes
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
Will Pfleger
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
klopez4212
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
Will Pfleger
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