Commit Graph

1704 Commits

Author SHA1 Message Date
Duncan
0b18b2bc60 Merge remote-tracking branch 'origin/main' into duncan/phase1-merged-agent-edit
* origin/main:
  Refresh mobile utility surfaces and theme picker (#6944)
  fix(desktop): complete project empty and context states (#6980)
  Fix mobile jump-to-latest flicker (#6807)
  refactor(relay): NIP-98 admin auth with Operator/Moderator roles and NIP-11 discovery (#3777)
  refactor(db): split channel membership store (#6782)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-28 14:13:37 -04:00
Duncan
fdd41e551b fix(desktop): guard concurrent definition edits with an authoritative pre-write fetch
The concurrent-definition guard compared the seed-time revision against the
React-query-cached ctx.updatedAt, which can lag a concurrent writer. Writer B
could submit before its cache received writer A's newer write, so both values
read stale-equal, the guard passed, and B's full-replacement input clobbered
A's fields. Refetch the persisted definition before the first write and compare
that authoritative revision, closing the TOCTOU window.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-28 14:11:10 -04:00
thomaspblock
a89f5df4c5 fix(desktop): complete project empty and context states (#6980)
## Summary

First-time project users can now create a project directly from the
empty state instead of reaching a dead end. The right-side project
context also uses the intended Activity label and aligns its first row
with the channel header.

The populated and empty project views share one creation flow, so
navigation, success feedback, and compatibility warnings remain
consistent.

### Related issue

Related: #6939

### Testing

- Desktop check, TypeScript typecheck, and repository file-size gate
- 10 focused project overview context unit tests
- E2E-mode build and 3 focused Playwright scenarios covering empty-state
creation, right-sidebar alignment, and Activity context

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-28 10:52:03 -07:00
Duncan
e2e0fe6271 fix(desktop): name the publish notice from the observed persona
The publish-catalog success notice reused the instance-oriented agentName, which falls back to the pre-save def.displayName on a definition-only edit (both agent candidates null). A published Alice->Bob rename then toasted "Updated Alice." Derive the notice name from observedPersona.displayName first, separate from the instance-oriented name used by the sync warning and generic toast.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-28 11:09:44 -04:00
Duncan
bbd7854185 Merge remote-tracking branch 'origin/main' into duncan/phase1-merged-agent-edit
* origin/main:
  feat(auth): add NIP-FI canonical assertion verifier and contracts (#6776)
  fix(desktop): resolve exact typed mentions on space (#6862)
  perf(desktop): restore project context during startup (#6939)
  fix(desktop): lift right auxiliary pane above shared header backdrop (#6966)
  fix(ci): bump Codex CLI to 0.150.1 to unhang security review jobs (#6962)
  feat(desktop): implement 30178 team catalog backend (#5112)
  feat(model-capabilities): humanize Databricks UC model families (#6955)
  feat(agent): discover Databricks Unity Catalog models (#6918)
  test(db): use canonical channel roster fixtures (#6819)
  preserve channel description paragraph breaks (#6946)
  fix(cli): enrich template cardinality error with per-candidate presence and profile hints (#4825)
  Fix Codex security review authorization (#6913)
  fix(db): disable heartbeat vacuum truncation (#6898)
  chore(deps): update rui314/setup-mold digest to 7e4f20a (#6663)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-28 10:46:41 -04:00
Duncan
08a2944878 fix(desktop): keep Relay Mesh runtime switch on the shown layer and name the observed agent
handleProviderDropdownChange forced buzz-agent through the instance handler unconditionally, so a linked edit pinned the clicked instance to buzz-agent while the definition kept its old runtime. Route through the definition handler when a definition is shown and consult that layer's runtime id.

The full-success toast preferred latestAgent?.name, which only advances on a non-throwing updateManagedAgent, so a rename that committed but whose command threw after commit reported the stale pre-save name. Prefer the observed name from the final refetch.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-28 10:44:17 -04:00
Matt Toohey
b49675894b fix(desktop): resolve exact typed mentions on space (#6862)
## Summary

- Convert an exact, authorized manually typed `@Name` into the existing
visual mention chip when plain Space is pressed.
- Reuse autocomplete selection so outbound mention pubkeys, agent
styling, and address behavior remain identical.
- Preserve partial names, longer multi-word names, duplicate-name
ranking, modifier keys, and IME composition.

Before:


https://github.com/user-attachments/assets/2bccc0eb-5c36-4554-8a2d-d67e41d85e64

After:


https://github.com/user-attachments/assets/12859538-031d-44b0-ab38-40e1fb25c256


### Related issue

None found. Originating Buzz conversation: channel
`5efbefaf-f478-4574-927e-32f28df07f09`, thread
`563cb6709454b924405caa07f120ba9b88f074242fc3ee168686da76038b303f`.

### Testing

Validated at `7f30db7b54e484cb510bd0a616db98a49ce1b6ae`:

- `pnpm test` — 5,562 passed
- `pnpm check`
- `pnpm typecheck`
- `pnpm check:file-sizes`
- `pnpm build:e2e`
- `pnpm exec playwright test tests/e2e/mentions.spec.ts --project=smoke
--reporter=line` — 74 passed

---------

Signed-off-by: Jitter <d14dfe033ef0f809866f9f984de04821b0d900d7652fd85a54776ee40ca3a68f@buzz.block.builderlab.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Jitter <d14dfe033ef0f809866f9f984de04821b0d900d7652fd85a54776ee40ca3a68f@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 15:07:54 +10:00
thomaspblock
9733863516 perf(desktop): restore project context during startup (#6939)
## Summary
Project channels previously appeared as ordinary channels until the
community-wide project enumeration completed, delaying the contextual
right rail most noticeably on larger relays.

- Restore the last fully validated, relay-and-identity-scoped project
collection immediately while keeping live relay data authoritative.
- Resolve the active channel's project home through scoped
`#buzz-channel` queries instead of waiting for the complete project
scan.
- Keep snapshots aligned with community removal and relay reconnect
invalidation.

### Related issue
None found.

### Testing
- Pre-push `file-size-check`, `desktop-check`, `desktop-typecheck`, and
`desktop-test`
- Project enumeration, snapshot persistence, and relay invalidation unit
coverage
- Targeted Playwright scenario: project sidebar rows open the home
channel and nest extra channels

No numeric startup benchmark was captured; this draft validates the
cache and scoped-query behavior while leaving timing measurement for
review.

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-27 22:36:26 -04:00
Will Pfleger
350caf1de3 fix(desktop): lift right auxiliary pane above shared header backdrop (#6966)
## Summary

The sidebar close (`X`) and `Edit` controls disappeared for in-channel
side panels on `main`. They stayed clickable but were visually washed
out.

[#6901](https://github.com/block/buzz/pull/6901) added `isolate` to
`RightAuxiliaryPane` so project workspace sheets slide over an open
thread cleanly. The pane carries no `z-index`, so isolating it makes the
whole pane subtree paint at stacking level 0 — below the channel's
sibling `z-30` shared-header backdrop in split layout. That translucent
blur strip then washes out the pane's own `z-40` header chrome, where
`X`/`Edit` live. The backdrop is `pointer-events-none`, so the controls
remained clickable but invisible (matching the reported videos).
Threads, channel settings, and in-channel agent panels all route through
this wrapper, so all three were affected; the standalone Agents-nav
panel does not render inside `ChannelPane`, so it had no backdrop and
worked.

## Fix

Add `z-31` to the pane's `aside` — above the `z-30` backdrop, still
below the `z-41` thread drawer/sheet overlays. `isolate` stays, so
#6901's sheet-over-thread layering is preserved and both behaviors
coexist.

## Regression coverage

`tests/e2e/auxiliary-pane-close-visibility.spec.ts` opens a split-layout
thread from inside a channel and asserts the pane establishes its
`isolate` stacking context and that its `z-index` outranks the
shared-header backdrop. It fails on the pre-fix tree (pane `z-index` is
`auto`) and passes with `z-31`. #6901's `project workspace sheet stays
independent from an open thread` spec still passes.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-27 20:22:28 -04:00
Will Pfleger
a7c7414a7f feat(desktop): implement 30178 team catalog backend (#5112)
Implements the Rust/backend half of 30178 team catalog sharing on the
community catalog. No TS callers yet — this is PR 1 of 2;
[#3995](https://github.com/block/buzz/pull/3995) (stacked here) adds the
parse layer, hooks, `CommunityCatalogDialog`, and e2e tests.

## What this adds

**Projection builder** (`team_catalog.rs`):
`build_team_catalog_event/content` produces a 30178 event from a team +
member definitions. Size contracts: 192 KiB total ceiling, per-field
bounds (name 256 B, text 4 KiB, system-prompt 16 KiB, avatar URL 32 KiB
for projection fields; https URLs additionally validated at 2,048
bytes). Avatar handling: oversized raster data URLs downscaled to fit;
oversized built-in avatars silently omitted; oversized https URLs
rejected with a named error.

**Share/unshare/tombstone** (`commands/teams/pending.rs`):
`prepare_team_publication_at` retains a signed 30178 head for the flush
loop; share state is relay-scoped (one community can share while another
does not). `refresh_or_retract_shared_head_at` rebuilds or tombstones
the head immediately on team/member edit — no stale publication until
the next boot. Both the 30178 catalog and 30176 team tombstones are
signed with a `created_at` that strictly dominates the retained head's
(read inside the delete transaction), so a future-dated head cannot
survive its own deletion under the relay's `created_at <=` soft-delete
gate.

**Serialized-publisher share command** (`commands/teams/sharing.rs`,
`managed_agents/persona_events.rs`): `set_team_shared` publishes the
retained head through `flush_pending_events_at` rather than submitting
the prepared event directly. A direct submit ran outside
`managed_agents_store_lock` and raced `delete_team`: the delete
atomically purges the head's retained row and enqueues a newer 30178
tombstone in one transaction, and a delayed direct submit could land the
old shared head *after* that tombstone. Because 30178 replacement has no
deletion watermark, the deleted team would go publicly live again.
Routing through the flush is necessary but not sufficient, because the
flush is not itself a single publisher: several call sites (the 30s
sweep, this share toggle, managed-policy updates) invoke it
concurrently, and each invocation has an await gap between its per-row
re-read and its relay POST. A second flush could publish the tombstone
in that gap while an earlier flush's delayed POST lands the just-purged
head after it. The flush now acquires a per-scope publisher mutex —
keyed by the canonical retention `db_path`, which already *is* the
durable scope identity (hashed normalized relay URL + owner pubkey) —
and holds it across its entire invocation: snapshot, per-row re-read,
POST, and `mark_synced`. Serialized-per-scope flush ⟹ within one scope
the only interleavings are head-before-tombstone (the head lands first,
then is dominated by the later tombstone) or purged-row-skip (the delete
committed first, so the re-read skips the head) — a purged head can
never publish after its tombstone. The lock is a
`LazyLock<Mutex<HashMap<PathBuf, Arc<tokio::sync::Mutex<()>>>>>` static
rather than an `AppState` field: it keeps the invariant at its
acquisition site and out of the size-ratcheted `app_state.rs`
(precedent: `agent_models_databricks.rs`'s `AUTH_GATE`), and the
std-mutex map guard is released before the async guard is awaited so it
never spans an await point. Keying per scope — rather than one
process-global lock — means a stalled or hostile relay in one community
can no longer block publication in every other community, and each
per-row submit is additionally wrapped in a
`tokio::time::timeout(PUBLISH_TIMEOUT = 60s)`:
`submit_signed_event_at_with_keys` first waits on the process-wide
admission gate (up to 300s on a 429) and then POSTs on the app-wide
`http_client`, which leaves reqwest's connect/read/total timeouts unset,
so a relay that accepts the connection and never finishes the response
would otherwise pin the lock forever. A timeout takes the same
`Err`/`continue` path as a relay rejection — the row stays pending for
the next 30s sweep and a timed-out tombstone keeps its replacement
deferred this pass — so a live admission gate now surfaces as
timeout-pending rather than a held lock, the correct durable behavior
since the sweep retries. `publicationStatus` (Published/Queued) is
derived by re-reading the retained row's pending flag after the flush.
The previously-unused `relayMessage` field is dropped from
`SetTeamSharedResult`: the flush loop swallows every per-event relay
rejection to its own log and only surfaces local DB faults (which the
status re-read already propagates), so the field was permanently `null`
on this path. #3995's `tauriTeams.ts` mapping and `useTeamActions.ts`
log branch drop with it.

**Domination-aware flush** (`managed_agents/persona_events.rs`): a
strictly-dominating tombstone can be signed past the relay's ingest
acceptance window (`MAX_TIMESTAMP_DRIFT_SECS`, ±900s from server time).
Republishing such a byte-frozen event verbatim from the pending queue
lets it age out of the window and be rejected forever, stranding the
head live. The flush loop is now domination-aware for every retained
kind:5 (covering both 30176 and 30178). For a pending tombstone with
floor `f` (= the retained row's own `created_at`):

- `f <= now` → re-date and re-sign at `now` (mirrors the existing
archive-request re-sign branch; `mark_synced` stays keyed to the
untouched retained row, so a re-date can't mask a concurrent edit),
- `now < f <= now + 900` → publish verbatim at `f`, inside the window,
- `f > now + 900` → skip the sweep; the event stays pending and its
replacement keeps deferring (via `failed_tombstones`) so out-of-order
retraction remains impossible, converging as the wall clock advances.

No path emits an event the gate rejects, and a boundary reject
self-heals through the submit-error requeue. Durable across offline gaps
of any length.

**Atomic adopt** (`commands/teams/adopt/`): `add_team_from_catalog`
re-fetches and signature-verifies the head from the relay, then plans
and commits a multi-entity add across two store writes with byte-exact
rollback on any failure (crash window between writes explicitly
retained). `plan_add` resolves full member provenance (owner, d-tag,
member-key, projection-hash), reuses a recipient's own local built-in
only when the published slug matches and the reuse hint's
`projection_hash` — recomputed from the member's own embedded fields at
the parse boundary and rejected on mismatch — equals
(case-insensitively, matching the boundary's hex tolerance) the
recipient's local built-in hash, and reactivates deactivated copies on
re-add. Because the boundary already proves the hint hash describes the
reviewed projection, a publisher cannot pair a real built-in's slug +
hash with arbitrary reviewed fields to make adoption install the
recipient's built-in in place of what was shown. `commit_stores`
snapshots both stores before writing and restores them on failure. The
commit and the retention enqueue are sequenced inside
`commit_and_enqueue`, the sole route to a durable adoption commit: once
the store write succeeds it enqueues a pending 30175 for every member
copy the add wrote or reactivated and a pending 30176 for the team, so a
crash before the next boot reconcile cannot lose the only adopted copy.
A provenance match on an already-active copy is retained too (not just a
reactivation): a recovery retry after a crash between the persona write
and post-commit retention finds the copy active with no 30175 row, and
`plan_add` short-circuits once the team row exists, so this reuse branch
is the only place that retry can re-enqueue the orphaned member head.
Retaining unconditionally is conservative, not exact — an active copy
still referenced by a standalone managed agent can already hold a live
head, and re-retaining only bumps it monotonically; reused built-ins are
handled separately and never reach this branch. A byte-identical reused
built-in and an idempotent replay write nothing and enqueue nothing; a
failed commit propagates and enqueues nothing. Enqueue is best-effort
per row (the boot reconcile is the backstop). The frontend refreshes via
the `useAddTeamFromCatalogMutation` query invalidation in #3995, so no
`agents-data-changed` emit is needed here.

**Startup reconcile** (`event_sync.rs`):
`reconcile_team_catalog_heads_at` walks all retained 30178 heads at
boot: republishes heads whose content changed, tombstones heads whose
team or member was deleted, skips unshared heads and unchanged content.
Multi-team continuation — all shared teams processed in one pass.

**Cross-device catalog retention** (`commands/personas/inbound.rs`):
both recovery paths above — the boot reconcile worklist and the
interactive `refresh_or_retract_shared_head_at` — key off a retained
30178 row and guard-return without one. A second device therefore never
retained the owner's own catalog head published from another device, so
its later edit or delete could never supersede or retract that
discoverable head. The inbound reconcile now retains an inbound 30178
head as this device's publication witness through
`retain_inbound_catalog_witness`, a self-gating dispatcher invoked
unconditionally on the production non-deletion path: newest-wins via
`retain_inbound_event`, arrival-scoped, no local JSON store, and
deliberately **no** refresh or republish on arrival — a 30178 arrival is
either this device's own echo or the other device's publication, and
rebuilding on either would make two devices ping-pong identical heads.
Retention advances the witness and stops. The tombstone router accepts a
kind:5 covering a 30178 coordinate, so an inbound deletion purges the
retained head on the receiving device (the covered-head purge already
happens inside `commit_inbound_tombstone_with_store`; a 30178 head has
no local record to remove). After a successful inbound persona/team
upsert, this device refreshes the affected shared heads so the community
catalog tracks the inbound edit (persona edit → every team whose
resolved members include it, resolving the local persona `id` by d-tag;
team edit → that team's head); after an inbound tombstone, a team
deletion retracts its 30178 coordinate and a persona deletion refreshes
the teams that listed it. The refresh is idempotent across devices:
`refresh_or_retract_shared_head_at` skips the publish when the rebuilt
projection is byte-identical to the retained head and still shared, so
the editing device's own published head triggers no churn republish on
the receiving device.

**Executable-text concealment gate** (`team_catalog.rs`,
`definition_validation.rs`): `validate_team_catalog_content` — the
single chokepoint both the publish builder
(`build_team_catalog_content`) and the adopt parser
(`team_catalog_content_from_event`) funnel through — now rejects
invisible, default-ignorable, and bidirectional-override characters
(e.g. U+200B, U+2066, U+202E) in every field delivered verbatim to the
ACP harness or rendered as reviewed identity in the catalog UI. This is
the same invariant the persona catalog already enforces at its own parse
boundary (`persona_catalog::parse_agent`); the 30178 boundary was the
outlier. Member `display_name` + `system_prompt` go through
`validate_agent_definition_text` per member (exact parity with
`parse_agent`: display-name rule with no layout controls, prompt rule
allowing `\n`/`\t`); `name_pool` entries take the display-name rule
since they are minted verbatim as instance display names; team
`instructions` take the visible-text rule with layout controls allowed,
since they reach `BUZZ_ACP_TEAM_INSTRUCTIONS` multiline. The team `name`
takes the display-name rule (no layout controls) and the `description`
takes the visible-text rule with layout controls allowed, since both are
rendered verbatim in the catalog UI as reviewed identity.
`validate_visible_text` is exposed `pub(crate)` from
`definition_validation.rs` and re-exported via `managed_agents`. A
signed, shared, current head can no longer smuggle concealed control
characters into executable configuration or reviewed catalog text
through either the publish or the adopt path; emoji (VS16/ZWJ) names and
multiline instructions still pass.

**Types**: `TeamRecord` and `AgentDefinition` extended with `shared`,
`catalog_source`, `team_catalog_source` fields. All commands registered
in `lib.rs`.

## Tests

- `team_catalog/tests.rs`: projection, size contracts, member-key
stability, tombstone rollback, fixture matrix
- `adopt/tests.rs`: head verification, store planning, provenance,
rollback
- `team_catalog/tests/concealment.rs`: the chokepoint rejects
default-ignorable (U+200B) and bidi controls (U+2066, U+202E) in member
`display_name`, `system_prompt`, `name_pool`, team `instructions`, and
the team `name`/`description`, on both the publish and adopt paths; an
emoji-bearing display name and multiline instructions still pass, so no
legitimate team becomes unshareable
- `team_catalog/tests/reuse_hint.rs`: a member pairing a real built-in's
slug with that built-in's genuine `projection_hash` but carrying
unrelated reviewed fields is rejected at the parse boundary, so adoption
can never substitute the recipient's built-in for the reviewed
projection; an honestly-stamped built-in reuse hint (including an
uppercase form of its true hash) still passes the boundary. Removing the
boundary recompute lets the tampered member validate, proving the test
discriminates the substitution class
- `adopt/tests/reuse.rs`: `reusable_builtin` reuses a local built-in for
an exact-match hint (one record, no copy), reuses it just the same when
the genuine hash is uppercased (case-insensitive, matching the boundary
— one record, not two), and falls through to an authoritative embedded
copy when the hash does not match. Comparing the hash case-sensitively
turns the uppercase case red (two records instead of one reused)
- `adopt/tests/concealment.rs`: a signed, shared, current head carrying
a bidi override drives the `add_verified_team` sequence (verify+parse →
`plan_add` → `commit_and_enqueue`) through real temp stores and a real
retention scope, asserting the head is rejected AND the personas store,
teams store, and retention rows are all left byte-unchanged. Stripping
the concealment call at the chokepoint turns it red — the parse then
succeeds and both stores are written, proving the test discriminates a
validate-after-write regression, not just an error return
- `adopt/tests/retention.rs`: adoption drives `commit_and_enqueue`
through a spy commit + a real temp-dir retention scope and asserts
persisted pending rows — commits-then-enqueues (30175 per minted member
+ 30176 team), a failed commit enqueues nothing, an idempotent replay
skips both the commit and the enqueue, a reused built-in retains only
the team, a reactivated copy is re-retained, and a partial-commit crash
recovery (active member copy with no retention row, team row absent)
re-enqueues the orphaned member's 30175. Deleting the enqueue inside the
seam turns these red — the wiring, not just the helper, is protected
- `sharing/tests.rs`: publish/queue lifecycle plus three concurrency
gate tests. (a)
`concurrent_flushes_never_land_the_head_after_its_tombstone` prepares a
share, runs a concurrent delete's purge+tombstone, flushes the tombstone
to a recording relay, then releases the delayed share and asserts the
purged 30178 head is never published after its tombstone and no pending
row survives (removing the lock turns it red — the relay sees the
resurrected head after the tombstone). (b)
`a_stalled_scope_does_not_block_publication_in_another_scope` pins one
scope's flush mid-POST on a stalled relay and runs a second scope's
flush to completion, asserting it publishes without waiting
(re-globalizing the key turns it red). (c)
`a_stalled_relay_releases_the_publisher_lock_within_the_bound` proves a
never-completing POST returns within `PUBLISH_TIMEOUT`, leaves the row
pending, and releases the lock so a subsequent same-scope flush proceeds
(removing the timeout turns it red).
`test_relay_rejection_stays_durably_queued` asserts queued +
still-pending rather than a relay-message string, matching the
flush-routed contract where the rejection text is no longer surfaced
- `pending/tests.rs`: share/unshare/tombstone lifecycle, edit
refresh/retract, tombstone timestamp domination, typed outcomes,
cross-device catalog convergence — device B retains device A's inbound
head then supersedes it on a member edit and tombstones the coordinate
on a delete, a byte-identical rebuild is a no-op (`Noop`, `created_at`
untouched), and an inbound retention alone queues no outbound publish
(the no-ping-pong guard). Neutralizing the inbound retention leg turns
the supersede/tombstone regressions red — B stays blind (`Noop`, no
dominating tombstone) — proving they discriminate the load-bearing leg
- `catalog_reconcile_tests.rs`: a signed kind:30178 head is driven
through the real production entrypoint
`reconcile_inbound_persona_event_blocking` over a `MockRuntime`
`AppHandle` (retention scope resolved from the handle's `app_data_dir`
under an overridden `$HOME`/`$XDG_DATA_HOME`), asserting the arrival
witness is retained at the owner coordinate with `pending_sync=false`,
stored verbatim, and no outbound publish is queued. An early return for
`KIND_TEAM_CATALOG` immediately before the production
`retain_inbound_catalog_witness` invocation turns it red, proving the
seam under test is the production dispatch path and not a test-only shim
- `pending/tests/gate.rs`: flush driven through a stub relay that logs
every `POST /events` with its accept/reject status and enforces the real
±900s ingest gate, for both 30176 and 30178 — within-window
publish+dominate; beyond-window stays-pending with **zero POSTs** (the
gate never receives a rejectable event); and the delayed/offline-retry
case where a tombstone signed strictly past a then-future head has aged
more than 900s into the past, so flush must re-date to `now` to publish.
The reversal check — restore the byte-frozen replay in
`persona_events.rs` and the delayed-retry and zero-POST assertions go
red — is what proves the suite discriminates the fix from the rejected
implementation
- `teams/tests.rs`: 30176 tombstone timestamp domination and no-head
fallback
- `event_sync_team_catalog_tests.rs`: reconcile scenarios including
multi-head continuation
- 26 shared JSON parity fixtures
(`tests/fixtures/team_catalog_content/`) consumed by both this PR's Rust
tests and #3995's TS tests

## Durable ordering, deletion reconcile, and relay-contract alignment

The catalog paths above sit on the shared inbound/deletion retention
seam. This PR makes that seam's ordering structural rather than
conventional, adds a negative-side (deletion) counterpart to the
existing positive-side boot backstop, and aligns inbound resolution with
the relay's actual soft-delete and NIP-33 winner rules.

**Preflight-then-commit (`commit_inbound_with_store`,
`commands/personas/inbound.rs` + `retention.rs`).** A named primitive
runs the fallible store mutation first and advances the durable
retention head only on success; an event that loses preflight returns
`Skipped` without touching the store. The persona/team upsert arms and
the inbound kind:5 removal path all route through it, so no inbound arm
can advance the head ahead of the store write it represents. The
managed-agent arm keeps its own preflight (its runtime transition must
not run for a skipped event) and still advances the head only after
`save_managed_agents`.

**Atomic + monotonic tombstone helpers (30175 / 30176 / 30177).** The
three ordinary tombstone helpers (`commands/personas/pending.rs`,
`commands/agents_pending.rs`, `commands/teams/mod.rs`) each read the
prior head, sign, delete, and retain inside one `BEGIN IMMEDIATE`
transaction, and sign the kind:5 with a `created_at` that strictly
dominates the prior head (`monotonic_created_at(prior_head)`). This
mirrors the 30178 `tombstone_team_catalog_coordinate` precedent. The
three siblings deliberately duplicate the `BEGIN IMMEDIATE` shape rather
than sharing a helper this round — the shared-helper consolidation is
deferred to the persona-tombstone follow-up PR where the flush-replay
class already lives.

**Deletion reconcile (`event_sync.rs`, negative-side counterpart of the
positive-side boot backstop).** The positive side already reconstructs
missing retained heads at boot from surviving disk records. Deletion is
the asymmetric gap: an atomic tombstone helper preserves the head on
failure, but boot reconcile enumerated disk records — deletion already
removed them — so an orphan retained head was never tombstoned. The
deletion reconcile enumerates retained 30175/30176 heads and tombstones
only genuine orphans (a retained head with no matching disk record),
routing through the now-atomic helpers. It reconciles only against a
successfully parsed store: a truncated, malformed, or wrong-shape JSON
store fails loudly (never triggers a tombstone); a missing file is
treated as empty. Managed agents (30177) are excluded by design: their
inbound sync retains a head *without* minting a local disk record
(agents carry device-local secrets that can't come from a relay event),
so a retained 30177 head with no matching record is the normal
cross-device state for every agent created on another device — not a
lost deletion. Sweeping it would tombstone and archive another device's
live agents at boot. Agent deletion-retry therefore stays a pre-existing
gap owned by the direct delete path (tracked in Follow-ups).

**Inbound relay-contract alignment (`retention.rs`,
`commands/personas/inbound.rs`).** Two inbound rules now match the
relay:
- *Equal-second tie-break.* `inbound_event_outcome` previously treated
every equal timestamp as stale; the relay resolves equal `created_at` by
lowest event id. Preflight now matches the relay tuple — strictly newer
timestamp wins, equal timestamp resolves by lower event id, an exact
echo skips — so two devices authoring different same-second successors
converge instead of one silently republishing an event the relay
refuses.
- *Covered-head resolution on inbound kind:5.*
`reconcile_inbound_tombstone` previously consulted only the retained
kind:5 row, so a historical tombstone replayed after a newer recreation
deleted the recreated record and never purged the covered head.
Preflight now resolves both the tombstone row and the covered
`(target_kind, owner, d_tag)` head: a target head strictly newer than
the tombstone preserves JSON and skips; an actually-covered head is
removed from disk first (fallible), then the tombstone row commit and
covered-head purge happen atomically. A JSON-save failure advances
neither, so the identical event stays retryable.

**Atomic agent-archive coupling (`commands/agents_pending.rs`).** The
30177 tombstone and the 9035 archive request now enqueue in one `BEGIN
IMMEDIATE` transaction, with the archive's `persona_id` derived from the
retained head (`persona_id_from_head`) rather than the deleted record,
where it survives the tombstone as owner-signed historical alias data.
The standalone `archive_managed_agent_pending` production callers are
removed — there is no double-enqueue path. Unlike personas/teams, this
coupling is not re-enqueued by the boot deletion reconcile (30177 is
excluded, above): a crash after the disk-authoritative record is removed
but before this transaction commits leaves agent deletion-retry a
pre-existing gap owned by this direct delete path (tracked in
Follow-ups).

## Follow-ups

`publish_prepared_persona` (`commands/personas/sharing.rs`) has the
identical direct-submit-outside-the-lock race for 30175 persona heads
that this PR fixes for 30178 team heads. It predates this work and is
tracked in the separate persona-tombstone follow-up PR, not folded here.

The app-wide `http_client` leaves reqwest's connect/read/total timeouts
unset (`app_state.rs` builder configures only pool options). This PR
bounds the team-publish call site with a `tokio::time::timeout`, but
every other consumer of the shared client remains exposed to a
non-responding endpoint. A client-wide default timeout is the broader
fix; it is pre-existing on main and affects all consumers, so it is out
of scope here.

Managed-agent deletion-retry: the atomic 30177 tombstone + 9035 archive
is best-effort, and unlike personas/teams it is deliberately excluded
from the boot deletion reconcile (a device-local-absent 30177 head is
the normal cross-device state, not a deletion). A crash between the
delete's store write and its retention enqueue therefore has no boot
backstop for agents. The durable fix is a local deletion intent written
before the record is removed; deferred rather than folded into this
round.

Stack: this PR → [#3995](https://github.com/block/buzz/pull/3995)

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-27 17:44:58 -04:00
Will Pfleger
ae0ecd9a67 feat(model-capabilities): humanize Databricks UC model families (#6955)
Chat-eligible Unity Catalog models from the Gemini, DeepSeek, GLM, Grok,
Llama, Qwen, Gemma, and Inkling families were rendering as raw FQNs such
as `system.ai.gemini-3-5-flash` in the agent model picker. #6918
surfaces these models, but the capability manifest carried no records
for most of them and the registry-label path could not strip their Unity
Catalog or `goose-` prefixes.

I added a display-only `label_family_tokens` list and `databricks_v2`
exact records for the sixteen previously unlabeled endpoint stems. All
26 target FQNs and their `goose-` aliases now resolve to curated labels
through `databricks_registry_label`, while capability resolution
continues to use only the existing `family_tokens` (`claude-`, `gpt-`,
and `kimi-`). This keeps label discovery from changing capability
profiles for unrelated model IDs.

Each new record includes reconciliation metadata against the pinned
[models.dev catalog](https://models.dev/api.json). Nine records adopt
first-party effort evidence that the Databricks MLflow Chat transport
can express: Gemini 3.5 Flash, Gemini 3.5 Flash Lite, Gemini 3.6 Flash,
Gemini 3 Pro Image, DeepSeek V4 Flash, DeepSeek V4 Pro, GLM-5.3 Flash,
Grok 4.6, and Inkling. The remaining seven deliberately retain the
`databricks_v2/concrete_unknown` fallback because upstream evidence is
absent, identifies a non-reasoning model, or provides only
toggle/token-budget controls that `reasoning_effort` cannot represent.

The normative corpus adds the sixteen exact records, four Unity Catalog
alias probes, and two capability-isolation probes. Existing base vectors
remain unchanged, and Rust and TypeScript consume the same strict
manifest and corpus.

Related: #6918

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Thufir <7ebdb0b67dab08a570b9faf7bbada97535673b4ccaba2cbd546ad3ba84c87fa6@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Thufir <7ebdb0b67dab08a570b9faf7bbada97535673b4ccaba2cbd546ad3ba84c87fa6@buzz.block.builderlab.xyz>
2026-08-27 17:25:19 -04:00
Kalvin C
be8d7756d5 feat(agent): discover Databricks Unity Catalog models (#6918)
## Summary

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

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


## Validation

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

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

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

---------

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-27 13:37:12 -07:00
tulsi
69096c9a8d preserve channel description paragraph breaks (#6946)
**Category:** fix
**User Impact:** Channel descriptions now keep their paragraph structure
everywhere people and agents read them.

**Problem:** Multi-paragraph channel descriptions were flattened in the
desktop interface and in agent context, making operational instructions
harder to scan and potentially changing their meaning. A running agent
harness could also continue using a stale description after someone
edited it.

**Solution:** Preserve authored line and paragraph breaks across the
desktop surfaces, and render descriptions as bounded, indented
agent-context blocks with semantic delimiters escaped. Prompt-visible
channel metadata now refreshes before each turn with cached fallback,
while one shared desktop derivation keeps the header and channel intro
consistent.

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

**crates/buzz-acp/src/pool.rs**
Refresh prompt-visible kind-39000 metadata with one bounded request per
turn so edits reach running harnesses, while retaining cached metadata
as a degradation fallback. Add regression coverage for description edits
and update request-count fixtures for the refresh lifecycle.

**crates/buzz-acp/src/prompt_framing.rs**
Add semantic text escaping for untrusted values embedded inside paired
prompt sections.

**crates/buzz-acp/src/queue.rs**
Render multi-paragraph descriptions as indented context blocks, preserve
blank lines, retain the 500-character limit, and escape semantic
delimiters. Add helper- and full-prompt tests for paragraph
preservation, UTF-8-safe truncation, and adversarial input.

**desktop/src/features/channels/lib/channelDescription.ts**
Introduce one shared channel-detail derivation and preserve newlines
between status context and authored detail text.

**desktop/src/features/channels/lib/channelDescription.test.mjs**
Cover shared field precedence, multiline descriptions, status-prefix
separation, and fallback behavior.

**desktop/src/features/channels/ui/ChannelPane.helpers.ts**
Use the shared channel-detail derivation for the channel intro instead
of maintaining a second field order.

**desktop/src/features/channels/ui/ChannelPane.helpers.test.mjs**
Verify description text wins consistently over legacy purpose text when
both are present.

**desktop/src/features/channels/ui/ChannelManagementSheetRows.tsx**
Preserve paragraph whitespace in editable and read-only channel
summaries and allow enough lines for compact multi-paragraph
descriptions.

**desktop/src/features/channels/ui/ChannelManagementSheetRows.test.mjs**
Render both channel-summary variants and assert the multiline whitespace
and four-line clamp contract.

**desktop/src/features/messages/ui/ChannelIntroBlock.tsx**
Display description line and paragraph breaks in the empty-channel
intro.

**desktop/src/features/messages/ui/ChannelIntroBlock.test.mjs**
Add render-level coverage for the intro's multiline whitespace contract.

</details>

## Reproduction steps

1. Run the desktop app and create an empty channel.
2. Give it a description with two paragraphs, including a blank line and
a multi-line second paragraph, then save and reopen channel management.
3. Confirm the empty-channel intro preserves the blank line and
subsequent line breaks.
4. Confirm the channel-management summary shows the paragraph structure
for both an editor and a read-only member rather than a detached
ellipsis.
5. Hover the channel title and confirm its tooltip keeps the
description's line breaks.
6. Add an agent, send one prompt, edit the description, then send
another prompt. Confirm the next generated agent context contains the
updated description as an indented multiline block.

## Screenshot

A real-app screenshot of the multiline channel intro and
channel-management summary is attached in the PR comments below.

Closes AIDA-1980.

---------

Signed-off-by: tulsi <tulsi@block.xyz>
2026-08-27 15:37:20 -04:00
Duncan
b2a4d8caac test(agents): pin persona edit ctx rebind at the hook seam
The prior seam test called resolveLatestPersonaToEdit() directly and fed
its result to the coordinator, so reverting the production useMemo or its
returned field left all assertions green while the clobber path was
restored. Render usePersonaActions and assert the RETURNED personaToEdit
advances on a same-id query refresh, then drive the coordinator abort —
failing under both the removed-memo and reverted-return mutations.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-27 14:41:33 -04:00
Duncan
96ccaa847e fix(agents): rebind edit ctx to latest persona so drift guard fires
The concurrent-edit drift guard lives in the coordinator, but the
Agents-library route fed it the open-time snapshot: usePersonaActions
stored the clicked AgentPersona in personaToEdit and AgentsView passed
that stale object straight through as ctx.definition. A same-id
personasQuery refresh never rebound it, so both ctx.updatedAt and the
seed-time ref held the old revision, the guard compared equal, and a
one-field edit still clobbered a concurrent writer's untouched fields
via the full-replacement UpdatePersonaInput.

Re-derive the edited persona from live query data by id so ctx advances
with the store while the seed-time ref stays pinned — which is what
makes the guard trip. Deleted-while-editing falls back to the stored
snapshot (editing still drift-guards against last-known state; close and
delete stay reachable). The agent-origin review route
(AgentManagementDialogs) already derives currentPersona from live
personasQuery.data by useMemo, so it was never exposed.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-27 14:15:40 -04:00
Duncan
167f6db5fb fix(agents): surface refetch failures and guard concurrent definition edits
The verification refetch adapter awaited refetchQueries bare; TanStack Query
v5 swallows refetch errors unless throwOnError is passed, so a failed
verification refetch resolved and getQueryData read stale cache — the
coordinator misclassified a possibly-committed write as an observed
non-persist. Extract the seam into refetchAgentStores with throwOnError:true so
a failed refetch rejects into the coordinator's verification-unknown path.

Add a concurrent-edit guard: capture the definition updatedAt at form seed
time and abort the save before any write when the latest ctx definition was
revised since — the stale full-replacement input would otherwise clobber the
newer writer's values.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-27 13:58:12 -04:00
Duncan
f361a4ebc4 Merge remote-tracking branch 'origin/main' into duncan/phase1-merged-agent-edit
* origin/main: (39 commits)
  chore(deps): update dependency vitest to v4.1.11 (#6667)
  chore(deps): update dependency @tanstack/react-virtual to v3.14.10 (#6666)
  chore(deps): update ubuntu:24.04 docker digest to 33ceb71 (#6664)
  fix(projects): allow owners to delete agent projects (#6533)
  Fade expanded video controls on hover (#6926)
  fix(db): exclude kind:30179 ciphertext from brownfield FTS (#6822)
  fix(client): resurface hidden DMs from live activity (#6885)
  fix(desktop): keep the draft space when typing right after a mention pick (#6875)
  broker: define the agent-to-broker action contract (#6742)
  fix(desktop): keep project sheets independent from threads (#6901)
  Add gated security reviews (#6816)
  fix(desktop): accent-colored mention badges that count thread mentions (#6900)
  Add Buzz benchmark evaluation layers (#6823)
  fix(desktop): show edited head content in thread panel (#6887)
  fix(desktop-tooltip): increase surface contrast (#6897)
  Deduplicate ACP thread prompt context (#6706)
  Apply access policy when reusing channel agents (#6838)
  feat(sidebar): prioritize unread DMs in overflow navigation (#6842)
  feat(projects): add agent and CLI project-home support (#6590)
  feat(desktop): restore message quick reactions (#6892)
  ...

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-27 11:55:06 -04:00
Duncan
7c71ae5f3c fix(agents): contain refetch rejection in save coordinator
A verification refetch that rejects after a write may have committed
must not escape the save path. runAgentSaveCoordinator awaited
refetchStores() bare at every settlement boundary, and handleSubmit
wrapped the coordinator in try/finally with no catch — so a rejection
left saveError unset, no toast, and the dialog silently stuck on
"Saving...", inviting a blind retry over a possibly-durable write.

Catch every refetch rejection and settle with an explicit
verification-unknown outcome: dialog stays open, toast says persistence
could not be verified (never that the write failed, since it may have
landed). handleSubmit gains a catch as a belt-and-braces backstop.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-27 11:52:52 -04:00
Wes
9176e34405 fix(projects): allow owners to delete agent projects (#6533)
## Summary

- expose project deletion when the current user directly owns the
project or owns/manages its author agent
- publish a viewer-signed, project-only tombstone after fetching the
exact live head, then detect concurrent replacement
- honor relay-authorized owner-signed tombstones in desktop project read
models

## Testing

- pre-push hook: desktop Biome, TypeScript, 5,358 desktop tests, Rust
tests, Tauri checks, and file-size ratchet passed at `a36f7a4cf`
- `cargo check -p buzz-test-client --tests`
- relay E2E coverage added as ignored; not run against a live relay

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Signed-off-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
2026-08-27 09:33:04 -06:00
klopez4212
27c1a7c91b Fade expanded video controls on hover (#6926)
## Summary
- fade expanded video controls out when the pointer leaves the video
- restore controls on video hover or keyboard focus while keeping touch
controls visible
- respect reduced-motion preferences

## Testing
- focused expanded-video hover Playwright test
- full video attachment E2E suite (12 tests)
- desktop check and typecheck
- desktop unit tests (5,674)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-27 09:27:54 -06:00
klopez4212
4d77175c78 fix(client): resurface hidden DMs from live activity (#6885)
## Summary

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

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

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

Addresses #6316.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
2026-08-27 10:29:26 -04:00
Matt Toohey
c856be0fb9 fix(desktop): keep the draft space when typing right after a mention pick (#6875)
## Problem

Desktop Smoke E2E flaked on `mentions.spec.ts` ("typing a mention before
existing text does not interleave spaces"): picking a suggestion
mid-draft and typing immediately could swallow the draft's own space,
producing `hello @bob abcworld` instead of `hello @bob a world`.

With `hello world` drafted and ` @bo` typed after `hello`, picking the
suggestion leaves `hello @bob world` — the mention's trailing space
followed by the draft's space. Chromium reconciles that whitespace run
by re-emitting it as a replacement (`replace(" " -> " a")`), anchored at
*either* edge of the run. The settling-time text-input redirect only
recognized a caret or a rewrite anchored at the chip edge, so the other
shapes fell through to ProseMirror's destructive default and ate the
separator.

The race pre-dates this branch and reproduces on `main`.

## Fix

`mentionHighlightExtension.ts` now redirects on the invariant instead of
on a specific rewrite shape. While autocomplete is settling the user
cannot have a selection (a selection cancels settlement), so a
whitespace-only replacement adjacent to a mention's trailing space is
always the browser normalizing whitespace — never an intentional delete.
The handler keeps every space already in the document and inserts only
the typed characters after the token, whichever edge the rewrite is
anchored at. Non-whitespace replacements still fall through as real
edits.

`insertPosForMentionTextInput` / `mentionTextInputInsertPos` become
`insertionForMentionTextInput` / `mentionTextInputInsertion`, returning
`{insertAt, text}` so the handler can rewrite the payload, not just the
position.

## Tests

- Helper-level cases for all four anchors (caret at chip edge, caret
past the trailing space, run rewritten from the chip edge, draft space
rewritten alone), plus space/NBSP variants and a non-whitespace
replacement that must be left alone.
- A plugin-level test drives the real `handleTextInput` prop and falls
back to ProseMirror's default when it declines — exactly as the browser
does with the return value — so it fails against the previous production
code rather than depending on winning a timing race in a headless
browser.

Desktop unit suite passes on pre-push (5558/5558).

---------

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 17:37:42 +10:00
thomaspblock
cada3028ce fix(desktop): keep project sheets independent from threads (#6901)
## Summary

Project workspace sheets now slide in from the right as an independent
layer over an open conversation thread instead of replacing the thread
or rendering inside its panel. Closing the sheet restores the unchanged
thread, Escape applies only to the topmost surface, and the covered
thread's resize divider no longer leaks through the overlay.

### Related issue

Related: #6602. No matching issue found.

### Testing

- `pnpm --dir desktop exec biome check
src/features/channels/ui/RightAuxiliaryPane.tsx
tests/e2e/project-commit-detail.spec.ts`
- `pnpm exec playwright test tests/e2e/project-commit-detail.spec.ts
--project=smoke --grep "project workspace sheet stays independent from
an open thread"`
- Pre-push desktop formatting, file-size, typecheck, and unit-test
checks

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-26 20:59:47 -04:00
tulsi
ceb8ba605f fix(desktop): accent-colored mention badges that count thread mentions (#6900)
**Category:** fix
**User Impact:** Sidebar mention numerals now match the user's
theme/accent color and appear for mentions inside threads, not just
top-level channel messages. Hover-to-preview on channel rows is
unchanged.

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

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

**Solution:**

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

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

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

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

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

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

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

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

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

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

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

</details>

## Reproduction steps

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

## Verification

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

## Screenshots

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

Signed-off-by: tulsi <tulsi@block.xyz>
2026-08-26 17:44:05 -04:00
Salman Mohammed
eb8e97cfe7 fix(desktop): show edited head content in thread panel (#6887)
## Why

Fixes #6886. In the channel timeline an edited message shows its newest
body, but opening its thread panel showed the stale pre-edit body until
the async thread-aux backfill landed (and permanently if it raced or
missed).

## Root cause

`desktop/src/features/messages/lib/independentThreadPanel.ts` built
`events = [head, ...replyEvents]`, discarding the head's aux overlay
events (edits `kind:40003`, deletions, reactions) already present in the
channel window. `formatTimelineMessages` only applies an edit when the
edit event sits in the same array as its target, so the thread head
relied solely on `withThreadAux` to re-supply it.

## What

- Carry the head's non-content aux events (via
`!isTimelineContentEvent`) from the channel window into the thread
head's event array, deduped against `replyEvents` by id.
- Thread head is now byte-identical to the main timeline the instant the
thread opens, independent of the async aux fetch.
- Restricted to non-content kinds so reply content rows never leak in.

## Tests

- Unit regression `independentThreadPanel.test.mjs` (6 cases): red
pre-fix (`"two PRs"`), green post-fix (`"these PRs"`).
- e2e `thread-head-stale-edit.spec.ts`: delays thread-replies aux to
force the channel-window-vs-thread-aux divergence, then opens the thread
via the reply button and asserts the panel head shows the edited body.
Verified red pre-fix (stale `message-thread-panel` head), green
post-fix.
- Full desktop unit suite: **5562 pass / 0 fail**. `biome` + `tsc
--noEmit` clean.

Origin: Buzz channel `fix-reply-thread-old-view-bug`
(#464b982f-fc05-42c8-918f-409e1f05a1df).

---------

Signed-off-by: Leo <5faf251baee50ee6bcde338aef6acdd70bb3e60115664c2cd490d94a55dfc488@buzz.block.builderlab.xyz>
Co-authored-by: Leo <5faf251baee50ee6bcde338aef6acdd70bb3e60115664c2cd490d94a55dfc488@buzz.block.builderlab.xyz>
2026-08-26 20:57:19 +00:00
Taylor Ho
03bfc837cb fix(desktop-tooltip): increase surface contrast (#6897)
**Category:** fix
**User Impact:** Tooltips now use a higher-contrast surface, making
their labels easier to read across desktop themes.

**Problem:** Tooltip surfaces could blend into hovered content, making
labels difficult to distinguish.

**Solution:** Use the existing popover surface and foreground tokens for
clearer visual separation and theme-aware contrast.

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

**desktop/src/shared/ui/tooltip.tsx**
Updates shared tooltip colors to use higher-contrast semantic surface
and foreground tokens.

</details>

## Reproduction steps

1. Run the desktop app and hover a control that displays a tooltip.
2. Compare the tooltip against the content beneath it in both light and
dark themes.
3. Confirm the tooltip surface remains clearly distinguishable and its
label is readable.

## Screenshots

| Before | After |
| --- | --- |
| ![Tooltip using the lower-contrast secondary
surface](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6897/tooltip-before.png)
| ![Tooltip using the higher-contrast popover
surface](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6897/tooltip-after.png)
|

## Validation

- `pnpm build:e2e`
- `pnpm exec playwright test tests/e2e/tooltip-semantics.spec.ts
--project=smoke` — 4 passed
- `pnpm exec biome check src/shared/ui/tooltip.tsx
tests/e2e/tooltip-semantics.spec.ts`
- Pre-push desktop checks, typecheck, and unit tests
- `git diff --check`
- Full smoke project attempted; 345 of 1,222 tests passed without
failure before the 10-minute local execution limit.

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-26 20:56:21 +00:00
Jordan Mecom
01c87a350b Apply access policy when reusing channel agents (#6838)
Reused managed identities now apply their intended inbound-author policy
before attachment or startup. An explicit policy wins, then the linked
persona policy, then `owner-only`.

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

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

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

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

---------

Signed-off-by: Jordan Mecom <jm@squareup.com>
2026-08-26 13:22:09 -07:00
Taylor Ho
4f554b6385 feat(sidebar): prioritize unread DMs in overflow navigation (#6842)
**Category:** improvement
**User Impact:** Users can see which unread direct messages are waiting
offscreen and jump to the most relevant one first.

**Problem:** The sidebar overflow control treated every offscreen unread
conversation alike, so an unread DM could be hidden behind channel
activity with no visible clue about who was waiting.

**Solution:** Prioritize the first offscreen unread DM, preview up to
three senders with avatars, and keep the arrow, avatars, separator, and
message count intact while only the trailing phrase truncates in narrow
sidebars.

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

**desktop/src/features/sidebar/lib/useUnreadOverflow.ts**
Adds ordered DM preview data to unread-overflow calculations while
preserving the sidebar's existing visibility and direction rules.

**desktop/src/features/sidebar/lib/useUnreadOverflow.test.mjs**
Covers DM prioritization, ordering, offscreen filtering, and
channel-only fallback behavior.

**desktop/src/features/sidebar/ui/AppSidebar.tsx**
Passes unread DM previews into the lower overflow control and navigates
to the preferred DM before the next unread channel.

**desktop/src/features/sidebar/ui/MoreUnreadButton.tsx**
Renders the frontmost-first avatar stack, accessible DM target label,
middot separator, and message-count treatment.

**desktop/src/features/sidebar/ui/MoreUnreadButton.test.mjs**
Locks the three-avatar cap, stacking order, preferred target, fallback,
accessible copy, and truncation structure.

**desktop/src/shared/ui/UnreadPill.tsx**
Contains the primary pill and allows only its trailing label to
ellipsize under width pressure.

**desktop/src/shared/ui/UserAvatar.tsx**
Allows the sidebar preview stack to identify each rendered avatar in
regression coverage.

</details>

## Reproduction steps

1. Open enough sidebar conversations that unread items fall below the
visible area.
2. Leave one or more offscreen DMs unread, along with unread channels.
3. Confirm the bottom overflow pill shows up to three overlapping DM
avatars followed by a middot and the total new-message count.
4. Select the pill and confirm it navigates to the first offscreen
unread DM rather than the nearest unread channel.
5. Resize the sidebar from 420px through 300px to 220px and confirm the
arrow, avatars, middot, and full numeric count remain visible; only `new
messages` may ellipsize.
6. Repeat with channel-only unread overflow and confirm the control
keeps its channel fallback behavior without avatars.

## Screenshots

Built-UI matrix with 100px of visible space beyond the sidebar edge. All
images are hosted through `pr-screenshots`.

| State | 220px min | 300px default | 420px max | Result |
| --- | --- | --- | --- | --- |
| Channel-only · 13 | ![Channel-only · 13 at
220px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/01.png)
| ![Channel-only · 13 at
300px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/02.png)
| ![Channel-only · 13 at
420px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/03.png)
| Full label throughout. |
| Channel-only · 999 | ![Channel-only · 999 at
220px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/04.png)
| ![Channel-only · 999 at
300px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/05.png)
| ![Channel-only · 999 at
420px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/06.png)
| Full label throughout. |
| One DM only · 1 | ![One DM only · 1 at
220px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/07.png)
| ![One DM only · 1 at
300px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/08.png)
| ![One DM only · 1 at
420px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/09.png)
| Full label throughout. |
| One DM + channels · 99 | ![One DM + channels · 99 at
220px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/10.png)
| ![One DM + channels · 99 at
300px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/11.png)
| ![One DM + channels · 99 at
420px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/12.png)
| Full label throughout. |
| Two DMs only · 2 | ![Two DMs only · 2 at
220px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/13.png)
| ![Two DMs only · 2 at
300px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/14.png)
| ![Two DMs only · 2 at
420px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/15.png)
| Full label throughout. |
| Two DMs + channels · 47 | ![Two DMs + channels · 47 at
220px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/16.png)
| ![Two DMs + channels · 47 at
300px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/17.png)
| ![Two DMs + channels · 47 at
420px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/18.png)
| Full label throughout. |
| Three DMs + channels · 128 | ![Three DMs + channels · 128 at
220px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/19.png)
| ![Three DMs + channels · 128 at
300px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/20.png)
| ![Three DMs + channels · 128 at
420px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/21.png)
| 220px: `128 new messa…`; full at 300/420. |
| Three DMs + channels · 999 | ![Three DMs + channels · 999 at
220px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/22.png)
| ![Three DMs + channels · 999 at
300px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/23.png)
| ![Three DMs + channels · 999 at
420px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/24.png)
| 220px: `999 new mess…`; full at 300/420. |
| Three DMs + channels · 9,999 | ![Three DMs + channels · 9,999 at
220px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/25.png)
| ![Three DMs + channels · 9,999 at
300px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/26.png)
| ![Three DMs + channels · 9,999 at
420px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6842/27.png)
| 220px: `9,999 new mes…`; full at 300/420. |

**Matrix result:** 27/27 states remain contained. Exactly 3/27 states
ellipsize—all are three-avatar cases at 220px. The arrow, all avatars,
middot, and complete numeric value remain visible; only the trailing
`new messages` phrase yields.

## Validation

- Desktop unit suite: 5,517 passed
- TypeScript: passed
- Biome on all seven changed files: passed
- Pre-push desktop checks, typecheck, tests, file-size, and branch-skew
gates: passed
- 27-state built-UI matrix capture: 27/27 contained; only the three
220px three-avatar states truncate, and only the trailing phrase
- `git diff --check`: passed

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@users.noreply.github.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
2026-08-26 12:59:14 -07:00
thomaspblock
de188ebf06 feat(projects): add agent and CLI project-home support (#6590)
## Summary
- inject bounded project-home identity and repository context into
managed agent sessions
- add project-aware CLI flows for creating projects, repositories,
issues, and related channels
- match project homes through the existing relay query surface, then
filter channel metadata client-side

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

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

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

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
2026-08-26 12:09:06 -07:00
morgmart
22cdda441d feat(desktop): restore message quick reactions (#6892)
## Summary

- restore three learned quick-reaction shortcuts to the desktop message
action rail
- keep Add reaction (`SmilePlus`, `h-4 w-4`) immediately after shortcuts
and left of the restored divider
- preserve Reply, Copy link, More, custom emoji rendering, accessibility
labels, and responsive behavior
- hide shortcuts and the divider below `sm`, while keeping Add reaction
available

## Tests

At `24c4732c2341b846dccf4ae8fdc2455f700fc44d`:

- `pnpm exec biome check src/features/messages/ui/MessageActionBar.tsx
tests/e2e/message-copy-link.spec.ts tests/e2e/custom-emoji.spec.ts`
- `pnpm exec tsc --noEmit`
- `pnpm test` — 5,567 passed
- `pnpm build:e2e`
- `pnpm exec playwright test tests/e2e/message-copy-link.spec.ts
tests/e2e/custom-emoji.spec.ts --project=smoke` — 19 passed
- `pnpm exec playwright test tests/e2e/reaction-order.spec.ts
--project=smoke` — 2 passed
- pre-push: file-size-check, desktop-check, desktop-typecheck,
desktop-test

Vogue design review: **SHIP**.

## Screenshot

Restored desktop message action rail: three learned quick reactions, Add
reaction (`SmilePlus`), divider, Reply, Copy link, and More.

![Restored desktop message action
rail](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6892/restored-message-action-rail.png)

Signed-off-by: Rivet <a08d9a8418c7ff03afe19964724c8fd87bf1776ab9e9b9cafb8cc920edd02a6e@buzz.block.builderlab.xyz>
Co-authored-by: Rivet <a08d9a8418c7ff03afe19964724c8fd87bf1776ab9e9b9cafb8cc920edd02a6e@buzz.block.builderlab.xyz>
2026-08-26 10:32:46 -07:00
Salman Mohammed
f177f4909d Use paired tags for standing & per-turn context (#6701)
## Why
PR #6503 bundled a broader context compiler with several prompt-shape
and telemetry changes, making it difficult to isolate whether paired
XML-like top-level boundaries improve agent behavior. This keeps the
follow-up experiment focused on framing only.

## What
- Replace top-level standing and per-turn bracket headers with paired
tags while preserving inner section content and event/message framing
- Carry only metadata already present in dynamic headers as attributes,
such as thread counts, truncation, event type, and batch count
- Preserve every section body verbatim so the model executes the exact
bytes reviewed in Desktop
- Keep routing metadata, event content, batching, and delivery behavior
unchanged
- Teach Desktop diagnostics to parse the new framing while retaining
compatibility with archived bracket-framed sessions; ambiguous paired
boundaries render as the complete captured prompt

## Risk Assessment
Medium — the change is limited to agent prompt framing and diagnostic
parsing, but it affects both standing context and every dynamic agent
turn. Bodies and delivery behavior remain unchanged, and compatibility
parsing remains for prior sessions.

## References
- Follow-up to #6503, scoped only to the paired-tag framing hypothesis

## Update — 2026-08-24
- Addressed review feedback by removing entity encoding/decoding from
the execution and diagnostic paths.
- Added regressions for literal closing tags, placeholders,
entity-looking text, ampersands, and authored boundary whitespace.
- Verified `cargo test -p buzz-acp` (805 unit + 9 integration), Desktop
tests (5,412 before the main sync; 5,439 in the pre-push gate), Desktop
typechecking, and Tauri clippy/tests.
- Live local relay startup and message ingestion passed. The live ACP
reply step is blocked locally because installed Goose 1.0.23 no longer
provides the documented `goose acp` command and no alternate ACP adapter
is installed.

---

**Update Aug 25, 12:13 EDT:** Extended paired tags to per-turn top-level
sections.

- Converted context, conversation/thread context, event batches, and
steer/interrupt boundaries without changing their inner bodies.
- Preserved existing header metadata as attributes only; added no new
prompt metadata.
- Verified 806 Buzz ACP unit tests plus 9 integration tests, 5,441
Desktop tests, Desktop typechecking and formatting, and all differential
pre-push lanes.

---

**Update Aug 25, 13:58 EDT:** Addressed framing review feedback.

- Updated operational base instructions to reference the emitted
`<context>` section and added a regression guard against stale
`[Context]` instructions.
- Restored batched steer/interrupt cardinality in Desktop diagnostic
titles with focused coverage for both paths.
- Verified 807 Buzz ACP unit tests plus 9 integration tests, 5,442
Desktop tests, Desktop typechecking and formatting, and every
differential pre-push lane including Tauri clippy/tests.

---

**Update Aug 25, 15:37 EDT:** Hardened legacy transcript compatibility.

- Semantic standing-context parsing now requires paired framing at the
start and complete input consumption.
- Historical bracket-framed personas keep literal balanced tag examples
in their original section; malformed or mixed current framing displays
as the complete captured prompt.
- Added regressions for the reported legacy persona case and trailing
unframed text; verified 60 focused parser tests, 5,444 full Desktop
tests, formatting, and typechecking.

---

**Update Aug 25, 16:31 EDT:** Preserved slash-command prompt block
boundaries.

- Desktop now sets aside a valid connector-facing slash-command block
only when the following block begins recognized paired framing, parses
the semantic blocks normally, and retains the command as visible Prompt
metadata.
- Added an end-to-end transcript regression using the production
three-block ACP shape, including the user bubble, author pubkey, event
ID, and grouped prompt context.
- Verified 113 combined transcript tests, 5,445 full Desktop tests,
formatting, typechecking, and all applicable pre-push lanes.

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-08-26 12:05:41 -04:00
Wes
583af02299 fix(cli): preserve signatures in event reads (#6884)
## Summary
- preserve string Nostr `sig` values in normal `messages get`, `messages
thread`, `messages search`, and `feed get` output
- omit absent or non-string signatures instead of fabricating empty
values
- keep compact output at exactly `{id, content, created_at}` and update
CLI/managed-agent contract docs

## Validation
- `cargo test -p buzz-cli --lib` (367 passed at final head
`a2498faa0e07f90efc0ab0165606ba3b048327c7`)
- release candidate live-relay matrix: 49/49 normal events independently
recomputed to their NIP-01 IDs and passed BIP-340 verification; all four
compact commands preserved exact three-key output and ordering
- `git diff origin/main...HEAD --check`
- independent adversarial review: clear, no findings

## Compatibility
The normal JSON change is additive for valid relay events. In-repo
consumers use tolerant field lookup. Compact output is unchanged and
locked by regressions for both message and feed formatting.

Generated by Carl, an automated contributor working with Wes.

---------

Signed-off-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
2026-08-26 09:13:11 -06:00
Matt Toohey
db514b153a fix(desktop): stop pulsing addressed agents on send (#6873)
## Summary

- Stop replaying the selected-agent pulse when a remembered agent
remains addressed across a send.
- Preserve the existing pulse for genuine selections and shake feedback
for failed sends.

### Related issue

Reported in Buzz:
buzz://message?channel=2745947d-f905-4f31-8a3f-fc4485d3c36c&id=0cffab376186be9cc4e7f29027352cc1c7941aead48b3fed948f36181f4a59af

### Testing

- `cd desktop && pnpm test` (5,556 passed)
- `cd desktop && pnpm typecheck`
- `cd desktop && pnpm check`
- `cd desktop && pnpm build:e2e`
- `cd desktop && pnpm exec playwright test
tests/e2e/persistent-agent-audience.spec.ts --grep 'always-mentioned
agents remain selected|failed always-mentioned send'` (2 passed)

This changes animation timing only; the regression is covered by
Playwright assertions on the pulse and shake versions, while a static
screenshot is unchanged.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-08-26 21:46:19 +10:00
Matt Toohey
4f529a45f2 fix(desktop): prioritize sidebar channel status (#6861)
## Summary

- Treat the sidebar temp-channel icon as a fallback behind working,
mute, numeric unread, and thread-unread indicators.
- Keep the header affordance and the sidebar temp icon when it is the
only row status.
- Extend the ephemeral-channel browser scenario across working, idle,
collapsed-sidebar, and unread states.

## Before / After

| Before: five temp icons compete with duration and unread states |
After: all five duration and unread states remain clear |
| --- | --- |
| ![Before: five long temporary channel rows each show a temp icon, long
duration, and unread
state](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6861/temp-icon-before-five.png)
| ![After: the same five long temporary channel rows retain long
durations and unread states without temp
icons](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6861/temp-icon-after-five.png)
|

## Testing

- `node --import ./test-loader.mjs --experimental-strip-types --test
"src/**/*.test.mjs"` (5,556 passed)
- `tsc --noEmit -p desktop/tsconfig.json`
- `biome check desktop/src/features/sidebar/ui/SidebarSection.tsx
desktop/tests/e2e/channels.spec.ts`
- `vite build --mode e2e`
- Isolated Playwright run of `create ephemeral stream shows sidebar and
header affordances` (1 passed)

## Duplicate check

No matching open PR or issue found for `ephemeral sidebar icon`.

Signed-off-by: Jitter <d14dfe033ef0f809866f9f984de04821b0d900d7652fd85a54776ee40ca3a68f@buzz.block.builderlab.xyz>
Co-authored-by: Jitter <d14dfe033ef0f809866f9f984de04821b0d900d7652fd85a54776ee40ca3a68f@buzz.block.builderlab.xyz>
2026-08-26 20:19:02 +10:00
Matt Toohey
e8172b5bac feat(desktop): hyperlink selected composer text on link paste (#6684)
## Summary

Pasting a link while composer text is selected now turns that text into
a hyperlink instead of replacing it — the behavior people expect from
every other editor.

- `resolveExactLinkPaste` recognizes a clipboard payload that is exactly
one supported link (bare or `<…>`-wrapped `http(s)`, or a Buzz deep
link, canonicalized through the existing composer-link attribute
resolver) and returns the href to apply.
- The paste handler applies that href as a `link` mark over the current
selection, collapses the caret to the end of the newly linked range,
clears stored marks so subsequent typing isn't linked, and refocuses the
editor.

Edge cases handled:

- **No link mark available / selection can't carry marks** — falls
through to the existing replace-with-node path rather than silently
dropping the paste.
- **Selection contains a composer message-link chip** — keeps the old
replacement behavior instead of nesting a link over a chip.
- **Whitespace in the clipboard payload** — `unwrapExactHttpLink` now
rejects any text containing whitespace, so multi-line or "text plus URL"
pastes are not treated as an exact link.
- **Empty selection (caret paste)** — unchanged.
- **Idempotent re-paste** of an already-wrapped link is consumed rather
than doubling up.

## Testing

`desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs`
gains coverage for exact-link resolution (per-shape accept/reject cases,
Buzz canonicalization) and for the paste handler: linking selected text,
canonicalizing Buzz links over a selection, idempotent wrapped-link
paste, fall-through when the selection can't carry marks, all-selection
collapse, chip-containing selections, and preserved caret-paste
behavior.

Full pre-push gate ran green on this branch: `file-size-check`,
`desktop-check`, `desktop-typecheck`, and `desktop-test` (5425 tests, 0
failures).


## Demo

Pasting an `https://` URL over selected composer text turns the selected
words into a hyperlink (instead of replacing them); text typed
immediately after is not linked:


https://github.com/user-attachments/assets/bbc93574-dd9d-4b1b-bad0-53307b6e887c

---------

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-08-26 15:19:44 +10:00
Will Pfleger
52621c09be chore(release): release Buzz Desktop version 0.5.20 (#6839)
## Buzz Desktop release v0.5.20

- **Frozen main:** `8471049c430073474939336dfc6aa98272bc8762`
- **Reviewed candidate:** `95154bee4034ca7a40b33095c2ddbde8c9aa1614`
- **Previous desktop release:** `desktop-v0.5.19`
- **Proposed immutable tag:** `desktop-v0.5.20`

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: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
2026-08-25 23:46:04 +00:00
klopez4212
8471049c43 feat(desktop): add KLIPY GIF search to composers (#5554)
## Summary

- Supersedes #1913 with a KLIPY-hosted URL implementation.
- Adds KLIPY GIF search and trending results to desktop message and
forum composers.
- Keeps selected GIFs hosted by KLIPY; Buzz stores only the external URL
and media metadata (no imeta tag, since relays only accept hash-backed
local `/media/` entries).
- Aligns the Emoji/GIF picker with Buzz's standard segmented control,
theme surfaces, and motion behavior.

## Relay-to-provider boundary

- The relay proxies KLIPY search/share so the `BUZZ_KLIPY_API_KEY` never
reaches the desktop; the key stays server-side behind a redacted `Debug`
impl.
- The dedicated GIF `reqwest` client sets `redirect::Policy::none()`.
Because the API key rides in the request path, following a provider
`3xx` could replay a key-bearing URL to an attacker-chosen host (an
SSRF/key-disclosure primitive). With redirects disabled, a `3xx` returns
as a non-success status that the handlers map to a generic `502`; the
`Location` target is never read or forwarded.
- Admission reuses the established NIP-98, membership, replay, and
per-pubkey rate-limit gates, with an upstream response-size cap and
allowlisting so KLIPY error bodies never cross the relay boundary.

## Accessibility

- Under `prefers-reduced-motion: reduce`, the picker grid renders a
static provider poster (a normalized `jpg` asset) instead of the
animated preview, or a named static placeholder when no poster is
available. It reacts to preference changes while mounted.
`no-preference` keeps the animated preview.
- Selected GIFs carry their title through `ImetaMedia`'s `displayLabel`,
so the composer thumbnail, preview dialog, editor, lightbox, and remove
control all derive one non-empty accessible name instead of an empty
`Attachment ` label. Ordinary hashed uploads keep their existing
hash-derived names.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
2026-08-25 19:09:55 -04:00
Taylor Ho
e8cd7516e6 fix(desktop): respect automatic mention preference after send (#6837)
**Category:** fix
**User Impact:** Disabling automatic agent mentions now keeps one-time
agent mentions out of the next message draft.

**Problem:** A successfully sent inline agent mention was treated as
eligible for post-send restoration even when **Automatically mention
agents** was disabled, so the agent immediately reappeared in the
composer. **Solution:** Gate the send-success restoration path on the
live preference while leaving explicitly pinned agents and enabled
automatic mentions unchanged.

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

**desktop/src/features/messages/ui/MessageComposer.tsx**
Skips the automatic post-send mention restoration path when the
preference is disabled.

**desktop/tests/e2e/persistent-agent-audience.spec.ts**
Reproduces disabling the toggle before sending an inline agent mention
and verifies the next composer is empty while the outgoing mention still
reaches the agent.

</details>

## Reproduction steps

1. Enable **Automatically mention agents** in the composer mention
options.
2. Disable it again.
3. Compose and send `@Agent test` using the inline mention picker.
4. Confirm the message still mentions the agent, but the cleared
composer does not repopulate `@Agent`.
5. Re-enable the preference and repeat; confirm the mention is restored
for the next message.

## Testing

At `a179a2d00a6ac5cf0bc4a1c7c1ef3d5dfa9d0875`:

- Full desktop unit suite: 5,501 passed.
- Desktop check and typecheck passed in the pre-push hooks.
- Desktop file-size ratchet passed in the pre-push hooks.
- Full `persistent-agent-audience.spec.ts` Playwright file: 17 passed,
including preference off (empty composer), preference on (restored
mention), and the existing address-undo journey.

## Visual validation

| Automatic mentions off | Automatic mentions on |
| --- | --- |
| After sending a one-time mention, the outgoing message is preserved
and the next composer stays empty. | After sending, the agent remains
addressed and is restored in the next composer. |
| ![Automatic mentions disabled: empty composer after
send](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6837/disabled-after-send.png)
| ![Automatic mentions enabled: agent restored after
send](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6837/enabled-after-send.png)
|

<details>
<summary>Original regression</summary>

With automatic mentions disabled, sending `@Alia test` still repopulated
`@Alia` in the cleared composer.

![Original regression: the sent agent reappears in the
composer](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6837/auto-mention-regression-before.png)

</details>

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
2026-08-25 15:59:38 -07:00
Will Pfleger
7a1b7d8e09 chore(release): release Buzz Desktop version 0.5.19 (#6828)
## Buzz Desktop release v0.5.19

- **Frozen main:** `ee6ca5fa28bce04dfecb6717de65b08a57f2ac47`
- **Reviewed candidate:** `ab691bcdaeccaa6698a2199beaba3f6e93daae81`
- **Previous desktop release:** `desktop-v0.5.18`
- **Proposed immutable tag:** `desktop-v0.5.19`

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: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
2026-08-25 17:36:57 -04:00
Wes
22f32c99e2 docs(nest): make commit attribution policy-neutral (#6707)
## Summary

- replace the generated Nest's unconditional human author/sign-off rules
with portable guidance that separates authorship, material
co-authorship, DCO certification, and cryptographic signing
- defer attribution to repository-local policy, forbid inferred or
guessed identities, and require inspection of every outgoing commit
- bump the Nest template version so existing installations refresh, with
regression coverage for fresh generation and upgrade preservation

### Related issue

None found. Related runtime identity work exists in #6177, but this PR
is intentionally limited to the generated Nest guidance and its refresh
behavior.

### Testing

- `bin/just desktop-tauri-clippy`
- `bin/just desktop-tauri-test`
- `bin/just file-size-check`
- `git diff --check`
- pre-push hooks: `push-head-scope`, `branch-skew`, `file-size-check`,
and `desktop-tauri-checks`

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-08-25 14:35:53 -06:00
Taylor Ho
b58de7cfa7 fix(desktop-messages): preserve inline agent mentions with persistent addressing (#6793)
**Category:** fix
**User Impact:** Users can place `@Agent` mentions inline anywhere in a
message—even when that agent is also persistently addressed—so separate
agents can receive separate instructions in the same message.

**Problem:** Selecting an agent from the composer mention picker could
turn the selection into persistent addressing instead of leaving an
inline `@Agent` at the cursor. That made persistent addressing and
inline composition mutually exclusive: once the persistent behavior took
over, users lost the clear `@Agent A do X, @Agent B do Y` message
structure they previously had.

**Solution:** Make inline mentioning and persistent addressing
independent behaviors:

- Ordinary mention selections always insert `@Agent` inline at the
current cursor position.
- This remains true when the agent is already persistently addressed;
the persistent audience never blocks or replaces an inline mention.
- If **Automatically mention agents** is enabled, a successfully sent
inline mention may additionally make that agent persistent for later
messages. Existing saved preferences remain respected.
- The dedicated automatic-mention control and primary+Shift+Enter
shortcut continue to add or remove persistent addressing directly.
- For users without a saved preference, **Automatically mention agents**
now defaults off.

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


**desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.test.mjs**
Updates preference coverage for the default-off behavior while
preserving explicit saved choices.


**desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.ts**
Defaults automatic post-send persistence off when no valid preference
exists.

**desktop/src/features/messages/ui/useAgentAddressLockPicker.test.mjs**
Verifies ordinary picker selections insert inline mentions without
changing or pulsing the persistent audience, including when the selected
agent is already persistently addressed.

**desktop/src/features/messages/ui/useAgentAddressLockPicker.ts**
Keeps ordinary mention selection on the existing inline insertion path
while preserving the separate persistent-address controls and shortcut.

**desktop/tests/e2e/persistent-agent-audience.spec.ts**
Covers separately targeted inline instructions to two agents, signed
outgoing recipients, default-off persistence, and explicit opt-in
persistence scenarios.

</details>

## Reproduction steps

### Inline mentions without persistence

1. Open a channel with at least two available agents and leave
**Automatically mention agents** disabled.
2. Use the composer mention picker to insert the first agent, type an
instruction, then insert a second agent and type a different
instruction.
3. Confirm the draft reads like `@Agent A review this, @Agent B test
that` and neither agent appears in the persistent addressed-agent
controls.
4. Send the message and confirm both agents are recipients while neither
remains persistently addressed for the next draft.

### Inline mentions with persistence

1. Enable **Automatically mention agents**, then mention an agent inline
and send successfully.
2. Confirm that agent becomes persistently addressed for later messages.
3. In a new draft, select the same agent from the mention picker again.
4. Confirm a new inline `@Agent` is inserted at the cursor while the
agent remains persistently addressed.
5. Confirm the dedicated automatic-mention control and
primary+Shift+Enter shortcut can still add or remove persistent
addressing directly.

## Demo

- **Before:** Persistent addressing could consume an ordinary picker
selection, preventing users from placing that agent inline in the
message.
- **After:** Ordinary selection always produces an inline `@Agent`;
persistence is a separate optional behavior that can coexist with inline
mentions.

### Related issue

N/A — reported through the Buzz feature room.

### Testing

At `a4579c0a666f3644abe44271614186d7b9356bf5`:

- Post-push hooks passed desktop check, desktop typecheck, and the full
desktop unit suite.
- The two persistence-focused Playwright smoke scenarios passed after
explicitly opting into **Automatically mention agents**.
- The one-time multi-agent targeting and persistent-address shortcut
Playwright scenarios passed on the feature changes before the final
rebase; CI is validating the rebased branch.

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-25 13:32:09 -07:00
Taylor Ho
7ba1197aa6 feat(desktop): persist agent addressing across composer messages (#6714)
**Category:** improvement
**User Impact:** Agent addressing now stays visible, editable, and
consistent across messages, so users can keep talking to the same agents
without rebuilding the audience for every send.

**Problem:** Agent addressing was transient and easy to lose track of.
Inline mention text, persistent audience state, restored drafts, and
sent-message rendering could drift apart, making it unclear who would
receive the next message.

**Solution:** The composer now treats addressed agents as a persistent
audience represented by ordinary editable mention chips and a
synchronized compact avatar tray. Eligible agent mentions can become
persistent immediately, addressing survives sends without flashing
empty, and removing or explicitly unpinning an agent updates that state
without disturbing unrelated draft text. `Cmd+Shift+M` / `Ctrl+Shift+M`
provides a fast way to toggle the current recipient or select the best
eligible agent.

- Promotes eligible inline agent mentions into persistent addressing
when the preference is enabled; human mentions remain unchanged.
- Keeps inline chips, restored drafts, persistent recipient state, and
the avatar tray synchronized.
- Clears an agent's persistent address when its final chip is removed,
while explicit unpinning preserves unrelated draft text.
- Restores persistent mentions optimistically during send and restores
the full draft on failure, avoiding an empty-composer flash.
- Deduplicates inline mentions and tag-backed address prefixes in sent
messages.
- Toggles the current addressed agent with `Cmd+Shift+M` /
`Ctrl+Shift+M`, or selects the best eligible agent when none is
addressed.
- Ranks shortcut selection by recent explicit stream mentions, then
active/member/runnable/stable fallbacks; DM participant fan-out does not
affect recency.
- Adds focused avatar feedback for entry/exit, send bounce, and failure
shake, with reduced-motion and narrow-layout support.
- Applies the same behavior to main and thread composers while
preserving draft restoration, channel switching, and send-race
correctness.

### Screenshots

#### Light theme
The compact tray keeps persistent recipients visible alongside editable
mention chips.

![Light theme persistent agent
addressing](https://raw.githubusercontent.com/block/buzz/f34e87a8c083d36abf20422ac9d8ca2c43a49af6/pr-6714--01-light-theme.png)

#### Dark theme
Persistent addressing remains clear and legible in the dark composer
treatment.

![Dark theme persistent agent
addressing](https://raw.githubusercontent.com/block/buzz/f34e87a8c083d36abf20422ac9d8ca2c43a49af6/pr-6714--02-dark-theme.png)

### Reproduction steps

1. Open a channel with an eligible agent and enable persistent agent
addressing.
2. Insert an agent mention and confirm it appears as a normal editable
chip while the compact avatar tray reflects the same persistent
recipient.
3. Send several messages and confirm the agent remains addressed, the
mention restores without an empty-composer flash, and sent messages do
not duplicate the addressed agent.
4. Remove the agent's final mention chip and confirm its persistent
address clears. Add unrelated draft text, explicitly unpin the agent,
and confirm that text remains intact.
5. Press `Cmd+Shift+M` on macOS or `Ctrl+Shift+M` on Windows/Linux.
Confirm it toggles the current addressed agent or, when none is
selected, chooses the most relevant eligible agent.
6. Verify recent explicit channel/thread mentions influence shortcut
selection, while DM participant fan-out does not.
7. Force a failed send and confirm the addressed avatars shake and the
full draft is restored; confirm successful sends use the subtler bounce
feedback.
8. Repeat in the thread composer, after switching channels, in dark
theme, with reduced motion enabled, and at a narrow composer width.

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

**desktop/src/features/channels/ui/ChannelPane.tsx**  
Feeds recent explicit agent mentions into the composer for contextual
shortcut selection.

**desktop/src/features/messages/lib/getVisibleAgentAddressPubkeys.ts**
and tests
Show tag-backed addressed agents only when they are not already
represented by inline mention text.

**desktop/src/features/messages/lib/mentionCandidates.ts**,
**mentionRanking.ts**, **mentionSuggestionMapping.ts**,
**recentMentionPubkeys.ts**, and tests
Model agent eligibility and activity, derive explicit mention recency,
and rank the best shortcut recipient.

**desktop/src/features/messages/lib/useActiveAgentPubkeys.ts**,
**useDefaultAgentSuggestion.ts**, and **useMentions.ts**
Expose active-agent and recent-recipient context to mention consumers.

**desktop/src/features/messages/ui/ComposerAddressControls.tsx** and
tests
Render the compact persistent-recipient tray with enter, exit, send,
failure, layout, and reduced-motion states.

**desktop/src/features/messages/ui/MessageComposer.tsx** and
**MessageComposer.types.ts**
Connect editable mention text, persistent addressing, shortcut
selection, draft restoration, and send feedback.

**desktop/src/features/messages/ui/MessageRow.tsx**  
Avoids duplicating addressed agents already rendered as inline mentions.

**desktop/src/features/messages/ui/MessageThreadPanel.tsx**  
Brings the same recency and persistent-addressing behavior to thread
composers.

**desktop/src/features/messages/ui/useAgentAddressLockPicker.ts**,
**useAutoPinMentionedAgents.ts**, **useAlwaysAddressShortcut.ts**,
**useMentionSendFlow.ts**, and tests
Synchronize mention chips with persistent audience state, promote
eligible mentions, handle shortcut toggling/default selection, and
preserve recipients through send success or failure.

**desktop/src/features/messages/ui/useComposerPasteHandler.ts** and
**useMentionSendFlow.types.ts**
Keep the composer implementation focused by extracting paste and
send-flow boundaries.

**desktop/src/shared/lib/keyboard-shortcuts.ts**  
Documents `Cmd+Shift+M` / `Ctrl+Shift+M` as the agent-address shortcut.

**desktop/tests/e2e/persistent-agent-audience.spec.ts**  
Exercises persistence, selection, removal, sends, failures, restored
drafts, themes, reduced motion, and narrow layouts.

</details>

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-25 18:44:25 +00:00
Kalvin C
a526dca9bc feat: navigate images across message threads (#6705)
## Summary
- navigate across image occurrences that are currently rendered inside
the nearest explicit open-thread DOM scope
- preserve rendered DOM order, duplicate occurrence identity,
hidden-spoiler exclusion, preview-image entry parity, and the exact
thumbnail return target
- keep adjacent timeline messages and unrelated surfaces as separate
galleries
- make Copy and Download follow the current gallery item, including when
a link-preview image opens the gallery first and navigation reaches
Markdown media

## Scope contract

This is intentionally a **rendered-media gallery only**. Gallery
membership comes from mounted image triggers under the nearest explicit
thread scope. Collapsed or unloaded descendants are excluded until the
thread UI renders them. There is no canonical thread fetch, descendant
projection, depth override, or synchronous message reparse in this
change.

`ChannelPane.tsx` remains unchanged from `main`;
`useSearchHighlightProps` and both timeline/thread prop spreads are
preserved.

## Verification
- rebased onto `origin/main` at
`113a33b7e49b7173ee1767c49ef2f49c63803034`
- `git diff --check`
- differential file-size check
- full Desktop check (Biome plus text/pubkey policy checks; only
pre-existing informational diagnostics)
- Desktop TypeScript typecheck
- Desktop production build and rebuilt E2E build
- full Desktop JS units: `5,476/5,476` passed
- full Desktop Tauri workspace tests passed (one documented native
performance test ignored)
- rebuilt image gallery Playwright spec: `14/14` passed
- search-highlight smoke cases: `5/5` passed
- reduced diff: 6 files, `+417/-41`; production `+78/-41`, E2E `+339/-0`

The four added E2E cases cover cross-message thread navigation and
return targeting, rendered-only exclusion of collapsed descendants,
preview-first current-item actions, and adjacent-message isolation.

---------

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-25 11:34:16 -07:00
Duncan
fe0d526a43 test(desktop): assert deep-link focus lands at the mounted dialog boundary
Mount the full AgentEditMergedDialog from an env_key / model deep link and
assert the real document.activeElement end state, exercising the runtime
discovery, conditional-section mounting, and EnvVarsEditor pipeline together.
The env_key case is the acceptance test that caught the required-key churn
focus bug; the model case covers focus deferral until model discovery resolves.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-25 13:41:06 -04:00
Duncan
95bd78d5e0 fix(desktop): stabilize env-key deep-link focus against required-key churn
The EnvVarsEditor deep-link focus effect keyed on requiredKeys by array
identity. advancedRequiredEnvKeys is a fresh filter() array every render, so
content-equal re-renders while discovery/file-config queries settle gave it a
new identity, re-ran the effect, and its cleanup canceled the pending focus
rAF before it fired. The one-shot guard had already latched, so no retry ever
rescheduled and focus was permanently stranded. Depend on the key-set content
signature instead so the effect re-runs only when the set actually changes.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-25 13:41:06 -04:00
Duncan
43aa339e89 test(desktop): cover deep-link focus retry and env-key expansion
Exercise useAgentEditDeepLinkFocus directly against the two scenario-8
blockers: the model combobox must stay unfocused while disabled during
discovery and focus once it resolves; an env_key target must expand the
Advanced accordion; and onOpenAutoFocus must defer to Radix while the
target is unfocusable and claim focus once it is. Each assertion goes red
when its fix is reverted.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-25 13:41:06 -04:00
Duncan
83c73b34da fix(desktop): land deep-link focus on model and env-key targets
Two deep-link focus targets never received focus in the merged edit
dialog. The model/provider combobox is disabled while model discovery is
in flight, so the one-shot rAF focus effect fired against a disabled
control (a silent no-op) and latched its guard, never retrying. Guard
the latch on focusability and add modelDiscoveryLoading to the deps so
the effect re-runs and focuses once discovery resolves.

The env_key target lives inside the Advanced accordion, which seeds
collapsed, so EnvVarsEditor never mounted to receive its focus key. Add
an effect that expands Advanced when an env_key target is requested.

Replace the blanket onOpenAutoFocus suppression — which stranded focus
on <body> whenever the target wasn't focusable yet — with conditional
suppression: claim focus here only when the normalized target is already
focusable, otherwise let Radix autofocus a baseline control while the
targeted effects move focus once the target materializes.

Extract the three focus effects and the autofocus handler into
useAgentEditDeepLinkFocus, deduplicating the target-id resolution and
keeping AgentEditMergedDialog.tsx under the desktop file-size ratchet.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-25 13:41:06 -04:00
Duncan
b5465b04de feat(desktop): port effort picker into merged agent edit dialog
#4557 mounted EffortPickerField into AgentInstanceEditDialog, which this PR
deletes. Fold the write control into the merged dialog's instance section
under the same gating (local backend + discovered effortConfigId), wiring the
agent config surface query in the parent. Preserves the direct-write path,
surface invalidation, and edit-agent-effort id unchanged.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-25 13:41:06 -04:00
Duncan
aff889b5b5 test(desktop): drive merged edit dialog in access-tightening e2e
PR #6086's agent-access-warning spec assumed the pre-merge two-dialog
structure (Advanced toggle, "Customize for this agent" tab,
#persona-system-prompt). The merged dialog surfaces the instance-owned
respond-to control inline and renames the definition prompt field to
#edit-agent-system-prompt, so the stale locators no longer resolve.
Adapt the spec to the merged surface; the instance-scoped save
assertions are unchanged.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-25 13:41:06 -04:00
Duncan
0594b2b917 fix(desktop): suppress dialog open-autofocus for deep-link focus targets
A config-nudge deep link opens the merged agent-edit dialog with an initialFocus target, but Radix's default open-autofocus lands on the first focusable control and, running after the dialog's rAF-scheduled targeted focus, steals it back — so the deep link never lands on the requested field. Prevent the default open-autofocus whenever an initialFocus is supplied and let the targeted effect own initial focus.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-25 13:41:06 -04:00