Commit Graph

1624 Commits

Author SHA1 Message Date
renovate[bot]
d874d67c11 chore(deps): update rust crate futures-util to v0.3.33 (#5448)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [futures-util](https://rust-lang.github.io/futures-rs)
([source](https://redirect.github.com/rust-lang/futures-rs)) |
dependencies | patch | `0.3.32` → `0.3.34` |
| [futures-util](https://rust-lang.github.io/futures-rs)
([source](https://redirect.github.com/rust-lang/futures-rs)) |
workspace.dependencies | patch | `0.3.32` → `0.3.34` |

---

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

---

### Release Notes

<details>
<summary>rust-lang/futures-rs (futures-util)</summary>

###
[`v0.3.34`](https://redirect.github.com/rust-lang/futures-rs/blob/HEAD/CHANGELOG.md#0334---2026-08-11)

[Compare
Source](https://redirect.github.com/rust-lang/futures-rs/compare/0.3.33...0.3.34)

- Preserve cloned waker identity.
([#&#8203;3032](https://redirect.github.com/rust-lang/futures-rs/issues/3032))
- Updato `syn` to 3.
([#&#8203;3028](https://redirect.github.com/rust-lang/futures-rs/issues/3028))

###
[`v0.3.33`](https://redirect.github.com/rust-lang/futures-rs/blob/HEAD/CHANGELOG.md#0333---2026-07-18)

[Compare
Source](https://redirect.github.com/rust-lang/futures-rs/compare/0.3.32...0.3.33)

- Fix `ReadLine`'s soundness issue regarding to exception safety.
([#&#8203;3020](https://redirect.github.com/rust-lang/futures-rs/issues/3020))
- Fix unsound `Send` impl for `IterPinRef` and `Iter`.
([#&#8203;3003](https://redirect.github.com/rust-lang/futures-rs/issues/3003))
- Fix stacked borrows violation in `compat01as03` implementation.
([#&#8203;3012](https://redirect.github.com/rust-lang/futures-rs/issues/3012))
- Fix memory leak in `FuturesUnordered::IntoIter`.
([#&#8203;3005](https://redirect.github.com/rust-lang/futures-rs/issues/3005))
- Add `portable-atomic-alloc` feature and use it in `FuturesUnordered`.
([#&#8203;3007](https://redirect.github.com/rust-lang/futures-rs/issues/3007))
- Re-export `alloc::task::Wake`.
([#&#8203;3010](https://redirect.github.com/rust-lang/futures-rs/issues/3010))
- Update `spin` to 0.12.
([#&#8203;3014](https://redirect.github.com/rust-lang/futures-rs/issues/3014))

</details>

---

### Configuration

📅 **Schedule**: (UTC)

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

🚦 **Automerge**: Enabled.

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-21 18:28:08 -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
Max Lampert
eb484387d5 chore(desktop): drop unused ORIGINAL_CONTENT from empty-edit-delete spec (#6517)
`biome check` fails with `lint/correctness/noUnusedVariables` on
`ORIGINAL_CONTENT` in `desktop/tests/e2e/empty-edit-delete.spec.ts`,
which fails `pnpm check` (Desktop Core) for **every PR touching desktop
paths** — e.g. it currently blocks #6460. It presumably landed while
Desktop Core was path-skipped on the introducing PR.

One-line removal; the constant has no remaining references (the
assertions use `RENDERED_ORIGINAL_CONTENT`).

Signed-off-by: Max Lampert <maxwell@squareup.com>
2026-08-21 18:20:19 -06: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
Tyler
7da8f9abf3 Clarify huddle message destination (#6496)
## Summary
- identify the injected channel ID as the attached main channel, not the
live huddle channel
- direct spoken replies to the channel UUID in the current Context block
- pin the distinction with regression assertions

## Testing
- `just desktop-tauri-test`
- pre-push desktop Tauri checks

Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
2026-08-21 13:52:40 -04:00
Will Pfleger
fc2ce6728b feat(archive): add observer-frame retention schema and gated DB adapter (#5719)
The local-save archive (`~/.buzz/archive/archive.db`) grows without
bound — observer frames (kind 24200) are 99.95% of it by bytes (~1.3M
rows, 75–375 MB/day). This lays the schema and process-wide foundations
for bounding that growth: a single global retention window for observer
frames, kept in `archive_meta`, plus the gated DB adapter every later
phase builds on. The prune worker that actually deletes lands in a
follow-up; nothing here removes data.

## What this adds

- **Migration M4** (`add_archive_meta`, crash-safe): creates the
`archive_meta` k/v table and the `archived_at`-covering scope-age index
used by the future prune scan, and seeds `observer_retention_days=30`.
Runs under one `BEGIN IMMEDIATE` with an in-lock marker recheck and the
marker written last, so a crash before COMMIT rolls back every object
and the next open re-runs from scratch. Fails closed on an
externally-created `archive_meta` present without the marker.
- **Process init barrier + gated `ArchiveDb` adapter** owning every
production open. All subscription-mutation commands and the archive sync
task route their DB work through `ArchiveDb::with_conn`, which awaits
the init barrier once. This also fixes a pre-existing bug where
`create_save_subscription` ran blocking DB work on the async runtime.
- **Commands**: `get_observer_retention_days` /
`set_observer_retention_days` (fail-closed — rejects days `< 1` or `>
36500` before writing) and `archive_size_stats` (physical file bytes for
the main DB + `-wal` sidecar, plus `page_size` / `page_count` /
`freelist_count` — PRAGMAs and file metadata only, no payload scans).

## Scope

Observer frames (24200) are the only kind with a retention setting.
NIP-AM metrics (44200) and every other archived kind are kept
indefinitely with no retention state — no policy table, no per-kind or
per-subscription configurability. This is the simplified design ruled in
over the earlier per-subscription × per-kind approach: bounding observer
frames alone captures essentially all the value.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-21 13:35:33 -04:00
Tyler
6039fed565 fix(desktop): restore human barge-in over agent TTS in huddles (#6431)
## Problem

Humans cannot interrupt agents in huddles: agent TTS keeps playing over
a talking human (reported by @tlongwell-block, 2026-08-20). The full
requirement: **any human talking — local or remote, any input mode, any
audio rig — must interrupt any agent on the huddle.**

## This is a restoration, not a new feature

- **`b29c8cdaa` (#4281, 2026-08-04) deleted working local VAD
barge-in.** The pre-image shipped `BARGE_IN_DEBOUNCE_FRAMES = 20` (320
ms sustained speech cancels TTS), live at the production call site
(`pipeline.rs` passed `Some(tts_cancel)` unconditionally). The same day,
`ce3cf3cd2` (#4694) flipped the default input mode from VAD to
push-to-talk, which masked the loss.
- **`068a83b09` (#5671, 2026-08-13) removed the remaining TTS-awareness
plumbing from STT** (deliberately, to keep transcribing over agent audio
— a good change). Consequence: this PR is a re-plumb through the
`PlaybackCoordinator` from #6341, not a revert.
- A gap that **never** worked is also closed: PTT-mode users who open
the mic via the mute button (`manual_mic_unmuted` postdates the
deletion) transcribed fine but could not barge in.

## Design

All floor state lives under the single `PlaybackCoordinator` lock —
onset acceptance, epoch bump, synthesis invalidation, player
replacement, and output lease are one committed transition. No cancel
flag can be observed out of order with the playback state it describes.

- **Human floor**: local + per-peer remote ownership with epoch
invalidation. Onset cancels playback by queue replacement; late
synthesis for a stale epoch cannot append or restart.
- **Local onset (VAD)**: on an **isolated output route** (all CoreAudio
output-stream terminals report headphones), a confirmed short onset
interrupts immediately — no echo path exists. On a **coupled route**
(speakers, unknown, virtual, mixed), the restored **20-frame / 320 ms
sustained-speech debounce** discriminates a real human from speaker
bleed; the deleted code's comment records that 80 ms was tried and
false-triggered on laptop speakers. Route classification is queried
fresh at each onset (never cached — default-device re-routing mid-huddle
would strand a stale verdict).
- **Mic-open gate is per-frame**: barge-in observes on any frame where
the mic is actually open — pure VAD mode, or PTT with the mic manually
unmuted (key-held frames defer to the shortcut's own cancel).
- **Remote onset**: sustained non-DTX frames from a peer enter the same
persistent floor (independent of whether playback is live — a human
speaking while TTS is idle blocks late-arriving synthesis from starting
over them). Release on sustained DTX/absence, peer departure, and
recv-loop exit, with guards so a vanished peer cannot wedge the floor.
- **Output lease**: accepted appends renew an `Active` lease;
drain/cancel/onset start a 100 ms tail hangover (conservative against
measured ~12 ms/~1 ms CoreAudio tails), so speaker-tail bleed in the
just-drained window cannot self-trigger the coupled path.

## Known limitations (phase 2 pointers)

- Coupled-route mid-output barge-in pays the 320 ms debounce; a
playback-reference echo discriminator would shorten it.
- A speakers-rig participant's bleed can enter their mic and hold the
floor for other machines (bounded by release debounce).
- Non-macOS routes classify as coupled (fail-safe).

## Verification

- Full `buzz-desktop --lib` suite at head `b0459ae4a`: **2707 passed, 0
failed, 18 ignored** (pinned cargo 1.95.0).
- Exact CI recipe `just desktop-tauri-clippy`: PASS at head; base arm at
merge-base `b728a2af3` confirms the two
`#[allow(clippy::too_many_arguments)]`s cover branch-caused threshold
crossings (human_floor threading), not inherited noise.
- Regression tests pin: 20-frame threshold + reset-on-gap, short-coupled
rejection, sustained-coupled acceptance, coupled-idle acceptance,
remote-idle delayed-TTS rejection, output-tail hangover boundary (during
= rejected, after = accepted), isolated onset, per-frame mic-open gate
truth table, PTT+manual-unmute sustained coupled acquisition.
- `LocalBargeIn::observe` is covered as two joined halves (gate truth
table in `local_barge_in.rs`, floor transition in `tts_playback.rs`);
its body is a straight-line wrapper around a live CoreAudio query, left
uninjected deliberately.
- Coverage precision (mutation-verified):
`manual_open_ptt_sustained_speech_acquires_coupled_floor` pins the gate
→ 20-frame debounce → acquire → floor-blocked chain on the
coupled-**idle** cell. The live-output override leg is carried by
`sustained_coupled_speech_overrides_live_output_suppression`
(tts_playback.rs); the joiner's `sustained_coupled` argument is not
load-bearing there (flipping it to `false` leaves the test green, while
shortening the debounce by one frame turns it red).
- Live arms in progress: pre-regression build `b29c8cdaa^` staged to
confirm the deleted mechanism worked; two-endpoint remote-leg test
pending a second human.

## Commits

1. `fb681a5a9` — restore human barge-in (coordinator floor, lease, route
isolation, remote floor, 320 ms coupled debounce)
2. `b4265418e` — enable barge-in for manually opened mics (per-frame
gate; closes the PTT-open-mic gap)
3. `886489f2b` — extract local barge-in policy module (file-size
ratchet; also hoists the CoreAudio route query from per-frame to
per-onset, named in the commit message)
4. `b0459ae4a` — two targeted clippy allows for the widened worker
signatures

## Credits

Built by **Wren**. Regression archaeology and the PTT-open-mic gap by
**Dawn** (who also killed her own first fix as vacuous and caught a
clippy blocker before it hit CI). Review blockers (coordinator
serialization, idle-onset floors, output lease) by **Mari**. Live rig
verification by **Max**. Coordination and verification by **Eva**.
Opened by Eva with Tyler's explicit direction; commits carry agent
trailers.

---------

Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
2026-08-21 12:51:55 -04:00
Wes
1e6f1a2584 chore(release): release Buzz Desktop version 0.5.18 (#6489)
## Buzz Desktop release v0.5.18

- **Frozen main:** `aea0ef8df9fc24d9aa8bf5c761ab2910026a601b`
- **Reviewed candidate:** `39f8b46935736334cdd7045a4e4b5d7eb1a33888`
- **Previous desktop release:** `desktop-v0.5.17`
- **Proposed immutable tag:** `desktop-v0.5.18`

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

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

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
2026-08-21 10:37:05 -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
Matt Toohey
9891e64f6b fix(desktop): clarify add agents channel action (#6374)
"Create agent" button takes you to a UI to **invite** your existing
agents to a channel. Rewording the button to make this clear.

Before
<img width="481" height="251" alt="Screenshot 2026-08-21 at 1 58 40 pm"
src="https://github.com/user-attachments/assets/1b64f722-62b8-46fa-a6c4-dafcf2bdfaa7"
/>


After
(Sorry about different elements being in hover state in the screenshots)
<img width="481" height="313" alt="Screenshot 2026-08-21 at 1 58 16 pm"
src="https://github.com/user-attachments/assets/f68a9c9b-2276-4ef5-ace8-c4017977b1e8"
/>
## Summary

- Clarify that the empty-channel intro action adds existing agents to
the channel.
- Assert the exact action title and description in the existing E2E
coverage while preserving the separate Welcome create-agent flow.

### Related issue

None found.

### Testing

- `../node_modules/.bin/biome check
src/features/channels/ui/useChannelIntro.tsx tests/e2e/channels.spec.ts`
passed.
- `./node_modules/.bin/tsc && ./node_modules/.bin/vite build --mode e2e`
passed.
- The isolated Playwright smoke case `empty channel shows intro actions`
passed (1/1) after installing the repo-pinned Chromium.
- `env -u BUZZ_AGENT_PROVIDER just ci` passed.
- Screenshots not captured; this is a copy-only UI change.

---------

Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-08-21 07:07:36 -04:00
Taylor Ho
de8a2741c7 feat(desktop-messages): show compact Buzz link metadata (#6252)
**Category:** improvement
**User Impact:** Buzz links in messages now resolve into compact,
descriptive chips with predictable navigation, while tooltips use the
neutral secondary surface consistently across light and dark themes.

**Problem:** Raw Buzz-native links and opaque identifiers make message
context difficult to scan, while oversized previews disrupt the
conversation. Missing, delayed, or deleted metadata also needs a stable
presentation that does not mislead users or break navigation.

**Solution:** Keep message and issue chips metadata-independent: message
chips render only the channel name, while issue chips render only the
repository name—never fetched content/title or an event hash. Fetched
context remains available in each hover tooltip. Other native links
continue to resolve into concise inline labels, with muted secondary
fallbacks, distinct deleted states, and existing navigation paths
preserved. This also makes two explicit shared-tooltip behavior changes:

1. **Color:** shared and rich tooltips move from the primary color pair
to the neutral secondary semantic pair, including dedicated huddle-token
handling and matching rich-descendant foregrounds.
2. **Hover behavior:** tooltip roots set Radix
`disableHoverableContent`, and tooltip content is pointer-transparent,
so moving off the trigger dismisses the tooltip instead of letting the
pointer enter and keep the popover open.

Together these preserve readable contrast, authoritative project
context, and predictable hover dismissal.

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

**desktop/src/features/communities/useCommunityInit.ts**
Primes channel metadata needed to resolve native links promptly.

**desktop/src/features/messages/lib/messageLinkMetadata.test.mjs**
Covers message preview extraction, fallbacks, and deleted-message
semantics.

**desktop/src/features/messages/lib/messageLinkMetadata.ts**
Normalizes fetched message metadata into compact labels and status-aware
presentation.

**desktop/src/features/messages/ui/SentFromThreadLine.tsx**
Provides thread context to linked-message metadata resolution.

**desktop/src/shared/lib/useResolvedLinkPreviews.ts**
Exposes resolved preview state for native entity chips.

**desktop/src/shared/styles/globals/markdown.css**
Adds compact chip layout and semantic disabled-state styling.

**desktop/src/shared/styles/globals/theme.css**
Defines the semantic colors used by unavailable and deleted chips.

**desktop/src/shared/ui/markdown.test.mjs**
Exercises native-link rendering and provider-backed entity metadata
behavior.

**desktop/src/shared/ui/markdown.tsx**
Routes native markdown links through metadata-aware chip components
without changing navigation contracts.

**desktop/src/shared/ui/markdown/ChannelDeepLink.test.mjs**
Covers resolved, unresolved, private, archived, and forum channel
presentations.

**desktop/src/shared/ui/markdown/ChannelDeepLink.tsx**
Renders channel names and concise channel metadata inline with stable
fallbacks.

**desktop/src/shared/ui/markdown/MessageLinkPill.tsx**
Renders loading, ready, unavailable, DM, forum, and deleted message
states compactly.

**desktop/src/shared/ui/markdown/entityLinks.tsx**
Enriches repository, issue, and pull-request chips and uses
containing-project context in tooltips.

**desktop/src/shared/ui/markdown/types.ts**
Extends markdown runtime metadata required by native chip rendering.

**desktop/src/shared/ui/markdown/useMessageLinkMetadata.ts**
Fetches and classifies linked-message metadata while preserving
navigable fallbacks.

**desktop/src/testing/e2eBridge.ts**
Adds deterministic metadata controls for runtime visual and navigation
tests.

**desktop/tailwind.config.js**
Makes semantic disabled colors available to chip styles.

**desktop/tests/e2e/entity-link-recipient-cards.spec.ts**
Verifies entity labels, project-aware tooltips, delayed metadata, and
navigation behavior.

**desktop/tests/e2e/navigation.spec.ts**
Verifies channel and message chip states, metadata resolution, deletion
handling, and navigation.

**desktop/src/shared/ui/tooltip.tsx**
Migrates the shared tooltip surface from the primary color pair to the
semantic secondary pair. It also sets Radix `disableHoverableContent`
and makes tooltip content pointer-transparent so leaving the trigger
dismisses the tooltip predictably.

**desktop/src/features/agents/ui/RestartDiffBadge.tsx**
Aligns restart-diff tooltip values and supporting copy with the
secondary tooltip foreground.

**desktop/src/features/channels/ui/AddChannelBotTeamsSection.tsx**
Aligns rich team tooltip descriptions, persona chips, avatars, and names
with secondary tooltip semantics.

**desktop/src/features/projects/ui/ProjectAuthorIdentity.tsx**
Uses the matching secondary tooltip foreground for project author
metadata.

**desktop/src/features/projects/ui/ProjectCards.tsx**
Aligns repository-unavailable tooltip descriptions with the secondary
tooltip foreground.

**desktop/src/shared/styles/globals/utilities.css**
Makes huddle tooltips consume their dedicated palette directly without
misleading primary or secondary aliases.

**desktop/src/shared/styles/globals/tooltipSemantics.test.mjs**
Pins direct huddle-token consumption and rejects semantic alias drift.

**desktop/tests/e2e/tooltip-semantics.spec.ts**
Verifies simple and rich tooltip surfaces and descendant tokens in Buzz
light and Catppuccin Mocha dark themes.

**desktop/playwright.config.ts**
Registers tooltip semantic coverage in the desktop smoke suite.

</details>

## Reproduction steps

1. Run the desktop app and open a message containing Buzz links for a
channel, message, repository, issue, and pull request.
2. Confirm message links show only the channel name and issue links show
only the repository name inline, with no fetched content/title or event
hash; confirm other native links remain compact rather than rendering
raw identifiers or expanded previews.
3. Hover the chips and confirm the neutral secondary tooltip adds
concise context without duplicating the visible label.
4. Open each chip and confirm it follows the same channel, message,
repository, issue, or pull-request navigation as before.
5. Inspect links while metadata is loading, unavailable, or definitively
deleted and confirm the fallback remains compact, legible, and navigable
where appropriate.
6. In Buzz light and Catppuccin Mocha dark themes, compare a simple
channel-members tooltip and the rich Add agents team tooltip; both
should use secondary semantics with readable nested content.
7. Move the pointer from a trigger toward its tooltip; confirm the
tooltip dismisses rather than remaining open as hoverable content.

## Screenshots

The matrix below covers both the default light theme and Catppuccin
Mocha with an orange accent. Message/issue-chip rows affected by the
stable-label pivot are temporarily omitted until they can be recaptured
from the real built UI; the prior images showed the obsolete expanding
inline label.

| State | Default light | Dark · orange accent |
|---|---|---|
| Public stream channel tooltip | ![Public stream channel tooltip —
default
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/light-default/01-public-stream-channel-tooltip.png)
| ![Public stream channel tooltip — dark orange
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/dark-orange/01-public-stream-channel-tooltip.png)
|
| Private forum channel tooltip | ![Private forum channel tooltip —
default
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/light-default/02-private-forum-channel-tooltip.png)
| ![Private forum channel tooltip — dark orange
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/dark-orange/02-private-forum-channel-tooltip.png)
|
| Public forum channel tooltip | ![Public forum channel tooltip —
default
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/light-default/03-forum-channel-tooltip.png)
| ![Public forum channel tooltip — dark orange
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/dark-orange/03-forum-channel-tooltip.png)
|
| Channel references, including unresolved | ![Channel references,
including unresolved — default
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/light-default/04-unresolved-channel-reference.png)
| ![Channel references, including unresolved — dark orange
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/dark-orange/04-unresolved-channel-reference.png)
|
| Unknown channel permalink | ![Unknown channel permalink — default
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/light-default/05-unknown-channel-permalink.png)
| ![Unknown channel permalink — dark orange
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/dark-orange/05-unknown-channel-permalink.png)
|
| Archived channel tooltip | ![Archived channel tooltip — default
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/light-default/06-archived-channel-tooltip.png)
| ![Archived channel tooltip — dark orange
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/dark-orange/06-archived-channel-tooltip.png)
|
| Repository entity | ![Repository entity — default
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/light-default/13-repository-entity.png)
| ![Repository entity — dark orange
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/dark-orange/13-repository-entity.png)
|
| Pull request entity | ![Pull request entity — default
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/light-default/14-pull-request-entity.png)
| ![Pull request entity — dark orange
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/dark-orange/14-pull-request-entity.png)
|
| Entity metadata fallback | ![Corrected entity metadata fallback
tooltip — default
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/17-entity-metadata-fallback-corrected-light.png)
| ![Corrected entity metadata fallback tooltip — dark orange
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/17-entity-metadata-fallback-corrected-dark-orange.png)
|



### Tooltip semantic surface spot check

| Buzz light | Catppuccin Mocha dark |
|---|---|
| ![Rich team tooltip on the secondary surface in Buzz
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6290/buzz-team-tooltip.png)
| ![Rich team tooltip on the secondary surface in Catppuccin Mocha
dark](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6290/catppuccin-mocha-team-tooltip.png)
|

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-20 19:01:18 -07:00
Will Pfleger
c6e3fe7dfa feat(workflows): reply in-thread from send_message action (#6178)
The `send_message` workflow action could only post new top-level channel
messages. This adds a `reply_in_thread` option so a `message_posted`
workflow can reply in the triggering message's thread, plus a
`trigger_is_reply` filter variable so a workflow can fire only on
top-level messages.

Every NIP-10 thread-marker reader in the tree now routes through one
shared parser and one collapse rule in `buzz-core`, deleting four
hand-rolled copies that had drifted on id-validity and marker semantics.

## What changed

- **Schema** (`crates/buzz-workflow/src/schema.rs`): new
`reply_in_thread: bool` (serde default `false`) on
`ActionDef::SendMessage`. `validate()` rejects `reply_in_thread: true`
on `schedule`/`webhook` triggers — they carry no message to reply to.
- **Executor** (`crates/buzz-workflow/src/executor.rs`): threads
`reply_to: Option<&str>` (the trigger `message_id`) through
`ActionSink::send_message` when `reply_in_thread` is set; errors clearly
if the trigger has no `message_id`. `resolve_step_templates` carries the
new field. Adds `trigger_is_reply` (boolean) to the eval context.
- **Relay sink** (`crates/buzz-relay/src/workflow_sink.rs`): when
`reply_to` is set, resolves parent/root/depth from the known trigger
event, persists real thread metadata instead of the hardcoded top-level
`depth: 0`, and pushes the live kind:39005 thread-summary overlay after
insert so subscribed desktops update the root's reply badge without
refetching — matching the human ingest path. Emits the same NIP-10
`e`-tag shape as `buzz_sdk::builders::thread_tags`: a single `["e", id,
"", "reply"]` tag for a direct reply (parent == root), and the `root` +
`reply` pair only when nested — so every writer produces one wire shape
per reply kind.
- **Ingest resolver** (`crates/buzz-relay/src/handlers/ingest.rs`): new
`resolve_relay_reply_thread_meta` + `ReplyAncestry`. Unlike the
client-facing `resolve_nip10_thread_meta` (which validates
client-supplied tags), this computes root/depth from the known parent
and enforces the same same-channel and depth-limit invariants. When the
parent has no `thread_metadata` row, both resolvers share
`derive_ancestry_from_parent_tags`, which applies
`ThreadMarkers::resolve()`: a marked nested parent remains depth 2,
while a root-only, malformed, or unmarked parent is top-level and starts
its reply thread at itself. Both the client resolver and the
parent-ancestor fallback now read markers via the shared parser instead
of hand-scanning `e`-tags.
- **Shared NIP-10 parser** (`crates/buzz-core/src/nip10.rs`):
`parse_thread_markers` reads an event's `root`/`reply` markers, honoring
a marker only when its event id is exactly 64 ASCII-hex characters.
`ThreadMarkers::resolve()` is the single definition of the (root, reply)
→ (root_id, parent_id) collapse: `root`+`reply` as-is, `reply`-only →
`(reply, reply)`, a lone `root` or neither → top-level. A slice-based
`parse_thread_markers_from_parts` entry point serves consumers holding
raw JSON tag arrays. Relay ingest, the workflow `trigger_is_reply`
predicate, ACP anchoring, and the CLI reply resolver all call these, so
they cannot drift on marker, id-validity, or collapse semantics.
- **ACP** (`crates/buzz-acp/src/queue.rs`): `parse_thread_tags`
delegates marker parsing and collapse to `buzz_core::nip10`, keeping
only its local `p`-tag mention collection. This fixes a parity gap with
ingest: a malformed non-64-hex marker id is no longer counted as a
thread link, and a lone `root` marker is now top-level rather than being
treated as root == parent.
- **CLI** (`crates/buzz-cli/src/commands/messages.rs`):
`find_root_from_tags` routes its JSON tags through the shared slice
parser and collapse rule. CLI replies to root-only, malformed, or
unmarked parents now correctly start at that immediate parent;
reply-only and root+reply parent ancestry remain intact.
- **Trigger context** (`crates/buzz-workflow/src/lib.rs`):
`build_trigger_context` derives `is_reply` solely from a valid NIP-10
`reply` marker (no DB hit). A lone `root` marker is top-level to ingest,
so it does not count; neither does a `reply` marker whose event id is
malformed, nor a bare/unmarked `e`-tag (mentions, quotes).
- **Desktop** (`workflowFormTypes.ts`, `WorkflowStepCard.tsx`):
`replyInThread` form field with YAML round-trip, and a "Reply to
triggering message in thread" checkbox shown only for message-based
triggers. Switching to `schedule` or `webhook` clears the option on
every step before serialization, so a value hidden behind an action
change cannot resurrect when the action is switched back. The form
parser also requires `reply_in_thread` to be a boolean and refuses
`reply_in_thread: true` under an ineligible trigger, falling back to
YAML mode rather than silently normalizing a backend-invalid definition.

## Usage

```yaml
trigger:
  on: message_posted
  filter: trigger_is_reply == false
steps:
  - id: auto_reply
    action: send_message
    text: "pre-written reply text"
    reply_in_thread: true
```

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-20 19:44:21 -04:00
Will Pfleger
c63ac746cd perf(desktop): split discover_acp_providers into cheap and forced paths (#6330)
Splits the `discover_acp_providers` Tauri command into a cheap cached
path and an explicit forced re-discovery path.

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

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

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

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

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

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-20 19:44:05 -04:00
Wes
569308c23c fix(desktop): restore recent channel sorting (#6402)
## Summary

- batch desktop last-message queries into groups of 128 so workspaces
with larger channel counts stay within the relay's explicit-channel
limit
- propagate timestamp query failures instead of replacing the sidebar's
cached recency with all-null data
- advance channel recency from live message events so Recent ordering
updates without waiting for the next refresh

## Root cause

The desktop sent one explicit `#h` filter per channel in a single
`/query`. The relay rejects a request with more than 128 aggregate
explicit channel values. The desktop swallowed that rejection and
returned no timestamps, so Recent correctly fell back to A–Z for every
channel.

## Testing

- `cargo test --manifest-path desktop/src-tauri/Cargo.toml
last_message_filters_stay_within_relay_channel_cap --lib`
- `cd desktop && pnpm test` (5,132 passed)
- `cd desktop && pnpm exec biome check
src/features/channels/useLiveChannelUpdates.ts
src/features/channels/lib/channelRecency.ts
src/features/channels/lib/channelRecency.test.mjs
tests/e2e/channel-sort.spec.ts`
- `cd desktop && pnpm check:file-sizes`
- `cargo fmt --all -- --check`
- `cd desktop && pnpm build:e2e` followed by `pnpm exec playwright test
tests/e2e/channel-sort.spec.ts --project=smoke --workers=1` against a
dedicated static server (4 passed)

## Manual test

In a workspace with more than 128 channels, choose **Channels → Sort →
Recent**. Channels should order by latest message instead of A–Z. While
Recent is selected, a new message in a visible channel should move that
channel to the top immediately.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-08-20 17:31:39 -06:00
Matt Krueger
b298a175c4 fix(desktop): isolate main timeline stacking context from focus drawer (#6398)
The "Jump to latest" pill (`z-50`) in the main timeline renders on top
of the focus thread drawer (`z-41`) because both share the same stacking
context. This wraps the timeline contents in an `isolate` div so the
pill's z-index can't escape above the drawer.

The diff is large but the actual change is two lines — one wrapper
`<div>`. The rest is biome re-indenting the children. The
`DropZoneOverlay` also moves inside the wrapper, so its top edge no
longer extends behind the header during drag-over.

### Repro

1. Open a channel with enough history to scroll up past the bottom
2. Scroll up so the "Jump to latest" pill appears
3. Open a thread in focus mode — the pill renders on top of the drawer

### Related issue
None found.

Before:
<img width="824" height="730" alt="Before"
src="https://github.com/user-attachments/assets/eae0eb88-bcef-4c2a-bb05-08d0af6f7f38"
/>

After:
<img width="1280" height="720" alt="after-drawer-hides-pill"
src="https://github.com/user-attachments/assets/0370ba71-efc5-478a-a8e0-aac902a66082"
/>

Signed-off-by: Matt Krueger <krueger.matt@ymail.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-20 17:28:06 -06:00
Wes
06e8be9098 fix(desktop): make reconnect repair lossless (#6415)
## Summary

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

## Scope and behavior

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

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

## Testing

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

---------

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

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

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
2026-08-20 15:20:24 -04:00
Wes
ee0c7076af Fix cross-owner relay agent mentions in owner-only builds (#6338)
## Summary

- keep `BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY` scoped to Desktop-managed
local start and provider deployment boundaries
- stop filtering independently operated relay agents by viewer/owner
equality in native discovery, autocomplete, and send-time revalidation
- preserve NIP-OA ownership verification, owner-authored `respond_to`
policy, shared-channel membership, and fail-closed send-time checks
- replace the packaged-build regression expectation with coverage for
cross-owner allowlisted and `respond_to=anyone` relay agents, including
the emitted agent `p` tag

Fixes #6329.

## Why

The packaged 0.5.17 build reused its managed-runtime owner-only
capability in relay-agent mention admission. That silently hid correctly
configured shared agents owned by another operator, even when their
verified policy explicitly authorized the viewer. The capability is
intended to constrain runtimes Desktop starts or deploys, not external
relay agents.

## Validation

- `pnpm --dir desktop test` (5093 passed)
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml
relay_directory --lib` (4 passed, 2 ignored real-relay tests)
- focused agent mention unit tests (35 passed)
- Desktop TypeScript/E2E build
- focused Playwright mention tests (3 passed)
- pre-commit formatting hooks
- pre-push branch-skew, file-size, Desktop check/typecheck/unit tests,
and Tauri checks
- `git diff --check`

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-20 12:35:07 -06:00
Taylor Ho
1934e83bf5 feat(workflows): add workflow editor (#6248)
**Category:** new-feature
**User Impact:** Users can create, edit, duplicate, and deep-link to
workflows in a responsive visual editor without losing unsupported YAML
or unsaved work.
**Problem:** Workflow editing was split across disconnected surfaces and
lacked reliable URL state, lifecycle protection, and parity between Form
and YAML modes. **Solution:** This adds a route-addressable editor
foundation with stable pane identity, guarded dirty exits, lossless
Form/YAML transitions, responsive workflow and channel controls, and
matching reaction-filter execution support.

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

**crates/buzz-workflow/src/lib.rs**
Apply reaction trigger filters during workflow execution and cover
target-message gating.

**crates/buzz-workflow/src/schema.rs**
Extend the reaction trigger schema with the editor-owned filter field.

**desktop/src/app/navigation/useAppNavigation.ts**
Add navigation helpers for explicit workflow create, edit, and duplicate
editor modes.

**desktop/src/app/routes/WorkflowsRouteScreen.tsx**
Coordinate route state with the shared workflow library and editor
dialog.

**desktop/src/app/routes/lazyWorkflowsRouteScreen.ts**
Share one lazy route component across workflow route entry points to
avoid loading flashes.

**desktop/src/app/routes/workflows.$workflowId.tsx**
Parse workflow editor modes and pane deep links for workflow-specific
URLs.

**desktop/src/app/routes/workflows.tsx**
Parse library-level create state and render the shared workflow route
screen.

**desktop/src/app/AppWorkflowEditorOverlayProvider.tsx**
Host the shared workflow editor at the app-shell level so
channel-originated workflow dialogs stay above the active channel
instead of replacing it.

**desktop/src/shared/context/WorkflowEditorOverlayContext.tsx**
Expose route-independent open-existing and create-new workflow actions
to channel settings.

**desktop/src/features/workflows/ui/WorkflowEditorHost.tsx**
Share editor loading, unavailable, and dialog lifecycle wiring between
canonical workflow routes and the channel overlay.

**desktop/src/features/channels/ui/ChannelManagementSheet.tsx**
Add an experiment-gated, Canvas-style Workflows ingress below Canvas,
including channel-scoped loading, error, empty, and list states plus
open/create actions; disabled users issue no workflow query.

**desktop/src/features/channels/ui/ChannelWorkflowsSection.tsx**
Render the channel workflow list and New workflow action without pushing
the existing settings sheet past its file-size ceiling.

**desktop/src/features/workflows/ui/ChannelCombobox.tsx**
Adopt the final channel presentation, portalled scrolling, and one-shot
create-flow opening behavior.

**desktop/src/features/workflows/ui/CreateWorkflowDialog.tsx**
Remove the superseded create-only dialog in favor of the unified
workflow editor.

**desktop/src/features/workflows/ui/WorkflowCard.tsx**
Open workflow cards in the detail and run-history modal while preserving
explicit edit and duplicate actions.

**desktop/src/features/workflows/ui/WorkflowDetailDialog.tsx**
Present workflow Trigger/Steps in the shared modal chrome, with
top-chrome ingress to a responsive right-side run-history inspector and
an explicit edit action.

**desktop/src/features/workflows/ui/WorkflowDialog.tsx**
Unify create, edit, and duplicate lifecycle handling with URL panes,
generated-name synchronization, dirty-exit guards, stale-write
preservation, and protected webhook-secret handoff.

**desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx**
Build the responsive Form/YAML editor shell, stable step selection,
insertion and removal behavior, and lossless canonical-YAML
synchronization.

**desktop/src/features/workflows/ui/WorkflowStepCard.tsx**
Align workflow step controls and presentation with the final editor
interaction model.

**desktop/src/features/workflows/ui/WorkflowUnavailableDialog.tsx**
Show a non-disclosing loading or unavailable state for missing and
inaccessible workflow links, with retry and close actions.

**desktop/src/features/workflows/ui/WorkflowWebhookSecretDialog.tsx**
Obscure one-time webhook secrets by default and require explicit
confirmation before any close or navigation discards them.

**desktop/src/features/workflows/ui/WorkflowsScreen.tsx**
Connect library state and workflow actions to the route-addressable
editor.

**desktop/src/features/workflows/ui/WorkflowsView.tsx**
Restore the responsive workflow library, create tile, cards, loading
states, and shared action menu.

**desktop/src/features/workflows/ui/workflowEditorPane.test.mjs**
Cover pane parsing, serialization, and stable step-ID reconciliation.

**desktop/src/features/workflows/ui/workflowEditorPane.ts**
Define explicit trigger and stable step pane URL state.

**desktop/src/features/workflows/ui/workflowYamlDocument.ts**
Read and update header fields independently of full form validation so
incomplete steps cannot clear or disable the workflow title.

**desktop/src/features/workflows/ui/workflowYamlDocument.test.mjs**
Cover document-level workflow header reads and writes for incomplete
definitions.

**desktop/tests/e2e/workflow-title-stability.spec.ts**
Verify generated, renamed, saved, and duplicated titles remain stable
while moving between trigger and step panes.

**desktop/src/features/workflows/ui/workflowFormTypes.test.mjs**
Cover lossless Form/YAML round trips and actionable fallback for
unsupported fields.

**desktop/src/features/workflows/ui/workflowFormTypes.ts**
Own canonical workflow YAML conversion while preserving supported
trigger and step fields.

**desktop/src/shared/ui/PortalledScrollArea.tsx**
Provide bounded scrolling for popovers rendered outside their dialog
container.

**desktop/src/shared/ui/popover.tsx**
Allow workflow popovers to use the shared portalled scroll container.

**desktop/tests/e2e/channels.spec.ts**
Cover disabled and enabled Workflows experiment states, including
suppressed queries while disabled, placement beneath Canvas, channel
workflow listing and opening, channel-preselected workflow creation, and
direct return to the channel Workflows panel after close, discard, or
cancel.

**desktop/tests/e2e/workflows.spec.ts**
Exercise library actions, deep links, create/edit/duplicate lifecycle,
dirty exits, responsive editor behavior, YAML safety, stale updates, and
one-shot channel selection.

</details>

## Reproduction steps

1. Open **Workflows** and confirm the responsive card library, create
tile, card action menu, and card-to-detail/run-history modal navigation.
2. Open `?view=create`; confirm the channel chooser opens once, the
trigger inspector stays hidden until a channel is selected, and closing
the chooser does not make it reopen after unrelated edits.
3. Create a workflow, switch between Form and YAML, add and remove
steps, refresh a pane deep link, and confirm the selected trigger or
stable step remains addressable.
4. Edit or duplicate a workflow, make an unsaved change, and confirm
close, Escape, browser navigation, and route target changes require
discard confirmation while pane-only navigation does not.
5. Enter unsupported YAML and confirm Form mode gives an actionable
fallback without rewriting the definition; verify reaction triggers
preserve and execute their filter.

6. Open a channel’s settings, select **Workflows** below **Canvas**, and
open or create a workflow; confirm the shared modal stays over the
channel, the channel URL does not change, New workflow preselects that
channel, and closing or discarding returns directly to the channel’s
**Workflows** panel.
7. In create, edit, and duplicate modes, move between the trigger and
incomplete step panes and confirm the generated or edited title remains
visible and editable.

## Screenshots

Fresh captures from product head
`c5c3abc91a71fe511d43e6cc9168b1626d0c217c`; the later review-guidance
fix does not alter these pictured states.

### Workflow library and actions

![Workflow library with action
menu](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6248/workflow-library-actions-c5c3abc.png)

### Workflow editor — wide

![Wide workflow editor with step
details](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6248/workflow-editor-wide-c5c3abc.png)

### Workflow editor — narrow inspector overlay

![Narrow workflow editor with inspector
overlay](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6248/workflow-editor-narrow-c5c3abc.png)

### Workflow editor — active channel overlay

![Workflow editor portalled over the active
channel](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6248/workflow-editor-channel-overlay-c5c3abc.png)

## Review feedback addressed

- Corrected Diff Posted condition guidance to use the executor-supported
`str_contains(trigger_text, "deploy")` syntax and added a visible
Playwright regression assertion.
- Scoped direct workflow-detail navigation coverage to the stable `Edit
workflow` dialog name, asserted the workflow title separately, and
retained the trigger-node assertion.
- Routed dirty channel-overlay Duplicate/Edit transitions through the
existing discard confirmation, preserving the original YAML draft when
the user keeps editing.
- Made workflow deletion await relay success, remain non-dismissible
while pending, and retain the confirmation/editor/draft with an
actionable inline error on rejection.
- Gated the channel-settings Workflows ingress and channel workflow
query behind the `workflows` experiment, with defensive rendering if the
flag changes while that view is active.
- Matched the Reaction Added trigger to the prototype reaction picker,
preserving native/custom/legacy values in canonical YAML and providing
an explicit clear action.
- Kept a channel’s Workflows panel mounted beneath channel-origin
editors so clean close, dirty discard, and create cancel return directly
to that panel without changing the channel URL.

## Verification

Verified at exact pushed head
`76ebba7b7ace1e13445744200da70ca9da231b7e`:

- Push hooks passed: destination-org policy, branch-skew, differential
file-size, Desktop checks, TypeScript typecheck, and **5,115/5,115**
Desktop unit tests
- Focused channel lifecycle E2E passed: dirty edit discard returns
directly to the channel Workflows panel; create cancel does the same
while preserving the channel URL and preselected channel
- E2E production build, standalone TypeScript typecheck, and Biome
checks on both touched files passed
- Reaction-picker regression spec remains recorded at
`dc28ffa98ec34b4e0656757fb2dd93e60a84674d`: **5/5 passed** (picker
interaction, canonical YAML persistence/clear, legacy-value
preservation, save/reopen round trip, narrow viewport containment)
- Blox existing workflow E2E regression set at
`dc28ffa98ec34b4e0656757fb2dd93e60a84674d`: **36/36 passed**
- Earlier blocker and review regressions remain recorded at
`3760c3d657a525f5af98e8d0f98bdd03999d8e61`: **5,114/5,114** Desktop unit
tests and the dirty-overlay/deletion focused checks
- Working tree clean; local branch, remote branch, and PR head all match
the exact SHA above

### Related issue

None found. Closest prior work: #231.

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Codex <noreply@openai.com>
2026-08-20 12:15:56 -06:00
Tyler
7ebe3ea699 fix(desktop): preserve huddle speech boundaries (#6397)
## Problem and intent

Buzz huddles have been clipping speech into fragments: one live turn
arrived as the single letter “M,” and another ended mid-sentence. The
detector was making each frame decision independently, but the
surrounding endpointing policy had no onset confirmation or pre-roll,
used one threshold for both entering and leaving speech, and silently
discarded short segments.

This PR keeps Earshot 1.1.0 and fixes that policy around it. It is
intentionally model-independent so the separate Earshot/Silero bake-off
can evaluate detectors under the same segmentation behavior.

## What changed

One production file changed: `desktop/src-tauri/src/huddle/stt.rs`
(**+367/−79** versus `main`).

- Add a pure `VadEndpoint` state machine around Earshot probabilities.
- Preserve 256 ms (16 frames) of pre-roll before confirmed onset.
- Require three consecutive frames above 0.50 to enter speech.
- Use 0.35 to leave speech, preserving hysteresis-band audio.
- Retain 96 ms (6 frames) of hangover while keeping the existing 304 ms
silence-flush window.
- Make short-segment drops visible in logs instead of silent.
- Add boundary, onset, hysteresis, hangover, drop-path, and PTT policy
tests.
- Bind the thresholds to Earshot 1.1.0 in source. Earshot 1.2.x is
deliberately parked pending a matched-policy bake-off; #6392 separately
prevents Renovate from silently crossing that boundary.

## Boundary behavior and known tradeoffs

A hard message boundary clears pre-roll. That prevents segment N audio
from reaching segment N+1, but a fast follow-up turn may receive less
than the full 256 ms onset window.

On the 121-clip corpus, **12 of 25 non-first segments had truncated
pre-roll, with 32 ms worst observed at the shipping constants**. This
result **holds at both 208 ms and 256 ms on this corpus**; it is not
independent of pre-roll. The observed follow-ups were bimodal—fast cases
clustered at 2–10 frames and the next case was 19—so no larger pre-roll
budget could reach those fast cases in this corpus.

If a future refactor makes pre-roll survive a boundary, segment N
reaches segment N+1 under the strict predicate:

```text
gap < VAD_PRE_ROLL_FRAMES - VAD_ONSET_FRAMES
```

At the shipped values that means gaps 0–12 leak and gap 13 is the first
clean case. Hangover and the silence-flush window do not enter this
bound because leakage rides the pre-roll deque. Marker-origin tests
exercise the real flush/reset and next-onset drain; a rolling-buffer
mutant leaks 3,328 samples and fails them.

Other known behavior:

- `voiced_frames` now counts frames above the 0.35 exit threshold once
speech begins, not only frames above 0.50. Corpus drop counts measured
under `> 0.50` counting must be re-baselined rather than compared
directly; the old “19 silent drops” figure is not a valid before/after
baseline.
- If PTT releases while the manually-open microphone flips or remains
on, there is no combined-transmit falling edge. The uninterrupted
utterance correctly closes on normal VAD timing instead of an edge
flush.
- The GUI-to-`push_audio_pcm` wiring is unchanged. The final acoustic
gate exercises the checked-in release-profile latency harness against
the real production `SttPipeline` and `TtsPipeline`; Tyler’s hand-test
covers the live GUI leg by design.

## Verification ledger

All gates attest exact head `2dad6bb0e2e2a0af115a9a06c26cf6183fedb633`.

- **Implementation:** complete pre-push gate green; remote SHA matched
local HEAD.
- **Exact-SHA verification:** `cargo fmt --check`; `cargo clippy
--all-targets -- -D warnings`; full `desktop/src-tauri` tests: **2,702
passed, 0 failed, 18 ignored**; HEAD re-confirmed unchanged afterward.
- **Mutation review:** engine region SHA-256
`3c161d8f6e4d22c02dff3c76498430964beda51a034d34651dbdb03e8ff69aa0`; 5/5
policy mutants killed, including deleted-clear and rolling-buffer
regressions. Direct gap sweep leaked at 0–12 and was clean from 13.
- **Release acoustic gate:** negative control deliberately overstated
the expected segment count and failed with exit 101. Soft/short onset
produced 1/1 segment (`“I'm happy.”`). Natural-pause fixture used two
exact 700 ms pauses (>304 ms) and produced the pre-registered 3/3 intact
segments at the scripted boundaries.
- **Release CPU beside Pocket TTS:** soft fixture median 32.1%, p95/max
56.8% (5 sparse samples); natural fixture median 14.55%, p95/max 17.2%
(8 sparse samples). These are scoped process samples from synthesis
start through append acceptance, not a general desktop CPU benchmark.

Local review receipts (not committed to the repository):

- `.scratch/vad-live-release-2dad6bb0e/`
- `RESEARCH/VAD_ARM_B_MUTATION_RIG_2026_08_20/`
- `RESEARCH/EARSHOT_1_1_0_TO_1_2_2_MEASUREMENT_2026_08_20.md`

## Hand-test focus

Before merge, exercise the two live shapes that originally failed:

1. Soft short openers such as “M” and “yes.”
2. Natural mid-sentence pauses.

Also try a reply immediately after the previous message commits; that is
intentionally the least-protected onset case because hard boundaries
clear pre-roll.

---

Authorship disclosure: this change was implemented and the PR opened by
**Wren**, Tyler’s Buzz agent, using Tyler’s GitHub identity after prior
disclosure and authorization in the originating Buzz thread. Tyler
remains the accountable human reviewer/operator.

---------

Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
2026-08-20 14:06:00 -04:00
Diem Nguyen
886cef7f72 test(desktop): use a wordlist-safe separator in passphrase word-count test (#6356)
## Summary

Fixes #6249.


`key_backup::tests::generated_passphrase_respects_word_count_and_separator`
joined words with `-` and asserted the phrase splits back into exactly
`count` parts. The EFF short wordlist contains exactly one hyphenated
entry (`yo-yo`, line 1281 of 1296), so drawing it into either
hyphen-joined arm yields one extra part — a ~1-in-186 flake per full
suite run (`1 - (1 - 1/1296)^7 ≈ 0.539%`).

This switches the two hyphen arms to `|`, which cannot appear in the
wordlist — the exact guard the sibling test
`generated_passphrase_clamps_word_count` already documents and uses. The
space, dot, and empty-separator arms are untouched (no wordlist entry
contains a space or a dot), so the test still covers word count,
wordlist membership, and minimum length.

`generate_passphrase` itself is unchanged — a hyphenated word in a
hyphen-joined passphrase is not a product defect, only an ambiguity the
test's parsing could not handle.

## Verification

At `main` (196d62f), compiled the desktop test binary once and looped it
2000× per state:

| State | Failures / 2000 runs | Expected |
|---|---|---|
| old code | 15 | ~10.8 (P ≈ 0.539%) |
| fixed | 0 | 0 |

Full desktop Tauri suite (`cargo test --workspace` in
`desktop/src-tauri`): **2693 passed, 0 failed**.

Signed-off-by: Fizz <1f3b09af3c417274e5516bf95fadd3c118f35a31ae922257d550c69159ba931c@buzz.block.builderlab.xyz>
Co-authored-by: Fizz <1f3b09af3c417274e5516bf95fadd3c118f35a31ae922257d550c69159ba931c@buzz.block.builderlab.xyz>
2026-08-20 11:53:09 -06:00
Will Pfleger
2ce8df8533 fix(models): curate Databricks alias-aware labels for 5 missing endpoints (#6360)
Curates Databricks model labels through prefix-stripping aliasing and
materializes exact records for five endpoints missing from the
Databricks catalog.

**Alias-aware label resolution** (Databricks v2)
- Strip workspace-specific prefixes (`goose-`, `team-x-`, etc.) before
exact-record lookup so any prefixed alias resolves to its canonical
label without enumerating every alias variant.
- Return `None` for uncurated ids instead of falling back to a raw
string; callers control the fallback display.

**New exact records — label-only (Anthropic Messages route, axes from
family rules)**
- `databricks-claude-fable-5` → "Claude Fable 5" (materialized from
`anthropic-adaptive-xhigh-fable-5`)
- `databricks-claude-opus-4-8` → "Claude Opus 4.8" (materialized from
`anthropic-adaptive-xhigh-opus-4-8`)
- `databricks-claude-opus-5` → "Claude Opus 5" (materialized from
`anthropic-adaptive-xhigh-opus-5`)
- `databricks-claude-sonnet-5` → "Claude Sonnet 5" (materialized from
`anthropic-adaptive-xhigh-sonnet-5`)

**New capability-bearing record — Kimi K3 (MLflow Chat route)**
- `databricks-kimi-k3` → "Kimi K3" with axes from models.dev Moonshot
catalog: reasoning toggle + effort `[low, high, max]`, no default;
`_reconciliation_doc` records the source and reconciliation policy.

**Corpus and tests**
- 6 new normative-corpus vectors (canonical + one `goose-*` alias per
new id); executable vector count updated to 113 in both Rust and TS
gates.
- Rust and TS label tests extended to cover all 3 new canonical ids and
their aliases via the prefix stripper.
- Sentinel variable fix in discovery-provider test to prevent false
failure in Databricks dev environments where `BUZZ_AGENT_PROVIDER` is
set.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-20 13:46:10 -04:00
thomaspblock
3c228b1082 feat(desktop): refine context-aware Projects collaboration (#6396)
## Summary
- give inline project agents bounded visible-page and selection context
while reusing the shared message-thread presentation
- add contextual collaboration actions for discussing project entities
in related channels
- align project list metadata, context rails, and work-item
communication actions with the active workspace

This is Part 3 of the Projects v6 stack, following #6368. Part 4
contains the remaining navigation and detail-page polish.

## Testing
- Desktop unit suite: 5,125/5,125 passed
- Projects smoke specs: 62/62 passed
- TypeScript, Biome, typography, pubkey, and differential file-size
checks passed
- full pre-push gate passed

## Post-Deploy Monitoring & Validation
- validate Projects overview/detail agent chat and discuss-in-channel
journeys in the first staging Desktop session
- healthy signals: context matches the active project/repository/work
item, messages remain in the chosen channel, and restored conversations
exclude unrelated DM history
- failure signals: stale or cross-project context, duplicate/missing
thread rows, or collaboration actions targeting the wrong channel;
mitigate by reverting this PR

Related: #6335

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-20 12:59:09 -04:00
Wes
2e7583bf5a fix(desktop): distinguish duplicate agent devices (#6337)
## Summary

- distinguish same-name owned agents by management provenance: `managed
here` for Desktop-managed identities and `managed elsewhere` for
same-owner relay identities
- show provenance only when same-name suggestions collide, alongside
each identity's short npub
- preserve exact-pubkey selection and keep unique-agent autocomplete
unchanged
- add a composed mock-bridge regression covering relay owner
propagation, rendered labels, keyboard/pointer selection, and outbound
mention pubkeys

## Testing

- focused mention suggestion mapping and label tests
- composed Desktop E2E passes for both same-name identities and
exact-pubkey routing
- causal mutation verified: replacing the relay candidate's
`ownerPubkey` with `null` makes the composed E2E fail on the `managed
elsewhere` assertion
- pre-push Desktop checks: Biome, TypeScript, file-size ratchet, and
5,103 Desktop tests

## Manual test

With two same-name owned agents visible in a channel, type `@<name>`.
Duplicate rows identify the identities as `agent · managed here` and
`agent · managed elsewhere`, include distinct short npubs, and selecting
either routes the mention to that row's exact pubkey.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-20 10:16:01 -06:00
Matt Kursmark
e5d1dfef7b feat(desktop): close Buzz window with Cmd+W (#6314)
## Summary

- close the current Buzz window with `Cmd-W` on macOS
- keep Buzz Term's existing `Cmd-W` behavior when the terminal owns
input, where it closes the active terminal tab
- cover the close-window chord and its modifier/composition guards with
unit tests

## Why

Buzz removes macOS's native Close Window menu accelerator so the webview
can conditionally route `Cmd-W` to Buzz Term. That also left normal Buzz
mode without the standard close-window shortcut. The new app-level
bubble-phase handler restores window closing while allowing the
terminal's capture-phase handler to retain priority.

For the main window, the existing Rust close-request handler keeps the
webview alive and hides it to the tray, matching the titlebar close
button.

## Validation

- desktop unit tests: 5,093 passed
- focused close-window and terminal shortcut tests: 28 passed at
`71a2cce42013440e297c7b237d45b20e9638078d`
- desktop TypeScript: passed
- desktop checks: passed (pre-existing warnings only)
- workspace Rust clippy: passed
- Tauri Rust clippy/check: passed
- desktop build: passed
- mobile format/analyze: passed
- Tauri tests: 2,592 desktop tests and 26/27 terminal tests passed; the
existing `default_prog_child_observes_the_login_argv0` PTY fixture
repeatedly timed out waiting for `/bin/sh`, including when run alone,
and is unrelated to this TypeScript shortcut change

---------

Signed-off-by: Matt Kursmark <kursmark@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
2026-08-20 16:31:39 +01:00
thomaspblock
2b7be9340d feat(desktop): make Projects workspaces selectable (#6368)
## Summary

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

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

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

### Related issue

Related: #6335

### Testing

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

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-20 07:42:02 -07:00
thomaspblock
f88cda9eb8 feat(desktop): make the Projects overview follow the selected section (#6335)
## Summary

Part 1 of 4 stacked PRs continuing the Projects work from #6003.

- The Projects overview now follows the selected section: the right-hand
context pod stays visible across the Projects / Repositories / Reviews /
Tasks / Channels tabs and shows section-specific people, stats, and
contribution graphs, so it reads as live context rather than a detached
summary.
- Entity list rows are consolidated onto one compact line (title first,
icons after, aligned dates/counts), shared across projects,
repositories, PRs, issues, and channel lists via `ProjectEntityListRow`.
- New `projectRelatedChannels` helper resolves the channels a project is
discussed in for the overview.

Follow-ups in this stack: part 2 (selectable workspaces), part 3
(context-aware collaboration), part 4 (navigation & detail-page polish).

## Test plan

- [x] Desktop unit tests (`pnpm test`) — pass
- [x] TypeScript (`tsc --noEmit`), Biome, clippy — clean via pre-push
gate
- [x] Playwright specs updated for the new overview behavior
(`projects-v3-screenshots.spec.ts`, `sidebar.spec.ts`) — pass locally in
the smoke project

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-19 18:55:47 -07:00
Tyler
09718fbb60 refactor(desktop): coordinate TTS playback (#6341)
## Summary

- introduce a `PlaybackCoordinator` as the exclusive owner of the rodio
`Player`
- serialize append, cancellation, synthesis-flight, and drain-release
state
- cancel without blocking the coordinator by replacing the player queue
- preserve first-append/onset bookkeeping across cancellation
- increase the existing playback lead-in from 20 ms (480 samples) to a
derived 22 ms (528 samples), covering rodio 0.22.2's 512-source-sample
add-time bootstrap span

This is the behavior-preserving coordinator prerequisite for the
follow-up floor-authorization policy. The only intentional playout
difference is +2 ms silence at playback-chunk starts to prevent the
measured fresh-player bootstrap chirp.

## Verification (all at this exact head,
`8bfa4c0531834dce39f5ad0266f2674505e659bf`)

- `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --workspace
--all-targets -- -D warnings` — clean
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml --package
buzz-desktop` — 2,596 passed / 17 ignored / 0 failed
- coordinator concurrency tests: stalled cancellation, append-vs-cancel
serialization, cancel election, first-append rearming, and the post-swap
`starts_playback_chunk` invariant
- headless known-answer sweep: 528-sample lead-in clean across 7/7
device configurations (incl. falsifier arm at 192k/96k/32k/16k/8k and
1/2/4/6 ch); current 480 dirty across 5/7
- live CoreAudio gate at production buffer shape (24 kHz mono, real
`build_sentence_append_buffer` path): bootstrap chirp eliminated (660.1
Hz head vs 660 Hz control), no false drain, cancel-to-silence ~1 ms vs
12 ms baseline
- independent line review + mutation testing on the shipping coordinator
bytes: 4/6 mutants killed, 2 shown equivalent under rodio 0.22.2
`Player::drop` semantics

## Playout cost

- +2 ms lead-in at each playback-chunk start versus main
- first post-cancel utterance receives 22 ms total intentional silence
(not stacked cushions)

## Provenance

Authored and reviewed by Buzz agents (Wren: implementation; Dawn: line
review, known-answer control, mutation testing; Max: live CoreAudio
gates; Eva: coordination and final verification), with review records on
the Buzz relay. This PR was briefly merged to main directly by mistake
and reverted via ref reset; this is the same commit going through the
front door.

---------

Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: Dawn <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Dawn <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@buzz.block.builderlab.xyz>
2026-08-19 21:26:03 -04:00
Evan Chen
9c2f05346f fix(desktop): show complete repository trees (#5102)
## Summary

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

### Related issue

Fixes #4960. No duplicate PR found.

### Testing

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

Generated with Codex

---------

Signed-off-by: Evan Chen <evanc@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
2026-08-19 15:53:14 -07:00
morgmart
9a1e861ab8 Add appearance preference previews (#6193)
## Summary

- Replace the Link previews and Thread layout dropdowns with the shared
segmented control from #5644.
- Pair each setting with a real, inert preview: production Compact/Rich
link cards and a simplified Focus/Split thread diagram.
- Keep scrub previews local to Settings so only a completed selection
updates the saved preference and real app behavior.
- Keep each setting and preview responsive as one grouped unit, with
focused light/dark and narrow/wide coverage.

## Dependency

- Stacked on #5644, which provides the typography/density foundation,
shared `SegmentedControl`, and responsive `SettingsOptionRow`.
- After #5644 merges, this PR should be retargeted/rebased onto `main`.

## Validation

- `pnpm typecheck`
- `pnpm check:px-text`
- `pnpm check:file-sizes`
- `pnpm exec playwright test tests/e2e/appearance-previews.spec.ts
--project=smoke` (3 passed)
- Focused existing Appearance coverage (2 passed)
- Pre-push desktop checks, typecheck, tests, and Tauri checks

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
2026-08-19 14:32:17 -07:00
Wes
f96f1883c5 fix(desktop): restore emoji recents (#6263)
## Summary

- normalize emoji-mart's poisoned literal `{}` recents state before
initialization
- preserve the established idle emoji-index prewarm so first picker open
stays responsive
- keep the mounted quick-reaction tray behavior unchanged from main
- cover full-picker same-session recents, prewarm ordering/callback
behavior, and the production picker-to-prewarm import boundary

## Validation

- focused prewarm tests passed 2/2 at
`98188797e219db20670b46379a240493ff8f4cbe`
- mutation check passed: changing `EmojiPicker` back to direct
`@emoji-mart/data` consumption fails the new production-boundary
assertion
- pre-push branch-skew, file-size, Desktop check, typecheck, and full
Desktop unit suite passed (5,024/5,024) at exact pushed head
`98188797e219db20670b46379a240493ff8f4cbe`
- focused full-picker lifecycle plus existing custom-emoji/tray smoke
passed 2/2 on the preceding runtime-equivalent head
- independent Royal Court scope review and headless field test passed
the full-picker journey on the preceding runtime-equivalent head

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-19 13:31:15 -07:00
Tyler
bbd20fae75 perf(desktop): move five hot renderer paths from JS into Rust (#6024)
## Summary

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

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

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

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

### Known behavior notes

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

### Related issue

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

### Testing

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

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

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

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

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

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


---

## Review round 2 — head `9128b9389`

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

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

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

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

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

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

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

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

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

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

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

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

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


---

## Round 3 — final head `9a8128ba4`

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

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

### Receipts at `9a8128ba4`

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

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

Independent reviewer, same exact head, own instruments:

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

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

---------

Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Dawn <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-19 14:20:58 -04:00
Logan Johnson
fe7c6808e7 fix(desktop): hide archived channels from #/Tab autocomplete (#6156)
🤖

## Summary

Desktop's `#`/Tab channel autocomplete offered archived channels as if
they were valid destinations. Stale channels competed with active ones
in the picker, including a confusing uppercase `STREAM` entry — the
badge of an archived stream channel leaking into the suggestion list.
After this change, autocomplete suggests only active, non-DM channels.
Links to archived channels in old messages still resolve normally.

## Demo

Before/after, real archive flow:

![Demo — archived channel disappears from #
autocomplete](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6156/demo-archive-autocomplete.gif)

## What changed

`useChannelLinks.ts` builds the autocomplete suggestion list from the
raw relay channel list and previously excluded only DMs, so archived
channels slipped through. The fix filters `archivedAt === null` while
generating suggestions only — the shared navigation list stays
unfiltered, so resolving historical links to archived channels is
unchanged.

- `desktop/src/features/messages/lib/useChannelLinks.ts` — suggestion
selection centralized in `selectChannelSuggestions`; excludes archived
channels and DMs; keeps the existing eight-result limit.
- `desktop/src/features/messages/lib/useChannelLinks.test.mjs` — new
tests: archived stream/forum excluded, active ones included, DMs
excluded, mixed lists.

## Validation

At exact head `5a963b8407bd48f40d992e026d0a841d2445647f`: `pnpm check`,
`pnpm typecheck`, and the full Desktop unit suite (4,960 passed). Smoke
E2E run compared against exact base `f956e6fe`: the only failures are
two pre-existing failures that reproduce on base and four documented
pass-on-retry flakes — none touches the autocomplete surface.

Signed-off-by: loganj <loganj@squareup.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-08-19 12:57:25 -04:00
thomaspblock
e5a6e2022f fix(desktop): morph the drawer panel icon instead of sliding it (#6306)
## Summary
- Replaces the drawer panel icon's CSS `translateX` slide with a
`motion/react` width + corner-radius morph, so the icon reads as the
panel opening rather than the glyph drifting sideways.
- Honors `prefers-reduced-motion` via `useReducedMotion` (no animation
for users who opt out).

Follow-up polish to the projects workspaces work that just landed in
#6003 — the slide animation shipped there was the wrong visual.

## Test plan
- [x] Desktop unit tests and typecheck pass with this file at this
content (validated as part of the projects-v6 branch validation)
- [ ] Visual check: open/close the right drawer and confirm the icon
morphs in place

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-19 09:06:58 -04:00
thomaspblock
87f8ff82ae feat(desktop): refine repository-aware project workspaces (#6003)
## Summary
- Makes Projects repository-aware across browsing, branch and tag
selection, local and remote source management, work items, commits, and
contextual repository actions.
- Refines project details into consistent single-column reading surfaces
with a resizable, section-aware context pod, while keeping project
conversations available in the attached chat panel.
- Adds persistent sidebar project navigation, direct entity links,
repository discussion channels, contributor identity matching, and
consistent loading and activity presentation.
- Keeps branch-specific controls on code-oriented sections while Tasks,
Reviews, and Channels remain repository-scoped, reducing misleading
context and actions.

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

### Related issue
N/A

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

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Wintermute <165f0c871dd2586bb18b6aa109eeaf57bb2132ff4d27b10120f4368a0f627022@buzz.block.builderlab.xyz>
Co-authored-by: Wintermute <165f0c871dd2586bb18b6aa109eeaf57bb2132ff4d27b10120f4368a0f627022@buzz.block.builderlab.xyz>
2026-08-19 12:50:00 +01:00
Will Pfleger
a362fecc23 perf(desktop): parallelize relay agent directory rebuild (#6258)
The shared-agent directory rebuild resolves runtime directories, owner
profiles, and managed policies for every candidate agent via dozens of
exact-author query batches. Issuing those batches serially made a
~100-agent rebuild take 6.5–8.4s, which dominated @mention autocomplete
latency.

#6224 already scoped the *send-path* revalidation
(`revalidate_relay_agents`) to just the mentioned pubkeys, so the send
stall is fixed. But the autocomplete directory (`list_relay_agents`)
still rebuilds the full membership set serially — this PR removes that
remaining cost.

## Change

Run the query batches with bounded concurrency via a shared
`query_filter_batches` helper. Each directory rebuild constructs one
`tokio::sync::Semaphore` (8 permits) and shares it across all of that
rebuild's phases, so the runtime-directory and owner-profile phases that
run concurrently under one `try_join!` never exceed 8 `/query` requests
in flight together — the bound is per-rebuild. The policy phase reuses
the same budget. Same events, keyed by pubkey downstream so ordering is
irrelevant.

Both `list_relay_agents` (autocomplete) and `revalidate_relay_agents`
(scoped send-path check from #6224) funnel through
`list_relay_agents_for_selection`, so the helper is a no-op for the tiny
1–3-mention revalidation set and only the full autocomplete rebuild sees
the win — the scoped send path is untouched.

## Measurement

Live on the production relay, warm connection, full `list_relay_agents`:

| | Full-directory rebuild |
|---|---|
| Serial (before) | 6.5–8.4s |
| Bounded-concurrency (after) | 2.0–3.4s |

Returned pubkey set is byte-identical pre/post. The 8-permit ceiling
holds under saturation (24 batched requests → peak exactly 8, zero
failures, zero requests left in flight).

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-18 22:35:50 +00:00
Will Pfleger
121e4b3ce7 fix(desktop): exclude archived agents from nest, order regeneration (#5905)
The managed "Active Agents" table in `~/.buzz/AGENTS.md` was rendered
from every managed-agent record with no filtering, so archived duplicate
instances kept appearing under the active relay's header. This scopes
the roster to identity-active agents and makes regeneration safe under
concurrency.

## Roster filter: identity-archive only

`render_dynamic_section` now drops only records whose pubkey is present
in the relay's `kind:13535` archive snapshot. Local records can't tell
they're archived — they all carry `is_active: true` (that flag is a
*definition*-archive, not an identity-archive), so archive truth lives
only relay-side. The read **fails open**: an unreachable relay yields an
empty set and hides no one.

There is deliberately **no** relay-scope filter. `relay_url` is a legacy
creation-era field that `effective_agent_relay_url()` ignores — every
agent is eligible on every community, and snapshot-imported records
store `relay_url: ""` by design. Filtering on it would hide valid,
runnable agents after a workspace switch or import. Foreign-relay relic
records leave the table via record deletion, not code.

## Regeneration on archive / unarchive

`archive_identity` and `unarchive_identity` submitted the relay event
and returned without refreshing `AGENTS.md`, unlike the ~20 other
mutation sites that call `try_regenerate_nest`. A just-archived agent
therefore lingered on the roster until an unrelated edit or the next
launch. Both commands now trigger a regeneration after a successful
submit.

Regeneration is bound through a `NestRegenTrigger` trait rather than
constructing the `try_regenerate_nest` callback at the Tauri-command
delegation site. The command cores take `regen: &impl NestRegenTrigger`
and own the `|| regen.trigger()` binding; the thin wrappers only pass
`&app` (whose `impl` calls `try_regenerate_nest`). This puts the regen
wiring inside the unit-tested core — a `CountingRegen` double proves
each core fires exactly one regeneration — instead of an untestable seam
where a wrapper could silently lose the refresh while the suite stayed
green. The regen races the relay's `kind:13535` snapshot update, so it's
best-effort and fail-open — a stale render self-heals on the next cycle.

## Ordered regeneration

`try_regenerate_nest` previously spawned unconstrained tasks that each
snapshotted state, awaited two relay requests, then wrote — so a slow
pre-edit generation could overwrite a newer one. Boot made this
deterministic: the boot regen races the `apply_workspace` regen, and the
fallback-relay render could finish last.

`NestRegenGate` fixes this with a single `highest_requested` watermark.
A monotonic generation is claimed *synchronously* at request time
(encoding call order) and advances the watermark under one lock; the
spawned task carries its generation and, at commit, reads the watermark
under that same lock — the compare-and-write is atomic with no await
held across it. A generation whose number is below the current watermark
drops its result instead of rolling the file back.

Gating on highest-*requested* rather than highest-*written* is the
load-bearing choice: if a newer generation is requested but then fails
its relay reads, an older in-flight generation must **not** publish its
now-obsolete roster. Behavior delta: once a newer regeneration has been
requested, no older generation will ever write; if that newer generation
fails, the file is left as-is and self-heals on the next trigger rather
than regressing to a stale snapshot. This is an ordered,
latest-request-wins gate — not a work coalescer: superseded generations
still perform their relay reads and drop the result at commit time.

The gate's commit lock is acquired with a poison-to-`io::Error` mapping
rather than `expect()`, so a poisoned lock degrades to the same
warn-and-continue path as any other commit failure instead of panicking
the desktop process (a best-effort housekeeping write must never take
down the app).

## One relay target per regeneration

A regeneration read the workspace relay override three times — the
NIP-11 signer in `fetch_relay_self`, the snapshot query in
`query_relay`, and the rendered footer — so a workspace switch
mid-flight could pair one relay's advertised signer with another relay's
snapshot, fail open, and render archived agents as active.
`capture_relay_target` now resolves the effective relay (WebSocket +
HTTP API base) once, before any network work, and
`fetch_archived_pubkeys_at` threads that single target through both the
NIP-11 fetch and the `/query`; the footer renders the same target.
Signer, snapshot, and footer always belong to one relay.

## Monotonic archival snapshot publishing

`publish_nipia_archival_list` stamped its `kind:13535` snapshot with a
whole-second `created_at`. A rapid archive→unarchive within the same
second produced two events whose NIP-16 replaceable-event tie-break
(higher event id wins) could strand the older, stale archive state as
canonical. Publishing now uses a bounded retry that re-reads current
archive state and rebuilds the snapshot each attempt, so the published
list reflects the latest intended state rather than a fixed same-second
race loser. After the attempt budget (8) is exhausted the publisher
`bail!`s; the sole caller treats that as a `warn!` side effect and
continues, matching the surrounding best-effort submit path.

## Test-file split

The renderer, `upsert_managed_section`, and the regeneration-gate tests
moved to `nest/render_tests.rs` so each test file stays under the
repository's 1000-line ratchet.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-18 18:26:35 -04:00
klopez4212
7e2651791d Add font size and conversation density preferences (#5644)
## Summary

- Add separate **Font size** and **Conversation density** controls in
Appearance.
- Use a 13 / 14 / 15px conversation text ramp for Smaller / Default /
Larger while scaling interface typography through a shared virtual-rem
system.
- Keep layout geometry fixed while typography changes, and compose Cmd
+/- text zoom on top of the selected preference.
- Let Compact, Comfy, and Spacious control conversation row spacing plus
paragraph and list rhythm in Markdown content across Buzz.
- Preview font size and density together with click-and-drag comparison;
interrupted scrubbing restores the saved preference.
- Synchronize preferences across open Buzz windows, including full
local-storage clears during sign-out or dev reset.
- Promote the segmented control to shared UI and make Settings rows
adapt to their card width.

## Validation

- `just ci`
- 4,965+ desktop unit tests across the final merged branch
- 1,465 mobile tests through the full repository gate
- Focused Playwright coverage for font-size/density independence, drag
preview cancellation, Inbox geometry, thread rendering, keyboard zoom,
and cross-surface typography
- Fresh E2E build against the final merged branch

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: morgmart <98432065+morgmart@users.noreply.github.com>
2026-08-18 15:24:09 -07:00
Taksh Kothari
6e8d078ffe fix(desktop): emit camelCase config-write payload fields (#6062)
Fixes #6015.

`ConfigWriteMechanism` is internally tagged and carried only `rename_all
= "camelCase"`. On an internally tagged enum that renames the
**variants**, never the variants' fields, so the payload went out
snake_case:

```
{"type":"respawnWithEnvVar","env_key":"K"}
{"type":"acpSetConfigOption","config_id":"c"}
{"type":"acpSetSessionModel"}
{"type":"gooseNativeConfigWrite","config_key":"g"}
{"type":"readOnly"}
```

against `envKey` / `configId` / `configKey` in
`desktop/src/shared/api/types.ts:615-620`. That output is a probe run of
the real module before the fix, not a reading of the code.

What makes it read as correct is the asymmetry: the variant *names*
rename fine, so the `type` discriminant and every `switch
(writeVia.type)` behave; and the enclosing `NormalizedField`'s own
fields (`writeVia`, `overriddenValue`, `isRequired`) rename fine too,
because `rename_all` does apply to struct fields. Only the variant's
field is wrong.

Adding `rename_all_fields = "camelCase"` fixes it. `rename_all_fields`
appeared zero times in `desktop/src-tauri` before this.

**Severity, stated plainly: latent, not currently user-visible.**
Nothing in `desktop/src/**` reads `.envKey`/`.configId`/`.configKey` off
a `writeVia` — `AgentConfigPanel.tsx` is the only `RuntimeConfigSurface`
consumer and never touches the field, and no Rust code deserializes the
type either. The write-back path these fields exist for is not wired
yet. The hazard is for whoever wires it: `invokeTauri<T>` is an
unchecked cast, so they get `undefined` with a green `tsc`.

I also carried the attribute onto `ConfigFieldType`. Its only payload
field is `options`, single-word, so that half is not a fix — it is the
attribute the next multi-word field would silently need.

One divergence from the issue's suggested step 3: the 20 `e2eBridge.ts`
sites already emit camelCase, and camelCase is the contract, so they are
correct as written — changing them would have been wrong. What they
lacked was provenance, since agreeing with `api/types.ts` while the
backend emitted something else is exactly what let this sit. They now
name the Rust test that pins the bytes.

**Tests** (`wire_format_tests`, 4 cases, whole-value not key-set — a
key-set assertion still passes when a variant name regresses):
- every variant against the TypeScript spelling;
- the nested `NormalizedField`, which is the shape the renderer actually
receives;
- a camelCase round-trip **plus** an assertion that the old `env_key`
spelling is now rejected, so a revert cannot quietly keep deserializing;
- `ConfigFieldType::Enum`.

Removing `rename_all_fields` again turns three of the four red.

Verified locally: full Tauri library suite 2444 passed / 15 ignored / 0
failed; `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml
--workspace --all-targets -- -D warnings` clean; `cargo fmt
--manifest-path desktop/src-tauri/Cargo.toml --all -- --check`; in
`desktop/`: `pnpm typecheck`, `pnpm check`, `pnpm test` 4954 passed;
`git diff --check`. Not run: the app itself — there is no UI path to
this field yet, which is the same reason the bug is latent.

---------

Signed-off-by: Taksh <takshkothari09@gmail.com>
2026-08-18 15:19:05 -07:00
Will Pfleger
e2ade93f02 fix(desktop): downscale large avatars for agent-share PNG body (#6260)
Sharing an agent encodes a `.agent.png` snapshot with the avatar as the
PNG image body. Non-PNG avatars were transcoded at full resolution and
already-PNG avatars were carried through unchanged, so a large source
avatar could produce a PNG that exceeds the 10 MiB
`MAX_SNAPSHOT_PNG_BYTES` cap and fail the send. A 2764×4096 webp avatar
encoded to ~26 MB — over 2.5× the ceiling — which is why sharing that
agent hung for a few seconds (fetching and transcoding the image) and
then failed.

The share dialog compounded the problem: on failure it discarded the
pipeline's real error (`Snapshot exceeds the 10 MiB size limit…`) and
toasted a generic `Couldn't send … Try again.`, hiding the actual cause.

## Changes

- **Downscale the avatar for the snapshot PNG body.** The body is only a
card thumbnail, so raster avatars are now downscaled to a 512px longest
edge (`MAX_PNG_BODY_EDGE`) before PNG re-encoding, mirroring the
frontend SVG rasterizer's 512×512 cap in `snapshotAvatarPng.ts`.
Already-PNG avatars over the dimension cap route through the same
downscaling transcode path instead of a straight tEXt-chunk injection.
The manifest's `avatar_url`/`avatar_data_url` source reference is
untouched — only the PNG image body is downscaled.
- **Surface the real error in the share dialog.** `PersonaShareDialog`
(used by `SnapshotShareDialog`/`TeamShareDialog`) and
`AgentCardViewerDialog` now toast the send controller's actual error
message, falling back to the generic string only when it is empty. A
`getCurrentError()` accessor reads the error through a ref because the
render-captured `state.error` is stale in the closure immediately after
`beginSend` resolves.

A unit test pins the invariant: an oversize (2764×4096) avatar produces
a snapshot that stays far under the cap with body dimensions clamped to
512px.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-18 15:18:48 -07:00
Tyler
6ea7a2b221 fix(desktop): preserve early relay auth challenges (#3320)
## Summary

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

## Root cause

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

## Verification

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

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

---------

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

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

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

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

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

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

## Distinguish a rejected model switch from silent success

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

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

## Scope explicitly excluded

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

## Sanctioned follow-ups

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

Closes #2692, #2884, #3493

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-18 16:29:28 -04:00
Taylor Ho
d7e8fdb10c fix(shared-ui): delay hover disclosures by default (#5821)
**Category:** fix
**User Impact:** Hover tooltips and informational popovers now wait for
deliberate pointer dwell instead of appearing while users move around
the app.

**Problem:** Tooltips and hover-controlled popovers appeared after
inconsistent, often very short delays, so moving across composer and
navigation controls could obstruct the next interaction.

**Solution:** Establish a 500 ms shared dwell default with no tooltip
skip-delay cascade, apply it to informational hover-controlled Popovers,
and preserve immediate click and keyboard behavior. The responsive
Community actions navigation submenu retains its documented 80 ms open /
160 ms close timing.

## Before / after

| Before | After |
|---|---|
| ![Before: hover disclosures appear during pointer
transit](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5821/before-hover-disclosure.gif)
| ![After: hover disclosures wait for deliberate pointer
dwell](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5821/after-hover-disclosure.gif)
|

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

**desktop/src/shared/ui/tooltip.tsx**
Wraps the Radix provider with documented 500 ms and zero skip-delay
defaults. The provider API still permits a future proven exception, but
no current `desktop/src` caller overrides either timing.

**desktop/src/shared/ui/popover.tsx**
Exports the documented shared hover-open timing for controlled popovers;
ordinary click/focus Popovers remain immediate.

**desktop/src/main.tsx**
Uses the shared Tooltip provider defaults at the application root.

**desktop/src/shared/ui/sidebar.tsx**
Removes the sidebar's instant Tooltip timing override.

**desktop/src/features/channels/ui/AddChannelBotTeamsSection.tsx**
Removes the local short Tooltip timing override.

**desktop/src/features/home/ui/InboxDetailPane.tsx**
Removes the local short Tooltip timing override.

**desktop/src/features/messages/ui/MessageTimeline.tsx**
Removes the timeline's local short Tooltip timing override.

**desktop/src/features/messages/ui/MessageTimestamp.tsx**
Drops the timestamp-only provider now that its 500 ms, zero-skip
behavior is shared globally.

**desktop/src/features/channels/ui/BotActivityBar.tsx**
Raises composer agent-activity hover dwell from 150 ms to the shared
default while preserving immediate click/focus opening.

**desktop/src/features/sidebar/ui/ChannelActivityPopover.tsx**
Raises channel activity hover dwell from 250 ms to the shared default
while preserving immediate focus opening.

**desktop/src/features/profile/ui/UserProfilePopover.tsx**
Reuses the shared hover timing in place of its equivalent local
constant.

**desktop/src/shared/ui/PubKey.tsx**
Reuses the shared hover timing in place of its equivalent local
constant.

**desktop/src/shared/ui/markdown/InlineEmojiPopover.tsx**
Raises emoji inspection hover dwell from 200 ms to the shared default
while preserving immediate focus opening.

**desktop/src/features/messages/ui/MessageReactions.tsx**
Raises reaction inspection hover dwell from 200 ms to the shared default
while preserving immediate focus and reaction clicks.

**desktop/src/features/communities/ui/CommunitySwitcher.tsx**
Documents the Community actions navigation submenu as an intentional
timing exception: 80 ms to open responsively and 160 ms to preserve the
pointer bridge into its portalled panel.

</details>

## Reproduction steps

Move the pointer rapidly across each surface first, then hold it still
over a labeled control. Hover-only disclosures should stay closed during
transit and open after about **500 ms** of deliberate dwell. Moving
directly between adjacent Tooltip triggers should start a fresh 500 ms
dwell rather than cascading the next Tooltip open immediately.

| Surface | Where to test | What to expect |
|---|---|---|
| Composer controls | Attachment, emoji, image editor, formatting, and
composer toolbar buttons | No Tooltip while sweeping across controls;
the hovered control's Tooltip opens after ~500 ms. Clicking remains
immediate. |
| Message actions | Hover a message, then test reply, react,
more-actions, edit, and related action-bar controls | Each Tooltip waits
~500 ms, including when moving between adjacent actions. The action
itself still runs immediately on click or keyboard activation. |
| Message metadata and content tools | Message timestamps, code-block
copy controls, diff controls, system-message controls, and video-player
controls | Tooltip appears after ~500 ms. Timestamp behavior should look
unchanged; it was already 500 ms with no skip cascade. |
| Reaction pills | Hover a reaction with one or more reactors; also
click the pill | Reactor Popover waits ~500 ms instead of 200 ms.
Clicking still toggles the reaction immediately. |
| Inline custom emoji | Hover a rendered custom emoji in message
Markdown, then focus it with the keyboard | Emoji inspector waits ~500
ms on hover instead of 200 ms. Keyboard focus opens it immediately. |
| Masked links | Hover a masked Markdown link, including one revealed
inside a spoiler | Destination Tooltip waits ~500 ms instead of the
former app-level 300 ms. Hidden spoilers still reveal no destination;
keyboard focus remains immediate. |
| Main and collapsed sidebar controls | Collapse the sidebar and hover
icon-only navigation/menu buttons; also test community-rail controls |
Sidebar Tooltips wait ~500 ms instead of opening instantly. Rapid
movement across icons should not produce a tooltip cascade. |
| Channel header and management controls | Channel members, huddle,
settings, thread-view mode, management rows, and quick-agent controls |
Each Tooltip waits ~500 ms; click/keyboard behavior remains immediate. |
| Add-channel team chips | Open Add channel where saved teams are
available and hover a team chip | Team details Tooltip waits ~500 ms
instead of 150 ms; clicking the chip still toggles the team immediately.
|
| Channel activity preview | Hover a sidebar channel that has activity,
then keyboard-focus its trigger | Activity Popover waits ~500 ms instead
of 250 ms. Focus opens it immediately, and quickly crossing channel rows
should not leave previews in the way. |
| Composer agent activity | Run an agent so the composer activity
control is present; hover, click, and focus it | Hover Popover waits
~500 ms instead of 150 ms. Click and keyboard focus still open it
immediately. |
| Inbox and draft controls | Home inbox open-context/more-actions
controls, inbox-list controls, draft detail, and drafts panel | Tooltips
wait ~500 ms instead of the inbox detail's former 200 ms/local defaults.
Clicks remain immediate. |
| Profile and public-key previews | Hover avatars, names/mentions,
project author identities, and displayed public keys | Profile/pubkey
Popovers open after ~500 ms, matching their prior behavior; the change
centralizes that timing. Click actions and focus behavior remain
immediate. |
| Agent, team, memory, and update controls | Managed-agent rows, team
identity cards, restart-diff badges, memory actions, setup steps, and
update indicator | Tooltips wait ~500 ms and do not cascade when
traversing adjacent controls. Actions remain immediate. |
| Huddle controls | Huddle bar/indicator, mic controls, and
participant-list actions | Tooltips wait ~500 ms; mute, join,
participant, and keyboard actions remain immediate. |
| Projects and activity surfaces | Project/repository cards, overview
rail, contribution graph, activity feed, reviewers, and Pulse note
controls | Tooltips wait ~500 ms with a fresh dwell between adjacent
targets. Clicking/focusing interactive controls remains immediate. |
| Intentional interaction-mode exceptions | Focus a Tooltip trigger;
click/focus an ordinary Popover; move the pointer from an open hover
Popover into its panel | Tooltip focus and Popover click/focus open
immediately because they are explicit user intent, not incidental hover.
Hover Popovers retain their 180–200 ms close grace so the pointer can
cross into the panel. The Community actions navigation submenu is the
intentional exception: it opens after 80 ms and keeps its 160 ms pointer
bridge; informational hover Popovers use the shared 500 ms delay. |

## Verification

- `pnpm typecheck`
- `pnpm check` (passes with three existing diagnostics outside this
diff)
- `pnpm test` — 4,775 passed
- `pnpm build`

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-18 12:55:00 -07:00
Taylor Ho
c442a90a17 fix(desktop-chrome): preserve balanced layout when sidebar collapses (#6000)
**Category:** fix
**User Impact:** The desktop content surface now keeps balanced chrome
spacing when the sidebar is collapsed or multiple communities are
visible, with a cleaner sidebar transition.

**Problem:** Collapsing the left sidebar removed the content surface's
left inset, while multi-community layouts also exposed uneven visible
gutters and sidebar content during the exit transition. **Solution:**
Preserve an 8px visible gutter around the content surface, clip and fade
only the sidebar's inner content while it trails off canvas, and leave
the opaque shell in place until the existing visibility transition
completes.

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

**desktop/src/app/BuzzThemeSurfaces.tsx**
Preserves a balanced visible left gutter after accounting for the
content-edge shadow.

**desktop/src/shared/ui/sidebar.tsx**
Clips the collapsing sidebar, disables interaction off canvas, and
fades/translates its inner content without exposing a different
background.

**desktop/tests/e2e/community-rail.spec.ts**
Adds gutter assertions for single-community collapsed-sidebar and
multi-community layouts.

**desktop/tests/e2e/sidebar.spec.ts**
Covers the collapse opacity, translation, clipping, pointer-event
behavior, and restoration on reopen.

</details>

## Reproduction steps

1. Launch the desktop app with the Buzz theme and at least two
communities.
2. Collapse the channel sidebar.
3. Confirm the main content surface keeps equal visible left and right
gutters and does not overlap the community rail.
4. Reopen and collapse the sidebar again; confirm its contents fade and
trail right while the chrome background stays opaque, then restore fully
when reopened.

## Screenshots

| Single community | Multiple communities |
| --- | --- |
| ![Sidebar collapse with a single
community](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6000/multiple-communities-collapse.gif)
| ![Sidebar collapse with multiple
communities](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6000/single-community-collapse.gif)
|

## Validation

- `pnpm build:e2e`
- focused sidebar integration tests: 2 passed
- collapsed multi-community smoke regression: 1 passed
- Biome check on all four changed files
- pre-push desktop check, typecheck, and test hooks
- `git diff --check`

Related issue/PR: none found.

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-18 12:54:40 -07:00
Wes
8232299cbe chore(release): release Buzz Desktop version 0.5.17 (#6234)
## Buzz Desktop release v0.5.17

- **Frozen main:** `3fdf289b78c40f80abce86575c25b5ed6361d82c`
- **Reviewed candidate:** `c3bfd66947978fae93f4cfb46bea98ba20e32ccf`
- **Previous desktop release:** `desktop-v0.5.16`
- **Proposed immutable tag:** `desktop-v0.5.17`

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

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

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
2026-08-18 09:40:31 -07:00
Wes
3fdf289b78 fix(desktop): bound remote agent mention authorization (#6224)
## Summary

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

## Security and trust boundary

The targeted command reuses the existing authoritative chain:

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

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

## Validation

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

## Notes

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

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz>
2026-08-18 08:46:26 -07:00