Commit Graph
309 Commits
Author SHA1 Message Date
Thomas Petersen 65e24bafe1 feat(desktop): refine repository project navigation
Make project repository states, branch actions, loading feedback, split-pane chrome, and persistent sidebar expansion behave consistently across the workspace.

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-15 11:33:18 -04:00
Thomas Petersen bdc8e5a480 feat(desktop): make project workspaces repository-aware
Unify project navigation, repository controls, contextual agent chat, and compact work-item views so repository state and access recovery stay visible in one workspace.

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-15 09:57:50 -04:00
122a8b8988 Projects v3: unify sharing, discussions, and issue ownership (#5792)
## Summary

Projects v3 makes repository work shareable, discussion-aware, and
easier to scan in one coherent workspace. People can copy canonical
links, reopen the exact workspace tab, understand issue and pull-request
context at a glance, find related channel conversations, and assign or
unassign issues across Desktop and CLI.

- **Unified workspace** — top-level sections sit above repository
controls in one rounded workspace, with navigation positioned close to
the page heading. README and Files retain branch selection; every
section has a labeled icon header, and Issues and Pull Requests expose
creation from a consistent right-aligned action.
- **Repository management** — the repository selector is always
available, including single-repository projects. Its integrated add flow
lets project owners create a repository manually or select an existing
repository without a separate toolbar button.
- **Readable work-item lists** — issue and pull-request rows use
plain-language context instead of opaque metadata. Files, commits,
issues, pull requests, channels, and contributors share consistent row
density and right-aligned timestamps, while deterministic
fallback-avatar colors keep participants distinct on light backgrounds.
Inbox pull-request metadata wraps between complete phrases and truncates
long channel names instead of compressing copy into narrow columns.
- **Reliable entity links** — projects, repositories, issues, pull
requests, and commits have canonical `buzz://` links, preview cards, OS
deep-link routing, and tab-aware navigation. Reopening the same link
re-applies its destination instead of leaving the user on a locally
selected tab.
- **Related conversations** — repository and work-item views surface
channels discussing the current entity, including participants, channel
navigation, message context, and an explicit notice when discovery
reaches its 500-result cap.
- **Reversible issue ownership** — trusted assignment and unassignment
events work across Desktop, Tauri, `buzz-sdk`, and `buzz issues`.
Assignees appear in project views and the assigned inbox, while
authorized users can remove assignments directly from the assignee row.

Assignment state is derived chronologically from labeled Nostr notes.
Issue authors and repository owners may change any assignee; other users
may only assign or unassign themselves. Shared golden fixtures keep
entity-link grammar and validation aligned across TypeScript and Rust.

The branch also updates `webbrowser` to the patched release for
RUSTSEC-2026-0257.

### Related issue

N/A.

### Testing

- [x] `just ci` — formatting, lint, typechecking, unit tests, and builds
passed
- [x] Full pre-push suite — organization, branch-skew, Desktop checks,
typechecking, and tests passed on the latest push
- [x] `cargo test -p buzz-cli` and focused `buzz-sdk` assignment tests
passed
- [x] Focused Tauri recipient-note and 500-result search-limit tests
passed
- [x] Desktop entity-link and issue-assignment unit tests passed
- [x] Playwright smoke coverage passed for assignment, repeated
entity-link navigation, repository create/select flows, section headers
and actions, timestamp alignment, timeline icons, sentence-style
issue/PR metadata, header spacing, avatar contrast, and Inbox metadata
at stacked and side-rail breakpoints
- [ ] Manual staging pass: link round-trips, Channels tab, assignment
flows, and inbox routing

### Screenshots

Pull requests explain who opened the request, where it lives, and which
branch it comes from; fallback avatars remain visually distinct.

![Pull request list with conversational
metadata](https://raw.githubusercontent.com/block/buzz/2a536de86f7e6f79b349d7bc147b2923ff2b817d/pr-5624--05-pr-list-metadata.png)

Issues use the same sentence-style hierarchy while keeping status and
recency easy to scan.

![Issue list with conversational
metadata](https://raw.githubusercontent.com/block/buzz/2a536de86f7e6f79b349d7bc147b2923ff2b817d/pr-5624--06-issue-list-metadata.png)

The wide Inbox detail keeps author, timestamp, and origin context
readable beside its metadata rail.

![Pull request Inbox detail with readable
metadata](https://raw.githubusercontent.com/block/buzz/e65b433e14b97c45365ed7b68ea402ec01d26615/pr-5624--02-pull-request-detail-wide.png)

[View the complete six-state Projects v3 screenshot
set](https://github.com/block/buzz/pull/5624#issuecomment-5268039672)
and [the compact/wide Inbox
comparison](https://github.com/block/buzz/pull/5624#issuecomment-5268614585).


---

> Supersedes #5624, whose head commit accumulated permanently-queued
required check suites (block-dco-check et al.) that GitHub never
dispatched. History flattened into a single signed-off commit on latest
main; tree verified byte-identical (`git merge-tree`) to merging the
original branch into main.

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Co-authored-by: Wintermute <3f1797424fd9ad6653a83665c660517777cd7f8c228c0d5907f49e01537f3ca5@buzz.block.builderlab.xyz>
2026-08-14 16:48:41 -04:00
f086eb6544 fix(link-previews): send while previews finish in background (#5697)
**Category:** fix
**User Impact:** Messages send immediately after submission while link
previews finish in the background, with an option to skip delayed
preview preparation.

**Problem:** Waiting for link-preview metadata or snapshot uploads kept
the composer occupied after users pressed Send, while races between
completion, timeout, and cancellation risked inconsistent payloads.
**Solution:** Freeze and promote speculative preview work into a bounded
background send task, clear the composer immediately, and
publish exactly once with prepared previews or gracefully without them
when skipped, failed, or timed out.



https://github.com/user-attachments/assets/987d2f2c-679f-473a-965f-dfb279951e52



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

**desktop/src/features/communities/useCommunityInit.ts**
Resets pending link-preview preparation when community context changes
so work cannot cross community boundaries.

**desktop/src/features/messages/lib/linkPreviewPreparationStore.ts**
Adds the coordinator-owned preparation state machine, bounded fallback,
Skip behavior, and exactly-once terminal publication handling.

**desktop/src/features/messages/ui/ComposerUploadProgressOverlay.tsx**
Extends floating background progress UI to include link-preview
preparation.

**desktop/src/features/messages/ui/ComposerUploadProgressPill.tsx**
Adds the preparing-link-preview label and Skip action to the progress
pill.

**desktop/src/features/messages/ui/MessageComposer.tsx**
Hands submitted preview work to the background coordinator and clears
the composer immediately.

**desktop/src/features/messages/ui/messageComposerAutoSubmit.test.mjs**
Updates auto-submit unit coverage for coordinator-owned preview
preparation.

**desktop/src/features/messages/ui/messageComposerAutoSubmit.ts**
Allows submit to promote unfinished preview work instead of blocking
composer submission.

**desktop/src/features/messages/ui/useComposerLinkPreviews.tsx**
Starts preview work speculatively and exposes frozen preparation jobs
for adoption by the send flow.

**desktop/src/features/messages/ui/useMentionSendFlow.helpers.ts**
Carries prepared preview tags through the mention and media payload
helpers.

**desktop/src/features/messages/ui/useMentionSendFlow.ts**
Integrates prepared preview tags into final message publication.

**desktop/src/shared/lib/useResolvedLinkPreviews.ts**
Exposes the in-flight metadata promise so promoted work can be adopted
rather than restarted.

**desktop/tests/e2e/messaging.spec.ts**
Covers immediate submit, upload handoff, Skip/completion races, failure
fallback, auto-send, and exactly-once publication.

</details>

## Reproduction steps

1. Enter a supported link and press Send while preview metadata or
snapshot upload is still pending.
2. Confirm the composer clears immediately and the floating progress UI
shows **Preparing link preview · Skip**.
3. Let preparation finish and confirm one message is published with its
preview.
4. Repeat and choose **Skip**; confirm one message is published without
waiting for the preview.
5. Simulate preview failure or timeout and confirm the message still
publishes once without preview tags.

## Validation

- TypeScript, Biome/format, file-size, px-text, and pubkey checks
- Full desktop unit suite: 4,734 passed
- Focused Playwright messaging suite: 5 passed
- Push hooks at `86c0aa7de2ff81b79286c99bf23db12345adc6ca`: desktop
check, typecheck, and tests passed

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-14 12:16:36 -07:00
5acb930821 feat(desktop-messages): render compact Buzz permalink chips (#5638)
**Category:** improvement
**User Impact:** Buzz channel, message, repository, pull request, and
issue links now open reliably and display recognizable context in the
desktop app.
**Problem:** Buzz links could appear as raw or ambiguous URLs, and
navigation links received during startup or community transitions could
be dropped before the UI was ready. Repository and issue shares in
particular required hover context to understand at a glance.
**Solution:** Queue desktop channel/message navigation until the UI is
ready, then render bare Buzz permalinks as icon-prefixed chips with
concise entity context while preserving user-authored Markdown labels as
ordinary links.

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

**desktop/src-tauri/src/deep_link.rs**
Adds validated channel-link parsing and a deduplicated, acknowledged
queue so navigation survives frontend startup.

**desktop/src-tauri/src/lib.rs**
Registers the pending-navigation state and commands with the desktop
application.

**desktop/src/features/communities/useCommunityInit.ts**
Resets queued navigation safely across community boundaries without
leaking stale destinations.

**desktop/src/features/messages/lib/channelLink.test.mjs**
Covers valid, malformed, and canonical channel permalink forms.

**desktop/src/features/messages/lib/channelLink.ts**
Defines strict parsing and detection for `buzz://channel/<uuid>` links.

**desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs**
Extends composer-node coverage for normalized Buzz link content.

**desktop/src/features/messages/lib/composerMessageLinkNode.ts**
Keeps composer link-node handling aligned with the expanded Buzz link
surface.

**desktop/src/features/messages/lib/remarkChannelDeepLinks.test.mjs**
Verifies bare channel URLs become renderable deep-link nodes without
touching code.

**desktop/src/features/messages/lib/remarkChannelDeepLinks.ts**
Transforms eligible bare channel links into dedicated Markdown nodes.

**desktop/src/features/messages/lib/remarkEntityLinks.test.mjs**
Covers bare repository, pull-request, and issue detection and code-span
exclusions.

**desktop/src/features/messages/lib/remarkEntityLinks.ts**
Adds dedicated Markdown nodes for bare Buzz project entities.

**desktop/src/shared/deep-link.test.mjs**
Exercises queued navigation, acknowledgement, serialization, and
community-switch behavior.

**desktop/src/shared/deep-link.ts**
Serializes pending deep-link drains and acknowledges destinations only
after successful navigation.

**desktop/src/shared/styles/globals/markdown.css**
Aligns permalink icon geometry and spacing with agent mention chips.

**desktop/src/shared/ui/markdown.test.mjs**
Adds integration coverage for every permalink chip, authored labels,
fallbacks, icons, and static rendering.

**desktop/src/shared/ui/markdown.tsx**
Routes channel and entity nodes through the shared presentation path
while preserving authored link text.

**desktop/src/shared/ui/markdown/BuzzLinkChip.tsx**
Introduces the shared interactive/static permalink chip and
authored-label inline-link components.

**desktop/src/shared/ui/markdown/ChannelDeepLink.tsx**
Renders channel shares and references with Hash icons, names, and
shortened-ID fallbacks.

**desktop/src/shared/ui/markdown/MessageLinkPill.tsx**
Renders ordinary message shares with message icons and channel/message
context while retaining sent-from-thread behavior.

**desktop/src/shared/ui/markdown/entityLinks.tsx**
Maps repositories, pull requests, and issues to Projects-aligned icons
and contextual labels.

**desktop/src/shared/ui/markdown/nodeCache.ts**
Includes entity-link rendering in cached Markdown node handling.

**desktop/src/shared/ui/markdown/utils.ts**
Allows validated channel links through the Buzz URL transform.

**desktop/src/shared/useMessageDeepLinks.ts**
Drains queued navigation links safely and clears them during teardown.

**desktop/src/testing/e2eBridge.ts**
Extends the mock bridge with pending-navigation command behavior.

**desktop/tests/e2e/community-rail.spec.ts**
Verifies queued links do not cross community boundaries.

**desktop/tests/e2e/navigation.spec.ts**
Covers channel/message deep-link navigation during startup and active
sessions.

**desktop/tests/helpers/bridge.ts**
Adds reusable deep-link mock state and acknowledgement helpers.


</details>

## Reproduction steps
1. Run the desktop app and open a channel containing bare
`buzz://channel`, `buzz://message`, `buzz://repo`, `buzz://pr`, and
`buzz://issue` URLs.
2. Confirm each bare URL renders as one cohesive chip with a type icon,
a useful name or shortened identifier, and no duplicated channel `#`
character.
3. Add an authored Markdown link such as `[design
discussion](buzz://issue?...)` and confirm the supplied label remains an
ordinary link rather than becoming a chip.
4. Select channel and message links and confirm they navigate correctly
in warm and cold-start states.

## Screenshots / demos
Houston dark theme with custom purple accent (`#a855f7`), captured from
rebased visual implementation `ad411cc06`; current head `0aafa144f` only
adjusts E2E expectations for the visible mention-label behavior shown
here.

**Composer — channel, message, repository, pull request, and issue
pills**

![Composer with all Buzz permalink pill types in Houston dark theme and
purple
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5638/composer-all-permalink-pills-dark-purple.png)

**Message list — channel, message, repository, pull request, and issue
pills**

![Message list with all Buzz permalink pill types in Houston dark theme
and purple
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5638/message-list-all-pill-types-dark-purple.png)

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-14 10:18:18 -07:00
c8da06c5e9 Speed up initial direct messages (#5658)
## Summary

- avoid blocking first-DM navigation on a full channel-list refresh
- publish the initial message through the acknowledged HTTP path instead
of waiting on a missing WebSocket acknowledgement

## Validation

- 4,715 desktop unit tests
- desktop typecheck and checks
- focused new-DM Playwright coverage

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz>
2026-08-13 16:24:48 -07:00
bcf353c969 fix(desktop): enforce agent mention authorization at send boundaries (#5681)
## Summary
- allow channel-member remote/headless agents only with current kind
`10100` directory evidence, while stale member identities remain hidden
- fail closed while managed/relay directories load, error, or
background-refetch across channel, forum, and cached autocomplete
surfaces
- revalidate agent mention authorization immediately before normal sends
and message-edit saves, including after deferred uploads
- in owner-only builds, fetch fresh authoritative profile ownership at
send time and deny missing, changed-owner, or unavailable proofs
- preserve human mention tags when agent authorization is revoked or
unknown

Supersedes #5536 because its contributor-fork head cannot be updated by
maintainers.

## Validation
Exact head: `7278cdd5fbcee676c7b858ea098503c62eeeff0d`

- mandatory pre-push suites passed: desktop check/typecheck/tests, Rust
tests, mobile tests, desktop Tauri checks, branch-skew
- desktop unit tests: 4,732 passed
- focused edit/ownership regressions: 8 passed
- focused mention E2E: 5 passed (remote positive, stale-member negative,
directory error, pre-send revocation, mid-send revocation)
- file-size ratchet passed

One first focused E2E batch had a timing-only miss where the send click
did not emit; the isolated rerun passed. One separate pre-push attempt
hit the existing randomized passphrase separator test; the successful
exact-head push reran and passed the mandatory suite.

---------

Signed-off-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: JDiz00 <174381550+JDiz00@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-13 21:40:02 +00:00
2693e0db1f Make workflow run history authoritative in Desktop (#5780)
## Summary

- persist stable workflow run `error_code` values separately from human
diagnostics
- expose NIP-98 authenticated, channel-authorized run history and
approval reads with stable keyset pagination
- connect Desktop to those authoritative reads and return the
relay-created run ID on trigger
- show truthful loading, failure, and pending-trace states, and do not
render approval actions from non-actionable stored hashes

## Validation

- pre-push `branch-skew`, `desktop-typecheck`, `desktop-test`,
`rust-tests`, `desktop-tauri-checks`, and `desktop-check` all passed on
`a097dbe5f`
- Desktop tests: 4,761 passed, 0 failed
- `cargo check -p buzz-relay`
- `git diff --check`

## Remaining gate

This does not claim a relay-backed Playwright workflow journey. The
browser relay bridge still routes workflow invokes through in-memory
handlers; that production-shaped acceptance gate remains follow-up work
before Workflows can leave preview.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz>
2026-08-13 12:56:37 -07:00
Alex RosenzweigandGitHub a96af89526 Harden shared agent instruction review (#4220)
## Summary

- render shared-agent instructions as literal text so Markdown cannot
conceal spoiler contents, link destinations, or image sources
- reject non-reviewable Unicode controls at every agent-definition
boundary while preserving legitimate rendered emoji sequences
- verify shared catalog event IDs and signatures before trusting
authorship, coordinates, pagination, or executable content
- preserve the exact system-prompt bytes between review and execution
instead of silently stripping or normalizing content

## Security rationale

Shared system prompts are executable configuration. Previously, catalog
prompts were projected through the chat Markdown renderer, which could
hide text, replace link destinations with benign labels, and turn image
syntax into remote loads. Zero-width and bidirectional controls could
also make reviewed text differ from what the agent executes.

This change establishes a review invariant: the prompt a user sees is
the prompt the agent executes. Definitions that cannot be reviewed
faithfully are rejected rather than rewritten. Catalog events must also
pass Nostr ID/signature verification before they can claim a publisher,
coordinate, or cursor.

## What changed

- catalog instructions render as exact literal text rather than rich
Markdown
- catalog relay events are verified on a fresh wire-shaped object before
paging, coordinate selection, attribution, or projection
- forged content, pubkeys, signatures, and invalid newer heads are
ignored and cannot shadow a valid signed definition
- TypeScript catalog parsing rejects unsafe remote definitions before
they reach the UI
- shared Rust validation covers persona create/update/import, inbound
relay sync, definition-less managed-agent sync, and catalog publication
paths
- definition-less managed agents now fail closed on local create, local
update, and publication before persistence or relay retention
- linked managed agents validate their local name while treating the
persona definition as authoritative; their inert record-level prompt is
not executed or published
- names reject layout controls; prompts retain ordinary newlines and
tabs
- legitimate emoji composition is supported, including contextual VS16,
ZWJ, skin-tone, family, flag, and keycap sequences
- detached selectors/joiners, bidirectional controls, tag characters,
zero-width concealment, and other default-ignorables remain rejected
- names are bounded to 128 characters and prompts to 64 KiB
- contributor guidance documents the byte-for-byte review requirement
for future sharing paths

Validation reports the offending code point and never silently removes
it.

## E2E recording


[buzz-shared-agent-security-e2e.webm](https://github.com/user-attachments/assets/44d6b75f-0877-490f-bda4-a716fae3f700)

The recording demonstrates:

- a safe definition remains visible
- a prompt containing zero-width `U+200B` is rejected
- a name containing bidi override `U+202E` is rejected
- the prompt is preserved exactly
- spoiler, link, and image syntax remains literal and does not render or
load

## Verification

Passed locally:

- `just test`: all 10 unit and Docker-backed integration stages
- desktop frontend unit suite: 4,295 tests
- persona catalog relay unit suite: 32 tests, including forged-event and
cursor-shadowing cases
- focused Rust definition-validation coverage: 3 local create/update
tests and 6 publication-filtered tests
- complete desktop Tauri library suite after rebase: 2,263 passed, 14
ignored, 0 failed
- desktop Tauri clippy with warnings denied and Rust formatting
- complete agent Playwright spec: 34 tests
- the exact formerly failing `inbox-edit` immediate-attachment smoke
test after rebase: 1 test
- focused shared-agent publish, literal-review, hidden-control,
signature, and cross-member import Playwright coverage
- desktop E2E production build and TypeScript typecheck
- changed-file formatting/lint and file-size ratchet
- pre-commit secret scan and DCO signoff

The branch was rebased onto current `main`, which includes the upstream
attachment-button label fix. Fresh post-rebase GitHub CI is green for
every required and selected check: Desktop Core, all four Desktop Smoke
E2E shards, both Desktop E2E Integration shards and their aggregate,
Desktop E2E Relay, Desktop Build (macOS), Windows Rust, Rust Lint, DCO,
security scanners, and Desktop Release Candidate. The previously failing
`Desktop Smoke E2E (3)` shard now passes.

The repository-wide desktop check also reports existing CSS
formatting/`!important` findings in `components.css` and `terminal.css`;
neither file is changed by this PR. GitHub's Desktop Core lint and
format stage passes on the rebased branch.

---------

Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
2026-08-13 13:11:45 +10:00
c86443c599 perf(desktop): persist channel snapshot hash (#5684)
## Summary

- persist each relay/identity's complete channel list and server hash as
one integrity-checked snapshot
- paint the snapshot immediately on cold boot, then revalidate with
`knownHash`
- fail slow-never-wrong: malformed/legacy/partial snapshots and
mismatched not-modified responses force an unhashed full fetch
- add sidebar boot diagnostics and deterministic unit/E2E coverage for
boot, identity/relay isolation, partial writes, mismatch fallback, and
community switches

## Safety invariants

- channel list and hash are serialized in one localStorage document and
replaced together
- snapshot ownership is scoped to normalized relay URL plus identity
pubkey
- a not-modified response is accepted only when its hash exactly matches
the hash describing the available list
- any missing or impossible hash/list pairing retries
`getChannels(null)` before replacing persistence

## Validation

At exact commit `19ca25d23c434cc0b8893a93691aaf4c77794f60` with a clean
working tree:

- `cd desktop && pnpm check && pnpm typecheck` — passed (existing
informational Biome findings only)
- `cd desktop && pnpm test` — 4,723 passed
- `cd desktop && node --import ./test-loader.mjs
--experimental-strip-types --test
src/features/channels/channelSnapshot.test.mjs` — 13 passed
- `cd desktop && pnpm exec playwright test e2e/sidebar-snapshot.spec.ts
--grep "cold boot paints" --repeat-each=5` — 5 passed
- `cd desktop && pnpm exec playwright test e2e/sidebar-snapshot.spec.ts`
— 8 passed
- push hooks repeated desktop check/typecheck and all 4,723 unit tests
successfully

The Playwright suite uses injected bridge delays. Its roughly 0.5–0.6 s
snapshot paint and 3.0 s snapshot-to-live readings are synthetic
invariant evidence, not production desktop performance measurements.

## Measurement context

The controlled current-main investigation is documented separately in
`RESEARCH/DESKTOP_PERF_DEEP_DIVE_2026_08_12.md`; its raw local artifacts
are `.scratch/summer-perf-deepdive-results-v2.json`,
`.scratch/summer-perf-deepdive-run.log`,
`.scratch/summer-perf-deepdive-run-2.log`, and
`.scratch/summer-perf-deepdive-build-2.log`. Those original timings are
also synthetic Chromium/mock-bridge measurements and are not presented
as shipped Tauri/WKWebView or production-relay numbers.


## Latest review delta

At exact tip `e8e2b1d617aac7ea008258ad9974bbf8da9cd2eb`, storage-denial
reads fail open to the live fetch, hashless retries reject `channels:
null` before pair/persistence updates, identity-read failure enables a
hashless live fetch, and repeated consumers reuse snapshot
parsing/integrity validation by storage key + raw document. The four
remaining review nits are deferred as non-blocking follow-ups.

Validation at this tip: sidebar snapshot E2E 30/30 serial; desktop unit
suite 4,725/4,725; full push gate green (desktop check/typecheck/unit,
Rust, Tauri).

---------

Signed-off-by: Summer <1fdd3cc104e2911eb3b2da6f97d1b25f4a7f3550ded4492b24ff1d95acd66766@buzz.block.builderlab.xyz>
Co-authored-by: Summer <1fdd3cc104e2911eb3b2da6f97d1b25f4a7f3550ded4492b24ff1d95acd66766@buzz.block.builderlab.xyz>
2026-08-12 14:23:41 -07:00
Taylor HoandGitHub 884ed8a5d3 fix(link-previews): proxy sent preview media (#5627)
## Overview

**Category:** fix
**User Impact:** Sent link previews now reliably display their thumbnail
and favicon when the media is hosted on the relay.

**Problem:** Sent preview cards loaded relay-hosted snapshot media
directly, so authenticated relay requests could fail even though the
snapshot itself was valid. **Solution:** Rewrite snapshot media at the
shared card render boundary through Buzz's authenticated local media
proxy, preserving the original display domain and rerendering when the
proxy becomes ready.

## Changes

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

**desktop/src/shared/ui/link-preview-attachment.tsx**
Routes sent preview thumbnails and favicons through authenticated relay
media handling above the Compact/Rich fork while preserving original
metadata.

**desktop/src/testing/e2eBridge.ts**
Adds an opt-in proxy-readiness seam that deterministically re-arms the
production media lookup when released.

**desktop/tests/e2e/messaging.spec.ts**
Covers the real send, snapshot, recipient, and card-render path for
Compact and Rich previews, including fallback URLs, proxied URLs, and
decoded image content.

**desktop/tests/helpers/bridge.ts**
Exposes the opt-in media-proxy startup state to E2E tests.

</details>

## Reproduction Steps

1. Send a link whose preview snapshot includes a relay-hosted thumbnail
and favicon.
2. Inspect the sent message card in Compact mode and confirm both images
render after the local media proxy becomes ready.
3. Switch link previews to Rich mode and confirm the thumbnail and
favicon continue to render.
4. Run the focused Playwright regression:
`pnpm exec playwright test tests/e2e/messaging.spec.ts --project=smoke
--grep "sent link preview media uses the authenticated proxy"`


## Before / After

| Before | After |
| --- | --- |
| Relay-hosted preview media fails to load. | The sent preview thumbnail
and favicon render through the authenticated media proxy. |
| ![Before: sent link preview with a missing
thumbnail](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5627/link-preview-before.png)
| ![After: sent link preview with the thumbnail
rendered](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5627/link-preview-after.png)
|

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-12 10:24:21 -07:00
63f961c7e4 Refine channel settings and profile panels (#5574)
## Summary

- simplify channel settings into concise detail, member, canvas, and
action sections
- align human and agent profiles around shared rows, segmented tabs, and
top-level actions
- add agent runtime presentation, sticky glass behavior, and
scroll-linked action transitions

## Snapshots

### Channel settings

![Channel
settings](https://raw.githubusercontent.com/block/buzz/72d3958374a6f31c0f15912a8307101415eed084/pr-5574--01-channel-settings.png)

### Agent info

![Agent
info](https://raw.githubusercontent.com/block/buzz/72d3958374a6f31c0f15912a8307101415eed084/pr-5574--02-agent-info.png)

### Agent runtime

![Agent
runtime](https://raw.githubusercontent.com/block/buzz/72d3958374a6f31c0f15912a8307101415eed084/pr-5574--03-agent-runtime.png)

## Validation

- `pnpm -C desktop check`
- `pnpm -C desktop test` (4,604 passed)
- `pnpm -C desktop build:e2e`
- focused channel settings and agent profile Playwright tests

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz>
2026-08-12 08:25:04 -07:00
Will PflegerandGitHub f35930104b fix(desktop): remove 0.5.9+ perf regressions, speed up get_channels (#5599)
Desktop input latency regressed sharply for users on v0.5.9 and worsened
on latest main: multi-second stalls when clicking back into the app,
slow fresh boots, intermittent lockups, and scroll/mouse degradation.
Reverting to `119a84897` (pre-0.5.9) was confirmed to resolve it,
isolating the regression to that range. Profiling a live production
renderer plus a commit-level audit of the range found three independent,
additive causes — fixed here — plus a long-standing `get_channels` cost
that made every remaining refetch expensive, also addressed here.

## 1. Focus-return refetch storm (`refetchOnWindowFocus`)

#5490 wired TanStack's `focusManager` to app focus and flipped ~20 query
sites to `refetchOnWindowFocus: true`. A focus return after >60s away
fires them all within milliseconds — and a click into an unfocused
window *is* a focus return, so the burst runs before the click is
processed. That is the "click into the composer, wait 5 seconds"
symptom, and it also explains why mouse input feels worse than keyboard
(clicks arrive with focus transitions; typing happens while already
focused). A 5-second `sample` of a live production renderer caught a
single window activity-state transition consuming ~1.25s of main-thread
time, dominated by `JSON.parse` in the focus listener's microtask drain.

#5535 already established the fix pattern but applied it to only two
families (channels, home-feed). This PR extends the same 5-minute
`staleTime` discipline to the remaining families: pulse (×5), workflows
(×4), agents (×4), forum (×2), presence, user-status, custom-emoji,
channel-templates, and the persona catalog. Polling cadences and
push-invalidation paths are untouched — interval refetches and
`invalidateQueries` both bypass `staleTime`, so live-update behavior is
unchanged. Each gated family exports its focus-refetch policy as an
options object that the production hook spreads into `useQuery`, and a
`focusRefetchPolicy.test.mjs` drives a `QueryObserver` with that same
production object — locking the policy behaviorally (fresh focus return
→ 0 fetches; stale → refetch) and failing if a hook's
`staleTime`/`refetchOnWindowFocus` wiring drifts.

Four families deliberately keep tighter freshness, all surfaces where
the 5-minute gate would suppress the only refresh path and none of which
feed the app-wide storm: `repo-sync-status` keeps its fresh focus
refetch (its inline comment documents the "committed in a terminal,
switched back to the app" flow as intended); the workflow-runs list
stale-gates at 10s because a remotely-started run has no push
invalidation and its conditional 1s poll is off while the cache shows no
active runs; the workflow list queries (`useChannelWorkflowsQuery` and
the all-channels aggregate) stale-gate at 10s because they have no poll
and no relay subscription, and mutation-driven invalidation only covers
this renderer — remote workflow creates/edits/deletes surface only via
focus refetch; and the managed-agent log stale-gates at one poll tick
(30s) so returning to a live agent log refreshes immediately. Run
approvals keep the 5-minute gate under
`RUN_APPROVALS_FOCUS_STALE_TIME_MS` — their focused 10s poll already
covers freshness.

## 2. Synchronous localStorage sweep on the boot/focus path

#5453's stale-cache sweep synchronously `getItem` + `JSON.parse`s every
whitelisted localStorage entry on the main thread (multi-MB on seasoned
profiles), scheduled with a `requestIdleCallback` timeout of 1.5s that
guaranteed it landed mid-boot, and re-armed on every hidden→visible
transition — stacking it onto the exact moment the focus storm fires.
#5454's `trimSelfProfileCaches()` additionally scanned every
localStorage key on every `writeSelfProfileCache()` call (which fires
per relay self-profile delivery at boot).

Now: the first sweep waits `BOOT_SWEEP_FLOOR_MS` (30s) after startup,
the scan is time-sliced across idle callbacks, and the visibility
trigger is removed — boot-delayed plus hourly still covers the 14-day
TTL contract. The sliced sweep re-checks staleness immediately before
each removal (a key rewritten fresh mid-sweep survives), isolates
per-key storage errors so one bad entry can't strand the rest of the
snapshot, defers oversized values once rather than parsing them on a
zero-budget slice, guarantees forward progress on timeout-fired
callbacks, and cancels its scheduled slice when stopped. The profile
trim keeps a lazily-initialized memoized key count so the common
under-cap write is O(1); the full parse scan runs only when the count
exceeds a cap, resyncs if external deletions made it stale, and a failed
scan skips the trim instead of aborting the write. Sweep semantics
(rules, TTLs, eviction) are unchanged, and tests cover the scheduling,
slice-progress, error-isolation, defer-once, and trim short-circuit
behaviors.

## 3. The macOS window was never opaque

#5478's glass appearance is correctly opt-in at the CSS layer, but the
compositor cost was baked in deeper than its native `on_webview_ready`
transparency call: the main window is declared `"transparent": true` in
`tauri.conf.json` (added for the original glass work in #1671), which
makes tao call `NSWindow.setOpaque(false)` at creation and resolve every
later `set_background_color(None)` to `clearColor` — and no runtime
`setOpaque(true)` path exists through tauri, while wry's runtime
background setter can only force the WKWebView's `drawsBackground` off,
never back on. So "restore the platform default" was unreachable: every
launch, glass or not, ran with a non-opaque NSWindow, defeating
WindowServer's opaque-window compositing fast path and forcing full
window compositing every frame — compounded by the existing
`backdrop-blur` chrome overlapping the scrolling timeline. This matches
the compositor-shaped symptoms (scroll and pointer input degrading
first).

The window is now created opaque (`"transparent": false`) and the
NSWindow layer is never made transparent at runtime. Glass never needed
a transparent window: behind-window `NSVisualEffectView` vibrancy
renders inside opaque windows (this is how Finder and Notes draw vibrant
sidebars); it only requires a transparent WKWebView canvas, which the
`set_window_vibrancy` enable path already establishes at runtime
(`macos-private-api` compiles that in independent of the window flag).
Enabling glass installs the vibrancy layer and then makes only the
webview canvas see-through; disabling clears the vibrancy layer — the
canvas may stay non-drawing afterwards (wry's flag is one-way at
runtime), which is harmless because glass-off CSS paints fully opaque
above an always-opaque NSWindow. The boot-path first-frame backing
writes touch only the NSWindow backing color and are therefore inert to
glass state regardless of how they order against the `ThemeProvider`'s
vibrancy call on a persisted-glass-on cold boot. Glass-off users (the
default) get an end-to-end opaque window from boot for the first time.

## 4. `get_channels`: serial round-trips and a multi-MB payload on every
refetch

The stale gates in (1) cut refetch frequency; this cuts the cost of the
refetches that legitimately remain (boot, and focus returns after more
than 5 minutes away — previously still a multi-second stall).
`get_channels` made ~8 fully serial relay round-trips (~3.2–3.6s at
1,100+ channels), then shipped the full `ChannelInfo` list — including
every channel's member pubkeys — across IPC, where the renderer's
`JSON.parse` of the multi-MB payload froze the main thread (the ~1.25s
stall captured in the live sample).

- **Concurrent stages**: the membership chain, the open-channel
directory scan, and the hidden-DM snapshot run concurrently, as do the
member-count and last-message queries that follow. The critical path
drops from ~8 sequential round-trips to 2 phases. Filters, limits,
pagination, and merge semantics are unchanged.
- **Not-modified short-circuit**: the command now takes a
client-supplied content hash (FNV-1a 64 over the channel list,
canonicalized by id and excluding `last_message_at`) and omits the
channel list from the response when nothing else changed. Last-message
timestamps — which change on nearly every message anywhere — ship as a
small separate map that the client overlays onto its cached list with
reference preservation, so React Query's structural sharing also skips
downstream re-renders. On a typical refocus the renderer parses
kilobytes instead of megabytes. The hash is stored in the query cache
itself, tying its lifecycle to the data it describes so a community
switch can never leak a stale hash.

The E2E mock bridge speaks the new payload shape — including the
complete `last_messages` map the client treats as authoritative — and
hash canonicalization plus overlay reference-preservation are
unit-tested on both sides.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-11 19:47:19 -04:00
cd2aa5c12d Add glass appearance and cohesive settings (#5478)
## Summary

- add an opt-in native glass sidebar with opacity controls and live
theme previews
- refine sidebar spacing and Buzz-only active rows while preserving
production defaults
- unify settings section cards, subtitles, and agent runtime rows

## Validation

- repository format, lint, type, and file-size checks
- 4,538 desktop tests and 2,270 native desktop tests
- desktop and web production builds
- 1,261 mobile tests in the completed full gate
- focused Playwright appearance, sidebar, settings, pairing, and runtime
coverage

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz>
2026-08-11 10:25:23 -07:00
b0795a10ea Add Send to channel for thread messages (#5305)
## Summary

- Share eligible self-authored or owned-agent thread messages into the
parent channel as new top-level messages.
- Link the shared message back to the exact root thread with a semantic
channel label and excerpt.
- Add a dedicated channel-arrow icon plus ownership and navigation
coverage.

## Validation

- Desktop lint, size, and text guards
- Desktop TypeScript build and all 4,543 unit tests
- Focused Playwright send-to-channel and thread-link navigation tests

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-11 10:18:54 -07:00
be48ce98bd fix(link-preview): reliably render previews sent right after they resolve (#5245)
## Overview

**Category:** fix  
**User impact:** Link previews no longer disappear when a message is
sent while preview metadata or media is still settling. Fast Enter,
rapid Enter, and confirmed-draft auto-send now preserve the preview
without duplicate sends or stale tags.

**Problem:** The composer could look ready before its sender-authored
snapshot tag existed. Send paths could then race preview
resolution/upload, while debounced preview state could attach a tag for
a URL that had already been removed. The same timing also caused
confirmed-draft auto-send to be consumed without sending.

**Solution:**
- Debounce preview resolution to avoid card flicker while typing, then
disable every submit path while a supported external preview settles. A
2-second escape cap still permits a bare-link send if resolution stalls.
- Keep submit synchronous: acquire a composer-local lock before
asynchronous send work, read ready tags from the live URL set, and
reject Enter/form submits while a snapshot is pending.
- Retry confirmed-draft auto-submit until preview settling clears, then
submit exactly once.
- Upload thumbnail and favicon independently. A failed upload shows a
toast and degrades to the surviving media (or text-only) rather than
leaving the card spinning.
- Exclude message-edit mode from preview resolution, upload, and Save
gating. Edit-time preview snapshots remain follow-up #5273.
- Canonicalize fragment-bearing URLs for preview lookup/snapshot
identity while preserving the original fragment links in message text.

## Link preview state walkthrough

Captured using PR #5245's actual public Open Graph metadata and artwork.
The deterministic E2E bridge controls only upload timing so the
transient disabled state can be captured reliably.

| State | Expected behavior | Screenshot |
| --- | --- | --- |
| **1. Snapshot upload pending** | The real PR preview is visible, but
Submit remains disabled until its sendable snapshot tag is ready. Click
and Enter cannot send a bare link during the settling window. | ![PR
5245 pasted with its real preview visible and Submit
disabled](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5245/01-real-pasted-submit-disabled.png)
|
| **2. Snapshot ready** | Once snapshot upload settles and the tag is
ready, the same preview remains and Submit becomes active. | ![PR 5245
preview ready in the composer with Submit
enabled](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5245/02-real-resolved-submit-enabled.png)
|
| **3. Message sent** | The sent event carries the snapshot tag and
renders the PR title, description, and artwork inline instead of
degrading to a bare URL. | ![PR 5245 real link preview rendered inline
in the message
list](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5245/03-real-sent-preview-inline.png)
|

## Regression coverage

- Enter during metadata resolution or snapshot upload cannot send early.
- Paste-and-immediate-Enter sends after settling; rapid Enter submits
exactly once.
- Confirmed-draft auto-send waits for settling and fires exactly once.
- Removed/replaced URLs cannot leak stale snapshot tags or media refs.
- Thumbnail upload failure toasts and sends with the surviving favicon.
- Edit mode does not resolve/upload previews or gate Save.
- Fragment variants share a canonical preview while original fragment
links remain clickable.
- Existing ready-preview, suppression, bare-link fallback, and
multi-preview behavior remains covered.

## Reproduction steps

1. Open a channel and paste a supported external URL into the composer.
2. Press Enter immediately, before preview metadata/media finishes
settling.
3. Before this fix, the event could be sent without its preview snapshot
(or confirmed-draft auto-send could be lost). With this fix, submit
waits behind the disabled state and fires once with the matching
snapshot tag.
4. Remove or replace the URL and press Enter inside the debounce window.
The sent event contains tags only for URLs still present in the
submitted content.

## Validation

All required PR checks are green, including Desktop Core, Desktop Smoke
E2E shards, Desktop E2E Integration shards, macOS build, security
checks, and DCO.

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-10 20:27:30 -07:00
3f2f32641f Polish desktop onboarding flow (#5310)
## Summary
- standardize onboarding navigation and horizontal step transitions
- refine the avatar editor with live preview, segmented modes, search,
skin tones, and reduced-motion-safe feedback
- simplify harness/default-model actions and supporting copy

## Testing
- desktop typecheck and static guards
- desktop E2E build
- 9 focused onboarding smoke tests
- 4 focused onboarding/profile integration walkthroughs
- 4,535 desktop unit tests

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
2026-08-10 14:33:23 -07:00
Taylor HoandGitHub 1922d49cb2 feat(desktop): adding rich link previews to messages (#3818)
## Overview

**Category:** improvement  
**User impact:** Link previews appear in the composer and travel as
privacy-safe sender-authored snapshots, so recipients never contact the
linked site merely by opening a conversation.
**Problem:** Cold-cache link paste could freeze the composer before the
URL painted; recipient-side unfurling leaked visits; invalid or
unresolved preview work could interfere with sending or leave dead cards
behind.
**Solution:** Paint pasted links before starting cold resolver work,
resolve only in the sender's composer, attach only complete validated
snapshots at Send, and render authored snapshots without recipient
fallback fetching.

## Behavior

- **Cold paste stays responsive:** bare and angle-bracket URL paste
paths commit the visible link before resolver work begins.
- **Sender-only fetching:** metadata is resolved while composing;
recipients render only the sender-authored snapshot.
- **Send never waits:** pending, failed, invalid, and unsendable
previews are omitted. They do not block or cancel the message.
- **Terminal misses disappear:** failed, timed-out, or 404 resolver
results remove the composer card while preserving visible link text.
- **Display-text links work:** Markdown links such as `[review the pull
request](…)` produce and send the same snapshots as bare URLs.
- **Compact and Rich presentation:** Compact remains the default; Rich
preserves source description line breaks and paragraphs.
- **Immediate draft-wide dismissal:** clicking × immediately hides all
previews for the draft, suppresses links pasted later, and emits only
`["link-preview", "none"]`. No confirmation detour. Suppression resets
after send or clearing the draft.
- **Zero recipient fallback:** missing, stale, malformed, off-relay,
unsupported, or suppressed snapshots remain ordinary visible links;
recipients never regenerate them.

## Implementation

- Resolve previews from deferred composer URL state so paste can paint
first.
- Upload finished preview media to the active community relay and
snapshot only valid, sendable media references.
- Atomically capture ready snapshots at submit time; never append a late
preview after send.
- Validate snapshot and suppression tags in desktop/native and relay
ingestion, rejecting duplicate or mixed forms.
- Render composer previews as stable 55px attachment cards at desktop
and narrow widths.
- Add deterministic E2E coverage for cold paste,
ready/pending/failed/invalid previews, display-text links, multiline
Rich descriptions, immediate dismissal, later-pasted links, and
suppression reset.

## Validation

Validated head: `9807ba8952f190e76153834abf8ab61dd40be5e2`

- Push hooks passed: `check-push-org`, branch skew, desktop check,
mobile tests, desktop tests, Rust tests, and desktop Tauri checks.
- Focused screenshot E2E at the validated head: 5/5 passed across
Compact/Rich composer and recipient states, 800px/420px geometry,
display-text links, multiline descriptions, and immediate dismissal.
- PR CI was triggered for this exact head and is currently running;
completed checks are green at the time of this update.
- Worktree is clean and both PR head and validated branch resolve to
`9807ba895…`.

## Screenshots

### Compact composer

| Loading | Ready |
|---|---|
| ![Compact composer
loading](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3818/compact-composer-loading.png)
| ![Compact composer
ready](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3818/compact-composer-ready.png)
|

### Rich composer

| Loading | Ready |
|---|---|
| ![Rich composer
loading](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3818/rich-composer-loading.png)
| ![Rich composer
ready](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3818/rich-composer-ready.png)
|

### Responsive composer

| 800px loading | 800px ready |
|---|---|
| ![800px composer
loading](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3818/composer-800-loading.png)
| ![800px composer
ready](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3818/composer-800-ready.png)
|

| 420px loading | 420px ready |
|---|---|
| ![420px composer
loading](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3818/composer-420-loading.png)
| ![420px composer
ready](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3818/composer-420-ready.png)
|

### Recipient presentation

| Compact | Rich |
|---|---|
| ![Recipient
compact](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3818/recipient-compact.png)
| ![Recipient
rich](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3818/recipient-rich.png)
|

### Display-text Markdown link

| Composer | Recipient |
|---|---|
| ![Display-text link in
composer](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3818/display-text-composer.png)
| ![Display-text link with recipient
preview](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3818/display-text-recipient.png)
|

### Rich multiline description

![Rich preview preserving description
paragraphs](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3818/rich-multiline-recipient.png)

### Immediate dismissal

| Before × | Immediately after × |
|---|---|
| ![Preview before immediate
dismissal](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3818/dismissal-before.png)
| ![Composer immediately after preview
dismissal](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3818/dismissal-after.png)
|

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-07 10:56:08 -07:00
6eb65919f1 feat(identity): recover desktop identity from a signed-in phone (#4845)
**Category:** new-feature
**User Impact:** People who lose a desktop identity can securely restore
it from a signed-in Buzz phone without creating a replacement identity.

**Problem:** A fresh or identity-lost desktop could not recover its
existing full Buzz identity from an already-authorized phone.

**Solution:** Add a SAS-confirmed reverse NIP-AB transfer, durable
desktop import, a dedicated mobile recovery entry point, and clearer
desktop recovery dialogs with tested loading, drag-and-drop, and failure
states.


https://github.com/user-attachments/assets/e9215c9c-80d0-462f-9161-0fa184ca2f74

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

**crates/buzz-core/src/pairing/session.rs**
Adds the reverse encrypted payload and source-completion state
transitions used for phone-to-desktop recovery.

**desktop/src-tauri/src/commands/identity.rs**
Exposes the existing guarded identity commit path for recovery imports.

**desktop/src-tauri/src/commands/pairing.rs**
Adds recovery-mode pairing, durable nsec import, start serialization,
stale-task protection, and explicit rejection of unsupported recovery
payloads.

**desktop/src-tauri/src/lib.rs**
Registers the recovery pairing command.

**desktop/src/app/App.tsx**
Refreshes the recovered identity before continuing onboarding.

**desktop/src/features/onboarding/machineOnboarding.ts**
Adds recovery transitions to the onboarding state machine.

**desktop/src/features/onboarding/ui/BackupPasswordTimeline.tsx**
Adds the visual backup-to-password-to-unlock progression.

**desktop/src/features/onboarding/ui/IdentityRecoveryPairing.tsx**
Implements QR generation, copy fallback, SAS confirmation, cancellation,
expiry, and completion UI.

**desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx**
Connects private-key, phone, and backup recovery paths to the onboarding
flow.

**desktop/src/features/onboarding/ui/NostrKeyImportForm.tsx**
Polishes recovery dialogs, backup drag-and-drop, loading stability, and
security copy.

**desktop/src/shared/api/tauri.ts**
Keeps the existing pairing API surface focused on standard
desktop-to-mobile pairing.

**desktop/src/shared/api/tauriPairing.ts**
Adds the recovery pairing invoke without growing the ratcheted shared
API file.

**desktop/src/testing/e2eBridge.ts**
Mocks recovery pairing commands and lifecycle events for browser tests.

**desktop/tests/e2e/identity-lost.spec.ts**
Covers lost-identity entry, QR/copy recovery, SAS, cancellation, expiry,
success, errors, backup import, drag-and-drop, and screenshots.

**desktop/tests/e2e/onboarding.spec.ts**
Verifies recovered identities continue through harness setup without
replacement-key side effects.

**mobile/lib/features/pairing/pairing_page.dart**
Adds recovery-only scanning and explicit identity-handoff warnings.

**mobile/lib/features/pairing/pairing_provider.dart**
Recognizes recovery codes, returns the signed-in nsec after mutual SAS
approval, and waits for desktop completion.

**mobile/lib/features/settings/settings_page.dart**
Accepts the recovery route builder at the app composition boundary to
preserve feature isolation.

**mobile/lib/features/settings/settings_page/connection_section.dart**
Adds the signed-in “Send identity to desktop” settings action.

**mobile/test/features/pairing/pairing_page_test.dart**
Covers recovery-only validation and handoff messaging.

**mobile/test/features/pairing/pairing_provider_test.dart**
Covers reverse payload encryption, confirmation ordering, success,
failure, timeout, and cleanup.

</details>

## Reproduction steps

1. Launch Buzz Desktop with identity-lost state and choose **Recover
from your phone**.
2. Confirm the QR and persistent **Copy pairing code** fallback appear
without layout shift.
3. On a signed-in phone, open **Settings → Send identity to desktop**,
scan or paste the recovery code, and compare the six-digit SAS on both
devices.
4. Confirm on both sides and verify Desktop restores the identity and
continues to harness setup.
5. Repeat from identity-lost state with **Recover from a backup file**;
verify picker and drag-and-drop both advance to password entry and
restore the encrypted backup.
6. Exercise cancellation, mismatched/unsupported codes, expired
sessions, and an invalid backup; verify each returns actionable,
non-stuck UI.

## Screenshots

### Desktop phone recovery — complete flow

| Recovery entry | Pairing QR | Code match | Receiving identity |
|---|---|---|---|
| ![Desktop recovery
entry](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/desktop-phone-01-recovery-entry.png)
| ![Desktop phone recovery
QR](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/desktop-phone-02-qr.png)
| ![Desktop security-code
match](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/desktop-phone-03-sas.png)
| ![Desktop receiving
identity](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/desktop-phone-04-receiving.png)
|

### iOS Simulator — complete handoff flow

| Settings entry | Recovery scanner | Manual recovery code | Code
confirmation |
|---|---|---|---|
| ![iOS Settings entry for Send identity to
desktop](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/ios-01-settings-entry.png)
| ![iOS recovery scanner
entry](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/ios-02-recovery-entry.png)
| ![iOS manual recovery code
entry](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/ios-03-manual-code.png)
| ![iOS security-code
confirmation](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/ios-04-sas-verification.png)
|

### Encrypted backup recovery — adjusted file flow

| File picker | Drag-and-drop target | Password step |
|---|---|---|
| ![Desktop encrypted-backup file
picker](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/desktop-backup-01-file-picker-settled.png)
| ![Desktop encrypted-backup drag-and-drop
target](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/desktop-backup-02-drag-drop.png)
| ![Desktop backup password
step](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4845/desktop-backup-03-enter-password.png)
|

## Verification

- `cargo test -p buzz-core pairing` — 71 passed
- `just mobile-test` — 1,169 passed
- `pnpm build:e2e && pnpm exec playwright test identity-lost.spec.ts
--project=smoke` — 15 passed
- Full pre-push gates — desktop checks, desktop unit tests, Rust tests,
Tauri checks, and mobile tests passed

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-06 11:47:18 -07:00
19b41e9c8e fix(desktop): stop rate-limited reconnect backfill from tearing down the authenticated socket (#4990)
## Problem

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

Mechanism (all on `main`):

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

## Fix

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

## Red/green proof

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

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

## Verification

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

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

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

---------

Signed-off-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz>
Signed-off-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
2026-08-06 07:39:17 -07:00
16cc3de6d6 fix(desktop): enforce owner-only access in internal builds (#4053)
## Problem

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

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

## Changes

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

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

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

## Screenshots

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

## Tests

Added coverage for:

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

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

---

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

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Signed-off-by: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz>
Co-authored-by: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz>
Co-authored-by: Amp <amp@ampcode.com>
2026-08-05 19:58:24 -06:00
eb6a37569d fix(desktop): enable message editing in Inbox (#2198)
### What changed?

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

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

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

### Why?

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

### How is it tested?

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

Added tests:

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

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

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Signed-off-by: npub1ft62tztwwm2x9xamk25smmuaj4sfckdkldksruf2x2jwqalffkrq0g7arr <4af4a5896e76d4629bbbb2a90def9d95609c59b6fb6d01f12a32a4e077e94d86@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1ft62tztwwm2x9xamk25smmuaj4sfckdkldksruf2x2jwqalffkrq0g7arr <4af4a5896e76d4629bbbb2a90def9d95609c59b6fb6d01f12a32a4e077e94d86@sprout-oss.stage.blox.sqprod.co>
2026-08-05 17:00:23 -06:00
719f9730d4 feat(desktop): allow leaving your final community (#3621)
**Category:** improvement
**User Impact:** People can leave their final Buzz community and return
to **Join or create a community** without losing their signed-in
identity.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

</details>

### Reproduction steps

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

### Test plan

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


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

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: npub14ndfusear8wdpe4kss8h7juc7wjk78atnqzf63zvppcpneknv4sq6x9370 <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: npub14ndfusear8wdpe4kss8h7juc7wjk78atnqzf63zvppcpneknv4sq6x9370 <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-05 14:02:43 -07:00
25a9cf1be6 feat: paste composer text without formatting (#4801)
## Summary

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

## Testing

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

## Manual verification

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

Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-04 16:16:14 -07:00
e30db7028f feat(projects): support multiple repositories (#4671)
## Summary
- adopt the finalized NIP-MP project model so one project can enumerate
and switch between multiple NIP-34 repositories
- add project and repository navigation, activity summaries,
existing-repository attachment, and repository access-channel management
- preserve privacy-safe activation provenance for agent-authored
patches, pull requests, issues, and associated commits

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

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

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-04 17:30:12 -04:00
ce3cf3cd25 Polish Huddle voice controls (#4694)
## Summary

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

## Why

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

## Validation

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

---------

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

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

## What changed

### Rust

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

### Build / CI

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

### TypeScript

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

### Tests

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

## Preservation of explicit opt-outs

Users who have previously toggled the setting off are unaffected:

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

## Result

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

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-04 16:02:34 -04:00
e5efd04705 fix(desktop): close reconnect gaps that previously required CMD+R (#4737)
> Opened by Brain (agent) on behalf of @wesbillman.

## Problem

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

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

## Fixes

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

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

## Validation

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

## Not covered / follow-ups

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

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: npub1gjuws2a2dc8z2nszprtg7v6u9q7ffeah3hgl5yx45jwn7y7aqs6s5e9xj6 <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz>
Co-authored-by: npub1yxv5wk0u0fh6dwt925wntn7h397jvteyj4r87ttcd9xae7n2t3lqqj9jmm <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
Co-authored-by: npub1gjuws2a2dc8z2nszprtg7v6u9q7ffeah3hgl5yx45jwn7y7aqs6s5e9xj6 <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz>
2026-08-04 18:33:42 +00:00
f86dfc5883 feat(desktop): surface config diff in restart-required badge (#3637)
The "Restart required" badge reports that an agent's running config has
drifted from its spawn-time config, but never says what changed. This
ships the full feature: a typed Rust diff engine and a TS/UI layer that
renders it at every badge site.

## Rust core (spawn-snapshot diff engine)

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

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

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

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

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

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

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

## TypeScript / UI layer

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

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

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

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

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

## Wire shape

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

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

## Tests

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

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

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

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-04 12:21:59 -04:00
f18a9cb106 Defer desktop media uploads until send (#4522)
## Summary

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

## Why

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

## Validation

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

Updated phase snapshots are included in the PR comments.

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

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-04 06:59:43 -07:00
631b05c883 feat: ship Buzz Term (#4347)
## Summary

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

## Banner behavior

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

## Screenshots

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

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

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

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

## Verification at `98ebc8f9048bd5f0ceb7e843b67874d642f0b7fd`

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

## Follow-ups in this PR

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

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

---------

Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Co-authored-by: npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6 <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@buzz.block.builderlab.xyz>
Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
2026-08-03 21:51:17 -04:00
b29c8cdaa4 feat(desktop): redesign the Huddle experience (#4281)
## Summary

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

## Why

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

## Validation

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

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-03 17:16:09 -07:00
985cdcc6ea feat(agents): model-tuning parity in global Agent Defaults editor (#4578)
## Overview

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

## What changed

### Phase 1 — Catalog projection

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

### Phase 2 — Field model

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

### Phase 3 — UI

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

### Phase 4 — Tests

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

## Result

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

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

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

Snapshots are attached in a follow-up comment.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-03 14:12:21 -07:00
d4a4570b97 fix(desktop): clarify inherited agent parallelism (#4010)
## Summary

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

## Why

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

## Testing

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

---------

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

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

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

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

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

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

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

</details>

## Reproduction steps

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

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
2026-08-03 13:05:47 -07:00
c1b88af8d7 feat(desktop): improve channel template discovery (#4549)
## Summary

- move **Channel templates** from Communities to Personal settings
- always expose the template picker in New Channel, using **None** as
the no-template value
- create a channel template directly from the picker and select it on
return
- preview the selected template's current visibility, canvas, agents,
and teams
- order the channel-creation controls as **Type / Visibility /
Template** and mark Template **Optional**
- cover populated and empty libraries, inline creation, selection,
visibility overrides, mixed agent/team inventory, field order, optional
labeling, and settings navigation in Playwright

## Validation

Validated at desktop-only tip `76442270c88aa1d533ddca5de9f87cd615183919`
with a clean worktree:

- focused channel-template Playwright: 2/2 passed
- Type / Visibility / Template ordering and muted Optional treatment
visually inspected in the replacement screenshot
- `git diff --check origin/main...HEAD` passed
- PR diff contains exactly nine Desktop files and no Mobile files

The pre-push hook was bypassed only for the corrected history push
because the inherited Mobile test `keeps follow mode off while a tall
newest message stays visible` passes in Linux CI but fails on macOS
because its offscreen-child mounting assertion is platform-sensitive. No
Mobile code or tests are changed by this PR.

## Screenshot

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

Originating Buzz channel: `efba7343-e147-48b7-a2aa-15a5f04abc57`

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-03 09:30:15 -07:00
f810a2f49e fix(desktop): make OpenAI key re-enterable after first save in card mint dialog (#4140)
Fixes a write-once dead-end in the card mint dialog where a user with an
expired OpenAI key had no way to replace it.

**Source-aware key status (Rust + TypeScript).** `card_mint_key_status`
returns a layer discriminant (`"none" | "global" | "persona" | "agent" |
"process"`) instead of a boolean. A pure `resolve_key_layer()` helper in
`card.rs` owns the classification logic; `card_mint_key_status`
delegates to it, so the production path is under direct test with no
duplicate logic.

**Mint form always reachable.** The key panel replaces the mint form
only for `none` (first-time setup) or when the user explicitly opens the
edit panel (`editingKey`). Keys from agent/persona/process layers show
an inline provenance row on the mint form with a "Why?" affordance;
clicking it shows the read-only redirect in a panel with a Cancel button
that returns to the mint form — never a terminal state.

**Precise auth-error matching.** The 401 handling in `cardMintStore.ts`
matches `startsWith("Card mint failed (HTTP 401 ")` plus the specific
`Incorrect API key` text, so avatar-fetch 401 errors pass through
unchanged.

**Tri-state key status row.** "Using your saved OpenAI key · Update"
renders only when `keyLayer === "global"` (confirmed writable key).
Query pending or errored hides the row without asserting key existence.

**Real tests.** Panel visibility derivations live in
`cardMintKeyUtils.ts`, which `AgentCardMintDialog.tsx` imports directly.
Tests cover all layers including the mint-reachability invariant (Mint
reachable for every resolved layer; only `none` gates setup).

- `card.rs` — new `resolve_key_layer()` pure helper;
`card_mint_key_status` delegates to it; 999 lines (under the 1000-line
ratchet)
- `card/tests.rs` — precedence test calls `resolve_key_layer()` directly
(no test-local closure); adds process-layer and blank-value cases
- `tauriPersonas.ts` — `CardMintKeyLayer` type; updated
`cardMintKeyStatus` signature
- `cardMintKeyUtils.ts` — `showKeyPanel`, `showReadOnlyRow`,
`showCancelButton`, `keyPanelTitle`, and helpers; component imports all
of them
- `AgentCardMintDialog.tsx` — inline provenance rows for all key
sources; key panel only for setup/edit; no unused variables
- `cardMintStore.ts` — precise 401 prefix matching
- `e2eBridge.ts` — `card_mint_key_status` stub returns `"global"` (not
boolean)
- Tests: 3959 JS passing, 2089 Rust passing, `tsc --noEmit` clean

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

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1ng3jzsaqxdhrfq22dg85j3lpr0zsh3jp7g2h9jyxl59wraayapnsu6kvfg <9a232143a0336e34814a6a0f4947e11bc50bc641f21572c886fd0ae1f7a4e867@buzz.block.builderlab.xyz>
2026-08-03 11:12:09 -04:00
2c0ac24674 fix(desktop): stop the create-agent provider config probe from erasing keystrokes (#4411)
## What

Fixes the create-agent dialog's "Run on" provider config fields eating
keystrokes — reported by Tyler in buzz-remote-agents (channel
`29414326`, thread `db76677a`): the Kubernetes **Kubeconfig context**
field would not accept typing.

## Why it happened (the Typewriter Eraser, shipped in #4289)

`WhereToRunSection`'s probe `useEffect` depended on the whole `draft`:

1. every keystroke changed the draft → effect re-fired → provider binary
re-probed;
2. each probe result is a fresh object written into the draft → the
effect re-triggered **itself**, respawning the provider binary in a loop
for as long as the dialog sat on a provider;
3. every probe resolution reset `providerConfig` to schema defaults —
erasing whatever was typed. A field with no schema default (`context`)
snapped back to empty, i.e. "won't let me type". Unrelated to how many
kubeconfig contexts you have.

## Fix

- **Probe once per provider selection**, keyed on the provider's stable
`binaryPath` — not the draft, not the provider object (a
`useBackendProvidersQuery` refresh must not reprobe an unchanged
selection).
- **Latest-state resolution** via `React.useEffectEvent` + a new pure
`applyProbeResult` helper: schema defaults merge **beneath** the current
`providerConfig`, so a probe landing after the user typed can never
clobber in-flight input (per Wren's pre-patch red-team: changing deps
alone leaves a stale closure).

Existing `cancelled` cleanup keeps provider-switch/unmount safe;
selection reset (`emptyWhereToRunDraft`) and the fail-closed probe-error
path are unchanged.

## Tests

- **Unit** (`whereToRunIntent.test.mjs`): `applyProbeResult` merge
semantics — defaults under typed values, user-cleared fields stay
cleared, schema-less results, unrelated fields preserved.
- **E2E** (new `where-to-run-config.spec.ts`, added to the smoke
project, **red-first verified**: all 3 fail against the unfixed
component):
- typing into a defaultless provider field sticks, and
`probe_backend_provider` fires exactly once per selection;
- the config form is gated on probe resolution (slow probe: no
half-rendered form, defaults prefill once);
  - provider → local → provider re-probes and resets cleanly.
- Mock bridge gains `backendProviders` / `backendProviderProbeResult` /
`backendProviderProbeDelayMs` seams (defaults preserve prior behavior).

## Verification at 8eb7680

- `pnpm check` + `tsc` clean, `pnpm test` 3926/3926;
- new spec 3/3 green (and 3/3 red on the unfixed component);
- pre-push lefthook: desktop-test, desktop-check, desktop-tauri-checks,
rust-tests, mobile-test all green.

---------

Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
2026-08-03 07:36:13 -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
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
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
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
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
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
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
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