Commit Graph
1390 Commits
Author SHA1 Message Date
npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6andDawn 54cd502009 docs(desktop): record why G3's max bound cannot be widened
The p95 comment claimed 31x of headroom "lets this run on a loaded CI
runner," and read as if it justified the whole gate. It justifies the
quantile only. A p95 discards its worst samples by construction and so
degrades gracefully under load; a max over 200 samples is one
observation that any single scheduler preemption exceeds. No budget
makes it robust, because that tail belongs to the scheduler.

Measured on one 16-core host at the same commit: pass at load ~6, fail
at ~31 (p95 65535 us, max 164889 us). Sami measured a run where p95 sat
4x inside budget while max alone blew at 38150 us -- a p95-only gate
would have called a 38 ms stall green.

That asymmetry is what makes the ignore-plus-native-release placement
load-bearing rather than tidy, and it is the reason the repair for a
flake here is to fix the host, never to raise the number: a hitch is a
max-event, so widening the max is the one change that removes the only
assert catching the user-visible failure.

Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
2026-08-01 20:41:43 -04:00
npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehrandtlongwell-block 25fcb5b26d Merge branch 'dawn/tui-engine-core' of ../buzz-tui-dawn into tui-substrate-perci
* 'dawn/tui-engine-core' of ../buzz-tui-dawn:
  feat(desktop): make a frame's viewport comparable in one value

Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
2026-08-01 20:15:26 -04:00
tlongwell-blockandDawn 105aadb22c feat(desktop): make a frame's viewport comparable in one value
The integration lane's core operation is "does this frame describe the grid I
am currently showing?" -- a frame captured before a resize can arrive after it,
because frame delivery and the resize call travel separate paths. Answering
that meant comparing three loose fields, and a consumer that checks two of the
three is wrong on exactly the resize that changed the one it skipped.

Group them into `Viewport { generation, columns, screen_lines }` so the
question is one equality, and return it from `resize` rather than making the
caller ask afterwards: across a transport a follow-up query races the next
resize, so its answer can describe a grid that has already been replaced. A
same-size resize stays inert and reports the unchanged generation.

Two mutants, both caught: returning the requested viewport instead of the
applied one, and stamping a frame at encode time instead of capture time.

The stamping fixture was tautological on its first draft -- it compared a
frame's viewport against a copy taken from that same frame, which the language
already guarantees. It now asserts capture-time values explicitly, and the
comment records why the obvious form proves nothing.

Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
2026-08-01 20:12:36 -04:00
npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehrandtlongwell-block 59648f0706 test(desktop): harden terminal engine gates
Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
2026-08-01 20:10:30 -04:00
tlongwell-blockandDawn a094adce9a feat(desktop): add the resize seam to buzz-terminal
The engine had no resize entry point, so the integration lane was about to
build a coalescing protocol against a `Frame` that could not say which
viewport it described. A consumer could only infer that from message
ordering, which is exactly the thing a resize in flight breaks.

`Terminal::resize` takes one coalesced target size, never a stream: resize is
superlinear in scrollback (2.5-4.7 ms for a single column change at 10k
history) and runs while holding the terminal. Frames now carry `generation`,
`columns` and `screen_lines`, so a full frame is self-describing and a grid
discontinuity is detectable from the frame rather than assumed from ordering.

Three fixtures, each shown failing under its mutation. One of them only
exists because the first version of this test suite was vacuous:

Deleting the encoder's dedup-cache clear left all the resize tests green.
The width-changing fixture cannot catch it -- changing the width changes
every cell, so the hashes differ and the rows are republished for the wrong
reason. A height-only resize is the discriminator: the width is unchanged, so
reflowed rows hash exactly as before and a stale cache suppresses them right
after the consumer was told to discard what it had. Growing 10 -> 20 lines
moves row 0 to row 1, and under the mutation the frame carries 13 of 20 rows
-- seven rows blank on screen. With that arm added the mutant dies.

Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
2026-08-01 20:07:49 -04:00
tlongwell-blockandDawn 7e7731b89b feat(desktop): add buzz-terminal engine crate
Scaffolds the terminal emulator behind the Buzz substrate: grid state, the
parser and its two hardening fences, the closed listener set, and the damage
encoding the renderer will consume. No PTY, no child process, no window --
those belong to the embedder, so this crate stays testable against byte
fixtures on every platform.

Three things ship together because each is load-bearing for the others:

* The crate itself.

* The explicit `members` key in the workspace manifest. With a bare
  `[workspace]`, `cargo test/check --workspace` expands to a set that
  excludes this crate, and its gates pass green-and-empty over a real
  defect. Verified as a four-arm matrix on this repo.

* A file-size ratchet rule for `src-tauri/crates`. The ratchet's only Rust
  root was `src-tauri/src`, so the crate would be born outside the repo's
  one size discipline -- silently, since the check exits 0. Verified: a
  1500-line file under `crates/` passes without the rule and fails with it,
  while the real crate passes.

Two fences, both enforced on byte counts rather than a clock, because a
clock makes the bound depend on machine speed:

* F1 aborts a synchronized update once it buffers 64 KiB.
* F2 rebuilds the parser once 256 KiB of parser-visible bytes are charged.

F1 turned out to be dual-purpose. Measured under a flood, the reader's lock
hold is p50 1 us while the renderer's *wait* for the same lock is p50
4245 us -- so the plan's original "reader hold under one frame" requirement
passes trivially while the window is visibly stuck. The gate is now the
renderer's acquire latency, and F1 is what holds it down: p95 127 us with
the fence, 16383 us without.

That gate also needs a fair lock. `std::sync::Mutex` lets the read loop
barge, and the renderer completed 12 frames to the reader's 39893
acquisitions; `FairMutex` under the same flood gives 76 and 63 us.

