Commit Graph

1244 Commits

Author SHA1 Message Date
Duncan
adb3d17c19 fix(sync): key the sidebar durable outbox per window
localStorage has no atomic compare-and-delete or transactional
read-modify-write, so a single outbox key shared across every window
could not be mutated safely: one window's read-then-write or
read-then-remove races a peer's write in the gap and drops its
still-unpublished edit. A per-write ownership token narrowed that window
but could not close it — the token proves what was read, not what is
still stored at the destructive op.

Key the outbox per window instead: <prefix>:<pubkey>:<relay>:<nonce>,
where the nonce is minted once and parked in sessionStorage. Each window
is the sole writer of its own key, so a hot-path write is one
unconditional setItem — the write race is designed out, not guarded.
Resume enumerates every window's key: merge lanes (stars/mutes) fold all
records order-independently; whole-blob lanes (sort/sections) replay the
max-queuedAt record with a nonce tiebreak. Redundant foreign keys are
reclaimed at boot, gated on durable relay evidence (merge: head subsumes;
whole-blob: head created_at supersedes) and re-read immediately before
removal so a live peer's fresh write in the recheck gap survives.
Reclamation runs only on a successful head fetch, never on a failed one.
The token contract is deleted entirely — ownership is the key.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-26 19:38:44 -04:00
Duncan
9141fe2922 fix(sync): make the sidebar durable outbox multi-window safe
Sections, stars, mutes, and sort each persist an unpublished edit under one
localStorage outbox key per identity+relay, but generation ownership is only
in-memory per window. Without cross-window ownership, one window's completing
publish could clear a peer window's still-unpublished edit, and (on the merge
lanes) a peer's write could overwrite an edit before it published.

Every outbox write now mints an ownership token and stores a {store, token}
envelope; a completing publish compare-and-clears only when the stored token
still matches its own, so a peer's newer write survives an older window's ACK.
Stars and mutes additionally read-merge-write both the durable outbox and the
main store via their per-entry mergeStores, so two windows editing different
channels both survive; sort and sections replace whole-blob with LWW resolution
matching the relay. Sort also gains the durable outbox + bounded retry it
previously lacked, and stars/mutes/sort now clamp publish created_at inside the
relay's future-drift window like sections. The envelope reader tolerates a
legacy token-less entry so an outbox written by a prior build still resumes.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-26 12:45:28 -04:00
Duncan
c7933fd9da fix(sync): clamp publish timestamps and give sort prefs the durable lane
Carl's re-review found three edit-loss paths remaining after the
stars/mutes rev-merge landed.

[P2] Stars, mutes, and sort stamped createdAt = lastRemoteCreatedAt + 1
uncapped, so a self-authored head accepted near the relay's +900s drift
boundary wedged every later publish until wall time caught up. Extract
the sections clamp into a shared clampPublishCreatedAt in
sidebarSyncWatermark.ts (all four surfaces already import that module)
and wire sections/stars/mutes/sort to it.

[P1] Sort preferences never got the durable lane: doPublish only logged
on failure, destroy() discarded the pending edit, and a live remote
cancelled the debounce with the edit stranded. Port the sections lane —
durable outbox + bootstrap resume, generation/CAS ownership, single-
flight + completion re-drive, 2s->30s backoff, 60s reconcile loop, and
pending-aware remote application. Sort stays whole-blob LWW; a lost head
is adopted at pre-publish rather than republished.

Tests: a clamp test per surface, and sort lifecycle coverage — outbox
resume after destroy-inside-debounce, retry without a later edit,
overlapping-generation safety, live-remote-during-debounce adopt, and a
hook-level pending-defer test.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-26 11:35:16 -04:00
Duncan
16aeab68b0 test(sidebar): make the two accepted-residual tests faithful to their contract
The hard-eviction-branch test asserted only the rev tuple outcome on two
one-entry stores while its comment claimed the >500-entry eviction/remount
setup. Build the real fixture: >500 equal-updatedAt entries so the bound
evicts the target by the id tiebreak, then a remounted rev-1 click merged
against the retained rev-100 remote, asserting the deterministic rev-100
outcome in both merge orders. Stars and mutes.

The unobserved-future mixed-fleet residual was stated but never exercised
directly: the fast-clock suites cover only the observed-future fix. Add a
click with an empty high-water at t, then a genuinely unobserved
opposite-value head at t+300 that wins on the primary updatedAt key. Both
hook suites.

Test-only; no production source changes.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-26 10:54:32 -04:00
Duncan
439df98240 refactor(sidebar): fold channel stars/mutes onto Lamport-rev max-merge
Replace the LWW register plus ownership/dirty-set/canonical-supersession
machinery with a per-entry Lamport `rev` and a single max-merge on every
path, mirroring the read-state data model. Each entry carries an additive
optional `rev` (missing implies 0; payload stays `version: 1` so older
builds keep parsing our blobs). One `mergeStores` orders by
updatedAt then rev then the starred/muted-true leaf, and ends in the
500-entry bound.

Clicks stamp `updatedAt = max(now, localEntry?.updatedAt ?? 0,
maxUpdatedAtSeen(id))` and mint `rev = max(localEntry.rev, maxRevSeen(id))
+ 1`, so a click strictly dominates every state its replica has observed
and cannot lose to a same-second remote. The sync managers hold a
per-channel two-field high-water map fed by a single `observe()` on every
ingest path. Stars sync keeps the generation-CAS + single-flight lane and
bounded-backoff retry plus a durable outbox so an in-flight publish can
never clear a newer pending edit; mutes sync mirrors it. Remote ingestion
never touches the pending lane. Deleted: mergeApplyingRemote,
mergeStoresWithTie, mergeCanonicalSupersession, dirtyChannelIds,
lastAppliedRemoteTs/lastAppliedEventId, and the event-clock branch.

Sections and sort are untouched.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-26 10:54:32 -04:00
Duncan
51af859759 fix(sidebar): overlay local edits on canonical correction and fold ambiguous-ACK heads
Round-4 client-side convergence fixes for channel-sections/stars/mutes sync,
closing two silent edit-loss variants that survived publish serialization.

Ambiguous-ACK fold: a publish whose ACK is lost may still have been accepted
by the relay. Retain each attempt's signed id; when a later cycle's pre-publish
fetch returns a head whose id matches a prior attempt, fold it forward as our
own accepted predecessor and publish above it instead of adopting it away and
erasing the queued edit. A head the relay never accepted can never surface by
id, so the fold is proof-gated on an exact id match.

Canonical-supersession dirty overlay (stars + mutes): a lower-id canonical
correction that arrives after a same-second local click must not clobber the
click. Apply the correction to the prior remote layer, then overlay entries
changed locally since that layer; never cancel a pending publish merely because
a correction arrived.

Client-only: loss discovery relies on the existing pre-publish fetch, live
subscription, and reconcile loop rather than a relay conflict signal.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-26 10:54:32 -04:00
Duncan
d94b8bb22b fix(sidebar): serialize section publishes and scope remote-tie merge
Each prior round patched one cross-generation interleaving and opened
another a layer deeper. Kill the race class structurally instead.

Sections: serialize publish cycles (one in-flight at a time; a newer edit
queued mid-cycle defers and the completion re-drives it). The per-edit
pre-publish baseline is frozen at queue time, so a genuine remote observed
during the debounce window still adopts, while our own accepted head is
folded forward via canonicalMax so a stale generation's own write is never
mistaken for a competing remote and adopted away. Dual generation guards in
doPublish (post-fetch and pre-publish) stop a stale generation signing or
publishing after a newer edit exists.

Stars/mutes: scope mergeApplyingRemote (remote-wins on entry-tie) and the
pending-publish cancel to fire only on a canonical supersession of an
already-applied same-timestamp larger-id head. Every other application
(bootstrap/live/newer-timestamp) keeps local-wins mergeStores and does not
cancel the pending publish, so a later same-second local click is no longer
clobbered by an older remote entry that decrypts late.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-26 10:54:32 -04:00
Duncan
53ba5ab4b1 fix(sidebar): close pre-publish and per-entry merge convergence holes
Two convergence holes one layer under the pass-1 fixes:

Sections: the pre-publish head check compared the fetched head against
the mutable lastRemoteCreatedAt, which a live event observed during the
debounce window already advanced to that same head — equality fell
through to publish and the local blob overwrote a remote that became
head after the edit was queued. Freeze a canonical head baseline
(created_at, id) at publishSections and compare the fetched head against
that generation baseline instead, adopting when the head advanced.

Stars/mutes: applyRemote admits the canonical lower-id winner but then
mergeStores resolved equal per-entry updatedAt as local/prev-wins, so a
stale larger-id value delivered first survived and undid the winner. Add
mergeApplyingRemote which resolves an entry-timestamp tie toward the
canonical incoming blob while keeping strictly-newer local entries.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-26 10:54:32 -04:00
Duncan
d5d83e0909 fix(sidebar): correct equal-timestamp tie-break in stars, mutes, sort
useChannelStars, useChannelMutes, and useChannelSortPreference carried the
same inverted equal-timestamp comparator as channel sections: applyRemote kept
the largest event id, opposite the relay/database canonical order (created_at
DESC, id ASC -> lowest id wins). Two devices writing the same second could
leave the UI showing an event the relay did not store.

Apply a strictly-lower id and ignore ids >= the last applied, matching the
sections fix and the relay winner across all four 30078 sidebar surfaces. Each
hook gains a regression test: larger-then-lower id delivery at equal timestamp,
lower-id store wins (mutation-checked - reverting >= to <= fails each).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-26 10:54:32 -04:00
Duncan
1046e88904 fix(sidebar): close pending-edit convergence races in channel sections
Three cross-layer races defeated the one-convergence-mechanism design:

- An older in-flight publish unconditionally cleared pending state on
  completion, erasing a newer edit queued mid-flight. Each pending edit now
  carries a monotonic generation; a completion clears pending/outbox/retry only
  via compare-and-swap on the generation it published.
- Hook-level remote application (bootstrap/live/periodic) cancelled the pending
  publish's timers without deciding supersession, stranding the durable outbox
  and clobbering the optimistic edit. applyRemote now defers entirely to a
  pending edit, whose own debounced publish converges via publish-or-adopt; the
  manager's adopt path clears pending before write-through so the winning remote
  still applies.
- The equal-timestamp tie-break kept the largest event id, opposite the
  relay/database canonical order (created_at DESC, id ASC → lowest id wins).
  applyRemote now applies a strictly-lower id and ignores ids >= the last
  applied, so the UI converges on the event the relay actually stored.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-26 10:54:32 -04:00
Duncan
a05280f164 fix(desktop): converge channel sections on conflict and never drop edits
Channel-section sidebar state diverged between a user's devices and
sometimes never self-healed. Four client-side gaps fed the divergence:

