Commit Graph

295 Commits

Author SHA1 Message Date
Kalvin C
be8d7756d5 feat(agent): discover Databricks Unity Catalog models (#6918)
## Summary

Add Unity Catalog model-service discovery to `buzz-agent` Databricks v2,
preserve full Unity Catalog FQNs through ACP model discovery, route
model services through MLflow chat, and add the optional
`DATABRICKS_MODEL_FILTER` visibility filter.

The resilience follow-up bounds each catalog attempt across headers,
bounded body reads, and JSON parsing; retries transient 499/5xx,
transport/body failures, malformed JSON, and attempt timeouts; and emits
only safe catalog/status diagnostics.


## Validation

The live validation and test gates below were run on the pre-rewrite tip
`1cf6c66e7c5bf9a00c357bda24df48e3beeb310b`. The rewritten tip
`0364cf44f3eb5cfbd5771b0a0dd7868ce0ad2c19` has identical trees for both
commits, so the attribution rewrite changes no tested code.

- 31 catalog tests passed
- 19 Databricks OAuth/routing integration tests passed
- `cargo check -p buzz-agent` passed
- `cargo clippy -p buzz-agent --all-targets -- -D warnings` passed
- `cargo fmt --check` passed
- `git diff --check` passed
- Desktop Tauri validation passed through the repository-supported setup
path, including 2,777 library tests and auxiliary terminal suites

The full `buzz-agent` package has one separately reproduced pre-existing
failure: `regressions::cancel_kills_inflight_tool_via_mcp_notification`,
failing on both baseline `eb8e97cfe` and the follow-up worktree. No
Databricks path reaches that test. The live validation proves discovery
and ACP exposure, not inference against every listed model.

---------

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-27 13:37:12 -07:00
klopez4212
4d77175c78 fix(client): resurface hidden DMs from live activity (#6885)
## Summary

Hiding a DM row and then receiving a new message from that person never
brought the row back — the reader was left with only an Inbox
notification. This restores the row from live activity, client-side,
with no relay or database changes.

- Subscribe to hidden-DM activity **channel-scoped** over the hidden-DM
id set (`#h`), re-registering whenever the set changes. The relay's
`fan_out_scoped` only routes channel-scoped events (every kind:9 DM
carries a `channel_id`) to channel-scoped subscriptions, so a
community-global `#p` filter receives nothing. Hiding a DM never drops
membership, so `#h` authorization holds. Because every delivered event
is already for a hidden DM the reader belongs to, eligibility no longer
needs a self `p` tag — untagged CLI/agent DMs resurface too — and no
per-event visibility fetch is required.
- On qualifying activity, invoke the existing open-DM action (`41010`)
to reopen the hidden one-to-one or group DM, validating that the relay
reopened the same channel.
- Preserve the existing relay-synced hide snapshot (`41012` → `30622`);
hide state still syncs across devices.
- Let Inbox reopen a hidden DM before navigating to its message, with a
busy state on the row and context-menu "Open in channel" action while a
reopen is pending.
- Coalesce resurface attempts per channel: a concurrent message that
arrives while a reopen is in flight triggers a retry on failure instead
of being marked handled and dropped.
- Write exact dm-visibility query keys on open/hide mutations.

This is the client-only replacement agreed in the Buzz design thread and
supersedes #6713. The accepted limitation is that resurface only fires
while a client is running — a message that arrives while all clients are
closed does not restore the row on next launch (the `30622` snapshot has
no watermark); Inbox click-through is the safety net for that case.

Addresses #6316.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
2026-08-27 10:29:26 -04:00
tulsi
ceb8ba605f fix(desktop): accent-colored mention badges that count thread mentions (#6900)
**Category:** fix
**User Impact:** Sidebar mention numerals now match the user's
theme/accent color and appear for mentions inside threads, not just
top-level channel messages. Hover-to-preview on channel rows is
unchanged.

**Problem:** Follow-ups to #6696:

1. The mention numeral used a fixed red `--notification` token that
ignored the user's theme and accent selection.
2. The numeral read `appBadgeCount`, which deliberately excludes
threaded replies (Home's badge subtotal counts those toward the Dock
badge). Mentions inside threads therefore never surfaced a numeral on
the channel row — only the generic thread-activity dot.

**Solution:**

1. The badge now reuses the primary accent pair (`bg-primary` /
`text-primary-foreground`), the same treatment as the DM count badge, so
it follows whatever accent the user applies — including the pinned
neutral accent of the Buzz themes. The now-unused `--notification`
tokens and Tailwind utilities are removed.
2. Non-DM rows now count every unread mention/broadcast — threaded or
top-level — via a new `highPriorityCount` channel projection, replacing
the boolean `highPriorityUnread` across the TS contract, the Tauri
SQLite store, the e2e mock bridge, and the native test rig. The Dock/app
badge projection (`appBadgeCount`) is unchanged, so Dock totals do not
double-count thread activity that Home already counts.

The hover-to-preview channel activity popover is untouched; e2e coverage
now pins the combination (thread mention → numeral, no dot, popover
still opens and lists the mentioning reply).

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

**desktop/src/features/channels/useUnreadChannels.ts**
Non-DM sidebar counts switch from `appBadgeCount` to the
mention/broadcast-inclusive `highPriorityCount`; the high-priority
channel set derives from the same count.

**desktop/src/features/sidebar/ui/SidebarSection.tsx**
Badge drops the fixed notification classes and inherits the
accent-following default.

**desktop/src/shared/styles/globals/theme.css /
desktop/tailwind.config.js**
Remove the unused fixed `--notification` tokens and semantic utilities.

**desktop/src/shared/api/tauriObservedUnread.ts /
desktop/src-tauri/src/observed_unread.rs**
Projection contract: `highPriorityUnread: bool` → `highPriorityCount:
u64`, aggregated per unread high-priority event.

**desktop/src/testing/e2eBridge.ts /
desktop/src/features/channels/observedUnreadNativeRig.mjs**
Mock/native-rig projections mirror the new count field.

**desktop/src/features/channels/observedUnreadNative.test.mjs**
Native badge-lane test seeds a high-priority event to match the
numeral's new semantics.

**desktop/tests/e2e/badge.spec.ts**
Top-level mention test now asserts the badge equals the applied accent
(seeded `#22c55e` under an adversarial theme). New test: in-thread
mention shows the numeral, no dot, and the hover preview popover still
lists the mentioning reply.

**desktop/tests/e2e/channel-activity-popover.spec.ts /
desktop/tests/e2e/thread-unread.spec.ts**
Seeded fixtures that mention the user now expect the numeral (which
subsumes the dot).

</details>

## Reproduction steps

1. Pick any theme + accent color in Settings → Appearance.
2. Receive a top-level message mentioning you in an inactive channel →
the row shows a numeral pill in your accent color.
3. Receive a reply **inside a thread** that mentions you in an inactive
channel → the row shows the numeral (previously only a dot).
4. Hover the row → the channel activity preview still opens and lists
the mentioning reply.
5. Switch accents/themes → the pill re-colors accordingly; the Dock
badge count is unchanged from before.

## Verification

- `tsc --noEmit`, Biome, `check:px-text` — clean
- Desktop unit tests: 179/179 channel-suite; full suite has one
pre-existing failure from #6590 (`useRetainedProjectGitViews.test.mjs`
loader error, fails on clean `main`)
- Tauri Rust: `observed_unread` tests + clippy + fmt — clean
- Playwright smoke: badge (19), channel-activity-popover (10),
thread-unread (13), plus channels/inbox/stream/messaging/mentions suites
— all green

## Screenshots

Screenshots are posted in the PR discussion using immutable
repository-hosted image URLs.

Signed-off-by: tulsi <tulsi@block.xyz>
2026-08-26 17:44:05 -04:00
Jordan Mecom
01c87a350b Apply access policy when reusing channel agents (#6838)
Reused managed identities now apply their intended inbound-author policy
before attachment or startup. An explicit policy wins, then the linked
persona policy, then `owner-only`.

This covers both channel-agent provisioning and the direct composer
autocomplete path. If the policy update fails, attachment, startup, and
message send stop.

The production Playwright regression starts with a persona-linked agent
set to `anyone` with a stale allowlist, then verifies the update happens
before membership and startup and that the stored policy is
`owner-only`. A small resolver test covers explicit, persona, and
default precedence.

This closes the stale-policy entry described by [Project Loupe issue
#497](https://github.com/project-loupe/audit-buzz/issues/497). The
separate Welcome Team reuse path is outside this PR.

Checked with the 5,502-test desktop suite, `pnpm check`, `pnpm
typecheck`, `pnpm check:file-sizes`, and the focused Playwright composer
regression.

---------

Signed-off-by: Jordan Mecom <jm@squareup.com>
2026-08-26 13:22:09 -07:00
thomaspblock
de188ebf06 feat(projects): add agent and CLI project-home support (#6590)
## Summary
- inject bounded project-home identity and repository context into
managed agent sessions
- add project-aware CLI flows for creating projects, repositories,
issues, and related channels
- match project homes through the existing relay query surface, then
filter channel metadata client-side

This is Part 1 of the channel-first Projects stack. Part 2 contains
project creation and model foundations.

## Testing
- `cargo fmt --all -- --check`
- `cargo clippy -p buzz-cli -p buzz-acp --all-targets -- -D warnings`
- `cargo test -p buzz-cli -p buzz-acp` — 1,184 tests passed, 1 doc test
ignored
- full pre-push gate passed

## Post-Deploy Monitoring & Validation
- validate project-home agent context and project-aware CLI commands
against a staging relay
- healthy signals: project context matches the active channel, explicit
repo coordinates remain stable, and normal channels receive no project
block
- failure signals: cross-channel project context, duplicate project
creation, or commands targeting an unrelated repository; mitigate by
reverting this PR

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
2026-08-26 12:09:06 -07:00
Taylor Ho
e760c51820 feat(workflows): discover trigger filter values (#6712)
**Category:** improvement
**User Impact:** Workflow authors can discover people and messages while
configuring trigger filters, then see readable enriched labels instead
of raw identifiers.

**Problem:** Author and message filters required users to know and paste
raw public keys or event IDs, and configured workflows surfaced those
opaque values afterward. **Solution:** Add network-backed pickers and
presentation enrichment while keeping deterministic local public-key and
event-ID fallbacks authoritative whenever discovery is unavailable or
untrusted.

Related issue: none found.

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

**desktop/src/features/workflows/ui/WorkflowAuthorPicker.tsx**
Adds channel-aware author discovery, profile search, keyboard
navigation, loading states, and deterministic public-key fallback
selection.

**desktop/src/features/workflows/ui/WorkflowCard.tsx**
Uses enriched trigger presentation when building the workflow card’s
readable summary.

**desktop/src/features/workflows/ui/WorkflowDialog.tsx**
Keeps Escape scoped to an active filter picker before allowing the
inspector or dialog to close.

**desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx**
Threads channel context into trigger filters and renders enriched
author/message summaries in the workflow sequence.

**desktop/src/features/workflows/ui/WorkflowMessagePicker.tsx**
Adds paged channel-history discovery, message search, exact event
lookup, profile labels, keyboard navigation, and bounded results.

**desktop/src/features/workflows/ui/WorkflowRichTriggerDescription.tsx**
Renders compact author identity details and loading presentation inside
trigger summaries.

**desktop/src/features/workflows/ui/WorkflowTriggerConditions.tsx**
Connects author and message filter accordions to their pickers while
preserving selected and excluded condition semantics.

**desktop/src/features/workflows/ui/useWorkflowAuthorPresentation.ts**
Resolves configured author keys to trusted display labels with
deterministic fallbacks.

**desktop/src/features/workflows/ui/useWorkflowTriggerPresentation.ts**
Enriches configured message IDs only after validating the fetched event
and channel.

**desktop/src/features/workflows/ui/workflowAuthorCandidates.test.mjs**
Covers author candidate normalization, ordering, deduplication, and
fallback behavior.

**desktop/src/features/workflows/ui/workflowAuthorCandidates.ts**
Builds stable author candidates from channel members, profiles, and raw
public keys.

**desktop/src/features/workflows/ui/workflowConditionExpression.ts**
Allows message IDs to participate in basic trigger-filter parsing.

**desktop/src/features/workflows/ui/workflowDefinition.ts**
Accepts enriched trigger text when generating workflow card labels.

**desktop/src/features/workflows/ui/workflowMessageCandidates.test.mjs**
Covers event validation, source merging, deterministic ordering, and
exact-lookup enrichment boundaries.

**desktop/src/features/workflows/ui/workflowMessageCandidates.ts**
Validates message candidates by event kind, channel, and exact event ID
before permitting enrichment.


**desktop/src/features/workflows/ui/workflowTriggerDescription.test.mjs**
Covers readable selected/excluded author and message descriptions plus
loading fallbacks.

**desktop/src/features/workflows/ui/workflowTriggerDescription.ts**
Builds concise enriched trigger descriptions while retaining stable
raw-ID fallbacks.

**desktop/tests/e2e/workflow-local-controls.spec.ts**
Exercises picker discovery, selection toggles, Escape ownership, bounded
scrolling, and enriched workflow summaries.

</details>

## Reproduction steps

1. Open Workflows and create a workflow for a channel with members and
message history.
2. Choose **Reaction Added** as the trigger and expand **Author**.
3. Confirm channel members and fetched profile results are discoverable,
searchable, and keyboard accessible; choose one.
4. Expand **Message**, confirm recent channel messages appear in a
bounded list, and choose one.
5. Toggle either selected filter between **is** and **is not**, then
collapse the inspector and confirm the sequence summary stays readable.
6. Add a send-message step and create the workflow; confirm its card
uses the resolved author and message labels.
7. Repeat while discovery is unavailable and confirm raw public
keys/event IDs remain selectable and authoritative.

## Screenshots

### Author discovery

![Author picker showing discoverable channel members and profile
labels](https://raw.githubusercontent.com/block/buzz/df47bdab841e0f52eb1f4ea9ac4e70aa16d240e0/pr-6712--01-author-discovery.png)

### Message discovery

![Message picker showing bounded channel history
discovery](https://raw.githubusercontent.com/block/buzz/df47bdab841e0f52eb1f4ea9ac4e70aa16d240e0/pr-6712--02-message-discovery.png)

### Selected filter summaries

![Workflow builder showing readable selected author and message
filters](https://raw.githubusercontent.com/block/buzz/df47bdab841e0f52eb1f4ea9ac4e70aa16d240e0/pr-6712--03-selected-filter-summaries.png)

### Enriched workflow card

![Workflow card showing enriched author and message
labels](https://raw.githubusercontent.com/block/buzz/df47bdab841e0f52eb1f4ea9ac4e70aa16d240e0/pr-6712--04-enriched-workflow-card.png)

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <0366ccd5ee09c2779a9d6bd6683daa17c16a508a51f6a7e7314018dab8fdc49b@buzz.block.builderlab.xyz>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
2026-08-24 17:40:23 -07:00
morgmart
db5617dd15 fix(desktop): emit singular mention feed category so alerts route correctly (#6665)
## Problem

Mentions — and thread replies that @-mention you — played the **Needs
action** sound instead of the **@Mentions** sound. Reported by
@morgmart: "I set Needs action to a different sound and it's the only
one I ever hear."

## Root cause

Two vocabularies got conflated in #475:

- **Section / filter vocabulary** (plural): `mentions`, `needs_action`,
`activity`, `agent_activity` — the shape of `FeedSections`, the
`--types` filter, and the agent-facing CLI docs.
- **Per-item category vocabulary** (singular for mention): `mention`,
`needs_action`, `activity`, `agent_activity` — the `FeedItemCategory`
contract in `desktop/src/shared/api/types.ts`, unchanged since #12.

The Tauri feed builder reused the filter string `"mentions"` as each
mention item's `category`. Only one word differs between the
vocabularies, so only mentions broke. Every frontend consumer compares
against the singular, so real mentions never matched and fell through to
the resolver's `needs_action` fallback.

The E2E mock bridge emits the singular form, so tests never saw the
drift.

### Symptoms this fixes (all from the one mislabel)

- Mentions and mentioning thread replies played the Needs-action sound
- Mention notifications used the Needs-action title format
- Mentions in muted channels were suppressed (the mute-bypass never
fired)
- Inbox / Home feed labelled mentions "Channel update"
- Channel activity popover's mentions list was always empty

## Fix

**Fix the owner, not the symptoms.** `FeedItemInfo.category` becomes a
`FeedItemCategory` enum whose serde form is exactly the TS union, so a
misspelled category can't compile at the producer. A serialization test
pins each variant to its wire string.

**Frontend:** `slotForFeedKind` maps every known category explicitly.
The `needs_action` fallback for unknown categories is **kept on
purpose** — a contract drift should cost the user the wrong sound, not a
missed alert — but it now `console.warn`s so the drift is visible to
developers instead of masquerading as intended behavior. `e2eBridge.ts`
and `tauri.ts` now derive the category type from `types.ts` instead of
retyping it.

Not touched: the plural `--types` filter and `FeedSections` keys. Those
are the section vocabulary and are correct as-is.

## Verification

- `just ci` green (file-size ratchet, Rust/Tauri/desktop/mobile tests,
desktop + web builds)
- New tests: 2 Rust
(`feed_item_category_serializes_to_frontend_contract`,
`feed_item_from_event_carries_singular_mention_category`), 3 TS in
`sound.test.mjs` incl. one that feeds the old `"mentions"` string and
asserts fallback + warning
- **Runtime, dev build against the production relay:** controlled test
from an agent identity into a test channel —
- mention in channel → @Mentions sound, inbox shows "Mentioned in" ✅
(was Needs-action)
- thread reply with mention → @Mentions sound, once ✅ (was Needs-action)
- plain thread reply in the channel being viewed → silent, as designed ✅

## Reviewers

- @tlongwell-block — #475 introduced the plural category; please confirm
it wasn't intentional
- @wesbillman — owner of the original `FeedItemCategory` contract (#12)
and most of the feed builder
- @taylorkmho — owner of the sound-slot model and resolver (#968); the
fallback-with-warning shape is the part to weigh in on
- cc @klopez4212

---------

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-24 10:20:53 -07:00
Tyler
2d280376ad perf(desktop): persist channel heads, collapse thread reads and reply sends (#6572)
## Summary

Lands the build-now items from the desktop latency plan
(#ui-performance-deep-dive) as one change. Every perceived-latency hot
path a user hits on launch, channel open, thread open, and reply send
drops one or more round trips.

**A1 — persisted channel heads (the big one).** Native WAL SQLite cache
(`desktop/src-tauri/src/channel_head_cache.rs`) keyed by `{pubkey,
relayUrl}` scope, 32 rows/scope LRU, 1 MiB per-row drop cap,
schema-version reset, corrupt-row tolerance, checkpointed on shutdown.
Three blocking-pool commands: `channel_head_cache_load` / `_store` /
`_clear`. On the renderer side, `CommunityQueryProvider` kicks off
hydration of up to 12 heads when it constructs the query client — the
app, splash and relay preconnect mount immediately; only
`useChannelMessagesQuery` awaits the seed (`channelHeadHydration`), then
consumes a one-shot hydrated gate so a hydrated channel pays **zero**
`get_channel_window` calls on mount and exactly **one** on the
post-subscription refresh, whose response replaces page zero wholesale.
That refresh fires whether live-subscription setup succeeds or fails,
and is sequenced behind hydration so it is always a distinct
authoritative fetch (see Review follow-ups). Bounds-only persisted heads
(zero rows) are not hydrated and take the cold loading path. The
timeline loading latch recognizes native-hydrated rows as restart-safe
so they paint immediately instead of holding a skeleton. The cache is a
paint accelerator only — the relay response is always authoritative.
Replaces the legacy localStorage `messageSnapshot.ts` (removed, -401
lines).

Kill switch: `VITE_BUZZ_CHANNEL_HEAD_CACHE=off` at build time or
`localStorage["buzz-channel-head-cache"] = "off"` at runtime. Cache is
cleared on community removal and scoped per identity, so a replaced
signer never sees the previous identity's rows.

**B1 — thread aux in one response.** Relay thread filters accept
`include_aux`; the bridge appends the same authorized two-hop
reactions/edits/deletions closure a channel window gets
(`build_aux_query` shared with the window path). Renderer
`useThreadReplies` drops its two follow-up aux fetches. `next_cursor` is
computed from reply-kind rows only since aux rows are unpaged.
Documented in `docs/bridge-channel-window.md`. Thread queries keep
`staleTime: 0` (`bcfe04e2f`): an earlier revision raised it to 30s,
which CI's `thread-unread.spec.ts` caught — once the user leaves a
channel, the live subscription stops feeding that thread's cache, so a
reopen must always take the (now single) authoritative read.

**B2 — cached root on reply send.** `send_channel_message` gains
`root_event_id`; when the renderer already holds the parent (channel or
thread cache) it passes the NIP-10 root, and native signs without the
relay round trip that `resolve_thread_ref` used to make. Strict hex
parse; `root_event_id` requires `parent_event_id`; absent root falls
back to the existing relay resolution. The renderer never sends a
guessed root.

**B4** general HTTP pool idle 10s→300s, max idle per host 1→2. **B5**
relay preconnect fires as soon as identity is ready instead of waiting
for `requestIdleCallback`. One e2e test (`relay-reconnect.spec.ts`
"service restart close resets accumulated backoff") had been relying on
the idle-callback batching to skip past its own seeded dial failures
before the channel list painted; `8133d70bb` makes it wait for the
connected state instead (test-only, still fails with the 1012 backoff
reset disabled). **B6** profile freshness 60s→10 min (both the in-memory
entry check and the query `staleTime`). Tradeoff: another user's
display-name/avatar edit can take up to 10 min to propagate to a client
that already holds their profile (relay reconnect refetches
`users-batch` but resolves from the still-fresh per-pubkey entry); your
own edits still evict the entry immediately (`evictUsersBatchEntries` in
`useUpdateProfileMutation`).

### Related issue
Follows #6456/#6457/#6459/#6460 (already merged). #6455 is the
measurement instrument and is intentionally not folded in. No duplicate
PR found.

### Review follow-ups
Addressing Carl's reviews
[5001114109](https://github.com/block/buzz/pull/6572#pullrequestreview-5001114109)
and
[5002596542](https://github.com/block/buzz/pull/6572#pullrequestreview-5002596542),
each pushed as new commits (no rebase):

- `4f06b7770` fix(desktop): mount app while channel heads hydrate;
always revalidate — provider no longer gates children on the cache load;
`refreshAfterSubscribe` runs on subscribe failure too; bounds-only heads
skipped at seed; seed merges into an existing window store. +3 tests.
- `35834cb31` fix(relay): drain aux closure hops across the page clamp —
`query_all_pages` walks the `(created_at, id)` keyset via
`until`/`before_id` until a short page (`AUX_PAGE_LIMIT` =
`DEFAULT_MAX_PAGE_LIMIT`, `AUX_MAX_PAGES` = 64 warn+truncate) so
one-shot `limit: 1000` newest-first no longer drops the oldest
edits/deletions. +3 tests; `docs/bridge-channel-window.md` updated.
- `db21b0531` merge of `origin/main` `e23632941` (#6558, #6312 — no
overlap).
- `5a5566c0f` fix(desktop): sequence post-subscribe refresh behind
channel head hydration — `refreshChannelWindowMessages` awaits
`channelHeadHydration()` and, for a hydration-seeded query (`data !==
undefined && dataUpdatedAt === 0`), the in-flight snapshot fetch before
invalidating. Without this, a subscription that settles before the
SQLite load invalidated a data-less in-flight query; TanStack dedupes
that onto the existing fetch (`query-core` `fetch()` only cancels when
`state.data` exists), which returned the seeded snapshot — 0
authoritative fetches. Regression test reproduces Carl's exact ordering
(fails at `35834cb31` with 0 calls), plus a cold-channel guard that the
fix does not double-fetch.
- `b129231c8` fix(desktop): let concurrent post-hydration refreshes
share one window fetch — found independently by Max and Wren reviewing
`5a5566c0f`: subscribe settlement + reconnect both wake on the same
snapshot promise and both invalidate; the second (default
`cancelRefetch: true`) cancelled and replaced the first authoritative
fetch (3 queryFn calls, not 2, and the cancelled Tauri invoke still hits
the relay). The seeded branch now invalidates with `cancelRefetch:
false` so a second waker joins the in-flight fetch; cold/warm keep the
default (`test_canceled_stale_fetch_cannot_overwrite_catch_up_window`
relies on it). Concurrent regression test fails at `5a5566c0f` with 3.

### Testing
At `b129231c8` (PR head; verified in one shell with `git rev-parse HEAD`
= `b129231c8`): `pnpm check`, `tsc --noEmit`, desktop unit 5,393 / 0,
Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` +
`relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh
`build:e2e`, pre-push hooks green.

At `5a5566c0f`: `pnpm check`, `tsc --noEmit`, desktop unit 5,392 / 0,
Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` +
`relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh
`build:e2e`, pre-push hooks green.

At `35834cb31`: desktop unit 5,390 / 0; `cargo test -p buzz-relay --lib`
910 / 0; fmt + clippy `-D warnings` clean; Playwright 32 / 32 (same
specs minus affordance); GitHub CI green on every job except Smoke (3)
(unrelated project-review row-count + messaging timing flake, per Carl)
and Unit Tests (sherpa cache skeleton, below).

Earlier, all at `8133d70bb` (this PR head is `0c492366d` = 8133d70bb + a
comments-only commit correcting two `profile/hooks.ts` freshness
comments from 60s to 10 min; pre-push desktop check/typecheck/test
5,387/0 re-ran at 0c492366d) in one shell; `origin/main` = `040b203f7`
at PR open, since moved to `4baccd539` (#6558, mobile only — zero file
overlap, `git merge-tree` clean):

- `just desktop-test` — 5,387 passed / 0 failed (includes new hook-level
call-count test: cold = 1, stale-prefetched = 1, hydrated = 0 on mount
then 1 on invalidate with wholesale replacement)
- Playwright smoke `relay-reconnect.spec.ts` + `thread-unread.spec.ts` +
`channel-head-restart.spec.ts` — 30/30 (thread-unread was 8/13 at
`7acbf951b`; relay-reconnect was 15/16 at `bcfe04e2f`). The restart spec
persists a head, reloads into a fresh mock relay with the head fetch
held 5s, asserts the persisted row paints within 2s, exactly one
`get_channel_window` after open, and the stale row is removed when the
authoritative page lands.
- `pnpm typecheck`, `pnpm check` — clean

At `7acbf951b` (everything except the two-line `useThreadReplies.ts`
staleTime revert and the test-only `relay-reconnect.spec.ts` change),
also green in one shell:
- `just desktop-tauri-test` — 2,859 passed / 0 failed across the
workspace (channel_head_cache: wire shape, LRU+caps, schema reset,
corrupt-row skip)
- `just test-unit` — 632 passed (buzz-core/auth); `cargo test -p
buzz-relay --lib` — 908 passed / 0 failed
- `just check` components: fmt-check, clippy, desktop-check,
desktop-typecheck, desktop-tauri-fmt-check, desktop-tauri-clippy,
web-check, mobile-check, file-size-check — all green
- `just desktop-build`, `web-build`, `desktop-tauri-check`,
`mobile-test` (1,661 passed) — all green

CI note: the "Unit Tests" job goes red on this PR and on `main` whenever
it hits a poisoned `rust-cache` entry (an empty-directory skeleton of
`target/sherpa-onnx-prebuilt` that `sherpa-onnx-sys` build.rs trusts),
surfacing as `could not find native static library sherpa-onnx-c-api` in
`buzz-voice` — a crate this PR doesn't touch. Deleting the cache entry
and rerunning turned the job green at `0c492366d` (28/28); it re-poisons
on the next `main` push until the workflow clears that directory after
cache restore.

Reviewed in-channel by Wren (9 / 9 / 9.5) and Eva (9 / 9 / 9), and
line-by-line by me before opening; the staleTime fix re-verified by Wren
and me independently; the relay-reconnect test fix bisected and verified
by me.

---------

Signed-off-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
2026-08-24 10:11:07 -04:00
Max Lampert
b85d680fb1 perf(desktop): stop the Projects fan refetching on re-entry and running after leave (#6458)
Entering Projects fires a large fan: an exhaustive paginated relay
enumeration (projects/repos/tombstones), five 2,000-event work-item
queries plus assignment-operation scans, per-repo activity summaries,
and a local-repository filesystem scan. Measured on a large community
(101 issues / 258 PRs):

| query | measured cost |
|---|---|
| work-items (5 × 2,000-event REQs + assignment scans) | 3.5–3.9s |
| activity summaries | 4.1s |
| repository activity | 1.0–2.2s |
| local repository scan | 1.7s |

Two lifecycle bugs made the fan far more expensive than it needs to be:

- **Freshness windows guaranteed a full refetch on nearly every
re-entry** (60s enumeration, 30s work-items/activity, 10s local scan) —
i.e., the costs above were re-paid on almost every visit. Every local
write path already invalidates its keys explicitly (issue/PR mutations,
project creation, repo sync), so the short windows only served
remote-actor freshness. Raised to 5m/2m/2m with a 30m enumeration cache:
re-entries now paint from cache, and the fan re-runs at most every 2–5
minutes.
- **Leaving Projects left the whole fan running**, competing with the
next surface's channel fetches on the same relay connection. AbortSignal
is now threaded through the enumeration and assignment pagination loops
(optional params — behavior identical without a signal), and leaving the
surface cancels the work-items query. Deliberately NOT cancelled: the
enumeration (the always-mounted sidebar projects section observes it and
its 30m cache is valuable), repo snapshots and local scans (native work
that can't abort — cancelling would discard the finished result and
force the same clones again), and activity summaries (a single bounded
request).

Abort behavior is covered by red-first unit tests on both pagination
loops. Remaining follow-up (out of scope): the queries themselves want a
relay-side aggregate instead of shipping thousands of events to compute
counts client-side.

---------

Signed-off-by: Max Lampert <maxwell@squareup.com>
2026-08-21 17:40:35 -06:00
Will Pfleger
c63ac746cd perf(desktop): split discover_acp_providers into cheap and forced paths (#6330)
Splits the `discover_acp_providers` Tauri command into a cheap cached
path and an explicit forced re-discovery path.

The profiling flight-recorder identified `discover_acp_providers` as the
acute UI-freeze correlate: a 20–65s probe pipeline (PATH re-fetch, CLI
auth probes, process spawns) was running on routine renders like channel
switches, because hot surfaces remounted the wrapping query and
automatic staleness refetches re-ran the full pipeline. This is
complementary to the idle auto-reload (#5588) and bounded-accumulator
(#5596) work.

## Backend
- Added a `force` parameter to `discover_acp_providers` /
`discover_acp_runtimes_from`. The default (cheap) path serves cached
availability and auth statuses with zero process spawns; `force: true`
runs the full pipeline (cache clear, PATH re-fetch, CLI auth probes).
- Added a single-flight coalescer (`forced_single_flight`) so concurrent
forced runs share one probe instead of stacking.
- Extracted the auth-status cache and `resolve_auth_statuses` into
`discovery/auth_status_cache.rs`.

## Renderer
- Raised `staleTime` to 30 min; forced refresh flows only through an
explicit `fetchQuery` path (`refreshAcpRuntimes`), so automatic
staleness refetches can never re-run probes.
- Added `useAcpRuntimesQueryForced` for Settings and onboarding
surfaces; connect/install/save/delete mutations force-refresh in
`onSettled`.
- Switched `SetupStep`, `HarnessCatalogDialog`, and
`HarnessesSettingsPanel` to the forced hook.

## Structure
The touched files were at or over the grandfathered 1000-line file-size
ceiling, so the change is delivered as cohesive extractions rather than
in-place growth: `forced_single_flight.rs`, `auth_status_cache.rs`,
`discovery/tests/forced_discovery.rs`, `acpRuntimesQuery.ts`, and
`tauriAcpDiscovery.ts`. Dependencies are one-directional (`hooks.ts`
imports from `acpRuntimesQuery.ts` and re-exports the forced hook, so
consumer imports are unchanged; no import cycle).

## Tests
Added Rust tests pinning the cheap-vs-forced split: a probe is spawned
only when forced, the cheap path reuses the cache, and auth status is
`Unknown` before any probe runs.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-20 19:44:05 -04:00
Wes
06e8be9098 fix(desktop): make reconnect repair lossless (#6415)
## Summary

- repair exact-channel reconnect gaps from a source-coupled 1,865-second
floor (`900` seconds of relay future tolerance + `960` seconds of DB
created-at floor + `5` seconds of fence margin)
- page the existing authenticated `POST /query` bridge losslessly with
`(until, before_id)`, including dense timestamp boundaries and an
unbounded first page that does not trust the renderer clock
- keep the restored live REQ unchanged while deduplicating live/repair
overlap until both live EOSE and repair completion for the active
connection generation

## Scope and behavior

The wider repair applies only to live subscriptions containing the exact
channel-event kind set and one `#h`. Profile, read-state, global,
channel-less, and other subscription behavior is unchanged. Failed or
stale repairs retain their pinned lower bound for the next reconnect.

This intersects the replay path described in #3215, but does not claim
to fix that issue. Desktop already has a bounded notification-side
event-ID guard; this PR adds transport dispatch dedupe for the
restored-live/repair overlap. Notification behavior should remain a
focused regression check while this wider repair window rolls out.

## Testing

- `cd desktop && pnpm typecheck`
- `cd desktop && pnpm biome check
src/shared/api/channelReconnectRepair.ts
src/shared/api/relayClientSession.ts src/shared/api/relayClientShared.ts
src/shared/api/relayClosedRecovery.ts
src/shared/api/relayReconnectReplay.ts
src/shared/api/relayReconnectReplay.test.mjs src/testing/e2eBridge.ts`
- `cd desktop && node --test
src/shared/api/relayReconnectReplay.test.mjs` (26 passed)
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml
channel_reconnect_repair` (2 passed)
- pre-push hooks passed at `743283439eb6be7969e19951cbb6720c5e493a04`

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-08-20 17:13:47 -06:00
Will Pfleger
54bbe7305b perf(desktop): resolve references without directory scans (#6328)
## Summary

- keep the channel poll membership-only and fetch the open-channel
directory only from explicit discovery surfaces
- resolve unknown ID-bearing references through one bounded
`get_channel_details` lookup, while name references remain limited to
member channels plus a browse/search-warm directory cache
- fail closed for fetched private metadata and cover cold lookup,
warm/member hits, private visibility, and cached misses with mounted
hook contracts

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
2026-08-20 15:20:24 -04:00
thomaspblock
2b7be9340d feat(desktop): make Projects workspaces selectable (#6368)
## Summary

Projects workspaces now support consistent selection across projects,
repositories, reviews, tasks, and channel results. A user can collect
related entities, see the active selection without losing their current
workspace, and pass a bounded, injection-safe representation to an agent
for discussion.

Detail views now keep repository source and availability explicit while
preserving contextual actions across local and remote workspaces.
Missing or inaccessible repositories produce a recoverable state instead
of leaving the workspace ambiguous.

This is part 2 of the Projects v6 stack. Parts 3 and 4 will add
context-aware collaboration and navigation/detail-page polish.

### Related issue

Related: #6335

### Testing

- Full pre-push gate passed: file-size checks, Biome, TypeScript,
Desktop unit tests, Tauri checks, and Rust tests
- Project context safety suite: 9/9 passed, including bounded untrusted
overview data and hidden-context stripping
- Updated Playwright coverage exercises grouped selection,
selection-aware agent context, repository source states, and detail
actions

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-20 07:42:02 -07:00
Evan Chen
9c2f05346f fix(desktop): show complete repository trees (#5102)
## Summary

Large repositories were silently limited to the first 250
lexicographically sorted paths, which could leave the Files tab showing
only dot-directories. Retain all tracked paths while limiting eager
content previews, and replace the Files tab hard cutoff with incremental
200-entry pagination.

### Related issue

Fixes #4960. No duplicate PR found.

### Testing

- Targeted Rust regression: `cargo test --manifest-path
desktop/src-tauri/Cargo.toml
parse_ls_tree_keeps_paths_after_eager_preview_limit --lib`
- Pagination helper regression: `node --import ./test-loader.mjs
--experimental-strip-types --test
src/features/projects/lib/projectsViewHelpers.test.mjs`
- Desktop Biome, type checking, and file-size checks pass
- All 4,393 desktop tests pass
- Repository pre-push hooks pass
- `just ci` currently stops on pre-existing Clippy warnings in
`crates/buzz-acp/src/queue.rs` and `crates/buzz-agent/src/hints.rs`,
outside this PR diff

Generated with Codex

---------

Signed-off-by: Evan Chen <evanc@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
2026-08-19 15:53:14 -07:00
Tyler
bbd20fae75 perf(desktop): move five hot renderer paths from JS into Rust (#6024)
## Summary

Moves five hot renderer paths out of JavaScript and into Rust, targeting
main-thread CPU on the desktop app: wakeups, IPC round trips, and
per-frame JS work during reconnect storms, catch-up, and archive sync.

Five commits, reviewed and ratified sequentially (each round included
independent re-review, mutation-tested coverage, and full-suite gates at
pinned heads):

| Commit | Change |
|---|---|
| `9b5a82ab9` | Batch inbound relay frames in the native websocket
plugin: N run-loop wakeups collapse into 1 per 8ms window, byte-bounded
so no batch crosses the Tauri direct-execute threshold onto the slow
fetch path. NIP-42 AUTH bypasses the window. |
| `8b047a35e` | Move the local-archive subscription into Rust: the
renderer no longer sees archived frames at all. Shared
`native_relay_client` (one authenticated socket per relay+pubkey,
declarative subscription reconciliation, CLOSED-driven retry with
write-time eviction), lease/epoch lifecycle ownership so delayed IPC
cannot resurrect a stopped task. |
| `609962b49` | Move persona catalog fetching into Rust: signature
verification (~473us/event, ~0.24s per 500-event page formerly on the
webview thread) now runs under `spawn_blocking`; one projected DTO
crosses IPC. |
| `38afea0a0` | Batch unread catch-up behind one IPC call: N per-channel
REQs/awaits become one command with global two-pass classification,
making the completion-order race unrepresentable. Both IPC DTO surfaces
pinned by wire tests asserting serde output against the TS contract. |
| `b955f528b` | Move observed unread state into native SQLite: scoped
WAL DB with a sequence/revision protocol, transactional localStorage
migration, epoch-tagged rebuild detection. Membership no longer
serializes five capped arrays per catch-up: measured 327.4 KiB before,
0.2 KiB after at the same populated 5x1000 fixture. |

Full design rationale, failure matrices, and verification detail are in
the individual commit messages.

### Known behavior notes

- **Native migration is a one-way door per profile.** A successful
native open sets `migration_complete` in the first open's transaction
and the renderer then deletes the legacy localStorage key
(`useObservedUnreadPersistence.ts:290-294`,
`observed_unread.rs:420-442`). The one-release JS fallback remains for
native open *failure*, but a profile that has completed migration cannot
return to the JS store. Relevant if a rollback path is ever needed.
- **The membership seed is built unconditionally at startup**
(`useUnreadChannels.ts:245-250`), before any `isNative()` check. It is
ref-cached and small, but it is a startup cost the baseline does not
pay, including on the fallback path.

### Related issue

None found. Work originated and was reviewed in the Buzz
`buzz-gui-performance` channel.

### Testing

**Current head is `9a8128ba4d2d695113505162be388fcec62cda0a`. For
receipts at that head, see [Round-3
receipts](#round-3--final-head-9a8128ba4) at the bottom; the block
immediately below is the historical record for the original five-commit
pack and its counts are superseded.**

The receipts in this subsection are at exact pack head
`b955f528b6dfa34db089b21cc718fba0f9284d6f` (tree oid `dcc711a66`),
verified by three independent instruments (implementer, reviewer,
gatekeeper), with `git rev-parse HEAD` confirmed in the same shell as
each run:

- `cargo test --lib` (full package): **2511 passed / 0 failed** (+7 +3
in the other targets)
- `pnpm test`: **4920 / 4920 across 74 suites**
- `cargo fmt --check`, `clippy --all-targets -D warnings`, `tsc
--noEmit`, `pnpm check` (including the file-size ratchet): all clean
- Mutation controls on the final review round: badge-lane controls A and
B each turn exactly one test red; seed-guard (M4) and
anchor-monotonicity (M5) mutants each killed by exactly their own
witness; restored bytes fully green
- The observed-unread native path is exercised by an in-tree
`__TAURI_INTERNALS__` protocol rig (entry, replay, gap, rebuild,
markers, membership, badge lane), added after review found the original
suite never left the localStorage fallback

**Open gates, stated plainly (status as of head `9a8128ba4`):**

- **Paired CPU benchmark still not run.** The three-arm design (baseline
`78cbffeb6` vs pack vs a null arm with native forced off, fresh profile
per arm) was instrument-reviewed but never executed. The IPC payload
reduction above is measured; end-to-end CPU improvement is predicted,
not measured. This gate is unchanged since `b955f528b`.
- **Live-local gate at the final head: GREEN at `9a8128ba4`.** A real
native Tauri live-local pass per `TESTING.md` was run at this exact
head. Receipts:
`.scratch/pr6024-final-9a8128ba4d2d695113505162be388fcec62cda0a/`,
`MANIFEST.txt` `start_utc=2026-08-17T21:58:07Z`, `FINAL_ATTESTATION.txt`
`end_utc=2026-08-17T22:20:47Z` — both after the commit's `21:32:51Z`,
and the build worktree's HEAD is `9a8128ba4` with a clean tree. **I
opened these files rather than relaying the summary**, and recomputed 6
SHA256SUMS entries against the sealed manifest (6/6 match). From the raw
artifacts: historical catch-up persisted 10 seeded events into the
native `observed-unread.db`; a realtime event advanced it 10→11; the
relay was killed and restarted with Desktop up and an event sent after
recovery advanced it 11→12 within 2 polling ticks; Desktop was then
stopped, an event sent offline, and after relaunch with the appdir
preserved catch-up advanced it 12→13. `relay.log` shows 2 app WebSocket
connections, the restart-close signal, and fresh connections after each
recovery — the app-attributable socket evidence the previous round
lacked. The preserved appdir database is still on disk and `select
count(*) from observed_events` returns **13**. The ignored real-relay
archive wire test passed **1/1** at this head. This exercises the
`lib.rs` / `e2eBridge.ts` merge recombination under a real app and
blocker 2's failure-mode family (relay killed mid-session, app killed
with offline traffic in flight, sync did not go dead). It does **not**
directly force the precise overlapping B/C archive-ownership timing
window through the GUI; that invariant is pinned by the compiled
`drop(owner)` mutation proof below, and is deliberately not relabelled
as live race evidence. **Retraction retained for the record:** an
earlier revision of this description said live-local was GREEN at
`9a8128ba4` while citing a run executed against `f079d0914`, the pre-fix
head, roughly three hours before `9a8128ba4` existed. That claim was
false and was corrected before this run existed. The GREEN above rests
on the new receipts only.
- **Workspace-wide `cargo test --workspace` is red on `main`, not on
this PR.** The exact-head live run's workspace logs contain honest
`rc=101` results. All are in crates this PR does not touch: `git diff
origin/main...9a8128ba4` is **empty** for `crates/git-sign-nostr`,
`crates/buzz-relay`, and `crates/buzz-pair-relay`.
`git-sign-nostr::tests::test_parse_envelope_rejects_invalid_oa_pubkey`
fails **deterministically on clean `main`** — reproduced independently
in fresh worktrees at merge-base `a282e0643` and at current tip
`7f61cf431` (55 passed / 1 failed, same assertion at `lib.rs:2136`).
Root cause: `nostr` 0.36→0.44 changed `PublicKey::from_hex` from a
curve-point parse to a plain hex decode, so an all-zeros (off-curve) key
now parses; measured with a standalone probe, `from_hex("0"*64).is_ok()`
is `false` on 0.36.0 and `true` on 0.44.7. Filed as block/buzz#6175,
with the observation that nothing in CI runs this crate's tests (`just
test-unit` enumerates packages and omits it; `server-cross-compile`
compiles it and runs nothing). The mesh-demo and pair-relay failures in
the same logs are timing flakes in equally untouched crates. **None of
these gate this PR.**
- **Full smoke + integration: GREEN at `9a8128ba4`.** CI run 32072147445
is `success` with every job green — all 4 Desktop Smoke E2E shards, both
Desktop E2E Integration shards, Desktop Core, Desktop Build (macOS),
Desktop E2E Relay, Rust Lint, Windows Rust, DCO, Dead Token Reference
Guard. Attempt 1 was cancelled by an infrastructure hang, not by this
code: both integration shards stalled 19m37s inside `Install Playwright
system dependencies` (an apt install that runs before the e2e bundle is
built or the relay starts), so no test executed and all downstream steps
were skipped. The same step took 16s at `9128b9389` and 12s/18s on
attempt 2 of these same bytes. Attempt 2 was a job rerun, not a push;
the head is unchanged.

---
Opened by Eva (agent) on Tyler's behalf at his request.


---

## Review round 2 — head `9128b9389`

Carl's review
[4953970089](https://github.com/block/buzz/pull/6024#pullrequestreview-4953970089)
raised two blockers against `f079d0914`. Both were confirmed real at the
source by a second reader and both are fixed in `9128b9389`.

**Blocker 1 — recovery dropped the mutation whose ingest failed.** Every
captured mutation now gets one retry after `reopen()` refreshes
sequence/revision, which is safe because ingest is idempotent (events
upsert `DO NOTHING`, channel latest advances by `MAX`, membership is
`INSERT OR IGNORE`, and a replayed sequence returns a snapshot rather
than reapplying). `snapshotRequired` takes the same recovery path
instead of being treated as success. If reopen and retry both fail,
native is explicitly degraded and equivalent state is applied to the JS
fallback so `isNative()` stops reporting healthy. Regressions assert the
*originally rejected* marker, destructive clear, and membership delta
each survive; removing the retry takes 5 tests red including all three.

**Blocker 2 — a superseded archive start could tear down the newer
scope's relay session.** `begin` now returns an `ArchiveOwnership` token
that holds both ownership guards, and `archive_session` requires one, so
session acquisition happens inside the ownership critical section rather
than after it.

### Deviation from the requested regression test for blocker 2

Carl asked for "a production-wiring regression that pauses B after
`begin`, fully installs C, resumes B, and proves only C's session and
subscriptions remain live." **We did not write that test, deliberately,
and wrote a stronger one instead.**

Under this fix that scenario is unreachable by construction. The fix is
not a revalidation check that a test could observe passing or failing —
it is a type-level fence: `begin` hands the winner a token that holds
the ownership locks, and `archive_session` cannot be called without one.
A test that staged "B pauses after `begin`, C fully installs" would be
staging a state the type system now forbids, so it could only pass
vacuously.

Two things close the blocker, and only one of them is a test:

1. *A superseded start cannot reach `archive_session` at all.* Enforced
by the compiler, not by a test. `ArchiveOwnership` is un-constructible
outside `archive::sync`. Verified by attempting both bypasses: calling
`archive_session` without a token fails **E0061**, and forging the token
from a real non-archive caller fails **E0451** ("fields `_latest` and
`_running` are private"), with a second independent barrier because
`RunningSync` is itself a private type.
2. *While an owner holds the token, no newer start can claim.* This is
the property the token's usefulness rests on, and it is what the new
regression pins —
`a_newer_start_cannot_claim_while_the_owner_holds_its_token`.

That test is aimed at the one mutant that actually threatens this
design: keeping the token but releasing the guards inside `begin`, which
is what someone reaches for to avoid holding a lock across an await. It
**compiles**, it keeps every other lifecycle test green, and it restores
the exact race. Against it the new test goes **single-red** — the other
26 tests in the module stay green.

Proving mutual exclusion over the whole claim → acquire → install unit
is strictly stronger than staging a pause the type system forbids, which
is why we chose it.

Also recorded in the code, in `ArchiveOwnership`'s doc comment: what
this token does **not** cover. It serializes archive lifecycle against
archive lifecycle, not against a plain `NativeRelayClient::session()`
call from a non-archive feature. That window predates this PR and is
theoretical today given renderer gating, so it is documented rather than
fixed here.

### Round-2 receipts at `9128b9389`

- `cargo test --lib`: **2513 passed / 0 failed**
- `pnpm test`: **4926 / 4926**
- `cargo clippy --all-targets -D warnings`, `cargo fmt --check`, `pnpm
check`: rc 0
- Merged against `origin/main` in a scratch worktree (clean, zero
conflicts) and re-gated on that merged tree: **2544** Rust / **4956**
frontend, both 0 failed


---

## Round 3 — final head `9a8128ba4`

`9a8128ba4d2d695113505162be388fcec62cda0a` is a merge of `origin/main`
(`a282e0643`) into the pack branch, taken **before** final verification
so that verification would run on the bytes CI builds rather than on a
head that was about to move.

The merge is textually clean but touches two files the pack also
touches, `desktop/src-tauri/src/lib.rs` and
`desktop/src/testing/e2eBridge.ts`, so recombination was checked
explicitly rather than inferred from the absence of conflicts: **all 342
pack-added lines in those files survive (0 missing), and all 17
main-added lines survive (0 missing)** — no silent revert in either
direction. Main's contribution is a `#[doc(hidden)] pub use
print_agent_access_owner_only_probe_if_requested` in `lib.rs`, plus a
Bumble→Pollen rename and workflow revision / `workflowUpdateError` mocks
in `e2eBridge.ts`.

### Receipts at `9a8128ba4`

Implementer, in a shell with `git rev-parse HEAD` confirmed at this SHA:

- `cargo test --lib` (full package): **2544 passed / 0 failed** / 18
ignored
- `pnpm test`: **4956 / 4956** across 74 suites
- `cargo clippy --all-targets -D warnings`, `cargo fmt --check`, `pnpm
check`: rc 0
- The 4 remaining `pnpm check` findings (`channelMutesStorage.test.mjs`,
`channelStarsStorage.test.mjs`, `terminal.css`,
`empty-edit-delete.spec.ts`) are all in files this PR never touches
- All 7 pre-push hooks green, including `branch-skew`, which now passes
on its own because the branch contains `origin/main`

Independent reviewer, same exact head, own instruments:

- `cargo test --workspace`: green; `cargo clippy --workspace
--all-targets -- -D warnings`: green *(a later workspace run at
`9a8128ba4` hit pre-existing `main`-side failures in untouched crates —
see the workspace-gate entry under Open gates above; the difference is
flake timing plus which run happened to reach `git-sign-nostr`, not a
change in this PR)*
- `pnpm test`: **4956 / 4956**; `pnpm check`: rc 0, same known findings
outside touched files
- **Mutation proof of the blocker-2 fix:** dropping the ownership
token's hold (`drop(owner)`) turns
`a_newer_start_cannot_claim_while_the_owner_holds_its_token` red;
restoring the shipped bytes returns the suite green. The invariant that
a superseded start cannot claim or disturb a newer archive session is
pinned by a test, not just by the type system.
- **Live-local at this exact head: attested and verified.** The
mis-cited `f079d0914` run that originally appeared here is retracted; a
real run at `9a8128ba4` replaced it, and its receipts were opened
file-by-file (not relayed) by a second agent. See the live-local entry
under Open gates above for the receipt path, wall-clock bounds,
store-count chain, and socket evidence.
- Verdict: minimalness **9/10**, elegance **9/10**, correctness
**9.5/10**. **Score restored** now that the exact-head live-local run
exists and its provenance has been checked from the files (SHA-named
receipt dir, self-dating manifest, receipt mtimes after the commit time,
sealed digests spot-checked). Gatekeeper re-bless at `9a8128ba4`:
**9/10, merge recommended.**

The paired CPU benchmark remains the one gate not run. It is a
measurement of the pack's *benefit*, not of its correctness, and it is
stated as unrun rather than estimated.

---------

Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Dawn <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-19 14:20:58 -04:00
thomaspblock
87f8ff82ae feat(desktop): refine repository-aware project workspaces (#6003)
## Summary
- Makes Projects repository-aware across browsing, branch and tag
selection, local and remote source management, work items, commits, and
contextual repository actions.
- Refines project details into consistent single-column reading surfaces
with a resizable, section-aware context pod, while keeping project
conversations available in the attached chat panel.
- Adds persistent sidebar project navigation, direct entity links,
repository discussion channels, contributor identity matching, and
consistent loading and activity presentation.
- Keeps branch-specific controls on code-oriented sections while Tasks,
Reviews, and Channels remain repository-scoped, reducing misleading
context and actions.

Replacement for #5981 with an identical final tree flattened into one
signed-off commit because the required DCO check suite remained stalled.

### Related issue
N/A

### Testing
- [x] Desktop pre-push checks, TypeScript typecheck, and unit tests
- [x] E2E production build with `pnpm build:e2e`
- [x] Focused Playwright smoke coverage for project workspace, task,
review, commit, sidebar, and contextual-panel behavior
- [ ] Attach final before/after screenshots after design review

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Wintermute <165f0c871dd2586bb18b6aa109eeaf57bb2132ff4d27b10120f4368a0f627022@buzz.block.builderlab.xyz>
Co-authored-by: Wintermute <165f0c871dd2586bb18b6aa109eeaf57bb2132ff4d27b10120f4368a0f627022@buzz.block.builderlab.xyz>
2026-08-19 12:50:00 +01:00
Tyler
6ea7a2b221 fix(desktop): preserve early relay auth challenges (#3320)
## Summary

- buffer native WebSocket frames until `plugin:websocket|connect`
returns the socket ID and the NIP-42 auth waiter is installed
- drain those frames in order before normal inbound handling begins
- add a deterministic E2E seam and regression for an AUTH challenge
delivered before connect resolves

## Root cause

The native WebSocket task starts forwarding relay frames before the
connect command returns its socket ID. Buzz relay sends the NIP-42 AUTH
challenge immediately on connection, so Desktop could process that
challenge while `wsId` and `authRequest` were still unset. The challenge
was discarded, the cold connection failed authentication, and the first
plain-text send failed. Channel history still appeared because it loads
through the Tauri channel-window command rather than this frontend
WebSocket.

## Verification

- deterministic regression is red before the fix (`connecting` after 5s)
and green after it
- `pnpm test`: 3,721 passed
- `pnpm build:e2e`: passed
- focused early-AUTH E2E: passed
- existing failed-initial-dial E2E: passed
- `pnpm typecheck`: passed
- Biome + file-size pre-commit checks: passed

The complete 7-test relay-reconnect file passed once before the final
line-count-only compaction. After that compaction, two full-file reruns
each had the same unrelated startup-seam flake in the existing
initial-dial test; both the new test and that existing test pass
independently at the committed HEAD.

---------

Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
2026-08-18 15:09:58 -07:00
Will Pfleger
50a71137e6 feat(managed-agents): close five Claude Code agent-config gaps (#4557)
Closes five Claude Code agent-config gaps in Buzz Desktop, split into
three commits that share the spawn-time and live-switch surfaces.

## Config isolation, model authority, and Auto mode (#2692, #2884,
#3493)

- **`CLAUDE_CONFIG_DIR` isolation (#3493).** `config_bridge` resolves
both `settings.json` and `.claude.json` panel paths from the agent's
effective env (`resolve_effective_agent_env` — baked floor → definition
→ global → persona → record), so the panel reads the same directory the
agent runs against. `mcp_config_file_path_for_runtime` honors a custom
dir; empty/blank is treated as unset, matching Claude's
`CLAUDE_CONFIG_DIR || homedir()` semantics. `AgentConfigPanel` shows a
Keychain caveat when a custom dir is active (a custom dir maps to a
fresh Keychain namespace unless `CLAUDE_SECURESTORAGE_CONFIG_DIR` is
also set).
- **Single startup model authority (#2692).** `ANTHROPIC_MODEL` is the
sole startup model authority for Claude. Local spawns write
`ANTHROPIC_MODEL` and strip `BUZZ_ACP_MODEL` so the harness never sees
two authorities; remote deploys send `ANTHROPIC_MODEL` in `policy_env`
instead of `BUZZ_ACP_MODEL`. Non-Claude runtimes are unchanged.
- **`PermissionMode::Auto` (#2884).** Wire string `"auto"`, model-gated,
degrades to the agent default when the active model doesn't advertise
it.

## Thinking effort end-to-end for local Claude agents

Effort flows from the running session's discovered `thought_level`
config option through the config surface to a local-only write control
and a read-only two-facts display.

- **Reader.** Discovers the `thought_level` config option from the
session cache (never hardcoded) and populates `effort_config_id` /
`effort_options` on `RuntimeConfigSurface`. The canonical effort tier
orders record env > `record.effort_level` (`BuzzExplicit`) > ACP >
persona > global > definition > file, so the panel shows the effort the
next spawn will launch with while `resolve_with_override` surfaces the
running ACP value as the struck-through override — neither masks the
other silently.
- **Write control.** `persist_agent_effort_level` is a direct-write
Tauri setter (writes `record.effort_level` + `updated_at`,
`save_managed_agents`) that rejects non-local backends — remote effort
is set at deploy time via `policy_env`. `EffortPickerField` mounts in
`AgentInstanceEditDialog` beside the Model block, gated on a local
backend **AND** a discovered `effortConfigId`. It persists directly and
invalidates the config surface, mirroring the
`setManagedAgentAutoRestart` standalone-setter precedent, so the frozen
`UpdateManagedAgentInput` shape stays frozen.
- **Display.** The read-only configured-vs-current two-facts display is
the `thinkingEffort` normalized field in `AgentConfigPanel`, fed by the
reader's canonical tier ordering.
- `buzz-acp` applies the startup effort env at session start.

## Distinguish a rejected model switch from silent success

A live model switch collapsed adapter rejection into success:
`apply_model_switch` returned `Ok` on both an accepted switch and an
application-level refusal, so the caller cached pre-switch capabilities
as if they described the target model and Desktop reported the pick as
landed.

- `ModelSwitchOutcome::{Applied(Value),Rejected}`. Transport-class
errors still propagate as `Err` (respawn the poisoned stdio); an
application-level refusal is now `Rejected`.
- The caller drives everything off `post_switch_snapshot`: `Applied`
refreshes `model_capabilities` from the target model's echoed
`configOptions` (or drops to `None` when none are echoed, so a
pre-switch snapshot is never mistaken for the target's); `Rejected`
preserves pre-switch caps and emits a `failure` `control_result`. Effort
resolution and the session-config capture read the post-switch snapshot
so they converge on the model the session actually runs;
`modelOverridden` is gated on `switch_succeeded`.
- `liveSwitchOutcome.ts` gains a distinct `"failed"` outcome for the
adapter `failure` frame and treats the busy-path `"sent"` ack as
provisional — it never counts toward success. Success is confirmed only
by a real positive terminal frame (the busy-path deferred apply emits a
correlated `switched` `control_result` when it lands), and the fallback
timeout resolves an honest `"pending"` (accepted, apply deferred), never
a false `"ok"`. `ModelPicker` surfaces a distinct toast per outcome —
failed, unsupported, and pending.

## Scope explicitly excluded

Per-agent config dir provisioning, `CLAUDE_SECURESTORAGE_CONFIG_DIR`
sentinel injection, `settings.json` projection, protected-key stripping,
MCP inheritance, spawn serialization, and the `last_spawn_warnings`
surface are absent from this diff. Silent-fallback machinery for
non-Claude runtimes (#2265/#4004) is a tracked follow-up.

## Sanctioned follow-ups

- **Live mid-conversation effort switching.** Effort is spawn-scoped
only in this PR: the worker reads `BUZZ_ACP_EFFORT_LEVEL` once and
applies it at session creation. The live effort-switch machinery
(mid-conversation effort RPC + ack frame) was deliberately removed and
is archived on `archive/claude-config-gaps-live-effort` for a future
plan-gated revival.
- **Idle-path late model-switch rejection is unobservable.** An idle
switch acks `switched` immediately after catalog validation, but the
real `set_config_option` runs at the next session creation — potentially
much later — so a rejection there is not surfaced back to the picker
(holding a subscription that long is not sensible). Pre-existing,
catalog-gated behavior; a durable fix is a tracked follow-up.

Closes #2692, #2884, #3493

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-18 16:29:28 -04:00
Wes
3fdf289b78 fix(desktop): bound remote agent mention authorization (#6224)
## Summary

- add a targeted native `revalidate_relay_agents(pubkeys, channel_id)`
command for send-time authorization
- scope membership discovery to the destination channel and selected
pubkeys before runtime/profile/policy queries
- replace full relay-directory rebuilds with targeted checks before
agent side effects and again at publication
- preserve managed-agent evidence independently and retain internal
owner-only filtering

## Security and trust boundary

The targeted command reuses the existing authoritative chain:

1. relay-signed kind:39002 membership scoped by viewer and destination
`d` tag
2. agent runtime directory event
3. agent-signed owner profile verification
4. owner-signed managed policy
5. internal-build `owner_only` filtering before policy lookup and on
final results

Relay-only agents are dropped on any targeted directory failure. Fresh
managed-agent evidence remains valid when the unrelated relay directory
fails.

## Validation

- pre-push gate passed: branch skew, Desktop check/typecheck/tests,
Tauri checks, Rust tests, and mobile tests
- Desktop unit suite: 4,992 passed
- targeted Rust relay-directory tests passed
- focused Playwright mention-send acceptance passed
  - relay-only send emits the expected `p` tag
- exactly two targeted `revalidate_relay_agents` calls on the
already-member send path: pre-side-effect and pre-publication
  - no `list_relay_agents` call on that send path
  - relay failure and revocation remain fail-closed
  - internal owner-only mode hides other-owned agents
- manual local Desktop testing by Wes: mention sends felt materially
improved

## Notes

This intentionally does not clear the composer early or add an
optimistic timeline row. Publication still waits for fresh
authorization.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz>
2026-08-18 08:46:26 -07:00
Taylor Ho
edc4a09aaa feat(workflows): add responsive library card actions (#6008)
**Category:** improvement
**User Impact:** Users can scan what each workflow does and trigger,
edit, duplicate, enable, disable, or delete it directly from the
library.
**Problem:** The workflow list buried common actions and did not expose
each automation's trigger-to-action shape at a glance.
**Solution:** Add a responsive workflow library with a persistent create
tile, compact trigger/action diagrams, prominent workflow titles with
supporting descriptions, and shared card actions while preserving
existing detail, editor, and run-history entry points. Card toggles
refresh both list and open-detail caches so status and definition stay
consistent.

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

**desktop/src/features/workflows/ui/WorkflowActionsMenu.tsx**
Adds a shared card menu for trigger, edit, duplicate, enable/disable,
and delete actions.

**desktop/src/features/workflows/ui/WorkflowCard.tsx**
Reworks cards around the prototype's visual hierarchy: color-coded
trigger, action flow, sentence-case eyebrow, prominent title, supporting
description, status, channel, and update date without a footer clock
icon.

**desktop/src/features/workflows/ui/WorkflowsView.tsx**
Adds the responsive grid, create tile, mutation wiring, and list/detail
cache invalidation. Container breakpoints keep cards two-across at
medium widths and three-across in the 1280px desktop layout.

**desktop/src/features/workflows/ui/workflowDefinition.ts**
Adds immutable enabled-state updates plus narrow trigger and
first-action readers used only to select card icons.

**desktop/src/features/workflows/ui/workflowDefinition.test.mjs**
Covers neutral icon selection, enabled-state immutability, and status
presentation.

**desktop/tests/e2e/workflows.spec.ts**
Covers the create tile, title/description hierarchy, selected-card
enable/disable consistency, and deterministic narrow/medium/wide
captures while retaining existing action coverage.

</details>

## Reproduction steps

1. Open **Workflows** and confirm the create tile stays first as cards
flow from one to three columns with available width.
2. Confirm each card shows a sentence-case trigger eyebrow, prominent
workflow title, supporting description when present, status, channel,
and update date without a clock icon.
3. Open a card's overflow menu and trigger, edit, duplicate,
enable/disable, or delete the workflow.
4. Leave the detail panel open while toggling and confirm its badge and
JSON definition update with the card.

## Screenshots

Real built E2E UI with representative workflow data at three viewport
sizes.

### Narrow — 800 × 720

![Workflow library at 800 by
720](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6008/workflow-library-narrow-482d1b4c8.png)

### Medium — 1024 × 720

![Workflow library at 1024 by
720](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6008/workflow-library-medium-482d1b4c8.png)

### Wide — 1280 × 720

![Workflow library at 1280 by
720](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6008/workflow-library-wide-482d1b4c8.png)

### Card actions

![Workflow library actions at 1280 by
720](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6008/workflow-library-wide-actions-482d1b4c8.png)

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-17 16:56:34 +00:00
Wes
f716eef437 fix(desktop): enforce shared agent access across devices (#6086)
## Summary

- discover shared managed agents from authenticated relay directory
records instead of treating channel membership as sufficient proof
- publish and refresh access-policy changes immediately so running
clients converge across machines without a restart or five-minute poll
- route profile edits through the exact managed instance and
stop/restart runtimes around access changes so unrelated edits cannot
silently widen access
- keep mention send-time revalidation and Block owner-only build
enforcement fail closed
- explain invalid custom provider/model configuration instead of leaving
Save silently disabled

### Related issue

Fixes #3204

### Known residuals

- a brand-new remote agent's first policy record can wait for the
bounded directory poll when no authenticated directory coordinate exists
yet; send-time mention revalidation remains fail closed
- a failed remote-provider policy redeploy is recorded but cannot
undeploy the older provider instance until the provider protocol gains
the destructor tracked by #5570

### Testing

- full Desktop unit suite: 4,961 tests passed
- focused profile editor Playwright workflow passed, including Customize
access edits and prompt-only edits after tightening an instance
- Desktop TypeScript, Biome formatting, file-size ratchet, Tauri checks,
and pre-push suites passed
- independently reviewed for authenticated directory trust, live
subscription teardown, runtime revocation ordering, fail-open edit
paths, and per-agent provider deployment serialization

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
Co-authored-by: diegorumo <diegorumo@gmail.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
2026-08-17 09:51:00 -07:00
thomaspblock
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
Taylor Ho
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
Wes
df9e773a13 Scope desktop presence subscriptions to active demand (#5830)
## Summary

- replace the desktop's global kind-20001 presence subscription with one
author-filtered subscription derived from active TanStack presence
queries
- reconcile changing demand without a delivery gap: promote only after
relay EOSE, keep the last confirmed subscription on failure, discard
stale opens, and close entirely when demand is empty
- preserve REST presence as the initial seed and TTL/crash-recovery
backstop
- add transport-seam and lifecycle tests for readiness, normalization,
churn, retries, close failures, reconnect ownership assumptions, and
disposal

## Why

The desktop currently receives presence heartbeats from every identity
on the relay. A live tap measured roughly 2,700 events/minute (45/sec),
about 1 MB/minute and 71.5% of readable traffic, from approximately
1,300 distinct fleet identities. Most are discarded only after
WebSocket, Tauri IPC, and JS parsing.

This change applies normal Nostr author filtering at relay fan-out,
before those costs. It deliberately does not introduce a relay digest
protocol or client-side event batching; relevant-author traffic should
be small after scoping, and the existing signed-delta/REST-TTL model
remains intact.

## Correctness model

- active query observers are the demand source; inactive cached queries
retain no authors
- replacement opens before old closes and is promoted only after EOSE
- timeout/CLOSED rejects and closes the candidate while preserving the
last good subscription
- rapid A→B→C and A→B→A churn cannot unseat current A with stale B
- empty demand never sends an unfiltered subscription
- RelayClient continues to own reconnect replay; the reconciler does not
duplicate subscriptions on reconnect

## Validation

Exact pushed head: `8845093aec0330be16efe52d3459ff67f1000ff4`

Pre-push hooks passed:
- desktop check and file-size ratchet
- desktop TypeScript
- desktop unit suite: 4,791/4,791
- branch-skew check

Focused lifecycle/transport suite: 34/34 passed before commit.
Independent Royal Court review found and blocked two prototype flaws
(timeout-as-success and starvation-prone trailing debounce); both were
fixed and the final worktree was cleared with no remaining correctness
or lifecycle blockers.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-13 20:32:34 -07:00
Wes
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
Wes
59f613c404 fix(desktop): defer foreground resume work (#5696)
## Summary

- defer foreground resume work until the activation task has returned, a
frame has painted, and a trailing task gets a turn
- centralize app-focus subscribers and remove the broad TanStack
`refetchOnWindowFocus` fan-out
- coalesce relay recovery and preserve an explicit deferred refresh only
for workflow data without a polling/push freshness path
- defer the notification permission native check while keeping blur and
cheap correctness signals immediate

## Why

Buzz Desktop 0.5.10 can spend roughly 1.5 seconds in the WebKit
window-focus listener/microtask checkpoint before returning to the run
loop. Focus currently fans out into query refetches, React polling
updates, relay reconnect/replay, and native work in one activation turn.
This patch establishes an interaction-first foreground boundary rather
than letting those consumers compete with the activating input and first
paint.

## Validation

- focused foreground/workflow/relay tests: 18/18 passed before commit
- `pnpm --dir desktop typecheck`: passed before commit
- pre-commit desktop check and file-size gate: passed
- pre-push desktop check, typecheck, and full desktop unit suite:
4,743/4,743 passed at `704e7b4b6618fafce655bb2b07c7a9fe0fc8c643`
- Princess Donut independent adversarial review: PASS after two
lifecycle/freshness blockers were resolved

## Manual test

1. Install the PR build and use Buzz long enough to populate channels,
home, workflows, agents, and other polling surfaces.
2. Switch to another app for 30-60 seconds.
3. Return by clicking Buzz and immediately click a channel or scroll.
4. Confirm the first interaction and paint are prompt, then confirm
channels/home/workflows refresh and a degraded relay reconnects after
the activation boundary.
5. Repeat while rapidly switching away again to verify no resume work
starts after focus has been lost.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-12 13:46:49 -07:00
Will Pfleger
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
klopez4212
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
Taylor Ho
83ca595ada fix(desktop): preserve theme when opening communities (#5266)
**Category:** fix
**User Impact:** Previously selected community themes are preserved when
opening a relay through onboarding, while first-time theme migration
still completes for communities with no saved theme.

**Problem:** During community initialization, desktop queried theme
history before establishing live delivery. If a replacement event
arrived while an empty history query was in flight, the client could
incorrectly treat the theme as absent and publish the default over the
user's saved selection.

**Solution:** Subscribe before fetching history, expose whether live
readiness reached EOSE, flush buffered live events before resolving
EOSE, and retain the newest delivered replacement through hydration.
Seed the inherited/default theme only when both live and history
snapshots reach EOSE with no valid or unreadable event; subscription
failures, CLOSED, readiness timeout, relay failure, and unreadable
events fail closed without publishing.

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

**desktop/src/shared/api/relayClientSession.ts / relayClientShared.ts /
relayClosedRecovery.ts**
Distinguish EOSE from CLOSED/timeout readiness and flush buffered events
before resolving an EOSE fence.

**desktop/src/shared/theme/CommunityThemeController.tsx**
Seed and complete first-community migration only for confirmed absence;
uncertain hydration remains non-publishing.

**desktop/src/shared/theme/communityThemePreference.ts**
Keep the inherited appearance for the first migrated community and the
stable default for later empty communities.

**desktop/src/shared/theme/communityThemeSync.ts**
Arbitrate live and history results into valid, confirmed-absent,
invalid, or unavailable hydration outcomes.

**Tests**
Cover EOSE/CLOSED readiness, subscription failure, timeout, unreadable
and live-racing events, no-op initialization, and first-to-later
community fallback isolation.

</details>

## Reproduction steps

1. Save a non-default appearance for a community relay.
2. Remove the community locally, then open the same relay again through
onboarding.
3. Arrange for the saved replacement event to arrive live while the
initial history query returns empty.
4. Confirm the saved appearance remains selected and the client does not
publish the default theme over it.
5. On an account with no theme records, open a first empty community and
confirm its inherited appearance is migrated; open a later empty
community and confirm it starts from the stable default.

## Validation

- Pre-push desktop checks, typecheck, and full desktop tests: passed at
`f79556b0e`
- Focused theme/relay readiness tests: 38 passed
- Desktop file-size ratchet and diff check: passed

---------

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-11 08:21:34 -07:00
Wes
07a3c768d6 fix(desktop): quiesce renderer polling while hidden (#3677) (#5490)
Fixes #3677.

## Problem

The renderer never quiesces: recurring timers, query polling, and
re-render tickers run at full rate whether the window is visible,
hidden, or minimized. Measured on a live installed app: **27.5% mean
renderer CPU visible vs 28.7% hidden** (60×1s `ps` samples of the
WebContent process; `sample(1)` dominated by
`WebCore::timerFired`/ThreadTimers, microtask checkpoints, JSON parsing,
style matching). Matches all three reproductions in #3677 (macOS
prerelease, Linux/WebKitGTK A/B/A minimize test, stable macOS).

Per-timer instrumentation (dev build, wrapped
`setInterval`/`setTimeout`/rAF) attributed the recurring work: `useNow`
60 fires/min, 40 active TanStack refetch intervals, agent-turn pruning
12/min, auto-restart ticks, huddle/reminder polls — none
visibility-gated.

## Fix (two-tier gating, standard mechanisms only)

Two separate signals in `desktop/src/shared/lib/useDocumentVisible.ts`,
because they mean different things and (see residuals) are delivered
differently on macOS:

- **`useDocumentVisible`** — true Page Visibility only
(`document.visibilityState`). Gates local UI work that must keep running
on a visible-but-unfocused window: `useNow` relative clocks, agent-turn
pruning, huddle bar state/model-status polling, auto-restart tick.
Hidden ⇒ paused; `useNow` snaps to fresh `Date.now()` on return.
- **`useAppFocused`** — visible AND `document.hasFocus()`. Gates network
refetch polling only (`useFocusedRefetchInterval`, ~15 query families:
forum/home/agents/channels/templates/emoji/user-status/projects/workflows/persona-catalog/pulse/presence-list).
TanStack's `focusManager` is wired to this signal (idempotent, single
install) with `refetchOnWindowFocus: true`, so stale queries refresh
promptly on return. Deliberate side effect, documented in code: query
retries pause on blur; mutations and the presence heartbeat (`retry: 0`)
are unaffected.
- **Never gated:** reminder due-notification poll (fires while
hidden/unfocused — extracted to `reminderNotificationPoll.ts` with
regression test), huddle pipeline hot-start (`check_pipeline_hotstart`
survives backgrounding for the duration of a huddle), relay stall
watchdog, presence heartbeat. Live WebSocket delivery untouched
throughout.
- Huddle model-status indicator now clears only on huddle phase end, not
on visibility/focus changes.

## Validation

- Instrumented dev build, populated channel, fires/min:
**visible+focused** unchanged (`useNow 60 / prune 12 / watchdog 6 /
query 4 / auto-restart 4 / low-rate huddle/reminder/presence`);
**visible+blurred**: query polls 0, UI clocks continue (`useNow 60 /
prune 12`), reminders 2, presence live; **truly hidden**: only watchdog
6, reminders 2, presence ~2 — everything else 0. Return restored
visible+focused, selection preserved, queries refreshed.
- Hide-vs-blur decomposition (instrumented probe instance,
AppleScript-driven): on macOS WKWebView, Cmd-H / minimize / full
occlusion did **not** reliably produce `visibilityState === "hidden"` —
they reliably produced focus loss. The CPU-dominant quiescence path on
macOS is therefore the focus gate; the visibility gate is exercised
fully on platforms that report hidden (e.g. WebKitGTK minimize per the
Linux repro).
- Gate-regression tests: signal separation, `useNow` hidden-pause +
fresh-snap on return, focus-gated interval pause/resume-with-refresh,
reminder delivery while hidden+unfocused (5 new, plus primitive wiring
tests).
- Push gate: desktop check, typecheck, full desktop suite **4549/4549**
at `1237548d1`.

## Known residuals

- **macOS hidden-signal limitation:** because WKWebView rarely reports
`hidden` on app-hide/minimize, hidden-only consumers (`useNow`, prune,
huddle UI polls) may keep ticking on macOS when the app is hidden. These
are cheap local timers; the expensive network polling still quiesces via
focus loss, which is what the measured 28% CPU was attributed to. If the
residual local-timer cost proves measurable, the follow-up is bridging
Tauri window hidden/minimized events into the visibility signal.
- End-to-end CPU confirmation on a packaged build is the post-merge
follow-up (against the 28% idle baseline).
- Visible-state costs (skeleton animation pileups on stuck loading
views, per-poll JSON payload churn) are intentionally out of scope —
separate follow-up issue.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Meeseeks <2e96988f190ed1bd3c568760103aa4cadb2bc6195b832e252c984392c89039bd@buzz.block.builderlab.xyz>
2026-08-10 11:42:31 -07:00
Will Pfleger
5e4c05f90b feat(desktop): NIP-AM agent-usage backend — P2 emission/transport/archive + P4a aggregation/D6 (#4000)
## What

Implements Phases 2 and 4a of the Usage v2 plan (plan events
`d0268cd0`/`0e95b035`), extending the archive backend to emit,
transport, archive, and aggregate both cache categories and billing
identity fail-closed.

### P2 — emission, transport, archive

**Tri-state accumulators** (`Unseen`/`Exact`/`Unknown`) for cache-read
and cache-write in `buzz-agent` turn and session state. Absent field =
Unknown (never zero) through the full pipeline. No `unwrap_or(0)` on the
cache path. Both cache folds are gated on usage-bearing responses (same
gate as the total-state and identity folds) — a response with no usage
at all must not poison either accumulator.

**Overflow-aware input token parsing and accumulation** — closed
end-to-end from parse through wire to ACP:
- `sum_usage()` returns `SumUsageResult` (`Exact(u64)` | `Overflow`) —
checked arithmetic, never clamps. `anthropic_input_tokens()` returns
`Option<SumUsageResult>` since it sums three fields (`input_tokens +
cache_read_input_tokens + cache_creation_input_tokens`) that can
collectively overflow. Single-field callers (`prompt_tokens`,
`completion_tokens`, etc.) convert via `.into_exact()` — their
single-field sums cannot overflow.
- `LlmResponse.input_tokens_overflowed: bool` propagates the parse-layer
signal into the run loop. When set, `input_tokens` is `None` (clamped
value discarded), the context-gate baseline
(`last_request_input_tokens`) is frozen at its prior reading, and
`turn_input_tokens` is poisoned to `TurnIOState::Poisoned` before any
emission — including mid-turn `emit_usage_update` calls. A dedicated
enum on `LlmResponse.input_tokens` would ripple into ~20 existing test
assertions on `r.input_tokens == Some(...)`; the bool flag confines the
change to the two call sites that check it.
- `TurnIOState` (`Unseen`/`Exact`/`Poisoned`) for input and output:
per-round fold uses `checked_add`; overflow poisons permanently at turn
and session level, no healing. Absence does not poison (pass-2-cleared
contract unchanged). Wire emission omits
`accumulatedInputTokens`/`accumulatedOutputTokens` when poisoned — never
null, never `u64::MAX`. ACP treats absent = publisher-poisoned:
`delta_reliable: false`, null turn fields, null cumulative for that
category; session cumulative stays unknown for all subsequent turns once
poisoned.

**Conditional wire emission** for `accumulatedCachedInputTokens` and new
`accumulatedCacheWriteTokens`: fields are omitted when the cumulative is
Unseen or Unknown. ACP `_goose/unstable/session/update` contract
documented next to the payload with tests for all absence/zero variants.

**`PricingIdentity` stamping (publisher-side)**:
- `pricing_authority()`: canonical parsed-URL endpoint comparison
against the official allowlist — HTTPS only, exact allowlisted host
(lookalike-safe), default port (omitted or explicit :443), required API
base path, rejects userinfo/query/fragment/path-prefix lookalikes.
- Model: the actually-requested `request_model` after mesh/auto
resolution (not `effective_model_str`).
- Turn discipline: identity retained only while ALL usage in the current
turn carries one identical proven identity; any mismatch,
unproven-usage-bearing response, or unpaired cumulative snapshot poisons
to absent; a later matching notification does not heal a mixed turn.

**ACP `UsageTracker` identity fold**: per-in-flight-turn tri-state
identity accumulator replacing last-update-wins. Any absent identity on
a token-advancing notification or exact mismatch poisons to absent;
poison survives later updates; reset in `begin_turn()`/`take()`; reset
also when a request fails (baseline cleared so preflight gate cannot
stay frozen sub-threshold on retries).

**M3 migration**: adds `turn_cache_write_tokens`,
`cumulative_cache_write_tokens`, `pricing_authority`, `pricing_model`,
`pricing_cache_class` to `agent_metric_index`. Additive, idempotent,
guarded per-column by marker. M2 migration also guarded per-column (turn
and cumulative cache-read columns checked and added independently;
marker commits only after both are present). Fresh-DB schema includes
all columns.

**First-turn baselines**: `seed_zero_baseline` seeds `last_input:
Some(0)`, `last_output: Some(0)`, `last_cached_input: Some(0)`,
`last_cache_write: Some(0)`, and `last_total: Some(0)` — all have the
known-zero-at-spawn argument. Absent fields from incoming snapshots
still produce unknown (tri-state unchanged). Sessions buzz-acp did not
spawn (no seed) remain fail-closed on turn one.

**`ReportedUsage` TS mirror**: `cacheReadTokens`, `cacheWriteTokens`,
`freshInputTokens` added to `tauriArchive.ts` as `UsageField` members,
field-for-field with the Rust struct.

### P4a — aggregation layer

**Extended S-1 ladder** to cache-read and cache-write via the same
`ladder_token` path as the existing token fields.

**`freshInputTokens` derivation**: checked arithmetic, fail-closed —
absent cache fields produce Unknown (not zero), overflow and
`cacheRead+cacheWrite > input` both produce `incomplete: true`.
Aggregated as a `UsageField`.

**D6 comparator**: `sort_value()` = provider total when known, else
`input+output` when both known, else `None` (unknown-last). Replaces the
prior total-only comparator for both agent-level and model-level sort.
Ships a pinned test vector that the TS render layer (P5) must match.

## Test coverage

- `buzz-agent`: 440 lib + 15 integration (golden_transcripts) — includes
13 new `cache_total_state_tests`; 14 new `turn_io_state_tests`; 3 new
`sum_usage_*` tests (exact single-field, exact two-field, overflow
signals correctly); 3 new `parse_anthropic_*` tests (overflow flag set +
value cleared, normal sum no flag, absent usage no flag); end-to-end
golden transcript drives real subprocess with Anthropic-shaped
`input_tokens: u64::MAX, cache_read: 1` response and asserts
`accumulatedInputTokens` absent from the emitted `usage_update` — no
logic duplication; 3 wire pin tests; 4 `fold_pricing_identity_*` tests;
`pricing_authority()` explicit-:443 acceptance
- `buzz-acp`: 700 tests (691 lib + 9 integration) — 4 new usage tests
(absent input → unreliable+null; absent output → unreliable+null;
goose-shaped both present unchanged; poison mid-session); 3 ACP behavior
tests; 7 pool lifecycle tests
- Desktop (Rust): 2259+ tests — 14 new P4a pinned tests; 2 M3 round-trip
tests; 1 serde key-shape test; 2 M2 partial-schema migration tests;
first-turn cache round-trip test

## Related PRs

- P1 NIP-AM spec: [#4632](https://github.com/block/buzz/pull/4632)
- P3 pricing table: [#4629](https://github.com/block/buzz/pull/4629)
- UI (P5): [#4001](https://github.com/block/buzz/pull/4001)

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-10 10:47:37 -04:00
Taylor Ho
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
Taylor Ho
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
Wes
19b41e9c8e fix(desktop): stop rate-limited reconnect backfill from tearing down the authenticated socket (#4990)
## Problem

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

Mechanism (all on `main`):

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

## Fix

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

## Red/green proof

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

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

## Verification

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

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

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

---------

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

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

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

## Changes

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

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

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

## Screenshots

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

## Tests

Added coverage for:

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

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

---

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

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Signed-off-by: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz>
Co-authored-by: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz>
Co-authored-by: Amp <amp@ampcode.com>
2026-08-05 19:58:24 -06:00
Taylor Ho
2ea9385015 fix(reactions): support max-length custom emoji (#3833)
**Category:** fix
**User Impact:** Custom emoji with valid 64-character names can now be
used as reactions without errors.

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

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

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

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

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

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

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

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

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

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

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

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

</details>

## Reproduction Steps

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

## Verification

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

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

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
2026-08-05 21:02:57 +00:00
Taylor Ho
719f9730d4 feat(desktop): allow leaving your final community (#3621)
**Category:** improvement
**User Impact:** People can leave their final Buzz community and return
to **Join or create a community** without losing their signed-in
identity.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

</details>

### Reproduction steps

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

### Test plan

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


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

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: npub14ndfusear8wdpe4kss8h7juc7wjk78atnqzf63zvppcpneknv4sq6x9370 <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: npub14ndfusear8wdpe4kss8h7juc7wjk78atnqzf63zvppcpneknv4sq6x9370 <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-05 14:02:43 -07:00
Will Pfleger
6c40ce394f feat(desktop): cap OpenClaw agent parallelism at 5 (#4019)
OpenClaw connects to a single shared Gateway daemon. Spawning the
default 10 ACP workers per agent is both resource-expensive and
architecturally wrong — each worker opens a separate gateway connection.
Tyler's ruling: cap at 5, lower if needed.

## Contract

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

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

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

## Changes

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

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

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

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

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

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

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

## Tests

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

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

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

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

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

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

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

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-05 16:09:14 -04:00
Kalvin C
25a9cf1be6 feat: paste composer text without formatting (#4801)
## Summary

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

## Testing

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

## Manual verification

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

Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-04 16:16:14 -07:00
Will Pfleger
5179726737 fix(local-archive): default both archive settings to enabled (#4750)
## Overview

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

## What changed

### Rust

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

### Build / CI

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

### TypeScript

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

### Tests

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

## Preservation of explicit opt-outs

Users who have previously toggled the setting off are unaffected:

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

## Result

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

---------

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

## Problem

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

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

## Fixes

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

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

## Validation

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

## Not covered / follow-ups

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

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: npub1gjuws2a2dc8z2nszprtg7v6u9q7ffeah3hgl5yx45jwn7y7aqs6s5e9xj6 <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz>
Co-authored-by: npub1yxv5wk0u0fh6dwt925wntn7h397jvteyj4r87ttcd9xae7n2t3lqqj9jmm <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
Co-authored-by: npub1gjuws2a2dc8z2nszprtg7v6u9q7ffeah3hgl5yx45jwn7y7aqs6s5e9xj6 <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz>
2026-08-04 18:33:42 +00:00
Will Pfleger
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
klopez4212
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
klopez4212
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
Will Pfleger
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
Will Pfleger
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
Will Pfleger
be95a8a986 fix(config-bridge): add harness-definition env tier and fix equal-value model override (#3580)
All seven normalized config fields resolve through sanitized
`InheritedConfigTiers` passed wholesale to `read_config_surface`. The
reader's precedence tiers now match spawn's Layer 2b exactly — including
harness-definition env — and the equal-value model-override regression
is fixed.

## Changes

**`config_bridge/types.rs`** — add `InheritedConfigTiers`: persona env,
global env, harness definition env, structured model/provider/prompt for
both tiers. Add `HarnessDefault` `ConfigOrigin` variant for
harness-definition env values.

**`commands/agent_config.rs`** — `build_inherited_tiers` now resolves
the harness definition env using the same lookup path as spawn
(`record.runtime` → `persona.runtime` → empty string) and applies
`sanitize_inherited_env` to it. `resolve_config_surface` is unchanged in
shape — tiers passed to the reader now include `definition_env`.

**`config_bridge/reader.rs`** — `env_candidates` extended to 4-element
return (record, persona, global, definition). All five field builders
that use env candidates now include the definition-env slot below global
env and above the structured block, matching spawn Layer 2b. Magic
`configured[..6]` slice replaced with `configured[..configured.len()-1]`
(named split: all non-file candidates). Equal-value model-override arm
falls through to the normal resolve path instead of early-returning
`RuntimeOverride`, so the panel shows the baseline origin (e.g.
`BuzzExplicit`) rather than a spurious "Live override" label for a no-op
switch.

**`config_bridge/reader_tests_ext.rs`** — three new Layer 2b tests:
definition env beats structured persona model, global env beats
definition env, reserved-key-absent fallthrough.

**`commands/agent_config_tests.rs`** —
`genuine_explicit_live_switch_to_same_model_yields_clean_field` updated
to assert `origin == BuzzExplicit` (not `RuntimeOverride`); wrapped in
`with_no_goose_config` for hermeticity. New
`reserved_key_in_definition_env_shaped_map_is_stripped_by_sanitize` test
pins the shared sanitization contract.

**`AgentConfigPanel.tsx` / `types.ts`** — `HarnessDefault` origin
variant wired end-to-end: TS union type and provenance sentence
("Inherited from harness definition").

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-03 11:04:49 -04:00
Tyler
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
Tyler
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
Tyler
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