Six fixtures, each shown failing under the mutation it exists to catch:
deleting F1, deleting F2, omitting flush accounting, charging raw input,
and swapping the fair lock for a barging one.

Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
2026-08-01 19:56:44 -04:00
8e81afa431 fix(desktop): keep thread-open affordance in archived channels (#4012)
## Problem

Threaded replies "disappeared" from archived Buzz channels: the **"N
replies →"** summary row and the huddle-started **"View thread"** button
vanished, so existing threads were unreachable from the channel
timeline. The thread data was intact — this was a UI gate, not data
loss.

## Root cause

A single `onReply` prop drove two distinct affordances:
- the **compose** affordances (hover "Reply" button, inline reply
target), and
- the **view** affordances ("N replies →" summary row, huddle "View
thread").

`ChannelPane` nulls `onReply` on archived channels to keep them
read-only. That correctly hid composing — but also hid the view
affordances, since they keyed off the same prop.

## Fix

Two independent props, one per concern:

- **`onReply`** drives the compose affordances and is gated on
`archivedAt` — nulled on archived channels, so no new replies can be
started.
- **`onOpenThread`** drives the view affordances and is passed
regardless of archived state, threaded `ChannelPane → MessageTimeline →
TimelineMessageList → MessageRow`.

Opening a thread on an archived channel is read-only: the thread panel's
composer is independently gated via `isComposerDisabled` (includes
`archivedAt !== null`, `ChannelPane.tsx:318`).

### Before
<img width="811" height="794" alt="Screenshot 2026-07-31 at 20 26 00"
src="https://github.com/user-attachments/assets/670d9db4-30da-4c6d-97dc-275b5dbebca8"
/>

### After
<img width="873" height="791" alt="Screenshot 2026-07-31 at 20 28 04"
src="https://github.com/user-attachments/assets/88525231-2539-4eb3-8117-8e58a0cb3855"
/>

## Validation

- `pnpm typecheck` clean
- biome lint clean on touched files
- full `pnpm test` suite green (3885 tests)
- pre-push `branch-skew` / `desktop-check` / `desktop-test` hooks passed

Signed-off-by: Trey Wood <treyw@squareup.com>
Co-authored-by: npub14h0tw3uj7jm77qfxcwn6um2s5h55l0klrt2w9srzp3m3yvjc0mpsjsuk6e <addeb74792f4b7ef0126c3a7ae6d50a5e94fbedf1ad4e2c0620c771232587ec3@buzz.block.builderlab.xyz>
2026-08-01 08:41:42 -04:00
3ade48d503 fix(desktop): point Oh My Pi preset at omp.sh (#3516)
## Summary

Points the Oh My Pi preset at the `omp.sh` installation page instead of
the GitHub repository.

The project serves its current installer from `omp.sh/install.sh`.

### Related issue

Extracted from the maintainer request in #3111. I found no matching open
pull request in a final duplicate check.

### Testing

`https://omp.sh/` returned HTTP 200 with the installation page.

`https://omp.sh/install.sh` resolved to the current installer and
returned HTTP 200.

`cargo test --manifest-path desktop/src-tauri/Cargo.toml preset_entry --
--nocapture` passed 5 tests.

`just ci` passed.

This changes metadata only, so screenshots do not apply.

Signed-off-by: Shreyash Vengurlekar <262980978+kiranmagic7@users.noreply.github.com>
Co-authored-by: Shreyash Vengurlekar <262980978+kiranmagic7@users.noreply.github.com>
2026-07-31 22:30:10 -04:00
fa1a5b1a79 fix(mesh): stop restarting a busy or loading shared-compute node (#3909)
## Problem

Sharing compute with a large model (e.g. `gemma-4-26B`) put the desktop
app into a **restart loop**: toggle Share → app appears to "download" /
stall → the whole app restarts → repeat. Small models (E4B) were
unaffected, which made it look model-specific and flaky.

It is not model-specific and not flaky. It is a **false-positive
liveness check**.

## Root cause (proven by black-box measurement)

A `serve` node's OpenAI ingress (`:9337`) serializes **all** HTTP —
including the `/v1/models` liveness probe — behind the current in-flight
inference. It is *also* HTTP-unresponsive during model load and
package-layer download. In every one of those phases the node is alive
and progressing, but it cannot answer an HTTP probe.

Measured on a standalone `gemma-4-26B` node (randomized ~30k-token
prompt, cache-miss):

| during one ~30s inference | result |
|---|---|
| concurrent `GET /v1/models` | **27.0s**, then 200 |
| concurrent small `/chat/completions` | **28.8s**, then 200 |
| `tcp_connect(:9337)` throughout | **~0ms** |

Both HTTP calls simply queued behind the turn; TCP kept accepting
instantly. A probe with any timeout shorter than the turn reads the node
as dead.

Buzz then acted on that false "dead" reading in two places, **both
restart paths added in #2823**:

1. **Ingress watchdog** — after 2 consecutive `/v1/models` timeouts,
evicts the node; for a serve node eviction means
`app.request_restart()`. Two dead probes landing inside a prefill window
→ restart loop.
2. **Start / restore paths** — on a `wait_for_mesh_inference` timeout,
`stop()` the node and (fresh start) `request_restart()` the app "to
guarantee cleanup" — even though the node was still loading weights or
downloading layers. This is the exact line in the incident log: `started
node failed inference readiness … Buzz is restarting`.

## Fix

Treat a **bound TCP port as alive**. Death has exactly one unambiguous
signal: a *closed* port.

- **Watchdog** (`recovery.rs`): only `PortClosed` may evict. A
bound-but-HTTP-unresponsive `Unhealthy` port is never evicted, at any
probe streak or urgency. Closed-port eviction is unchanged.
- **Start / restore** (`commands/mesh_llm.rs`): install the runtime
**before** probing readiness (so it is always tracked by `AppState` and
can never be orphaned — which is what the restart was guarding against),
and on a readiness timeout **leave it warming up** instead of
stopping/restarting. Launch-restoration stays disarmed until real
inference is confirmed, so a genuinely broken start is retried next
launch rather than silently disabling Share Compute.

### What this deliberately does *not* do

Detecting a node that is bound-but-internally-wedged needs a liveness
signal that bypasses the inference lock. There is none today, so this
fix cannot distinguish "wedged" from "busy" and errs toward not
restarting. That gap is a mesh-llm bug, filed upstream:
**Mesh-LLM/mesh-llm#1126** (lock-free `/live`+`/ready` on the ingress).
A follow-up here can consume it once it lands.

## Tests

- Watchdog never evicts a bound/busy port at any probe streak or urgency
(the regression).
- Closed-port eviction still fires (dead listener still reclaimed).
- Black-box: a listener that accepts TCP then stalls HTTP classifies as
`Unhealthy`, not `PortClosed`.
- **Mutation-proven**: reverting the eviction rule to the old
count-based logic fails the busy-node test.

`cargo test` (desktop, `--features mesh-llm`) green, fmt + clippy clean.

## Not covered here

The intermittent nature means I could not force the live loop
deterministically on a warm machine; the proof is the measured
serialization + the mutation-proven unit/black-box tests. Live behaviour
(app no longer restarts while a 26B node loads/serves) still merits a
manual check before merge.

---------

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
2026-07-31 22:19:30 -04:00
John Matthew TennantandGitHub 45314fc504 fix(desktop): preserve first huddle speech (#3962)
## Context

On the first huddle after launching Buzz Desktop, a live agent reply can
arrive after agent membership is known but before the initial
TTS-enabled state has loaded. The subscription previously released
buffered messages at the membership boundary, so that first reply was
evaluated while speech was still disabled and was silently skipped.
Later replies worked, and later huddles usually worked because the state
was already warm.

## Summary

Hold initial live agent replies until both authoritative agent
membership and the initial TTS state are known. This preserves the first
eligible reply after a cold app launch without changing live-only
routing, ordering, or fail-closed behavior.

## Changes

- Replace the membership-only startup gate with a two-signal readiness
gate for membership and TTS state.
- Release buffered live messages in arrival order only after both
signals resolve.
- Drop buffered messages if either initial lookup fails.
- Add a deterministic regression for the observed ordering: membership
resolves first, TTS enables second, and the first reply is spoken.

## Related issue

None found.

## Testing

Manual validation in the daily-driver build confirmed that the first
agent reply is spoken in the first huddle after a fresh app launch.

The regression scenario was also run against both revisions:

```text
main: FAIL — actual spoken replies: []; expected: ["first agent reply"]
PR:   PASS — 10 passed, 0 failed
```

## Screenshots

N/A, nonvisual speech behavior.

## Reviewer-reproducible examples

1. Quit Buzz Desktop completely.
2. Reopen it with Pocket TTS enabled.
3. Start the first huddle of the session with a running agent.
4. Send a prompt that produces a spoken agent reply immediately after
the huddle starts.
5. Confirm the first reply is spoken, not only the second reply.
6. Stop the huddle, start another one, and confirm subsequent huddles
retain the same behavior.

For a deterministic red/green check, run the same
membership-before-TTS-state ordering from `desktop/`.

On `main`:

```bash
node --import ./test-loader.mjs --experimental-strip-types --input-type=module -e '
import assert from "node:assert/strict";
import { createInitialMembershipGate, createOrderedSpeaker } from "./src/features/huddle/lib/ttsLiveMessages.ts";
const spoken = [];
const speaker = createOrderedSpeaker(async text => spoken.push(text), error => { throw error; }, false);
const gate = createInitialMembershipGate(text => speaker.enqueue(text, 1));
gate.push("first agent reply");
gate.succeed();
speaker.setEnabled(true);
await new Promise(resolve => setTimeout(resolve, 0));
console.log("spoken:", JSON.stringify(spoken));
assert.deepEqual(spoken, ["first agent reply"]);
'
```

Observed failure:

```text
spoken: []
AssertionError: Expected values to be strictly deep-equal
```

On this PR branch:

```bash
node --import ./test-loader.mjs --experimental-strip-types --input-type=module -e '
import assert from "node:assert/strict";
import { createInitialTtsReadinessGate, createOrderedSpeaker } from "./src/features/huddle/lib/ttsLiveMessages.ts";
const spoken = [];
const speaker = createOrderedSpeaker(async text => spoken.push(text), error => { throw error; }, false);
const gate = createInitialTtsReadinessGate(text => speaker.enqueue(text, 1));
gate.push("first agent reply");
gate.markMembershipKnown();
speaker.setEnabled(true);
gate.markTtsStateKnown();
await new Promise(resolve => setTimeout(resolve, 0));
console.log("spoken:", JSON.stringify(spoken));
assert.deepEqual(spoken, ["first agent reply"]);
'
```

Observed output:

```text
spoken: ["first agent reply"]
```

---------

Signed-off-by: John Tennant <jtennant@squareup.com>
2026-07-31 22:16:56 -04:00
eb049ddf81 feat(desktop): Agent Trading Cards — mintable agent-snapshot card PNGs with optional NIP-44 lock (#3278)
## Agent Trading Cards

"Create Agent Card" action in the agent panel that mints an AI-generated
trading card PNG which **is** the agent: the card carries the
`buzz_agent_snapshot` tEXt chunk and is drag-in importable like any
snapshot PNG.

### What's in here
- **Mint pipeline (Rust):** one OpenAI Responses call — `gpt-5.6-sol` as
card designer with `gpt-image-2` via the `image_generation` tool (~2–3
min). New `mint_agent_card` / `save_agent_card` commands; preview with
reroll; save or send as `.agent.png` with round-trip verification before
any bytes leave the app.
- **Snapshot/chunk work stays in Rust,** reusing the existing
encoder/decoder seams (byte-compat golden vector proves the plain path
is identical to the pre-envelope encoder for placeholder, PNG-injection,
and JPEG-transcode paths).
- **Locked cards (NIP-44):** optional `buzz-agent-snapshot-encrypted`
envelope encrypted to the (owner, agent) pair. `parse_canonical_pubkey`
performs lift-x curve validation before any API spend; wrong-key decrypt
returns a fixed refusal; the plain decoder refuses locked cards.
- **Guardrails:** 10 MiB ceiling on final bytes, memory structurally
`none` in the snapshot, full-manifest import disclosure, API-key hygiene
via env layering (record > persona > global > process), fail-early
validation ordering (all key/lock/NIP-44-cap checks before Responses
spend).
- **Import side:** full-manifest disclosure dialog, locked-card import
disclosure, bounded avatar fetch.

### Review
Code reviewed by Wren across the full arc; final locked-card
cross-review **APPROVED 9/9/9** at exactly this head (`64f819dc8`), with
independent same-SHA verification: Rust lib 1,843/1,843, clippy
`--all-targets -D warnings`, desktop file-size gate.

### Live-mint evidence (real API, shipping seams, this SHA)
- **Plain (Honey):** 188s, 1500x2250, 5,101,503 bytes (< 10 MiB);
decoded manifest == built manifest; memory=none.
- **Locked (Fizz):** 176s, 4,670,184 bytes; owner-key and agent-key
decrypt both verified via logical manifest compare; wrong-key refusal
exact; plain decoder refuses.
- **Live finding:** built-in agents' ~171 KB inline avatars exceed the
NIP-44 65,535-byte plaintext cap and the fail-early guard fires before
API spend — clean error path, noted as a UX follow-up for large-avatar
agents choosing lock.

Full evidence (cards + dialog screenshots) posted in the originating
thread.

---------

Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
2026-07-31 22:01:17 -04:00
3a96acea09 chore(release): release Buzz Desktop version 0.5.3 (#3972)
## Buzz Desktop release v0.5.3

- **Frozen main:** `54c8ef30a9bb9c59a4415a8a7ee84c7c5454b48a`
- **Reviewed candidate:** `d0c06978bbf494ded6fe1a55d69d810ae9b65863`
- **Previous desktop release:** `v0.5.2`
- **Proposed immutable tag:** `desktop-v0.5.3`

This PR must be **squash merged** only after the Desktop Release
Candidate check passes. The branch must remain based directly on current
; 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-07-31 19:07:31 +00:00
WesandGitHub bb34bc4d98 Revert "chore(release): release Buzz Desktop version 0.5.3" (#3960)
Reverts block/buzz#3944
2026-07-31 17:38:42 +00:00
WesandRelease Automation d12b3d6a79 chore(release): release Buzz Desktop version 0.5.3
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
2026-07-31 10:43:30 -06:00
c104eecfb3 feat(desktop): import local Pocket voices (#3259)
## Context

Pocket TTS currently offers bundled reference voices. People also need a
local, private way to add a voice without sending audio to a cloud
service.

## Summary

Add a Pocket voice import flow to Voice settings. Buzz opens the native
file picker, decodes common audio formats in the reusable `buzz-voice`
crate, canonicalizes the selected audio, stores it under a
content-derived identity in app data, selects it, and lets the user
delete it later.

## Changes

- Accept WAV, M4A, MP3, FLAC, OGG, and AIFF files between 2 and 30
seconds, including multichannel sources.
- Decode and downmix accepted audio to canonical mono 32 kHz PCM16 WAV
before hashing and storage.
- Store imported voices behind stable `pocket:imported:<sha256>`
identities and content-addressed files.
- Keep absolute file paths inside the native process and expose only
voice metadata to React.
- Include imported voices in Pocket preview and live huddle playback.
- Add Add voice and delete controls while preserving the bundled Pocket
voice catalog.
- Fall back to Mary when the selected imported voice is deleted.
- Keep durable import, selection, and deletion successful when a live
TTS worker acknowledgement is delayed.
- Preserve bundled voices when optional import metadata is unreadable
and keep failed deletion retryable.

## Related issue

None found.

## Testing

Production decoding was exercised with WAV, M4A with AAC, MP3, FLAC, OGG
Vorbis, and AIFF fixtures. Each format canonicalized to mono 32 kHz
PCM16 WAV. Manual validation in the combined daily-driver build covered
native-picker import, Preview, live-huddle playback, deletion, and Mary
fallback.

## Screenshots

The Voice settings card preserves the bundled Pocket catalog and adds
the local Add voice action.

![Pocket TTS voice
import](https://raw.githubusercontent.com/block/buzz/c03ba29060ca544c5ac3394c212f376651b386a3/pr-3259--pocket-voices.png)

## Reviewer-reproducible examples

Create common-format fixtures and run them through the production
importer:

```bash
. ./bin/activate-hermit
fixtures="$(mktemp -d)"
ffmpeg -hide_banner -loglevel error -f lavfi -i "sine=frequency=220:duration=3" -ac 2 -ar 44100 "$fixtures/voice.wav"
ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" -c:a aac "$fixtures/voice.m4a"
ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" "$fixtures/voice.mp3"
ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" "$fixtures/voice.flac"
ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" -c:a libvorbis "$fixtures/voice.ogg"
ffmpeg -hide_banner -loglevel error -i "$fixtures/voice.wav" -c:a pcm_s16be "$fixtures/voice.aiff"
BUZZ_VOICE_IMPORT_TEST_DIR="$fixtures" \
  cargo test -p buzz-voice imports_common_audio_format_fixtures -- --ignored --nocapture
```

Exercise import persistence, synthesis, deletion, and bundled-voice
fallback with an installed Pocket model:

```bash
BUZZ_POCKET_MODEL_DIR=/path/to/pocket-model-bundle \
  cargo test -p buzz-voice --test pocket_import_audio \
  objective_import_synthesis_delete_and_mary_fallback \
  -- --ignored --nocapture
```

Exercise the native-picker boundary, selection, preview dispatch,
deletion, cancellation, and invalid-file states:

```bash
cd desktop
pnpm build:e2e
pnpm exec playwright test tests/e2e/voice-settings.spec.ts --project=smoke
```

---------

Signed-off-by: John Tennant <jtennant@block.xyz>
Signed-off-by: John Tennant <johnmatthewtennant@gmail.com>
Signed-off-by: John Tennant <jtennant@squareup.com>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: John Tennant <jtennant@block.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
2026-07-31 11:15:09 -04:00
39ce3dfc3c fix(desktop): open profiles from avatars (#3751)
## Summary
- show profile descriptions in hover cards as a single truncated line
- open the profile panel when avatars are clicked across desktop
surfaces
- make the direct-message intro avatar clickable

## Validation
- Desktop static checks
- 3,807 desktop tests via pre-push

---------

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-07-31 14:24:51 +00:00
61ba9dfaa0 refactor(voice): extract reusable Pocket primitives + Pocket voice settings (relands #2467 + #3208) (#3910)
Relands **#2467** (extract `buzz-voice` crate) and **#3208** (Pocket
voice settings) onto main, after #3266 and #3180 merged.

## Why a fresh PR
The repo is squash-only with delete-branch-on-merge. Squashing #3266
deleted `jtennant/pocket-tts-2026-04`, which was #2467's base — GitHub
auto-closed #2467 and it cannot be reopened. Squash merges also sever
ancestry, so GitHub's natural merge-base reports phantom conflicts for
the whole remaining stack.

## Content provenance
- Byte-identical to the blessed `jt/buzz-voice-refactor` branch
(`93029c577`, tree `6729e0eff` — reviewed by Dawn (#2467) and Max
(#3208) at exact heads) **except** the three files where #3180 and #3208
genuinely interact.
- Three-file resolution (union of both sides):
- `huddle/mod.rs` — #3180's pipeline re-exports + #3208's
`agent_tts_routing` imports.
- `huddle/state.rs` — `reset_preserving_generation` preserves both
`huddle_generation` (#3180) and `tts_enabled` (#3208); test sets merged
into one `tests` module.
- `desktop/src/testing/e2eBridge.ts` — both switch arms kept; no
duplicate case labels.

## Verification at cf32dacd7
- `cargo test` (desktop/src-tauri, pinned 1.95.0): **2047 + 3 pass / 0
fail** (14 ignored: 8 keychain, 4 real_relay, 2 flag-gated)
- `cargo clippy --all-targets -- -D warnings`: clean; `cargo fmt
--check`: clean
- `cargo check --workspace` (root, includes new `buzz-voice` member):
clean; `cargo test -p buzz-voice`: 5/0
- `pnpm test`: **3885 / 0**; `tsc --noEmit`: clean; lint: clean

The 3180×3208 interaction resolution is getting an independent team
re-review before merge.

Buzz channel: buzz-desktop-voice `fd5fb402-b651-4238-89b1-bb3e2fa4dc96`,
thread `b4798ecc`.

Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
2026-07-31 09:41:09 -04:00
4632c55041 feat(desktop): auto-enable huddle transcription for agents (#3180)
## Context

Before this change, every huddle initialized with transcription off.
Joining or adding an agent did not enable it, so the agent could not
receive spoken conversation until a person clicked the transcript
control. Starting a huddle from an agent DM could also omit that agent,
and adding an agent who already belonged to the parent channel could
attempt an unnecessary role change and show a warning.

Agent detection uses authoritative huddle membership. A participant
counts as an agent when the ephemeral membership identifies it with the
`bot` role, or when the existing agent identity model identifies the
participant in an agent DM.

## Summary

Buzz now enables transcription once when the first authoritative agent
is present. After that initial automatic action, explicit user control
is authoritative: manual ON or OFF survives membership refreshes,
reconnects, and UI remounts. Removing the last agent does not change the
current transcription state.

Agent-DM huddles enroll the agent automatically. Adding an agent who
already belongs to the parent channel preserves the existing parent role
and completes without a role-mutation warning.

| Scenario | Before | With this change |
| --- | --- | --- |
| First authoritative agent joins or is hydrated | Transcription stays
off | Transcription turns on once |
| User explicitly turns transcription on or off | Manual control exists
without an agent policy | The explicit choice suppresses later automatic
changes |
| Last agent leaves | No defined agent-presence behavior | The current
transcription state remains unchanged |
| Huddle starts from an agent DM | The agent can be omitted | The known
agent is enrolled automatically |
| Added agent already belongs to the parent channel | Buzz can attempt a
role rewrite and warn | Existing parent membership and role are
preserved |
| Transcription is active | The control is not visually distinct | The
control is highlighted and exposes `aria-pressed=true` |

## Changes

- Derive agent presence from authoritative bot-role huddle membership
and known agent-DM identity.
- Apply the one-time auto-enable rule during create, join, membership
hydration, reconnect, pipeline startup, and local agent addition.
- Preserve explicit user state and use huddle-generation guards so stale
asynchronous work cannot alter a replacement huddle.
- Keep backend and React transcription state synchronized, with a
visible and accessible active control.
- Enroll known agent-DM participants and make parent-channel membership
updates idempotent.
- Cover hydration ordering, reconnects, remounts, explicit OFF,
last-agent removal, DM enrollment, existing membership, and active
styling.

## Related issue

None found.

## Testing

Manual validation in `pending-seed` confirmed the product contract:

1. Started a huddle from the owned, running Fizz agent DM.
2. Confirmed the authoritative roster contained the human and Fizz as an
agent.
3. Confirmed transcription enabled without clicking the control: `Stop
transcript`, `aria-pressed=true`, with the highlighted active
background.
4. Turned transcription off and confirmed `Start transcript`,
`aria-pressed=false` remained stable.
5. Removed Fizz while transcription was off and confirmed the state
stayed off.
6. Left the huddle cleanly.

## Screenshots

The same control has distinct active and inactive states.

![Active transcript
control](https://raw.githubusercontent.com/block/buzz/2dcb266244e93d358f85e5371d190de77b03c86d/pr-3180--active-transcription.png)

![Inactive transcript
control](https://raw.githubusercontent.com/block/buzz/2dcb266244e93d358f85e5371d190de77b03c86d/pr-3180--inactive-transcription.png)

## Reviewer-reproducible examples

From a fresh checkout:

```bash
pnpm --dir desktop build:e2e
pnpm --dir desktop exec playwright test tests/e2e/huddle-transcription.spec.ts --project=smoke
pnpm --dir desktop exec playwright test tests/e2e/mentions.spec.ts --project=smoke --grep "system agent profile exposes owned agent actions|system agent avatar exposes owned agent actions|owned bot profile exposes message and huddle actions|owned agent mention profile exposes message and huddle actions"
```

The huddle scenario exercises initial authoritative hydration, exactly
one automatic enable, explicit OFF persistence, unchanged state after
last-agent removal, newer events winning over delayed hydration,
agent-DM enrollment, and idempotent parent membership. It also asserts
`aria-pressed` and distinct computed active styling.

---------

Signed-off-by: John Tennant <jtennant@squareup.com>
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
2026-07-31 07:06:41 -04:00
081f805d5e feat(agent): optional reply guard reminds a silent turn to publish (#3763)
## Why

A Buzz agent's assistant text and reasoning are never shown to anyone —
only what it posts through the CLI. A turn that runs fifteen tool calls
and never publishes is a silent failure: the requester waits on a result
that was produced and thrown away.

This adds an optional reminder at the end-of-turn gate, off by default.

Tyler asked for it in buzz-mesh; plan iterated to **9.5/10 with @Wren**
(Minimalness 9.7, Elegance 9.5, Correctness 9.3).

## What

`BUZZ_AGENT_REQUIRE_REPLY=1` (default off, per-agent opt-in). A turn
about to end with no recognized attempt to post gets a reminder and is
rerolled. **At most two, then the turn ends regardless** — the guard
catches accidental omission, it does not compel speech. The reminder
text explicitly licenses silence so it cannot fight the base prompt's
"silence is usually correct."

**This is not a new MCP hook.** `RunCtx::run` *is* the turn, so the two
per-turn locals need no plumbing, and every tool call already passes
through it with arguments visible. The objection is appended at the
existing `_Stop` gate and rides `push_hook_outputs_as_tool_results`, so
the model receives it as a lower-trust tool result with `{hook, server,
text}` attribution. No new trust path, no new lifecycle event, no
dev-mcp or CLI protocol change.

Earlier revisions of this plan needed four crates (a `_UserPromptSubmit`
hook, a marker file, a `buzz-cli` change, dev-mcp state). Tyler pointed
out the agent already knows both facts; that deleted all of it. Net
runtime change is ~35 lines in `agent.rs` + ~4 in `config.rs`.

### Recognition contract

A registered non-hook tool whose qualified name ends in `__shell`, whose
`command` argument contains `messages send` or `reactions add`.

- **The `__` separator is exact, not approximate.** Given `has()` +
`!is_hook()`, `ends_with("__shell")` is *provably equivalent* to a bare
name of `shell`: registration forbids `__` in server and bare names
(`mcp.rs:227,268`) and qnames are `{server}__{bare}`, so a trailing
`__shell` could only straddle the separator if the bare name began with
`_` — which `is_hook` excludes. Without the separator, `powershell` and
`noshell` would match.
- **Reads the structured `command` field**, not serialized arguments, so
a `description` that quotes a send cannot disarm the guard, and a
non-string `command` is rejected rather than coerced.
- **Detects an attempt, not a successful publish.** A failed send
already returns non-zero exit and error JSON — louder than this
reminder. The variable is named `buzz_reply_call_seen` so the code can't
pretend otherwise.
- **Checked after the per-turn tool-call cap**, since a discarded call
never ran.
- `messages send` also covers `messages send-diff`. Reactions count
because the base prompt directs agents to react rather than post a bare
acknowledgement.

**Known limits, both deliberate and documented:** a command assembled at
runtime (`$CMD`) or hidden in a wrapper script is missed; text that
merely quotes a send (`echo "buzz messages send"`) matches. Missing a
real post is the expensive direction and substring matching is the
forgiving one there. Neither edge is pinned by a test, so the matcher
stays free to improve.

### Budget

Reminders share `BUZZ_AGENT_STOP_MAX_REJECTIONS`, the existing outer cap
on every end-turn objection. Default 3 fits both; at 1 only one fits; at
0 the guard is off with the hooks. A round carrying both a hook
objection and a reminder costs one rejection and delivers both texts. An
independent budget would either violate that bound or need a second
arbitration rule.

## Prior art

- **#3467** (closed) built the same detector one layer up in `buzz-acp`
for a different remedy. None of its symbols are on main — this borrows
its permission to be coarse, but reads structured data that ACP didn't
have.
- **#3648** (open) detects turns with *no output at all*; a turn with
fifteen tool calls and no post counts as output there, so it does not
cover this case.
- **#3741** (merged) is mesh-only.

## Testing

**14 new tests.** 4 unit tests on the matcher; 10 integration tests
through the ACP wire harness: off by default, `=0` still off, opted-in
silent → exactly 2 reminders then `end_turn`, registered `fake__shell`
send → 0 reminders, hallucinated `fake__shell` → still reminded, publish
call truncated past the 64-call cap → still reminded, budget 1 → 1
reminder, budget 0 → off, combined `_Stop` hook objection + reminder →
one round both texts and after 2 reminders the hook objection continues
alone, unparseable `=true` → startup error naming the key.

**10 mutation checks, each breaking a specific named test** — neutralize
the nag cap, stop sharing the budget, neutralize `buzz_reply_call_seen`,
drop `has`/`is_hook`, ignore the flag, drop the `__`, drop `reactions
add`, read serialized args, move detection before truncation.

`tests/bin/fake_mcp.rs` gains `FAKE_MCP_SHELL_TOOL=1`: it previously
exposed no tool with a bare name of `shell`, so the satisfied-guard path
was untestable.

Full `cargo test -p buzz-agent` green at 9e0ae1f04; clippy `-D warnings`
and `cargo fmt --check` clean.

**Unrelated flake found:**
`cancelled_turn_with_usage_emits_notification_before_response`
(`tests/fake_llm.rs`) is timing-sensitive. Under 10 loaded cores it
fails **2/20 on this branch and 1/20 at unmodified
`origin/main@02be413b8`** — pre-existing, not caused by this change
(which is inert without the env var). Flagging so it isn't misattributed
to the next PR that's open when CI hits it.

## Docs

`crates/buzz-agent/README.md` is the primary home (env var, recognition
contract, limits, budget interaction). `docs/MCP_DRIVEN_HOOKS.md` gets a
short cross-reference explaining this is *not* a hook — otherwise
readers hunt for a `_ReplyGuard` tool that doesn't exist.

---------

Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
2026-07-31 07:02:48 -04:00
d48b0e0eec feat(desktop): upgrade Pocket TTS model (#3266)
## Context

Buzz Desktop currently installs an older Pocket TTS model bundle. The
current
bundle changes the tokenizer, learned BOS input, recurrent-state
contract, and
prompt behavior, so updating download URLs alone is not compatible.

## Summary

This PR upgrades Buzz Desktop to the current pinned Pocket TTS model. It
preserves existing product behavior and the hard 50-token model-input
limit
while adding the required runtime support, verified acquisition, and
crash-safe
cache migration.

## Changes

- Pins an immutable Pocket TTS revision, artifact names, exact byte
sizes,
  SHA-256 checksums, Mary reference voice, and license.
- Loads the bundle-matched SentencePiece tokenizer, learned BOS
embedding, and
  bundle-declared recurrent states.
- Uses one pinned Pocket TTS configuration; no precision or
model-version
  selector is added.
- Preserves the resident engine's exact `<= 50` token contract without
changing
  Desktop segmentation policy.
- Bumps the Pocket cache manifest to v4, verifies size and checksum
before
adoption, atomically swaps the cache, and recovers the last verified
cache
  after interrupted installs, including an incomplete final directory.
- Keeps acquisition, cache migration, worker adoption, and tests within
the
  existing Desktop implementation.
- Removes the obsolete model-quality harness, which was coupled to the
  superseded production prompt and model layout.

## Related issue

None.

## Testing

Manual listening completed on the exact Desktop build. The updated model
improved speech quality and resolved the phrase-start and sample-onset
artifacts. Reproducible integrity and model checks are below.

## Screenshots

N/A. This changes model installation and speech synthesis, not a visual
surface.

## Reviewer-reproducible examples

### Before and after model identity

```sh
git show 35305bfc8fd456ca9a17caa1ddbfaabd87d46981:desktop/src-tauri/src/huddle/models.rs \
  | grep -E 'sherpa-onnx-pocket-tts|TTS_MODEL_VERSION'

git show 211d17c58567448fe7ac95c4fa0ad2b88378849a:desktop/src-tauri/src/huddle/pocket_models.rs \
  | grep -E 'MODEL_REPOSITORY|MODEL_REVISION|MODEL_PRECISION|MAX_TOKENS'
```

The target branch identifies the January bundle. The PR branch
identifies the
immutable April revision, INT8 precision, and 50-token maximum.

### Deterministic runtime validation

Use the pinned artifacts listed in `pocket_models.rs` and run the
model-dependent Pocket tests with the model directory supplied by the
test environment. The checked-in long-sentence fixture must preserve its
expected 48 and 44 token split and produce non-silent PCM.

### Manual listening validation

John listened to an untrimmed Pocket TTS onset-stress clip generated
from the exact user-provided passage, with every sentence synthesized
separately and identical 100 ms digital-silence boundaries. The clip
used no leading period, onset trimming, gain adjustment, or loudness
normalization.

The updated model produced better-quality speech and resolved the
start-of-sample artifacts.

---------

Signed-off-by: John Tennant <jtennant@block.xyz>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: John Tennant <jtennant@block.xyz>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
2026-07-31 07:00:15 -04:00
d88313f369 feat(desktop): delete a message by clearing its edit to empty (#3813)
## What

Clearing an edit to empty and hitting accept now **deletes the message**
instead of hanging. One of Sam's frequent workflows is to delete a
message by editing it, clearing the text, and pressing Enter — which
previously no-op'd (a deliberate guard blocked empty edits).

## How

Pure client-side wiring — **no relay, schema, or Rust changes.**

1. **`MessageComposer.tsx`** — the edit path had a guard that *blocked*
empty edits (`if (!trimmed && !hasMedia) return;`). That guard is simply
**removed**, so empty content flows through the normal edit path to
`onEditSave("", [], [])`. `buildOutgoingMessage("")` is a safe no-op.
2. **`handleEditSave` in `useChannelPaneHandlers.ts`** — when an edit is
submitted with empty text and no media tags, it exits edit mode and
opens the **same "Delete message?" confirmation** the Delete menu action
shows, rather than publishing an empty edit.
3. **`DeleteMessageConfirmDialog.tsx`** — the confirmation dialog,
extracted into **one shared component**. `MessageActionBar` renders it
for the Delete menu action (previously inline), and `ChannelScreen`
renders it for the empty-edit path. No duplicated dialog UI. **Delete**
runs the existing `deleteMutate`; **Cancel** leaves the message
untouched.

Because both the main timeline and the thread panel already route
edit-save through `handleEditSave`, this covers both surfaces with a
single dialog at the `ChannelScreen` level — no per-composer plumbing.

- Image-only edits (empty text but attachments present) still publish
normally — only a *fully* empty edit prompts to delete.
- An empty edit can never publish an empty body: `handleEditSave`
returns before the edit mutation.

## Review history

This PR was reworked three times in response to review — each pass made
it smaller:

1. First cut wrapped this in a new "Delete message?" `AlertDialog`
rendered from a composer hook — a verbatim duplicate of the confirmation
already in `MessageActionBar.tsx`. Removed.
2. Second cut threaded a dedicated `onDeleteEditTarget` callback down
`ChannelScreen → ChannelPane → MessageComposer / MessageThreadPanel`.
Also redundant — the delete decision moved entirely into
`handleEditSave`, which every edit-save already flows through.
3. Third cut added a special-case empty branch to the composer, which
pushed `MessageComposer.tsx` over the file-size ratchet and led to an
unrelated emoji-helper extraction to make room. Both gone: deleting the
pre-existing guard (rather than adding a branch) is net-negative, so
there's no ratchet pressure and **nothing emoji-related in this PR**.
`MessageComposer.types.ts` is back to baseline too.
4. Fourth pass (this one): an unconfirmed, no-undo delete was too sharp.
The empty-edit path now routes through the same **"Delete message?"
confirmation** as the menu action — shared as one
`DeleteMessageConfirmDialog` component (so it's reuse, not the duplicate
dialog from cut #1).

## Testing

- **E2E:** `desktop/tests/e2e/empty-edit-delete.spec.ts` (Playwright,
smoke project), three tests, all passing locally:
- *clearing an edit to empty prompts to delete, then deletes on confirm*
— edits the mock identity's own `#general` message, clears it, Enter →
the **"Delete message?"** dialog appears; Delete → the row disappears
and edit mode exits.
- *cancelling the empty-edit delete keeps the message* — same up to the
dialog, then Cancel → the message survives.
- *a non-empty edit still edits and never deletes* — guards the other
direction (no dialog).
- `pnpm typecheck`, biome, file-size + px-text guards all clean; full
desktop unit suite (3847 tests) passing locally.

> Heads-up for the reviewer: pushed with `--no-verify` because the
pre-push hook runs the Rust **integration** suite, which needs Docker
(Postgres/Redis) that isn't available in this environment — it doesn't
apply to this desktop-only change. CI runs the real gates.

---

🐝 Built by Bumble in Buzz, from a conversation in #test-swesterman.

---------

Signed-off-by: Sam Westerman <swesterman@squareup.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 22:34:12 -07:00
10d5a26414 feat(relay): raise hosted community limit to five (#3829)
## Summary
- raise the relay authoritative default community ownership limit from 3
to 5
- raise the desktop hosted-community treatment from 3 to 5
- preserve `BUZZ_MAX_COMMUNITIES_PER_OWNER` as a deployment override

## Validation
- `pnpm -r check`
- `cargo fmt --all -- --check`
- `cargo test -p buzz-db` (94 passed, 151 Postgres-dependent tests
ignored)
- pre-push hooks: desktop checks/tests, Rust tests, Tauri checks (all
passed; 1,995 desktop Rust tests passed)

Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-07-30 18:20:58 -07:00
468647a51f feat(desktop): locally stored NIP-49 encrypted key backup (#2937)
## Summary

Adds a locally stored **NIP-49 encrypted key backup** (`ncryptsec`) to
the desktop app, per the plan reviewed in buzz-development (Rev 3,
approved 9/10 by Wren; implementation also reviewed and approved 9/10).

**Two-artifact design — canonical bytes originate entirely in Rust:**
- `create_ncryptsec_backup` runs under the `identity_mutation` lock:
encrypt → decrypt-verify against the live pubkey → atomic `0o600` write
to `{app_data_dir}/identity.ncryptsec` → reread/byte-compare → return
the exact persisted bytes. The frontend never re-derives or re-encrypts.
- `save_ncryptsec_copy` writes a portable copy via the save dialog
(parse-gated, secret-file semantics) and never mutates canonical state.
- `generate_backup_passphrase`: 6 words from the EFF short wordlist via
`OsRng` (custom passphrases min 12 chars).
- Import accepts `ncryptsec1` with optional password; the raw-`nsec`
path is untouched. Different-pubkey import and sign-out wipe the
app-managed backup (post-commit, best-effort — a failed import can never
destroy the still-live identity's backup; regression-tested).

**Never-relay guarantee (egress guard + tripwires):**
- `egress_guard.rs` fail-closed at all 8 `/events` submission boundaries
(relay submit funnel, 3× `relay.rs`, huddle STT, both engram submitters,
native WS choke point), rejecting `ncryptsec1`/`NCRYPTSEC1` in text and
binary frames. Scope is deliberately ncryptsec-only: pairing
intentionally carries raw nsec inside its encrypted session.
- Site-granular `/events` inventory tripwire: per-file (`/events` count,
guard-call count) pairs; unlisted files expect zero. Mutation-style
tests prove a ninth site in an existing file, a removed guard, and a new
unlisted file all fail the scan.
- ncryptsec source-allowlist scans in **both** trees (Rust + TS).

**Frontend:** onboarding `BackupStep` is encrypted-by-default — the
default path never invokes `get_nsec` (e2e asserts the command log).
Raw-nsec export stays behind an explicit click with prior semantics.
Shared `EncryptedBackupCreator` powers onboarding + a new settings row;
the import form auto-switches to encrypted mode on `ncryptsec1` paste
(case-insensitive HRP).

**Open product call for @tlongwell-block:** onboarding default is
*encrypted* in this PR; flipping to raw-default is a small change either
way (documented in the plan).

Review history: plan Rev 3 and the implementation were both iterated
with Wren to 9/10 (two blockers from round 1 — import ordering,
inventory granularity — plus an uppercase-bech32 hardening gap, all
fixed in `dde37183e`). Thread: buzz-development.

### Related issue

Follow-up to the direction explored in #385 (NIP-PB, closed) — this
ships local NIP-49 (the standard) instead of a new NIP. No open
duplicate found.

### Testing

All at exactly `dde37183e` (same shell, HEAD verified):

- `cargo test` — 1680 passed / 0 failed / 14 ignored (includes a
deliberate ~70s log_n-18 NIP-49 round trip, spec vector, wrong-password,
NFKC, uppercase-vector decrypt, injection test per egress boundary,
inventory mutation tests, import-ordering regression tests)
- `cargo clippy --all-targets -- -D warnings` — clean; `cargo fmt
--check` — clean
- `pnpm typecheck` — clean; JS unit suite 3529/3529; biome (repo-pinned
2.4.16) clean
- Playwright `onboarding-backup` / `onboarding` /
`onboarding-agent-defaults` / `profile-nsec-reveal` — 86 passed, 1 known
avatar-reservation flake (passed on rerun; untouched by this diff).
`passThroughBackupStep` now exercises the encrypted default, so every
downstream onboarding spec covers the new path.
- Note: browser e2e fakes the crypto via the mock bridge (fixed
spec-vector blob); decryption correctness is proven in the Rust tests.

## Latest onboarding integration

The current head adds an additive `IdentityInfo.storage` field
(`ephemeral`, `system-keyring`, `local-file`, or `environment`) so
onboarding can accurately explain where the active identity is
protected. It surfaces storage metadata only—never key material—and
leaves the existing lost/keyring-locked recovery behavior intact.

---------

Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
2026-07-30 20:58:03 -04:00
f3e5e81267 fix(catalog): update Amp tagline (#3806)
## Summary

Update Amp's runtime catalog description to use its current tagline:

> The coding agent and development environment that runs anywhere and
everywhere.

### Related issue

N/A. This follows the Amp description update in
https://github.com/block/buzz/pull/3758.

### Testing

* `pnpm -C desktop check`
* `pnpm -C desktop typecheck`
* `pnpm -C desktop test` (3,835 passed)

No screenshot is included because this changes only the catalog
description text. It does not change layout or interaction behavior.

Signed-off-by: AJKemps <AJKemps@users.noreply.github.com>
Co-authored-by: AJKemps <AJKemps@users.noreply.github.com>
Co-authored-by: Alex Kemper <alex@ampcode.com>
2026-07-30 22:54:41 +00:00
9e8fcfda09 fix(desktop): channel topic and membership metadata cleanup (#3642)
First slice of #2216, scoped to the system/status lines in the chat
timeline.

## Why

Two problems on the same surface.

**Clearing a channel topic renders as empty quotes.** The relay reports
a clear as a `topic_changed` event carrying an empty string — there's no
separate "cleared" event type. So the timeline printed:

> Alice
> changed the topic to “”

which reads as if the topic were *set to* two quote marks. Same for
purpose.

**The membership caption reads like a headline, not a metadata line.**
`title` and `action` render on separate lines — the member's name sits
in the header row with the avatar and timestamp, and the caption sits
beneath it. So the caption was "was added by Alice Chen" standing alone
under a name, while its siblings on that same line are "joined the
channel" and "left the channel".

## What

- Blank, missing, or whitespace-only topic/purpose now reads **"cleared
the channel topic"** / **"cleared the channel purpose"**.
- Membership captions drop "was": **"added by Alice Chen"**, matching
"joined the channel" and "left the channel".
- The wording moves to `lib/systemEventCopy.ts` as a pure function, so
it's assertable in a unit test instead of only reachable through the
DOM. That also removes two JSX fragments from `SystemMessageRow.tsx`,
taking it 911 → 900 lines.

## Two E2E assertions this exposed

Both were measuring something other than what they claimed, and the copy
change tipped them over. Neither is a product bug, but both would have
failed the next person too.

1. **`mentions.spec.ts:1245`** asserted a button was un-underlined while
the mouse was still parked from a previous `hover()`. Any reflow — new
rows, scroll-to-bottom, a different text wrap — can slide that button
under the stationary pointer, so the assertion measured *where the mouse
happened to be* rather than the resting style. Dropping four characters
changed the text wrap, changed the row height, changed the scroll
offset, and the pointer landed on it. Now parks the pointer off-target
first.
2. **`mentions.spec.ts:1253`** used a bare `role=tooltip` lookup. Once
the first tooltip animates out while the second opens, two elements
match and strict mode trips. Now scopes to the open tooltip via
`:not([data-state="closed"])`.

## Deliberately out of scope

- **Timestamps.** The day divider, per-message clock times, the Inbox
thread pane, and the inbox list have three divergent date
implementations and none fully match the writing standard's
Today/Yesterday/weekday/date progression. That's its own slice of #2216.
- **Whose avatar shows.** An addition puts the *added* member in the
header; a removal puts the *remover* there. Possibly intentional, but
it's a design question, not copy.
- **`the channel` vs `this channel`.** joined/left/removed say "the
channel"; created/archived/unarchived say "this channel". Worth
normalizing, but it touches lines this PR otherwise leaves alone.

## Validation

- `pnpm check`, `pnpm typecheck` — clean
- Unit: **3781/3781**, including 6 new tests in
`systemEventCopy.test.mjs` covering set/blank/undefined/null/whitespace
for both fields, plus a guard that no variant can emit empty quotes
- Smoke E2E `mentions` + `messaging`: **85/85**
- The previously fragile test run with `--repeat-each=5`: **5/5**

Signed-off-by: Clay Delk <clay.delk@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:53:04 -07:00
Bradley AxenandGitHub ede2686334 fix(desktop): align data deletion labels (#2230)
## Why

The Profile settings action still says “Sign Out,” while its
confirmation action says “Delete My Data.” Both buttons trigger the same
destructive local-data wipe and should name it consistently.

## What

- Label both destructive actions “Delete my data”
- Assert the matching section and confirmation labels in the existing
Playwright coverage

## Risk Assessment

Low — copy and test assertions only; sign-out behavior is unchanged.

## References

- Follow-up to #2208
- #2216 also touches this copy and should preserve “Delete my data” when
rebased
- `just desktop-check`
- `just desktop-test` (3,275 tests)
- Desktop E2E build and sign-out Playwright spec (2 tests)

Generated with Codex

Signed-off-by: Bradley Axen <baxen@squareup.com>
2026-07-30 15:47:35 -07:00
36571f4adc fix(desktop): allow linux-only media items as dead code off-linux (#3811)
Local `desktop-tauri-clippy` fails on macOS with dead-code errors for
`PROD_ORIGIN`, `DEV_ORIGIN`, and `is_trusted_media_origin`, which are
only used inside `#[cfg(target_os = "linux")] enable_media_capture`. The
items are intentionally platform-independent so unit tests run
everywhere. Added `cfg_attr` allow attribute to suppress the warnings on
non-Linux targets.

Since [#3607](https://github.com/block/buzz/pull/3607), this affects all
Rust developers on macOS.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78 <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
2026-07-30 18:11:03 -04:00
74cd571219 fix(desktop): report authenticated relay recovery (#3812)
## Summary

- report the relay as connected immediately after socket open and
successful AUTH
- keep rate-limited subscription replay, the connect promise, and
reconnect listeners unchanged
- cover authenticated reconnect while replay is held behind the shared
rate-limit gate

## Why

After WARP recovery, the socket could reopen and authenticate
successfully while subscription replay waited behind the existing
rate-limit gate. `connect()` kept `ConnectionState` at `reconnecting`
during that intentional delay, so the desktop displayed “Can’t reach the
relay” despite authenticated traffic already flowing.

This is separate from #3774: that fix keeps routine operations from
bypassing scheduled reconnect backoff. This patch preserves those
protections and only corrects the authenticated transport-state
boundary.

## Failure semantics

If replay fails after the early `connected` transition, the existing
`replayLiveSubscriptions()` catch calls `resetConnection()`, closes the
socket, returns state to `reconnecting`, and schedules recovery.
Operation waiters and reconnect notifications still do not complete
until replay succeeds.

## Validation

At commit `c8a4308e1079f4f9e6a72f0f0bfba280fe822ec0` with a clean
working tree:

- `pnpm --dir desktop typecheck`
- `pnpm --dir desktop test` — 3,847 passed
- `pnpm --dir desktop check` — passed; two pre-existing informational
template-literal notices
- `pnpm --dir desktop exec playwright test
tests/e2e/relay-reconnect.spec.ts` — 8 passed
- regression test proven red before the production ordering change
(`reconnecting` after 3 seconds) and green after it

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-07-30 22:04:14 +00:00
Sumit MadanandGitHub 29dfe4821e fix(desktop): don't gate hover affordances on the hover media query (#3657)
## What problem this solves

Tailwind v4 compiles every `hover:` variant inside `@media (hover:
hover)`. Some
Windows hosts answer that query `false` **even with a mouse attached**,
and then
every hover-revealed control in the app is permanently `visibility:
hidden`.

Measured in the app's own WebView2 devtools console, on a mouse-driven
Windows 11
desktop:

```js
matchMedia('(hover: hover)').matches      // false
matchMedia('(any-hover: hover)').matches  // false
matchMedia('(pointer: fine)').matches     // false
matchMedia('(any-pointer: fine)').matches // false
navigator.maxTouchPoints                  // 10
```

Windows itself, on the same machine at the same moment, reports a mouse
present
and an integrated digitizer:

```
GetSystemMetrics(SM_DIGITIZER)      = 197   // INTEGRATED_TOUCH | INTEGRATED_PEN
                                            // | MULTI_INPUT | READY
GetSystemMetrics(SM_MAXIMUMTOUCHES) = 10
SystemInformation.MousePresent       = True
```

So this is not "the user has no mouse". Windows knows a mouse is
attached, and
Chromium still reports `any-pointer: fine: false` and `any-hover: false`
— the
`any-*` queries exist precisely to describe *any* available input
device, and
they are wrong here. The presence of an integrated touch digitizer
collapses the
reported capability to touch-only.

The compiled rule that never applies:

```css
.group-hover\/member\:visible {
  &:is(:where(.group\/member):hover *) {
    @media (hover: hover) { visibility: visible; }
  }
}
```

The row genuinely matches `:hover` (verified: `row.matches(':hover') ===
true`),
the button is in the DOM, the utility class is generated — and the
declaration
still never lands.

## Why this is more than one control

Not a single menu. Confirmed newly-ungated in the production bundle
after the
change:

| utility | media-gated before | after |
|---|---|---|
| `group-hover/member:visible` | yes | no |
| `group-hover/inbox-item:opacity-100` | yes | no |
| `group-hover/channel-row:opacity-100` | yes | no |
| `group-hover/attachment:opacity-100` | yes | no |
| `hover:bg-muted` | yes | no |

On an affected host the channel-member action menu (remove member,
change role,
start/stop agent) has **no reachable affordance at all**: `visibility:
hidden`
also removes the button from tab order, so there is no keyboard path
either.

## The fix

One line, at the root, next to the existing variant override:

```css
@custom-variant hover (&:hover);
```

This trusts the actual hover event rather than the capability query.
Chromium
only fires `:hover` when a real pointer is present, so behaviour on
hosts that
report the capability correctly is unchanged.

Verified against a production `vite build`, not just the dev server —
the
override cascades to the *named* group variants (`group-hover/member`,
etc.),
which is the part that matters here.

## Prior art in this repo

#2849 overrides Tailwind v4's `dark:` variant default at the *exact same
insertion point* in this file, for the same class of reason (a v4
default that
does not match how this app actually works). This change follows that
precedent.

**Note for whoever merges second: #2849 and this PR will conflict
textually** —
both append a `@custom-variant` immediately after `@config`. The
resolution is
to keep both lines; they are independent.

## Scope

Desktop only. `web/src/shared/styles/globals.css` has the same Tailwind
v4
default, but `web/src` contains **zero** `group-hover` usages, so there
are no
hover-revealed affordances to strand there. Adding the override to web
would be
speculative.

One `hover` capability query is deliberately left in place —
`.buzz-wave-hover-trigger` in `animations.css` gates a decorative
wave-hand
animation on `(hover: hover) and (pointer: fine)`. That is a cosmetic
flourish
rather than an affordance, so it stays inert on affected hosts instead
of
widening this diff.

## Reproducing

The trigger is **an integrated touch digitizer anywhere on the
machine**, not the
display you are actually working on. This was found on a touch-capable
laptop
docked to an ordinary non-touch external monitor, driven entirely by a
mouse — so
"I'm on a desktop monitor" does not rule you out. Check with:

```js
matchMedia('(hover: hover)').matches   // false ⇒ affected
```

Not reproducible on macOS, or on a Windows machine with no digitizer at
all —
`hover: hover` is true there and every affordance works normally. If you
are on
such a host, emulate it in devtools by forcing `hover: none` / `pointer:
coarse`,
then open a channel's member list and hover a row: no action menu
appears.

## Tradeoff worth naming

On a genuine touch-only device, a bare `&:hover` can latch after a tap
and stay
applied until the next interaction, where the media-query default would
have
suppressed it. That is the real cost of this change.

The judgement here is that a stuck hover style is a cosmetic annoyance,
while an
unreachable "remove member" button is a functional dead end — and that
the
affected hosts are overwhelmingly mouse-driven machines that merely
*happen* to
ship a digitizer, as the `MousePresent = True` reading above shows. If
you would
rather scope this to `@media not (hover: hover)` as an additive fallback
instead
of overriding the variant, I am happy to rework it.

Signed-off-by: sumit-m <33051892+sumit-m@users.noreply.github.com>
2026-07-30 18:00:22 -04:00
Will PflegerandGitHub 114d40d9d3 feat(relay): gate kind 30178 team-catalog reads behind the shared tag (#3358)
Team catalog projections (`kind:30178`) embed every member's system
prompt, so they need the same read gate personas already have: only the
author sees an unshared event. The gate was hardcoded to `kind:30175` at
six read surfaces plus the SQL pushdown, so rather than adding a second
special case it becomes kind-generic over `SHARED_GATED_KINDS = {30175,
30178}`.

## Kind 30178

New parameterized-replaceable kind, addressed by `(pubkey_o, 30178,
team_id)`. It embeds sanitized member projections instead of referencing
`kind:30175` heads — a foreign reader of a shared team could not
otherwise hydrate members whose own persona events are unshared or, for
built-ins, absent entirely. `kind:30176`'s wire body is untouched, so
device sync keeps its contract.

## Kind-generic shared gate

`buzz_core::kind` replaces `is_persona_shared_kind` /
`is_unshared_persona_event` / `persona_event_is_shared` with
`SHARED_GATED_KINDS` and the kind-agnostic `is_shared_gated_kind` /
`is_unshared_gated_event` / `event_is_shared`. Every read surface
consults the set:

| Surface | File |
|---|---|
| REQ historical delivery + `ids` lookup |
`crates/buzz-relay/src/handlers/req.rs` |
| Live fan-out | `crates/buzz-relay/src/handlers/event.rs` |
| COUNT fallback | `crates/buzz-relay/src/handlers/count.rs` |
| NIP-98 HTTP `/query`, `/count`, `/search` |
`crates/buzz-relay/src/api/bridge.rs` |
| Pre-`LIMIT` SQL pushdown | `crates/buzz-db/src/event.rs` |

The SQL clause generalizes from `kind != 30175` to `kind NOT IN (...)`
bound from `SHARED_GATED_KINDS`, still applied before `ORDER BY … LIMIT`
so a page of newer private events cannot starve an older shared one off
the candidate set. `EventQuery::persona_reader` is renamed
`shared_gated_reader` and `needs_persona_filtering` to
`needs_shared_gate_filtering` to match.

Because the `buzz-core` rename has consumers outside the relay, the four
desktop call sites of `persona_event_is_shared` travel with it:
`desktop/src-tauri/src/commands/personas/pending.rs`,
`desktop/src-tauri/src/event_sync.rs`, and two in
`desktop/src-tauri/src/managed_agents/persona_events.rs`. Each call is
unchanged apart from the name — the persona `shared` projection behaves
exactly as before.

## Ingest validation

`validate_persona_envelope` splits into two reusable pieces —
`validate_shared_tag` (exactly-two-element `["shared","true"]`, at most
one occurrence) and `single_bounded_d_tag` (exactly one `d` tag,
non-empty, `<=64` chars, no ASCII control characters or whitespace).
`validate_team_catalog_envelope` composes both; personas additionally
keep the slug grammar `^[a-z0-9][a-z0-9_-]{0,63}$`.

`kind:30178` deliberately does **not** get the slug grammar. Team ids
are UUIDs or built-in identifiers such as `builtin-team:welcome`, and
the colon is not slug-legal; rewriting ids to fit would break NIP-33
addressing against the team's own `kind:30176` head. The non-empty and
exactly-one checks are load-bearing regardless — without them generic
NIP-33 storage maps a missing `d` onto `(pubkey_o, 30178, "")` and every
team overwrites its predecessor.

The exact two-element `shared` shape is enforced because the SQL
visibility clause is JSONB containment (`tags @>
'[["shared","true"]]'`), which would match a three-element superset such
as `["shared","true","extra"]`.

`kind:30178` is also added to the `Scope::UsersWrite` allowlist and to
`is_global_only_kind`, so a stray `h` tag cannot channel-scope an
owner-authored definition.

## Deferred

`kind:30176` is deliberately not a gate member. Its writers never emit
`shared`, so catalog opt-in semantics do not describe it — it needs
owner-private reads driven by an authenticated principal set, tracked as
a separate follow-up.

## Tests

- 19 new `ingest.rs` unit tests covering the 30178 envelope (UUID and
colon `d` tags, 64-char boundary, non-ASCII bound,
empty/valueless/duplicate/missing `d`, embedded newline, `shared`
false/three-element/duplicate, scope and global-only membership).
- Persona regressions for the valueless `["d"]` shapes, since the
`d`-tag helper is shared by both validators.
- Existing `kind.rs` gate tests generalized and extended to assert the
gate applies to 30178 as it does to 30175.
- New `crates/buzz-test-client/tests/e2e_team_catalog.rs`: 9 WS-level
tests over a live relay covering author reads of unshared heads, foreign
omission from REQ, `ids`-lookup denial, COUNT existence-leak, share and
unshare transitions, and the mixed-kind filter case.
- `.github/workflows/ci.yml` adds `--test e2e_team_catalog` to the Relay
E2E job so the new suite runs.

## Docs

`docs/nips/NIP-AP.md` gains a "Team catalog projection: kind:30178"
section and an "Ingest validation: kind:30178" subsection, records the
gate as kind-generic, documents 30178 deletion vs. unshare semantics,
and adds a security note that sharing a team exposes every member's
instructions even when that member's own `kind:30175` head is unshared.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-07-30 17:53:30 -04:00
b9e4ed616f test(desktop): click visible thread collapse guide (#3800)
## Summary

- target the visible thread branch collapse guide in the messaging smoke
test
- avoid clicking the underlying collapse rail when the guide overlaps it
- retain the existing post-click assertions that verify the two-reply
branch collapses

## Context

`main` CI failed because Playwright repeatedly attempted to click the
lower `thread-collapse-rail` while the matching `thread-collapse-guide`
intercepted pointer events. Both controls dispatch collapse for the same
branch; the guide is the actual topmost user target and is already used
by `thread-unread.spec.ts`.

Failing run: https://github.com/block/buzz/actions/runs/30575425126

## Validation

- focused Playwright smoke test: 1 passed
- pre-push hooks: desktop check passed; 3,835 desktop tests passed
- `git diff --check`

## Review

Princess Donut reviewed the test-only approach and locator determinism
with no blockers. Mongo review is pending.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-07-30 14:50:51 -07:00
d40a33290e feat(desktop): raise the install ceiling and make installs observable (#3368)
Windows installs of Goose and other harnesses failed at exactly five
minutes with an empty error (#2401). The 300s ceiling was killing
installs that were working, just slowly — the Goose step pulls a ~79MB
release asset, and Windows Defender scans every file npm extracts. When
the ceiling fired it discarded the output it had already read, so the
user got a bare timeout string and no way to tell a hang from a large
download.

## The ceiling

`INSTALL_TIMEOUT` is 900s, and the error names the limit: `install
command exceeded the 15m ceiling and was terminated`. It stays a pure
wall-clock ceiling with no inactivity kill — nothing observable
distinguishes a hung installer from one silently transferring a large
artifact, so silence alone never kills an install. A ceiling kill
remains non-retryable; re-running a command that already burned 15
minutes costs the user more time with no plausible path to success.

The child's exit and both stream drains fold into one resumable settle
governed by a single deadline. Waiting on the drains outside that
deadline would let a descendant that outlived the install shell hold the
output pipes — and the per-runtime install guard behind them — open with
no bound, which is the failure the ceiling exists to prevent. So the
deadline path terminates the process group on the normal-exit branch
too: a leader that exited with a real status still gets its stragglers
killed, and the guard cannot stick either way. Whether the leader had
already exited only decides the verdict — its real status outranks a
timeout.

The install shell is a session leader and its descendants inherit the
output pipes, so signalling only the leader left them running and the
drains blocked on a pipe nobody would close. Escalation keys off the
*group's* liveness rather than the leader's, since a descendant that
ignores SIGTERM outlives the leader and would otherwise never receive
the group SIGKILL. Reaping the killed child and finishing the drains
share one bounded grace, so a termination that failed outright cannot
extend the ceiling that just fired.

## Output capture

Each stream drains into a bounded capture that is *shared* with the
reader rather than returned by it, so whatever arrived before a stall is
readable at the ceiling — exactly when the output matters most. Output
of any size costs a fixed amount of memory.

One capture holds two independently bounded views of the same bytes:

| View | Head / tail | Cut marker |
|------|-------------|------------|
| UI (`InstallStepResult`) | 512 B / 1024 B | `... (N bytes omitted)
...` |
| Log file | 128 KiB / 128 KiB | `... [N bytes omitted at cap] ...` |

The UI budget is screen space; the log's is disk. Both markers are
inline, so neither ever implies completeness it does not have. Both ends
are cut at arbitrary byte offsets, so a partial character is trimmed and
the partial token each cut left behind is dropped — the marker's byte
count includes both trims.

## Install log

`steps` carries only the last attempt of each step, truncated for
display. Everything else — earlier retries, the prerequisite step that
actually broke, the managed-Node bootstrap — used to be discarded.
`InstallReporter` now appends one self-contained record per attempt of
per step to `install-<runtime-id>.log` beside the agent logs, and
`InstallRuntimeResult.log_path` carries the file to the UI, where a
failure message ends with `Full log: <path>`.

Each record is bounded independently by the log-scale capture that
produced it, so a first attempt that printed megabytes cannot push out
the later record explaining the failure; the run's total is bounded by
steps × attempts × per-record cap. Every early return builds its result
through one `InstallReporter::failed` helper, so no failure path can
omit the log pointer, and synthesized steps go through `record_step` — a
step that reaches the UI without passing it would be invisible in the
file.

Install output can echo a registry token or proxy credential from the
environment it ran in, and the file is written unattended. Redaction
keys off the *names* of the environment variables the install inherited,
snapshotted once per run, rather than a list of known secret value
prefixes: a credential with no recognisable shape is exactly the one a
prefix match misses. Three name rules apply, because the variables need
different treatment:

| Rule | Variables | Redacted |
|------|-----------|----------|
| URL userinfo | `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`,
`NPM_CONFIG_PROXY`, `NPM_CONFIG_HTTPS_PROXY`, `NPM_CONFIG_REGISTRY` |
`user:password` only |
| Exact name | `NPM_CONFIG_KEY`, `NPM_CONFIG__AUTH`, `NPM_CONFIG_OTP` |
whole value |
| Marker substring | `*TOKEN*`, `*SECRET*`, `*PASSWORD*`, `*_PAT`, … |
whole value, 8-byte floor |

A proxy or registry keeps its host and port, because an install that
fails behind one is diagnosable only if the record still says which one
it went through, and a bare `user@` with no password is not treated as a
credential. npm's own settings are listed by exact name rather than
matched on `KEY` or `AUTH` substrings — both occur throughout an
ordinary environment on values that are paths and people's names — and
they bypass the 8-byte floor, since a six-digit one-time password is a
credential at that length. Matching is case-insensitive, which is what
npm's lowercase `npm_config_*` spelling needs. `0o600` is set by the
create rather than a later `chmod`, which would leave a window where the
umask decides. A runtime id that cannot safely be a filename yields no
log rather than a sanitized one — a rewritten id could collide with
another runtime's log.

The file holds exactly one run. A run opens its own session after the
runtime id has been canonically resolved — the previous file rotates to
`.1` and any older `.1` is removed before the rename, since a rename
that will not replace its destination would otherwise wedge rotation
permanently on Windows. The session writes a header naming the runtime,
the app version (`app.package_info().version` on the Rust side — cannot
be mocked or fail), the OS (`std::env::consts::OS`), and the start time:
a Windows failure and a macOS one on the same runtime are different
bugs, and a stale app version explains a failure that no longer
reproduces. Each record carries its attempt's elapsed time.

## Live output line

A 15-minute ceiling with nothing behind it but a spinner is
indistinguishable from a hang. The same drain seam feeds an
`acp-install-output` event carrying the newest complete line, and the
three install entry points — Doctor harness rows, the harness catalog
dialog, and onboarding runtime cards — render it under the spinner with
`aria-live="polite"`.

Ordering is keyed on a `seq` monotonic across the whole install, not on
the attempt number, which restarts at 1 for every step: keyed on
attempt, one step succeeding on attempt 2 would make the next step's
attempt-1 output look stale and freeze the display for the rest of the
install. Each executed attempt begins with an unthrottled `line: null`
clear signal, so a stale failure line cannot sit under the spinner while
the retry runs. Events are otherwise throttled to four per second, and
the throttle *retains* the newest pending line and flushes it when the
window reopens rather than dropping it — at an attempt boundary a drop
would silently eat the new attempt's first line.

The subscription is mounted for the runtime's whole lifetime rather than
started when the install begins. The install command is invoked from the
click handler, so the clear and a fast command's first lines can be
emitted before React has committed the pending state, and nothing
replays them — a subscription that waited for that state would lose the
entire output of a short install. The run boundary resets the ordering
key when the install settles, since `seq` restarts for the next run, and
the line renders only while installing, so a straggler from a finishing
drain cannot appear under a fresh Install button.

The 15-minute ceiling deliberately stops waiting on stuck drain threads
— a hung installer must not freeze the app. That means a drain thread
can outlive its `InstallReporter`. Without a generation guard, a drain
that calls `offer` after the run settles would publish an event with the
run's high `seq`, poison the permanent listener's React state, and cause
the next install's restarted `seq=0` events to be rejected. `Live` now
carries a `lifecycle: Arc<RwLock<bool>>`; drain threads hold a **shared
read guard** from the admission check through the `(self.emit)(...)`
call, making the check-then-emit pair atomic with respect to shutdown.
`InstallReporter::drop` takes the **exclusive write guard** and stores
`false` — this blocks until every in-flight drain publication releases
its read guard, then prevents any new admission. Deactivation is
bounded: the write lock holds only for the flag store, so it can block
at most for the duration of one emit call (microseconds to low
milliseconds). Rust drops locals in reverse-declaration order, so
`reporter` drops before `_guard`, ensuring the exclusive write completes
before the per-runtime concurrency guard releases and a new install can
start.

## Also

Install result types move to `desktop/src/shared/api/installTypes.ts`,
following the existing `searchTypes.ts` / `workflowTypes.ts` convention,
and are re-exported from `tauri.ts` and `types.ts` — both already over
the file-size cap, so neither can grow to carry them.

Two comments described `AdapterOutdated` as applying only to the
deprecated package; it also covers a version below the supported floor.

Report: #2401

---------

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-07-30 17:07:22 -04:00
fenner888andGitHub 1b3ff96a57 Add Devin as a preset ACP harness (#3225)
## Summary

- Add Devin to the built-in preset harness catalog using the official
native ACP invocation: `devin acp`.
- Link setup guidance to Cognition's official Devin CLI documentation.
- Render a bundled, attributed Devin mark on a white canvas through
Buzz's existing runtime-icon system.
- Keep preset capability metadata in the Rust catalog; no duplicate
TypeScript runtime table or React runtime checks.
- Move the existing preset catalog and its focused tests into a Rust
submodule without changing existing preset behavior, keeping the touched
files within the repository's file-size limit.

### Related issue

Follow-up to the generic BYOH harness work in #2773.

### Scope

This is the small preset/data-entry follow-up described in the #2773
discussion. It uses the generic preset readiness contract and does not
add Devin-specific authentication probing, permission bypasses, model
switching, cloud handoff, or cloud Devin capability claims.

The preset supplies:

- ID: `devin`
- Executable: `devin`
- Arguments: `acp`
- Installation guidance: https://docs.devin.ai/cli

### Testing

Local verification was rerun at the final PR head,
`7bb9aa6e862a47a5062b5b8234fdb5ce2aae6c1d`.

- Focused Rust preset tests: 7 passed
- Desktop JavaScript tests: 3,768 passed
- Desktop lint, formatting, file-size, and text guards: passed
- Full Tauri test suite: 1,851 passed, 14 ignored
- Root Rust unit-test groups: passed
- Web production build: passed
- Mobile format, analyze, and test suites: passed
- Full repository `just ci`: passed

The branch also merges cleanly with the current Block `main`. The
upstream fork-triggered CI workflow is awaiting maintainer approval;
DCO, Semgrep OSS, and zizmor are passing.

The bundled SVG was rendered and visually inspected in both its source
dimensions and a 512px preview. The cross-language preset-logo guard
verifies that the Devin mapping exists and the asset is present on disk.

Signed-off-by: Mark Fenner <markfenner57@yahoo.com>
2026-07-30 16:58:28 -04:00
4d47aa8345 feat(desktop): improve agent activity header ui (#3321)
**Category:** improvement
**User Impact:** Activity feeds now clearly identify the agent and keep
update recency visible even when channel names are long.

**Problem:** The activity header led with a generic label, making it
hard to tell which agent was in view, while channel scope and recency
competed for limited horizontal space. Long channel names could hide the
update timestamp entirely.

**Solution:** Lead with the resolved agent avatar and name, then place
mode and scope in a truncating metadata region with recency pinned at
the right edge. This preserves the compact two-line header while keeping
the most important identity and freshness signals legible.

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

**desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx**
Reorganizes the activity header around the agent identity, reuses the
existing resolved profile avatar and label helpers, and separates scope
truncation from the always-visible recency label.

**desktop/tests/e2e/activity-scope-label-screenshots.spec.ts**
Expands activity-header coverage across channel-scoped, all-channel,
raw, long-name, and narrow layouts, including measured truncation and
recency visibility.

</details>

## Reproduction steps

1. Open an agent's activity feed from a channel.
2. Confirm the agent avatar and name lead the header.
3. Open a feed scoped to a channel with a long name and resize the panel
narrowly.
4. Confirm the mode and channel scope truncate while the recency label
remains visible at the right edge.
5. Toggle Raw mode and open an all-channel feed to confirm the same
hierarchy and truncation behavior.

## Screenshots

| Long channel | Narrow layout |
|---|---|
| <img width="380" height="671" alt="image"
src="https://github.com/user-attachments/assets/19682aac-9938-41ed-8c27-fe59bf8b7535"
/> | <img width="371" height="771" alt="image"
src="https://github.com/user-attachments/assets/92a6fc05-c9ba-4c11-a18c-22b5225d8b9a"
/> |

| Raw mode | All channels |
|---|---|
| <img width="380" height="671" alt="image"
src="https://github.com/user-attachments/assets/c8135600-e3c9-4644-8350-fa5f6b2d3aaa"
/> | <img width="380" height="671" alt="image"
src="https://github.com/user-attachments/assets/bac73a6a-4ed5-42d6-98cc-039a75c48ef3"
/> |

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
2026-07-30 12:34:35 -07:00
bf139e8d0b perf(presence): reduce heartbeat frequency (#3783)
## Summary

- send desktop presence heartbeats every 60 seconds instead of every 30
seconds
- extend presence TTL from 90 to 180 seconds to preserve the existing
three-heartbeat expiry window
- add mutation-sensitive tests that pin the one-minute / three-window
timing contract
- update presence documentation to match

This halves steady-state **desktop** presence `SET` + `PUBLISH` traffic
while retaining tolerance for two missed heartbeats. Mobile already uses
a 60-second heartbeat, so the fleet-wide reduction depends on desktop's
share of connected clients.

## Rollout order

Deploy the relay TTL increase before shipping the desktop heartbeat
change. Old desktop + new relay is safe; new desktop + old relay leaves
only a 90-second TTL on a 60-second cadence and can flap after one
missed heartbeat.

## Verification

At initial live-test commit `00816e233b187bc5ba12c667d675ed050a8cc1c9`:

- isolated clean-room relay built from the exact SHA against fresh
Postgres, Redis, and MinIO
- live Redis `MONITOR` observed kind-20001 writes as `SET ... EX 180`,
global `PUBLISH`, and clean-disconnect / explicit-offline `DEL`
- normal workflows passed: channel create/update/archive/unarchive;
message send/get/reply/thread/search; archived-channel write rejection
and resumed write after unarchive

At follow-up commit `bf38a8c5c96f196ff8ee46e48d4141ee7811f186`:

- `pnpm -C desktop test` — 3829 passed
- `pnpm -C desktop typecheck`
- `cargo test -p buzz-pubsub` — 24 passed, 11 Redis-dependent tests
ignored
- mutation probes fail when the server TTL changes to `999999` or the
desktop heartbeat changes back to 30 seconds
- `git diff --check`

The pre-push suite's relevant checks passed, but its unrelated Tauri
clippy step fails on current `origin/main`:
`desktop/src-tauri/src/linux_media.rs` has three dead-code warnings on
macOS. This PR does not modify that file, so the branch was pushed after
independently running the suites above.

## Buzz context

Originating channel: `buzz-redis-cluster-mode`
(`f4e36d32-afdb-447f-8c87-ab003e069d18`)

---------

Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
2026-07-30 15:26:11 -04:00
klopez4212andGitHub 6e419b9f1c Tighten continuation message rows (#3724)
## Summary

- use uniform 4px top and bottom padding for continuation rows
- keep continuation timestamps top-aligned and remove the thread-only
minimum-height gutter
- raise continuation hover actions by 12px
- align virtualized row estimates with the compact layout

## Validation

- `pnpm test` (3,782 tests via pre-push)
- `pnpm check`
- desktop snapshots

## Screenshots

### Mention-chip continuation

![Mention-chip
continuation](https://raw.githubusercontent.com/block/buzz/85b88763ef8147f3376c9bf794bc0973a0211a57/pr-3724--thread-continuation.png)

### Emoji continuation

![Emoji
continuation](https://raw.githubusercontent.com/block/buzz/85b88763ef8147f3376c9bf794bc0973a0211a57/pr-3724--channel-continuation.png)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-07-30 19:39:12 +01:00
klopez4212andGitHub f48f3f055f Fix video reviews in thread replies (#3719)
## Summary
- Show video review comments when a video is opened from a thread reply.
- Reuse review-context construction across timeline and thread views.

## Validation
- `pnpm run build:e2e && pnpm exec playwright test
tests/e2e/video-attachment.spec.ts --project smoke --grep "video replies
in threads open the review comments view"`
- `pnpm test`

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-07-30 11:36:30 -07:00
cca8839034 Make relay reconnect backoff authoritative (#3774)
## Summary

- make the relay reconnect coordinator authoritative during outages so
query, publish, and subscription traffic waits for the scheduled attempt
instead of cancelling backoff
- release waiting operations after the coordinated AUTH +
live-subscription replay attempt, while preserving one explicit manual
reconnect fast path
- suppress duplicate notification side effects when reconnect replay
overlaps previously delivered events

## Root cause

`resetConnection()` scheduled exponential backoff, but
`ensureConnected()` cleared any pending reconnect timer. Operation-level
retry paths immediately called `ensureConnected()`, so ordinary app
traffic could repeatedly bypass the reconnect policy during an outage.
The resulting churn also replayed overlapping live events into
notification side effects without a shared event-ID guard.

## Validation

- `pnpm --dir desktop typecheck`
- `pnpm --dir desktop test` — 3,823 passed
- pre-push: `desktop-check`, `desktop-test`, and `branch-skew` passed
- file-size, px-text, and pubkey-truncation ratchets passed

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-07-30 11:25:08 -07:00
bd0bff24bf feat(desktop): add password-protected backups in settings (#3701)
**Category:** new-feature
**User Impact:** Users can create, download, and verify a
password-protected backup of their private identity from desktop
Settings.

**Problem:** Buzz does not currently give signed-in users a
Settings-based path to protect or validate their private identity
independently of onboarding. **Solution:** Add a focused backup menu to
the private-key row, keep encryption and verification local in Rust, and
preserve completed encrypted backups briefly so native saves can be
retried without repeating encryption.

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

**desktop/src/features/settings/**
Adds the background backup lifecycle, create and test dialogs,
private-key menu integration, password handling, and focused unit
coverage.

**desktop/src/features/onboarding/ui/NsecMaskedDisplay.tsx**
Extends the masked private-key display with reusable overflow-menu
actions used by Settings.

**desktop/src/app/App.tsx**
Mounts the backup provider at app scope so encryption and save work
survive closing Settings or the modal.

**desktop/src/shared/api/tauriIdentity.ts**
Adds typed desktop bindings for local backup creation, save, selection,
and verification.

**desktop/src-tauri/src/key_backup.rs and
desktop/src-tauri/src/commands/identity.rs**
Implements local NIP-49 encryption, password generation, file handling,
and public-identity-only verification results.

**desktop/src-tauri/src/egress_guard.rs and guarded call sites**
Blocks encrypted secret material from relay, websocket, snapshot,
sharing, and huddle egress paths.

**desktop/src-tauri tests and fixtures**
Covers encryption, verification, file behavior, and fail-closed
no-egress protections.

**desktop/src/testing/e2eBridge.ts, desktop/tests/, and
desktop/playwright.config.ts**
Expands the mock native bridge and browser coverage across create,
retry, expiry, and current/different-identity verification states.

**desktop/src-tauri/Cargo.toml, Cargo.lock, and assets**
Adds the local cryptography/password-generation dependencies and
embedded short-word list.

</details>

## Reproduction steps

1. Run the desktop app and open **Settings → Profile → Identity**.
2. Open the private-key overflow menu and choose **Create backup**.
3. Enter or generate a valid password, submit, and confirm progress
continues if the dialog or Settings is closed.
4. Save the resulting `.ncryptsec` file; cancel and retry to confirm the
temporary download remains available.
5. Choose **Test backup**, select the file, enter a wrong password, then
retry with the correct password.
6. Confirm success identifies whether the backup matches the current
identity and displays only the public `npub`.

## Screenshots

| Settings identity | Private-key menu | Create backup |
|---|---|---|
| <img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/981e391b-6829-4081-95ca-ca75a369de71"
/> | <img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/7972c68e-7635-47d8-b0ad-9639390d3e6c"
/> | <img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/4709c8f7-cf02-46f1-bec9-b3f98fe56fb2"
/> |

| Encrypting | Download available | Test success |
|---|---|---|
| <img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/1ac3e934-2b4b-4135-bae6-126c715c8c59"
/> | <img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/cb6f07ee-a16f-44a5-b9a0-6b9fe0e4d40d"
/> | <img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/ea58b1b1-966c-46aa-8d59-92c9f06a25bd"
/> |

Visual review and additional states: [Buzz
thread](buzz://message?channel=50ca7ef1-201e-4159-9499-40de3964b7c3&id=87eceb5f0f82fd50c32e560de3d35be48e293760f6620718aafdcef289d475fe)

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
2026-07-30 11:15:39 -07:00
f44b5a2477 fix(desktop): reuse profiles when joining communities (#2155)
## Why
People joining a community with an existing relay profile should not be
asked to recreate their name and avatar.

## What
- Check the active identity's relay profile after the joined community
becomes active
- Skip directly to the starter-team step when a kind-0 profile event
exists
- Preserve the profile setup path when no event exists or discovery
fails
- Cover both new-profile and existing-profile join paths in E2E tests

## Risk Assessment
Low — the lookup is scoped to the community onboarding profile stage,
runs once per transaction, and fails open to the existing flow.

## References
- `pnpm build:e2e && pnpm exec playwright test --project=integration
tests/e2e/onboarding.spec.ts --grep 'first-community direct join reaches
profile|community onboarding reuses an existing relay profile'` (2
passed)

Generated with Codex

Signed-off-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co>
2026-07-30 10:48:54 -07:00
61b96c9828 fix(catalog): update Amp description (#3758)
## Summary
- replace Amp's outdated Sourcegraph attribution in the runtime catalog
- describe Amp neutrally as a coding agent for the terminal and editor

## Verification
- `pnpm test` (desktop: 3,819 passed)
- `pnpm typecheck`
- pre-push `desktop-check`, `desktop-test`, and `branch-skew` hooks

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-07-30 12:54:13 -04:00
02be413b82 feat(catalog): resolve publisher display name in catalog detail pane (#3640)
The catalog detail pane hardcoded "Community member" for every non-own
catalog entry. The publisher pubkey (`catalogSource.ownerPubkey`) was
already on every entry — it just was not being resolved to a name.

## What changed

**`desktop/src/features/agents/ui/PersonaCatalogDialog.tsx`**

`PersonaCatalogDetail` now calls `useUsersBatchQuery([ownerPubkey])`
when the selected entry is a community (non-own) catalog agent. The
label derivation is extracted into the exported pure function
`resolveCatalogOwnerLabel` and uses truthy fallbacks to handle empty or
whitespace-only kind:0 fields:

- Own entry → `"You"` (unchanged)
- `displayName` present and non-blank → the display name
- `displayName` absent/blank but `name` present and non-blank → the name
- Loading, unresolvable, or both candidates blank → `"Community member"`
(fallback preserved)

The batch query is disabled (`enabled: false`) when the entry is not a
community entry, so there is no extra network call for own entries or
built-in agents.

**`desktop/src/features/agents/ui/personaCatalogOwnerLabel.test.mjs`**

Unit tests for `resolveCatalogOwnerLabel` covering: populated
`displayName` wins; whitespace-only `displayName` falls through to
`name`; both candidates empty/whitespace/null/undefined all fall through
to `"Community member"`.

**`desktop/tests/e2e/agents.spec.ts`**

- Updated the existing assertion — it previously checked for the
hardcoded fallback; now asserts the resolved mock display name
`"alice"`.
- Added "catalog detail shows Community member when the publisher
profile cannot be resolved" — installs a catalog event from an unknown
pubkey and asserts the fallback still renders.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
2026-07-30 11:38:28 -04:00
4933672eb4 feat(mesh): upgrade embedded mesh to v0.74 and harden shared compute (split 1/2 of #3467) (#3741)
## Summary

This is **part 1 of 2** split out from #3467 (per Tyler's request),
carrying only the mesh-scoped changes. The agent/ACP response-behavior
changes and the new `send_message` tool stay in #3467 as part 2. All
commits are @michaelneale's work, cherry-picked with authorship
preserved.

- Upgrade embedded Mesh to v0.74.0 (tag-pinned instead of commit rev)
and use canonical Gemma model IDs.
- Keep shared compute serving through member joins, roster changes, app
recovery, and community switching.
- Wait for actual model readiness and avoid resuming incomplete
downloads after quit.
- Leave `BUZZ_AGENT_THINKING_EFFORT` unset by default so each model's
chat template picks its own thinking default (`none` suppressed Gemma
tool-calling entirely; pinning `low` made Qwen3 burn ~4x output budget).
Explicit agent/persona/global values still win.

## Relationship to #3467

Contains the mesh commits from #3467 (`2cd640b23`, `0ad81c341`,
`ad13ed841`) rebased onto current main, with one deliberate exclusion:
the `crates/buzz-agent/src/llm.rs` reasoning→text parser change from
`2cd640b23` is **not** here. That change unconditionally affects every
OpenAI-compat/Responses provider, so it belongs with the reply-behavior
work in part 2, where it can be reviewed as what it is.

Not included (remaining in #3467 / part 2):
- typed `send_message` tool in dev-mcp + `BUZZ_ACP_SEND_MESSAGE_TOOL`
gating
- plain-reply delivery fallback in buzz-acp
(`BUZZ_ACP_DELIVER_PLAIN_REPLIES`)
- the mesh_agent_e2e P5/P6 rewrite (exists to prove the reply path)
- the two `env.insert` preset opt-ins in `relay_mesh.rs` for the flags
above
- the llm.rs parser change

This PR is independently mergeable; part 2's flags are all off by
default so it can land before or after.

## Testing

- `cargo test -p buzz-relay --locked` — 780 passed (one telemetry test
is order-sensitive under parallel default settings; passes in the
pre-push suite and standalone, unrelated to this diff — files untouched
here).
- `just desktop-tauri-test` (default features) — 1877 passed.
- `cargo test --locked --features mesh-llm` in `desktop/src-tauri` —
1961 passed, including the new relay-mesh preset and
coordinator/recovery tests.
- Both `Cargo.lock`s resolve with `--locked` against the v0.74.0 tag.
- Full pre-push hook suite green (rust-tests, desktop-check/test, tauri
checks).

Live validation of the mesh v0.74 upgrade itself is documented on #3467
(two-Mac cross-version test).

---------

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
2026-07-30 11:37:27 -04:00
9a386a0def Refine agent sharing dialog (#3699)
## Summary

- Refine the agent share dialog around recipient sharing, link copying,
catalog sharing, and export.
- Show memory settings only when a linked agent has memories to include.
- Use a catalog toggle for custom agents and keep built-in agents out of
the catalog flow.

## Validation

- `pnpm typecheck`
- Focused Playwright share and catalog flows

---------

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-07-30 15:23:16 +00:00
Matthew BeckleyandGitHub c9aa55505c desktop: enable getUserMedia in the Linux WebKitGTK webview (#3607)
Microphone/camera capture works on macOS (WKWebView) and Windows
(WebView2) but fails on Linux with `NotAllowedError`. WebKitGTK ships
with `enable-media-stream` off and a default `permission-request`
handler that denies every request.

This reaches the underlying `webkit2gtk::WebView` from
`on_webview_ready` and enables `enable-media-stream`, then installs a
**deny-by-default** `permission-request` handler: a `UserMedia` request
is allowed only from a trusted app origin (`tauri://localhost` in prod,
the Vite dev origin in debug) **and** when it targets an audio/video
device — everything else is denied. No-op on macOS/Windows.

- `webkit2gtk` is pinned to the version wry already uses (`=2.0.2`) so
there's a single shared copy of the native binding.

---------

Signed-off-by: Beckley <mattcbeckley@gmail.com>
2026-07-30 11:19:42 -04:00
klopez4212andGitHub 73589408db fix: align responsive agent views (#3688)
## Summary
- Keep the Agents header full width while cards reflow independently.
- Collapse header actions into an overflow menu at the compact layout
threshold.
- Apply the same responsive grid rules to Agent Teams.

## Validation
- `pnpm -C desktop build:e2e`
- Focused Agents Playwright coverage
- Pre-push desktop checks and unit tests

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-07-30 15:58:48 +01:00
d0a24bcb52 Add macOS agent menu-bar menu (#3565)
## Summary

- Add a **macOS-only** monochrome Buzz menu-bar icon with **Running**
and **Recent** agent sections.
- Show each agent as one selectable macOS row with `Name · elapsed` and
its channel underneath.
- Keep completed work available for quick channel re-entry, alongside
New Channel, Open Buzz, and Quit Buzz.
- Keep the main window alive on close and restore it from both the menu
bar and the macOS Dock.
- Fence queued channel actions by community generation so an in-flight
action from the previous community cannot navigate the newly selected
community.
- Leave Windows and Linux unchanged; cross-platform tray lifecycle
support can follow with platform-specific validation.

<img width="812" height="760" alt="Buzz macOS agent menu"
src="https://github.com/user-attachments/assets/19bfd874-8f06-4496-bdda-7ed2e7b5733f"
/>

## Validation

- `cargo fmt --check`
- Desktop Tauri suite: 1,860 passed, 14 ignored after merging current
`main`
- Desktop tests: 3,769 passed
- Desktop lint, file-size, text, and pubkey checks pass (two
pre-existing informational template-literal notices)
- Regression coverage verifies stale `OpenChannel` actions are discarded
across community changes while `NewChannel` survives

## Manual validation remaining

A native macOS smoke test is still requested before merge: menu
appearance, elapsed updates, Running → Recent, channel navigation, New
Channel, minimized/closed/Dock restore, Open Buzz, and Quit. E2E stubs
the tray IPC and does not exercise the native menu.

---------

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-07-30 07:51:25 -07:00
klopez4212andGitHub 4672ee55c4 Fix pending message feedback (#3543)
## Summary

- Keep `Sending…` beside message timestamps, including grouped pending
messages.
- Match the profile-card hover surface to inactive channel rows.

## Snapshots

### Pending message

![Pending message
status](https://raw.githubusercontent.com/block/buzz/655189aec0904677a68651f8aabf0e17d69734cd/pr-3543--pending-message-inline.png)

### Profile hover

![Profile
hover](https://raw.githubusercontent.com/block/buzz/655189aec0904677a68651f8aabf0e17d69734cd/pr-3543--profile-hover.png)

## Validation

- `pnpm -C desktop typecheck`
- `pnpm -C desktop check:file-sizes`
- `pnpm -C desktop build:e2e`
- `pnpm -C desktop exec playwright test
tests/e2e/message-feedback-snapshots.spec.ts --project=smoke`

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-07-30 07:46:56 -07:00
thomaspblockandGitHub c55e421a06 fix(desktop): remove remaining Projects panel fills (#3742)
## Summary

- let the Projects page surface flow through repository, pull request,
and issue list/grid layouts
- remove opaque fills from project-detail content across Overview,
Files, Commits, Issues, Pull Requests, and Contributors
- preserve borders, hover feedback, and intentional nested fills for
code, inputs, badges, and warnings

## Related

Follow-up to #3416.

## Testing

- `pnpm exec biome check` on the changed Projects UI and E2E files
- `pnpm build:e2e`
- focused Playwright smoke coverage for overview, subsection list/grid,
and project-detail transparency (3 passed)
- pre-commit checks passed
- desktop pre-push checks passed; the unrelated integration hook remains
blocked by a stale local checksum for migration 25

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-07-30 15:31:10 +01:00
310df2ec33 desktop: restore direct community member adds (#3634)
## Summary

- restore direct community member adds in **Settings → Invites**
- keep one consolidated entry point: the dialog now supports both adding
someone directly and sharing an invite link
- accept npub or 64-character hex keys while preserving role hierarchy
(owners: Member/Admin; admins: Member only)
- verify an npub direct-add publishes the decoded hex key in a kind
`9030` NIP-IA event

## Why

The Invites consolidation left `AddMemberDialog` without a live mount
point, so the existing direct-add capability disappeared even though its
mutation path still existed. This reuses that implementation rather than
introducing a second one.

## Before and after

| Before | After |
| --- | --- |
| The consolidated dialog only offered a share link; there was no
direct-add path. | The same dialog now presents direct add and
share-link controls as one invitation flow. |
| ![Before: invite dialog with share-link controls
only](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3634/before-invite-dialog.png)
| ![After: polished community invite dialog with direct-add and
share-link
sections](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3634/invite-dialog-polished.png)
|

<details>
<summary>Before: Invites page entry point</summary>

![Before: Invites settings
page](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3634/before-invites-page.png)

</details>

## Verification

- `pnpm --dir desktop build:e2e`
- `pnpm exec playwright test
tests/e2e/invites-settings-screenshots.spec.ts --project=smoke` — 4
passed
- targeted Biome check on modified files
- push hook: Desktop check and 3,783 Desktop tests passed
- GitHub CI green except Desktop E2E Relay still running at last status
snapshot

---------

Signed-off-by: Joah Gerstenberg <joah@squareup.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Joah Gerstenberg <joah@squareup.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: kenny lopez <klopez4212@gmail.com>
2026-07-30 07:04:49 -07:00