Commit Graph

773 Commits

Author SHA1 Message Date
Will Pfleger
83aab8cb5c feat(desktop): remove communities locally and stop their agent pairs (#7997)
🤖 Leave contacts the relay before it removes anything locally: it checks
the relay's `/info` for NIP-43 membership support, then waits for the
relay to accept a signed leave request. When a relay has been torn down,
that check fails, Leave throws, and the community stays in the rail.
Removing a community also left its relay's managed agent pairs running,
and they kept reconnecting to it until the app was quit.

**Remove from this device**
- A new community action, **Remove from this device**, sits under
**Leave community** in the profile menu's community actions. It never
contacts the relay. A confirm dialog explains that relay membership
isn't revoked.
- `useCommunityNavigationTransitions` splits the old `removeCommunity`
into `removeCommunityFromDevice` (local-only cleanup and navigation to a
fallback community or the discovery screen) and
`leaveAndRemoveCommunity` (calls `leaveCommunity`, then runs the same
cleanup). Leave's flow hasn't changed.
- When Leave fails, the error now adds: "If the community no longer
exists, use Remove from this device." The error shown when community
discovery state can't be saved no longer says "Membership was removed",
so its wording fits both actions.

**Agent pairs on a removed relay**
- Once the local removal completes, the shared cleanup stops every live
agent pair on that relay, including manually started ones. It matches
relays with `canonicalRelayUrl`. Stopping is best-effort: failures are
logged and the removal still completes. Pairs on other relays aren't
touched, and nothing is stopped if another community still uses the same
relay.
- Both reconcile call sites (`useManagedAgentRuntimeReconciliation` and
`bootstrapManagedAgentRuntimePairs`) now go through
`reconcileConfiguredManagedAgentRuntimes`. A reconcile that is already
running in Rust can't be cancelled, so after it resolves, this helper
stops any pair it started whose relay is no longer in the saved
community list.
- Launch restore runs in the background after `apply_workspace` and can
finish after its community is removed. `apply_workspace` takes a restore
ticket (the current `launch_restore_generation` in `AppState`) before
handing its lock to the restore task. Removing the active community
calls the new `invalidate_launch_restore` command, which bumps that
generation while holding the runtime transition lock.
`restore_managed_agents_on_launch` re-checks its ticket under the same
lock and spawns nothing if it changed. If restore takes the lock first,
its pairs are registered before invalidation returns, and the removal
stop then finds them. Removal never changes `relay_url_override`: the
outgoing relay stays applied until the next `apply_workspace`, exactly
as on `main`, so in-flight work never falls back to the default relay.
Invalidation never takes `workspace_apply_lock`, which restore holds
while waiting on the transition lock, so lock order is unchanged.
- With no relay applied, backend readers fall back to the default relay,
the same state as launch before the first `apply_workspace`. A normal
quit and relaunch doesn't bring a pair back on a removed relay: restore
targets the applied workspace, and the legacy per-record relay pin is
ignored.

**Tests**
- A `community-rail` e2e case makes the mocked relay's membership check
fail, the way an unreachable relay does. It checks that Leave shows the
error and keeps the community, and that Remove from this device then
removes it, switches to the fallback, stops the agent pair on the
removed relay, and leaves the pair on the other relay running.
- Unit tests in `managedAgentRelayCleanup.test.mjs` cover relay
matching, a stop or listing failure not throwing, and the reconcile
fence stopping a pair that started on a relay removed mid-flight.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-10-01 18:43:21 +00:00
Matthew Boston
036215636f Allow following channel messages before their first reply (#7692)
## Summary

Channel messages only offered Follow thread after receiving a reply. Add
Follow/Unfollow to delivered messages immediately, so users can opt into
future replies. Keep the same follow state when broadcast replies gain
replies of their own.

Threads you authored, participated in, or explicitly followed offer
Unfollow. A mention alone leaves Follow available. These controls apply
to channel timelines; DM notification behavior remains unchanged.

### Related issue

None found in existing issues or PRs.

### Testing

- Browser regressions cover keyboard follow/unfollow, persistence after
reload, first-reply unread and notification delivery, suppression after
unfollow, pending-message exclusion, broadcast reply transitions after
revealing buffered arrivals, and immediate unfollow for your own
messages.
- Additional regressions cover following after a mention and the absence
of Follow/Unfollow controls in DM timelines.
- Desktop unit tests, typecheck, lint, and production build pass.
- Browser notification routing is covered; native OS banner appearance
was not tested.

### Screenshots

| Before | After |
|--------|--------|
| <img width="979" height="301" alt="before"
src="https://github.com/user-attachments/assets/1a739187-5bc3-4e23-a334-723ca69672f5"
/> | <img width="979" height="337" alt="after"
src="https://github.com/user-attachments/assets/57df4ff9-bac4-4b9d-a39f-5664e0f9ece0"
/> |

---------

Signed-off-by: Matthew Boston <mboston@squareup.com>
2026-10-01 10:15:45 -06:00
klopez4212
0ee2ad7fbf Fix rounded macOS icon on older Macs (#8018)
Older macOS versions display Buzz’s opaque icon as an oversized square.
Bake rounded corners and transparent padding into the macOS `.icns`,
preserving the original bee artwork and leaving other platforms
unchanged.

Generation uses Hermit-pinned uv, Python 3.14.3, and Pillow 12.2.0.
`scripts/macos-icon.sh --check` regenerates temporarily and compares all
decoded representations; macOS CI runs it before packaging. Pixel
reproducibility is verified independently of Apple’s ICNS container
encoding.

Validated native AppKit preview, all 10 representations, fresh-cache
generation, one-pixel/missing-representation drift rejection, workflow
lint, and pre-push checks. Full local `just ci` is running. Packaged
Dock checks on older/current macOS remain outstanding.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-01 17:00:57 +01:00
Will Pfleger
bd0896f5c0 fix(desktop): read channel metadata and member lists from writer after writes (#7999)
🤖 This fixes two problems with the same cause. Since #3268, the relay
can answer an unpinned `/query` from the Aurora read replica, as long as
the replica has proved it's within `BUZZ_REPLICA_READ_MAX_AGE_MS` (1000
ms) of the writer. That's fine for display reads, but it doesn't
guarantee a client sees its own write.

**Problem 1: channel creation (Wes's report).** Right after creating a
channel, the desktop read back the channel's kind:39000 metadata by
`#d`. If that read landed on a replica that hadn't replayed the event
yet, it failed with `channel created but metadata not yet available`.
The channel existed, but the chosen template (canvas, agents) was never
applied.

**Problem 2: member lists.** After any membership change, the
member-list refetch could also hit a lagging replica. The relay returned
the list from before the change, and the desktop cached it as fresh for
five minutes (`CHANNEL_MEMBERS_STALE_TIME_MS`). Someone just removed
could still show as a member, or someone just added could be missing.

## Fix 1: metadata read-backs go to the writer

`channel_metadata_filter` is a shared builder that adds `"consistency":
"strong"`, so the relay serves the read from the writer. It follows the
same pattern as the canvas head reads. All four metadata read-backs that
follow a write use it: `create_channel`, `update_channel`,
`fetch_starter_channel_metadata` (its retry and fallback are unchanged)
and the open-DM refetch in `dms.rs`. A unit test fails if the flag is
removed from the builder.

The template's agent-add step also reads channel members (kind:39002)
right after create. `get_channel_members` takes an opt-in
`read_your_writes` flag that sends `"consistency": "strong"`. Only
`createChannelManagedAgents` sets it on that path, so the agent-add step
sees the creator's membership.

## Fix 2: a 31-second writer-read window after membership changes

Every membership write the desktop makes calls
`noteChannelMembershipChange` for each channel it changed:

- the native write wrappers `createChannel`, `openDm`, `joinChannel`,
`leaveChannel`, `addChannelMembers`, `removeChannelMember` and
`changeChannelMemberRole`
- the Bestie conversation open
- huddle agent add and remove, for both the huddle channel and its
parent
- `ensure_starter_channels` and `sync_agents_to_active_huddle`. Both
return every channel they created, joined or changed in
`changed_channel_ids`, including ones that landed before a later step
failed. The original error text is rethrown unchanged.
- live join, leave, removed and `admin_kick` system messages, and
member-added and member-removed notifications
- a report kick resolved from the Admin Console, once the relay confirms
it succeeded and only when the report's channel is in the active
community. The kick goes through the admin HTTP API, not a channel
event. Ban, timeout and their lifts only change `community_bans`, not
channel membership, so they need no record.

For 31 seconds after a change, measured on `performance.now()`,
`getChannelMembers` for that channel sends `readYourWrites`. Why 31
seconds: the relay caps its replica staleness budget at
`FENCE_STALENESS` (30 s), so once the window closes, no supported
configuration can serve the pre-change list. The constant's comment ties
it to `FENCE_STALENESS`. One app-level listener cancels any roster fetch
already in flight and refetches. When it registers, it also refreshes
every channel whose window is still open. All other member-list reads
still use the replica.

**Community switch.** The window map is cleared when the community
changes. Each awaited write captures a generation through
`beginChannelMembershipWrite()` before its first native call, and the
reset advances it. So a write that settles after a community switch
records nothing in the new community. Live events don't need this fence,
because the reset disconnects the relay client first and frames from the
old connection are dropped.

## Coverage

- A Rust test drives `ensure_starter_channels` against a stub relay that
accepts the creates but never serves metadata, and checks that both
created starter channels are still reported.
- Desktop tests cover the writer-read window, the Admin Console kick,
and writes that settle after a community switch.
- The desktop E2E test bridge (`desktop/src/testing/e2eBridge.ts`)
mirrors the strong reads and `changed_channel_ids`, so E2E runs exercise
the fixed behavior.

This doesn't address the smaller race in the relay's per-pod
accessible-channels cache, and it doesn't change mobile.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-10-01 11:32:16 -04:00
Diem Nguyen
12dbb114aa test(desktop): fix flaky forum empty-draft replacement e2e (#7981)
## Summary
The `replacement=` cases (`add` and `publish`) of `forum source visit …:
A→B→A retains source draft` in `forum-agent-invitation.spec.ts` are
flaky. They fail at line 449 because the source draft still reads
`@RemoteScout hello`.

Cause: Playwright's `fill("")` on a contenteditable sets a DOM selection
and then presses `Delete`. ProseMirror handles `Delete` with its own
selection, which sometimes hasn't synced from the DOM yet. When that
happens the key press does nothing, `onUpdate` never fires, and the
clear is never recorded. I confirmed this with temporary logging:
failing runs have no authored-content update after the fill. Non-empty
`fill` uses `insertText`, which goes through ProseMirror's DOM input
path, so it isn't affected.

Fix (test only):
- Clear the draft with `ControlOrMeta+a` followed by `Delete`, so
ProseMirror owns the selection.
- Assert the replacement text before navigating away.

### Related issue
None found. The spec hasn't changed since #7125. This test failed in 10
of the last 40 CI runs of `Desktop Smoke E2E (2)`, for example #3700.

### Testing
- `--repeat-each=100 --retries=0 --workers=4` on the two affected cases:
  - Before the fix: 14 of 200 runs failed at `:449`.
  - After the fix: 200 of 200 passed.
- Full `forum-agent-invitation.spec.ts`: 19 passed.
- Pre-push hooks passed (desktop typecheck and desktop tests).

Signed-off-by: Fizz <6be7be23ec5bba868f3e1db5ec86013d76bd1a293dce5b435a192158fec13641@buzz.block.builderlab.xyz>
Co-authored-by: Fizz <6be7be23ec5bba868f3e1db5ec86013d76bd1a293dce5b435a192158fec13641@buzz.block.builderlab.xyz>
2026-09-29 15:59:30 -07:00
Luke Tornquist
6918dd3a83 Schedule the deletion drain safely (#7827)
## Why

The deletion engine already exposes a one-shot `buzz-admin deletions
drain` command, but operators have no safe way to schedule it. A generic
workload wrapper could expose arbitrary commands or inherit relay
credentials, which would add a second control plane and widen the blast
radius.

## What

This PR adds an opt-in Kubernetes CronJob for the typed deletion drain
command. It is stacked on #7818, which adds owner-request admission.

## How

The chart keeps the job disabled by default. The CronJob uses `Forbid`,
zero Kubernetes retries, bounded runtime and history, and only the
database, Redis, and object-store settings required by the executor.
PostgreSQL leases, checkpoints, and retry timing remain the sole durable
retry authority.

The closed `_operator-jobs.tpl` helper centralizes safe CronJob
mechanics without creating an arbitrary command or environment registry.
This leaves the touched system simpler than a general job framework
would.

## Risk

The deployment blast radius is low because the job is disabled by
default. When enabled, it can run destructive deletion work that the
database has already approved, so the chart limits concurrency, retries,
credentials, and execution time.

## Testing

Rendered every `deploy/charts/buzz/ci/*.yaml` fixture with `helm
template`; all passed at `e68cb753bb6673aa1baa60046969bdb2d4738b62`.

Ran the full independent Desktop, Tauri, and web lanes; all passed at
`e68cb753bb6673aa1baa60046969bdb2d4738b62`. Flutter package resolution
made no progress within three minutes, so the mobile lane is not claimed
green.

## Bigger picture

A later stack layer must inventory and approve admitted owner requests.
Until then, this scheduler cannot make a new owner request destructive.

Generated with Codex

---------

Signed-off-by: tornquist <tornquist@squareup.com>
Signed-off-by: Codex <noreply@openai.com>
Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
2026-09-28 18:23:42 -04:00
Logan Johnson
f1e50be410 test(desktop): wait for channel head refresh before paging thread summary test (#7955)
🤖

## Summary
Main's Desktop Smoke E2E (4) job fails on `scroll-history.spec.ts`
("thread summary badge survives a retained older-history prepend"). This
change fixes the test's timing. It does not change app code.

When a channel opens, the app starts a live subscription. When that
subscription is ready, the app fetches the newest page again, and that
fetch replaces the whole loaded history. This test scrolled up before
that refresh happened. On slower CI runners, the refresh came after the
older page loaded but before it showed on screen. The older rows went
away, the reader stayed at the top, and no more pages loaded. All three
attempts failed that way.

The test now calls `waitForMockChannelHeadReady` before it scrolls, like
the other pagination tests in this file already do. That helper waits
for the live subscription and its refresh to finish.

### Related issue
Main CI failure at `15d44dcc`
(https://github.com/block/buzz/actions/runs/36470599751/job/109092076235).
The same failure was reported on #7827.

### Testing
- Added temporary logs to see the order of events. Locally, the refresh
ran after the older page had loaded: the loaded history went from 2
pages back to 1. The test passed only because it checked before the next
render. With the wait, the refresh runs first and nothing replaces the
loaded pages later.
- `playwright test --project=smoke tests/e2e/scroll-history.spec.ts`: 18
passed. This test with `--repeat-each=8`: 8 passed.

### Follow-up (not in this PR)
The app has the same race for real users. If someone scrolls up before
the live subscription is ready, or during a reconnect, the refresh can
drop the older history they loaded. If they are at the top of the list,
paging stops until they scroll down and back up.

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-28 14:44:16 -06:00
Bradley Axen
548718e042 test(desktop): scope video menu e2e probes to emitted messages (#7953)
## Summary

Fixes the `Desktop Smoke E2E (4)` failure on `main`. The test at
`desktop/tests/e2e/video-attachment.spec.ts` ("right-click menus expose
distinct selectors for links, relay video, and off-relay video") fails
3/3 attempts on main and on every PR branch since ~Sept 25. Because
"Desktop" is a required check, that blocks merges.

**Cause:** the relay-video and off-relay-video probes picked their
player with `getByTestId("video-player").last()`. When mock messages
share a second, timeline ordering can put a different video last. The
locator then resolves to the wrong player, and the "Download video" menu
detaches before the click, so the test times out.

**Fix:** scope each probe to the `data-message-id` of the message
`emitVideoMessage` just returned. Test-only change, 16 lines.

## Attribution

This is a cherry-pick of `dfd5db4e0` by @tornquist, originally part of
#7827 (the deletion-drain cron, which is stacked on another feature
branch). It's pulled out on its own so main can go green without waiting
on that stack. Authorship is preserved.

## Testing

- `pnpm build:e2e && playwright test tests/e2e/video-attachment.spec.ts
--project=smoke`: 13/13 passed locally, including the target test.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: tornquist <tornquist@squareup.com>
Signed-off-by: murderbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz>
Co-authored-by: tornquist <tornquist@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-28 19:46:14 +00:00
Will Pfleger
52c4c08c9e fix(sidebar): converge stale-at-open state across devices (sections/sort/stars/mutes) (#7805)
Adds bounded-backoff recovery reads to all four sidebar sync hooks
(sections, sort, stars and mutes). A stale relay read at app open now
converges without a user edit or a reconnect.

## What this does

**Shared recovery hook (`useStaleReaderRecovery`).** All four hooks use
one retry effect. It reads immediately on mount, then at 5, 10, 30 and
60 s, staying at 60 s after that. A change of `visibilityState` to
`visible` triggers an immediate read. Reads are single-flight. The
effect restarts whenever `pubkey` or `relayUrl` changes, so a hook that
stays mounted while switching relays drops the old relay's in-flight
reads.

**Guards that stop a recovery read from rolling back a local edit:**

1. **Skip the read while an edit is pending.** If an edit is pending
when a tick starts, the tick skips its read and just schedules the next
one. Without this, the read could return the pre-edit blob after the
edit published and pending cleared. At that point neither apply-time
guard could reject it.
2. **Pending check inside the state updater.** `hasPending()` and the
effect lifetime are re-read inside `setStore(prev => ...)`. That way the
check runs when React executes the updater, not when the response
arrives.
3. **Local-mutation revision.** Each hook counts its local edits. The
count is snapshotted when a read starts and checked again at apply time.
If an edit landed while the read was in flight, the response is
discarded.
4. **Head advanced only after a successful cache write.** A failed
`write*Store()` leaves the same head retryable on the next tick.

**Ownership-aware pending clear.** Each sync manager's
`doPublish(store)` now clears `pendingStore` only while it still holds
that same `store`, on both the identical-payload early return and the
success path. Before, an older publish that finished after a newer edit
had replaced `pendingStore` wiped the newer edit's pending marker, so a
recovery read before the newer debounce could apply the older relay head
over it. `destroy()` still clears unconditionally.

## Scope

Reader-side recovery plus the ownership-aware pending clear. No
outboxes, arbitration or replay changes. It applies to
`useChannelSections`, `useChannelSortPreference`, `useChannelStars` and
`useChannelMutes`.

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

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-28 12:48:16 -06:00
Tom Brow
02c6309f53 fix(ci): select runtime suites from PR changes only (#7843)
A documentation-only update to #7809 selected desktop builds and E2E
tests because path detection compared an old PR base SHA with GitHub's
newer synthetic merge commit. The [failing run's path-detection
log](https://github.com/block/buzz/actions/runs/35789957773/job/106955810630)
includes four unrelated desktop files from `main`; `VISION_MOBILE.md`
did not match a runtime filter.

Use GitHub's PR file list for pull requests so unrelated changes in the
synthetic merge commit cannot select runtime suites. The existing
directory filters are unchanged: root and docs/ Markdown do not select
runtime suites, while Markdown under runtime directories still selects
its affected suites. Mixed code/documentation changes retain normal
coverage. Always-on security, policy, and source-contract checks and
full push-to-main coverage are unchanged.

Added regression coverage runs the pinned paths-filter action against
real fixture repositories, including a synthetic merge containing
unrelated upstream desktop code. All 35 selection scenarios and 26
required-check scenarios pass; the new regressions failed before the
fix. Existing required-context isolation, file-size policy, and
security-review contract checks pass, as do script lint and workflow
syntax validation. Full workflow lint reports the same two pre-existing
shell-quoting findings in the untouched dead-token guard. A desktop E2E
build was run to diagnose the unrelated required smoke failure.

Related: #7809 (incident, unchanged) and #5756 (shared-input path
coverage, separate scope). No matching issue found.

PRs with at least 3,000 changed files now select every runtime suite,
avoiding GitHub's PR-file-list ceiling. Boundary tests cover 2,999,
3,000, and 3,001 files, including a runtime file omitted beyond the API
cap; disabling the safeguard makes the latter two regressions fail.

Required checks now run and fail when path selection fails, is
cancelled, or is skipped. Regression coverage exercises the workflow
conditions and shell checks for all 13 required wrappers, plus an
API-denial case against the pinned action. Mutating either the
scheduling guard or the result check makes all 13 failure regressions
fail.

The required mention-settings smoke test expected a pin after explicitly
disabling automatic mentions. Waiting for its old avatar to exit
reproduced the CI failure 3/3; the test now checks that subsequent
mentions remain manual and retains outgoing recipient-tag assertions.
Corrected browser regression: 20/20 repeated runs; related picker unit
tests: 18/18. No desktop production behavior changed. The always()
requirement is now pinned in required-wrapper regression tests; deleting
it fails all 26 gate cases.

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-09-25 23:19:32 +00:00
Bradley Axen
a1fb6645d9 test(desktop): synchronize upload edit smoke test (#7903)
## Summary
Fix the upload/edit smoke-test race observed on main at
`6410e685a80d42db0645fadc9bbe710559ef911e`.

- Hold the mock upload at the Tauri IPC boundary and release it
explicitly after checking edit rejection, rather than assuming menu
interaction finishes within one second.
- Wait for the Radix menu to unmount: edit dispatch happens in
`onCloseAutoFocus`, after the click. Without that wait, the negative
assertion can pass before the edit callback actually runs.
- Keep the existing attachment-preservation and subsequent
successful-edit assertions. No production code or CI coverage is
changed.

## Evidence and scope
- [Main
failure](https://github.com/block/buzz/actions/runs/36189213149/job/108250341388):
`opening edit during an immediate photo upload preserves the draft`,
including an upload-progress timeout on retry.
- Investigated from #7791, which changes only `docs/nips/NIP-AR.md`. Its
PostgreSQL failures and video-menu timeout are separate; PostgreSQL
passed on this main run, and the video spec passed locally. This PR does
not claim to fix those failures or the separate pointer-interception
failure seen in the main job.
- #7843 already addresses unrelated runtime suites being selected for
docs-only PRs.

## Validation
- Full desktop `pnpm test`: 6,676 Node tests + 92 jsdom tests passed.
- Upload/edit regression repeated 5 times: passed. Holding the upload
without waiting for menu teardown failed all 5 runs, demonstrating the
second race.
- Complete file-attachment and video-attachment Playwright specs: 30
passed.
- Pre-push desktop lint, typecheck, full desktop tests, and file-size
gate passed at `46e3d4392e5b56dcb74ea864aa19aa10287f27d3`.
- `just ci` attempted: initial formatting issue corrected; second run
exceeded the 5-minute local limit during Tauri clippy. Not claiming full
repository CI green.

## Review / human verification
Self-reviewed the diff against the test contract; no production behavior
changes. Draft pending human verification and CI.

To verify: activate Hermit, then `cd desktop && pnpm test:e2e:smoke
file-attachment.spec.ts --grep "opening edit during" --repeat-each=5`.
Expect all five runs to pass, retaining the upload in the draft and
allowing edit only after upload release.

Originating conversation:
buzz://message?channel=aa7f2b48-d367-4ca1-be09-116a2eb45b99&id=1b68585ba4483a87f5a453fd7bbdd11d434e332dcb4aff8acfe1f5d2601b6561

Signed-off-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz>
Co-authored-by: Smartie <fb3185faacfc3760b6b4fe0085a8878d8dd537f7a7671c7d3184fe1aa1b04df4@buzz.block.builderlab.xyz>
2026-09-25 22:52:02 +00:00
Logan Johnson
dd1c2afc2a Discover alternate Buzz ACP commands (#6948)
🤖 I’m Larry.

## Summary
Managed-agent definitions can now choose which installed ACP transport
will launch the agent. The choice sits beside **Agent harness** while
creating or editing an agent, so it is made before first deployment and
remains visible afterward.

Buzz discovers executable commands named `buzz-*-acp`, keeps **Buzz ACP
(default)** as the safe default, and preserves existing custom command
values. Discovery and launch share the same resolver, so a command
offered in the picker is the command Buzz will execute.

## UX walkthrough

### 1. The ACP command picker appears while creating the agent
The default is explicit, and the helper text explains that this
selection controls deployment.

![Create-agent form with the ACP command picker beside Agent
harness](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/01-persona-create-acp-selector.png)

### 2. Installed wrapper commands are offered by name
Here Buzz has discovered `buzz-janet-acp` alongside the stock transport.

![ACP command menu offering Buzz ACP and
buzz-janet-acp](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/02-discovered-wrapper-dropdown.png)

### 3. The wrapper is selected before deployment

![Create-agent form with buzz-janet-acp
selected](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/03-wrapper-selected-before-deploy.png)

### 4. The deployed profile reports the effective command

![Deployed agent Runtime profile showing
buzz-janet-acp](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/04-deployed-profile-effective-acp.png)

### 5. Editing the deployed agent reopens its definition with the choice
preserved

![Edit-agent form retaining
buzz-janet-acp](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/05-edit-shows-persisted-wrapper.png)

## Details
- Definitions carry the selected ACP command through relay publication,
snapshots, imports, team adoption, deployment, and later edits.
- Existing definitions without this field continue to use `buzz-acp`.
- Unknown persisted commands remain visible but unavailable rather than
being silently replaced.
- Discovery is read-only and never executes candidates. It filters
non-files and non-executables, supports Windows `.exe`, `.cmd`, and
`.bat` shims, deduplicates aliases, and returns a stable sorted list.
- Command names remain portable; Buzz does not persist machine-specific
absolute paths.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@nostr>
Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-24 11:33:24 -04:00
Will Pfleger
d01e5f8205 feat(agents): humanize uncurated Databricks model ids with a label grammar (#7844)
New Databricks endpoints (Claude Opus 5.5, GPT-6 Astra/Sol, etc.) showed
as raw FQNs in the model picker until someone added a curated corpus
record for each. This adds a strict token-class grammar as a third label
tier inside the Databricks label helpers (Rust `model_capabilities.rs` +
TS `modelCapabilities.ts`): exact record → unique alias → **generated
label** → raw id. In TS it fires only for an explicit `databricks_v2`
provider; providerless ids stay raw.

## Grammar

Takes the last component of a UC FQN (or strips one wrapper:
`databricks-`, `goose-`, `kgoose-`, `builderbot-`; an id that is neither
stays raw), splits on `-`, and classifies each token as version,
letter+digit version, size, trailing date (dropped), or word. Labels are
built only from the id's own text, and the grammar never reinterprets a
version. When in doubt it returns raw:

- **Known families only.** The family (leading letters of the first
token) must be in the manifest's existing `label_family_tokens`. Custom
endpoints such as `prod.ai.rag-agent-3` or `builderbot-pr-reviews-v2`
stay raw. Supporting a new vendor means adding one entry to that list,
not a record per model.
- **Attached digits stay whole** (`gpt10` → `GPT10`). If the family
already carries digits and the next token is a version
(`goose-qwen3-5-122b-a10b`, `system.ai.llama3-1-8b`,
`system.ai.claude3-5-sonnet`), the id stays raw; this is checked before
any reorder.
- A third consecutive number (`gemini-3-7-1-flash`) returns raw rather
than splitting off a patch version.
- Any non-ASCII input returns raw, checked before trimming or case
folding.
- Any unclassifiable token, or no version-bearing token
(`builderbot-pr-reviews`), returns raw. `(latest)`/`Preview` never
appear unless an exact record supplies them.

Special cases, each justified by an unseen-id shape rather than the
corpus score:

- `meta-llama-` → `Llama`: a namespace convention.
- Claude numeric-first reorder: Goose names Claude endpoints
`claude-4-7-opus`. Only 1–2 version tokens followed by
`opus`/`sonnet`/`haiku` reorder (`claude-5-6-opus` → `Claude Opus 5.6`);
anything else keeps its order (`claude-5-6-thinking` → `Claude 5.6
Thinking`). Both current numeric-first ids also have exact records.
- Qwen compact decimal: Qwen's own naming `qwen35` → `Qwen3.5` when the
second digit is nonzero.

## Colliding labels in the picker

The same model is often served from several sources, so humanized labels
collide (four rows are all `GPT-5.5`). `disambiguateModelLabels` in
`formatAgentModelLabel.ts` suffixes only colliding Databricks labels
with their source:

- UC FQN: its `catalog.schema`, e.g. `GPT-6 Astra (system.ai)` / `GPT-6
Astra (sandbox.public)`.
- Workspace endpoint: its wrapper, e.g. `GPT-5.5 (databricks)` /
`GPT-5.5 (goose)` / `GPT-5.5 (kgoose)`.
- Still colliding after that (e.g. a date variant in the same schema):
the full id, e.g. `Gemini 3.7 Flash (system.ai.gemini-3-7-flash-0925)`.

Unique labels, raw-id rows, the default row, and other providers are
unchanged. It runs in `getDiscoveredPersonaModelOptions` (covering
`AgentDropdownSelect`, `PersonaModelCombobox`, `PersonaDropdownField`,
whose closed triggers read the option label) and in `ModelPicker`. Model
search in `AgentDropdownSelect` and `PersonaModelCombobox` also matches
the raw id, so searching `system.ai` finds those rows. Rust
`ModelEntry.name` is unchanged.

## What labels drive

Picker values, persistence, routing, catalog dedup, `resolve()` and
capabilities all stay keyed on the raw id; two ids sharing a label both
remain selectable. Labels do drive picker search (alongside the raw id),
the workspace catalog's equal-time sort, and `TeamIdentityCard`'s "Mixed
models" summary, which compares lowercased base labels, so `goose-` and
`system.ai` GPT-6 Astra read as one model there (correct, since they are
the same model).

## Tests

Shared fixtures in `scripts/databricks-label-fixtures.json` are replayed
by both Rust and TS. Each row asserts its tier: `exact` rows match a
record, `alias` rows resolve with generation off, `generated` rows
return nothing with generation off (so a future alias or record cannot
mask them), and `raw` rows return nothing either way. A curator guard
requires that across all 57 exact records, the ids the grammar does not
reproduce equal exactly the 10 curator-only ids.
`disambiguateModelLabels` has unit tests for namespace, wrapper, and
full-id-fallback collisions, untouched rows, other providers, and
case-insensitive grouping. The defaults-picker e2e checks the suffixed
rows and trigger, raw-id search, and that choosing the second uncurated
FQN by its visible name persists its raw id and restores its label. An
edit-agent e2e covers the existing-agent model dropdown
(`#edit-agent-model`): both suffixed rows render, the `system.ai` row is
chosen by name, and after save and reopen the agent stores the
`system.ai` id and shows `GPT-6 Astra (system.ai)`.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-23 17:12:02 -04:00
Will Pfleger
b6a26556ec feat(canvas): add version history with atomic restore (#6780)
🤖 Adds append-only channel canvas history, restore, and
relay-authoritative optimistic concurrency so stale edits cannot
silently replace a newer canvas.

## What this adds

- **Desktop history and restore** — browse revisions with author,
timestamp, preview, and diff; restore an older revision as a new signed
head after explicit confirmation. Persisted empty canvases remain
reachable, mutation outcomes are announced accessibly, and focus moves
to a stable destination after save or restore.
- **CLI history and restore** — `buzz canvas history --channel …`, `buzz
canvas get --channel … --revision <id>`, and `buzz canvas restore
--channel … --revision <id>` use keyset pagination over the retained
revision stream.
- **SDK canvas builders** — optional `expected-revision` tags, bounded
ancestry classification, and shared timestamp discipline keep
first-party writes ahead of the head they read without propagating
poisoned future timestamps.

## Authoritative concurrency contract

Tagged canvas writes use compare-and-swap semantics in the relay and
database:

- `expected-revision=none` succeeds only when no live canvas exists.
- An event ID succeeds only when it names the canonical live head.
- A byte-identical replay succeeds only when that event is already the
canonical live head.
- A successful candidate must sort strictly ahead of the current head
under `created_at DESC, id ASC`.
- Malformed preconditions and stale or non-advancing writes are rejected
before event rows, mentions, or fan-out are committed.

The database serializes tagged writes, unconditional kind `40100`
writes, and canvas soft deletion on the same `(community, kind,
channel)` advisory key. Untagged `buzz canvas set` remains an
unconditional write, but participates in that serialization boundary so
it cannot race a tagged save or deletion. Deletion classification is
derived inside the datastore transaction; unrelated event kinds do not
take the canvas lock.

## Read and timestamp consistency

Precondition and post-write ancestry reads request strong consistency
from `/query`, pinning them to the writer when replica routing is
enabled. Display-only history remains replica-eligible.

First-party canvas writers stamp `created_at = max(now, head + 1)` with
a conservative client skew limit. The relay applies a canvas-specific
future-timestamp ceiling before persistence, preventing an accepted
boundary head from making subsequent first-party writes invalid while
preserving the broader ingest limit for other event kinds.

## Durable outcomes and recovery

After an accepted publish, a failed verification read is reported as
durable success with `verified: false` rather than a failed save.
Desktop and CLI preserve the new event ID and direct users to History
when verification is unavailable. A successful read walks the bounded,
cycle-guarded `expected-revision` ancestry chain so transitive
descendants are not misclassified as supersession. Canvas caches
invalidate for every settled mutation outcome, including a retained but
superseded revision.

The 409 reconciliation path in `cmd_restore_canvas` now separately
establishes persistence evidence from ancestry classification.
`canvas_write_survived` checks reachability only; a secondary
writer-pinned IDs lookup distinguishes "A absent" from "A present but
superseded by a legacy write." The four-way outcome (survived /
persisted-but-superseded / genuinely-absent / read-fail) is now correct
for all lost-response shapes including unconditional legacy writes.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
2026-09-23 11:03:54 -04:00
Wes
fe56d311a7 test(desktop): stabilize unread and audio release smoke fixtures (#7821)
## Summary
Repair the mock-backed desktop smoke fixtures blocking release PR #7817.
No application unread logic, styling, audio component, scheduler, or
release-tool changes.

- Require an exact channel-scoped kind-9 subscription before injecting
unread messages, including after navigation. The old readiness check
could accept a global-only, wrong-channel, or wrong-kind consumer. The
extracted predicate is used by the real mock bridge and has regression
coverage for those false positives and legacy behavior.
- Seed the 24 audio messages offscreen with distinct timestamps, then
open the channel with all cards in the load margin. Same-second random
event IDs previously reordered the settling timeline, causing different
visible cards to start loads. This was viewport churn, not a component
restart defect.
- Expose the existing real audio scheduler snapshot through the E2E
bridge. Assert 3 active / 21 queued before navigation and 0 active / 0
queued afterward. Preserve the peak-3 cap, exact 3 fetches /
cancellations / releases, zero object URLs, and existing unread/bold
assertions. No sleeps, retries, skipped tests, or relaxed limits were
added.

## Validation
- Before commit, all 30 tests in `channel-activity-popover.spec.ts`,
`channel-mute.spec.ts`, and `voice-note.spec.ts` passed five repetitions
with retries disabled: **150/150**. This was the working repair on base
`312cf674f`, before final formatting/cleanup, not the entire smoke
suite.
- At committed head `1b126afbe75c2a2afbdfe13744f0938fa584764e`, normal
pre-push hooks passed: **6,591/6,591 desktop unit tests**, desktop
checks, TypeScript typechecking, differential file-size gate, and
branch-skew check. Pre-commit Biome formatting passed; working tree is
clean.
- Independent sidebar and audio reviews found no substantive blocker.
Their formatting findings are resolved in this commit.
- Full repository `just ci`, native release packaging, and the entire
Playwright smoke suite were not run locally. GitHub CI is the remaining
broader gate; this PR is not a release certification.

## Release follow-up
After this repair is merged and required checks pass, regenerate #7817
through the desktop release tooling and validate the new candidate's
exact head. This PR does not merge or publish a release.

Originating discussion:
buzz://message?channel=3428ec3f-a58b-429b-afbc-dc6a77918ce8&id=3303565b14334696e9bbf433ef3ad1aea4307acc69834f7f685a412e81354493

Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-09-23 01:07:00 +00:00
Diem Nguyen
312cf674fa fix(desktop): remember Inbox unread-only choice (#7672)
## Summary

Inbox forgets the **Show unread only** choice after navigation or
restart. Save the toggle with the existing safe storage helpers and
restore it when Inbox opens.

The preference stays on this installation and applies across accounts
and communities. Fresh installations keep the current off default. If
storage fails, the current view remains usable, but the choice may not
survive navigation.

### Related issue

Related report: #3669.

Overlaps with #6105 and #3682. #6151 also persists category filters and
pane widths. This draft keeps the change to the unread toggle and reuses
the existing storage helpers.

### Testing

The mock Desktop regression passed: enable the toggle, navigate to
Agents and back, reload, then repeat with the toggle disabled. Both
choices were restored. This exercises the UI with a synthetic fixture; a
native app restart was not tested.

The full local repository CI gate is pending. Keep this PR in draft
until validation completes.

### Before and after behavior

Same synthetic Desktop fixture in Chromium, 1280×720, on macOS.
**Before:** production source from
`41c5ace93740261ee5a5d962c2a17e8d846c2c1b`. **After:** PR head
`b50c1a06d53fdc080e7aae6ee9aa2e9ad6b60a82`. The capture test asserted
the displayed toggle state before taking each screenshot.

1. Enable **Show unread only**, open **Agents**, go back to **Inbox**,
and reopen **Inbox options**.
2. Enable the toggle again, reload the page, and reopen **Inbox
options**.

Before the fix the toggle is off after both journeys; after the fix it
stays on. These are mock-backed UI captures, not a native Desktop
restart or live relay test.

| Journey | Before: resets to off | After: stays on |
| --- | --- | --- |
| Inbox → Agents → back |
![before-navigation](https://raw.githubusercontent.com/block/buzz/8a4d4845a51ab096d573f96bd528aa0e7abb1dc9/pr-7672--before-navigation.png)
|
![after-navigation](https://raw.githubusercontent.com/block/buzz/8a4d4845a51ab096d573f96bd528aa0e7abb1dc9/pr-7672--after-navigation.png)
|
| Enable → page reload |
![before-reload](https://raw.githubusercontent.com/block/buzz/8a4d4845a51ab096d573f96bd528aa0e7abb1dc9/pr-7672--before-reload.png)
|
![after-reload](https://raw.githubusercontent.com/block/buzz/8a4d4845a51ab096d573f96bd528aa0e7abb1dc9/pr-7672--after-reload.png)
|

Generated with Codex

Signed-off-by: Diem Nguyen <diem@squareup.com>
2026-09-22 15:55:55 -07:00
Wes
c213d90d5c fix(desktop): bound startup request bursts and recover quota refusals (#7790)
Prepared by Brain on Wes’s behalf.

## Summary

Reduce startup request amplification and let quota-refused background
work recover without changing relay quotas or bypassing the existing
Send gate.

- Coalesce nest regeneration into one worker plus the latest pending
generation. Keep the stale-write fence; bound the archive read so an old
workspace cannot strand newer work.
- Claim pending channel subscriptions before awaiting setup; cancel
removed/unmounted entries and preserve still-owned subscriptions across
reconnect handoff.
- Recheck extended cooldown deadlines before retry dispatch; distinguish
an explicit zero-second hint from a missing hint.
- Recover native finite/unread queries within their original caller
deadline and a three-retry budget, preserving verified unique events,
complete filters and terminal errors.
- Pace renderer initial/live-CLOSED REQs at one per 250ms: visible
channel and explicitly interactive TTS/read-state/observer owners first,
then live-only auxiliaries, then background history. Publication and
finite queries bypass this queue. Reconnect replay now promotes explicit
interactive owners alongside the visible channel with stable
registration-order ties; its existing capped batches and cooldown remain
intact.

**Scope at `867495426`:** 39 source/test files, including 15 production
files (+665/-235) and 24 test/fixture files. The diagnostic recorder,
HTTP instrumentation, trace launcher and raw captures are deliberately
excluded. No profile reset, grouping, quota increase or new global
scheduler.

## Before / after evidence

These are observations from instrumented, human-operated development
Tauri/Vite runs against a production relay, plus explicitly identified
controlled regressions—not a packaged-release startup benchmark.

| Measurement | Before | After | Evidence boundary |
| --- | --- | --- | --- |
| Archive snapshot requests (kind 13535) | 290 | 3 | Recorder-only
baseline → nest-only intermediate candidate; unequal 67.436s / 39.273s
captures and hydration differences |
| Archive responses: HTTP 200 / 429 | 277 / 13 | 3 / 0 | Same
baseline/nest-only captures; not a claim that all quota pressure
disappeared |
| Successful archive responses’ advertised content-length sum |
21,208,505 bytes | 229,695 bytes | Header values, not actual transferred
bytes |
| 154-channel StrictMode replay | 308 REQs | 154 REQs | Controlled real
React hook → real session → mocked Tauri transport regression |
| Add one channel while 154 setups are pending | 309 REQs | 155 REQs |
Same production-boundary regression; ordinary mount remains 154 → 154 |
| Broad renderer request burst / spacing | 110 broad REQs within
10.766ms in pre-drain capture | 250.061ms minimum / 252.131ms median
between broad REQ writes in final longer capture | Different attended
runs; pacing rather than a universal request-rate cap |
| Native unread query outcomes | Baseline: 8 EOSE / 146 quota CLOSED out
of 154 queries | Final longer run: all 156 observed queries reach EOSE;
eight refused queries each recover after two refusals | Different
runs/workloads; query completion, not badge rendering |

### Timing metrics

| Observation | Measured value |
| --- | ---: |
| Baseline’s two accepted chat EVENT socket writes → positive OK |
173.340ms / 85.556ms |
| Nest-only intermediate candidate’s two accepted writes → positive OK |
277.854ms / 78.775ms |
| Final candidate, short run, accepted chat writes → positive OK |
80.636ms / 77.506ms |
| Final candidate, longer run, accepted chat writes → positive OK |
120.712ms / 86.895ms |
| Last initial EOSE among 163 observed broad channel registrations,
longer run | 64.550s after trace start |
| Last EOSE among 156 observed native unread queries, longer run |
73.972s after trace start |
| Native retry waits after 3s then 0s hints, longer run |
3000.7–3002.8ms, then 2001.5–2002.6ms |
| Renderer finite-query retry after a 2s hint, longer run | 2061.365ms |

Both Sends in each final run were accepted, with no recorded negative
OK. Every quota-refused query in the longer run subsequently completed
the same query. Its two Sends happened early (17.878s and 21.655s), not
near the end. These small samples do not support a percentage latency
improvement.

**Tradeoffs and limits**
- 296 background subscriptions imply about 74s of paced registration
before extra cooldowns/high-priority work. The longer capture observed
163 registrations; it did not independently enumerate the expected
membership set or record a queue-empty marker.
- This is not a global 4-REQ/s cap: finite queries, native requests and
reconnect replay are outside the drain and still share relay quota.
Quota refusals recover rather than disappear.
- Socket-write → OK starts after composer submission and any publisher
gate wait. No click-to-visible, launch-to-usable or badge-paint latency
is claimed.
- Native EOSE is not proof of command return, classification, React
state application or visible unread badges. Existing per-channel kinds,
since floors, limits, h-less routing, unread lease/scope guard and
eight-query concurrency remain intact.
- The attended measurements belong to the instrumented candidate, not a
fresh release run of this fixes-only commit. At the initial packaging
checkpoint (`1bf3d060`), eighteen of twenty files were byte-identical;
the other two removed diagnostic gate calls and isolated the nest test’s
temporary paths. The subsequent interactive priority/reconnect and
readiness fixes have controlled hook/session/browser evidence, not
another attended voice/performance run.

### Evidence provenance (captures retained locally, not uploaded)

| Capture | SHA-256 |
| --- | --- |
| Baseline `STARTUP_20260921T194814Z_98811.jsonl` |
`f4f0a89ad9aea91084bd6a7432510488166de4cbc906600d88c26d557f380994` |
| Nest-only `STARTUP_20260921T202610Z_28642.jsonl` |
`ce517b02a3f717c36d96b36bb8616e8fa80b03b12b356bacfd40475925ea3f3e` |
| Final short `STARTUP_20260922T013628Z_13506.jsonl` |
`bfea6f3cf80cd7b7e4e676632a3df2f16271fade5bfa0edec72b7bcbbe7839ff` |
| Final longer `STARTUP_20260922T014637Z_15846.jsonl` |
`f4abece613d108c4c2fe3cb808e633a0486bdc9993ca92323822dc00c3fae6ec` |

Final instrumented build fingerprint:
`b0b4fc27d793c3e643f4e10289b5afdb4620ee2e13a8b33fa0595dd3417fd5ca`. All
observed dropped-record counters in these final captures were zero.
Source manifests and offline correlation analyses are retained locally;
no raw captures, profile data or agent sessions are part of this PR.

## Validation

Current head: `86749542657a60abdfe82eed5cc576d3e162fe5d`, based on
`77729abfb692b25a0f4ec4a69add86af2e32c0dd`.

Review follow-ups:
- `8842dbd8`: explicit interactive TTS admission using the existing
main-window owner, independent of companion/timeline selection.
Real-hook/session/IPC coverage with 296 cold owners proves initial and
CLOSED priority, unchanged filters/cooldown/pacing, background progress
and deduplicated speech.
- `8cb088106`: reconnect preserves interactive priority even with no
visible channel. Stable ordering among foreground ties; real
hook/session reconnect regressions cover cooldown, capped batching, all
297 owners, deduplication and unmount cancellation. Independent reviewer
mutation restored the prior replay implementation, failed both reconnect
rows, then passed the restored candidate.
- `68e76d7c`: read-state and observer reuse interactive priority. Stop
and ModelPicker wait on the existing observer owner inside send
callbacks, after installing result listeners/deadlines. No new owner,
timer or scheduler; inherited CLOSED/fallback/error semantics remain,
rather than a new strict EOSE guarantee. The real Stop panel browser
probe holds observer IPC across the click and checks
observer-before-control order and correlated outcome. ModelPicker
component ordering is source-reviewed only.
- Mock live admission now replays matching stored history with
per-filter time/limit constraints, deduplication, untagged auxiliary
channel fallback and authoritative explicit h-tags. Kind-specific
fixture admission waits and retry-resolving hover actions fix
startup-dependent test setup without weakening event/badge assertions.
- `48e9c0dad` and `867495426`: steady-state scroll-prepend setup
observes live admission, subsequent head dispatch, successful/idle query
completion and initial bottom geometry. Phase-2 genuine-growth and ≤32px
anchor assertions are unchanged. This isolates steady-state prepending:
it **does not fix or certify production early-scroll/head-reset
behavior**.

Current validation:
- Normal push hooks at `867495426`: **6,582/6,582 renderer tests**, zero
failed/skipped/cancelled; lint/static checks, typecheck and differential
file-size gate passed. Native source is unchanged from the earlier
checkpoint; no new native run is claimed for these renderer/test
follow-ups.
- E2E build passed at `48e9c0dad` plus the exact subsequently committed
test-only delta (spec blob `469be2eed868c677ba80b859cc871208000fee60`).
The completed affected-smoke run passed **153/153** across
agent-control, badge, entity links, messaging, profile status and scroll
history; the full scroll file additionally passed twice (**36/36**). No
full CI-smoke pass is claimed from this six-file local run.
- Pinky independently reviewed production and test follow-ups, including
the final scroll spec blob `469be2eed868c677ba80b859cc871208000fee60`,
with no blocking finding. Review is source evidence, not an independent
test run.
- **Not merge-ready:** new-head required CI must complete, exact-range
security review requires authorization, and GitHub currently reports
REVIEW_REQUIRED; fresh exact-head reviewer approval is still needed. No
approval, dismissal or merge performed. No new packaged/native audio,
launch-to-usable or badge-paint observation is claimed.

Initial fixes-only checkpoint validation
(`1bf3d06024d284a63326fdd353baced0842aeb81`):

- Pre-commit formatting hooks passed; worktree clean.
- Renderer package hooks: 6,570 tests passed, zero
failed/skipped/cancelled; lint/static checks, TypeScript and
differential file-size gate passed.
- Native package hook passed: default and mesh-enabled all-target
Clippy, then the complete native workspace test suite. App library:
3,222 passed / 0 failed / 19 ignored; terminal/integration suites
passed. The real nest trigger/stalled-body test passed on macOS.
- Independent whole-change source review: no blocking findings at this
commit; all 20 file hashes matched the committed blobs and
review/validation manifests.
- Regression coverage includes real hook/session cancellation,
StrictMode/membership re-entry, reconnect ownership, extended/zero
cooldowns, burst pacing, auxiliary readiness (including EOSE before IPC
settles), native socket refusal/retry/deadline/cancellation, unread
fetch/classification and real nest trigger/HTTP/file persistence.
- Earlier isolated renderer mutations that started readiness before
enqueue or removed auxiliary priority each failed the real presence
regression; restored controls passed. No native mutation claim.
- Nest fixture uses explicit app-data/nest paths instead of assuming
Windows known-folder APIs obey HOME/APPDATA. Windows runtime execution
is not claimed; CI/build results remain separate.

Originating conversation:
buzz://message?channel=46ced308-71df-495d-a144-4ebf08334703&id=bc43c3422a3b0b2141268d0fa149b898421c824992b662aed8b73c4eee50404f

No approval or merge recommendation is implied by opening this PR.

---------

Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
2026-09-22 18:31:44 +00:00
Wes
a3117a4762 fix(desktop): register macOS badges for new and existing installs (#7783)
## Summary
- Request macOS badge authorization alongside alerts and sounds.
- Repair existing bundled installations at startup only when native
status is `Authorized` and `badgeSetting` is `NotSupported` (the
omitted/unregistered interaction). Leave user-disabled badges, denied
notifications, new users, and provisional/ephemeral authorization
untouched.
- Reuse the native query/request path in one background startup
operation. No frontend changes, read-state resets, persisted migration
flags, timers, or in-process retries.

## Validation
- Full `just ci` passed at implementation head `e6c4bc640`, before the
test-seam extraction and additional regression coverage.
- Native patch head `8ab8cf6d6`: normal pre-commit/pre-push hooks
passed, including macOS Tauri clippy (default and mesh-llm), complete
Tauri test suite (3,208 passed, 19 ignored), and repository file-size
gate.
- Regression coverage: all five native authorization statuses × all
three badge states; request exactly once only for Authorized +
NotSupported; settings/request failure propagation; startup wiring;
complete authorization options.
- Mutation verification: removing the startup task and removing the
Badge option each fail the corresponding regression.
- Isolated bundled native app on macOS 26.6.2: requesting Alert | Sound
produced Authorized + badge NotSupported; requesting Alert | Sound |
Badge then produced badge Enabled with no error. A direct AppKit Dock
label of 7 was visibly rendered in the isolated app.
- Independent review found no remaining code blocker at final head.

## CI repair (`f9a6ff2b0`)
- The original browser smoke failure was a 500ms startup assertion
racing loaded-runner rendering. Hosted failure snapshots contained all
14 expected rows; the unchanged test also reproduced the failure locally
with 6× CPU throttling.
- Reuse the existing one-shot mock channel response gate in the affected
test. Wait for startup settlement, defer a second real revalidation,
mutate display state while pending, release, then assert after query
settlement. No production changes in this follow-up.
- All 10 sidebar snapshot E2Es pass at clean head `f9a6ff2b0`; the
repaired case also passed 3/3 CPU-throttled runs before commit.
Temporarily removing display preservation or persisting the display
mutation each caused the corresponding assertion to fail; both mutants
were restored.
- Normal pre-commit/pre-push gates passed, including desktop
checks/typechecking, all 6,508 desktop tests, and the file-size gate.
Independent artifact/source review found no blocker in the repair.
- New hosted CI is running:
https://github.com/block/buzz/actions/runs/35633308957. This is not a
claim that hosted CI is green. Exact-range security review still
requires organization-member authorization.

## Runtime limitation
This fixes a verified missing authorization registration and provides an
automatic existing-install path. It is **not yet proof that the reported
blank Dock badge in the released Buzz app is fully resolved**: patched
bundled Buzz startup and unread-to-Dock recovery have not been
exercised. The isolated app is native API evidence, not a replacement
for that release smoke test. Installed Buzz and its read state were not
modified.

Before release, verify an existing Alert/Sound-only install gains the
badge setting on launch and renders an unread Dock badge; verify a
user-disabled badge remains disabled.

---------

Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-09-21 18:12:38 +00:00
Wes
5079c770fe fix(workflows): make deletion persistent and retryable (#7735)
## Summary

Fix workflow deletion leaving its definition queryable, and make public
deletion acceptance agree atomically with executable/definition state.

- Delete the executable workflow and soft-delete its definition under
the existing coordinate lock, in the same transaction as kind-5
acceptance and mention indexing. Failed deletion leaves no public
accepted event; identical retries can succeed. Legacy already-stored
failed requests still repair.
- Choose one committed dispatch/audit owner for concurrent identical
requests. Completed duplicates remain silent. Preserve owner/community
isolation, newer revisions, orphan cleanup, and intentional backdated
recreation.
- Align Desktop confirmation and architecture documentation with
recreation semantics. Add deterministic PostgreSQL, live WebSocket, and
Desktop regressions. No schema migration or new retry/delivery
subsystem.

## Current validation

Head: `40d86ddab0853ffb292316653b22c3fd4f983f0c`.

- **Exact clean head:** live relay workflow-deletion E2E **4/4 passed**.
All applicable normal pre-push hooks passed: Rust unit suites, full
Desktop unit suite, Desktop lint/typecheck, Tauri clippy/tests, and
file-size/branch checks. No hook bypass.
- **Pre-commit implementation tree, then committed at this head:** full
discovered DB/relay PostgreSQL lane **351/351 passed**; built Desktop
deletion Playwright smoke **3/3 passed** (success, failure preserving
editor/draft, corrected confirmation copy).
- **Deterministic mutation evidence:** unchanged previous relay
`659b79282` fails both new regressions: concurrent identical requests
produce two normal/dispatch-owner responses, and rejected deletion
leaves one public event. Both controls pass. Fixed relay passes all
four.
- **Independent source review:** transaction/authorization/lock-order
and regression/cleanup lanes clear. Reviewers did not run tests
independently.

## Honest limits / merge gates

The broad pre-commit DB/relay/test-client selection passed
**1,215/1,216**; the untouched mesh-demo echo test returned HTTP 504
instead of 200. It remains outside this repair and is not counted green.
Initial hook attempts also exposed an untouched Desktop inventory
ordering assertion and unidentified ACP/agent test failures; standalone
package reruns and the final complete hooks passed without changing
those packages. Causes are not established.

The previous Desktop CI shard failed before tests on a Hermit `just`
download (HTTP 500). New-head CI/security review and reviewer
disposition remain required. No deployment, fresh interactive Desktop
session, full local `just ci`, or multi-replica runtime validation
claimed. Postgres/Redis validation used task-owned local containers,
fresh identities, and disposable fixtures.

## Compatibility

No automatic cleanup of existing workflows: repeating deletion repairs
definitions left by the old path. Canonical UUID coordinates and legacy
name fallback (`LIMIT 1` for duplicate names) are unchanged. Backdated
recreation remains intentional per [Tyler’s
decision](https://github.com/block/buzz/pull/7735#issuecomment-5729881295),
not an irreversible tombstone. Mention-index failure now rolls back the
complete deletion rather than best-effort accepting partial work.

Detailed reviewer disposition and evidence:
https://github.com/block/buzz/pull/7735#issuecomment-5737367825

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-authored-by: Mordecai <f314b6033b6e13df94c8e9e1187d3666b6b9ce54e66ee986e31cdaa909e8dcbc@buzz.block.builderlab.xyz>
2026-09-21 08:37:14 -06:00
Jordan Mecom
f18ea45698 Replace personal and internal data in desktop test fixtures (#7748)
Replace personal contributor identities, copied internal image content,
and production Databricks hostnames in desktop tests and mocks with
synthetic fixtures. The related path and mock-profile fixtures use the
same example identity, and the image regression still verifies reserved
layout space while its mocked request remains pending. Contributor
matching and configuration decoding behavior are preserved.

Checked on Blox: 36 targeted JavaScript tests, 30 focused Rust tests,
the Playwright image-height regression, TypeScript/Biome, formatting,
security/file-size gates, 6,508 desktop tests, the full Tauri test
workspace (including 3,204 main-crate tests), and desktop/web builds.

The full `just ci` run could not finish mobile checks because Blox
connections to `pub.dev` were reset and a locked dependency was missing
from its offline cache. The broader Rust unit run passed 16 lanes but
hit an unrelated catalog timeout in `buzz-agent`; that exact test passed
in isolation.

Signed-off-by: jm <jm@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-18 22:26:19 +00:00
klopez4212
a9dd4d3bd7 Fix desktop onboarding regressions (#7659)
## Summary
- restore neutral identity guidance icons and reveal-first private-key
copying
- keep runtime availability sections contiguous and restore primary
onboarding colors
- route API-key setup directly to Buzz provider configuration, including
slow discovery and signed-out states

## Testing
- `pnpm check`
- `pnpm test` (6,501 passed)
- focused Playwright onboarding coverage (API routing 3/3; visual flow
1/1)

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-09-15 10:30:05 -04:00
klopez4212
81fba7bda4 Refresh desktop onboarding flow (#7528)
## Summary
- apply the refreshed card layout across desktop onboarding
- update identity backup, recovery, and key presentation flows
- clarify AI connection choices and default model settings
- simplify the backup ceremony from creation through optional
verification
- bring the harness and provider setup flow forward with validation,
preserved state, and consistent navigation
- fix action alignment and stray card scrolling in the refreshed layout

## Validation
- pre-push desktop lint, typecheck, frontend tests, and native tests
passed
- focused onboarding Playwright coverage passed
- manually reviewed the refreshed onboarding and backup flow in the
native app
- just ci passed all sections except one unchanged process-cleanup
timing test; its exact rerun passed

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Signed-off-by: Clay Delk <clay.delk@gmail.com>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Clay Delk <clay.delk@gmail.com>
2026-09-15 05:58:16 -04:00
Salman Mohammed
78618804ec Configure ACP session scope per agent (#7578)
## Summary

PR #6732 taught the ACP harness to honor `BUZZ_ACP_SESSION_POLICY`, but
the desktop exposed that policy as one global experiment. Turning it on
changed every managed agent at once, even though conversation scope is
part of how an individual agent should behave. Some agents need
continuity across a channel, while agents such as validators may need
clean context for each thread.

This moves the choice onto the agent definition under Advanced as
**Conversation context**, with **Entire channel** and **Each thread**
options. **Entire channel** remains the default, so existing definitions
keep their current behavior.

The policy now travels through create and update IPC, returned agent
summaries, persona events, snapshots, teams, and community catalog
copies. Launches pass the effective definition value to the harness.
Editing a running agent uses the same lifecycle as a model edit: it
shows the restart-required state, leaves the deployed process untouched,
and applies the new policy on restart.

The global Thread Scoped ACP Sessions experiment and its desktop
command/state have been removed because the setting no longer has a
useful global meaning. There is intentionally no migration for the old
preview override. Users who enabled it can choose **Each thread** for
the agents that need it.

Unknown or null policy values degrade to **Entire channel** across
stored agents, persona events, and catalog projections. This keeps
forward-version data from dropping complete agent records.

### Related issue

No matching issue found. Builds on #6732.

### Testing

- Focused Playwright create flow selected **Each thread** and verified
`create_persona` received `sessionPolicy: "thread"`.
- Focused Playwright catalog flow added a thread-scoped agent and
verified the local copy stored `session_policy: "thread"`.

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-09-11 20:38:41 +00:00
Taylor Ho
ec11f8e2f3 fix(avatars): scale agent squircles from normalized paths (#7307)
## Overview

**Category:** fix

**User Impact:** Agent avatars and empty agent-team placeholders now
keep the same intentional squircle silhouette at every size on desktop,
while human avatars remain circular.

**Problem:** Desktop agent avatars used percentage-rounded rectangles,
which made the silhouette vary by surface and required each avatar
implementation to recreate the shape. Empty team placeholders also drew
a regular CSS border inside the clip, leaving straight inset edges that
looked like a cropped square. **Solution:** Reuse one normalized SVG
clip path across desktop artwork. Empty team placeholders use a
separately clipped squircle outline behind the clipped surface, so both
the fill and outline follow the canonical silhouette.

> **Scope correction:** Mobile is deferred from this PR because a real
Flutter capture could not be obtained reliably in the current build
environment. Previously attached mobile images were hand-composed
synthetic layouts, not captures from the running app; they have been
removed and must not be treated as validation. The mobile implementation
is being reverted in one follow-up commit so it can be reapplied and
verified separately.

### Related issue

Follow-up to #7106.

## Changes

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

**desktop/src/features/agents/ui/AgentCreationPreview.tsx**
Applies the shared squircle clip to agent creation preview images and
fallbacks.


**desktop/src/features/agents/ui/AgentSessionToolItem/CompactMessageSummary.tsx**
Uses the shared agent-avatar silhouette in compact session summaries.

**desktop/src/features/agents/ui/IdentityInitialsAvatar.tsx**
Clips agent initials with the normalized squircle instead of a
percentage radius.

**desktop/src/features/agents/ui/TeamIdentityCard.tsx**
Uses the shared squircle for agent identities and gives empty teams a
separately clipped squircle outline instead of a regular inset border.


**desktop/src/features/agents/ui/activityRenderClasses/UserMessageBubble.tsx**
Keeps agent avatars in activity message bubbles on the canonical
silhouette.

**desktop/src/features/home/ui/InboxListPane.tsx**
Applies the shared clip to agent avatars in the inbox list.

**desktop/src/features/home/ui/InboxMessageRow.tsx**
Applies the shared clip to agent avatars in inbox message rows.

**desktop/src/features/huddle/components/ParticipantList.tsx**
Uses the normalized squircle for agent participants while preserving
circular human avatars.

**desktop/src/features/messages/ui/MessageRow.tsx**
Clips agent message avatars through the shared definition.

**desktop/src/features/messages/ui/SystemMessageAvatars.tsx**
Uses the canonical agent shape in system-message avatar groups.

**desktop/src/features/messages/ui/TypingIndicatorRow.tsx**
Uses the canonical agent shape in typing indicators.

**desktop/src/features/profile/ui/ProfileAvatar.tsx**
Scales the same squircle to the larger agent profile avatar.

**desktop/src/features/profile/ui/SelectedRecipientChip.tsx**
Uses the shared shape for selected agent recipients.

**desktop/src/features/projects/ui/IssueAssigneesRow.tsx**
Applies the agent squircle to issue assignees without changing human
avatar geometry.

**desktop/src/features/projects/ui/ProjectCards.tsx**
Uses the canonical shape for agent avatars on project cards.

**desktop/src/features/projects/ui/ProjectEntityListRow.tsx**
Uses the canonical shape for agent identities in project entity lists.

**desktop/src/features/projects/ui/ProjectsActivityFeed.tsx**
Uses the shared clip for agent avatars in project activity.

**desktop/src/features/projects/ui/ProjectsOverviewRail.tsx**
Uses the shared clip for agent avatars in the project overview rail.

**desktop/src/main.tsx**
Mounts the single shared SVG clip-path definition once for the desktop
application.

**desktop/src/shared/lib/cn.ts**
Registers `rounded-squircle` in the border-radius class group so
shadcn-style `cn(...)` composition resolves it against `rounded-full` by
normal last-class precedence.

**desktop/src/shared/styles/globals/utilities.css**
Registers the reusable Tailwind `rounded-squircle` utility and
references the normalized SVG clip path without agent-specific CSS
selectors.

**desktop/src/shared/ui/AvatarClipPaths.tsx**
Owns the reusable normalized object-bounding-box `rounded-squircle` path
so all desktop sizes reuse one definition.

**desktop/src/shared/ui/UserAvatar.tsx**
Routes the existing squircle shape option through the shared SVG clip.

**desktop/tests/e2e/agents.spec.ts**
Verifies team facepile and empty-team placeholder surfaces/outlines use
the canonical clip without a rectangular border.

**desktop/tests/e2e/messaging.spec.ts**
Verifies agent message and profile avatars reuse one clip definition and
scale across rendered sizes.

</details>

## Reproduction steps

1. Run the desktop app and open a channel containing both agent and
human messages.
2. Compare agent avatars in the message list, inbox, huddle
participants, projects, and profile panel. Agent images and fallbacks
should share one smooth squircle silhouette at each size; human avatars
should remain circular.
3. Open **Agents** and inspect an empty agent-team card. Its placeholder
fill and outline should both be smooth squircles, with no straight inset
cropped-square edges.
4. Open an agent profile from a message and confirm the larger profile
avatar keeps the same proportions as the smaller message avatar.

## Testing

- Desktop Biome check passed for the changed empty-team component and
Agents E2E spec.
- Desktop typecheck and differential file-size check passed.
- Full desktop unit suite passed: 6,453 tests.
- Full Agents integration spec passed: 37/37, including the empty-team
outline regression.
- Post-rebase pre-push gates passed at `304b159a2`: desktop checks,
TypeScript typecheck, differential file-size gate, and all 6,453 desktop
unit tests.
- The shadcn-aligned `rounded-squircle` registration passed changed-file
Biome, desktop TypeScript/build, and pre-push desktop checks at
`27e0c12d3`; the full desktop unit suite passed 6,459/6,459.

## Screenshots

Screenshots are temporarily omitted. The previous captures used the
desktop E2E mock bridge and were not evidence from a live running app,
so they were removed.

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
2026-09-11 06:08:44 -10:00
Taylor Ho
092c6a7277 fix(markdown): align mention chip wrapping (#7501)
**Category:** fix
**User Impact:** Human and agent mentions now break across lines with
the same cloned chip treatment as repository and permalink chips while
preserving the conversation text rhythm.

**Problem:** Profile-backed rendered mentions sat inside an
`inline-flex` popover trigger, unlike entity chips, so the wrapper
interfered with true inline fragmentation. The browser-layout test
measured text-range rows rather than the painted chip rectangles,
allowing touching decorations to pass as “separate” fragments.

**Solution:** Keep the profile trigger interactive but override its
layout to true `inline`, then give mention fragments 18px computed
leading inside the message’s 20px prose rhythm. Chromium paints each
fragment at 17px and advances it by 20px, leaving a visible gap between
cloned rounded rectangles. The browser test now measures the chip’s own
`getClientRects()` and asserts fragment count, height, gap, and step;
entity links retain their existing 22px leading.

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

**desktop/src/features/profile/ui/UserProfilePopover.tsx**
Allows inline consumers to override the trigger wrapper’s layout without
changing other profile-popover call sites.

**desktop/src/shared/styles/globals/markdown.css**
Keeps one shared wrapping-chip mechanic and gives mention decorations
enough room to separate visibly within 20px prose.

**desktop/src/shared/ui/markdown.test.mjs**
Pins both rendered mentions and entity links to the shared wrapping-chip
contract.

**desktop/src/shared/ui/markdown/MarkdownMention.tsx**
Makes the profile-popover trigger truly inline so the nested mention
chip can fragment with surrounding prose.

**desktop/src/shared/ui/mentionChip.ts**
Keeps `wrapping-inline-chip` as the single contract for fragmenting
decorated chips.

**desktop/tests/e2e/mentions.spec.ts**
Measures the painted chip rectangles, requires a positive fragment gap,
and verifies the inline trigger remains mouse- and keyboard-operable.

**desktop/tests/e2e/navigation.spec.ts**
Keeps a wrapped repository chip as the control, asserting its existing
22px line height and fragment advance.

</details>

## Reproduction steps

1. Open a channel in Buzz Desktop using dark theme.
2. Send a message containing a human mention and another containing an
agent mention; both chips should remain aligned with adjacent text on a
20px line.
3. Render a collision-qualified mention in a narrow message width; it
should break into separately decorated fragments exactly like another
wrapping chip, while each fragment follows the 20px prose rhythm.
4. Render a long repository or permalink chip in the same constrained
width; it should retain its roomier 22px fragment spacing.

## Screenshot

The dark-theme production renderer shows the real qualified label (`bob
(npub1hv3…tpuc)`) at an 8rem width. The two lines now paint as visibly
separate rounded fragments rather than one continuous rectangle.

![Qualified human mention wrapping into two visibly separate rounded
fragments in the dark-theme Buzz
timeline](https://github.com/user-attachments/assets/ee6e7be5-937e-4022-9c82-cf71f8203470)

## Validation

At commit `2b063e1b4ade30e11f1616269ad4ba4190366885`:

- Pre-push desktop gates — file-size check, Biome/checks, typecheck, and
6,483 unit tests passed
- `pnpm --dir desktop build` — passed
- Focused Playwright coverage for single-line agent mention, single-line
human mention, wrapped qualified mention including keyboard profile
activation, and timeline mention click — 4 passed
- `git diff --check` — passed

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <rizz@agents.buzz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-09-10 22:42:10 +00:00
cynfria
cec5c8fd92 fix(desktop): let inbox title and message author names truncate under narrow panes (#7550)
## Summary

Fixes two instances of the same dead-truncate pattern in the desktop
app, where a flex item's implicit `min-width: auto` prevented `truncate`
from engaging, so long text painted over adjacent controls instead of
ellipsizing:

- **Inbox detail title** (`InboxDetailPane.tsx`): the clickable
context-title button sized to its text instead of shrinking with the
pane, overlapping the header controls (open-in-channel, members, huddle,
more menu). Fixed by adding `max-w-full`.
- **Message author names** (`MessageHeader.tsx` /
`UserProfilePopover.tsx`): the `UserProfilePopover` inline-flex trigger
wrapper refused to shrink below the name's nowrap width, running long
author names under the hover action bar and off the pane edge. Fixed by
adding a `triggerClassName` prop to `UserProfilePopover` and passing
`min-w-0 max-w-full` at the author call site.

Two other suspected instances (project file breadcrumb, drafts pane
title) were stress-tested and already truncate correctly — no change.

### Related issue

N/A — none found.

### Testing

- New Playwright regression tests for both fixes
(`inbox-title-overlap.spec.ts`, `message-author-overlap.spec.ts`,
registered in the smoke project), each proven to discriminate: they fail
with the fix reverted (real measured overlap) and assert the ellipsis
actually engages with non-zero title width, so they can't pass
vacuously.
- Typecheck, lint, and full desktop unit suite green (pre-push hooks);
full desktop e2e smoke suite run earlier: 1402 passed, 3 pre-existing
unrelated failures (each fails identically with the fix reverted).

**Inbox title — before** (long title paints under the header controls):

![Inbox title before: title text overlaps the header control
icons](https://github.com/user-attachments/assets/d5a10f98-114f-4466-8012-468616b82807)

**Inbox title — after** (truncates with ellipsis, controls stay clear):

![Inbox title after: title truncates with an ellipsis before the
controls](https://github.com/user-attachments/assets/80520f7c-1120-4dbc-929d-1ef5eceb874b)

**Author name — before** (long name runs past the header row edge):

![Author name before: name glyphs bleed past the action
bar](https://github.com/user-attachments/assets/be9743ae-ef51-4db0-8c46-a0649e98f0d5)

**Author name — after** (clean cutoff):

![Author name after: name truncates cleanly inside the header
row](https://github.com/user-attachments/assets/24cc0412-ca99-40c4-add6-13380b3ae065)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: cynfria <yescynthia@gmail.com>
Signed-off-by: Tree Trunks <6ba22921d9dc2ad0aa6ecdf63787ddd24726e266d866da31af69f2e4e146ace5@buzz.block.builderlab.xyz>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Tree Trunks <6ba22921d9dc2ad0aa6ecdf63787ddd24726e266d866da31af69f2e4e146ace5@buzz.block.builderlab.xyz>
2026-09-10 10:00:15 -07:00
Logan Johnson
ad9591c43b fix(desktop): order unnamed roster members by full canonical npub (#7503)
🤖

## Summary

- Channel members appear in the Members sidebar. A member who has never
set a display name is listed under an abbreviated form of their public
key (npub), and the sidebar previously sorted those unnamed members by
that short label. Short labels are not unique — different keys can share
one — so the order of unnamed members could look arbitrary or unstable.
Unnamed members now sort by their full public key, so the order is
deterministic.
- When two members display the same name, the previous tiebreak was
membership order (who joined first), which is not visible to a reader
and can shift as roster data loads in. The tiebreak is now the full
public key, so identical display names always land in the same order.
- Nothing gets noisier on screen: the full key is used only for sorting,
and the sidebar still shows the compact abbreviated form. Priorities are
unchanged — authored (custom) names still outrank fallback labels, and
role/current-user grouping still applies.
- Scope is the desktop app's Members sidebar and member management: the
two existing sort comparators. Mobile and other lists in the app are
untouched.

### Related issue

Based on #7495 (introduced the abbreviated npub labels this follows up
on). The original five presentation PRs remain independently reviewable.
No closer duplicate found.

### Testing

- 6469 desktop unit tests, typecheck, and check pass.
- The 3 existing consumer-seam E2E tests still pass; a new E2E test
asserts the sidebar lists unnamed members in full-key order, with
fixture members deliberately inserted in the opposite order so incoming
membership order cannot mask the sort.
- Negative check: reverting only this change makes the new ordering
assertion fail, so it genuinely binds the new sort.
- CI has not run on this PR yet.

Buzz provenance: channel 1f0e4a3d-7e01-4efe-bb16-843b357f85c9 / task
340c3de9b27dbedb8453c0c7652220f9080d30fcc70a7c4f6e27fdd4fa378056

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-09 13:32:10 -07:00
Logan Johnson
2226b6f95b fix(desktop): npub identity controls across profile, agents, and workflows (#7489)
🤖
## Summary

Building on #7488's npub foundation, this PR finishes the identity
display change for the controls where you actually manage people and
keys: profile, settings, agent access, and workflows. Everywhere in
these surfaces, an identity key shows — and copies — as its canonical
npub (npub is the human-readable encoding of a Nostr public key: the
compact `npub1j57...fjmv` form where space is tight, the full npub where
the whole key matters), and accepts npub as input.

After this change:

- Profile panel: the public-key row and the managed-by / declared-owner
copies show the full npub. If a key can't be encoded, you see
"Unavailable" with no copy button — never a raw or partial key.
- Settings: the identity card shows and copies the npub. The
hosted-communities account identity derives from the bound key
(`pubkey_hex`) — the same authority as the mismatch gate and hosted
operations — so the display can never disagree with what the app acts
on; an unusable hex falls back to a neutral label instead of rendering
the unverified server npub. The connected claim and a community's
Connect action require that same usable bound key to match the local one
— with no usable binding the card cannot claim connected or start
Connect, while the community list, linking, and delete/rebind recovery
stay available.
- Hosted create/onboarding: the account and device identity rows in the
create flow and owner onboarding derive from the same authoritative
fields (bound key / local key), with the same neutral fallback;
readiness requires a usable bound key that matches the local one.
- Respond-to allowlist (controls who may respond to an agent): entries
can be typed or pasted as hex or npub; both spellings of the same key
are recognized as one entry and dedupe. Search results, chips, and
remove buttons use the compact npub.
- Workflow key fields: to/from keys display as npubs in the form and
save back as canonical hex. Templates like `{{trigger.author}}`, roles,
and free text pass through untouched; placeholders accept both
spellings.
- Recipient and agent dialogs: the verify popover is npub-only (the
raw-hex line is gone); denied-membership screens never show a raw key.
- The Rust-side truncated display name (used for native surfaces) shows
the same compact npub, so those surfaces match the web UI.

Internal representation is unchanged: keys are still stored, sent, and
matched as canonical 64-character hex — npub is a display and input
spelling, normalized to hex at the boundary, so existing data and
integrations keep working. Bound-key usability and comparison use one
normalized form (trimmed, lowercased, 64 hex characters; npub rejected),
so padded or mixed-case spellings of the same key match. Display names,
private keys, and event IDs are untouched.

## Details

- `respondToAllowlist` / `RespondToField`: npub entries normalize to
canonical hex; cross-form dedupe; compact npub in rows and chips;
direct-add accepts npub and stores canonical hex.
- `workflowFormTypes` / `WorkflowStepCard`: hex → npub for display, npub
→ canonical hex on save; templates, roles, and free text pass through in
both directions (roundtrip-tested).
- `UserProfilePanelFields`, `ProfileSettingsCard`,
`HostedCommunitiesSettingsCard`, `MembershipDenied`,
`SelectedRecipientChip`, `AddAgentToChannelDialog`: npub display and
copy; invalid keys → "Unavailable" with no copy; hosted identity rows
derive from the bound `pubkey_hex` (create/onboarding rows from the
bound and local keys), never the unverified server npub;
connected/readiness/Connect gates use the same usable-bound-key
predicate, and the settings Connect invocation callback re-checks it
before starting.
- `src-tauri/src/commands/identity.rs`: `truncated_display_name`
compacts to the first 8 + last 4 characters of the npub (above a 12-char
threshold), mirroring `truncateNpub`.
- e2e: profile key rows and clipboard polls assert npub forms and
raw-hex suppression; the display-screenshots spec pins the npub-only
popover; hosted specs drive the real settings card, create flow, and
onboarding rows through their real providers, and the unlinked/npub-only
identity cases assert no connected claim and no Connect action.

### Related issue

- Fixes: N/A. No separate issue; the related work is the stack below.
- Stack: builds on #7488 (shared npub foundation), now merged; this PR
is rebased onto main and stands on its own.

### Testing

At head `303c90ffa` (base: main `bfc38485`; 24 files, +1125/−146):

- Focused unit suites (respondToAllowlist, workflowFormTypes,
hostedCommunityApi bound-key helpers) green; mutation-checked — dropping
allowlist canonicalization fails the dedupe case, and dropping bound-key
normalization fails the npub-in-hex and padded same-key cases.
- Full desktop unit suite 6,477/6,477, `desktop-typecheck`,
`desktop-check` (formatting fixed narrowly with `biome check --write` on
the touched files only), and a fresh E2E build at the current head; the
add-community + hosted-communities-settings specs 18/18 and onboarding
integration 69/69 on a fresh dedicated port, with focused new-case runs
4+4 covering padded same-key (ready, Connect kept — no false rebind) and
npub-in-hex (neutral label, recovery, no Connect) across the settings
card, create flow, and first-community onboarding, plus the
unlinked-account settings regression asserting Connect cannot occur.
- `cargo fmt`/clippy (both feature sets) and `cargo test identity` (71
pass) passed at the earlier full-change head; since then, the only
production changes in this PR's delta are the hosted identity display
authority and its fail-closed bound-key gating/normalization above
(base-side fixes carry #7488's receipts) — every other change is
test-only.

### Task provenance

Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9`

Task:
buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-09 14:08:01 -04:00
Logan Johnson
ad2a84131e fix(desktop): npub identity displays for mention, member, and workflow surfaces (#7495)
🤖
## Summary

Every Buzz account is identified by a long public key. Before this
change, when someone had no display name, surfaces fell back to
inconsistent labels — mostly raw hex fragments like `abcd1234…wxyz`,
sometimes a generic role label with no key — so the same person looked
different from surface to surface, and nothing looked like an npub
address. This PR applies the npub identity foundation from #7488 to the
everyday surfaces: a person without a display name now falls back to the
same compact npub everywhere — `npub1xxxx…yyyy`, the human-readable
spelling of their public key (first 8 + last 4 characters of the full
npub) — across messages and mentions, reactions, huddles, member and
participant lists, the sidebar and channel activity, search, projects,
tray, notifications, and workflow surfaces.

- **Mentions and messages**: key-only mention chips render the compact
npub. Pasting a copied mention back still re-binds it byte-exactly to
the identity it declares, for both the new npub chips and legacy
hex-truncated chips copied by older clients — wrong, missing, or
tampered key qualification is rejected instead of silently degrading to
plain text.
- **Reactions and huddles**: huddle reaction events and the huddle
roster/participants render the compact npub for unnamed participants;
workflow reaction triggers describe authors with the same form.
- **Members and sidebar**: channel and community member lists,
add-member results and invites, the members sidebar, the
channel-activity popover, search, projects (assignees/reviewers/PR
panels), the tray menu, and desktop notifications all fall back to the
compact npub; titles and aria labels keep the machine-readable full
labels.
- **Profile labels**: panel/popover display names and owner handles fall
back to the compact npub (never raw hex) when there is no name;
linked-event (nevent) message metadata shows the npub-shaped author
fallback while the event lookup and event IDs are unchanged.
- **Workflows**: author-picker secondary labels, step destination keys,
and trigger-author references render compact npubs; event and blob IDs
keep their existing hex compacts (they are not identities).
- **Avatars stay distinct**: fallback avatars for key-only identities
derive initials from the key's tail, so prefixed role labels like
"Participant npub1…" no longer collapse every unnamed participant onto
the same initials; people with names keep their name initials.

Preserved exactly: display names and distinct avatars, internal hex keys
(storage/API forms unchanged), clipboard identity roundtrips, event/blob
ID compaction, private keys (no nsec path is touched), and nevent link
handling.

Scope: this PR changes what identity labels **display**, not identity
controls — profile/settings copy controls, the respond-to allowlist,
workflow key fields, and agent dialogs are the sibling slice #7489, and
the shared primitives (`canonicalNpub`, `truncateNpub`, the `<PubKey>`
gate, strict input parsing) come from the foundation #7488.

### Related issue

- Fixes: N/A. Searched existing issues/PRs for duplicates — none found;
the related work is the npub identity stack this slice belongs to.
- Base/dependency: stacks on #7488 (foundation) — this PR does not stand
alone on main.
- #7489 is a sibling slice on the same #7488 base
(profile/agent/workflow controls), not a dependency: this PR does not
require #7489, and #7489 does not require this PR — both only require
#7488.

### Testing

At exact head `4763cbeae1dd521309755e6d61f657324cb98667` (base:
`fix/desktop-npub-identity-d1a` @
`5f3a4a8111998c8aa41ad77cf66992bd1c85343c`; 71 files, +656/−189 —
production +277/−136, test support +379/−53):

- At this head: targeted `mentions.spec.ts` (1/1), the e2e build,
typecheck, and biome — green.
- 9 changed/related unit files: 100/100 green; typecheck, e2e build,
biome, and px text/truncation checks clean; huddle-roster focused run
green; channel-activity e2e 11/11; mutation checks confirm the fallback
wiring (removing it collapses shared initials and drops fallback rows).
- Known pre-existing local e2e failures, unchanged by this PR and
reproduced identically at the upstream merge-base: huddle-transcription
voice-menu attribution (25 pass / 1 fail) and the
`workflow-local-controls` 438px caret drift. Not claimed green locally.
- Update at head `236af9e6137386737e84d3a474d6bc808a704c50` (test-only
follow-ups `1143af345` + `236af9e6`): the `workflow-local-controls`
races were fixed in the test drivers, and the 438px diff was shown to be
a stale Darwin snapshot baseline (name-row enable switch already absent
and `message_posted` already MessageSquare at recording commit
`9390e11c9`) and refreshed — the focused screenshot test, including
keyboard/caret assertions, now passes locally (twice). The full spec was
not rerun after the snapshot refresh; the huddle-transcription item
above is unchanged.

Label/copy text changes are asserted by the e2e specs (`mentions`,
`mention-recipients`, `pubkey-display-screenshots`,
`huddle-transcription`, `channel-activity-popover`,
`workflow-local-controls`) rather than new screenshots; the screenshot
spec pins the compact npub text forms.

### Task provenance

Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9`

Task:
buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-09 14:07:24 -04:00
Logan Johnson
bfc3848558 fix(desktop): shared npub identity foundation (canonicalNpub, PubKey gate, strict parser) (#7488)
🤖
## Summary

Identity keys in the desktop app are displayed as raw 64-character hex.
A person's key shows up as something like `953d3363…` — unreadable,
impossible to recognize as the same identity on another screen, and a
hazard when copied by hand. Nostr (the protocol Buzz runs on) has a
human-readable spelling for identity keys — the `npub1…` form — but the
desktop app did not use it consistently.

This is the foundation of the desktop npub changes: it adds the shared
pieces every identity surface builds on, and two follow-up slices stack
directly on this branch — #7489 converts the identity controls (profile,
settings, allowlist, workflow key fields) and #7495 converts the
everyday display surfaces (mentions, member lists, sidebar, and other
name fallbacks).

After this change:

- The shared identity widget shows the compact npub form —
`npub1j57...fjmv` — instead of a hex prefix, everywhere it renders (for
example the owned-agent public-key row on a profile). Copying it puts
the full npub on the clipboard.
- Copy is a real interaction, verified end-to-end: both popover variants
put the exact canonical npub on the actual clipboard — never the raw hex
the popover also lists, never a truncation — and a portaled popover's
clicks no longer steal focus from the new-DM To-field mid-copy. Pointer
copy, a natural Space-then-Enter path, and inner/outer Escape are
covered.
- Anything that isn't a valid identity key fails neutrally: short or
corrupt values — including degenerate values that technically encode to
a checksum-valid npub but aren't real identity keys — show "Unavailable"
with no copy button, instead of a misleading value.
- Both valid npub spellings display: all-lowercase `npub1…` and
all-uppercase `NPUB1…` (Bech32, npub's encoding, permits either casing)
both render the same canonical lowercase npub. Mixed case is rejected by
the display path as written — `canonicalNpub` and the widget don't
case-normalize input — while input parsing (`parsePubkeyInput`) keeps
its trim-and-lowercase normalization and accepts mixed-case npubs; both
paths require the decoded payload to be exactly a 64-character identity
key.
- Identity-key input is strict on payload: an npub whose decoded payload
isn't exactly a 64-character identity key is rejected, matching the
validation the app's Rust side already applies to agent allowlists.

Intentional scope boundary: only surfaces that render through the shared
widget change here. Outer profile copy, settings identity cards, the
respond-to allowlist, and workflow key fields still show hex — they move
to npub in the controls follow-up (#7489). Nothing else changes identity
representation: display names, private keys, event IDs, and the hex the
app stores, sends, and matches internally are untouched; only the
user-facing spelling of an identity key changes.

## Details

- `desktop/src/shared/lib/pubkey.ts` — `canonicalNpub()`: strict
canonical full-npub helper (64-char hex in any case, or a
checksum-validated npub, returns the canonical npub; anything else
returns `null`); `truncateNpub()`: the compact display form; existing
exports unchanged.
- `desktop/src/shared/ui/PubKey.tsx` — the shared widget's identity gate
validates through `canonicalNpub`; the popover copies the npub only.
- `desktop/src/shared/lib/nostrUtils.ts` — `parsePubkeyInput` rejects
npubs whose payload is not exactly a 64-character identity key.
- `desktop/src/features/messages/ui/NewMessageScreen.tsx` — the To-field
focuses its search input only for clicks that land inside the field
itself, so portaled recipient popovers keep their focus while open (a
popover click previously dismissed it mid-copy).
- Unit suites cover the helper, widget, and parser (including the
degenerate-encode and uppercase regressions); the e2e specs that render
these rows assert the npub display.

### Related issue

- Fixes: N/A. Searched existing issues/PRs for npub identity display —
no existing match.
- Stack: #7489 is based on this branch and builds on these primitives;
it does not stand alone on main.

### Testing

At head `b3310c248` (base: main `44316ff72`; 12 files, +440/−39):

- Focused unit suites (pubkey, PubKey, parsePubkeyInput): 20/20 green;
mutation-checked — removing the decoded-length predicate fails the
short/empty checksum-valid-npub assertions in `canonicalNpub` and the
widget, and a wrong-identity clipboard value fails the new copy
assertions.
- `pnpm typecheck` and `pnpm check`: pass; full desktop unit suite
6459/6459 at this exact head.
- Targeted e2e at this exact head: 8/8 across the two specs that own the
clipboard flows — `agent-access-warning.spec.ts` (compact variant,
agent-access owner hint) and `pubkey-display-screenshots.spec.ts` (full
variant, new-DM recipient verification: pointer copy, popover surviving
the copy, inner/outer Escape, Space-then-Enter).
- No Rust-side or build files change in this PR, so those results are
unaffected.

### Task provenance

Buzz channel: `1f0e4a3d-7e01-4efe-bb16-843b357f85c9`

Task:
buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-09 10:32:26 -04:00
Taylor Ho
f038cbbb0d fix(sidebar): simplify unread indicators and emphasize priority activity (#7134)
**Category:** fix
**User Impact:** The sidebar now presents unread activity consistently:
unread rooms are bold, offscreen activity is counted by destination, and
DMs or directed activity receive stronger emphasis.

**Problem:** Sidebar unread state was split across competing signals:
agent work could trigger overflow, message totals inflated its count,
non-DM rows showed redundant numerals, and routine room activity looked
as urgent as DMs or directed messages. This made the sidebar noisy and
made the overflow value harder to interpret.

**Solution:** Use one stable offscreen unread control that counts rooms
and DMs—not messages—and remove agent work from that signal. Keep
ordinary room activity quiet; promote DMs, mentions, broadcasts, and
relevant thread replies; preserve DM avatars and targeting; bold every
unread room; and retain thread dots while removing non-DM numerals.

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

**desktop/src-tauri/src/unread_catch_up.rs**
Classifies relevant thread replies as priority activity during native
unread catch-up so startup state matches live rendering.

**desktop/src/app/AppShell.tsx**
Passes the priority unread destination set into the sidebar.

**desktop/src/features/channels/useUnreadChannels.ts**
Projects unread destinations, priority state, and thread replies
consistently while keeping Dock badge behavior separate from sidebar
emphasis.


**desktop/src/features/sidebar/lib/useOffscreenActivityChannelIds.test.mjs**
Removes tests for the superseded agent/activity overflow projection.

**desktop/src/features/sidebar/lib/useOffscreenActivityChannelIds.ts**
Removes the old agent-plus-message overflow projection so agent work
alone no longer creates the unread indicator.

**desktop/src/features/sidebar/lib/useSidebarActivityOverflow.ts**
Replaces activity-volume overflow state with unread-destination overflow
state.

**desktop/src/features/sidebar/lib/useSidebarUnreadOverflow.test.mjs**
Covers priority detection and destination-count labels for the new
overflow projection.

**desktop/src/features/sidebar/lib/useSidebarUnreadOverflow.ts**
Counts distinct offscreen unread destinations and determines quiet
versus primary treatment per direction.

**desktop/src/features/sidebar/ui/AppSidebar.tsx**
Renders one stable overflow control, preserves protected-DM visibility
filtering, and prioritizes visible unread DMs for previews and
navigation.

**desktop/src/features/sidebar/ui/AppSidebar.types.ts**
Adds the priority unread destination set to the sidebar contract.

**desktop/src/features/sidebar/ui/CustomChannelSection.tsx**
Stops forwarding non-DM unread counts into custom channel rows.

**desktop/src/features/sidebar/ui/MoreUnreadButton.test.mjs**
Updates control coverage for explicit emphasis, destination labels, and
DM targeting.

**desktop/src/features/sidebar/ui/MoreUnreadButton.tsx**
Applies quiet or primary treatment without changing geometry,
accessibility text, DM avatars, or click behavior.

**desktop/src/features/sidebar/ui/SidebarSection.tsx**
Removes non-DM row numerals while retaining unread weight and thread
preview affordances.

**desktop/src/shared/ui/UnreadPill.tsx**
Shares one composition between quiet and primary states so only color
treatment changes.

**desktop/tests/e2e/badge.spec.ts**
Covers destination counting, promotion without count inflation,
DM/thread priority, row bolding, and removed numerals; also captures the
reviewed UI states.

**desktop/tests/e2e/channel-activity-popover.spec.ts**
Updates channel activity assertions for the numeral-free row treatment.

**desktop/tests/e2e/channels.spec.ts**
Updates channel unread expectations to use bold text rather than a row
count.

**desktop/tests/e2e/thread-unread.spec.ts**
Keeps thread unread-dot and popover coverage while asserting the room
itself is bold.

</details>

## Reproduction steps

1. Open a workspace with enough sidebar destinations to scroll rooms
above or below the viewport.
2. Receive ordinary unread activity in an offscreen room; verify one
quiet `N unread` indicator appears and counts the room once regardless
of message volume.
3. Receive a mention, broadcast, or relevant thread reply in an
offscreen room; verify the same count becomes primary without changing
its geometry or adding a badge.
4. Receive an unread DM, including thread-only activity; verify the
indicator is primary, shows the DM avatar when available, and navigates
to that DM.
5. Scroll the destination onscreen or mark it read; verify the count and
treatment update from the remaining offscreen destinations.
6. Inspect unread non-DM rows; verify their names are bold, numeric
badges are absent, and thread dots still open the unread-thread preview.

## Screenshots

### Ordinary unread room
Routine offscreen room activity uses the quiet treatment.

![Quiet unread overflow
indicator](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/7134/sidebar-unread-overflow-default.png)

### Directed unread activity
The same destination count becomes primary when an offscreen room has
directed activity; geometry and label remain unchanged.

![Primary unread overflow
indicator](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/7134/sidebar-unread-overflow-primary.png)

### Thread-only unread DM
A DM stays primary and retains its avatar even when only its thread has
unread activity.

![Primary DM thread overflow
indicator](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/7134/sidebar-dm-thread-overflow-primary.png)

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-09-04 23:03:41 +00:00
Logan Johnson
4afef8649a fix(desktop): restore mention chip identity icons (#7338)
## Summary

Restore the missing **@ glyph for people and robot icon for agents**
after #7133, and incorporate the requested compact public-key display.

- Wrapping mention chips render their existing bounded icon-bearing
leading fragment.
- Readonly chips show bound keys using the same `8 leading…4 trailing`
formatter as the channel member list: `Scout (150b20bd…15dc)`.
- Full literal labels and exact keys remain authoritative in metadata,
profile targets, title/accessible-name attributes, editor text, saved
bodies and recipient tags. Display abbreviations are never used for
recipient lookup.
- Copy/paste restores the full literal label for a complete compact
chip; partial selections remain plain text. Two keys sharing the same
abbreviation still round-trip to their separate exact recipients.
- No recipient-resolution, authorization, wire-format, composer, or CSS
changes. Existing labels, icons, cloud markers and ordinary mentions
remain intact.

## Verification

Published candidate `9365ab9bc9d9c5d10802580cd576f8e378ff492f`, based on
main `e09f715c9d0ee2cb7bf8a39061e601f3a502f588`:

- **6,444 desktop unit tests passed** on this candidate's final source
tree.
- **44 mock-Chromium tests passed, zero retries in the final run**
across mention recipients, clipboard and cloud provenance: exact
recipient selection, ambiguity rejection, send/edit/reopen, forwarding,
full/partial copy, mismatched-key rejection, matching-abbreviation
collisions and 100%/150% narrow-window geometry.
- TypeScript, desktop Biome/check guards, protected-feature production
build and E2E build passed.
- The compact-renderer regression fails with the formatting call
removed. Original missing-icon and hidden-text accessibility regressions
have red/green evidence.
- Fresh self-review traced rendering, full-key metadata, copy
classifier, paste normalization and identity trust. The same display
formatter owns the accepted compact form on both clipboard sides.

Iteration exposed an existing team-insertion separator flake (passed
final full run) and two new fixture assumptions: non-member sends
require invitation, and Chromium rich paste may retain an NBSP
separator. Tests now exercise invitation and normalize only that
separator when comparing the captured full body and exact tags; no
product change was needed for either.

Earlier local repository-wide `just ci` completed in two invocations
because its initial call hit the ten-minute tool limit during Tauri
compilation. Unchanged native/mobile/backend evidence is reused; the
desktop delta received the full checks above and new remote CI. Native
VoiceOver, real Tauri selection, dark theme and non-Chromium observation
were not performed. Browser artifacts exercise real frontend with mocked
Tauri/relay boundaries, not an installed release.

## Review and visual evidence

Current-head CI and automated review must complete after this update;
the old `2997bfb5` green results do not establish this new head.
Required human review remains separate from agent approvals. No
merge/install/restart authorization.

Before/after icon evidence:
https://github.com/block/buzz/pull/7338#issuecomment-5544575866

Updated compact-key screenshots are posted below. The editor
intentionally retains the full literal address; only readonly chip
display is abbreviated.

Origin:
buzz://message?channel=3355d33a-b72a-423a-b064-a58275f9a8af&id=38b3a27e689f5a9604e273d45f4e3122fceba76081e7bcd3bbdbf439524a5a18

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-04 13:36:17 -07:00
morgmart
5d107836c6 Persist video playback speed preference (#7336)
## Summary

- persist the selected desktop video playback speed as a device-level
preference
- apply the shared preference to inline and review video players,
including after a reload
- cover persistence, validation, cross-window updates, and the
end-to-end playback flow

## Verification

- `node --test
desktop/src/shared/lib/videoPlaybackSpeedPreference.test.mjs`
- `cd desktop && pnpm exec tsc --noEmit`
- `cd desktop && pnpm check:px-text`
- `cd desktop && pnpm test`
- `cd desktop && pnpm build:e2e && pnpm exec playwright test
--project=smoke video-attachment.spec.ts`
- pre-push: desktop check, file-size check, typecheck, and 6,416 desktop
tests

---------

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
2026-09-04 13:10:17 -07:00
Logan Johnson
e7e29937a1 feat(desktop): invite owned agents from standalone forums (#7125)
## Summary

In Desktop's standalone Forums, selecting an owned agent from another
device could leave a post or reply unsendable if the agent had not
joined the forum. This adds **Invite / Cancel** to the send flow so you
can resolve membership without leaving your draft.

- **Invite** checks response policy and your permission to add members,
adds the agent to the forum, waits for refreshed membership, then
rechecks authorization before posting to the original destination.
Membership is forum-wide, not limited to one post.
- **Cancel / Escape** keeps the text, attachments and selected
recipients for retry. Unlike chat's **Do nothing / Send anyway**, this
dialog has no reference-only send choice. Invite is disabled while
pending; Cancel remains available.
- Leaving the source post/reply cancels its pending invitation, even if
you return. Errors remain visible, focus returns to the initiating
editor when appropriate, and late completion cannot resume a cancelled
post or interfere with a newer attempt.
- A rejected send restores text, uploaded media and exact selected
recipients to the source draft only if no newer edit, deletion, upload
intent or send supersedes it. Clipboard verification settles before
recipient capture, with stale edits/navigation fenced out.

### Related issue

Targets `main` after #7124 merged. This PR reuses its publication checks
and draft protection; the five forum commits have been replayed
unchanged onto the merged parent. Owned-agent discovery (#7122) is
already merged. Split from #7114.

Forum creation/templates, channel-less Notes and local-agent management
are unchanged. Duplicate-name binding from #7133 is already merged and
retained by this stack. Inviting does not start a remote agent or
promise that it is online or will reply.

### Testing

Forum composer lifecycle tests, including clipboard-settlement cases,
and TypeScript/changed-file formatting checks passed after the restack.
Earlier invitation, focus and transport-recovery browser coverage is
retained, not claimed as a fresh full browser run on this head. See
[live CI](https://github.com/block/buzz/pull/7125/checks) for
current-head results. Browser evidence uses mock IPC; no full local
`just ci` pass or native/live-relay validation is claimed.

To try it: open a forum post or reply, select an owned nonmember agent
and send. Cancel, then retry without reselecting; Invite should add that
agent before posting. Deny the add to check the visible error and
retained draft. Navigate away/back during a pending invitation or
rejected send; no stale publication or overwrite of a newer draft should
occur.

![Forum invitation with Invite and
Cancel](https://raw.githubusercontent.com/block/buzz/6481bd088d661975672addcc834231df6ac88705/pr-7125--forum-invite.png)

*Earlier mock-browser capture, not current-head runtime proof. [Error
and successful-post
captures](https://github.com/block/buzz/pull/7125#issuecomment-5513883462);
no before-state/native capture available.*

**Limits:** cancellation cannot undo accepted membership changes or
dispatched posts; authorization and publication are not atomic. Recovery
is same-window, subject to browser storage limits, and is not a durable
in-flight send journal: reload/crash can lose a pending snapshot.
Cross-window coordination and in-flight upload custody are unchanged.
The parent's legacy member-agent compatibility does not establish
ownership for nonmember invitations.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-04 17:33:30 +00:00
Logan Johnson
d595806fc3 fix(desktop): authorize remote mentions at publication (#7124)
🤖

## Summary

In Buzz Desktop, you could own an agent running on another device but be
unable to mention it in a channel where it had not yet joined: it was
filtered out before you could invite it. A selected agent could also
disappear from the message's recipients when permissions changed. This
lets you select an eligible agent in the existing **@ menu**, invite it
from the message composer, and send to that agent—or see an error and
keep your draft rather than silently sending without it.

#### Where the experience changes

| Screen / control | Before → after |
| --- | --- |
| A channel's **Message #…** composer, or a message's **Reply in thread
to …** composer | Type `@` (or use the existing @ button), choose your
agent, write the message and press **Send message**. An owned agent not
yet in the channel can now reach the existing **“Mention people outside
this channel?”** dialog when its response settings allow you to address
it. |
| That dialog's **Invite** button | Previously the membership
requirement could block the agent before the invitation. Now Invite
checks permission to add it, adds it as an agent member of the
**channel** (not just the thread), then rechecks membership and response
permission before sending the waiting message. An agent already in the
channel needs no invitation. |
| Existing direct message, or the new-message screen with the **To:**
field | A mention is checked against the conversation the message will
actually enter, including a newly created direct message, rather than
the old or not-yet-created destination. This does not add an Invite
control to direct messages. |
| Editing a message / sending attachments | The selected agent remains
part of the send or edit attempt through attachment upload and the final
permission check. Lost permission produces a visible error instead of
dropping that recipient. |

**Invite is not the only chat choice.** The existing **Do nothing**
button sends the message *without inviting or notifying the nonmembers*;
their names remain references in the text. Where you cannot invite, that
choice is labelled **Send anyway**. To abandon the send instead, dismiss
the dialog with Escape. Invitation actions are disabled while
preparation is pending, preventing duplicate clicks.

**Leaving and returning must not resurrect a cancelled send.** Switching
threads or leaving the composer cancels its pending invitation, even if
you return to the same thread. Cancellation before dispatch sends no
message; an accepted membership change cannot be automatically undone.
An ordinary send without a pending invitation remains bound to its
original destination rather than following you into another
conversation.

**Failed sends must not overwrite your next draft.** If you leave a
thread, return and replace or deliberately clear its draft while an
older send is pending, the older failure cannot restore deleted text,
recipients or files; success cannot erase the newer draft—even if its
text is identical. An untouched draft cleared automatically for sending
remains recoverable on failure. This protection also covers reopening
the composer and starting a newer send.

The channel timeline also keeps its existing **new-messages / Jump to
latest** button available when newer messages are waiting to be
displayed. For example, after sharing a reply to the channel and closing
the thread panel, you can click the catch-up button to reveal buffered
messages. Closing the thread does **not** guarantee the shared row
appears automatically or force you away from reading history.

### Related issue

Built on [#7122](https://github.com/block/buzz/pull/7122), base branch
`split/owned-agent-discovery`, which lets Desktop find and verify owned
agents independently of this device. Current integration head:
`1144465d00273cf74b7c22544ae5a3299bd98560`, built on exact published
root `3a56d17824522580fe04cae463b54f4c7ba66021`. Root #7122 has its own
CI and security gates; this PR must not land ahead of that dependency.
Finding an agent is not channel membership, online status or a promise
of a reply. This PR changes what the existing message controls can do
with those agents; it adds no profile, presence, cloud marker or remote
start/stop UI.

Standalone forum post/reply **Invite / Cancel** is added separately in
[#7125](https://github.com/block/buzz/pull/7125); here those composers
only gain visible authorization errors. Same-name selection/binding
fixes ([#7133](https://github.com/block/buzz/pull/7133)) and mention
spacing ([#7128](https://github.com/block/buzz/pull/7128)) are not
included.

Extracted from [#7114](https://github.com/block/buzz/pull/7114)
(historical source `98fe33ec`). [Behavior and draft-recovery
contract](1144465d00/docs/remote-mention-routing.md)
· [Originating
discussion](buzz://message?channel=f7a9536a-1738-4bad-a888-b3ea25010ef1&id=7aa1f0ab23dce514bd8a0221441cf005bf428914621171472b79747c50820848).

### Testing

![Channel composer after Send: RemoteScout is not a channel member;
Invite adds it before sending, while Do nothing sends without inviting
or notifying
it](https://raw.githubusercontent.com/block/buzz/b20345da0bd648ecbc78988637c89893a33ca2f3/pr-7124--remote-invite.png)

*Earlier candidate, mock desktop browser: the existing channel dialog
now reachable for an eligible owned agent on another device. The two
buttons have different send outcomes; Do nothing is not Cancel.*
[Success and denial
captures](https://github.com/block/buzz/pull/7124#issuecomment-5482292703)
· [Pending-state
capture](https://github.com/block/buzz/pull/7124#issuecomment-5482604329).
These show the relevant UI, not live agent availability, native
authorization or the later draft-storage/catch-up repairs. No
before-state screenshot is available.

Existing coverage exercises exact recipients, invitation
rejection/cancellation, new direct-message destinations, uploads, edits,
thread re-entry and stored-draft deletion. The timeline regression
checks the shared reply becomes visible using the available catch-up
action.

**Integration validation (2026-09-02):** independently reviewed the
routing delta onto root `3a56d178`: seven original patches unchanged;
two reconciliations retain generic publication-error toasts alongside
authorization errors and retain non-authored editability updates. Added
two production-hook regression tests (normal and queued-media
publication) requiring visible generic error, recovered draft and
released pending state.

- Writer validation: **5,995 Desktop tests**, **42 focused tests**, **22
mock-IPC browser journeys** (18 routing, 2 root provenance, 2
destination binding), and **1 voice-note failure journey** passed; lint,
types, size guards and E2E build also passed.
- The broad suite ran before the final formatting-only test amendment,
not as an exact-final-head rerun. Independent AST comparison confirmed
that amendment is semantics-preserving; **4 fresh assertions at final
`1144465d`** passed. Publication rechecked final-head TypeScript,
amended-test formatting and `git diff --check` successfully. No new full
repository `just ci` run is claimed.
- Browser tests use an isolated E2E build and **mock IPC**, not live
relay/native authorization. Historical screenshots above are explicitly
earlier UI evidence, not exact-head runtime certification. Packaged
Tauri/live-relay behavior was not independently witnessed.
- **Published-head gates:** [current CI
run](https://github.com/block/buzz/actions/runs/33657948560) and
[renewed exact-head formal review
request](https://github.com/block/buzz/pull/7124#issuecomment-5513218340)
must clear before landing. [Earlier CI
run](https://github.com/block/buzz/actions/runs/33438436438) and the two
earlier approvals cover `7ffead0f`, not this new head. Root CI/security
clearance remains separate; the independent scoped integration approval
is not merge authorization.

To try it: in a channel or thread, select an owned nonmember agent,
Send, then Invite or Escape and retry. Deny the add or revoke its
response permission before sending: expect a visible error and
recoverable draft, not a message missing the agent. During a pending
send, return to the source thread, edit or clear the draft, then leave
again: late completion must not overwrite that choice.

**Limits:** permission checks and sending are separate operations;
cancellation cannot retract a dispatched message. Draft protection is
same-window, not new cross-window deletion synchronization. Standalone
forum transport failure can still restore text/media without the exact
selected recipients. Native compatibility is inherited: open-source
builds may still recognize a valid legacy, self-declared agent already
in the channel when verified ownership is absent or rejected; that does
not establish ownership or unlock this owned-nonmember invitation path.
Invalid policy from a verified owner is still rejected. No agent
response is guaranteed.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-04 16:35:35 +00:00
Logan Johnson
ee883d73fb fix(desktop): bind duplicate mention selections to exact recipients (#7133)
## Summary

Selecting two people or agents named Scout could replace the first
recipient with the second even though the message still looked right.
This binds each selection to its exact identity: the first keeps
`@Scout`, and a conflicting selection gets `@Scout (<full public key>)`.
Removing one no longer removes or redirects the other.

- Reuse the existing **@ suggestion list**; team selection and automatic
agent addressing reserve and reuse distinct labels too. Typing an
ambiguous name manually shows an instruction to use the picker and
preserves the draft without publishing—in chat, edits and standalone
forums.
- Share literal mention matching across recipient extraction, removal,
display and editing so a shorter name cannot claim another recipient's
longer or qualified label.
- Rebuild authored references when a message is edited, and use the
latest authorized snapshot when reopening or forwarding.
Rendering/editing can recover qualified identities only from the
message's recorded references, not from a key typed into its body.
Unresolvable historical names remain literal rather than guessed
recipients.
- Wrap full-key labels within narrow/zoomed layouts while keeping their
complete accessible label and ordinary mention icons. Edit activation
waits for the action menu's focus cleanup before focusing the editor.

### Related issue

Targets `main`; mention spacing (#7128) is already merged. Split from
#7114. This is independent of the #7124 → #7125 remote-invitation stack
and does not expand agent eligibility or invitation permissions. The
separate Enter-selection suffix issue remains tracked in #7253.

### Testing

The desktop unit suite and focused mock-Chromium checks passed on the
published integration candidate, including pending-paste selection,
edit/forwarding, copy and narrow-layout cases; formatting, types and
frontend builds passed. On `0b3b18c0`, 64 focused trust/paste/selection
unit tests and six mock-Chromium tests passed with zero browser retries,
including actual timeline chip copy → fresh channel paste → send and
mismatched-key rejection; TypeScript, changed-file Biome and an isolated
E2E build passed. The broader browser run had copy failures before the
focused repairs and is not claimed as wholly green. See [live
CI](https://github.com/block/buzz/pull/7133/checks) for current-head
results. No full local `just ci` pass, native/live-relay or
cross-browser validation is claimed.

To try it: select two same-name recipients, remove one, send, then
edit/reopen and forward; only the intended identities should remain.
Type an ambiguous name without choosing a suggestion and check that
sending retains the draft with an error. Inspect full-key labels in a
narrow window at 150% text size, and open Edit and type immediately.

![Ambiguous mention keeps the chat
draft](https://raw.githubusercontent.com/block/buzz/c48053209b36b3b1bf7197cf2d606d27b48a27c0/pr-7133--ambiguous-general.png)

![Full-key label in a narrow, zoomed
composer](https://raw.githubusercontent.com/block/buzz/ac262f00de6fe3860bff2f87dc0e98165ed6b651/pr-7133--layout-1.5-composer.png)

*Earlier mock-browser captures, not current-head runtime proof. No new
before-state capture; screenshots alone do not prove recipient
delivery.*

**Clipboard trust:** generated full-key-qualified mentions now retain
their exact recipient after copy/paste when the full key matches the
clipboard record and community directory/profile state independently
vouches for the base alias, including numeric collision suffixes. A
qualifier alone does not establish trust. Arbitrary historical labels
still cannot always be reconstructed.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Bad Janet <150b20bdf6130418df9239dd1bd082c71612c8d653b47c277200365b9be215dc@buzz>
2026-09-04 11:51:41 -04:00
Matt Toohey
b1f6b7ef77 feat(desktop): preserve mentions across copy and paste (#7228)
Copying a message out of the timeline lost the mention. The rendered
chip drops
the `@` for display, so the clipboard carried "John Smith" — two
ordinary words
no composer could bind back to a pubkey. Pasting into another channel
produced
dead text, and sending it tagged nobody.

## What changed

Every Buzz copy now writes two clipboard flavors in one transaction:

- **`text/plain`** — readable anywhere, sigils restored, no pubkeys.
This is what
  TextEdit, Slack, and every other external app receive.
- **`text/html`** — the same content with each mention wrapped in a span
carrying
  `data-mention-pubkey` / `-label` / `-kind`.

On paste, the composer harvests those records, registers each `name →
pubkey`
with the existing mention machinery, and inserts the content: the chip
re-lights
and the send path recovers the identity the author tagged. A marker
attribute
records what the plain flavor holds, so a Markdown copy pastes through
the text
pipeline and a rendered copy through the HTML one.

**Covered surfaces:** timeline selection copy, thread-panel selection
copy,
forum post/reply selection copy, "Copy message", and composer copy/cut —
plus
paste in both the channel and forum composers.

## Trust boundary

Clipboard HTML is untrusted, and the branch treats it that way:

- Records are capped (50), labels bounded (200 chars), and a pubkey must
be
  64 hex before it can become a `p` tag.
- **Only mentions the paste actually shows are registered.** An empty
`<span data-mention-pubkey=… -label="Jane Doe">` would otherwise rebind
that
display name for the rest of the composer session, so a later
hand-written
@Jane Doe would chip-light convincingly against the attacker's key. Each
  branch registers only the records whose label appears in the text *it*
inserts, matched with `getMentionOffsets` — the same matcher the
send-time
  extractor uses.
- **The visibility gate reads only what ProseMirror will insert.**
`DOMParser` hard-drops `script`, `style`, `title`, `noscript`, `object`,
and
`head` content, so `visible<style>@Jane Doe</style>` beside an empty
chip span
used to smuggle a binding past the gate. Those elements are stripped
before
  either output is derived.
- **A partial chip never gains a sigil.** A selection crossing a chip
boundary
falls through to the browser's default copy, which serializes the full
identity
attributes around a slice of the text — pasting that invented "@Smith"
out of
"John Smith". Paste now leaves a fragment as plain text, tolerating only
what a
whole chip picks up in transit (restored sigil, author casing, U+00A0
swaps,
  and the label cap's own ellipsis).

Both clipboard sides share one `matchChipTextToLabel` helper, in the
module that
owns the label attributes, so copy and paste cannot drift on what counts
as a
whole chip.

## Notes

- Mention matching reuses `getMentionOffsets`, so code spans and fences
are
  excluded and the longest display name wins.
- The plain flavor inlines chip boxes before reading `innerText`; a chip
is a
flex container, so the browser's own copy split "@John Smith" onto its
own line.
- `MarkdownMention` and `MacEmacsTextShortcuts` are extracted verbatim
from
`markdown.tsx` and `useRichTextEditor.ts` to keep both files under the
size gate.

## Testing

- ~40 unit tests over the flavor builder/parser, the visibility filter,
the
  ignored-tag sweep, and the chip-match verdicts.
- A Playwright spec (`desktop/tests/e2e/mention-clipboard.spec.ts`)
driving real
  copy/cut/paste DOM events: timeline and "Copy message" of a multi-word
non-member mention pasted into another channel and sent with the
original
pubkey in its `p` tag; the forum copy → forum reply round trip; composer
  copy/cut; plain flavor asserted to contain no 64-hex string; the
boundary-crossing drag; and the hidden-record and `<style>` smuggling
vectors.
- Every regression test is bound to a production seam and fails with its
guard
  removed.
- `just ci` / pre-push lanes green (5927 desktop unit tests, typecheck,
lint,
  file-size gate).

---------

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-04 16:45:13 +10:00
Logan Johnson
4b0744d7f3 test(desktop): await Bestie drag and profile hover endpoints (#7294) 2026-09-03 23:52:55 -04:00
Luke Tornquist
2ac0aa1dd1 Collapse contiguous join messages (#7262) 2026-09-03 19:26:21 -04:00
Logan Johnson
6cf514ed90 fix(desktop): wrap message tables within the available pane (#7279)
## Summary
- Let Markdown tables use the available message width instead of their
maximum-content width, inheriting the renderer's existing
`wrap-anywhere` handling for long tokens and links.
- Top-align header cells as well as body cells. Give cells a `min-w-24`
readability floor so short labels do not collapse into one-letter
columns; retain the existing table-local scrollbar when many columns
genuinely cannot fit.
- Preserve semantic table markup, links, inline code, and surrounding
message layout. Add three browser regressions through the real message
renderer (channel, narrow thread, and many-column overflow).

### Related issue
Fixes #5313. No matching open table-readability PR found.

Owner-authorized task channel: `819b36d5-7371-4ed0-bf9b-d461d723779e`

Source:
buzz://message?channel=819b36d5-7371-4ed0-bf9b-d461d723779e&id=3bf2d1bd5df28fcb481e71159dc1be2b3888918b05f67e270354fbfea65a8c90

### Testing
Original production candidate:
`4427f2e6f9ef85a01a267fe9eabd1cf13cacfb78`; base:
`7a9a5233d9d755e715be0c585cf7850e935d28cf`.

- `pnpm -C desktop check` — passed.
- `pnpm -C desktop test` — 6,110 passed, none skipped.
- `pnpm -C desktop typecheck` — passed.
- `BUZZ_PROTECTED_BUILD_OUTPUT=<isolated-production-directory> pnpm -C
desktop build` — passed.
- `pnpm -C desktop build:e2e --outDir <isolated-candidate-directory>` —
passed.
- `just file-size-check` and `git diff --check` — passed.
- Chromium / mock Tauri bridge: all 3 new Playwright tests passed
against the fixed candidate build. Both wrapping tests fail on the
unchanged base build, while the overflow-fallback test passes. Each
browser run used a unique output directory and a non-reused local server
pinned to its build directory.
- At 883px channel message width: table scroll width **1,395 → 883px**.
At 292px thread message width: **1,395 → 292px**. All cells top-aligned;
long tokens/URLs wrap, link destinations and code text remain intact,
short labels stay on one line, and document width remains 1,280px.
- Focused fresh-frame review checked actual before/after screenshots and
the complete diff. It caught one-letter label wrapping in an early
width-only candidate; the final cell-width floor and regression
assertion address that finding.

Limitations: native Tauri/WebKit and relay-backed integration were not
exercised for this CSS-only delta. Repository-wide `just ci` was
attempted but exceeded the local command budget during unrelated Rust
compilation; it is **not** reported green. The full affected TypeScript
package gates above passed on the exact candidate. Production/E2E builds
retain existing chunk-size and mixed static/dynamic import warnings.

Before/after screenshots are posted below using the repository's
screenshot publication script. No merge, production installation, or
runtime restart is requested.


### CI-driven test-only follow-up
Current head: `925d964b6cf31ba704d74baf73769110774b2789`.

Smoke shard 3 exposed an older test in `messaging.spec.ts:457` that
still required three columns of ordinary prose to overflow horizontally
([failure
log](https://github.com/block/buzz/actions/runs/33771325404/job/100702242706)).
This expectation contradicts the intended wrapping change. Updated its
name and assertion to require containment; the separate many-column
local-scroll test is unchanged. No production code or harness settings
changed in this follow-up.

- Affected browser validation: **4/4 passed** (updated existing
prose/narrow table case plus the three new channel/thread/overflow
cases).
- Reused the immutable `4427f2e6` E2E build on a fresh non-reused
server; production source is unchanged at the current head. The
package/build checks above remain evidence for that unchanged production
tree, not a claim of rerunning the full suite at the new SHA.
- Targeted Biome check, file-size gate, and `git diff --check` passed.
- Current-head CI:
https://github.com/block/buzz/actions/runs/33773490446
- Prior unrelated Bestie baseline failure and reproduction:
https://github.com/block/buzz/pull/7279#issuecomment-5528077181
- Security authorization and review must target the **current** head,
not the old screenshot/build SHA. A Block organization member must
comment exactly `@buzz-security-review
925d964b6cf31ba704d74baf73769110774b2789`.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
2026-09-03 16:07:14 +00:00
Will Pfleger
7a9a5233d9 🤖 fix(desktop): harden smoke E2E tests against Bestie overlay and toast timing (#7270)
## What

Two remaining E2E hardening fixes from the Desktop Smoke flake pattern
introduced by `ac5a18697` (Bestie — added `VITE_BUZZ_BESTIE=1` to
`.env.e2e` and mounted `BestieGlobalOverlay` globally).

The toast/DM-retry fix landed independently in main via #7127
(`input.hover()` + bounded `toHaveCount` wait); that hunk is dropped
from this PR.

## Fixes

### `agent-control-regressions.spec.ts:240` — Stop does not accept an
unconfirmed or foreign-channel result

**Cause:** Playwright 1.60.0's `page.clock.install()` fakes all timers
including `requestAnimationFrame`. The test called it before opening the
settings menu. With RAF frozen, the `DropdownMenuContent`'s `zoom-in-95
duration-150` CSS enter-animation never advances — Playwright's
stability check observes a continuously-changing bounding box until the
30s test timeout.

**Fix:** Re-sequence so the menu is opened on real time first. After
`openAgentActivity`, open the trigger, assert visibility/enabled, call
`waitForAnimations(page)` to settle the enter-animation (real
`setTimeout`, no fake clock installed yet), then install the clock. The
`fastForward(8_001)` correlation timeout still works because it's
scheduled after the clock is active. Pointer actionability preserved —
normal `stop.click()` (no `force`) fails with pointer-interception under
a covering surface.

### `message-feedback-snapshots.spec.ts:97` — profile hover uses the
channel hover surface

**Cause:** `channel.hover()` triggers a CSS `transition-colors`
animation. With the Bestie `LayoutGroup` mounted, `evaluate()` captures
a mid-transition background value that never matches the profile card's
settled token.

**Fix:** `waitForAnimations(page)` after `channel.hover()` and before
reading `channelHoverColor`.

## Evidence

- Target specs pass: stop-turn 8/8, profile-hover passes.
- Full `agent-control-regressions.spec.ts` (7 tests) green.
- `just desktop-typecheck` clean at pushed head `0fbfe2a9f`.
- No production code changed.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
2026-09-03 08:59:49 -04:00
klopez4212
c6ca9d94d9 Show status and huddle indicators beside names (#7112)
## Summary

- show status and huddle emoji beside names in DMs and message rows
- provide accessible tooltips, fallback status emoji, and profile-menu
icon replacement
- add the desktop status editor with preset durations, a ShadCN
calendar, and a capped half-hour time menu

## Validation

- desktop checks, typecheck, and file-size guard
- 5,802 desktop tests
- focused Playwright coverage (3 passed)
- E2E build and native Builderlab staging verification

Updated visual snapshots are attached in the PR comments.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
2026-09-03 09:39:37 +00:00
Matt Toohey
40220d5618 perf(desktop): publish mention sends before waking agents (#7154)
## Summary

Cuts perceived agent-mention send latency by publishing the message
first and waking the agent afterwards, instead of blocking the send on a
synchronous agent start/deploy round-trip. A send that mentions a
stopped or undeployed managed agent now shows the message immediately;
the wake runs fire-and-forget after the relay accepts the publish. The
already-running-agent send also gets faster via revalidation dedupe and
NIP-11 caching.

## Changes

### Publish-first agent wake

- Wakes for mentioned managed agents are collected during send
preparation and flushed fire-and-forget only after `await send(...)`
resolves. No start can fire — and no "your message was sent" toast can
appear — for a message the relay never accepted; every abort path
(cancel, readiness error, publish rejection, dismissed non-member
prompt) simply drops the queue. Persona-create wakes ride the pending
draft behind the non-member prompt for the same reason.
- Each wake is bound to the tenant scope captured at send time: the new
`useDetachedAgentStart` hook passes `expectedRelayUrl` +
`expectedSignerPubkey` with every start, so a wake that outlives a
community switch fails closed at the backend instead of spawning against
the new tenant. A wake whose scope has not resolved yet (identity query
still loading, blank stored relay URL) is refused with a recoverable
toast rather than fired unscoped.
- In-flight wakes are deduped through a module-level map keyed by
`(relay URL, pubkey)` — the same tenant pair the backend keys on — so
two quick sends or two composers cannot double-spawn a cold agent during
the seconds-long start window. Entries are deliberately retained across
community switches (the key *is* the tenant scope, so a retained entry
can never affect another community, and clearing it let an A→B→A round
trip deploy a provider agent twice) and self-clean when the start
settles.
- Wake-failure toasts are fenced to the community they fired in via a
module-level scope mirror: a start that settles after a community switch
logs instead of rendering community A's failure over community B's UI,
and an A→B→A return re-delivers the warning where it is actionable.
- Membership attach and access-policy writes stay synchronous, so the
harness's first kind-39002 read still sees the channel.

### Replay floor

- The send timestamp travels with the wake as `BUZZ_ACP_REPLAY_FLOOR`,
threaded through both local spawns (`spawn_agent_child`) and provider
deploys (`deploy_to_provider` injects it into `launch.policy_env`), so
the harness's startup watermark replays back past the just-published
triggering message no matter how long the spawn takes. `buzz-acp` clamps
the floor to `[now − 15min, now]`.
- The floor is captured at enqueue time, not flush time — the flush runs
post-publish, so a flush-time stamp could exceed the message's
`created_at` and skip the very message the floor exists to cover.
- On local spawns the caller's floor is asserted *after* the user env
layering (and the ambient parent-process value is stripped
unconditionally), so a saved persona/global/agent env entry cannot
shadow this send's floor — mirroring the shadow-strip the provider path
applies to `launch.env`. Both halves share one `REPLAY_FLOOR_ENV_VAR`
const.

### Send-path latency reductions (already-running agents)

- Mention revalidation is deduped: the publish-boundary pass reuses the
pre-side-effect authorization pass unless an awaited round-trip actually
separated the two (background upload, link-preview settlement, DM
expansion, a real access-policy/membership write, or active-huddle
enrollment). This preserves the #5681 authorization boundary while
making the common send single-pass.
- NIP-11 `self` lookups are cached per relay URL for 5 minutes. Only
verified values are cached — non-2xx and malformed responses stay
retryable — and URL keying keeps community switches from serving another
relay's identity.
- `applyReusableAgentAccessPolicy` now reports its relay write
explicitly (`{ agent, wrote }`) instead of signalling through object
identity, so the revalidation trigger above is load-bearing by
construction.

### File splits

Four files crossed the repository file-size ratchet during this work;
one cohesive unit was extracted from each rather than raising a ceiling
— `runtime/setup_payload.rs`, `commands/agents_create_fields.rs`,
`app_state_accessors.rs`, and `useEnsureAgentMentionsReady.ts`. The
ratchet is green at the tip.

### Review follow-ups

The three concrete findings from the first review round are fixed at the
tip: the pre-publish wake and its false "your message was sent" toast
(fixed by queueing wakes behind the publish), the stale cross-community
failure toast (fixed by the scope-mirror fence), and the A→B→A duplicate
provider deploy (fixed by retaining the tenant-keyed in-flight entries
across switches). The fast-path admission-staleness point is answered in
the review thread: deferred paths already re-validate at the publish
boundary, and the remaining fast-path window is milliseconds against an
irreducible network-transit race.

Mid-branch send-perf instrumentation was added to attribute the residual
spinner latency and reverted once that analysis concluded — it is
net-zero in this diff.

### Deferred follow-ups

Durable mention catch-up via `event_mentions` (option 2 step 3) and
backend deploy-epoch coalescing for the wake paths that do not funnel
through `useDetachedAgentStart` (Agents-panel Start, restore,
inbound-persona deploys) are intentionally left for separate changes.

## Testing

- `cargo test --lib` on desktop/src-tauri: 3054 passed; clippy `-D
warnings` + fmt clean
- Desktop unit tests: 5856 passed (the 5 failures are the pre-existing
`inboxReopenNavigation` / `useRetainedProjectGitViews` baseline, present
on origin/main); `tsc --noEmit` and biome clean
- Full mentions (87), channels (89), and community-rail (25) Playwright
smoke suites against `pnpm build:e2e` bundles, with 3× stress reruns of
each new spec
- The load-bearing regression specs were confirmed red on the pre-fix
code: publish-failure → zero starts and no false toast, the dedupe hold
(1 call vs 2), the fail-closed scope refusal, the rail-switch toast
fence, and the A→B→A retention spec (1 deploy vs 2)
- New unit coverage pins the queue contract (enqueue-time floors,
attach-seam queueing), the scope capture and verbatim relay-URL handoff,
the dedupe map's keying and settle-then-repermit behavior, the unscoped
refusal, the toast-scope mirror, the `{ agent, wrote }` contract, and
the replay-floor env layering on both spawn paths

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-03 14:46:11 +10:00
Logan Johnson
5073e075d7 fix(desktop): unify owned-agent cloud provenance markers (#7129)
Unify presentation-only cloud provenance across agent identity surfaces. Keep successful local-inventory and verified-ownership gates; preserve main availability and mention spacing behavior.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
2026-09-03 02:35:37 +00:00
Logan Johnson
d5a73b9f35 fix(desktop): derive agent availability from relay presence (#7127)
🤖

## Requested rebase published — ef40744b

Rebased onto fetched main **47d068e2109d077414cbf2f4f1c927f6d051037a**,
published **ef40744b3aeb4baaf8c81416e1a644fb5b315f91** with the exact
expected-old `df7fad6a` force-with-lease. No merge.

Manual conflicts were additive: preserve main's exact-key identity
documentation alongside the availability contract, and retain both
Bestie props and the shared availability reader in
`UnifiedAgentsSection`. Range-diff confirms unchanged lifecycle policy:
exact-key action-time authority, Unknown versus Offline, rejected
shutdown retains record/memberships, and separate local/provider/owner
gates. Main's exact-key profile routing survives. Both test-only CI
synchronization repairs (natural toast expiry and bounded stderr wait)
are byte-identical to the prior head. All ten original
authors/messages/DCO/material coauthor trailers are preserved;
configured signing policy was not changed.

Fresh checks on the rebased candidate:
- TypeScript, Biome on 26 changed TypeScript files, differential
file-size gate, and diff whitespace: pass.
- Focused production-hook/card/profile units: **58/58**.
- Fresh E2E build, availability/deletion browser: **11/11**, no retries.
- Main exact-key profile cases plus failed-DM send/startup retries:
**6/6**, no retries.

Previously reviewed full Desktop/buzz-agent package and mutation
evidence is reused for unchanged behavior; no ceremonial full suite, new
native/provider test, or `just ci` pass is claimed. Local configs,
dependency links, and historical artifacts are preserved.

Hosted observation: **MERGEABLE**, **BLOCKED / REVIEW_REQUIRED**, no
new-head formal review. [CI
33699735990](https://github.com/block/buzz/actions/runs/33699735990) is
running (including Rust and Desktop lanes), not a completed success. DCO
and required Security aggregate passed at the observation; the separate
Codex advisory review was skipped. No completed failing check or new
inline feedback observed. Historical approvals are not new-head
approvals. No reviewer/security authorization or merge action was
performed.

---

## Feature summary and retained pre-rebase evidence

## Summary

In Buzz Desktop, an agent could look online just because it had been
started or deployed, even when there was no current sign it was
connected. Cards and profiles now show availability from the agent's
relay presence rather than a saved launch record, so you can distinguish
an online agent from one that was merely deployed.

Agents cards and profiles use presence reported through the shared
server (the relay). A successful presence read with no online agent
shows Offline; failed/disconnected evidence shows unknown, rather than
retaining a misleading cached Online state.

Lifecycle actions remain separate. An offline agent may still have a
Shutdown action because the deployment record exists. Shutdown reports a
**request**, not proof the process stopped. Offline does not imply that
starting a duplicate agent is safe, and Online does not promise a
response.

### Related issue

Independent base: `main`; no stack parent or child among the
replacements. Extracted from
[#7114](https://github.com/block/buzz/pull/7114), retained as historical
source (`98fe33ec`).

[Behavior
contract](f4bb2ed44e/docs/agent-availability.md).
Originating [Buzz
discussion](buzz://message?channel=f7a9536a-1738-4bad-a888-b3ea25010ef1&id=7aa1f0ab23dce514bd8a0221441cf005bf428914621171472b79747c50820848)
· channel `f7a9536a-1738-4bad-a888-b3ea25010ef1`.

### Testing

The same saved provider-backed agent, with only authored presence
changing. These are mock-browser states, not a before/after deployment
or live relay transport test; production UI is unchanged by the later
fixture repairs.

**No online presence:** gray dot, existing Shutdown control retained.

![Offline presence does not remove the deployment lifecycle
control](https://raw.githubusercontent.com/block/buzz/ed68b1b4597dde47118b7591b1e8ebf3702447ca/pr-7127--offline-deployment.png)

**Online presence:** green dot, same lifecycle control.

![Authored Online presence changes availability without changing the
deployment](https://raw.githubusercontent.com/block/buzz/ed68b1b4597dde47118b7591b1e8ebf3702447ca/pr-7127--online-deployment.png)

[Capture
details](https://github.com/block/buzz/pull/7127#issuecomment-5482264824).
To check manually, compare runtime-only transitions with presence
updates, then disconnect/fail the presence read and verify it does not
stay Online. A Shutdown request should not immediately claim confirmed
termination.

#### Historical pre-rebase evidence and limitations (df7fad6a)

Lifecycle production source remains
**`f4bb2ed44e5a989d93c5f51e93c0bbd2dca941be`**. Current published head
is **`df7fad6ae65dda78508317186a95522d1bb22ed9`**: the prior browser
synchronization at `b78d093e` plus an additive two-file Rust
test-harness synchronization described below. No production bytes,
dependency/configuration files, or prior commits were changed; no
rebase. Current live main `0dbd036f5bff33e7ade75e7639f3218d424a6e73` has
identical failing-test/toaster/send-flow source; the causal browser
comparison used latest successfully tested main
`04babf02655440b4dfd37f2e2df605ead0a030d8`.

**Lifecycle/deletion correction:** both Agents and actual profile
deletion now pass the shared exact-key availability reader, not raw
cached data. It reads the canonical query state and connection at action
time, including after awaited channel discovery.
Failed/disconnected/pending evidence and unqueried persona siblings are
unknown; successful missing means Offline only for a requested key.
Successful background refetch cache remains usable; settled failure
revokes it. No second cache or per-row polling was added.

Provider record + channel + Online/Away/**unknown** awaits shutdown
submission before local removal; rejection preserves record/membership
for retry. Established Offline preserves intentional no-request removal.
No route preserves warned local removal. Local agents retain native
stop-before-remove, independent of presence. Profile consent now
describes a shutdown **request**, not remote deletion or guaranteed
termination. Existing ownership and force gates are unchanged.

**Verified, reused exact-candidate validation:** the independently
approved eleven-file patch (SHA-256
`2f69fe12ef0420e62dea1fd8db28cfa22cde5eecaf8080e656310a3e60d0cf86`) was
committed without byte changes. Desktop **5,921 passed, 0
failed/skipped**, including **26 new mounted production hook/IPC
regressions**; rebuilt availability browser suite **11/11 passed, no
retries**, including four actual profile Delete journeys. Desktop check
(existing 4 warnings/5 infos), typecheck, production/protected-feature
artifact matrix, differential file-size/policy and diff checks passed.
No blanket rerun or new full-repository `just ci` is claimed for this
frontend correction.

Production regressions cover cached Online **and Offline**
failure/disconnection, genuine missing/Offline, pending, successful
inflight refetch versus settled error, retained reader, error during
awaited channel discovery, unqueried persona sibling, shutdown
rejection/order/cancel, no route and local authority. Browser fixtures
use safe mock IPC and a retained provider receipt, not a real
deployment. Three restored mutation controls fail: unknown → skip
shutdown (**15** regressions), Agents raw-cache reader (**6**), actual
profile raw-cache caller (**1 browser journey**, false removal on failed
cached Offline). Independent review approved the exact frozen bytes and
added **4/4 cached-empty failure/disconnection probes** across both
callers. This is local independent approval, not formal GitHub/A Team
clearance.

The prior native propagation/poll-count defects remain closed ([earlier
response](https://github.com/block/buzz/pull/7127#issuecomment-5512278755)).
The prior hover-popover correction at `b55423f6` remains covered by the
full 11-journey browser run: pending/failed/disconnected means no badge
or accessible status, genuine missing/Offline retains an Offline badge.
Its earlier fallback-restoration mutation failed as expected (badge
count 1 rather than 0); that historical witness is reused, not rerun.

**Reused unchanged native/system boundary:** local `just ci` at
`c59067d8` passed workspace/Tauri fmt/clippy, static/policy checks, Rust
unit recipe, native workspace **3,159 passed / 20 ignored**, Web build
and **2,019 mobile tests**. No native implementation changed in this
lifecycle correction. These are historical boundary results, not
new-head native/live certification. [Parent
CI](https://github.com/block/buzz/actions/runs/33650549130) passed with
**14 retry-recovered browser flakes**, not retry-free. Old-head
CI/reviews are not current-head clearance.

**Hosted gates:**
[CI33662151103](https://github.com/block/buzz/actions/runs/33662151103)
on `f4bb2ed4` **FAILED**: smoke shard1 had 322 pass, one failure, one
retry-recovered flaky, two skipped. The failed first-DM retry test timed
out on all three attempts because the error toast intercepted Send. That
failure is preserved, not waived; the scoped test repair below is
published as `b78d093e`.
[CI33668171165](https://github.com/block/buzz/actions/runs/33668171165)
on `b78d093e` subsequently **FAILED** the Rust unit budget regression
described below. Both original failures remain visible; neither was
retried to green. Exact `f4bb2ed4` and `b78d093e` APPROVED reviews cover
unchanged reviewed bytes, not formal approval of the new head. Fresh
exact-head CI and the established automated technical rereview are the
next gates for `df7fad6a`. Historical deletion responses remain
([5092381800](https://github.com/block/buzz/pull/7127#issuecomment-5513759316),
[5092391193](https://github.com/block/buzz/pull/7127#issuecomment-5513763497)).
No formal review dismissed. The [security
notice](https://github.com/block/buzz/pull/7127#issuecomment-5482231278)
and latest-push maintainer/codeowner policy remain separate actionable
gates: eligible Block organization members own current-range
authorization. No merge/security authority exercised.

**CI causal repair (`b78d093e`, test only):** the error `Message failed
to send: Mock first DM send failed.` is deliberately injected by the
existing fixture. CI screenshot and retry trace show the bottom-right
Sonner notification over the actual enabled Send button. `fill()` leaves
the pointer parked there; Sonner pauses its 4-second lifetime while
hovered. A fast run can click before animation settles (unchanged local
test passed in 2.7s; two actual tested-main CI cases passed first
attempt in 3.3s), which does not disprove the failure. Independent
controlled browser runs on `f4bb2ed4` and tested main `04babf` both
reproduced the same toast hit-test at Send `(1203,627,32,32)`,
persistent hover beyond 4s, and intercepted ordinary click with no
second send. Moving the real pointer to the editor allows natural expiry
and successful ordinary retry, preserving all original
DM-channel/recipient assertions. This same synchronization already
exists in the neighboring agent-startup-failure test.

The one-file correction keeps the visible error assertion, scopes its
toast locator, moves the pointer back to the editor and observes normal
toast removal (bounded 10s) before retry. No forced click, direct toast
dismissal, mocked clock, CSS override, skipped test, production behavior
change, or new backend mock. Six focused browser executions pass
(first-send/startup-failure, three repeats each, no retries); the
held-toast control fails on original bytes at the Send click while the
exact repaired test passes. Biome and diff checks pass. Reuse unchanged
5,921 Desktop / 11 availability browser / four independent probes above;
no semantic production change warrants repeating those suites. Original
failed CI attempt/retries, local fast pass, deliberate failing control
and all traces remain in `WORK_LOGS/AVAILABILITY_CI_B9210A40`. Browser
evidence is mock-IPC Chromium, not native/live-relay certification. The
UI still temporarily overlays Send while a notification is hovered; the
test exercises its real move-away/expiry recovery, not immediate
click-through.

**Rust CI causal repair (`df7fad6a`, test only):** [original Rust / Unit
Tests failure,
job100375291370](https://github.com/block/buzz/actions/runs/33668171165/job/100375291370)
tested GitHub merge `223dee91a396d8cb4ebf18b9b8559e5a54951235`.
`context_recovery_budget_exhaustion_surfaces_the_error` failed at
`regressions.rs:2756` in **0.091s** because its immediate stderr
snapshot lacked `context recovery budget spent`. ACP context-error
assertions had already passed. The captured prefix shows all three
budgets **32768 → 16384 → 8192 bytes**, above the 4096-byte floor, and
ends during the third attempt. This is **not evidence of floor
exhaustion**. The collector is an independent Tokio task; a stdout
response is not a stderr barrier. Recovery, harness and test blobs were
identical across the compared base/head/merge parents; no production
regression was implicated.

The shared test Harness now provides a bounded event/condition wait,
registering for collector notifications before reading the buffer to
avoid lost wakeups. The budget and adjacent terminal floor assertions
wait for their own diagnostic and retain the matching snapshot. The
budget test still requires the provider's ACP context error and exactly
three recovery rungs, now corroborated by **exactly four provider
calls** and no floor diagnostic. Timeout remains a real failure with
captured stderr. No fixed sleep, weaker assertion, skip,
provider-limit/logging change, dependency/config edit, or production
change.

**Deterministic causal control:** the same real agent/HTTP-provider/ACP
scenario holds only stderr collection behind a one-shot gate until after
stdout responds. The old immediate snapshot fails the original budget
assertion (intentional exit101); the repaired wait explicitly remains
Pending while held, then passes after release. No scheduler-speed
assumption or fixed sleep. This reproduces the observation race under
controlled delay, **not the exact historical CI schedule**. A
missing-diagnostic test proves the wait actually times out. Original
failure and deliberate failing-control logs/patch remain in
`WORK_LOGS/RUST_TRIAGE_06E32D2D` and `WORK_LOGS/RUST_SYNC_BC5758B9`.

**Final candidate validation:** focused recovery **9/9**, floor **1/1**,
absent-diagnostic timeout **1/1** pass. One full touched-package run,
`cargo test --locked -p buzz-agent`: **695 passed, 0 failed, 1 existing
ignored**, including all **54 regressions**. Local nextest was
unavailable, so this uses the repository-supported cargo-test fallback,
not a claim of nextest reproduction. `cargo fmt --check`, package-scoped
Clippy all-targets with `-D warnings`, differential file-size/policy and
diff checks pass. Previously reviewed availability production and the
Desktop/browser evidence above are unchanged and reused; no all-native
blanket rerun. The test-only delta was self-reviewed against collector
ordering, timeout and falsification evidence. Existing production
approval remains valid for those bytes; exact-new-head technical/CI
clearance is not assumed. The required **Security aggregate** is
distinct from optional Codex advisory feedback; no security
authorization, human review contact, or merge was requested.

**Remaining policy limits:** shutdown submission is not harness
acceptance or process termination; confirmed Offline/no-route local
removal may leave a remote process; route discovery is best effort,
membership cleanup uses `Promise.allSettled`, and multi-instance
deletion is sequential/non-atomic. No distributed singleton,
provider-health, tenant-switch cancellation, live relay TTL or packaged
WebView/VoiceOver certification is claimed. The pre-existing DM-header
raw-presence fallback (`ChannelScreenHeader`/`useActiveChannelHeader`)
remains outside this repair and uncertified. Screenshots above remain
historical mock-browser illustrations, not new deletion or native
transport evidence.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-02 21:53:15 -04:00
Logan Johnson
44d19f59a5 fix(desktop): preserve spacing after multi-word mentions (#7128)
🤖

## Summary

In Buzz Desktop, choosing a multi-word name and immediately continuing a
sentence could swallow the space after the mention: `Hey @Alice
Chenhello`. This keeps the separator, so the same action produces `Hey
@Alice Chen hello` without moving the caret or repairing the name by
hand.

The editor recognizes the complete selected label, including its
internal spaces, and settles the autocomplete caret after the trailing
separator. Deliberately moving left or clicking inside the label still
lets you edit there; this is not a rule that forces every caret to the
end of a mention.

### Related issue

Independent base: `main`. Child:
[#7133](https://github.com/block/buzz/pull/7133), whose disambiguated
labels also contain spaces. Extracted from
[#7114](https://github.com/block/buzz/pull/7114), retained as historical
source (`98fe33ec`).

[Behavior
contract](4fe451d9c2/docs/mention-editor.md).
Originating [Buzz
discussion](buzz://message?channel=f7a9536a-1738-4bad-a888-b3ea25010ef1&id=7aa1f0ab23dce514bd8a0221441cf005bf428914621171472b79747c50820848)
· channel `f7a9536a-1738-4bad-a888-b3ea25010ef1`.

### Testing

Select an existing member named Alice Chen, then type `hello`
immediately. Repeat after ArrowLeft or clicking inside the mention:
typing should follow your chosen caret position.

Mock-browser captures, not live remote-agent evidence:

#### Immediate typing preserves the separator
Choosing the complete label then typing produces `Hey @Alice Chen
hello`.


![separator](https://raw.githubusercontent.com/block/buzz/7258fe2d9f276b93f4d304ac2ac47c450f104157/pr-7128--separator.png)

#### Deliberate caret movement is respected
After ArrowLeft, typing edits at the chosen caret rather than forcing
the caret back beyond the separator.


![intentional-caret](https://raw.githubusercontent.com/block/buzz/7258fe2d9f276b93f4d304ac2ac47c450f104157/pr-7128--intentional-caret.png)

[Original screenshot
publication](https://github.com/block/buzz/pull/7128#issuecomment-5482715543);
immutable image URLs and captions retained here.

#### Evidence and limitations

**5,801 desktop tests**, **45 focused editor tests**, both new browser
regressions, the browser-test build and static/type/size checks passed.
[Applicable CI
passed](https://github.com/block/buzz/actions/runs/33421534320).

The broader browser run had **132 passes / 6 failures**: two
clipboard-origin setup failures and four generic caret-formatting
failures also reproduced on unchanged main. Full local `just ci` stopped
at three native timing/probe failures; a same-head native rerun passed
**3,005 tests** with 18 existing ignores. This is not a full local-CI
pass. The change fixes insertion and caret behavior, not duplicate-name
recipient selection, discovery or invitation.

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-02 21:51:45 -04:00
Arjun Mahanti
ac5a18697c feat(desktop): add persistent Bestie experience (#7223)
## Summary

- add a persistent, community-scoped Bestie designation for local
managed agents
- surface the designated agent in the sidebar, agent library, profile
actions, message toolbar, and draggable floating shortcut
- bloom the floating shortcut into a lightweight compact composer that
reuses the normal DM timeline, reactions, presence, and send behavior
- support message handoff with a bounded snapshot and a full Buzz thread
link so the agent can retrieve the complete conversation

## UX details

- the floating avatar and expanded panel stay above app chrome and drag
as one aligned surface
- closing the expanded panel returns it to its top-right anchor
- each mini-composer opening starts visually fresh while messages sent
during that opening remain conversational
- the designated Bestie's duplicate DM entry is hidden from the regular
DM list
- Bestie actions are suppressed inside the mini timeline to avoid
recursive handoff

## Reliability and maintainability

- preserve existing retention database paths across upgrades
- serialize assignment and deletion, clearing matching assignments
across community scopes before an agent is removed
- fence async conversation resolution against workspace and assignment
changes
- validate stale assignments against existing local agents before hiding
DMs
- share lightweight assignment state across agent cards and keep
protected-feature behavior out of the shared timeline API

## Testing

- `pnpm --dir desktop test` — 5,886 tests passed
- `pnpm --dir desktop exec tsc --noEmit`
- `pnpm --dir desktop exec biome check ...`
- `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml
--all-targets -- -D warnings`
- focused native retention and Bestie assignment/command tests — 34
passed
- `VITE_BUZZ_BESTIE=1 pnpm --dir desktop build:e2e`
- `VITE_BUZZ_BESTIE=1 pnpm --dir desktop exec playwright test
--project=smoke tests/e2e/bestie.spec.ts`
- pre-commit and differential pre-push hooks

## Rollout

The UI remains gated by the `bestie` build feature. Screenshots covering
setup, empty, assigned, floating, and message-handoff states are
included in the PR discussion.

---------

Signed-off-by: Arjun Mahanti <arjun@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-02 17:56:17 -04:00
Taylor Ho
b6dc533b7c fix(composer): align wrapped inline chip fragments (#7242)
**Category:** fix
**User Impact:** Wrapped channel and mention chips in the chat composer
now align continuation text with the chip edge while keeping the icon on
the first line.

**Problem:** Plain composer decorations used absolute icons plus cloned
icon-sized padding, so every wrapped fragment inherited an empty icon
gap and the icon aligned against the union of all lines. **Solution:**
Keep the icon in the first fragment's inline flow and restore normal
chip padding on continuation fragments, while explicitly leaving the
separate wrapping Buzz-link and sent-message rendering paths unchanged.

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

**desktop/src/shared/styles/globals/composer.css**
Scopes in-flow icon geometry and normal continuation padding to plain
composer mention and channel decorations, excluding wrapping atom-link
chips and preserving the human-icon vertical correction.

**desktop/tests/e2e/mentions.spec.ts**
Adds a rendered narrow-composer regression that checks two fragments,
static icon geometry, first-line icon space, and continuation-line
alignment to ordinary chip padding.

</details>

## Reproduction steps

1. Open a channel in Buzz Desktop.
2. Narrow the chat composer enough for `#all-replies` to wrap.
3. Confirm the channel icon occupies only the first line and `replies`
starts at the chip's normal left padding rather than an icon-sized
inset.
4. Send or view a long inline Buzz chip in the message list at a
constrained width.
5. Confirm its icon remains attached to the leading fragment and its
remaining label continues cleanly on following lines.

## Screenshots

**Composer — wrapped `#all-replies` channel reference**

![Wrapped channel reference in the narrow dark-theme
composer](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/7242/composer-wrapped-channel.png)

**Message list — existing wrapped inline-chip rendering preserved**

![Wrapped repository chip in a sent message at constrained
width](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/7242/message-list-wrapped-chip.png)

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-09-02 14:08:12 -07:00
Logan Johnson
83e5962984 fix(desktop): keep explicit agent profiles bound to their exact key (#7131)
🤖

## Summary

In Buzz Desktop, clicking a message from stopped agent A could open
running agent B—and B's controls—because both shared a persona (an agent
definition). This now opens the author you clicked and only that agent's
own controls, so you can inspect an old message without being redirected
to a different running agent.

An explicit public key—the identifier for one agent—now stays exact
across message authors, members, DMs, deep links and Instances rows,
including stopped, archived and relay-only agents. Local controls come
only from a matching local record for that key. A relay-only A cannot
borrow B's Start/Stop/Edit controls or configuration.

Deliberately opening a **persona** is different: it can still select a
representative that respects archived instances or offer Start when none
remains. The change removes competing historical-persona redirects
rather than adding another identity exception.

### Related issue

Independent base: `main`; no stack parent or child among the
replacements. Extracted from
[#7114](https://github.com/block/buzz/pull/7114), retained as historical
source (`98fe33ec`).

[Behavior
contract](9c4b6523ce/docs/agent-profile-identity.md).
Originating [Buzz
discussion](buzz://message?channel=f7a9536a-1738-4bad-a888-b3ea25010ef1&id=7aa1f0ab23dce514bd8a0221441cf005bf428914621171472b79747c50820848)
· channel `f7a9536a-1738-4bad-a888-b3ea25010ef1`.

### Testing

Synthetic Playwright mock-bridge state. After screenshots exercise this
independent profile extraction (`df6612b1`); no availability or
cloud-marker implementation is included.

#### Before: historical A redirects to running B
Unchanged main product code (`bc006f67`) with the same updated
historical-message fixture fails: clicking Earlier Parity Agent opens
Current Parity Agent and its Stop control.


![01-before-historical-redirect](https://raw.githubusercontent.com/block/buzz/2acd989f6eae9e7bc1ce5ae5eeafcbc1704f605f/pr-7131--01-before-historical-redirect.png)

#### After: historical A opens A
The clicked author remains Earlier Parity Agent, with A's public key and
its own Start control. The current sibling is not substituted.


![02-after-historical-exact](https://raw.githubusercontent.com/block/buzz/2acd989f6eae9e7bc1ce5ae5eeafcbc1704f605f/pr-7131--02-after-historical-exact.png)

#### Exact relay-only A while local sibling B exists
A's public key and owner-scoped profile are visible; no local
Start/Stop/Edit/Add control or sibling definition is borrowed.


![03-exact-relay-with-local-sibling](https://raw.githubusercontent.com/block/buzz/2acd989f6eae9e7bc1ce5ae5eeafcbc1704f605f/pr-7131--03-exact-relay-with-local-sibling.png)

#### Explicit persona navigation may select local B
Deliberately opening the persona selects its local representative, with
B's key and legitimate Stop/Restart/Edit controls.


![04-explicit-local-persona](https://raw.githubusercontent.com/block/buzz/2acd989f6eae9e7bc1ce5ae5eeafcbc1704f605f/pr-7131--04-explicit-local-persona.png)

#### Explicit persona without an instance may offer Start
This is a deliberately opened persona, not a relay-only key turned into
a persona surface.


![05-explicit-persona-without-instance](https://raw.githubusercontent.com/block/buzz/2acd989f6eae9e7bc1ce5ae5eeafcbc1704f605f/pr-7131--05-explicit-persona-without-instance.png)

[Original screenshot
publication](https://github.com/block/buzz/pull/7131#issuecomment-5482310890);
all five immutable image URLs and captions retained here. The final
documentation-only commit does not change this UI. These are synthetic
browser fixtures, not live runtime health evidence.

To check manually, open an old message from stopped A while same-persona
B is running; compare the displayed key and controls. Then open the
persona itself and verify that representative selection still works.

#### Evidence and limitations

**5,793 desktop tests**, **56 profile/archive browser cases**,
type/static/size checks and repository-wide `just ci` passed. The
historical-message regression fails on unchanged main by opening B
instead of A. [Published-head CI
passed](https://github.com/block/buzz/actions/runs/33422207592).

The [advisory security
check](https://github.com/block/buzz/actions/runs/33422240973) timed out
without a result; it is not a passing check. No availability,
cloud-marker, discovery or mention-routing change is included. These
screenshots do not establish remote delivery, agent execution or
termination.

#### Security authorization history (audit, not clearance)

The [security
gate](https://github.com/block/buzz/pull/7131#issuecomment-5482300054)
remains visible and unresolved. Existing authorization-request comments
were posted by `loganj`: [old-head
request](https://github.com/block/buzz/pull/7131#issuecomment-5482311163)
for `df6612b1db5a6f8d128cef955fd66a80b6828cb8` at 2026-08-31 17:55:11
UTC, then [current-head
request](https://github.com/block/buzz/pull/7131#issuecomment-5482320424)
for `9c4b6523ceaef0f3d92906fcdb5d9a3b9ede7e17` at 17:55:57 UTC. The
existing [issue-comment workflow
run](https://github.com/block/buzz/actions/runs/33422240973) ended
cancelled after the previously reported timeout; it did not produce a
completed security review. Latest exact-head Run/Post Codex jobs are
skipped, not security approval. Historical comments remain available at
their original links; consolidating their audit here does not withdraw
authorization or clear the gate. An authorized security workflow owner
must arrange the missing exact-range result.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-02 16:20:49 -04:00
Logan Johnson
434dafe958 fix(desktop): discover authenticated owned relay agents (#7122)
🤖

## Summary

An agent you own could be missing from **New message → To:** and
**Channel members → Add people and agents** on a machine that has never
managed it. This PR lets those existing lists find your agent without
requiring a shared channel first. Desktop now checks records proving you
own it, rather than looking only at agents in channels you've already
joined.

**No new screen or control is added.** For example, an agent with
verified ownership and **Who can send instructions → Only me (default)**
can now appear even with no shared channels. Each screen still applies
its existing access rules; this does not make every discovered agent
selectable everywhere.

| Screen / control | Before | After this PR alone |
| --- | --- | --- |
| **New message → To:** recipient picker | An owned agent absent from
this machine and shared-channel bot lists could be missing. | Its named
**agent** row can appear; selecting it adds a recipient chip. This is
recipient selection, not a guarantee that a later message will reach or
wake the agent. |
| **Channel members → Add people and agents** | The same agent could be
missing from **Not in this channel** search results. | Its row can
appear with the existing **Add** button. If you can add members, that
button submits the existing channel-membership request; finding the row
alone changes no membership. |
| **Stream / forum composer → @ suggestions** | An owned agent already
in the channel under an ordinary member role could be missing from agent
suggestions. | Its actual membership is recognized without requiring the
bot role. Agents not managed on this device still need membership in
that channel. |
| **Pulse → Agents** | An agent absent from both local management and
the server's agent list was omitted from the count and author lookup. |
The count and feed's author lookup can include it; notes appear only if
it has published them. |

Being listed does **not** mean the agent is online, add it to a channel,
or grant local Start/Edit controls. For agents not managed on this
device, global **Search** still excludes those configured for “Only me”,
and DM @ selection is not added here. DM @ selection and message-driven
nonmember invitation are addressed in
[#7124](https://github.com/block/buzz/pull/7124); the standalone forum
**Invite / Cancel** flow is in
[#7125](https://github.com/block/buzz/pull/7125).

<details>
<summary>Ownership and membership checks</summary>

A discovery lead is not proof: the latest agent profile must have a
valid signature and exactly one valid ownership attestation—the owner's
signed link to that agent. Its response policy must be signed by that
verified owner; an invalid latest policy cannot restore an older
permission. Membership comes separately from the latest server-signed
roster, including removals.

Existing profile cards, owner labels and agent-avatar shapes also use
this stricter verification: malformed or forged evidence must not supply
ownership/agent classification on its own. Valid ownership was already
recognized; no profile-picture or badge design changes.

Attestation time conditions apply to the signed event's timestamp, not a
live expiry timer. Existing legacy compatibility and builds requiring
verified owner policy retain their respective rules. Discovery and
sending remain separate operations, not an atomic permission check.

</details>

### Review corrections

- When runtime and owner policy overlap, **explicit online/away/offline
from the verified latest runtime is retained**. Policy still supplies
ownership/permissions; claimed runtime membership is not restored.
Missing/unrecognized status stays unknown, and invalid latest policy
cannot revive runtime permissions.
- Discovery without runtime evidence is now **unknown**, not offline:
native conversion, both IPC adapters, Pulse, Projects and
profile/session consumers preserve that distinction. Unknown has no
status dot and is not promoted to a deployed/running agent.
- Both relay-only picker paths retain the authenticated owner, including
the existing **managed by you** label. The analogous global Search
projection is fixed without changing its existing “anyone” filter.
- Authorized stored profile activity remains visible when liveness
becomes unknown/absent or the active turn ends. History reads do not
start a live subscription, grant access, or imply current availability.

### Related issue

Independent base: `main`. Child:
[#7124](https://github.com/block/buzz/pull/7124), then
[#7125](https://github.com/block/buzz/pull/7125). Extracted from
[#7114](https://github.com/block/buzz/pull/7114), retained as historical
source (`98fe33ec`).

[Behavior
contract](3a56d17824/docs/owned-agent-discovery.md).
Originating [Buzz
discussion](buzz://message?channel=f7a9536a-1738-4bad-a888-b3ea25010ef1&id=7aa1f0ab23dce514bd8a0221441cf005bf428914621171472b79747c50820848)
· channel `f7a9536a-1738-4bad-a888-b3ea25010ef1`.

### Testing

Current candidate: `3a56d17824522580fe04cae463b54f4c7ba66021`, a
four-file native/test/doc runtime-status repair atop published
`ae23c1c9680a881cee7eed94e259bf15bf8ce3f7`. Branch ancestry is main
`1c8321cd08feb597f8bcff5195c21148fb3e98ed`; refreshed main
`0e878664b08cdf7fb2d89d940bc2aa92cdc485f7` adds only the independent
CI-workflow split. Read-only mergeability succeeds; this is not a tested
merged-tree claim.

**Local CI attempt and continuation (not an uninterrupted green run):**
the new exact-head `just ci` passed formatting/static checks, workspace
and Tauri clippy, workspace Rust tests, **5,910 desktop tests**, desktop
production build and Tauri check. Its native main target finished
**3,073 passed / 1 failed / 19 ignored** (exit 101):
`cheap_discovery_reports_absent_before_any_forced_probe` saw a
process-global login-shell counter of 2 instead of 0. The counter
includes unrelated version/adapter probes whose tests do not hold the
failed test's PATH mutex; no managed-agent discovery implementation
changed in the runtime repair. The unchanged failing test then passed
**three isolated invocations**. Only the failed native workspace lane
was retried with `RUST_TEST_THREADS=1 just desktop-tauri-test`: **3,074
main-target tests passed / 19 ignored**, all additional workspace
targets passed (exit 0). The previously unrun `just web-build
mobile-test` tail then passed (exit 0; **2,019 mobile tests**). Earlier
successful lanes were reused; no source/guard changes or blanket CI
rerun. The original failure and all diagnostic/retry logs are retained.

- **71 native `nostr_convert` tests pass**, including seven new
production merge regressions: online/away/offline, missing/invalid
status, policy-only, status-less latest replacement and forged latest
replacement. Before production repair, those seven yielded **4 failures
/ 3 passing controls**.
- Reused frontend evidence from `ae23c1c9` (frontend is unchanged):
Desktop TypeScript and isolated E2E build pass; **9 browser tests / 0
retries**, covering both relay-only picker journeys and seven adjacent
stop-control regressions. Real UI with mock Tauri IPC, not live
relay/native webview.
- Earlier `ae23c1c9` local `just ci` passed without failures, including
3,067 native main-target tests / 19 ignored and 2,019 mobile tests; not
substituted for the new source gate above.
- Reused unchanged repair evidence: **17 real-store/hook history
regressions**, **161 focused tests**, and independent **9 mounted
owner/bot/identity revocation/regrant transitions** with zero hook-phase
native calls. The regression was falsified before repair (14 failures, 3
controls).
- Signed local-server fixtures cover discovery with no local/shared
record, ordinary-role membership, forged ownership, invalid signatures,
duplicate authentication, wrong-owner/latest-invalid policy, revoked
membership and wrong destinations. These establish native data checks,
not a live agent response.

GitHub checks and renewed technical/security review must apply to the
current published head; earlier-head green checks are not
replacement-head proof. Local source review is not formal
code-owner/latest-push approval or exact-range security authorization. A
green security workflow with substantive review skipped is not security
clearance.

### Screenshots

#### Relay-only picker evidence —
`ae23c1c9680a881cee7eed94e259bf15bf8ce3f7`

These cropped rows come from the two real production picker journeys in
[`owned-agent-discovery.spec.ts`](ae23c1c968/desktop/tests/e2e/owned-agent-discovery.spec.ts),
using mock Tauri IPC with **no local agents and no user-search
duplicate**. The fixture supplies verified-owner data and unknown
availability; the browser test checks its presentation, not native
signature verification. Both exact-tip journeys pass without retries. No
live relay, native webview, invitation, delivery or wakeup is claimed.

Before the repair, both relay-only candidate constructors discarded the
owner, so the existing “managed by you” label was absent. These are
after-repair captures; no before image was captured.

#### New Message → To
The relay-only agent retains its authenticated owner label.


![new-message-owner](https://raw.githubusercontent.com/block/buzz/536c6d3c90776cf85b1d5ce58666f2c8ad518829/pr-7122--new-message-owner.png)

#### Channel members → Add people and agents
The matching result retains “managed by you” beside the existing Add
action; the test does not click Add or claim membership changed.


![member-add-owner](https://raw.githubusercontent.com/block/buzz/536c6d3c90776cf85b1d5ce58666f2c8ad518829/pr-7122--member-add-owner.png)

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-02 16:18:54 -04:00