Commit Graph

1516 Commits

Author SHA1 Message Date
Duncan
f4efd99d43 feat(desktop): relocate permission-policy default to the agent definition
The editable permission-policy control belonged on the instance-edit
surface, but that surface is being retired and a definition-scoped
default is the correct home for an authority grant that describes how a
persona should behave.

Add the definition default as resolver tier 2 (instance override →
definition → global → built-in `ask`), live-resolved through the same
total `resolve_effective_permission_policy` signature every spawn,
summary, and deploy path already carries — never mint-copied, so a
definition-default change moves every linked instance on the next
summary. It is written through `apply_persona_behavior` as part of the
behavior group and is local-only: never published to the catalog and
never mint-copied, unlike `respond_to`/`parallelism`.

The instance surface (`AgentPermissionPolicyField`) becomes display-only:
it shows the resolved effective value, its source, and the per-instance
applied-vs-desired drift row, which stays instance-scoped because only a
deployed instance has a byte-stamped deploy receipt.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-12 15:08:25 -04:00
Duncan
0bc5683e52 fix(desktop): enforce deploy-receipt invariant for applied permission policy
The applied permission policy was extracted from the deploy payload via
.ok(), so a missing or unparseable launch.policy_env value stamped a silent
None on a successful deploy — suppressing the drift row and defeating the
field. The prior regression test manually seeded applied_permission_policy and
would have passed even with the production stamp deleted.

Extract the applied policy through extract_applied_permission_policy before the
provider is invoked; a broken payload invariant now fails the deploy instead of
stamping None. record_deploy_success/record_deploy_failure make the receipt
transitions explicit: success stamps the exact sent value, redeploy updates it,
failed redeploy retains the last confirmed value. Discriminating receipt and
transition tests replace the seeded regression, plus a TSX render test for the
drift row and an AGENTS.md contract entry.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-10 15:51:45 -04:00
Hayt
ca9acfa36a fix(desktop): exact-allowlist permission kinds, disclose persistent deny
PermissionDecisionButtons classified actionable options via kind.startsWith("reject"), so an unknown or malformed reject-prefixed kind (e.g. reject_later_v2) still rendered a trusted Deny button whose semantics the UI does not understand. Replace prefix matching with an exact recognized-kind allowlist (allow_once, reject_once, reject_always); any kind outside it fails closed and renders no action. reject_always stays actionable (persistent denial only reduces authority) but its label defaults to "Always deny" when the harness omits one, so a user never silently chooses persistence.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-10 15:45:26 -04:00
Duncan
00456e462a fix(desktop): persist applied permission policy for remote deploys
ManagedAgentSummary recomputed the displayed permission policy from the
mutable agent record plus global config, so flipping the global default
after a remote deploy made the UI report a policy the worker was not
actually running.

Stamp the byte-identical policy sent to the provider onto the record at
the deploy choke point, expose it on the summary as applied vs desired,
and surface drift in the policy field UI with a redeploy prompt.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-10 14:19:31 -04:00
Hayt
3ad999a618 fix(desktop): fail-closed PermissionDecisionButtons for allow_always and unknown kinds
allow_always rendered as an ordinary green Allow button, giving the observer
feed a one-click path to a durable grant — the same UX concern Wes flagged in
B2.  Unknown kinds also rendered as green Allow (Wes: should fail closed).

Tighten PermissionDecisionButtons:
- allow_once → actionable green Allow button (unchanged path)
- reject_* family → actionable red Deny button (unchanged path)
- allow_always → non-actionable amber badge: "Permanent grant — use request
  card".  The thread card (D5 disclosure) is the correct surface; the observer
  feed is advanced/debug and should not offer one-click durable grants.
- Unknown kinds → silently omitted (fail closed); no button rendered.