- A local edit that lost whole-blob LWW was silently republished as remote
  content while the UI kept showing the edit. Now the manager adopts the
  winning remote head (writes it through to state + storage, advances the
  watermark) and skips publishing, unifying with the relay's OK-false
  conflict path as one convergence mechanism.
- Edits made inside the 2s publish debounce were dropped on quit or
  community switch. A durable localStorage outbox persists every edit
  synchronously and resumes it on next mount; adopt clears the outbox so a
  superseded edit can never be replayed.
- A skewed remote head could push the published createdAt past the relay's
  future-drift window and wedge all later publishes. createdAt is now
  clamped inside that window.
- Stale-at-open state waited for a reconnect that a healthy socket never
  fires. A reconciliation loop periodically refetches the head (steady 60s,
  backoff on failure) and refreshes on window visibility.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-26 10:54:31 -04:00
Matt Toohey
db514b153a fix(desktop): stop pulsing addressed agents on send (#6873)
## Summary

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

### Related issue

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

### Testing

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

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

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

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

## Before / After

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

## Testing

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

## Duplicate check

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

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

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

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

Edge cases handled:

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

## Testing

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

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


## Demo

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


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

---------

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-08-26 15:19:44 +10:00
klopez4212
8471049c43 feat(desktop): add KLIPY GIF search to composers (#5554)
## Summary

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

## Relay-to-provider boundary

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

## Accessibility

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

---------

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

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

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

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

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

</details>

## Reproduction steps

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

## Testing

At `a179a2d00a6ac5cf0bc4a1c7c1ef3d5dfa9d0875`:

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

## Visual validation

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

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

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

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

</details>

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
2026-08-25 15:59:38 -07:00
Taylor Ho
b58de7cfa7 fix(desktop-messages): preserve inline agent mentions with persistent addressing (#6793)
**Category:** fix
**User Impact:** Users can place `@Agent` mentions inline anywhere in a
message—even when that agent is also persistently addressed—so separate
agents can receive separate instructions in the same message.

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

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

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

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


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


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

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

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

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

</details>

## Reproduction steps

### Inline mentions without persistence

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

### Inline mentions with persistence

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

## Demo

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

### Related issue

N/A — reported through the Buzz feature room.

### Testing

At `a4579c0a666f3644abe44271614186d7b9356bf5`:

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

---------

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

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

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

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

### Screenshots

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

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

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

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

### Reproduction steps

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

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

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

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

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

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

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

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

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

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

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

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

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

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

</details>

---------

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

## Scope contract

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

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

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

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

---------

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-25 11:34:16 -07:00
tulsi
12f3fea26e revert fixed mention highlight (#6716)
**Category:** fix
**User Impact:** Mention chips now follow the active theme without a
fixed yellow fill and remain readable across every supported theme.

**Problem:** PR #6696 added an opaque yellow treatment to every human
and agent mention. Removing that fixed color exposed another issue: some
theme accent colors do not provide WCAG AA contrast when also used as
normal-size mention text.

**Solution:** Remove the fixed yellow treatment and keep the active
theme accent as a subtle translucent chip background. Mention labels use
a light/dark semantic foreground selected for readable contrast,
including hover states.

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

**desktop/src/shared/styles/globals/markdown.css**
Removes the fixed-yellow treatment, retains theme-derived accent
backgrounds, and applies an AA-safe semantic foreground to human and
agent mention labels in resting and hover states.

**desktop/src/shared/styles/globals/theme.css**
Removes the obsolete fixed-yellow tokens and defines light/dark mention
foreground values.

**desktop/tests/e2e/mentions.spec.ts**
Adds rendered contrast coverage for resting and hover states across
every supported syntax theme, compositing translucent chip and ancestor
surfaces before checking the 4.5:1 threshold.

</details>

## Reproduction steps

1. Open a message containing a human or agent mention.
2. Confirm its subtle background follows the active theme accent rather
than using yellow.
3. Hover the mention and confirm the accent background strengthens while
the label remains readable.
4. Switch between default, light, dark, and custom syntax themes and
confirm mention labels remain legible.

This is a focused visual rollback of the mention treatment introduced by
#6696. The channel notification badge changes from that PR remain
intact.

---------

Signed-off-by: tulsi <tulsi@block.xyz>
2026-08-25 09:49:43 -07:00
tulsi
29f2054c69 highlight search terms in results and messages (#6702)
**Category:** fix
**User Impact:** Search terms are now highlighted in yellow in Cmd+F
results and in the message opened from a result.

**Problem:** Search returned relevant messages, but users still had to
reread each preview and destination message to discover where the query
appeared. Search navigation could also lose or misapply highlighting
during rapid typing, repeated navigation, thread opening, and forum
navigation.

**Solution:** Render match-focused previews with a shared yellow
treatment, carry the result query through navigation, and apply it only
to the clicked destination. The implementation binds highlights to the
debounced result set, supports token and prefix matching, and handles
channels, threads, forums, diffs, and wave messages consistently.

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

**desktop/src/app/AppShell.tsx**
Carries the active result query into search-hit navigation so the
destination can preserve the user's context.

**desktop/src/app/navigation/searchHitEventCache.ts**
Adds a bounded, one-shot cache for result queries and navigation IDs
alongside cached search-hit events.

**desktop/src/app/navigation/searchHitNavigation.test.mjs**
Covers query retention, one-shot consumption, repeated same-route
activations, forum posts, forced routing, and cancellation.

**desktop/src/app/navigation/searchHitNavigation.ts**
Associates each search-result activation with a unique navigation ID and
forwards it to channel or forum destinations.

**desktop/src/app/navigation/useAppNavigation.ts**
Extends channel and forum navigation to carry search navigation state
without changing normal navigation behavior.

**desktop/src/app/routes/ChannelRouteScreen.tsx**
Consumes search highlight state after navigation and retains it for the
selected message while clearing it on community context changes.

**desktop/src/app/routes/channels.$channelId.posts.$postId.tsx**
Validates and forwards forum search-navigation IDs.

**desktop/src/app/routes/channels.$channelId.tsx**
Validates and forwards channel search-navigation IDs.

**desktop/src/features/channels/ui/ChannelPane.tsx**
Passes the selected result ID and query into the main timeline and open
thread panel.

**desktop/src/features/channels/ui/ChannelPane.types.ts**
Defines the destination-highlight contract for channel panes.

**desktop/src/features/channels/ui/ChannelScreen.tsx**
Routes destination highlighting to either forum content or the channel
timeline.

**desktop/src/features/channels/ui/ChannelScreen.types.ts**
Defines route-level search-highlight inputs.

**desktop/src/features/channels/ui/ForumChannelContent.tsx**
Forwards search context into expanded forum threads.

**desktop/src/features/forum/ui/ForumThreadPanel.tsx**
Highlights the matching text in the selected forum post or reply.

**desktop/src/features/forum/ui/ForumView.tsx**
Carries selected-result context from forum routing into the thread
panel.

**desktop/src/features/messages/ui/DiffMessage.tsx**
Forwards the query into diff descriptions and rendered diff content.

**desktop/src/features/messages/ui/DiffViewer.tsx**
Provides a highlighted match excerpt for structured diffs and highlights
fallback raw diff text.

**desktop/src/features/messages/ui/MessageRow.tsx**
Passes destination queries into normal Markdown, diff, and wave message
renderers.

**desktop/src/features/messages/ui/MessageThreadPanel.tsx**
Applies highlighting to the selected thread head or reply without
tinting unrelated messages.

**desktop/src/features/messages/ui/WaveMessageAttachment.tsx**
Highlights matches in wave-message fallback text.

**desktop/src/features/search/lib/searchMatch.test.mjs**
Covers case-insensitive, literal, multi-term, prefix, one-character, and
late-preview matches.

**desktop/src/features/search/lib/searchMatch.ts**
Centralizes token extraction, case-insensitive match splitting, and
match-focused preview generation.

**desktop/src/features/search/ui/HighlightedSearchText.tsx**
Adds the reusable accessible mark renderer used by result and
specialized message surfaces.

**desktop/src/features/search/ui/TopbarSearch.tsx**
Highlights result previews, centers excerpts around matches, strips
search operators, and hides stale results during debounce transitions.

**desktop/src/features/sidebar/ui/AppSidebar.types.ts**
Updates the sidebar search callback contract to include the result
query.

**desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx**
Passes query-aware result selection through the pinned search entry
point.

**desktop/src/shared/lib/rehypeSearchHighlight.ts**
Uses the shared matcher to mark every matching Markdown text segment
while leaving code blocks untouched.

**desktop/src/shared/lib/searchHighlightStyle.ts**
Defines one yellow highlight treatment for light and dark themes.

**desktop/src/shared/ui/markdown/nodeCache.test.mjs**
Verifies all Markdown matches, one-character scoped searches, code
exclusion, and transient cache behavior.

**desktop/src/shared/ui/markdown/nodeCache.ts**
Enables transient highlighting for one-character scoped searches without
polluting the Markdown cache.

**desktop/tests/e2e/smoke.spec.ts**
Exercises result and destination highlighting, same-route forum
activation, and stale-query suppression end to end.

</details>

## Reproduction steps

1. Open a channel containing a longer message and press Cmd+F.
2. Search for a word that appears later in the message, such as
`mentions`.
3. Confirm each matching result shows the word in yellow and keeps the
match visible in its preview.
4. Open a result and confirm the same term is highlighted in yellow in
the destination message.
5. Repeat with a thread reply or forum post, and quickly change the
query to confirm stale results are not selectable.

## Demo

- **Before:** Search results returned the correct message but offered no
visual indication of where the query matched; opening the message also
showed no match treatment.
- **After:** Every matching term is marked in yellow in the result
preview and in the exact channel, thread, or forum message opened from
that result.

---------

Signed-off-by: tulsi <tulsi@block.xyz>
2026-08-25 16:22:12 +00:00
Kalvin C
9b6a637d01 fix(desktop): make lightbox zoom controls interactive (#6710)
## Summary

- Replace the decorative lightbox zoom icons with accessible Zoom out
and Zoom in buttons.
- Route button clicks through the existing 1x–3x, 5% stepped zoom state
without dismissing the viewer, including clicks on SVG icon descendants.
- Extract the zoom toolbar so `markdown.tsx` remains below the
repository file-size threshold.

### Testing

- `pnpm build:e2e`
- `pnpm exec playwright test image-attachment-gallery.spec.ts
--project=smoke` — 10 passed
- `pnpm typecheck`
- Full desktop unit suite — 5,397 passed
- Pre-push checks — file-size gate, desktop checks, typecheck, and
desktop tests passed
- `git diff --check`

### UI evidence

The rendered lightbox flow is covered by the gallery smoke test: Zoom
out is disabled at 100%, Zoom in changes the value to 105%, Zoom out
returns it to 100%, and the dialog remains open throughout. Existing
gallery, keyboard, spoiler, context-menu, and close-path assertions also
pass.

---------

Signed-off-by: sanic <c57bf9b4275088b2b33db7f746975407210f159fbd8bd733c0e532375f69aa80@buzz.block.builderlab.xyz>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Co-authored-by: sanic <c57bf9b4275088b2b33db7f746975407210f159fbd8bd733c0e532375f69aa80@buzz.block.builderlab.xyz>
Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
2026-08-25 09:09:13 -07:00
klopez4212
bb5b9357a7 Fix TipTap editor mount race (#6779)
## Summary

- wait for TipTap's mount lifecycle before reading the selection
formatting tray's editor DOM
- detach and reattach DOM listeners across editor view unmounts and
remounts
- cover mounted and pre-mount editor view access

## Testing

- `just ci`
- `pnpm build:e2e`
- `pnpm exec playwright test
tests/e2e/composer-selection-formatting.spec.ts --project=smoke --grep
"right-clicking selected composer text"`
- Native Builderlab staging: reproduced the pre-mount crash on main,
relaunched with the fix, and verified the composer mounts without the
TipTap error

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-25 16:50:21 +01:00
Taylor Ho
a8e1c66c4a fix(desktop): polish inline chip states (#6718)
**Category:** fix
**User Impact:** Inline chips now read more consistently, fit cleanly in
the composer, and show deleted message links with the same calm muted
treatment as unresolved links.

**Problem:** Deleted message links looked like destructive actions even
though they are informational, while mention chips had slightly uneven
vertical spacing, a high-set human icon, and could clip inside the
composer. **Solution:** Unify unavailable-state styling, tighten chip
spacing, optically align the human icon, and give composer chips enough
line height to paint without changing caret behavior.

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

**desktop/src/shared/styles/globals/markdown.css**
Makes chip padding vertically symmetric, moves only the human `@` icon
down by 1px, and shares muted colors between deleted and unresolved
message links while preserving their separate semantic classes.

**desktop/src/shared/styles/globals/composer.css**
Adds a composer-only line height derived from the text size and chip
padding so inline chips no longer clip while retaining inline caret
behavior.

**desktop/tests/e2e/entity-link-recipient-cards.spec.ts**
Verifies deleted and unresolved chips have matching computed colors
while deleted links retain their tooltip, semantics, and navigation
behavior.

**desktop/tests/e2e/mentions.spec.ts**
Covers composer chip height, clipping, the human icon's 1px optical
offset, and visual capture.

</details>

## Reproduction steps

1. Open a channel containing a link to a definitively deleted message
and compare it with a transient or unresolved message link; both should
use the muted unavailable treatment, while the deleted link still says
“Message deleted” and navigates to its fallback destination.
2. Insert a person mention in the composer; the chip should have
balanced vertical spacing and paint fully without clipping.
3. Compare a person mention with a channel chip; only the human `@` icon
should sit 1px lower.

## Screenshots

### Deleted message link

| Before | After |
| --- | --- |
| ![Deleted message link
before](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6718/deleted-chip-before.png)
| ![Deleted message link
after](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6718/deleted-chip-after.png)
|

### Composer chip polish

| Before | After |
| --- | --- |
| ![Composer mention chip
before](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6718/composer-chip-before.png)
| ![Composer mention chip
after](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6718/composer-chip-after-07411aba3.png)
|

## Validation

- `desktop/tests/e2e/entity-link-recipient-cards.spec.ts` +
`desktop/tests/e2e/mentions.spec.ts`: 82/82 passed
- Desktop unit suite: 5,397 passed
- Formatting and lint checks passed (existing informational warnings
only)
- Pre-push desktop check, typecheck, and unit tests passed at
`0255c3fd49f9244cd727d0dd20c514b3a1812152`

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-24 20:45:42 -07:00
Taylor Ho
e760c51820 feat(workflows): discover trigger filter values (#6712)
**Category:** improvement
**User Impact:** Workflow authors can discover people and messages while
configuring trigger filters, then see readable enriched labels instead
of raw identifiers.

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

Related issue: none found.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

</details>

## Reproduction steps

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

## Screenshots

### Author discovery

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

### Message discovery

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

### Selected filter summaries

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

### Enriched workflow card

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

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <0366ccd5ee09c2779a9d6bd6683daa17c16a508a51f6a7e7314018dab8fdc49b@buzz.block.builderlab.xyz>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
2026-08-24 17:40:23 -07:00
morgmart
c5166f2164 feat(desktop): simplify the message action rail (#6529)
## Problem

Quick-reaction shortcuts make the message action rail visually noisy.
Reaction access should remain first-class, but through one predictable
Add reaction action rather than several learned emoji shortcuts.

## Change

The rail is now:

**Add reaction · Reply · Copy link · More**

- Remove all quick-reaction shortcut buttons and their divider from the
message action rail.
- Preserve Add reaction as the first action, including its existing
picker, tooltip, accessible name, keyboard behavior, reaction behavior,
and feedback.
- Keep the existing Link2 Copy link action, shared copy handler,
More-menu entry, eligibility guards, and success feedback unchanged.
- Keep reveal, positioning, responsiveness, styling, and remaining menu
paths unchanged.

The now-unused quick-reaction rendering component and imports were
removed from `MessageActionBar`; shared reaction learning remains intact
for other reaction surfaces.

## Tests

Focused smoke coverage verifies:

- zero `React with …` shortcut buttons;
- exact ordered rail: `Open reactions → Reply → Copy link → More
actions`;
- the rail and More-menu paths emit the same canonical thread-aware
`buzz://message` URL;
- existing success feedback;
- pending and huddle rows omit both copy-link surfaces;
- the action bar stays within the open thread panel.

The zero-shortcut contract was mutation-checked by restoring the prior
production action bar: the focused test failed causally with expected 0
versus received 3 quick-reaction buttons.

## Validation

At `4f062e0d60b0f0c16b6862cdea7137c287060947`:

- `pnpm exec biome check src/features/messages/ui/MessageActionBar.tsx
tests/e2e/message-copy-link.spec.ts` — passed.
- `pnpm exec tsc --noEmit` — passed.
- `pnpm test` — 5,432 passed, 0 failed.
- `pnpm build:e2e` — passed; existing dynamic-import and chunk-size
warnings only.
- `pnpm exec playwright test tests/e2e/message-copy-link.spec.ts
--project=smoke` — 2 passed.
- Pre-push `file-size-check`, `desktop-check`, `desktop-typecheck`, and
`desktop-test` — passed.

Vogue’s design review: **SHIP** — reaction remains discoverable and
accessible as the visible first action; the extra click is an
intentional efficiency tradeoff for the simpler hierarchy.

---------

Signed-off-by: Trace (Engineer) <9d485ff0c62915e08a801162c195c7ef096f6f6143925407255cea2656c4b8a4@buzz.block.builderlab.xyz>
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Signed-off-by: Rivet <a08d9a8418c7ff03afe19964724c8fd87bf1776ab9e9b9cafb8cc920edd02a6e@buzz.block.builderlab.xyz>
Co-authored-by: Trace (Engineer) <9d485ff0c62915e08a801162c195c7ef096f6f6143925407255cea2656c4b8a4@buzz.block.builderlab.xyz>
Co-authored-by: Rivet <a08d9a8418c7ff03afe19964724c8fd87bf1776ab9e9b9cafb8cc920edd02a6e@buzz.block.builderlab.xyz>
2026-08-24 16:28:51 -07:00
Wes
30d2fc52f9 fix(desktop): restore icon-only remote marker (#6491)
## Summary

- restore the agreed icon-only cloud marker for an agent from another
setup
- retain the accessible `From another Buzz setup` label and native hover
title
- keep the merged loading, hover/focus persistence, and exact-pubkey
routing safeguards unchanged
- update the duplicate-agent E2E to require an icon with no visible
marker text in both autocomplete and Channel members

## Why

PR #6401 accidentally changed the agreed compact icon treatment into a
visible `Other setup` badge during review hardening. This is the
smallest correction and is intended for the active release.

## Testing

- Desktop JS: 5,280/5,280 passed
- Desktop TypeScript typecheck passed
- E2E build passed
- focused duplicate-agent Playwright journey: 1/1 passed
- file-size gate passed
- changed-file Biome and `git diff --check` passed

Carl, an automated reviewer, opened this via Wes's GitHub account.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-08-24 16:24:43 -06:00
Taylor Ho
f79d346a17 fix(composer): wrap Buzz chip labels without orphaning icons (#6581)
**Category:** fix
**User Impact:** Long Buzz link chips now wrap without overflowing in
composers and sent messages; icon-bearing stubs use at most five
graphemes when no earlier separator exists, so some sent chips attach
the icon to a shorter prefix than before. Labels over 48 graphemes are
visibly truncated in the composer while their full identity remains
available to assistive technology and in the tooltip.

**Problem:** Long repository, issue, pull request, and channel chip
labels could orphan their icon or overflow narrow composers and sent
messages. **Solution:** Keep the icon with a bounded, grapheme-safe
leading fragment while allowing the remaining text to break anywhere;
cap visible labels at 48 graphemes without changing the full tooltip or
accessible identity.

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

**desktop/src/features/messages/lib/composerMessageLinkNode.ts**
Splits composer chip content into an icon-bearing leading fragment and a
freely wrapping remainder while preserving the semantic label and link
metadata.

**desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs**
Updates renderer assertions for the fragment structure and verifies
every supported Buzz link kind retains the intended visible label.

**desktop/src/shared/styles/globals/composer.css**
Keeps ordinary composer mention decorations inline while relying on the
existing shared markdown chip wrapping rules for Buzz links.

**desktop/src/shared/ui/mentionChip.ts**
Centralizes grapheme-aware leading-fragment boundaries and label
truncation so composer and sent chips share the same visible identity.

**desktop/src/shared/ui/markdown/BuzzLinkChip.tsx**
Uses the shared grapheme-aware boundary when rendering sent-message chip
fragments.

**desktop/tests/e2e/navigation.spec.ts**
Covers increasing wrap depth across constrained widths, icon attachment,
the sent-message wrap, accessible labeling, and tooltip positioning over
both edge fragments.

</details>

## Reproduction steps

1. Open a desktop channel and paste a Buzz link with a long repository
or channel name into the composer.
2. Narrow the composer until the chip spans two or more lines.
3. Confirm the label breaks mid-string while the icon remains attached
to the first label fragment.
4. Send the message, hover both the first and last rendered fragments,
and confirm the tooltip follows the hovered fragment.

## Screenshots

**Before — the icon drops onto a separate line from its chip label**

![Composer chip with an orphaned icon before the
fix](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6581/00-before-inline-chip-icon-wrap.png)

**After — the icon stays attached while the remaining label wraps**

The same long repository chip at three composer widths. Its label gains
line breaks as space contracts, while the icon remains attached to the
leading fragment.

**420px — one line**

![Long repository chip on one line at
420px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6581/01-wide-420px.png)

**210px — two lines**

![Long repository chip on two lines at
210px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6581/02-medium-210px.png)

**150px — three lines**

![Long repository chip on three lines at
150px](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6581/03-narrow-150px.png)

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-24 14:55:05 -07:00
Will Pfleger
f6e6617a9d fix(desktop): bound thread /query and surface load errors, not false-empty (#6447)
Long threads in the desktop app sometimes never load (stuck on a
skeleton until you close and reopen the panel), and a failed load
silently renders as "No replies in this branch yet" — presenting a
broken fetch as an authoritative empty thread with no way to recover.
This fixes both, the two IMPORTANT findings from the thread-load
investigation.

## Defect 1 — unbounded `/query` request

The shared `reqwest::Client` in `relay.rs` sets no timeout, and neither
`/query` request builder set a per-request `.timeout(...)`. A stalled or
half-open connection (headers or body never arrive) leaves the request
pending forever, so a thread-history load hangs on the skeleton
indefinitely.

Fix: a 30s per-request deadline on both `/query` builders, funnelled
through one `send_query_request` helper so the timeout can never be
applied to one builder and dropped from the other. Scoped per-request
rather than client-level because the same client also serves STT/TTS
model downloads, builderlab auth, and the media proxy — a client-level
timeout would cut those off. The deadline sits above the 25s WS
`HISTORY_TIMEOUT_MS` so a slow-but-live relay isn't cut off before the
WebSocket path would be. A timeout surfaces through
`classify_request_error` as the stable `"relay unreachable: request
timed out"` string.

`send()` resolves as soon as response headers arrive, so a relay that
returns headers and then stalls the body trips the deadline during body
consumption, not at `send()` — and that consumption happens on two
paths: `parse_json_response` for 2xx, and `relay_error_message` for a
non-success status (500/429/…). Both paths route their body-consumption
error through one shared `classify_body_timeout` helper so they can't
drift: a stalled body surfaces the stable `"relay unreachable: request
timed out"` string on either path rather than the malformed-response
bucket (2xx) or a bare `"relay returned 500"` status label (non-2xx). A
genuinely non-stalled error still keeps its status classification.

## Defect 2 — terminal error painted as empty

`ChannelScreen` consumed only `isPending`/`data` from the thread-replies
query. Once React Query exhausted its one retry, `isPending` was false
and the zero-length data fell through `selectDeferredListRenderState` to
the `"empty"` state — indistinguishable from a genuinely empty branch,
with no retry affordance.

Fix: plumb `isError` + `refetch` through `ChannelScreen` → `ChannelPane`
→ `MessageThreadPanel`. A pure `selectThreadRepliesSurface` helper
decides the paint in strict precedence — the load-bearing invariant is
that a terminal error **never** resolves to `"empty"`, and cached
replies stay visible non-destructively under a later error (the error
card only surfaces when there is nothing to show). The panel renders an
explicit "Couldn't load replies" + Retry card (testids
`message-thread-replies-error` / `message-thread-replies-retry`).

`ProjectConversationPanel` is a second producer of the same shared panel
and used to hard-code `threadRepliesPending={false}` with no
error/retry, so a failed load in a Projects conversation still painted
the false-empty. It now propagates the same
`isPending`/`isError`/`refetch` from its `useThreadReplies` query.

The multi-root `useThreadRepliesForRoots` hook (the Huddle transcript
and Projects-agent conversation surfaces) had the same gap in its
`useQueries` `combine`: it returned only `{ events, isPending }`, so a
failed reply subtree contributed zero rows and vanished. The combine is
now a pure, unit-testable `combineThreadRepliesResults` that exposes
aggregate `isError`/`error` plus a `refetch` that re-runs only the
failed subtrees. Both multi-root consumers render the shared "Couldn't
load replies" + Retry card when a subtree fails: the Projects-agent
conversation after its transcript, and the Huddle transcript as a
non-destructive banner above the timeline. `useHuddleChannelMessages`
used to read only `.events` and discard the aggregate state, so one
summarized root failing left the flattened transcript presenting as
complete; it now propagates `threadRepliesError`/`onRetryThreadReplies`
through `ChannelScreen` into `ChannelPane`, where successful rows stay
visible and `onRetry` re-runs only the failed subtrees.

The error card carries `role="alert"` so its asynchronous appearance is
announced to assistive tech — without a live region a screen-reader user
parked in the composer never learns the load failed or that Retry became
available.

## Tests

- `stalled_query_request_times_out_with_classified_error` — a loopback
server that never responds; asserts the stable classified timeout
string.
- `stalled_response_body_times_out_with_classified_error` — a loopback
server that writes valid 2xx JSON headers then stalls the body past the
deadline; asserts the classified timeout string, not the malformed
bucket.
- `stalled_error_response_body_times_out_with_classified_error` — a
loopback that writes `500` headers promising a body it never sends;
asserts the classified timeout string rather than the `500` status
label.
- `non_stalled_error_response_yields_status_message` — a promptly-served
`500` still surfaces `"relay returned 500 Internal Server Error"`,
pinning that timeout preservation is scoped to actual timeouts.
- `selectThreadRepliesSurface` — pending→skeleton, terminal error→error
(never empty), page-2 failure never empty, cached rows stay visible
under error, successful-empty→empty, retry-success→list,
streaming→pending, and huddle-transcript collapse.
- `MessageThreadReplyState` mounted test — terminal error renders the
error card (asserting `role="alert"`), never the empty card.
- `combineThreadRepliesResults` — multi-root aggregation/order, a failed
subtree surfaces the aggregate error and never drops rows, aggregate
pending, refetch re-runs only failed queries, all-success yields no
error.
- `thread-load-failure.spec.ts` (smoke E2E) — binds the real
channel-thread panel wiring: forces a terminal `get_thread_replies`
failure at the IPC boundary, asserts the error card renders (never the
false-empty) and Retry recovers.
- `project-conversation-load-failure.spec.ts` (smoke E2E) — the same
guard for the Projects conversation producer, driven through the
Projects Channels-tab row.
- `huddle-thread-load-failure.spec.ts` (smoke E2E) — the consumer-level
guard the combine unit test can't provide: drives the real Huddle wiring
(`useHuddleChannelMessages` → `ChannelScreen` → `ChannelPane`) with two
summarized roots, fails one subtree's fetch at the IPC boundary, asserts
the surviving root's reply stays visible while the retry alert surfaces,
then Retry recovers the failed subtree and clears the alert.

## Structure

To stay under the desktop file-size ratchet, `relay.rs`'s inline test
module moved to `relay/tests.rs`, and two pure pieces were extracted
from the panel: the empty/error reply cards (`MessageThreadReplyState`)
and the per-row branch-highlight derivation
(`selectThreadRowHighlight`).

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-24 16:13:03 -04:00
Taylor Ho
4bf80978f5 fix(messages): route edits to the owning composer (#6575)
**Category:** fix
**User Impact:** Editing a channel message now opens in the main
composer, while editing a thread reply stays in the thread composer,
with focus ready for typing.

**Problem:** When a thread was open, Buzz treated its root message as
thread-owned and opened edits in the thread composer. Menu-driven edits
also lacked regression coverage for immediate focus.

**Solution:** Carry the message's semantic root/reply classification
into the edit target, route only actual replies to the thread composer,
and use the menu primitive's selection event for a reliable handoff.
End-to-end tests cover placement and focus for both paths.

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

**desktop/src/features/channels/ui/ChannelPane.tsx**
Routes edit targets by semantic thread ownership rather than membership
in the open thread panel.

**desktop/src/features/channels/ui/ChannelPane.types.ts**
Uses the shared composer edit-target type so routing metadata stays
attached to the target.

**desktop/src/features/messages/lib/draftMentionRefs.ts**
Classifies each edit target as a root or true thread reply from its
event tags.

**desktop/src/features/messages/lib/draftMentionRefs.test.mjs**
Covers semantic ownership for root and reply edit targets.

**desktop/src/features/messages/ui/MessageActionBar.tsx**
Handles Edit through the dropdown menu's selection event so focus
restoration and edit startup share the intended lifecycle.

**desktop/src/features/messages/ui/MessageComposer.types.ts**
Adds semantic thread ownership to the edit-target contract.

**desktop/tests/e2e/messaging.spec.ts**
Verifies root edits use and focus the main composer, while reply edits
use and focus the thread composer.

</details>

### Reproduction Steps

1. Send a channel message and open its thread.
2. From the thread panel, edit the root message; confirm its content
loads in the main composer and the editor is focused.
3. Send a reply in that thread.
4. Edit the reply; confirm its content loads in the thread composer and
the editor is focused.


### Screenshots

**Editing a channel-root message uses the main composer**

![Channel-root message editing in the main composer, dark theme with
purple
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6575/message-edit-root-main-dark.png)

**Editing an actual thread reply uses the thread composer**

![Thread reply editing in the thread composer, dark theme with purple
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6575/message-edit-reply-thread-dark.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-08-24 13:06:00 -07:00
Taylor Ho
9f55bf6745 fix(desktop): align jump-to-latest pill with composer height (#6606)
**Category:** fix
**User Impact:** Jump to Latest now stays above the composer as a draft
grows to multiple lines.

**Problem:** On WebKit, the pill's transform could retain a stale
inherited composer-height value after the composer expanded, leaving the
control stranded inside the composer.

**Solution:** Position and animate the pill with its absolute bottom
offset, which consumes the live composer height through layout rather
than a promoted transform layer. A smoke test now verifies that the pill
rises by the full composer growth and remains clear of the composer.

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

**desktop/src/features/messages/ui/MessageTimeline.tsx**
Anchor Jump to Latest with a live bottom offset instead of a translated
compositor layer so composer resizing reliably moves it.

**desktop/tests/e2e/smoke.spec.ts**
Add coverage that expands a detached timeline's composer and checks the
pill tracks the full height increase without overlapping it.

</details>

## Reproduction steps

1. Open a channel with enough messages to scroll.
2. Scroll away from the newest message until Jump to Latest appears.
3. Add several lines to the composer without sending.
4. Confirm Jump to Latest rises with the composer and remains directly
above it.

## Validation

- `pnpm --dir desktop test` — 5,397 passed
- `pnpm --dir desktop check` — passed with four existing informational
warnings outside this diff
- `pnpm --dir desktop typecheck` — passed
- `pnpm --dir desktop exec playwright test tests/e2e/smoke.spec.ts
--project=smoke` — 26 passed
- Push hooks — desktop check, typecheck, and tests passed


## Screenshots / Demos

Both captures use the same long, mid-history timeline and the same
four-line composer.

| Before | After |
| --- | --- |
| The stale pill position overflows into the expanded composer. | The
pill tracks the live composer height and stays clear above it. |
| ![Before — Jump to Latest overlaps the tall
composer](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6606/jump-pill-tall-composer-before.png)
| ![After — Jump to Latest clears the tall
composer](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6606/jump-pill-tall-composer-after.png)
|

Signed-off-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
2026-08-24 11:56:27 -07:00
morgmart
db5617dd15 fix(desktop): emit singular mention feed category so alerts route correctly (#6665)
## Problem

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

## Root cause

Two vocabularies got conflated in #475:

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

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

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

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

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

## Fix

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

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

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

## Verification

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

## Reviewers

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

---------

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-24 10:20:53 -07:00
tulsi
0e69b3fd7c show mention counts in channel notifications (#6696)
**Category:** fix
**User Impact:** Stream and forum channels now show an accessible
numeric badge for unread mentions while mention chips remain clear in
every theme.

**Problem:** Mention notifications contributed to the app and Dock
badge, but inactive stream and forum rows only became bold, making it
difficult to see where multiple mentions were waiting. Mention styling
and generic destructive colors could also lose contrast or visual
meaning in some themes.

**Solution:** Use the same app-badge projection for non-DM channel
mention counts, while preserving regular unread bolding, thread activity
dots, DM counts, and manual unread behavior. Dedicated notification and
opaque mention-highlight tokens keep the new treatments stable and
readable across syntax themes.

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

**desktop/src/features/channels/useUnreadChannels.ts**
Projects app-badge-eligible mention and broadcast counts into stream and
forum channel rows while retaining DM-specific counting and
manual-unread semantics.

**desktop/src/features/sidebar/ui/SidebarSection.tsx**
Renders an accessible numeric notification pill on inactive non-DM
channels and preserves the thread activity dot fallback.

**desktop/src/shared/styles/globals/markdown.css**
Applies the shared opaque yellow highlight to human and agent mention
chips, including hover treatment.

**desktop/src/shared/styles/globals/theme.css**
Adds fixed notification and mention-highlight tokens with
theme-independent contrast.

**desktop/tailwind.config.js**
Exposes the notification token pair through semantic Tailwind utilities.

**desktop/tests/e2e/badge.spec.ts**
Covers aggregated mention counts, broadcasts, unchanged unread tiers,
exact accessible text, and badge contrast under an adversarial theme.

**desktop/tests/e2e/mentions.spec.ts**
Covers human and agent mention styling, hover behavior, dark mode, and
WCAG text contrast.

</details>

## Reproduction steps

1. Open a stream or forum channel, then navigate to another channel.
2. Receive two messages that mention you in the inactive channel.
3. Confirm the inactive row is bold and shows a red `2` pill matching
the two notifications added to the app or Dock badge.
4. Receive a regular channel message and confirm the row only becomes
bold, without a numeric pill.
5. Receive a reply in an interested thread and confirm the channel
retains its activity dot instead of a mention count.
6. Switch between light and dark themes and confirm human and agent
mention chips remain yellow with near-black readable text, including on
hover.

## Screenshots

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

Signed-off-by: tulsi <tulsi@block.xyz>
2026-08-24 11:48:48 -04:00
Matt Toohey
2f13e30e88 fix(desktop): hide selection formatting tray on composer right-click (#6683)
## Summary

Right-clicking a text selection in the message composer left the
selection formatting tray floating over the native context menu. This
suppresses the tray for the duration of the right-click interaction.

## Changes

- `SelectionFormattingTray.tsx`: a `contextmenu` listener on the editor
DOM sets a suppression ref, cancels any queued rAF reposition, and hides
the tray. Suppression clears on the next left-click `pointerdown` or
`keydown` in the editor, which reschedules a normal position update.
- `scheduleUpdate`/`updatePosition` both honor the suppression ref, so
editor `selectionUpdate`/`transaction`/`focus` events fired during the
right-click can't bring the tray back.
- Extracted `cancelScheduledUpdate` to replace the duplicated rAF-cancel
logic, and reset suppression on editor change / cleanup.
- E2E coverage in `composer-selection-formatting.spec.ts`: double-click
to select, assert the tray shows, right-click and assert the tray hides
*and* that `contextmenu` is not `defaultPrevented` (the native menu
still opens), then re-select and assert the tray returns.

## Testing

`just` pre-push gate ran green: `desktop-check`, `desktop-typecheck`,
`desktop-test` (5397 passing), `file-size-check`.

## Demo


https://github.com/user-attachments/assets/2fdfced9-6cd6-4eb2-a6df-c03164ab1c42

Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-08-24 09:39:55 -06:00
Matt Toohey
72ba987c36 fix(desktop): stabilize members dialog scrolling (#6670)
## Summary

- Keep virtualized member rows measurable by removing
`content-visibility: auto` from the measured row subtree.
- Use the member card's 60px baseline as the virtualizer estimate while
retaining deferred rendering for eager search and archived-member lists.
- Cover large rosters with a regression test that checks stable scroll
extent across the list and verifies the final member remains reachable.

### Related issue

None found.

### Testing

- `just ci`
- `pnpm -C desktop build:e2e`
- `pnpm -C desktop exec playwright test tests/e2e/channels.spec.ts
--grep 'members sidebar virtualizes large channel rosters'
--repeat-each=5`

#### Before


https://github.com/user-attachments/assets/a5fcc040-6872-4200-bcc3-7b4197a4dd23

#### After


https://github.com/user-attachments/assets/f2c97f2f-3719-4c2a-b17a-2450c6c70a55

Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-08-24 09:18:29 -06:00
Matt Toohey
17af15effa fix(desktop): keep member runtime status off the UI thread (#6445)
## Summary
- skip managed-agent runtime discovery when the members sidebar has no
local managed bots
- run runtime listing disk, process, and mutex work on Tauri’s blocking
pool
- preserve local managed-bot status and Start/Stop behavior with
positive and negative E2E coverage

Opening Add people in a human-only channel could invoke synchronous
native runtime discovery before the sidebar painted, leaving the macOS
app beachballed. Human invites do not depend on that data.

## Why
I have seen slowness opening this dialog in the UI


https://github.com/user-attachments/assets/1955eb5e-ee47-4edf-8e5c-606d11ffbc25


### Related issue
Related overlap: #4851 is a broader managed-agent lifecycle change that
includes a similar native offload. This draft is intentionally limited
to the sidebar critical path and adds the human-only query gate.

### Testing
I have verified the pause in the video goes away after this change.

- `just ci`
- `just desktop-check`
- `just desktop-test` (5,241 passed)
- `just desktop-tauri-fmt-check`
- `just desktop-tauri-clippy`
- `just desktop-tauri-test` (2,702 passed; 18 ignored)
- focused Playwright: human-only sidebar skips runtime discovery
- focused Playwright: local managed bot retains status and Stop/Start
controls

No visual styling changed, so screenshots are not applicable.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-08-24 09:02:14 -06:00
Tyler
2d280376ad perf(desktop): persist channel heads, collapse thread reads and reply sends (#6572)
## Summary

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Signed-off-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
2026-08-24 10:11:07 -04:00
Max Lampert
040b203f73 perf(desktop): make the Projects surface render-cheap (#6460)
Diagnostic profiling on a large community (101 issues, 258 PRs) showed
Projects tab switches taking 2.5–3.6s, dominated by single React commits
of 0.5–1.5s and per-render recomputation — fetch work was already off
the main thread; the cost was building the UI.

### Measured: tab click → painted, per tab

| tab | before | after |
|---|---|---:|
| projects | 3,608ms | 320–580ms |
| repositories | 3,126–3,534ms | 310–410ms |
| tasks | 395–1,101ms | ~115ms |
| reviews | 322–2,603ms | ~96ms |
| activity | 597–741ms | ~148ms |

Single-commit ceiling dropped from 1,541ms to ≤200ms (growth steps
25–40ms). Fixes in profiled-cost order:

- **Profile popover body mounts only while open.** `UserProfilePopover`
carried seven query subscriptions plus interaction hooks per instance
even when closed; grids mount hundreds (five per card in people stacks,
one per row author) — measured **~40ms per card**, the dominant share of
the 1.2s card-tab commits. The always-mounted shell is now just the
Radix root + trigger; trigger markup, hover timing, and keyboard
handling are unchanged, and hover/tooltip event continuity is preserved
because the trigger never remounts.
- **Incremental row mounting.** The first 12 cards / 30 rows render in
the first commit; the rest stream in 36–60-per-frame low-priority
transitions. Grouped lists trim across group boundaries via a pure,
tested slicer; the mounted count survives in-place refetches.
- **Activity feed**: was rebuilt unmemoized on every render,
markdown-flattening every issue/PR/comment body in the community just to
sort and keep 30 items (~360+ flattens per render on the measured
community). Now memoized, and bodies stay raw until after the sort+slice
— 30 flattens, once per data change.
- **Contribution graph** (always-visible rail, so every tab paid for
it): ~180 day cells each wrapped in a Radix tooltip with per-cell Intl
date formatting per render. Now memoized, cells precomputed once per
data change, native `title` tooltips. (The activity-bar segments keep
their styled Radix tooltips — pinned by an existing spec.)
- **Rows/cards memoized with identity-stable props**: per-row selection
arrays were rebuilt per row per render (O(n²) — 258 PRs × 258-item
arrays each render) and are now hoisted and shared; people arrays derive
inside the memoized cards; the rail's stat walk over every issue/PR is
memoized.
- **`content-visibility: auto`** on cards and rows so offscreen entries
skip layout and paint; **tab switches run in a React transition** so the
click stays responsive while the new tree mounts.

Remaining known cost (out of scope): cold-entry data readiness — the
work-item and activity queries ship thousands of events to compute
counts (2–4s on a large community; see the fan-lifecycle PR). The
structural fix is a relay-side aggregate; tracked as follow-up.

---------

Signed-off-by: Max Lampert <maxwell@squareup.com>
2026-08-22 08:42:55 -06:00
Salman Mohammed
f99532585a refactor(acp): clarify agent prompt sections (#6501)
## Why
The ACP prompt puts a machine-specific Workspace prefix before static
base guidance and labels the user-facing agent instruction layer as the
generic System section. Because the cwd varies by launch and worktree,
leading with it reduces reusable prompt-prefix stability.

`[Workspace]` was added in [PR
#1194](https://github.com/block/buzz/pull/1194) as a defensive fix after
a broken `~/.sprout` → `~/.buzz` migration caused agents to scan `$HOME`
and trigger macOS TCC prompts. This change retains that grounding while
shrinking it to the current working directory and moving dynamic
environment context after the static Base prompt.

## What
- Emit the prompt in Base → Workspace → Agent Instructions order
- Reduce Workspace to `Current working directory: <absolute path>`
- Resolve cwd as an absolute native-platform path and preserve Windows
drive/UNC paths instead of checking for a leading `/`
- Emit Agent Instructions for persona and standalone agent instructions
across modern and legacy ACP paths
- Preserve parsing for archived observer frames that used System or the
former Workspace-before-Base order, and align the persona catalog label

## Risk Assessment
Medium-low — this changes prompt framing for every newly created agent
session. Existing archived observer frames remain parseable, and
execution still uses the same ACP working directory. Cwd resolution now
fails clearly instead of substituting `/` when the process directory
cannot be resolved.

## References
- https://github.com/block/buzz/pull/1103
- https://github.com/block/buzz/pull/1194

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-08-22 08:29:41 -06:00
klopez4212
8c0f42e187 Add mobile Huddles voice MVP (#6056)
## Summary
- add foreground mobile Huddles on Android and iOS with native Opus
capture/playback, mute, speaker routing, participants, lifecycle, and
minimized drawer UI
- keep mobile Huddle cards and roster state live, including ended rooms,
relay-resolved profiles, and agents
- broadcast desktop agent TTS through the existing Huddle audio protocol

## Scope
Foreground human-to-human voice MVP only. Agent setup/transcripts,
background calling, recording, and advanced device controls remain out
of scope.

## Validation
- `just mobile-check`
- `just mobile-test` — 1,500 passed
- `just desktop-check` and `just desktop-test` — 4,957 passed
- desktop typecheck, strict Clippy, and Tauri tests — 2,445 passed, 15
ignored
- mobile worktree identity contract checks
- physical Pixel/iPhone behavior reviewed during development

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: leader <71e9f2c44a6932b6772caaaccda1911d010463c3e2c6c40410b8329956046801@buzz.block.builderlab.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz>
2026-08-22 05:28:16 -07:00
Taylor Ho
a2d8be5efa feat(desktop-messages): keep agents addressed across messages (#6315)
**Category:** new-feature
**User Impact:** Users can keep selected agents addressed across
consecutive messages without retyping their handles.

**Problem:** Repeated conversations with agents require manually typing
the same mentions on every turn, which adds friction and makes
recipients easy to omit.

**Solution:** The composer can now keep agents automatically addressed
per channel, either from the mention controls or after a successful
inline mention. Addressed agents remain visible in the toolbar, apply to
channel threads, survive send failures safely, and never cross community
boundaries.

## Changes

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

**desktop/src-tauri/src/events/message_tags.rs**  
Preserves the automatic-address marker on validated mention reference
tags.

**desktop/src/features/channels/ui/ChannelPane.tsx**  
Wires the appropriate channel, thread, inbox, or forum composer context
without leaking audiences across surfaces.

**desktop/src/features/communities/useCommunityInit.ts**  
Clears composer audience state when the active community changes.

**desktop/src/features/forum/ui/ForumComposer.tsx**  
Wires the appropriate channel, thread, inbox, or forum composer context
without leaking audiences across surfaces.

**desktop/src/features/forum/ui/ForumComposerAutocompletes.tsx**  
Wires the appropriate channel, thread, inbox, or forum composer context
without leaking audiences across surfaces.

**desktop/src/features/home/ui/InboxDetailPane.tsx**  
Wires the appropriate channel, thread, inbox, or forum composer context
without leaking audiences across surfaces.

**desktop/src/features/messages/lib/agentAddressMention.d.mts**  
Defines helpers and types for marked automatic-address mention tags.

**desktop/src/features/messages/lib/agentAddressMention.mjs**  
Defines helpers and types for marked automatic-address mention tags.

**desktop/src/features/messages/lib/agentAddressMention.test.mjs**  
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.

**desktop/src/features/messages/lib/applyEditTagOverlay.mjs**  
Preserves automatic-address metadata when edited message tags are
overlaid.

**desktop/src/features/messages/lib/applyEditTagOverlay.test.mjs**  
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.


**desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.test.mjs**
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.


**desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.ts**
Stores the preference that keeps explicitly mentioned agents addressed
for later messages.

**desktop/src/features/messages/lib/extractMentionPersonas.ts**  
Separates persona recipients from the composer mention orchestration.

**desktop/src/features/messages/lib/persistentAgentAudience.test.mjs**  
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.

**desktop/src/features/messages/lib/persistentAgentAudience.ts**  
Maintains bounded, in-memory, channel-scoped automatic agent audiences.

**desktop/src/features/messages/lib/useMentionSelection.ts**  
Centralizes mention picker selection state and agent-first selection
behavior.

**desktop/src/features/messages/lib/useMentions.ts**  
Exposes explicit picker origins and selection controls while preserving
inline mention behavior.

**desktop/src/features/messages/ui/ComposerAddressControls.test.mjs**  
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.

**desktop/src/features/messages/ui/ComposerAddressControls.tsx**  
Renders compact addressed-agent avatars and the automatic-mention
management entry point.

**desktop/src/features/messages/ui/MentionAutocomplete.test.mjs**  
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.

**desktop/src/features/messages/ui/MentionAutocomplete.tsx**  
Adds automatic-mention controls and options to the existing mention
picker.

**desktop/src/features/messages/ui/MessageAgentAddressPrefix.tsx**  
Shows which agents were automatically addressed on a sent message.

**desktop/src/features/messages/ui/MessageComposer.tsx**  
Integrates automatic audiences, picker controls, accessible feedback,
shortcuts, and send behavior.

**desktop/src/features/messages/ui/MessageComposer.types.ts**  
Defines the simplified channel audience context shared by composer
hosts.

**desktop/src/features/messages/ui/MessageComposerToolbar.tsx**  
Places automatic-address controls in the composer toolbar without
crowding narrow layouts.

**desktop/src/features/messages/ui/MessageRow.tsx**  
Displays automatic-address metadata alongside sent message content.

**desktop/src/features/messages/ui/MessageThreadPanel.tsx**  
Wires the appropriate channel, thread, inbox, or forum composer context
without leaking audiences across surfaces.

**desktop/src/features/messages/ui/composerAgentKeyboard.test.mjs**  
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.


**desktop/src/features/messages/ui/persistentAgentAudienceHosts.test.mjs**
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.

**desktop/src/features/messages/ui/useAddressMentionPulse.test.mjs**  
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.

**desktop/src/features/messages/ui/useAddressMentionPulse.ts**  
Provides success and failure animation signals for addressed-agent
controls.

**desktop/src/features/messages/ui/useAgentAddressLockPicker.test.mjs**
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.

**desktop/src/features/messages/ui/useAgentAddressLockPicker.ts**  
Coordinates adding, removing, and announcing automatically addressed
agents.

**desktop/src/features/messages/ui/useAlwaysAddressShortcut.ts**  
Implements the platform-aware shortcut for toggling automatic
addressing.

**desktop/src/features/messages/ui/useAutoPinMentionedAgents.ts**  
Promotes successfully sent inline agent mentions and provides a single
undoable notification.

**desktop/src/features/messages/ui/useComposerMentionPicker.ts**  
Opens the mention picker without rewriting the current draft.


**desktop/src/features/messages/ui/useMentionSendFlow.helpers.test.mjs**
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.

**desktop/src/features/messages/ui/useMentionSendFlow.helpers.ts**  
Merges automatic and inline recipients, marks outgoing tags, and
restores failed sends safely.

**desktop/src/features/messages/ui/useMentionSendFlow.ts**  
Merges automatic and inline recipients, marks outgoing tags, and
restores failed sends safely.


**desktop/src/features/messages/ui/usePersistentAgentMentionHydration.ts**
Removes the prior draft-text hydration approach now that automatic
audiences stay at composer ingress.

**desktop/src/features/settings/ui/AgentsSettingsPanel.tsx**  
Replaces the old global behavior with explicit composer-level
automatic-mention controls.

**desktop/src/features/settings/ui/PreventSleepSettingsCard.tsx**  
Replaces the old global behavior with explicit composer-level
automatic-mention controls.

**desktop/src/shared/lib/keyboard-shortcuts.ts**  
Defines the user-facing automatic-address keyboard shortcut label.

**desktop/src/shared/ui/VideoReviewCommentMarkdown.tsx**  
Allows automatic-address prefixes to compose with video review
timecodes.

**desktop/tests/e2e/persistent-agent-audience.spec.ts**  
Covers the automatic-address behavior and its failure, keyboard, layout,
or persistence boundaries.

</details>

## Reproduction Steps

1. Open a channel with one or more agents and open the mention picker
from the composer.
2. Select an agent for automatic mentions, then send several messages
without retyping the handle; confirm the agent remains in the composer
control and receives each message.
3. Mention another agent inline, send successfully, and confirm the
agent becomes automatically addressed; use the notification's Undo
action to reverse it.
4. Open a thread in the same channel and confirm the same addressed
agents are available there.
5. Remove an agent from the composer control and confirm later messages
stop addressing it.
6. Switch communities and confirm addressed agents do not carry into the
other community.

## Screenshots

All states below use the dark Buzz theme with a selected lilac accent.

### Addressed composer

Selected agents stay visible at the composer ingress without adding
handles to the draft.

![Two automatically addressed agents in the dark composer with a lilac
accent](https://raw.githubusercontent.com/block/buzz/74c2cbbd80ed630a0c6e00c420505a691bd4994a/pr-6315--01-addressed-composer.png)

### Open mention menu

The @ ingress opens the existing mention menu and shows which agents are
already addressed.

![Open mention menu with automatically addressed agents
highlighted](https://raw.githubusercontent.com/block/buzz/74c2cbbd80ed630a0c6e00c420505a691bd4994a/pr-6315--02-open-mention-menu.png)

### Mention options

The inline options pane controls whether a successful one-time agent
mention carries into later messages.

![Automatic mention options expanded above the mention
menu](https://raw.githubusercontent.com/block/buzz/74c2cbbd80ed630a0c6e00c420505a691bd4994a/pr-6315--03-mention-options.png)

### Agent settings

The same preference is available in **Settings → Agents →
Conversations**.

![Automatic agent mentions preference in the Agents settings
pane](https://raw.githubusercontent.com/block/buzz/74c2cbbd80ed630a0c6e00c420505a691bd4994a/pr-6315--04-agent-settings.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-08-21 22:04:53 -07:00
Taylor Ho
723affe5d1 fix(desktop): remove Buzz entity link previews (#6512)
**Category:** improvement
**User Impact:** Buzz-native project, repository, issue, and pull
request links now appear once as compact inline chips, with their
details available on hover.

**Problem:** Buzz-native entity links rendered both an inline chip and a
standalone preview card, repeating the same metadata and adding visual
noise to conversations. **Solution:** Exclude Buzz-native links from the
shared standalone-preview extractor while leaving entity parsing intact
for chip tooltips and preserving external web previews and attachment
cards.

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

**desktop/src/shared/lib/linkPreview.ts**
Stops Buzz-native preview candidates after parsing, including same-relay
git clone URLs that normalize to repository entities, while allowing
external URLs through the existing snapshot path.

**desktop/src/shared/lib/linkPreview.test.mjs**
Covers project, repository, issue, pull request, markdown-labeled,
same-relay clone, and mixed external-link extraction behavior.

**desktop/src/shared/ui/markdown/useMessageLinkPreviews.test.mjs**
Confirms sent messages no longer merge a standalone Buzz entity card
while external sender snapshots still render.

</details>

## Reproduction steps

1. Open a desktop channel containing a `buzz://project`, `buzz://repo`,
`buzz://issue`, or `buzz://pr` link.
2. Confirm the link renders as an inline entity chip without a second
standalone Buzz card below the message.
3. Hover the chip and confirm its entity metadata remains available.
4. Post an external HTTPS link and confirm its web preview still
renders.
5. Paste a same-relay `/git/<owner>/<repo>` clone URL and confirm it
uses the repository chip without a duplicate card.

## Screenshots

| Before | After |
| --- | --- |
| Inline chip plus redundant standalone Project card | Inline chip is
now the sole presentation |
| ![Before: project chip and duplicate standalone
card](https://raw.githubusercontent.com/block/buzz/7e1a0d6cfb52382ef636d7331cf30ea334429c4d/pr-6512--before.png)
| ![After: project chip without a standalone
card](https://raw.githubusercontent.com/block/buzz/7e1a0d6cfb52382ef636d7331cf30ea334429c4d/pr-6512--after.png)
|

**After — rich metadata stays available on hover**

![After: dark theme with pink accent and Project tooltip showing
description and repository
count](https://raw.githubusercontent.com/block/buzz/7e1a0d6cfb52382ef636d7331cf30ea334429c4d/pr-6512--after-tooltip-rich.png)

## Verification

At commit `3fa74cdd342ac1f6721b7d56a7f111af31e0e6e9`:

- focused link-preview + Markdown unit suites — 119/119 passed
- targeted registered smoke E2E — 8/8 passed, including labeled
same-relay clone metadata, ordinary-link presentation, and in-app
navigation
- `cd desktop && pnpm exec tsc --noEmit` — passed
- `git diff --check origin/main...HEAD` — passed
- pre-push hooks — desktop check, TypeScript, and full desktop unit
suite passed

---------

Signed-off-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Co-authored-by: Mongo <5398c5fd039b963ce132b3e078e7c4af097dd997517bb5e14c2682fe68c25197@buzz.block.builderlab.xyz>
2026-08-21 22:04:17 -07:00
Taylor Ho
074561233e fix(composer): preserve caret when inserting mentions mid-message (#6531)
**Category:** fix
**User Impact:** Users can insert mentions earlier in a draft and
continue typing without the caret corrupting the rest of the message.

**Problem:** Caret correction ran after every document change, so typing
a mention before existing text repeatedly advanced across the mention
separator and interleaved spaces into the draft. **Solution:** Limit
correction to the autocomplete settlement it was designed for, with
transaction-level and browser-level regression coverage for known and
unregistered mentions.

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

**desktop/src/features/messages/lib/mentionHighlightExtension.ts**
Restricts trailing-space caret advancement to an armed autocomplete
settlement instead of every document change.

**desktop/src/features/messages/lib/mentionHighlightExtension.test.mjs**
Exercises the real ProseMirror plugin state and verifies mid-draft
mention typing, unknown tokens, end-of-message typing, and
completed-mention separators.

**desktop/tests/e2e/mentions.spec.ts**
Reproduces the reported composer workflow in Chromium and covers the
same corruption path for an unregistered `@token`.

</details>

## Reproduction steps

1. Open a channel and enter `hello world` in the composer.
2. Move the caret between `hello` and ` world`.
3. Type ` @bo`, select `bob` from autocomplete, and continue typing
`abc`.
4. Confirm the composer reads `hello @bob abc world` with the caret
after `abc`.
5. Repeat with an unregistered token such as ` @zzq` and confirm the
existing text remains intact.

## Before / After

| Before | After |
| --- | --- |
| Typing after a mid-draft mention walks the caret through the existing
message. | Continued typing stays after the inserted mention. |
| ![Before: mention caret corrupts existing draft
text](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6531/mention-caret-before.gif)
| ![After: caret remains after the inserted
mention](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6531/mention-caret-after.gif)
|

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Mongo <5398c5fd039b963ce132b3e078e7c4af097dd997517bb5e14c2682fe68c25197@buzz.block.builderlab.xyz>
2026-08-21 18:48:21 -06:00
morgmart
97b1fee5c3 fix(desktop): restore true zoom by scaling the root rem (#6514)
## Summary

Follow-up to #5644. Cmd +/- had become a text-only zoom: type scaled
while rem-based padding, gaps, widths, avatars, and controls stayed
frozen, which produced cramped layouts (see [#buzz-frontend
thread](buzz://message?channel=a410ffde-c61f-416a-96e0-c296b5f5ecc9&id=1a758115cf07b00c097f6e988553908c045165325a57637519cfa7ed9c9accec)).

Root cause: #5644 introduced a virtual typography rem so the **Font
size** preference could change text without moving layout — a good
decoupling — but it also routed **Cmd +/- zoom** through that same
px-valued token and pinned the real root at 16px. One decision ("freeze
layout") was applied to two dials that shouldn't share it.

This PR gives each dial one owner and lets CSS compose them:

| Control | Changes | How |
|---|---|---|
| **Cmd +/- zoom** | Everything — true zoom | Scales the real `<html>`
font-size again (`useWebviewZoomShortcuts`) |
| **Font size preference** | Text only | Sets `data-font-size`;
`typography.css` maps it to a unitless `--buzz-type-scale`, mirroring
how density already works |

`--buzz-type-rem` becomes `calc(1rem * var(--buzz-type-scale))` —
rem-relative, so it rides on zoom automatically. Resulting text px = `16
× zoom × scale × token-ratio`. The 13 / 14 / 15px conversation contract
is unchanged at default zoom. Density and the type ramp from #5644 are
untouched.

The preference module no longer does px math or knows about zoom; the
zoom hook no longer imports the preference module. Net deletion in
production code.

## Validation

- `pnpm test` — 5,308 desktop unit tests
- `pnpm check:px-text`, `tsc --noEmit`, biome
- Playwright: `top-chrome-zoom-clearance.spec.ts` (native-chrome
clearance stays fixed under root zoom),
`inbox-refactor-screenshots.spec.ts` (zoomed row padding now asserts
`4.4px` instead of the frozen `4px`), and both `profile.spec.ts` zoom
tests (composed zoom × preference, cross-window storage reset)
- Before/after screenshots at 140% zoom in the comment below

---------

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-21 17:21:36 -07:00
Taylor Ho
0e48ff2691 feat(workflows): clarify workflow setup and activation (#6470)
**Category:** new-feature
**User Impact:** Workflow authors can build filtered, runtime-aware
automations, understand them at a glance, and get a clear warning before
turning on workflows likely to run often.
**Problem:** Workflow setup exposed raw configuration without enough
help composing message templates, filtering triggers, or understanding
saved behavior; activation could also make a broadly triggered workflow
live without explaining its likely frequency.
**Solution:** Batch 3 adds local, deterministic template variables,
trigger filters, and semantic summaries, then refines cards and
activation around configured behavior and a risk-aware warning boundary.
Scheduling remains the already-shipped implementation, advanced
expressions remain lossless, and network-backed identity/message
enrichment stays in Batch 4.

| Message inputs | Trigger filters |
| --- | --- |
| Caret-aware, keyboard-accessible suggestions expose trigger-local
values and safe prior-step outputs in `send_message.text`. | Structured
conditions and validated manual IDs block invalid submission while
preserving advanced expressions. |
| ![Message variable
autocomplete](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6470/message-variables.png)
| ![Structured trigger
filters](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6470/trigger-filters.png)
|

| Workflow cards | Risk-aware activation |
| --- | --- |
| Semantic labels, channel-first hierarchy, configured reaction/action
visuals, real step stacks, and compact status controls make behavior
scannable. | Broad message and frequent schedule triggers explain the
risk before **Turn on**; narrowly scoped triggers proceed without
unnecessary ceremony. |
| ![Semantic workflow
card](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6470/workflow-card.png)
| ![Activation
confirmation](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6470/activation-choice-v2.png)
|

## Changes

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

**desktop/src/features/workflows/ui/WorkflowActionsMenu.tsx**  
Separates direct card status controls from secondary actions while
retaining modal status actions.

**desktop/src/features/workflows/ui/WorkflowCard.tsx**  
Adds semantic behavior, channel-first hierarchy, configured
reaction/action visuals, real subsequent-step stacks, status controls,
and reduced-motion-aware trigger feedback.

**desktop/src/features/workflows/ui/WorkflowDialog.tsx**  
Warns before activating broadly triggered workflows while allowing
narrowly scoped workflows to proceed directly.

**desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx**  
Connects structured trigger filters and template-aware step inputs while
preserving schedules, trigger transitions, and selected YAML authority.

**desktop/src/features/workflows/ui/WorkflowStepCard.tsx**  
Replaces generic labels with deterministic configured-step descriptions.

**desktop/src/features/workflows/ui/WorkflowTemplateTextarea.tsx**  
Adds caret-aware variable suggestions with keyboard navigation and focus
restoration.

**desktop/src/features/workflows/ui/WorkflowTriggerConditions.tsx**  
Adds structured local filters, validated author/message IDs, and a
lossless advanced-expression fallback.

**desktop/src/features/workflows/ui/workflowActivationWarning.ts** and
**workflowActivationWarning.test.mjs**
Classify broad message and frequent schedule triggers for contextual
activation warnings.

**desktop/src/features/workflows/ui/workflowConditionExpression.ts** and
**workflowConditionExpression.test.mjs**
Model and cover parsing, serialization, validation, and
advanced-expression preservation.

**desktop/src/features/workflows/ui/workflowDefinition.ts** and
**workflowDefinition.test.mjs**
Preserve trigger/step configuration and derive deterministic card
metadata across YAML round trips.

**desktop/src/features/workflows/ui/workflowStepDescription.ts** and
**workflowStepDescription.test.mjs**
Generate and cover local step summaries.

**desktop/src/features/workflows/ui/workflowTemplateVariables.ts** and
**workflowTemplateVariables.test.mjs**
Define and cover trigger-specific, order-bounded variables and caret
insertion.

**desktop/src/features/workflows/ui/workflowTriggerDescription.ts** and
**workflowTriggerDescription.test.mjs**
Generate and cover semantic trigger summaries without network lookups.

**desktop/tests/e2e/workflow-local-controls.spec.ts** and snapshot  
Cover filters, IDs, advanced expressions, autocomplete, activation
choices, summaries, and YAML authority.

**desktop/tests/e2e/workflow-reaction-picker.spec.ts**  
Covers configured reaction emoji in workflow nodes and summaries.

**desktop/tests/e2e/workflows.spec.ts**  
Covers risk-aware activation warnings, direct safe creation,
duplication, and card status controls.

</details>

## Reproduction steps

1. Create a message-posted workflow in **Workflows**, add a Send message
step, and type `{{trig`; verify keyboard-selectable variables insert at
the caret.
2. Configure message-text and manual ID filters; verify malformed IDs
block submission and advanced expressions survive Form/YAML transitions.
3. Create a broad message workflow; verify **Back** persists nothing,
**Keep off** saves it disabled, and **Turn on** enables it. Confirm a
narrowly triggered webhook skips the warning.
4. Inspect the saved card; verify its channel, semantic behavior,
configured actions/reaction, real step stack, and status are
understandable without opening YAML.

## Validation

Validated at exact clean head `f99503819889b95ee3c61657c5c3850aae35481e`
on base `24ec6a468ec9d0d425ee58fbfc4d416412c446ad`.

- Focused workflow regressions passed 59/60 locally; the only local miss
was a 438-pixel macOS snapshot drift, while the checked-in Linux
baseline comes from the failing CI artifact. Repository pre-push gates
and E2E build/typecheck passed.
- A broader 36-test smoke invocation had 31 passes and five unrelated
pre-existing expectation/snapshot failures, so it is not claimed as
fully green. Adversarial fixes are recorded in [round
one](https://github.com/block/buzz/pull/6470#issuecomment-5366366670)
and [round
two](https://github.com/block/buzz/pull/6470#issuecomment-5366407535).

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Mongo <5398c5fd039b963ce132b3e078e7c4af097dd997517bb5e14c2682fe68c25197@buzz.block.builderlab.xyz>
2026-08-21 16:53:50 -07:00
Max Lampert
b85d680fb1 perf(desktop): stop the Projects fan refetching on re-entry and running after leave (#6458)
Entering Projects fires a large fan: an exhaustive paginated relay
enumeration (projects/repos/tombstones), five 2,000-event work-item
queries plus assignment-operation scans, per-repo activity summaries,
and a local-repository filesystem scan. Measured on a large community
(101 issues / 258 PRs):

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

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

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

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

---------

Signed-off-by: Max Lampert <maxwell@squareup.com>
2026-08-21 17:40:35 -06:00
Max Lampert
b0466ac465 perf(desktop): keep the member roster off the channel-switch path (#6456)
Switching channels triggered a full-roster fetch (kind:39002 plus a
kind:0 profile batch with every member pubkey as an author) in the
common case, and several render paths walked the full roster per render.
None of this scales past a few hundred members; the product target is
10k+.

- **Members query staleTime 30s → 5min.** Every membership change the
client can observe already invalidates the key explicitly: live
join/leave system messages for the active channel, member-added/removed
notifications for the current identity, and all membership mutations —
including previously-uncovered direct write paths (moderation kick,
agent-deletion cleanup), which now invalidate through a shared helper.
The 30s window bought correctness we already had and charged a roster
fetch per switch.
- **ChannelMembersBar no longer mounts the roster query for non-DM
channels** — the count renders from the channel summary, and the
private-channel huddle gate accepts `channel.isMember` (derived from the
same kind:39002 event as the roster's self entry).
- **Roster-derived lookups are cached on roster identity**
(`rosterDerivations.ts`): role map, agent-member subset, member/bot
pubkey sets. These were rebuilt O(members) on every live message /
profile re-key. React Query's structural sharing keeps the roster
identity stable, so each derivation computes once per distinct roster.
- **Backend: the kind:0 profile join in `get_channel_members` is capped
at the first 500 members** (roster order). Members past the cap keep
`display_name: None` (UI falls back to pubkey labels and profile
caches); `role=="bot"` agent flags are roster-derived and unaffected.
Full roster pagination is the structural follow-up.
- **Composer keystroke path**: `useCanAddChannelMembers` re-scanned
channels + roster per keystroke; now memoized on data identities,
sharing the cached pubkey set.

### Measured / estimated impact

| metric | before | after |
|---|---|---|
| roster fetches while switching (live trace) | nearly every switch | ≤1
per channel per 5 min |
| roster fetch cost on the wire (live, 51-member channel) | 273ms per
fetch | amortized away |
| kind:0 `authors` filter size at 10k members | ~670KB per request (~67
B/pubkey) | capped at 500 authors (~34KB) |
| warm-switch longtask at 10k members (mock harness, 4× throttle) |
364ms | 318ms |
| per-render roster walks (role map, agent sets) at 10k members |
O(members) per live message | once per distinct roster |

Deferred deliberately: protocol-level roster pagination and removing
`memberPubkeys` from channel summaries (needs relay support).

---------

Signed-off-by: Max Lampert <maxwell@squareup.com>
2026-08-21 17:40:20 -06:00
Wes
aea0ef8df9 fix(desktop): simplify duplicate agent provenance (#6401)
## Summary

- leave the local same-name agent row unlabelled because locality is
implied
- mark only the remote identity with a compact cloud badge reading
`Other setup` in mention autocomplete and Channel members
- suppress the redundant `managed by you` text for duplicate owned
agents while retaining distinct short npubs and exact-pubkey routing

## Testing

- Desktop E2E build/typecheck: `pnpm build:e2e`
- focused Playwright smoke test: `duplicate owned agents preserve
provenance and exact pubkey selection` (1 passed)
  - visible provenance in autocomplete and Channel members
  - local identity remains unmarked
  - keyboard selection routes each exact pubkey
  - narrow 760×640 viewport containment
- focused Biome check on the changed marker and E2E files
- `pnpm check:px-text`
- `pnpm check:pubkey-truncation`
- `just file-size-check`
- `git diff --check`

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-08-21 10:05:45 -06:00
Wes
4e3c9e619c fix(desktop): sender names in notifications + macOS click-through routing (#6427)
> Opened by the agents Brain and Pinky on behalf of @wesbillman.

Fixes two desktop notification issues (requested in Buzz channel
`desktop-notification-improvements`):

## 1. Notifications now show who sent the message

Live DM and thread-reply notifications showed only "Direct message" /
"Reply in #channel", while home-feed mention toasts already carried the
sender's name. All message-notification copy is now centralized:

- **`formatMessageNotification`**
(`notifications/lib/notificationFormat.ts`) — canonical title/body for
all five sources (mention, approval, needs-action, DM, thread reply).
Sender-first titles with neutral fallbacks, never a raw pubkey:
  - DM: `Taylor` instead of `Direct message`
- Thread reply: `Taylor replied in #ship-room` instead of `Reply in
#ship-room`
- **`useNotificationSenderName`** — synchronous cache-only lookup
(react-query users-batch entry cache → persisted label cache); a cold
miss ships the fallback title immediately and warms the cache in the
background. No toast delay, no new network machinery.
- **`buildEventNotificationTarget` / `buildFeedItemNotificationTarget`**
(`notifications/lib/target.ts`) — the click-through target payload is
built in one place instead of three hand-rolled copies.

## 2. macOS notification clicks route to the target message (#3509)

On packaged builds, clicking a notification focused the app but never
navigated. Three independent gaps lined up behind one symptom:

- **Reveal hang**: notification navigation now starts before the
best-effort `unminimize → show → setFocus` chain, so a hung native
invoke cannot gate click-through; the existing 1.5s reveal timeout
remains as a secondary guard.
- **Lost emit**: the Rust delegate queues the activation target *before*
emitting `native-notification-activated`; a lost emit stranded the
target with nothing re-draining the queue. The macOS listener now also
drains on window `focus` / `visibilitychange` — delivered by WebKit
independently of the Tauri event channel, and always produced by the
click's own foregrounding.
- **Silent no-op**: `commitNavigation` skips same-href destinations;
`goChannel` / `goForumPost` / `openSearchHit` now accept `force`, and
the notification activation handler passes it so a click always routes.
Multiple queued activations are serialized FIFO with rejection
containment so an older click cannot finish after and overwrite a newer
one. Queue teardown now aborts already-running activations as well as
pending ones; async forum-comment destination resolution rechecks
ownership before community-scoped cache writes or routing.

Diagnostic evidence from the macOS unified log (packaged v0.5.17):
delegate confirmed live in release builds (`willPresent` honored —
`(["list"])` presentations); a real click response at 09:25:06 reached
`usernoted`, the app was fronted by LaunchServices, and no navigation
followed.

## Verification

- Fixed the stale relay-backed DM dedupe expectation: exactly one toast
remains required, with sender-first title `alice` instead of channel
title `alice-tyler`
- Rebased onto `origin/main` and verified 15 focused activation/click
tests, desktop typecheck/check, file-size gate, Biome on changed files,
and `git diff --check` at `21a7e0cf5`
- Notification + navigation + AppShell.helpers unit tests: 89/89 pass
(includes new `notificationFormat.test.mjs`, `target.test.mjs`,
`desktopActivations.test.mjs`)
- Full desktop JS suite run by Pinky at f03979ea: 5133/5133 pass;
pre-push hooks (desktop-test, desktop-typecheck, desktop-tauri-checks,
rust-tests, file-size ratchet) all green on this branch
- `tsc --noEmit` and changed-file Biome checks clean; re-verified after
rebasing current `origin/main`
- Click-through on a packaged build still needs human verification —
@wesbillman, next release build is the real test.

---------

Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-08-21 09:17:04 -06:00
thomaspblock
9b32e055fe polish(desktop): finish Projects navigation and context chrome (#6429)
## Summary

After #6396, Projects still split chrome across the workspace header, a
copy-link control, and a labeled Actions group that mixed people,
create, and metadata. This PR finishes that surface: the right-hand
context box is unlabeled actions plus a Details group, people stacks and
contribution heatmaps are gone from that box, Create review sits with
Create task, and the top chrome is terminal / chat / info with no
copy-link. Sent project context collapses to a pill, and review file
diffs keep the last good git view instead of flashing empty while
queries refetch.

This also lands the remaining navigation polish that followed Part 3:
overview and list presentation, readme and commit layout, and opening
the latest matching conversation from the Channels tab without leaving
the project.

### Related issue
N/A. Related: #6396

## Testing
- Walked Files, Tasks, Reviews, task/review detail, overview tabs, and
chrome chat vs info in the running desktop app
- Pre-push: desktop typecheck, unit tests, Tauri checks, and file-size
gate passed
- Updated Projects smoke specs for the new context groups, Create
review, chrome order, and removed copy-link control
- Merged current `origin/main`; one conflict in discussion-channel rows
kept conversation-panel navigation and took main's bounded channel-name
lookup

## Post-Deploy Monitoring & Validation
- validate Projects workspace chrome, context box, and review file diffs
in the first staging Desktop session
- healthy signals: context box shows unlabeled actions then Details,
chat toggle sits between terminal and info, review diffs stay populated
across selection changes
- failure signals: missing Create review, restored heatmap/people in the
context box, or empty Files Changed while the review is still selected;
mitigate by reverting this PR

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-21 05:45:55 -07:00