Add LifecycleActivity.render.test.mjs with five cases covering allow_once,
reject_once, allow_always (badge present, no button), unknown kind (nothing
rendered), and a mixed allow_once + allow_always card.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-10 12:13:01 -04:00
Duncan
ff5b21f374 chore: merge origin/main into duncan/permission-policy
Pull in #5406 (banner-dismiss fix) and successful_steer_deliveries
field added to TaskMeta. Resolves conflicts in lib.rs and pool.rs by
taking both permission_decision_tx (branch) and
successful_steer_deliveries (main) as distinct fields — each is
independently required.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-10 12:01:32 -04: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
Atish Patel
44456e200e fix(desktop): resolve overlapping member mentions (#5225)
## Why
Selecting or typing a member whose display name extends another member's
name, such as `@Fast Fizz Codex`, could emit p-tags for both identities
and wake the wrong agent.

## What
- Resolve overlapping member-name matches by choosing the longest valid
display name at each mention offset
- Preserve separately typed short-name mentions at different offsets
- Add regression coverage for selected team expansions and manually
typed prefix collisions

## Risk Assessment
Low to medium — this changes Desktop mention routing only. Exact
mentions and distinct offsets remain supported; same-length ambiguous
display names remain conservatively tagged because text alone cannot
disambiguate them.

Will resolve https://github.com/block/buzz/issues/2909

Generated with Goose

Signed-off-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz>
Signed-off-by: Atish Patel <atish@squareup.com>
Co-authored-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz>
2026-08-10 07:20:54 -07:00
renovate[bot]
e1ff91ecc1 chore(deps): update rust crate anyhow to v1.0.104 (#4447)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [anyhow](https://redirect.github.com/dtolnay/anyhow) | dependencies |
patch | `1.0.103` → `1.0.104` |
| [anyhow](https://redirect.github.com/dtolnay/anyhow) |
workspace.dependencies | patch | `1.0.103` → `1.0.104` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.

---

### Release Notes

<details>
<summary>dtolnay/anyhow (anyhow)</summary>

###
[`v1.0.104`](https://redirect.github.com/dtolnay/anyhow/releases/tag/1.0.104)

[Compare
Source](https://redirect.github.com/dtolnay/anyhow/compare/1.0.103...1.0.104)

- Update `syn` dev-dependency to version 3

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/block/buzz).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zLjIiLCJ1cGRhdGVkSW5WZXIiOiI0NC4zLjIiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-09 09:09:50 -07:00
Wes
97aa9e3185 fix(desktop): preserve Welcome banner dismissal (#5406)
## Summary

- remove the complete Welcome guidance surface when dismissal reaches
`hidden`
- preserve dismissal across the private and starter Welcome channels for
the active identity
- assert the starter channel's actual `welcome-everyone` title on
re-entry

## Why

PR #5330 introduced two deterministic Desktop E2E failures:

- the inner banner unmounted, but `welcome-composer-guidance-layer`
remained
- the re-entry test expected case-sensitive `Welcome` while navigating
to `welcome-everyone`

The state hook also scoped completion to channel IDs while `ChannelPane`
remounts during navigation. The Welcome guidance is one experience
spanning both Welcome channels, so completion now survives that remount
while remaining identity-scoped.

## Validation

At `b577eb42edffe889f63566f2457eacea720f3593`:

- `pnpm -C desktop typecheck`
- focused Biome check for all four changed files
- E2E build
- both `welcome-everywhere banner` integration tests repeated three
times: **6/6 passed**
- mandatory pre-push desktop check, typecheck, and full desktop unit
suite: **4,535 passed**
- `git diff --check`

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-09 09:05:47 -07:00
Hayt
60c247e20e test(e2e): align permission outcome assertion with label-based rendering
Test 03 asserted /Approved.*allow_once/ — the old format that rendered
the raw ACP kind string. describePermissionOutcome now returns the
harness-provided label (or verb-only fallback); the legacy non-ask path
has no label map, so the rendered text is 'Approved'.

Production code is untouched; this test file is the only change.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-08 17:59:53 -04:00
Hayt
f858cabb26 fix(desktop): sentinel-only edit gate in formatTimelineMessages; reword card doc
- formatTimelineMessages: after isAuthorizedMessageEdit passes, add sentinel
  check — if target content parses as a permission sentinel (v:1), only an edit
  signed by the original agent (byte-equal to target.pubkey) may replace the
  body. Owner/attacker edits of sentinels are silently dropped, leaving the
  authenticated pending body and editSignerPubkey unset. Generic owner-edit
  behavior for non-sentinel messages is unchanged.
- 4 integration regression tests in formatTimelineMessages.test.mjs drive the
  full formatTimelineMessages → computePermissionRequest path:
    1. agent-signed pending + owner-signed resolved edit → pending preserved
    2. edit-before-original arrival order → same result
    3. attacker-signed edit → pending preserved
    4. agent-signed resolved edit → card retires to resolved
- permission-request-card.tsx:1-15: reword doc comment from stale fence-protocol
  framing to version-1 bare-JSON sentinel; document wire format and the new
  formatTimelineMessages edit gate as a security invariant
- All 4637 desktop tests pass; all 6 gates green

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-08 17:18:31 -04:00
Hayt
31299008bc test(desktop): add harness integration fixture test for kind-9 parser
Embeds the exact kind-9 content string produced by buzz-acp's
build_sentinel_pending_payload (captured via kind9_content_fixture_structural_invariants
in crates/buzz-acp/src/acp.rs). Verifies that extractPermissionRequest
and isPermissionRequestSentinel accept the bytes the harness actually emits,
closing the harness→desktop wire boundary check.

Two new tests in the 'harness integration fixture' suite:
- test_harness_kind9_content_parses_to_pending_payload
- test_harness_kind9_content_identified_as_sentinel

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-08 16:03:02 -04:00
Hayt
a16b059836 fix(desktop): parse bare-JSON sentinel, unify expiry clock, tighten guards, add component tests
- extractPermissionRequest: replace fence parser with JSON.parse; sentinel is bare
  JSON object with v:1 (no code-fence wrapper)
- isPermissionRequestSentinel replaces stripPermissionRequestSentinel; MessageRow
  suppresses markdown body for agent sentinel messages
- PendingPermissionRequestCard owns single nowSecs tick; ExpiryCountdown and
  PermissionButtons share same clock — split-state expiry bug eliminated; click
  handler rechecks expiry at call time
- Type guards: outcome narrowed to union literal; expiresAt must be non-negative
  integer; originalEventId validated as 64-char lowercase hex; chosenOptionId
  non-null iff outcome===applied; every advertised optionId must have a label entry
- PermissionRequestCardBlock.test.mjs: 7 component tests (owner/non-owner/forged/
  edit-auth/expiry) with jsdom + @testing-library/react + fake timers
- permissionRequest.test.mjs: raw JSON fixtures (no wrap helper), 22+ rejection
  cases per frozen schema invariants, isPermissionRequestSentinel test suite
- computePermissionRequest.test.mjs: raw JSON fixtures, non-owner viewer path,
  replay/archive resolved state, past expiresAt accepted at parse time
- All 4631 desktop tests pass; all 6 gates green

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-08 15:55:30 -04:00
Duncan
74c9200ad8 Merge remote-tracking branch 'origin/main' into duncan/permission-policy
* origin/main:
  fix(desktop): welcome banner overlap and missing dismiss control (#5330)
  fix(desktop): prevent horizontal clipping in Prompt Context modal (#5324)
  chore(release): release Buzz Relay version 0.2.1 (#2856)
  chore(release): release Buzz Desktop version 0.5.8 (#5326)
  fix(buzz-agent): recover from 400-shaped image rejections; unbound benchmark agent rounds (#5318)
  Revert "fix(acp): reject unattended permission requests" (#5323)
  feat(desktop): unify add agent flows (#5015)
  fix(buzz-agent): budget summarizer reasoning separately so it cannot starve the handoff summary (#5248)
  infra: bind development services to loopback (#4871)
  chore(release): release Buzz Desktop version 0.5.7 (#5252)
  fix(desktop): isolate relay admission tests (#5221)
  fix(desktop): externalize boot <style> to prevent Tauri CSP nonce override (#5242)
  fix(desktop): let imported and recovered identities finish onboarding (#5228)

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-08 13:58:52 -04:00
Duncan
d808c4bdbf feat(acp): publish kind-9/40003 permission sentinel cards into channel thread
Implements Phase-1 of issue #4938: the harness now publishes kind-9
sentinel cards into the channel thread when a session/request_permission
reaches the Ask policy arm, and edits them to resolved state (kind-40003)
when the permission concludes.

Sentinel publish system (buzz-acp):
- Add relay_publisher, agent_relay_keys, agent_owner_pubkey_hex,
  turn_initiator_pubkey, sentinel_channel_id, sentinel_thread_reply_id
  fields to AcpClient; wired per-turn from pool.rs and lib.rs.
- D7-final admission check: Ask only fires for owner-initiated turns
  when relay_publisher is present; non-owner turns silently downgrade
  to reject so no card is posted for unattended sessions.
- kind-9 sentinel published after inserting the pending entry; event ID
  stored in PermissionEntry::sentinel_event_id for the edit.
- kind-40003 resolved edit published in finish_permission Ok path;
  skipped when sentinel_event_id is None (relay-absent sessions).
- Both publishes are fire-and-forget; permission flow never blocks on
  relay acceptance.
- publish_event_acked / PublishEventAcked / AckOutcome scaffolding
  preserved for future acked-publish callers.

Label fix (desktop):
- describePermissionOutcome: accepts optionLabels (display strings) and
  optionKinds (for deny/approve verb only) as separate maps; never
  renders raw ACP kind strings to the user.
- describePermissionTerminalReason: builds optionLabels from label ?? name
  fields; raw kind string falls back to verb-only ("Approved" / "Denied")
  when no display string is available.
- Legacy key path passes empty labels map (verb-only fallback).

Documentation (NIP-AO.md):
- Permission Sentinel Cards section: kind-9/40003 lifecycle, D7-final
  admission check, D5 durable-rule disclosure, sentinel authenticity.

Tests:
- 736 + 9 buzz-acp unit tests pass (sentinel publish skipped via
  relay_publisher: None in test helpers; D7 gate bypassed when
  relay_publisher is None).
- agentSessionTranscriptPermissions.test.mjs: 12 named tests for
  describePermissionOutcome and describePermissionTerminalReason.
- computePermissionRequest.test.mjs: tests for sentinel card parser.
- Updated expectations in agentSessionTranscript.test.mjs and
  ingestArchivedObserverEvents.test.mjs to match verb-only fallback.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-08 13:48:55 -04:00
Hayt
cef78d723d feat(desktop): render permission-request sentinel card in thread timeline
Implements the desktop side of issue #4938: a kind-9 sentinel published by
buzz-acp is parsed and displayed as an interactive card in the thread view.

Parser (permissionRequest.ts):
- Discriminated union PermissionRequestPending | PermissionRequestResolved
  aligned to the frozen schema (event b31c716e): requestNonce, sessionId,
  turnId, optionIds[], labels{}, hasDurableRule, durableRuleNote, outcome,
  chosenOptionId, originalEventId.
- All untrusted display strings capped at 200 chars; optionIds bounded to 10.
- extractPermissionRequest() and stripPermissionRequestSentinel() are
  render-safe (never throw).

Compute helper (computePermissionRequest.ts):
- D1 signer gate: kind-9 must be signed by the channel's known agent pubkey.
- Edit authenticity: resolved state accepted only from kind-40003 signed by
  the original agent (not owner, not attacker).
- selectProseOrPermission() mirrors selectProseOrNudge() from configNudge.

Card (permission-request-card.tsx):
- Pending: renders option buttons from optionIds + labels[optionId].
  Deny-style button heuristic uses label text (opaque optionIds carry no
  semantic). ExpiryCountdown ticks down to expiresAt; expired cards show
  'Timed out'. D5 durable-rule disclosure (durableRuleNote) shown below
  buttons when hasDurableRule is true.
- Resolved: shows outcome label derived from outcome + chosenOptionId.
- Actionable buttons gated: isOwner && state === 'pending' only.
- sendPermissionDecision() is fire-and-forget; button disables on click with
  'Decision sent'; re-enables on relay error so the user can retry.

Integration (MessageRow.tsx, formatTimelineMessages.ts, types.ts):
- editSignerPubkey threaded from formatTimelineMessages through TimelineMessage
  so the edit authenticity gate has the raw event signer without re-querying.
- PermissionRequestCardBlock wraps useIdentityQuery to resolve the current
  viewer, computes isOwner, and renders the card when a trusted sentinel is
  present. React.memo with custom comparator keeps MessageRow re-render budget
  clean.

Auth helper (permissionRequestAuthPubkey.ts):
- getPermissionRequestAgentPubkey() returns the agent pubkey only for
  known-agent-signed kind-9 events, mirroring getConfigNudgeAuthorPubkey.

Tests: 33 named tests covering all 6 frozen fixtures verbatim plus rejection
cases (bad version, unknown state, size bounds, malformed JSON, field
invariants) and the full signer/edit-authenticity gate matrix.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-08 13:17:54 -04:00
Will Pfleger
f029deafae fix(desktop): welcome banner overlap and missing dismiss control (#5330)
## Problem

The `WelcomeComposerGuidanceLayer` in the `#Welcome` channel was
positioned with `absolute inset-x-0 bottom-full z-[-1]` — outside the
`composerWrapperRef` measurement boundary. `useComposerHeightPadding`
observes `composerWrapperRef`'s block size to set `paddingBottom` on the
timeline scroll container, but the absolutely-positioned layer didn't
contribute to that size. The banner sat directly on top of the newest
message, blocking the thread affordance on that message, and had no
manual dismiss control.

## Fix

**Overlap**: Changed `WelcomeComposerGuidanceLayer` from `absolute
inset-x-0 bottom-full z-[-1]` to `relative` (in normal flow). As a
normal-flow child of `composer-dock`, the layer's full height is now
measured by the ResizeObserver and fed into the timeline's
`paddingBottom`, so the newest message is always fully visible and its
thread affordance is always clickable while the banner shows.

**Dismiss**: Added an `X` close button
(`data-testid="welcome-composer-dismiss-button"`) on the prompt state.
Clicking fires `onDismiss`, which drives `dismissing → hidden`
immediately (same slide-down animation as the auto-dismiss path) and
marks the channel ID as completed in the session ref so the banner does
not reappear on channel re-entry within the session.

**Refactor**: Extracted the banner state machine (refs, timers,
`useEffect`s, and callbacks) from `ChannelPane.tsx` into
`useWelcomeComposerBanner.ts`. This keeps `ChannelPane.tsx` well under
the 1000-line file-size ratchet and makes the state machine
independently testable.

## Changed files

- `desktop/src/features/channels/ui/WelcomeComposerBanner.tsx` —
`WelcomeComposerGuidanceLayer` positioning fix; `onDismiss` prop;
dismiss button; `overflow-hidden` / `mb-0` / `flex-1` cleanup
- `desktop/src/features/channels/ui/ChannelPane.tsx` — remove inline
banner state machine, use `useWelcomeComposerBanner` hook, pass
`onDismiss`
- `desktop/src/features/channels/ui/useWelcomeComposerBanner.ts` — new
hook owning all banner state

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
2026-08-08 13:00:09 -04:00
Will Pfleger
fbf89e3bed fix(desktop): prevent horizontal clipping in Prompt Context modal (#5324)
The Prompt Context modal (observer feed → check icon under sent
messages) was clipping all content and card right-padding at the dialog
edge.

**Root cause**: `PromptContextDialog` renders inside `DialogContent`,
which is a CSS grid. The child flex wrapper had default `min-width:
auto`, so the widest unbreakable token in the content (64-char hex event
IDs, `Tags: [[...]]` JSON) set the grid track width, blowing it past
`max-w-xl`. `overflow-hidden` then clipped everything at the dialog edge
— including the section cards' right padding.

**Fix**:
- `AgentSessionTranscriptList.tsx`: add `min-w-0` to the `flex
max-h-[85vh] flex-col` wrapper so the grid item can shrink below its
max-content width.
- `PromptSectionAccordion.tsx`: replace `wrap-break-word` with
`wrap-anywhere` on the body text (open and collapsed states) and the
title. `overflow-wrap: anywhere` reduces min-content width, which
`break-word` does not, letting long tokens wrap inside the cards rather
than inflating the track.

The `line-clamp-2` collapsed preview is preserved unchanged.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-08 12:59:41 -04:00
Wes
c815a9c6e1 chore(release): release Buzz Desktop version 0.5.8 (#5326)
## Buzz Desktop release v0.5.8

- **Frozen main:** `6a17d035f79ad582ca3f4f3cdc38d376f2c4087f`
- **Reviewed candidate:** `f3de860574bb3119018b4592353e9761635aeb07`
- **Previous desktop release:** `desktop-v0.5.7`
- **Proposed immutable tag:** `desktop-v0.5.8`

This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.

The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
2026-08-08 09:18:29 -07:00
Taylor Ho
02f640bc45 feat(desktop): unify add agent flows (#5015)
**Category:** improvement
**User Impact:** Users can create, discover, and import agents from one
consistent Add agent dialog.

**Problem:** Agent creation, discovery, and import were split across a
dropdown and separate dialogs, making the Add agent flow fragmented. The
existing E2E suite also continued targeting the deleted dropdown after
the flows were unified.

**Solution:** Route the new-agent card directly into a unified dialog
with dedicated Create, catalog, and Import navigation, then update the
affected E2E coverage to exercise that interface and its current empty
state.

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

**desktop/src/features/agents/ui/AgentDefinitionDialog.tsx**
Supports rendering the agent definition form inside the unified Add
agent experience while retaining the standalone dialog behavior.

**desktop/src/features/agents/ui/AgentDefinitionDialogShell.tsx**
Adds the shared shell used to present agent-definition content
consistently in embedded and standalone contexts.

**desktop/src/features/agents/ui/AgentDialog.tsx**
Passes the revised dialog state and close behavior through the existing
agent dialog entry point.

**desktop/src/features/agents/ui/AgentsView.tsx**
Connects the Agents page to the unified Add agent dialog and opens newly
added catalog agents in their profile panel.

**desktop/src/features/agents/ui/PersonaCatalogDialog.tsx**
Combines catalog browsing, agent creation, and snapshot import behind
persistent navigation, including dirty-navigation confirmation.

**desktop/src/features/agents/ui/UnifiedAgentsSection.tsx**
Replaces the new-agent dropdown with a direct Add agent entry point and
adjusts the responsive card grid.

**desktop/src/features/agents/ui/personaLibraryCopy.ts**
Updates catalog-facing copy for the unified experience.

**desktop/src/features/agents/ui/usePersonaActions.ts**
Returns the resolved local persona after catalog activation so the
caller can open the added agent.

**desktop/tests/e2e/agent-readiness-screenshots.spec.ts**
Opens the embedded create pane directly for readiness screenshots.

**desktop/tests/e2e/agents.spec.ts**
Covers unified Create, catalog, and Import navigation and asserts the
current shared-agent empty state.

**desktop/tests/e2e/global-agent-config-screenshots.spec.ts**
Updates global configuration screenshot setup for direct create-pane
entry.

**desktop/tests/e2e/inline-custom-harness.spec.ts**
Updates custom harness setup for the embedded create form.

**desktop/tests/e2e/persona-env-vars.spec.ts**
Updates environment-variable and model-provider scenarios for direct
create-pane entry.

**desktop/tests/e2e/persona-model-combobox-screenshots.spec.ts**
Updates model combobox screenshot setup for direct create-pane entry.

**desktop/tests/e2e/smoke.spec.ts**
Updates agent-creation smoke coverage for the unified Add agent dialog.

**desktop/tests/e2e/where-to-run-config.spec.ts**
Updates provider-selection coverage for the embedded create form.

</details>

## Reproduction steps

1. Open the Agents page and select the new-agent card.
2. Confirm the Add agent dialog opens directly on Create without an
intermediate dropdown.
3. Use the left navigation to browse shared agents and open Import.
4. Select a catalog agent and confirm the dialog closes and the added
agent's profile panel opens.
5. Run the affected desktop Playwright smoke and integration specs and
confirm all scenarios pass.

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-07 16:25:49 -07:00
Wes
13c9e900c8 chore(release): release Buzz Desktop version 0.5.7 (#5252)
## Buzz Desktop release v0.5.7

- **Frozen main:** `74b913cff8512c015dc6f1a7473b253fa803f954`
- **Reviewed candidate:** `f167818d25dd9f03115ab907a16f07daee2ece5c`
- **Previous desktop release:** `desktop-v0.5.6`
- **Proposed immutable tag:** `desktop-v0.5.7`

This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.

The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
2026-08-07 15:37:12 -07:00
Wes
74b913cff8 fix(desktop): isolate relay admission tests (#5221)
## Summary

- serialize the relay error-message test with all other tests mutating
the process-wide admission gate
- clear its 300-second rate-limit expiry after the assertion
- prevent the paused-time waiter test from observing another test's
state

## Root cause

`relay::tests::oversized_hint_is_capped_in_relay_error_message_string`
arms the process-wide gate for 300 seconds without taking `TEST_SERIAL`
or resetting it. In a parallel test run,
`relay_admission::tests::concurrent_429_extends_the_window_for_parked_waiters`
can observe that expiry, producing the reported `300.001s` instead of
`5s`.

## Validation

- focused admission suite + relay error test repeated 10 times
- pre-push `desktop-tauri-checks` passed, including the full Rust
workspace suite
- `branch-skew` passed

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-07 15:09:07 -07:00
Will Pfleger
dcc1231d6d fix(desktop): externalize boot <style> to prevent Tauri CSP nonce override (#5242)
## Problem

Two v0.5.6-only regressions were introduced by #4614 (the first enforced
Tauri CSP):

1. **Tab-complete caret regression** — after tab-completing an @mention,
#channel, or :emoji: shortcode, the cursor landed inside the inserted
text instead of after the trailing space. TipTap inserts the correct
text including the trailing space, but without its base stylesheet
(`.ProseMirror { white-space: break-spaces }`) the trailing space
collapses visually and the caret appears mid-name.

2. **Emoji picker unstyled** — the emoji-mart picker rendered as a giant
unstyled layout (oversized search SVG, collapsed grid) because
emoji-mart's shadow-root stylesheet injection was also blocked.

Both symptoms have the same root cause.

## Root Cause

Tauri's build-time asset processor scans `index.html` for inline
`<style>` elements, injects a nonce token, and adds the corresponding
`'nonce-…'` source to `style-src` at runtime. Per the CSP spec, **once a
nonce is present in a directive, the browser ignores `'unsafe-inline'`
for that directive**.

`index.html` contained an inline `<style>` with the boot background
color. When Tauri nonced it and injected `'nonce-…'` into `style-src`,
the intended `style-src 'self' 'unsafe-inline'` became effectively
`style-src 'self' 'nonce-…'` — blocking any runtime stylesheet injection
not covered by a matching nonce:

- TipTap's `injectCSS()` → `createStyleTag()` injecting `.ProseMirror {
white-space: break-spaces; … }`
- emoji-mart's shadow-root `document.createElement('style')` injection

(Inline scripts follow a separate path — they are SHA-256 hashed, not
nonced.)

This only reproduces in packaged builds (where Tauri's custom protocol
serves the HTML and enforces the policy). `tauri dev` loads from the
Vite dev server and is not affected.

## Fix

Move `html { background-color: #000; }` from an inline `<style>` in
`index.html` to `desktop/public/boot.css`, linked via `<link
rel="stylesheet">`. A linked stylesheet is not subject to Tauri's nonce
injection, so `'unsafe-inline'` in `style-src` applies as declared.

The `<link>` is render-blocking (same as the inline style was), so
boot-flash behaviour is identical.

**The production CSP string is unchanged.** This fix makes the policy
apply as intended — no security properties are altered. Will's follow-up
with the security team (Jordan Mecom / Eli Foster, authors of #4614) is
noted for post-ship.

A Tauri-faithful CSP harness for the Vite dev path (so this class of
regression is visible before a packaged build) is tracked as a separate
follow-up.

## Files Changed

- `desktop/index.html` — replace inline `<style>` with `<link
rel="stylesheet" href="/boot.css" />`
- `desktop/public/boot.css` — new file, the extracted `html {
background-color: #000; }` plus rationale comment
- `desktop/src-tauri/tests/csp.rs` — update comment: nonce for styles,
SHA-256 for the boot script

## Testing

- `just desktop-typecheck` ✅
- `just desktop-test` ✅ (4535/4535)
- `just desktop-tauri-test` ✅ (all Rust tests including `csp.rs`)
- Packaged validation: `pnpm tauri build --debug` completed; compiled
binary bakes `style-src 'self' 'unsafe-inline'` with no nonce source
injected ✅

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-07 15:05:38 -07:00
Taylor Ho
a5a9240241 fix(desktop): let imported and recovered identities finish onboarding (#5228)
**Category:** fix
**User Impact:** People who onboard by importing an existing key or
recovering from a phone can now use "Skip for now" (and Next) on the
harness setup and model config steps, instead of getting stuck.

**Problem:** On the "Set up your agent harnesses" and "Configure your
default model settings" onboarding steps, clicking **Skip for now** — or
**Next** — did nothing for anyone who reached those steps by importing
an existing key or recovering an identity from a phone. The app stayed
frozen on the step.

**Solution:** The onboarding state machine sets `continuingPubkeyRef` to
the current pubkey on import/recovery to keep the flow on `onboarding`
until setup finishes (added in #4845). But `complete()` never cleared
that ref, so once it matched the current pubkey the stage stayed pinned
to `onboarding` forever — completion could never win. `complete()` now
clears the ref so finishing/skipping actually settles the flow.
Fresh-generated keys never set the ref, which is why first-run fresh-key
skip already worked and the gap went unnoticed.

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

**desktop/src/features/onboarding/machineOnboarding.ts**
Clear `continuingPubkeyRef` inside `complete()` so an imported/recovered
identity's "continuing" marker no longer outlives completion and pin the
stage to `onboarding`.

**desktop/tests/e2e/onboarding.spec.ts**
Add a regression test that imports an existing key, reaches harness
setup, clicks **Skip for now**, and asserts onboarding exits (reaches
community onboarding). This fails without the fix. The existing skip
tests only exercised the fresh-key path, which never set the ref — hence
the gap.

</details>

## Reproduction steps

1. Start onboarding and choose **Use an existing key** (or recover from
a phone); import a key and continue to **Set up your agent harnesses**.
2. Click **Skip for now** (or **Next**). Before this change, nothing
happens — the step is stuck. The same trap hits **Configure your default
model settings**.
3. With this change, Skip/Next advances out of onboarding as intended.
4. Automated: `pnpm build:e2e && pnpm exec playwright test
onboarding.spec.ts --project=integration -g "imported-key users can skip
out of harness setup"` — passes with the fix, fails without it.

## Root cause

Introduced by #4845 (`feat(identity): recover desktop identity from a
signed-in phone`), which added `continuingPubkeyRef.current ===
currentPubkey` as an independent condition selecting the `onboarding`
stage. That guard has no off switch: `complete()` set the completion
flag but never cleared the ref, so the OR'd condition kept the stage
pinned. Not a revert candidate — the guard's intent (keep a
just-published identity in onboarding until setup finishes) is correct;
it just needed to release on completion.

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-07 15:03:43 -07:00
Duncan
e87f265d25 fix(desktop): use struct literal init to satisfy clippy::field_reassign_with_default
Four test-setup sites created GlobalAgentConfig::default() then immediately
assigned permission_policy. Clippy 1.95 flags this as field-reassign-with-default.
Rewrite each site to use a struct literal with ..Default::default().

Files: agents_deploy.rs (1 site), permission_policy.rs (3 sites).
Zero semantic change — test-only correction.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-07 18:00:34 -04:00
Duncan
2dd0706d8f refactor(desktop): extract RawManagedAgent/fromRawManagedAgent to managedAgentMapping.ts
tauri.ts exceeded the file-size ratchet after main's #3818 moved editMessage out,
shrinking the allowance from 1175 to 1161. The six permission-policy additions to
tauri.ts that were within the old headroom now exceed the tighter baseline.

Extract RawManagedAgent type and fromRawManagedAgent function into a dedicated
shared/api/managedAgentMapping.ts module (mirrors main's editMessage.ts pattern).
tauri.ts re-exports both for zero caller changes. Removes now-unused imports
(ManagedAgentBackend, PermissionPolicy, PermissionPolicySource, RawRestartDiffEntry).

Result: tauri.ts 1073 gate vs 1161 allowed (88-line margin).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-07 17:35:02 -04:00
Duncan
fe901d8300 Merge remote-tracking branch 'origin/main' into duncan/permission-policy
* origin/main: (32 commits)
  Recover from max-token response truncation (#5223)
  chore(release): release Buzz Desktop version 0.5.6 (#5214)
  fix(mobile): keep latest messages above composer (#4981)
  fix(sdk): preserve self-mention p tags in message and forum event builders (#4975)
  bump @tauri-apps/cli to ~2.11.4 to fix linux app icon issue (#4858)
  feat(desktop): adding rich link previews to messages (#3818)
  fix(buzz-agent): Responses reasoning summary, Anthropic display:summarized, ACP v2 messageId (#5195)
  fix(desktop): retain distinct agent instances in autocomplete (#5202)
  fix(desktop): defer channel visibility change to Save (#5203)
  feat(desktop): Projects follow-ups — access restrictions, fast loading, activity feed polish (#5073)
  refactor(cli): replace probe/decider/detail split with single typed extractor (#5191)
  fix(desktop): drop unhandled rejection from throwing window.Notification (#5143)
  fix(desktop): fence localStorage SecurityError from killing the React tree (#5142)
  fix(desktop): make terminal output selectable (#4980)
  fix(desktop): use WEBKIT_DMABUF_RENDERER_FORCE_SHM for NVIDIA/AppImage (#3654) (#4505)
  Make public starter channels best effort (#5192)
  Mobile: add anchored reaction popover (#5025)
  feat(mobile): add bee pull-to-refresh (#5059)
  Remove agent creation success modal (#5063)
  fix(buzz-agent): escalate LLM timeouts per retry and log per-call latency (#5130)
  ...

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>

# Conflicts:
#	desktop/src/shared/api/tauri.ts
2026-08-07 17:18:59 -04:00
Duncan
bcd9bacc97 refactor(desktop): extract permission/transcript types to fix file-size ratchet
Extract permission-related types from agentSessionTranscript.ts to
agentSessionTranscriptPermissions.ts, from tauri.ts to tauriEditMessage.ts,
and from types.ts to permissionPolicy.ts. Refactor AgentPermissionPolicyField
to a self-managing forwardRef component and extract useRespondToField hook to
OwnerOnlyAccessField.tsx to bring AgentInstanceEditDialog.tsx under its cap.
Trim doc comments on new permission fields. Fix &mut borrow on
apply_permission_policy_update call in agent_models.rs.

All nine ratcheted files are now at or below their allowances. Zero semantic
change — all exports and behavior are preserved via re-exports from the
original modules.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-07 17:02:19 -04:00
Wes
3855687e76 chore(release): release Buzz Desktop version 0.5.6 (#5214)
## Buzz Desktop release v0.5.6

- **Frozen main:** `78c87ae20e182fffdd99744d6c9ff99df82b159c`
- **Reviewed candidate:** `277d98a5cfb6d3b9af8b75122988f7a7df33ed5d`
- **Previous desktop release:** `desktop-v0.5.5`
- **Proposed immutable tag:** `desktop-v0.5.6`

This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.

The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
2026-08-07 14:54:16 -04:00
Tsung-Han Yu
c3c39cc263 bump @tauri-apps/cli to ~2.11.4 to fix linux app icon issue (#4858)
## Summary
<!-- What does this change and why? -->

#3419 is a tauri bug (https://github.com/tauri-apps/tauri/issues/15110),
which is already fixed in
https://github.com/tauri-apps/tauri/pull/15596. All we need is bump the
@tauri-apps/cli version to include the bug fix.

```sh
pnpm update --filter ./desktop @tauri-apps/cli@2.11.4
```

This pr simply includes the changes after running the update command.

### Related issue
<!-- Fixes #1234, or N/A. Before opening: search existing issues/PRs for
duplicates — link the closest one, or say "none found". -->

fix #3419

close #3436. this pr supersedes it.


### Testing
<!-- How was this verified? UI change? Include before/after screenshots
(or a short recording). -->

build the appimage and check the symlink in the appimage using
`unsquashfs`.
```sh
$ unsquashfs -o 944632 -ll /tmp/buzz/desktop/src-tauri/target/release/bundle/appimage/Buzz_0.5.4_amd64.AppImage | grep -i dirIcon
lrwxrwxrwx root/root                 8 2026-08-04 21:54 squashfs-root/.DirIcon -> Buzz.png
```

Signed-off-by: Tsung-Han Yu <14802181+johan456789@users.noreply.github.com>
2026-08-07 11:07:07 -07: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
Will Pfleger
742e8d1197 fix(buzz-agent): Responses reasoning summary, Anthropic display:summarized, ACP v2 messageId (#5195)
Three pre-existing gaps in the buzz-agent observer feed fixed together
per Will's ruling ("all 3 in the current PR"):

1. **OpenAI/DBv2-GPT route** — `responses_body` never requested
`reasoning.summary`; GPT-family models billed thinking tokens but
returned `summary: []`.
2. **Anthropic/DBv2-Claude route** — `anthropic_thinking_config()` never
sent `thinking.display`; newest Claude models (Opus 5, Sonnet 5, Fable
5, Mythos 5, Opus 4.7/4.8, Mythos Preview) default to
`display:"omitted"`, returning thinking blocks with an empty `thinking`
field — observer rendered nothing.
3. **ACP v2 compliance** — buzz-agent negotiates ACP v2 but emitted
`agent_thought_chunk` and `agent_message_chunk` without `messageId`,
which ACP v2's `ContentChunk` requires (`messageId` + `content` both
required at schema head `d13d1baa`).

## Changes

**`crates/buzz-agent/src/config.rs`**
- New `ThinkingSummary` enum (`Auto`/`Concise`/`Detailed`) with
`BUZZ_AGENT_THINKING_SUMMARY` env var (default `Auto`); mirrors
`BUZZ_AGENT_THINKING_EFFORT` pattern
- `anthropic_thinking_config()` now emits `"display": "summarized"` in
both the adaptive shape and the manual-budget shape whenever thinking is
enabled
- Rewrote `is_adaptive_thinking_model` and `anthropic_thinking_config`
doc comments to match Anthropic's exact three-way per-model terminology
(doc:
https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting#supported-models):
- Opus 4.6/4.7/4.8, Sonnet 4.6: **Off** — thinking OFF by default;
`type:"adaptive"` required to enable
- Opus 5, Sonnet 5: **On** — thinking on by default, can be disabled; we
still send `type:"adaptive"` to activate `output_config.effort`
- Fable 5, Mythos 5, Mythos Preview: **Always on** — thinking cannot be
disabled; we still send `type:"adaptive"` to activate
`output_config.effort`

**`crates/buzz-agent/src/llm.rs`**
- `responses_body` emits `reasoning.summary` alongside
`reasoning.effort` when effort is set (gated — no bare
`reasoning:{summary}` without effort)
- Covers both the pure-OpenAI Responses path and the DBv2 GPT-family
Responses path

**`crates/buzz-agent/src/agent.rs`**
- `agent_thought_chunk` carries `"messageId":
format!("{run_id}-thought-{round}")`
- `agent_message_chunk` carries `"messageId":
format!("{run_id}-message-{round}")`
- The two IDs are distinct (thought and assistant are two logical
messages per the ACP v2 Message ID RFD)
- `run_id` is a fresh random token per `session/prompt` invocation so
IDs are session-unique across multiple prompts

**`crates/buzz-agent/src/lib.rs`**
- `run_id` plumbed into `RunCtx` (was already generated in `run_prompt`,
just not threaded through)

**`crates/buzz-agent/tests/golden_transcripts.rs`**
- `test_acp_v2_chunks_carry_message_id` — negotiates v2, drives two
consecutive `session/prompt` calls, asserts: both chunk types carry
non-empty `messageId`; thought and message IDs are **distinct**; IDs do
**not** recur across the two prompts in the same ACP session

**`desktop/src-tauri/src/managed_agents/env_vars.rs`**
- `BUZZ_AGENT_THINKING_SUMMARY` added to `is_safe_to_reveal` allowlist

**`desktop/src-tauri/src/commands/agent_config_tests.rs`**
- Tests for `BUZZ_AGENT_THINKING_SUMMARY` allowlist entry
(case-insensitive)

## Tests added

- `parse_thinking_summary_round_trips_all_values`
- `parse_thinking_summary_unset_and_empty_yield_auto`
- `parse_thinking_summary_is_case_insensitive`
- `parse_thinking_summary_rejects_unknown_value`
- `thinking_summary_as_str_mapping`
- `responses_body_summary_present_iff_effort_set`
- `responses_body_emits_configured_summary_mode`
- `responses_body_concise_summary_mode`
- `anthropic_thinking_config_adaptive_emits_display_summarized`
- `anthropic_thinking_config_manual_budget_emits_display_summarized`
- `test_acp_v2_chunks_carry_message_id` (integration test — two-prompt
cross-session case)

## Notes

- **DBv2 gateway parity for `display`**: unverified — the DBv2 Claude
route proxies Anthropic Messages shape, but whether the gateway passes
`thinking.display` through is not confirmed. Flagged here rather than
blocking on it.
- buzz-acp and Desktop TS are unchanged — they already parse `messageId`
as optional and will pick it up from the wire automatically.
- Chat Completions and OpenRouter paths: untouched.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-07 13:45:43 -04:00
Atish Patel
e9925db54e fix(desktop): retain distinct agent instances in autocomplete (#5202)
## Summary
- preserve each distinct agent pubkey in autocomplete even when agents
share a persona or owner/name
- continue to collapse duplicate source rows for the same normalized
pubkey
- show a truncated pubkey in the channel member-add picker so same-named
instances are selectable

## Validation
- `pnpm --filter buzz test` — 4,489 passed
- `pnpm --filter buzz exec tsc --noEmit --pretty false`
- `pnpm --filter buzz exec biome check
src/features/agents/lib/agentAutocompleteEligibility.ts
src/features/agents/lib/agentAutocompleteEligibility.test.mjs
src/features/channels/ui/MembersSidebar.tsx`
- independent validation by Fast Fizz on
`509cb8d97b82f9708e24d4d59ad17c7b39516643`: typecheck, focused Biome,
22/22 focused tests, and `git diff --check`

Generated by Hardworking Honey.

---------

Signed-off-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz>
Co-authored-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz>
2026-08-07 17:31:59 +00:00
Kevin Chung
ef2ecaf873 fix(desktop): defer channel visibility change to Save (#5203)
## Problem

In the **Edit channel** dialog, flipping visibility (Public <> Private)
persisted **immediately on selection**, bypassing the **Save changes**
button — while every other field (name, description, temporary, TTL)
waited for an explicit save. This surprised users and gave no chance to
cancel a flip, e.g. a private->public change that instantly exposes
channel history.

Reported in the Buzz "Welcome" channel by Kevin Chung.

## Root cause

The visibility dropdown was wired to `handleConvertVisibility()`, which
called the update mutation on selection. This was intentional at the
time (there was even an e2e test named `02 — visibility updates
immediately` and an "Updating…" spinner), but it is inconsistent with
the rest of the dialog and is the surprising behavior reported.

## Change (defer to Save)

- Visibility becomes a **deferred draft** like the other fields:
selecting a value updates local `isPrivateDraft` and marks the draft
dirty. The change commits via `handleSaveChannelEdits` (which already
handled visibility) on **Save**, and is discarded on **Cancel**.
- The dialog title now reflects the **pending draft**
(`nextVisibility`), so the pending choice is visible before saving.
- The edit-dialog reset restores `isPrivateDraft` from server state.
- Removed the now-dead `handleConvertVisibility` handler,
`isConvertingVisibility` state, the `channelIdRef` race guard it needed,
and the unused `isPending`/"Updating…" spinner path in
`ChannelPermissionsSettings` (no caller passes `isPending` anymore).

## Tests

- Rewrote e2e `02` -> **`visibility defers to Save`**: select -> Save
enabled -> title reflects draft -> Save -> persists; toggling back to
the original value clears the draft and disables Save.
- Extended `09` (cancel discards drafts) to also cover a visibility
change.
- Repurposed `10`: the stale-update race it guarded is architecturally
gone, so it now asserts an **unsaved visibility draft does not leak
across a channel switch**.

## Validation

- `pnpm typecheck` — clean
- `biome check` (changed files) — clean
- `pnpm test` — **4497 passed / 0 failed**
- `playwright test --project=smoke channel-controls` — **10 passed**

Signed-off-by: Kevin Chung <chung@squareup.com>
Co-authored-by: Fizz <e3f95089179cc1bcc68d70c334b9bdf670d0470496db90bcdbb20386963432da@buzz.block.builderlab.xyz>
2026-08-07 17:25:13 +00:00
thomaspblock
fb73561e64 feat(desktop): Projects follow-ups — access restrictions, fast loading, activity feed polish (#5073)
## Summary

Follow-up batch on the Projects overview (continues merged #1677):

- **Repository access restrictions** — repositories the viewer can't
reach are surfaced with a reason instead of failing silently.
Channel-ACL denials (which arrive as the same 404 as a missing repo, for
anti-enumeration) are re-classified using the repository's channel
binding and the viewer's memberships (`useRepositoryAccess.ts`,
`projectRepoAvailability.ts`).
- **Projects loads in seconds instead of minutes** — enumeration no
longer crawls every kind:5 deletion event on the relay. It fetches
project/repo announcements first, then queries deletions scoped to those
coordinates via chunked `#a` filters (3 queries instead of hundreds on
staging).
- **Activity feed layout polish** — bare event-type glyph beside the
headline (no badge circle), timeline spine runs through the avatars
connecting consecutive cards, linkable actor/project names are bold in
theme foreground, rounded hover state, alignment fixes.
- **Create button pinned** — the "+" create menu is pinned to the pane's
top-right corner (equal 16px insets) and no longer scrolls away with the
page header.
- **List controls as a table header** — the scope selector (left) and
sort + layout toggle (right) render as the first row of the list
container on the Projects/Repositories/PRs/Issues tabs; in card view the
identical bar stands alone with the cards below
(`ProjectsListHeaderBar.tsx`).
- **Repository rows show the git location** — subtitle is
`github.com/org/repo` for external repos or `owner/repo` (resolved
profile name) for Buzz-hosted ones, instead of repeating the project
name (`repositoryDisplayPath`).
- **Uniform work-item row heights** — issue rows previously ran the
author chip in inline flow, letting the 20px avatar grow the line box
~3px taller than PR rows; both lists now share the same flex subtitle.

📸 Screenshots: [feed layout / pinned
button](https://github.com/block/buzz/pull/5073#issuecomment-5214114069)
· [list header / repo subtitles / row
heights](https://github.com/block/buzz/pull/5073#issuecomment-5217480712).

Note: two empty `chore: retrigger CI` commits exist on the branch from
working around the Aug 6 GitHub Actions incident; happy to drop them
with a signoff rebase before undrafting if preferred. Latest `main` is
merged in (`a0cc35220`).

## Test plan

- [x] Desktop unit tests (4,493 pass after merging main), Biome, tsc
- [x] New unit tests for scoped deletion enumeration and repo
availability re-classification
- [x] New unit tests for `repositoryDisplayPath` (external, Buzz-hosted,
unresolvable)
- [x] Screenshot verification of feed layout, connector spine, and
pinned button (top + scrolled states) — posted to the PR
- [x] Screenshot verification of the list header row (list + card), repo
subtitles, and matching PR/issue row heights — posted to the PR
- [ ] Manual pass against staging (projects list load time,
restricted-repo states)

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-07 19:23:35 +02:00
Duncan
73f2ffed83 fix(buzz-acp): thread single nonce through sync denial paths; upgrade two tests to pipe-level proof
Thread one nonce through each synchronous denial path so the acp_read
and acp_write telemetry frames always carry the same nonce. Before this
change, emit_permission_read_non_actionable generated its own nonce
internally while the caller passed a different nonce to
finish_permission_sync, producing one logical challenge/answer pair with
two different nonces. Desktop's nonce-only correlation rule left the read
card live because the write could never find it by nonce.

Fix: accept nonce as a parameter in emit_permission_read_non_actionable
(removing its internal new_permission_nonce() call) and drop the now-dead
caller_will_emit_read parameter from handle_permission_request and
emit_permission_read_with_nonce.

Upgrade two tests from telemetry-proxy assertions to direct pipe proofs:
- ask_permission_entry_deadline_equal_to_loop_hard_deadline_writes_denial_before_exit:
  replace observer telemetry assertion with a capture script that reads
  the denial line from child stdin NDJSON and parses the wire response.
- cancel_first_write_fails_stops_immediately_no_second_write: add a
  write-attempt counter (Arc<AtomicUsize> in write_ndjson_inner) to assert
  exactly ONE attempt was made and the loop stopped, not just that no
  successful writes occurred.

Add Rust tests proving the nonce is shared:
- sync_denial_malformed_options_read_and_write_carry_same_nonce
- sync_denial_preflight_failure_read_and_write_carry_same_nonce

Add TypeScript reducer tests:
- buildTranscript_sync_denial_write_with_matching_nonce_retires_card
- buildTranscript_sync_denial_write_with_mismatched_nonce_leaves_card_live

Update NIP-AO schema prose and observer.rs field comment to explicitly
document the permission_terminal exception to the authorization-only-on-
acp_read/acp_write rule.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-07 13:07:40 -04:00
iroiro147
e47894a133 fix(desktop): drop unhandled rejection from throwing window.Notification (#5143)
## Summary

WebKit throws `NotificationError` from the `Notification` constructor
when the notification backend becomes temporarily unavailable (measured
repro attached to #5081). Every existing call site used `void
sendDesktopNotification(...).then(...)` — discarding the returned
promise with no rejection handler — so a throwing constructor became an
unhandled promise rejection. The notification was silently dropped and
the only trace was console noise.

Closes #5081.

## What changed

Fenced the throw at the source inside `sendDesktopNotification`
(`desktop/src/features/notifications/lib/desktop.ts`):

- A new `try { ... } catch { ... }` wraps `new window.Notification(...)`
and the `onclick` attach.
- On catch, we `console.warn` once and `return false`, so the promise
the call sites discard is always fulfilled with the same boolean result.
No caller needs to change.

## Why at the source and not at each call site

The issue body lists four rejecting edges:
`useAppShellDesktopNotifications` (2×), `useReminderNotifications`,
`use-feed-desktop-notifications`. Patching them one-by-one leaves the
door open for the next consumer to make the same mistake — and the
function itself advertises `Promise<boolean>`, so callers are entitled
to assume the promise resolves with the delivery bit rather than
rejects. Fixing the inside satisfies both properties for every present
and future caller.

## Test plan

- Behavior change is a guarded return value around a single constructor;
unit coverage is best expressed inside the mounted-hook harness already
used in the repro. Existing notification helpers
(`shouldNotify*.test.mjs`) continue to pass.
- Full `just ci` runs on the blocker.
- The next notification after a backend blip delivers normally (the
throw is per-call, not sticky).

## Note on scope

This addresses the titled bug: unhandled rejection from a throwing
constructor. A separate, intended follow-up is to wire a user-visible
delivery-miss event if the platform exposes one — that's
notification-observability work, not a } catch.

---------

Signed-off-by: iroiro147 <sarthak.singh@mastersunion.org>
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-07 09:37:39 -07:00
iroiro147
8630e58eb0 fix(desktop): fence localStorage SecurityError from killing the React tree (#5142)
## Summary

WebKit throws `SecurityError` from `localStorage.getItem` (not just
`setItem`) when storage access is denied for the origin. With no
`ErrorBoundary` in `desktop/src`, any such throw inside a provider
render (`ThemeProvider`, `CommunitiesProvider`, `App` boot) propagated
to the reconciler, unmounted the root, and left a blank window. Measured
repro in #5078: a single throwing `getItem` on `buzz-communities` or
`buzz-active-community-id` kills the container.

Closes #5078.

## What changed

**New helper — `desktop/src/shared/lib/safeStorage.ts`**
- `getStorageItem(key, fallback?)` — wraps
`window.localStorage.getItem`; on a thrown error (SecurityError under
denied-storage origin) it warns once per key and returns the fallback.
- `setStorageItem(key, value)` and `removeStorageItem(key)` — same
fail-closed contract (return `false` on throw).
- Unit tests in `safeStorage.test.mjs` cover the happy path and the
`SecurityError` path.

**Rewired the init-path readers that ran before any UI existed**
- `desktop/src/features/communities/communityStorage.ts` —
`migrateLegacyCommunityStorage`, `loadCommunities`,
`loadActiveCommunityId`, `loadCommunityDiscoveryAfterLeave`,
`initFirstCommunity`
- `desktop/src/features/communities/legacyCommunityStorage.ts` —
`migrateLegacyCommunityStorageBeforeRender`
- `desktop/src/shared/theme/ThemeProvider.tsx` — `readStoredTheme`,
`applyCachedVars`, the `useState` initialisers for `accentColor` and
`followSystem`, and the accent re-read inside `applyTheme`

**Root-level fence — `desktop/src/app/RootErrorBoundary.tsx`**
- New top-level `ErrorBoundary` wrapping the whole provider tree in
`main.tsx`. Any remaining uncaught render error (a future storage read
that bypasses the helper, or any other render-time crash) renders a
degraded splash with a Reload button instead of a blank window.

## Test plan

- `desktop/src/shared/lib/safeStorage.test.mjs` — node `--test` runner,
11 assertions across healthy, absent, and SecurityError-throwing
storage.
- Full `just ci` runs on the blocker.
- Existing `communityStorage.test.mjs` and
`legacyCommunityStorage.test.mjs` continue to pass (they exercise the
same functions via in-memory Storage doubles; the new code path in
`migrateLegacyCommunityStorage` only adds a `try/catch` around the same
body).

## Why not an ErrorBoundary-only fix

A boundary alone can't help on a *clean* mount — the first throw already
unmounted the whole subtree before any state or fallback data was
loaded, so retrying would hit the same throw on the very next render.
The storage accessor has to fail closed *and* the boundary has to exist
for whatever bypasses it. Both are needed; neither is sufficient alone.

---------

Signed-off-by: iroiro147 <sarthak.singh@mastersunion.org>
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-07 09:37:04 -07:00
Wes
cc9a2f7833 fix(desktop): make terminal output selectable (#4980)
## Summary

- mirror the retained canvas terminal grid into a transparent,
selectable text layer
- preserve the canvas renderer and terminal focus behavior for ordinary
clicks
- reconstruct wide and combining glyphs correctly for clipboard text

## Why

Buzz Term renders output entirely on a canvas and deliberately called
`preventDefault()` on viewport mouse-down, so native selection and copy
could not work. A canvas has no selectable text even if that
cancellation is removed.

The transparent text layer stays aligned with the visible cell grid,
lets WebView native selection drive drag highlighting and copy, and
follows active-session switches without changing the renderer or PTY
protocol.

## Validation

- `pnpm --dir desktop typecheck`
- `pnpm --dir desktop test` — 4,373 passed
- pre-push `desktop-check`, `desktop-test`, and `branch-skew` hooks
passed on `1f2a3f8db63f6fe36b4a28bc911aea3c5186b2b0`

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-07 09:30:35 -07:00
Duncan
7d277dab4d fix(acp): address Thufir pass-4 review findings
- Every ask terminal routes through finish_permission(); applied path
  stops on false (poison) immediately instead of looping back. Cancel
  path uses None idle sentinel instead of dummy instant.
- Deadline equality: process expired entries first at entry.deadline ==
  hard_deadline, then return HardTimeout — fail-closed response is
  always written before exit.
- Wire-truth tests: production-path, cancel, and nine-request tests now
  capture child stdin NDJSON and assert parsed exact lines/ids. Temporal
  tests rebuilt around one continuously running loop per scenario.
- Desktop nonce-present = nonce-only: unknown nonce drops the frame
  without falling back to the id map. Legacy fallback keyed by compound
  (channel:session:turn:id), never bare id. Both indexes cleaned on every
  terminal (acp_write, permission_terminal) and backstop (turn_completed,
  turn_error). New tests: FOREIGN-nonce drop + cleanup assertions on both
  indexes for all four terminal paths.
- NIP-AO: permission_terminal in frame-kind table; synchronous policy
  outcomes (rejected/allowed/allow_failed_closed) in reason table with
  explanatory note distinguishing ask vs. synchronous paths.
- Desktop: permission_terminal handler uses pinned uncertain copy; tests
  for live replay and lifecycle-only archive replay.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-07 12:25:10 -04:00
Taksh Kothari
60ae74b656 fix(desktop): use WEBKIT_DMABUF_RENDERER_FORCE_SHM for NVIDIA/AppImage (#3654) (#4505)
## Summary
- Heuristic and `--safe-rendering` now set
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` instead of
`WEBKIT_DISABLE_DMABUF_RENDERER=1`
- Legacy `DISABLE_DMABUF` stays owned so operators can still set
`=0`/`=1` and take over the decision
- Linux troubleshooting docs updated to match (#3654)

## Test plan
- [ ] unit tests in `webkit_rendering::tests`
- [ ] On NVIDIA + WebKitGTK 2.52: workspace switch no longer SIGSEGVs
where the distro NVIDIA guard does not fire (Debian/Ubuntu
proprietary-NVIDIA may still crash — #3654 stays open for that path)
- [ ] `WEBKIT_DISABLE_DMABUF_RENDERER=0` still stands the heuristic down

---------

Signed-off-by: Taksh <takshkothari09@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-07 12:24:27 -04:00
Wes
daa8877581 Make public starter channels best effort (#5192)
## Summary
- treat public starter-channel provisioning as best-effort after
preserving the required private Welcome path
- let community onboarding complete and focus Welcome when the reported
metadata lookup error occurs
- remove the now-obsolete retry-toast expectations for optional starter
provisioning

## Scope
This intentionally does not change relay tombstone semantics or
auto-join existing public channels.

## Test plan
- `pnpm exec playwright test tests/e2e/deep-link-invite.spec.ts` (8
passed)
- `pnpm exec playwright test tests/e2e/onboarding.spec.ts --grep "failed
public starter channel setup"` (1 passed)
- `pnpm typecheck`
- `pnpm check`
- `pnpm test` (4483 passed)
- pre-push hook: branch-skew, desktop-check, desktop-typecheck,
desktop-test passed on `4658a07beb1e1d54443da5cd2e4a28fae0232f24`

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-07 09:21:29 -07:00
klopez4212
c8743b2f20 Remove agent creation success modal (#5063)
## Summary
- remove the post-creation private-key modal
- return directly to the underlying page with one “Agent created” toast
- preserve failed channel-attachment retry through an actionable toast

## Validation
- desktop checks and E2E build
- 4,392 desktop unit tests
- focused Playwright coverage for standard, customized, and
attachment-retry creation flows

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-07 17:03:16 +01:00
Duncan
6607eaf55e fix(acp): structural round — one nonce-keyed permission record, finish_permission() helper
Implements Thufir's minimal shape across three passes of residual defects:

Harness (acp.rs):
- Remove PermissionEntryState::Resolved — entries are removed from the map
  on every terminal transition (applied/timed_out/cancelled). The absence
  of a nonce is the replay guard; no tombstones means capacity counts only
  live (Pending|Writing) requests, fixing the 9th-request-in-one-turn bug.
- Add finish_permission() terminal helper owning Pending→Writing→Resolved
  for all terminals. Exactly one write+flush, exactly one nonce-correlated
  authorized acp_write with the terminal reason. Any write failure poisons
  the process and emits a permission_terminal observer-only event so Desktop
  can retire the card (the uncertain path).
- Cancel path: write failure also poisons and emits permission_terminal.
  cancel-during-write emits permission_terminal for the in-flight entry.
- Re-arm idle deadline when live pending count reaches zero so a slow human
  decision grants a fresh idle window instead of insta-cancelling the turn.
- Capacity check now counts only live (Pending|Writing) entries.
- Clippy: fix assert_eq!(x, true) → assert!(x), while-let-loop, doc
  overindented list items in acp.rs and config.rs.
- Fmt: cargo fmt applied.

Desktop (agentSessionTranscript.ts):
- acp_write authorized frames correlate exclusively by authorization.requestNonce
  (primary); JSON-RPC id correlation is a legacy fallback for non-ask paths.
- Terminal copy derives from authorization.reason (applied/timed_out/cancelled/
  uncertain) via describePermissionTerminalReason — timeout now renders 'Timed out'
  not 'Denied (reject_once)'.
- set actionable: false on all retirement paths.
- permission_terminal observer event handler retires the card via nonce.
- turn_completed and turn_error backstop: retireAllLivePermissionCards() retires
  any still-live cards so missing telemetry and archive replay cannot reconstruct
  live controls.
- Biome format applied.

lib.rs:
- fit_observer_event_to_budget: early return without mutation when
  event.authorization.is_some() — authorized frames are never leaf-trimmed
  or stubbed (NIP-AO §3 byte-for-byte requirement).
- Enqueue suppresses over-cap authorized frames entirely (defense in depth).
- Test: test_authorized_frame_payload_is_never_trimmed.

Tests:
- ask_production_path_emits_request_captures_nonce_and_delivers_decision:
  real script emits session/request_permission, harness captures nonce from
  in-process observer, routes decision through channel, asserts end_turn.
- cancel_writes_exactly_one_response_per_pending_id_no_replay: registers
  entries via production path, captures nonces before cancel, verifies each
  emitted cancel nonce matches a registered entry nonce, verifies no replay.
- ask_permission_idle_is_suspended_while_pending_entry_exists: paused-time,
  asserts entry present at 299s.
- ask_permission_deadline_fires_at_300_seconds: paused-time, asserts entry
  removed at exactly 300s.
- ask_permission_idle_rearmed_after_last_entry_resolves: paused-time,
  proves idle deadline re-armed after decision applied.
- ask_nine_sequential_requests_all_succeed_after_capacity_recovery: nine
  sequential requests each decided before the next is queued; asserts 9
  distinct authorized acp_write observer nonces.

NIP-AO.md:
- session_resolved = session establishment (not terminal).
- Added turn_completed and turn_error rows as terminal lifecycle events.
- uncertain path: permission_terminal observer event replaces wrong
  session_resolved reference.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-07 11:25:19 -04:00
klopez4212
cd2125c34b Improve video review readiness and controls (#5161)
![inline-hover-timeline](https://raw.githubusercontent.com/block/buzz/d7e047a684a3908f8f01e59fde531b0fbae47b8a/pr-5161--inline-hover-timeline.png)


![thread-timecode-chip](https://raw.githubusercontent.com/block/buzz/d7e047a684a3908f8f01e59fde531b0fbae47b8a/pr-5161--thread-timecode-chip.png)


![review-modal-ready](https://raw.githubusercontent.com/block/buzz/d7e047a684a3908f8f01e59fde531b0fbae47b8a/pr-5161--review-modal-ready.png)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-07 07:45:18 -07:00
klopez4212
c71f658539 Polish advanced agent setup and Welcome composer (#4926)
## Summary
- move **Run on** into Advanced, directly after **Who can send
instructions**
- reuse the modal’s shared dropdown styling
- give the Welcome guidance and composer matching glass treatment while
preserving the corrected exit layering

## Validation
- `pnpm -C desktop typecheck`
- focused Playwright: Run on configuration (3 passed)
- focused Playwright: Welcome onboarding flow (1 passed)
- desktop unit suite (4,290 passed)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Watcher <bb7abfd757d0af7b66569d02ab9c0316b616f9d0c151ecf5b964344c462e7f8f@buzz.block.builderlab.xyz>
Co-authored-by: Watcher <bb7abfd757d0af7b66569d02ab9c0316b616f9d0c151ecf5b964344c462e7f8f@buzz.block.builderlab.xyz>
2026-08-07 15:15:34 +01:00
Jordan Mecom
769ac70b74 fix(media): require authenticated reads (#4610)
This change requires a valid signed Blossom authorization request and
current relay membership for every media GET and HEAD request. It
removes the unauthenticated compatibility path and updates desktop reads
to send the required authorization.

This blocks anonymous retrieval and access after relay-membership
revocation. It does not yet bind a blob to its originating channel, so
someone removed from a private channel can still read a known blob while
remaining a relay member. That channel-ACL follow-up remains required
before closing the full finding.

## Testing

- `git diff --check origin/main...codex/security-media-read-auth`
- Rebased onto `origin/main` at `5c98932`
- Full CI pending

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

---------

Signed-off-by: Jordan Mecom <jm@squareup.com>
Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
Signed-off-by: Eli Foster <efoster@squareup.com>
Co-authored-by: Eli Foster <efoster@squareup.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:46:42 +00:00
Taylor Ho
f03de210cd fix(desktop): preserve authoritative agent avatars (#4984)
**Category:** fix
**User Impact:** Agent cards and catalog listings now show the avatar
belonging to the identity they represent.

**Problem:** Running agent cards could show a stale definition avatar
instead of the concrete agent profile, while adding another publisher's
catalog entry could let local edits repaint that publisher's listing.
This made agent identity look inconsistent across My Agents and the
Agent Catalog.

**Solution:** Treat the concrete agent pubkey profile as authoritative
for running-card avatars, with the linked definition as fallback. Keep
relay publications authoritative for foreign catalog presentation while
using local copies only for linkage and selection state.

| before | after |
|--|--|
| <img width="874" height="592" alt="Screenshot 2026-08-06 at 3 48
43 PM"
src="https://github.com/user-attachments/assets/2cc6c9f7-ea50-413c-9c7b-4d34bd8b4ec7"
/> | <img width="884" height="597" alt="Screenshot 2026-08-06 at 3 48
40 PM"
src="https://github.com/user-attachments/assets/b14a865c-65c4-458f-9c30-d1a557c877d7"
/> |
| agent-set avatar not showing | agent-set avatar is showing |

## Changes

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

**desktop/src/features/agents/lib/agentCardAvatar.ts**
Adds the explicit avatar precedence rule for running agent cards and
blocks avatar-dependent actions until the authoritative profile query
settles.

**desktop/src/features/agents/lib/agentCardAvatar.test.mjs**
Covers profile precedence, definition fallback, blank avatar handling,
and the profile-loading transition for linked-agent actions.

**desktop/src/features/agents/lib/personaCatalogRelay.ts**
Keeps publisher-provided catalog identity and behavior fields
authoritative after a local copy is added.

**desktop/src/features/agents/lib/personaCatalogRelay.test.mjs**
Verifies local copies contribute linkage and selection without
overriding publisher presentation.

**desktop/src/features/agents/ui/UnifiedAgentsSection.tsx**
Uses the concrete agent profile avatar before the linked definition
avatar on running-agent cards.

</details>

## Reproduction Steps

### Running agent card uses the agent profile avatar

Use two visibly different, publicly reachable image URLs: **A** for the
saved definition and **B** for the running agent profile.

1. In **Settings → Experiments**, enable **Agent-managed profiles**.
This prevents Desktop from restoring the definition avatar over an
agent's own relay-profile changes.
2. In **Agents**, create an agent with image **A** as its avatar and
start it.
3. In a channel containing that agent, ask it to update its own Buzz
profile avatar to image **B**. The exact CLI operation under the agent
identity is `buzz users set-profile --avatar <image-B-url>`.
4. After the agent confirms the update, reopen **Agents → My Agents**
(or reload the page so its kind:0 profile is fetched again).
5. Verify the running agent card shows image **B**, not definition image
**A**. Open **⋯ → Share** and verify the share flow also uses image
**B**.

Before this fix, the My Agents card and share flow preferred image **A**
whenever the linked definition had an avatar.

### Catalog listing remains publisher-authoritative

This scenario requires a second Buzz identity so the entry is foreign to
the account under test.

1. As the publisher identity, create an agent definition with a
distinctive name, avatar, and instructions, then use **Share → Share to
catalog**.
2. As the test identity, open **Agents → Discover agents**, find that
publication, and add it.
3. In **My Agents**, open the added copy's **⋯ → Edit**, change its
name, avatar, and instructions, and save.
4. Return to **Discover agents** and find the same publisher entry.
5. Verify it remains selected/added but still shows the publisher's
original name, avatar, and instructions—not the test identity's local
edits.

## Validation

- `pnpm test` — 4,376 passed
- `pnpm typecheck` — passed
- `pnpm check` — passed with existing non-error notices

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-06 17:14:12 -07:00
Taylor Ho
67b77344d6 fix(desktop): next/back navigation during key creation onboarding (#4978)
**Category:** fix
**User Impact:** Users can navigate back while an identity key is being
created, while Next remains visible and unavailable until creation
finishes.
**Problem:** The key-creation hold hid both navigation actions, leaving
users without an escape route or a clear indication of what would happen
next. **Solution:** Keep the onboarding footer mounted throughout
creation, leave Back enabled, and gate Next on the completed identity
state.

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

**desktop/src/features/onboarding/ui/BackupStep.tsx**
Keeps the onboarding navigation footer visible during key creation, with
Back available and Next disabled until the identity is ready.

**desktop/tests/e2e/onboarding-backup.spec.ts**
Covers the loading and completed navigation states so the intended
behavior cannot quietly crawl back out of the pit.

</details>

## Reproduction steps

1. Start desktop onboarding and choose to create a new identity.
2. Submit the profile step and observe the key-creation screen.
3. Confirm Back is enabled while Next is visible but disabled.
4. Wait for key creation to finish and confirm Next becomes enabled.


## Screenshots

| Before | After |
| --- | --- |
| Navigation actions are hidden during key creation. | Back remains
enabled while Next stays visible and disabled. |
| ![Before: key creation screen without navigation
actions](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4978/key-creation-before-bird.png)
| ![After: key creation screen with disabled Next and enabled
Back](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4978/key-creation-after-bird.png)
|

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-06 17:13:55 -07:00