Commit Graph
2337 Commits
Author SHA1 Message Date
Taylor HoandCarl db968fa22d feat(workflows): add URL-addressable editor foundation
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-17 17:01:44 -07:00
7f61cf431a Preserve managed agent mentions during relay errors (#6167)
## Summary

- preserve selected managed-agent `p` tags when fresh managed-directory
evidence succeeds but relay discovery or owner-profile lookup fails
- keep relay-only agents fail-closed unless fresh relay evidence and any
required owner proof are available
- cover selective admission with focused unit tests and a signed-event
Playwright regression

## Testing

- `node --import ./desktop/test-loader.mjs --experimental-strip-types
--test
desktop/src/features/messages/lib/agentMentionRevalidation.test.mjs` (7
passed)
- focused Playwright regression plus adjacent relay-revocation case (2
passed)
- pre-commit desktop Biome/file-size hook
- pre-push desktop check, TypeScript typecheck, and full desktop unit
suite (4,987 passed)

Fixes #6147

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-17 15:27:37 -07:00
Taylor HoandGitHub f7a01bda7b fix(workflows): preserve multi-channel listing semantics (#6009)
**Category:** fix
**User Impact:** Workflow listings reliably include every accessible
channel, including for users with more than 128 memberships and when
connected to older relays.
**Problem:** Multi-value `#h` filters could lose live delivery, apply
channel scoping after SQL limits, mishandle partial authorization or
revocation, and permit unbounded membership work. Desktop also submitted
every channel in one request, exceeding the relay's new 128-value safety
bound.
**Solution:** Preserve NIP-01 OR semantics across relay query, count,
and live-subscription paths while enforcing authorization and bounded
explicit-channel work before database or Redis operations. Desktop keeps
the older-relay-compatible one-channel-per-filter shape, sends filters
in bounded batches, combines responses, and deduplicates signed events
by event ID.

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

**crates/buzz-db/src/event.rs**
Distinguishes authorization channel scopes from explicit `#h` scopes in
list and count SQL so requested channels are applied before limits
without implicitly including global rows.

**crates/buzz-relay/src/handlers/req.rs**
Shares explicit-channel scope extraction and limits, preserves valid OR
siblings when malformed branches cannot match, repairs request-local
membership misses, and registers authorized live subscriptions per
channel.

**crates/buzz-relay/src/handlers/count.rs**
Applies the same bounded explicit-channel authorization to COUNT and
preserves channel scope when a multi-channel request narrows to one
authorized channel.

**crates/buzz-relay/src/api/bridge.rs**
Brings HTTP query and count behavior in line with WebSocket semantics
before SQL execution and rejects over-limit explicit-channel requests
before membership I/O.

**crates/buzz-relay/src/subscription.rs**
Indexes multi-channel subscriptions by every authorized channel and
shrinks, rather than destroys, their scope when one channel is revoked.

**crates/buzz-relay/src/handlers/side_effects.rs**
Releases only revoked channel topics and sends terminal closure only
when no authorized channel remains.

**crates/buzz-test-client/tests/e2e_relay.rs**
Adds ignored relay integration coverage for multi-channel delivery and
valid historical/live behavior with malformed or empty OR siblings.

**desktop/src-tauri/src/commands/workflows.rs**
Builds one single-channel filter per membership, submits at most 128 per
relay request, combines batches, and deduplicates by immutable signed
event ID.

**desktop/src-tauri/src/commands/workflows_tests.rs**
Covers filter compatibility, malformed input, 129-channel batching, and
cross-batch event-ID deduplication.

</details>

## Reproduction steps

1. Join multiple channels containing workflows, open **Workflows**, and
confirm workflows from every accessible channel appear.
2. Repeat with more than 128 memberships and confirm the listing remains
complete rather than failing the relay request.
3. Send a multi-value `#h` query/count and confirm only requested
authorized channels affect SQL limits and counts.
4. Subscribe to channels A and B, revoke A, and confirm B continues
delivering live events.
5. Subscribe with a valid channel branch plus a malformed or empty `#h`
sibling and confirm valid history, EOSE, and post-EOSE live delivery
still occur.

## Validation

At pushed head `c419a923f05e483ab26c006a0b3a80cfb3c73844`:

- Relay request tests: 53 passed.
- Desktop full Rust unit suite: 2,468 passed, 17 ignored.
- Relay E2E target compiled with `--no-run`.
- Strict relay clippy passed.
- Desktop Tauri clippy/check passed.
- Pre-push Rust tests and Desktop Tauri checks passed.
- Rust formatting and `git diff --check` passed.

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-17 15:16:14 -07:00
Salman MohammedandGitHub f64899e5d1 Remove Startup Recovery section in base prompt (#6161)
## Why

Managed channel sessions already receive authoritative per-turn context.
The old startup recovery checklist told every new session to scan the
global feed

## What

- Remove `Startup Recovery` with concise channel and heartbeat turn
contracts.

## Risk Assessment

Low. This changes prompt guidance and its test only; routing and runtime
behavior are unchanged.

Generated with Codex

---------

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
2026-08-17 18:11:27 -04:00
439c037491 fix(desktop): align preview sidebar row styling (#6163)
## Summary

- apply the inactive primary-navigation opacity treatment to every
sidebar destination, including Pulse, Projects, and Workflows
- remove the duplicated Inbox and Agents conditionals so future gated
rows inherit the same hierarchy
- add E2E coverage for all inactive rows and restoration to full opacity
when selected

## Validation

- `pnpm --dir desktop build:e2e`
- `pnpm --dir desktop exec playwright test badge.spec.ts --grep "primary
navigation rows share the same inactive emphasis" --project=smoke`
- pre-push hook: desktop check, typecheck, and 4,984 unit tests

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-17 14:49:31 -07:00
57feca2f20 fix(desktop): repair dropped team membership links at boot and on edit (#5904)
Two membership-propagation defects let an agent team silently lose
members — both observed live on Will's store (Sietch Tabr), not
hypothetical.

**Stale `persona_ids` dropped on save.** Team records written before
persona ids were namespaced hold bare slugs (`thufir`) instead of the
namespaced id (`sietch-tabr:thufir`). Nothing rewrites them, and the
interactive save path (`ensure_persona_ids_are_active`) *drops* any id
it cannot resolve — so the next in-app save shrinks the team. This nuked
four of five Sietch Tabr members.

**`team_id` drifts from team membership.** Team instructions are
injected at spawn by matching `record.team_id`
(`spawn_snapshot::effective_team_instructions`), so an instance's
binding must track its persona's membership. It drifts two ways: adding
a persona to a team leaves the persona's already-running instances at
`team_id: null` (a member in the roster but not in behavior — seen
twice, Gurney and Hayt), and removing a persona while keeping its agents
leaves the kept instance bound to a team that no longer lists it (still
drawing that team's instructions at spawn).

## Fix

A boot migration (`migration/team_membership.rs`) heals existing stores
in one pass over `teams.json` + `managed-agents.json`:

- **Rewrite stale ids.** A stale id is one no definition slug resolves.
Its target is the definition whose `source_team_persona_slug` equals the
bare slug, scoped to the team's source team (via `source_dir` for a
directory-backed team, or the unique `source_team` among resolvable
members for a detached one). Rewrite only when exactly one candidate
matches; zero or many leave the id in place — strictly safer than the
save path, which drops it.
- **Repair `team_id`.** Backfill an instance whose persona is a team
member but whose own binding is unset, and heal a stale binding whose
team no longer lists the persona (re-point when exactly one *other* team
claims it, otherwise unbind). Both directions gate on single-team
evidence — a persona spanning several teams has none (JSON team order is
not ownership), so it is left as-is and logged. A binding whose team
still lists the persona is authoritative and never touched.

Runs BEFORE `detach_directory_backed_teams` (so a not-yet-detached team
can still be scoped by its `source_dir`) and before any UI save can drop
an id. Rewrite-or-leave converges to a fixed point, so a second boot is
a no-op; the store is backed up once before either write.

The edit path (`commands/teams.rs`) propagates a membership change to
live instances immediately, without waiting for the next boot, scoped to
the delta between the pre-edit and post-edit rosters:

- **Added personas** (on the team now, not before) backfill `team_id` on
their unbound instances. An explicit add is legitimate binding evidence
even for a persona shared across teams — unlike the order-blind boot
case.
- **Removed personas** (on the team before, not now) clear `team_id` on
instances bound to *this* team (bindings to other teams are untouched),
so a "keep agents" removal stops feeding a kept instance the old team's
instructions.
- **Delta-scoping keeps a metadata-only edit inert:** with no roster
change, no instance is re-pointed — a shared unbound persona is never
silently bound to whichever team was edited last.

Propagation is best-effort after the authoritative `save_teams`
(mirroring `retain_team_pending`): the team already exists on disk, and
boot repair is the designed retry for a stale/unset binding, so a
secondary `managed-agents.json` write failure no longer fails a command
whose team write succeeded — which would otherwise let a UI retry mint a
duplicate team.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-17 14:44:43 -07:00
a282e0643f fix(cli): keep project replacement timestamps at or after wall clock (#5666)
Fixes #5665.

`next_timestamp` in `crates/buzz-cli/src/commands/projects.rs` computed
a replacement's `created_at` as `head.created_at + 1`. The relay's
ingest path rejects events more than ±900s from server time
(`MAX_TIMESTAMP_DRIFT_SECS` in
`crates/buzz-relay/src/handlers/ingest.rs`), so:

- `projects update` on any project whose head is older than 15 minutes
fails with `relay error 400: invalid: event timestamp too far from
server time` (live repro in #5665);
- inside the window, replacements are recorded at `head+1` —
seconds-to-minutes in the past — so a concurrent wall-clock writer
silently wins LWW and audit timestamps misstate when the write happened.

## Change

`next_timestamp` now returns `max(now, head.created_at + 1)`: strictly
after the observed head (preserving the dominate-the-head guarantee for
skewed/future heads), never behind the wall clock. This mirrors the
relay's own replacement-authoring pattern (`now.max(head+1)` in
`side_effects.rs`).

## Testing

- `cargo test -p buzz-cli --lib` — 344 passed; adds
`next_timestamp_uses_wall_clock_when_head_is_stale`, and the existing
far-future-head test still holds (`head+1` wins when head > now)
- `cargo clippy -p buzz-cli --all-targets` / `cargo fmt --check` — clean
- Live before/after on a self-hosted relay: vanilla CLI fails on a
2h-aged head; with this change the same update is accepted and the head
lands at wall clock.

Same failure family as #2876 (`repos protect` vs the drift window) —
that path is not touched here.

---------

Signed-off-by: Ika Minami <ika@infiniteidol.com>
Signed-off-by: Ravneet Arora <rarora@squareup.com>
Co-authored-by: Ika Minami <ika@infiniteidol.com>
Co-authored-by: Ravneet Arora <rarora@squareup.com>
2026-08-17 12:49:52 -07:00
Jordan MecomandGitHub 85bacea52b Remove GitHub security advisory commitment (#6144)
Removes the promise in `SECURITY.md` to publish a GitHub Security
Advisory after every security fix is released.

The disclosure policy continues to state that Buzz follows coordinated
disclosure and credits reporters unless they request anonymity.

Checked with `git diff --check`.

Signed-off-by: Jordan Mecom <jm@squareup.com>
2026-08-17 11:48:38 -07:00
076081bfc6 Rename Bumble agent to Pollen (#5864)
## Summary
- Rename the built-in Bumble agent to Pollen across desktop, onboarding,
docs, and test fixtures.
- Migrate existing stock definitions and instances in place while
preserving customized fields and the stable persona coordinate.
- Reserve the Pollen name by removing it from Fizz's generated-name
pool.

## Validation
- Pre-push desktop checks, typecheck, 4,791 frontend tests, Tauri
clippy, and 2,432 native tests
- Desktop E2E build

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-17 10:49:03 -07:00
d12d825778 fix(desktop): resolve agent profiles through one archive-aware selector (#5706)
Agent profiles resolve through one shared selector (`pickProfileAgent`)
at every entry point — the persona card, the profile panel, and library
grouping. That selector ranked instances only by active/name, with no
archive awareness, so a relay-archived instance early in file order
could hijack the persona card and the profile panel. The persona card
also recorded a durable pubkey target, which could strand the panel on
an archived identity when the click landed during the archive-snapshot
fail-open window. The profile panel's Runtime → Instances roster had the
same blind spot: it rendered every persona instance raw, so archived
instances appeared mixed in with live ones as if active.

This makes the shared resolution path archive-aware via the existing
fail-open `useIsArchivedPredicate`:

- `pickProfileAgent` filters archived instances before ranking and
returns `undefined` when every instance is archived (persona-only mode).
- `buildUnifiedGroups` drops archived agents from the standalone `Custom
agents` and `Unknown agents` buckets; matched persona groups keep their
full list and rely on the selector's persona-only fallback.
- `useCanonicalManagedAgentProfile` resolves through a pure
`resolveCanonicalManagedAgent` helper that applies the target-provenance
rules: a deliberately requested archived pubkey stays exact (so its
archive controller can unarchive it, even when a live sibling exists),
`preserveRequestedInstance` still pins a Runtime → Instances selection,
and non-archived historical navigation keeps its canonicalization.
- The persona card's main click records a persona target that
re-resolves every render, so it self-corrects to a live sibling after
hydration. Deliberate instance navigation and the runtime-error
affordance keep their explicit-pubkey path.
- The Runtime → Instances roster (`ProfileInstancesSection`) buckets
instances off the same predicate via `bucketPersonaInstances`: live rows
render as before, and archived rows move under a labeled `Archived`
subsection. The instance count reflects both buckets, and archived rows
keep their explicit-pubkey click so unarchive stays UI-reachable (the
deliberate-navigation path above).

The predicate is fail-open (treats every identity as live while the
relay archive snapshot loads) and self-exempt, so a cold start never
hides an identity and a user is never folded from their own client.
While the snapshot is loading, every instance renders in the live list —
nothing hidden, nothing labeled.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-17 13:46:25 -04:00
54f11219ef fix(acp): gate relay-signed workflow messages on their attributed author (#6129)
## Problem

Scheduled workflow `send_message` actions fire and land in the channel
with correct `p` tags for the mentioned agents — but the agents never
wake. The wake-up is silently dropped.

**Root cause:** workflow messages are signed by the **relay keypair**
(`workflow_sink.rs` signs with `state.relay_keypair`), so `event.pubkey`
is the relay's pubkey, not the workflow owner. In `buzz-acp`, the
inbound author gate (`author_allowed`) runs **before** the `p`-tag
mention check. Under the default `respond_to = owner-only`, the relay
pubkey is neither the owner nor a sibling, so every workflow wake-up
dies at the gate with a debug-level `"inbound author gate — dropping
event"`.

The relay-side comment even says the mention `p` tags exist *"so
mentioned agents are woken (wake is p-tag gated)"* — but wake is also
author-gated, and that path was missed.

## Fix

Gate relay-signed workflow messages on their **attributed author** — the
pubkey that created the workflow — instead of the relay pubkey:

- **Relay:** `workflow_sink.rs` now emits an explicit
`buzz:workflow-owner` tag carrying `workflow.owner_pubkey` (the workflow
creator, which the executor already passes as `author_pubkey` and whose
channel access the relay verifies before emitting). Ownership is never
inferred from `p`-tag order; mention `p` tags play no role in
attribution.
- **Harness:** at startup, `buzz-acp` fetches the relay's NIP-11 `self`
pubkey (new `RestClient::fetch_relay_self`, public `/info` endpoint).
Best-effort: fetch failure just logs a warning and preserves pre-fix
behavior.
- **Gate:** an event that is (a) authored by the relay `self` key, (b)
tagged `buzz:workflow`, and (c) carries a well-formed
`buzz:workflow-owner` pubkey is gated on that owner, through the exact
same owner/sibling/allowlist policy as a direct author.

## Security notes (all fail closed)

- No NIP-11 `self` pubkey → no exemption.
- `buzz:workflow` / `buzz:workflow-owner` tags on a non-relay-signed
event → ignored (a member cannot forge the exemption; the relay verifies
signatures on submission and only the relay holds its key).
- Relay-signed event without the tags, or with a malformed owner value
(not 64-hex) → plain author gate.
- Who is @mentioned in the message has no bearing on whose authority is
evaluated.
- A workflow owned by a random channel member still cannot wake an
owner-only agent — the owner's pubkey must pass the same policy.

## Testing

- 7 unit tests (`workflow_attributed_author_tests`) covering
attribution, fail-closed paths, p-tag independence, malformed owner
values, and the forgery case.
- Extended the PG-gated `workflow_send_message_p_tags_mentioned_member`
integration test to assert the `buzz:workflow-owner` tag.
- `cargo test -p buzz-acp`: 785 passed, 0 failed. `cargo test -p
buzz-relay --lib workflow_sink`: 17 passed. Clippy + fmt clean. (9
pre-existing `buzz-relay` failures in unrelated
`api::media`/`api::admin` tests fail identically on the base commit
without this change.)

Found while debugging scheduled automations in a Buzz review-pipeline
channel: two cron workflows fired daily @mentions at agents that never
responded, while direct human @mentions woke them instantly.

---------

Signed-off-by: Luke Tornquist <tornquist@squareup.com>
Co-authored-by: Fizz <3a9f8a30fbb462abec1e2977b2280a7ae50c7ff794433790be15bd48bfd52d0b@buzz.block.builderlab.xyz>
2026-08-17 13:29:20 -04:00
5b3f0375a2 fix(acp): replace Goose native system prompt (#5964)
## Why

Buzz currently appends its managed prompt to Goose's native prompt, so
managed agents receive both instruction sets instead of the intended
Buzz-only system prompt.

## What

- Send Goose's custom session system-prompt request with `mode: "set"`
- Lock the replacement contract in the ACP request test

## Risk Assessment

Low — the change is limited to Goose session setup; adapters that do not
implement Goose's custom method keep the existing method-not-found
fallback behavior.

## References

Goose v1.46.0 routes `set` to `override_system_prompt`, and its prompt
builder selects that override instead of rendering the native
`system.md`: [ACP
handler](https://github.com/aaif-goose/goose/blob/98c11ce2ee7b9b302978aa64b1eab7d0895607c7/crates/goose/src/acp/server/manage_sessions.rs#L57-L93),
[prompt
builder](https://github.com/aaif-goose/goose/blob/98c11ce2ee7b9b302978aa64b1eab7d0895607c7/crates/goose/src/agents/prompt_manager.rs#L153-L191).

Validated end to end against the official Goose v1.46.0 binary with a
local OpenAI-compatible capture server: the provider request contained
the exact Buzz replacement prompt and did not contain Goose's native
base-prompt marker.

---

**Update Aug 15, 13:17 CDT:** Added the [Terra-high prompt-ablation
comparison](https://github.com/squareup/buzz-benchmarks/blob/4492f76349ccb638219f7d070735a4d2b679bc26/data/prompt-ablation/20260815-terra-high/comparison.md).

The Goose conditions used GPT 5.6 Terra at high effort on the same 11
Terminal-Bench 2.1 tasks, with two attempts per task and concurrency
four. The matched `append-full` and `set-full` runs used the same
persona and included the same Buzz platform prompt; Active-h is the
primary measure because it excludes Buzz lifecycle overhead.

| Goose condition | Pass | Active-h | Median active | Agent-h | Wall-h |
Tool calls |
|---|---:|---:|---:|---:|---:|---:|
| Native prompt + Buzz prompt (`append-full`) | 21/22 | 0.3042 | 0.85
min | 0.3974 | 0.1496 | 234 |
| Native prompt + persona only (`append-persona-only`) | 22/22 | 0.3050
| 0.75 min | 0.3990 | 0.1498 | 204 |
| Buzz prompt replaces native prompt (`set-full`) | 22/22 | 0.3340 |
0.87 min | 0.4296 | 0.1551 | 275 |

Replacing instead of appending produced one additional passing attempt,
but it was not an efficiency improvement in this small sample: versus
`append-full`, `set-full` increased Active-h by 9.8%, median active by
2.0%, Agent-h by 8.1%, Wall-h by 3.7%, and tool calls by 17.5%. It was
faster on only two of eleven per-task active-time medians
(`distribution-search` and `prove-plus-comm`). With two attempts per
task, these are directional results rather than confidence intervals;
they support this change as an instruction-isolation/correctness fix,
not a performance optimization, and argue against Goose's appended
native prompt being the main source of active-time cost.

Generated with Codex

Signed-off-by: Atish Patel <atishpatel2012@gmail.com>
Co-authored-by: Codex <noreply@openai.com>
2026-08-17 13:03:14 -04:00
Taylor HoandGitHub edc4a09aaa feat(workflows): add responsive library card actions (#6008)
**Category:** improvement
**User Impact:** Users can scan what each workflow does and trigger,
edit, duplicate, enable, disable, or delete it directly from the
library.
**Problem:** The workflow list buried common actions and did not expose
each automation's trigger-to-action shape at a glance.
**Solution:** Add a responsive workflow library with a persistent create
tile, compact trigger/action diagrams, prominent workflow titles with
supporting descriptions, and shared card actions while preserving
existing detail, editor, and run-history entry points. Card toggles
refresh both list and open-detail caches so status and definition stay
consistent.

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

**desktop/src/features/workflows/ui/WorkflowActionsMenu.tsx**
Adds a shared card menu for trigger, edit, duplicate, enable/disable,
and delete actions.

**desktop/src/features/workflows/ui/WorkflowCard.tsx**
Reworks cards around the prototype's visual hierarchy: color-coded
trigger, action flow, sentence-case eyebrow, prominent title, supporting
description, status, channel, and update date without a footer clock
icon.

**desktop/src/features/workflows/ui/WorkflowsView.tsx**
Adds the responsive grid, create tile, mutation wiring, and list/detail
cache invalidation. Container breakpoints keep cards two-across at
medium widths and three-across in the 1280px desktop layout.

**desktop/src/features/workflows/ui/workflowDefinition.ts**
Adds immutable enabled-state updates plus narrow trigger and
first-action readers used only to select card icons.

**desktop/src/features/workflows/ui/workflowDefinition.test.mjs**
Covers neutral icon selection, enabled-state immutability, and status
presentation.

**desktop/tests/e2e/workflows.spec.ts**
Covers the create tile, title/description hierarchy, selected-card
enable/disable consistency, and deterministic narrow/medium/wide
captures while retaining existing action coverage.

</details>

## Reproduction steps

1. Open **Workflows** and confirm the create tile stays first as cards
flow from one to three columns with available width.
2. Confirm each card shows a sentence-case trigger eyebrow, prominent
workflow title, supporting description when present, status, channel,
and update date without a clock icon.
3. Open a card's overflow menu and trigger, edit, duplicate,
enable/disable, or delete the workflow.
4. Leave the detail panel open while toggling and confirm its badge and
JSON definition update with the card.

## Screenshots

Real built E2E UI with representative workflow data at three viewport
sizes.

### Narrow — 800 × 720

![Workflow library at 800 by
720](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6008/workflow-library-narrow-482d1b4c8.png)

### Medium — 1024 × 720

![Workflow library at 1024 by
720](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6008/workflow-library-medium-482d1b4c8.png)

### Wide — 1280 × 720

![Workflow library at 1280 by
720](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6008/workflow-library-wide-482d1b4c8.png)

### Card actions

![Workflow library actions at 1280 by
720](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6008/workflow-library-wide-actions-482d1b4c8.png)

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-17 16:56:34 +00:00
f716eef437 fix(desktop): enforce shared agent access across devices (#6086)
## Summary

- discover shared managed agents from authenticated relay directory
records instead of treating channel membership as sufficient proof
- publish and refresh access-policy changes immediately so running
clients converge across machines without a restart or five-minute poll
- route profile edits through the exact managed instance and
stop/restart runtimes around access changes so unrelated edits cannot
silently widen access
- keep mention send-time revalidation and Block owner-only build
enforcement fail closed
- explain invalid custom provider/model configuration instead of leaving
Save silently disabled

### Related issue

Fixes #3204

### Known residuals

- a brand-new remote agent's first policy record can wait for the
bounded directory poll when no authenticated directory coordinate exists
yet; send-time mention revalidation remains fail closed
- a failed remote-provider policy redeploy is recorded but cannot
undeploy the older provider instance until the provider protocol gains
the destructor tracked by #5570

### Testing

- full Desktop unit suite: 4,961 tests passed
- focused profile editor Playwright workflow passed, including Customize
access edits and prompt-only edits after tightening an instance
- Desktop TypeScript, Biome formatting, file-size ratchet, Tauri checks,
and pre-push suites passed
- independently reviewed for authenticated directory trust, live
subscription teardown, runtime revocation ordering, fail-open edit
paths, and per-agent provider deployment serialization

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
Co-authored-by: diegorumo <diegorumo@gmail.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
2026-08-17 09:51:00 -07:00
1b7e5ac1be feat(model-capabilities): drive model capabilities and labels from one manifest (#5597)
## Summary

Centralizes model capability knowledge — thinking mode, supported effort
levels, wire routes, and human-readable labels — into a single manifest,
`scripts/model-capabilities.json`. Rust and TypeScript each get a small
interpreter that reads the same manifest, replacing hand-maintained
tables scattered across both languages that had already drifted apart. A
capability change is now a data edit, not parallel edits to two code
paths. Supersedes the codegen approach explored in #3603.

A cross-language contract keeps the two interpreters honest:
`scripts/normative-corpus.json` is a golden snapshot generated from the
Rust resolver (103 vectors covering all six capability axes) and
replayed natively in TS. CI fails if either language disagrees with the
corpus or the corpus drifts from the resolver. Regenerate with `just
regen-model-corpus`.

## Behavior changes

- **Effort dropdown for `openai-compat` providers** no longer offers
`max`. The request path always clamped `max` to `xhigh` on the wire, so
the UI stops offering a value that was silently rewritten. UI-only,
wire-identical.
- **Databricks v2 routing (wire-visible):** uncurated endpoint names
carrying a bare Claude code-name segment (e.g. `goose-opus-5`) now route
to the MLflow chat wire instead of Anthropic Messages — they lose
Anthropic prompt caching but still succeed on a valid OpenAI-compatible
wire. Curated `databricks-claude-*` records and any name starting with
`claude` are unchanged. A handful of other uncurated/adversarial name
shapes similarly fall back to MLflow chat instead of pattern-matched
routes; every curated model resolves identically to before, all axes.
- **Curated model labels on the real discovery path.** The Databricks
API returns no display name, so discovery emits the raw endpoint id as
the model `name` (`{id, name: id}`) on every path. `ModelEntry.name` is
now curated at all four construction seams in `buzz-agent` — v2
discovery, v1 parse, the auth-empty default catalog, and the
configured-model fallback — via a read-only `databricks_registry_label`
lookup over the manifest's `databricks_v2` exact records; `id` stays the
raw wire/config value. A known id renders its curated label
(`databricks-gpt-5-5` → `GPT-5.5`), an unknown id passes through
unchanged, and the default-catalog row reads `GPT-5.5 (default
catalog)`. As a defense against older `buzz-agent` binaries and any
harness that echoes ids, `resolveModelLabel` treats a discovered name
equal to the trimmed id as absent and falls through to the registry
tier; a genuinely distinct name (including the suffixed default-catalog
label) still wins.

## Cleanup

Deletes the duplicated capability tables and their tests: the
`config.rs` gpt5 matchers, effort tables, and clamp logic; the legacy
segment-based Databricks v2 route classifier in `llm.rs`; and the TS
hand tables plus `effortTable.fixture.json`. All are replaced by
manifest lookups through the shared resolver — no line of capability
data exists in two places.

---------

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-17 11:23:28 -04:00
f956e6fe06 docs: refresh agent development guidance (#6049)
## Summary

- allow agents to build and run Flutter when it provides relevant
implementation or validation evidence
- keep mobile iteration fast by reusing simulators, incremental builds,
and configured staging or production communities
- correct stale CLI, E2E, CI, worktree formatting, and mobile launch
guidance
- point community singleton reset guidance at the canonical
implementation instead of duplicating a drifting inventory

## Validation

- `git diff --check origin/main..HEAD`
- `cargo run -q -p buzz-cli -- --format compact messages thread --help`
- `cargo run -q -p buzz-cli -- --format compact messages search --help`
- `just desktop-tauri-fmt-check` from the worktree
- pre-commit: mobile Dart formatting and `flutter analyze`
- pre-push: branch-skew check and full mobile test suite (1,465 tests)

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-16 08:25:51 -07:00
Taylor HoandGitHub d8281b9c93 feat(mobile): require device authentication for identity export (#5116)
**Category:** new-feature
**User Impact:** Mobile users must confirm with Face ID, biometrics, or
their device passcode before sending their Buzz identity to Desktop.

**Problem:** A signed-in phone could send its full identity, including
the `nsec`, to a desktop without fresh local verification.

**Solution:** Require OS device authentication before opening the
identity-recovery scanner, retain that authorization only for the active
pairing session and short pairing window, and require fresh
authentication again if it expires before the identity payload is sent.
Normal app opening, identity import, and community removal remain
unchanged.

## Screencasts

| Enable Face ID | Use Face ID |
| --- | --- |
| ![Enabling Face ID during identity
import](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5116/enable-face-id.gif)
| ![Using Face ID for identity
export](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5116/use-face-id.gif)
|

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

**Android and iOS integration**
- `mobile/android/app/build.gradle.kts` declares the AppCompat
dependency required by the biometric activity theme.
-
`mobile/android/app/src/main/kotlin/xyz/block/buzz/mobile/MainActivity.kt`
uses the activity type required by the system authentication prompt.
- `mobile/android/app/src/main/res/values/styles.xml` and
`mobile/android/app/src/main/res/values-night/styles.xml` use the
compatible launch theme.
- `mobile/ios/Podfile.lock` records the native local-authentication
dependency.
- `mobile/ios/Runner/Info.plist` explains why Buzz requests Face ID
access.

**Identity policy and pairing flow**
- `mobile/lib/shared/security/sensitive_action_authorizer.dart` wraps OS
authentication and maps platform errors to stable app-level outcomes.
- `mobile/lib/shared/community/community.dart` and
`mobile/lib/shared/community/community_storage.dart` persist the
sensitive-action policy.
- `mobile/lib/features/invites/invite_join_provider.dart` assigns the
explicit policy for invite-created communities.
- `mobile/lib/features/pairing/pairing_provider.dart` gates export,
binds grants to the active community/session, reauthenticates expired
grants, and clears grants on every terminal path.
- `mobile/lib/features/pairing/pairing_page.dart` lets users choose
biometric protection while importing an identity.
- `mobile/lib/features/settings/settings_page.dart` wires pairing into
settings.
- `mobile/lib/features/settings/settings_page/connection_section.dart`
authenticates before opening export recovery and bounds the
foreground-resume wait.
- `mobile/pubspec.yaml` and `mobile/pubspec.lock` add and lock
`local_auth`.

**Coverage**
- `mobile/test/shared/security/sensitive_action_authorizer_test.dart`
covers native result mapping, unsupported devices, and single-flight
behavior.
- `mobile/test/shared/community/community_test.dart` and
`mobile/test/shared/community/community_storage_test.dart` cover policy
defaults and persistence.
- `mobile/test/features/invites/invite_join_provider_test.dart` covers
the invite policy.
- `mobile/test/features/pairing/pairing_page_test.dart` covers import
protection controls.
- `mobile/test/features/pairing/pairing_provider_test.dart` covers
export/import authorization, stale/reset/concurrent guards, malformed
payload cleanup, and no-export failure paths.
- `mobile/test/features/settings/connection_section_test.dart` covers
the tap gate, lifecycle resume, and timeout behavior.

</details>

## Reproduction steps

1. Pair an identity into the mobile app.
2. Open Settings and choose “Send identity to desktop.”
3. Verify Face ID, biometrics, or the device passcode is required before
the recovery scanner opens.
4. Cancel device authentication and verify the scanner does not open and
no identity transfer begins.
5. Authenticate, scan a Desktop recovery code, confirm the SAS, and
verify the identity transfer completes.

## Validation

At `be5620f5f10aa6cc16e86a4f01f102f3d9aeef9b`:
- `cd mobile && ../bin/flutter analyze` — no issues
- `cd mobile && ../bin/flutter test` — 1,368 tests passed
- `cd mobile/android && JAVA_HOME=$(/usr/libexec/java_home -v 21)
./gradlew app:assembleDebug` — debug APK assembled successfully

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-15 18:34:02 -07:00
78cbffeb64 fix(desktop): hide the offcanvas-collapsed sidebar so it stops painting over the community rail (#5947)
## Summary

Collapsing the sidebar left a phantom copy of it painted over the
community/relay rail — opaquely on flat themes (vesper et al., which
made the rail look *removed*), and as ghost fragments (muted search-box
fill, truncated channel-name tails) on the Buzz themes whose chrome is
intentionally transparent for the gradient.

**Cause:** #4281 made the app-sidebar layer `overflow-visible` (the
huddle drawer needs to escape it). That removed the ancestor clipping
the offcanvas collapse relied on: the sidebar slides to `left:
-sidebar-width` but kept painting, exactly over the `z-0` rail (`z-10`
sidebar layer).

**Fix:** the offcanvas-collapsed sidebar container is now `invisible` +
`pointer-events-none`, with `visibility` added to the transition list so
the 200 ms slide-out still animates and the flip happens only at the
transition's end. Theme-independent; no per-theme CSS touched; the
huddle drawer's `overflow-visible` is preserved.

## Before / after

Left 420px of the app with the sidebar collapsed. Before = unpatched
`origin/main` @ 69107dc3b; after = this branch. Same seeded state, same
build pipeline (`build:e2e` between checkouts).

| theme | before (ghost sidebar over the rail) | after (rail clean: A /
B / + visible) |
|---|---|---|
| vesper |
![before-vesper](https://raw.githubusercontent.com/block/buzz/3f98c576e062e51d976940725d84b4e0be7fd53c/pr-5947--before-vesper.png)
|
![after-vesper](https://raw.githubusercontent.com/block/buzz/3f98c576e062e51d976940725d84b4e0be7fd53c/pr-5947--after-vesper.png)
|
| buzz |
![before-buzz](https://raw.githubusercontent.com/block/buzz/3f98c576e062e51d976940725d84b4e0be7fd53c/pr-5947--before-buzz.png)
|
![after-buzz](https://raw.githubusercontent.com/block/buzz/3f98c576e062e51d976940725d84b4e0be7fd53c/pr-5947--after-buzz.png)
|
| buzz-dark |
![before-buzz-dark](https://raw.githubusercontent.com/block/buzz/3f98c576e062e51d976940725d84b4e0be7fd53c/pr-5947--before-buzz-dark.png)
|
![after-buzz-dark](https://raw.githubusercontent.com/block/buzz/3f98c576e062e51d976940725d84b4e0be7fd53c/pr-5947--after-buzz-dark.png)
|

Before shots: ghost `⌘K` search chip + blue active-item pill painted
over the rail column; on vesper the opaque panel hides the rail buttons
entirely. After: the rail's community buttons (A, B) and `+` are visible
and clickable in all three themes.

Reported by Thomas P in #buzz-bugs:
buzz://message?channel=e62570dd-33ad-42c5-b92b-75f2689f9694&id=9ea401ca1d009f555ca4324e136f8d8d8156db2f8afa3ff89fd038d2c16260f7

cc @klopez4212 — this touches the layout your #4281/#5478 work shaped;
please confirm it doesn't defeat the huddle drawer or glass intentions.
The change deliberately hides only the *offcanvas-collapsed* container,
nothing in the expanded path.

## Test plan

- [x] New Playwright regression spec `sidebar-offcanvas-rail.spec.ts`
(buzz / buzz-dark / vesper): collapsed sidebar must be `visibility:
hidden` + `pointer-events: none`, community rail stays visible and
interactive. **Fails on unpatched build** (verified), passes with the
fix.
- [x] Full desktop unit suite: 4,954 pass / 0 fail
- [x] `pnpm typecheck`, `pnpm check` (biome + file-size ratchet +
px-text + pubkey-truncation) green
- [x] Before/after screenshots above captured via the e2e harness on
both builds

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Co-authored-by: Wintermute <165f0c871dd2586bb18b6aa109eeaf57bb2132ff4d27b10120f4368a0f627022@buzz.block.builderlab.xyz>
2026-08-15 07:49:41 -07:00
69107dc3bf Polish mobile message threads and composer (#5645)
## Summary

- refine mobile message metadata, search spacing, and Activity filter
semantics
- add channel-parity Latest navigation and stable tail following to
threads
- synchronize Android composer/keyboard geometry and keep Latest spacing
stable across IME transitions

## Validation

- `bin/just mobile-check`
- `bin/just mobile-test` (1,276 tests)
- Pixel 10 install/launch and channel/thread keyboard, Latest, tail, and
back-navigation review
- signed iPhone install/launch workflow

## Snapshots

See the review snapshots below.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
2026-08-15 09:14:14 +01:00
82f7ed1532 chore(release): release Buzz Desktop version 0.5.14 (#5917)
## Buzz Desktop release v0.5.14

- **Frozen main:** `1b3dbcaaea882eeea90359c1db02e306d2f4f50a`
- **Reviewed candidate:** `391495e7d347d20b67e39e3c240d17ef63c5c2c0`
- **Previous desktop release:** `desktop-v0.5.13`
- **Proposed immutable tag:** `desktop-v0.5.14`

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-14 17:35:49 -07:00
1b3dbcaaea ci(release): remove desktop smoke gate (#5914)
## Summary

- remove the GitHub-hosted desktop smoke job from the desktop release
workflow
- remove the smoke result from manifest assembly dependencies and
promotion conditions
- retain the local smoke tooling for future repair and targeted
validation

The first release execution of this gate spent its full 10-minute
Playwright timeout traversing the 10,000-row fixture, then produced a
987 MB diagnostics upload. All signed platform builds succeeded, but the
smoke prevented manifest publication. This restores the previously
established release boundary while the harness is made suitable for CI
separately.

### Testing

- parsed `.github/workflows/release.yml` with Ruby Psych and asserted
the smoke job/dependencies are absent
- `scripts/test-release-ref-contract.sh`
- exact pushed commit passed the repository pre-push hook

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-14 17:04:37 -07:00
51beba6038 chore(release): release Buzz Desktop version 0.5.13 (#5912)
## Buzz Desktop release v0.5.13

- **Frozen main:** `09768100ec3420f0aa7cd278bd00fe0baab5de8d`
- **Reviewed candidate:** `a239e0f6793ac6e88ccf92cc231054090a9753cc`
- **Previous desktop release:** `desktop-v0.5.12`
- **Proposed immutable tag:** `desktop-v0.5.13`

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-14 16:10:09 -06:00
09768100ec fix(ci): read Playwright version without nested shell quoting (#5910)
## Summary

- replace the nested one-line shell quoting used to read the Playwright
package version
- write the resolved version to `GITHUB_OUTPUT` from a multiline shell
step

## Why

The `desktop-v0.5.12` release smoke job failed before executing tests
because Bash received escaped quotes inside command substitution and
parsed the Node expression as shell syntax.

## Validation

- `bash scripts/test-release-ref-contract.sh`
- isolated execution of the new shell fragment with a fixture
`@playwright/test/package.json`, producing `version=1.58.2`
- `git diff --check`

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-14 14:28:06 -07:00
263c9bf76c fix(desktop): restore the agent trading-card mint button (#5900)
## Problem

PR #5574's profile-panel redesign dropped `ProfileSummaryView`'s
`onCreateCard` prop — the only caller of `setCardMintTarget` — so the
entire Agent Trading Cards feature (#3278) became unreachable from the
GUI while staying fully wired underneath: mint dialog, background job
store, viewer, gallery, composer chip, and the Rust
`mint_agent_card`/`save_agent_card` commands all survive at main. `git
log -S 'setCardMintTarget('` shows exactly two commits: the feature and
the accidental removal.

## Outcome

The mint trigger returns as a management row in the agent profile's Info
tab, directly under **Export agent**, gated `isBot && canManagePersona`
exactly like Duplicate/Export. Target resolution is byte-for-byte the
original logic: prefer the live instance pubkey, fall back to the
persona/definition id, allow locking only when an instance keypair
exists.

## Shape

- `UserProfileAgentManagementRows`: new optional `onCreateCard` row
(Sparkles icon, `user-profile-create-card-row`), placed after Export.
- Prop threaded `UserProfilePanel` → `ProfileSummaryView` →
`ProfileInfoTabContent` → management rows, mirroring `onExportAgent` at
every layer.
- The mint-target state + open callback move into a `useCardMint` hook
in `UserProfilePersonaDialogs` (beside the `CardMintTarget` type it
manages). This keeps `UserProfilePanel.tsx` at 999 lines — the file sits
at the size-ratchet cap and may not grow.

## Validation

- `pnpm check` green (biome, file-size ratchet, px-text,
pubkey-truncation).
- `pnpm typecheck` green.
- Full desktop unit suite: **4888 passed, 0 failed**.
- Profile e2e spec: **32 passed**, including the updated
management-row-order assertion and a new click → mint-dialog-visible →
Escape → closed exercise of the restored row.

Verified at `bff3110a0aeb3d63683eac9ed3e587829f9436da`, one commit atop
main `01f76ec97`.

Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
2026-08-14 17:22:42 -04:00
122a8b8988 Projects v3: unify sharing, discussions, and issue ownership (#5792)
## Summary

Projects v3 makes repository work shareable, discussion-aware, and
easier to scan in one coherent workspace. People can copy canonical
links, reopen the exact workspace tab, understand issue and pull-request
context at a glance, find related channel conversations, and assign or
unassign issues across Desktop and CLI.

- **Unified workspace** — top-level sections sit above repository
controls in one rounded workspace, with navigation positioned close to
the page heading. README and Files retain branch selection; every
section has a labeled icon header, and Issues and Pull Requests expose
creation from a consistent right-aligned action.
- **Repository management** — the repository selector is always
available, including single-repository projects. Its integrated add flow
lets project owners create a repository manually or select an existing
repository without a separate toolbar button.
- **Readable work-item lists** — issue and pull-request rows use
plain-language context instead of opaque metadata. Files, commits,
issues, pull requests, channels, and contributors share consistent row
density and right-aligned timestamps, while deterministic
fallback-avatar colors keep participants distinct on light backgrounds.
Inbox pull-request metadata wraps between complete phrases and truncates
long channel names instead of compressing copy into narrow columns.
- **Reliable entity links** — projects, repositories, issues, pull
requests, and commits have canonical `buzz://` links, preview cards, OS
deep-link routing, and tab-aware navigation. Reopening the same link
re-applies its destination instead of leaving the user on a locally
selected tab.
- **Related conversations** — repository and work-item views surface
channels discussing the current entity, including participants, channel
navigation, message context, and an explicit notice when discovery
reaches its 500-result cap.
- **Reversible issue ownership** — trusted assignment and unassignment
events work across Desktop, Tauri, `buzz-sdk`, and `buzz issues`.
Assignees appear in project views and the assigned inbox, while
authorized users can remove assignments directly from the assignee row.

Assignment state is derived chronologically from labeled Nostr notes.
Issue authors and repository owners may change any assignee; other users
may only assign or unassign themselves. Shared golden fixtures keep
entity-link grammar and validation aligned across TypeScript and Rust.

The branch also updates `webbrowser` to the patched release for
RUSTSEC-2026-0257.

### Related issue

N/A.

### Testing

- [x] `just ci` — formatting, lint, typechecking, unit tests, and builds
passed
- [x] Full pre-push suite — organization, branch-skew, Desktop checks,
typechecking, and tests passed on the latest push
- [x] `cargo test -p buzz-cli` and focused `buzz-sdk` assignment tests
passed
- [x] Focused Tauri recipient-note and 500-result search-limit tests
passed
- [x] Desktop entity-link and issue-assignment unit tests passed
- [x] Playwright smoke coverage passed for assignment, repeated
entity-link navigation, repository create/select flows, section headers
and actions, timestamp alignment, timeline icons, sentence-style
issue/PR metadata, header spacing, avatar contrast, and Inbox metadata
at stacked and side-rail breakpoints
- [ ] Manual staging pass: link round-trips, Channels tab, assignment
flows, and inbox routing

### Screenshots

Pull requests explain who opened the request, where it lives, and which
branch it comes from; fallback avatars remain visually distinct.

![Pull request list with conversational
metadata](https://raw.githubusercontent.com/block/buzz/2a536de86f7e6f79b349d7bc147b2923ff2b817d/pr-5624--05-pr-list-metadata.png)

Issues use the same sentence-style hierarchy while keeping status and
recency easy to scan.

![Issue list with conversational
metadata](https://raw.githubusercontent.com/block/buzz/2a536de86f7e6f79b349d7bc147b2923ff2b817d/pr-5624--06-issue-list-metadata.png)

The wide Inbox detail keeps author, timestamp, and origin context
readable beside its metadata rail.

![Pull request Inbox detail with readable
metadata](https://raw.githubusercontent.com/block/buzz/e65b433e14b97c45365ed7b68ea402ec01d26615/pr-5624--02-pull-request-detail-wide.png)

[View the complete six-state Projects v3 screenshot
set](https://github.com/block/buzz/pull/5624#issuecomment-5268039672)
and [the compact/wide Inbox
comparison](https://github.com/block/buzz/pull/5624#issuecomment-5268614585).


---

> Supersedes #5624, whose head commit accumulated permanently-queued
required check suites (block-dco-check et al.) that GitHub never
dispatched. History flattened into a single signed-off commit on latest
main; tree verified byte-identical (`git merge-tree`) to merging the
original branch into main.

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Co-authored-by: Wintermute <3f1797424fd9ad6653a83665c660517777cd7f8c228c0d5907f49e01537f3ca5@buzz.block.builderlab.xyz>
2026-08-14 16:48:41 -04:00
1f4c69eccf chore(release): release Buzz Desktop version 0.5.12 (#5903)
## Buzz Desktop release v0.5.12

- **Frozen main:** `757779bb1ef22cc4a1c233344baa0946d907e5a6`
- **Reviewed candidate:** `bfc34904adc414efcd8e9c5548dff82c3545b677`
- **Previous desktop release:** `desktop-v0.5.11`
- **Proposed immutable tag:** `desktop-v0.5.12`

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-14 13:20:24 -07:00
0bb7c60f82 fix(mobile): unwrap batched observer telemetry (#5805)
## Summary

Buzz Mobile now expands decrypted ACP observer batch envelopes into
their inner telemetry frames before sending them through the existing
per-agent dedupe, ordering, cap, and channel-filter pipeline. Singleton
observer events keep their existing behavior.

Malformed batch envelopes remain visible as outer frames, matching the
desktop consumer convention, while invalid inner frames use the existing
observer decrypt error path. This restores batched agent progress, tool
activity, and incremental transcript updates that Mobile previously
ignored.

### Related issue

Related to #4917.

### Testing

Added tests:

-
[`observer_subscription_test.dart`](https://github.com/block/buzz/blob/main/mobile/test/features/channels/agent_activity/observer_subscription_test.dart)
covers valid batches, singleton behavior, malformed envelopes, and
invalid inner frames.

Full mobile analysis, formatting, file-size validation, and Flutter
tests passed. The repository pre-push gate also passed.

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
2026-08-14 13:18:28 -07:00
757779bb1e perf(desktop): update active turns incrementally (#5897)
## Problem

Every observer-store publication made the active-turn bridge scan every
running/deployed agent and replay each agent's retained observer
journal. Watermarks kept the replay idempotent, but did not remove the
repeated work. Under an active fleet, one changed agent therefore caused
work proportional to the whole fleet and its retained history.

## Change

- observer publications now identify the changed agent and only the
newly admitted, retained events
- the active-turn bridge still performs one full hydration when its
agent list mounts or changes
- steady-state publications process only that changed active agent's
delta
- other observer-store subscribers keep their existing notification
behavior
- duplicate-only envelopes still do not publish

## Correctness

Regression coverage pins:

- retained/duplicate history is omitted from deltas
- stopped-agent updates do not enter active-turn state
- an incremental terminal clears a turn hydrated from retained history
- batching still publishes once and preserves transcript/terminal
outcomes
- existing watermark, tombstone, pruning, community restore, clear, and
eviction suites remain green

## Validation

Exact pushed head: `a480ffd2531023ea32b2a5518b5d9d41f04577c8`

- focused active-turn + observer-retention suites: 90 passed
- full desktop suite: 4,891 passed
- `pnpm --dir desktop typecheck`: passed
- `pnpm --dir desktop check`: passed (pre-existing repository warnings
only)
- mandatory pre-push hook at the exact pushed head: passed
`branch-skew`, desktop check/typecheck/test, mobile tests, Rust tests,
and Desktop Tauri checks

Packaged same-fleet CPU/RSS validation is follow-up evidence; this PR
proves the algorithmic amplification is removed without claiming an
installed-app percentage from unit tests.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-14 12:49:25 -07:00
f086eb6544 fix(link-previews): send while previews finish in background (#5697)
**Category:** fix
**User Impact:** Messages send immediately after submission while link
previews finish in the background, with an option to skip delayed
preview preparation.

**Problem:** Waiting for link-preview metadata or snapshot uploads kept
the composer occupied after users pressed Send, while races between
completion, timeout, and cancellation risked inconsistent payloads.
**Solution:** Freeze and promote speculative preview work into a bounded
background send task, clear the composer immediately, and
publish exactly once with prepared previews or gracefully without them
when skipped, failed, or timed out.



https://github.com/user-attachments/assets/987d2f2c-679f-473a-965f-dfb279951e52



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

**desktop/src/features/communities/useCommunityInit.ts**
Resets pending link-preview preparation when community context changes
so work cannot cross community boundaries.

**desktop/src/features/messages/lib/linkPreviewPreparationStore.ts**
Adds the coordinator-owned preparation state machine, bounded fallback,
Skip behavior, and exactly-once terminal publication handling.

**desktop/src/features/messages/ui/ComposerUploadProgressOverlay.tsx**
Extends floating background progress UI to include link-preview
preparation.

**desktop/src/features/messages/ui/ComposerUploadProgressPill.tsx**
Adds the preparing-link-preview label and Skip action to the progress
pill.

**desktop/src/features/messages/ui/MessageComposer.tsx**
Hands submitted preview work to the background coordinator and clears
the composer immediately.

**desktop/src/features/messages/ui/messageComposerAutoSubmit.test.mjs**
Updates auto-submit unit coverage for coordinator-owned preview
preparation.

**desktop/src/features/messages/ui/messageComposerAutoSubmit.ts**
Allows submit to promote unfinished preview work instead of blocking
composer submission.

**desktop/src/features/messages/ui/useComposerLinkPreviews.tsx**
Starts preview work speculatively and exposes frozen preparation jobs
for adoption by the send flow.

**desktop/src/features/messages/ui/useMentionSendFlow.helpers.ts**
Carries prepared preview tags through the mention and media payload
helpers.

**desktop/src/features/messages/ui/useMentionSendFlow.ts**
Integrates prepared preview tags into final message publication.

**desktop/src/shared/lib/useResolvedLinkPreviews.ts**
Exposes the in-flight metadata promise so promoted work can be adopted
rather than restarted.

**desktop/tests/e2e/messaging.spec.ts**
Covers immediate submit, upload handoff, Skip/completion races, failure
fallback, auto-send, and exactly-once publication.

</details>

## Reproduction steps

1. Enter a supported link and press Send while preview metadata or
snapshot upload is still pending.
2. Confirm the composer clears immediately and the floating progress UI
shows **Preparing link preview · Skip**.
3. Let preparation finish and confirm one message is published with its
preview.
4. Repeat and choose **Skip**; confirm one message is published without
waiting for the preview.
5. Simulate preview failure or timeout and confirm the message still
publishes once without preview tags.

## Validation

- TypeScript, Biome/format, file-size, px-text, and pubkey checks
- Full desktop unit suite: 4,734 passed
- Focused Playwright messaging suite: 5 passed
- Push hooks at `86c0aa7de2ff81b79286c99bf23db12345adc6ca`: desktop
check, typecheck, and tests passed

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-14 12:16:36 -07:00
01f76ec971 fix(desktop): cut steady-state relay traffic from polls and read-state echo (#5879)
## Problem

Desktop webview CPU stayed high after the presence-scope fix (#5830) and
the shared useNow ticker (#5861). A per-kind byte tap hot-patched into
`relayClientSession.ts` on a live desktop (~500 channels, large agent
fleet; 850 s capture correlated with CPU sampling) showed the remaining
steady-state relay traffic is mostly self-inflicted:

| kind | what | share of inbound bytes | shape |
|------|------|-----------------------|-------|
| 30078 | read-state | **34%** | our own ~44 KB nip44 blob echoed back
every ~10-30 s while reading |
| 30030 | emoji union | **33%** | 2-min poll refetching every member's
full set (~300 KB burst) |
| 30175 | persona catalog | **13%** | same 2-min backstop pattern, ~150
KB per walk |

CPU tracked the bursts directly: 3-5% in quiet 10 s buckets vs 44-54% in
buckets containing a poll burst or read-state echo. (The kind-24200
observer-frame theory was tested and disproven by the same tap: 9.7% of
bytes, steady trickle.)

## Outcome

- **Read-state echo drop.** `ReadStateManager` remembers the ids of
events it just published (FIFO set capped at 64) and drops their relay
echoes before the nip44-decrypt + `JSON.parse` step. Ids are recorded
*before* publishing so relay fan-out can't race the OK. The drop
consumes the id, so a reconnect replay of the same event still parses
normally. Events from other clients of the same pubkey are untouched.
- **Poll backstops stretched 2 min → 20 min** for the emoji union and
persona catalog queries. The live subscriptions (invalidate on any new
30030/30175) and the reconnect invalidations remain the freshness paths;
the poll only exists to cover a silently dropped live event. Behavior on
publish, focus, and reconnect is unchanged.
- Mechanical: localStorage identity helpers moved to
`readStateIdentity.ts` (no behavior change) to keep
`readStateManager.ts` under the file-size ratchet.

Expected effect on the measured profile: the poll stretch cuts the
30030/30175 bursts (46% of inbound bytes) by 10x; the echo drop removes
the recurring ~44 KB nip44-decrypt + parse per publish cycle (the echo
still arrives on the wire — nostr filters cannot exclude own-author
events — so this is a CPU/IPC saving, not a bandwidth one).

## Acceptance

- New tests: echo dropped **before** decrypt (mutation-checked:
disabling the drop fails the test), replayed duplicate of the same id
still parses, foreign-client events always parse, published-id set stays
capped when publishes fail (never-echoed ids).
- Full desktop suite **4794/4794**, `tsc --noEmit` clean, `pnpm check`
(biome + ratchets) clean at head.

## Not addressed (follow-ups)

- The 44 KB blob itself (one read-state event carries all ~500 channels;
a delta or per-channel-shard format is a protocol change).
- Duplicate delivery of the same events on concurrent `history-`
subscriptions (relay/client dedupe).
- Webview RSS of 12.5 GB observed on the same machine — retention hunt
is separate work; shrinking the heap multiplies the value of this PR
since the GC floor scales with live-heap size.

Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
2026-08-14 14:38:33 -04:00
207154706c fix(desktop): support channel message path links (#5889)
## Summary

- accept `buzz://channel/<uuid>/<64-hex-event-id>` as a compatibility
message deep link
- activate the desktop window and route path-form message links through
the existing durable message-navigation queue
- support the same path form when rendered or pasted inside Buzz, while
canonicalizing composer output to `buzz://message?...`
- retain the existing one-segment channel-link behavior and reject
malformed event IDs or extra segments

## Context

Buzz Desktop 0.5.11 has no native `channel` route. The recently merged
channel-link handling on main recognizes `buzz://channel/<uuid>`, but
rejects the externally shared `<channel>/<event-id>` form before window
activation. On macOS that presents as Buzz taking the menu bar while its
window neither foregrounds nor navigates.

## Test plan

- `cargo test --manifest-path desktop/src-tauri/Cargo.toml
parse_channel_deep_link`
- focused channel-link, composer-link, and markdown unit tests
- `pnpm typecheck`
- mandatory pre-push hook: desktop checks, full desktop unit tests, and
Tauri/Rust checks

Installed-app external-open behavior requires a build containing this
change; 0.5.11 cannot exercise it because that release predates native
channel-link handling.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-14 18:25:23 +00:00
5ddf23d700 feat(mobile-messages): render compact Buzz permalink chips (#5639)
**Category:** improvement
**User Impact:** Buzz channel, message, repository, pull request, and
issue links now display recognizable context and navigate reliably in
the mobile app.
**Problem:** Bare Buzz permalinks appeared as raw or ambiguous URLs on
mobile, while channel and message links were not handled consistently
across Markdown forms and startup states.
**Solution:** Normalize eligible bare Buzz URLs without consuming
Markdown syntax, render them as semantic icon-prefixed chips, and route
channel/message targets through the mobile deep-link dispatcher while
preserving authored Markdown labels as ordinary links.

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

**mobile/lib/features/channels/deep_link_dispatcher.dart**
Routes parsed channel and message links through the appropriate in-app
navigation callbacks.

**mobile/lib/features/channels/message_content.dart**
Presents all bare Buzz permalinks as semantic icon chips and keeps
authored labels as ordinary links.

**mobile/lib/features/channels/message_content/link_normalizer.dart**
Normalizes bare and autolinked Buzz URLs without consuming Markdown
delimiters, code, or punctuation.

**mobile/lib/shared/deeplink/deep_link.dart**
Adds strict channel and project-entity parsing alongside message deep
links.

**mobile/lib/shared/deeplink/pending_deep_link_provider.dart**
Preserves pending navigation until the mobile routing surface is ready.

**mobile/test/features/channels/channel_detail_page_test.dart**
Updates navigation integration coverage for icon-prefixed channel chips.

**mobile/test/features/channels/deep_link_dispatcher_test.dart**
Covers channel/message dispatch and missing-target behavior.


**mobile/test/features/channels/message_content/link_normalizer_test.dart**
Exercises Markdown-safe normalization across the full Buzz link suite.

**mobile/test/features/channels/message_content_test.dart**
Verifies chip labels, icons, semantics, authored-label opt-out, and
navigation callbacks.

**mobile/test/shared/deeplink/deep_link_test.dart**
Covers strict parsing for channel, message, repository, pull-request,
and issue links.

</details>

## Reproduction steps
1. Run the mobile app and open a channel containing bare
`buzz://channel`, `buzz://message`, `buzz://repo`, `buzz://pr`, and
`buzz://issue` URLs.
2. Confirm each bare URL renders as one cohesive chip with a type icon,
a useful name or shortened identifier, and no duplicated channel `#`
character.
3. Add an authored Markdown link such as `[design
discussion](buzz://issue?...)` and confirm the supplied label remains an
ordinary link rather than becoming a chip.
4. Select channel and message links and confirm they navigate correctly
from inline and autolinked forms.

## Screenshots / demos
**iOS Simulator — channel, message, repository, pull request, and issue
permalink chips**

Real app build (`37b2cb5eb`) running on an iPhone 17 Pro simulator.

![Mobile permalink chips on iOS
Simulator](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5639/mobile-permalink-chips-simulator.png)

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-14 10:44:58 -07:00
dbee2914ad test(desktop): await channel E2E bridge readiness (#5886)
## Summary

- wait for the channel mutation and cache invalidation E2E hooks before
using them
- make those hooks required after readiness instead of silently skipping
fixture setup
- keep the production channel settings behavior and assertion unchanged

## Why

On slower CI startup, `page.goto()` can resolve before the E2E bridge
installs its globals. The test used optional calls, so all three fixture
operations could silently do nothing and leave the seeded `General
discussion for everyone` description in React Query. The assertion then
failed deterministically, including both retries.

## Validation

At commit `5b4d5d290b316db5eef78c3596a17c7a270c8163`:

- `pnpm -C desktop build:e2e`
- focused Playwright test repeated 30 times: 30 passed
- `pnpm -C desktop exec biome check tests/e2e/channels.spec.ts`
- mandatory pre-push hooks passed on the exact pushed head:
`branch-skew`, `desktop-check`, `desktop-typecheck`, `mobile-test`,
`desktop-test`, `rust-tests`, and `desktop-tauri-checks`
- `git diff --check origin/main...HEAD`

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-14 10:29:37 -07:00
fd0ab47a1b fix(link-preview): refetch a link when it re-enters the composer (#5510)
### Overview

**Category:** fix
**User Impact:** When a user re-pastes (or finishes typing) a link that
previously failed to load a preview, the composer now refetches it
immediately and can never send a snapshot preview built from the old,
stale metadata.
**Problem:** The link-preview cache is shared with passive message-list
scroll, so a URL that resolved to a negative result (a hard `null` miss
or a transient fetch failure) stayed cached and re-usable. Re-pasting
that exact link into the composer served the stale negative and never
refetched. Worse, the stale metadata was still `snapshotReady`, so a
fast clear-then-repaste could attach a **stale snapshot preview tag** to
the sent message — a preview that no longer matched the link.
**Solution:** A freshly-entering link is forced to refetch, and the
composer is fenced against ever shipping a tag built from pre-re-entry
metadata. This closes three distinct races surfaced over successive
review passes: (1) the shared negative cache being reused on re-entry;
(2) the resolver's debounce swallowing a fast clear+re-paste so the
re-entry was invisible and the stale tag stayed sendable; and (3) an
in-flight media upload started from the stale metadata publishing its
tag after fresh metadata had already arrived. Healthy cached hits are
never touched (instant card, no redundant fetch), and passive
message-list scroll — which never opts in — keeps riding the shared
cache exactly as before.

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

**desktop/src/shared/lib/useResolvedLinkPreviews.ts**
Adds a loader `invalidateNegative(href)` that drops a cached negative
result (resolved `null` or transient fail) while leaving healthy hits
and in-flight promises alone, and a `refetchNewNegatives` option that
invalidates each newly-present href's negative entry before the
peek/load loop reads the cache. Also adds an optional `liveHrefs` input
so newness is judged against the caller's LIVE (undebounced) content — a
debounce-swallowed leave/re-entry of the same URL still counts as new.
Because the hook retains its own resolved metadata (the render that
scheduled the effect already read the stale negative from it), it also
clears its OWN negative key for every re-entered href, so the link
renders as pending until the fresh load wins. `buzz://` entity links are
skipped (they resolve off the relay, not this cache).

**desktop/src/features/messages/ui/useComposerLinkPreviews.tsx**
Opts the composer into `refetchNewNegatives` and feeds it the live
hrefs. Detects a same-URL re-entry at render time (React batches the
empty→repaste renders, so an effect keyed on the live set never observes
the transition), then blocks the re-entered href until the resolver's
forced refetch visibly cycles through pending: its stale ready tag is
dropped from state and excluded from the sendable output until a fresh
result re-tags. Only the sendable negative case (`fallback`) is blocked;
a healthy (`image`) re-entry keeps its instant card. Adds a per-href
upload generation token (`uploadsRef` becomes `Map<href, generation>`):
a live re-entry bumps the generation, the upload effect's dedup guard
and completion are generation-aware, so an in-flight upload from stale
metadata cannot publish its tag after settling and a fresh upload can
start even while the superseded one is still in flight.

**desktop/src/shared/lib/useResolvedLinkPreviews.test.mjs**
Adds resolver-level regressions: `invalidateNegative` drops a cached
miss (next load refetches) but preserves a healthy hit (no redundant
fetch); transient failure → URL removed → re-entered renders
pending/not-`snapshotReady` until a successful retry; and the
retained-negative + shared in-flight-fetch + re-entry interleaving
clears the local negative regardless of the shared entry's shape.

**desktop/src/features/messages/ui/useComposerLinkPreviews.test.mjs**
Adds composer-hook regressions driving the REAL hook through the hostile
gestures: a fast clear+re-paste inside the debounce window drops the
stale tag and holds Send pending until a fresh tag carrying the
newly-fetched media lands; and a stale in-flight upload held across the
clear+re-paste and fresh-metadata resolution cannot publish its
pre-clear tag, while a fresh upload starts and its tag wins.

</details>

### Reproduction Steps

1. Paste a link whose preview fails to resolve (force a transient fetch
failure) so the composer shows a blank/collapsed card.
2. Clear the composer and re-paste the same link (quickly, within the
~350ms debounce window).
3. Observe the preview refetches immediately rather than reusing the
stale negative result, and Send stays disabled until a fresh tag lands.
4. Send the message and confirm the attached preview tag reflects the
fresh fetch, never the stale pre-clear metadata.
5. Confirm passive message-list scroll of already-resolved links still
shows cards instantly with no extra fetches.

### Notes

Scope grew across three review passes from the original single resolver
opt-in into a full defense against shipping stale snapshot tags on link
re-entry — see the scope-adjustment comment on this PR for the detail.
Stacked on #5245 (`tho/link-preview-snapshot-race`), whose rewrite of
`useComposerLinkPreviews.tsx` is the sole overlapping file. The
transient-retry work stays in #5502, which touches no composer file and
remains based on main.

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-14 10:18:54 -07:00
5acb930821 feat(desktop-messages): render compact Buzz permalink chips (#5638)
**Category:** improvement
**User Impact:** Buzz channel, message, repository, pull request, and
issue links now open reliably and display recognizable context in the
desktop app.
**Problem:** Buzz links could appear as raw or ambiguous URLs, and
navigation links received during startup or community transitions could
be dropped before the UI was ready. Repository and issue shares in
particular required hover context to understand at a glance.
**Solution:** Queue desktop channel/message navigation until the UI is
ready, then render bare Buzz permalinks as icon-prefixed chips with
concise entity context while preserving user-authored Markdown labels as
ordinary links.

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

**desktop/src-tauri/src/deep_link.rs**
Adds validated channel-link parsing and a deduplicated, acknowledged
queue so navigation survives frontend startup.

**desktop/src-tauri/src/lib.rs**
Registers the pending-navigation state and commands with the desktop
application.

**desktop/src/features/communities/useCommunityInit.ts**
Resets queued navigation safely across community boundaries without
leaking stale destinations.

**desktop/src/features/messages/lib/channelLink.test.mjs**
Covers valid, malformed, and canonical channel permalink forms.

**desktop/src/features/messages/lib/channelLink.ts**
Defines strict parsing and detection for `buzz://channel/<uuid>` links.

**desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs**
Extends composer-node coverage for normalized Buzz link content.

**desktop/src/features/messages/lib/composerMessageLinkNode.ts**
Keeps composer link-node handling aligned with the expanded Buzz link
surface.

**desktop/src/features/messages/lib/remarkChannelDeepLinks.test.mjs**
Verifies bare channel URLs become renderable deep-link nodes without
touching code.

**desktop/src/features/messages/lib/remarkChannelDeepLinks.ts**
Transforms eligible bare channel links into dedicated Markdown nodes.

**desktop/src/features/messages/lib/remarkEntityLinks.test.mjs**
Covers bare repository, pull-request, and issue detection and code-span
exclusions.

**desktop/src/features/messages/lib/remarkEntityLinks.ts**
Adds dedicated Markdown nodes for bare Buzz project entities.

**desktop/src/shared/deep-link.test.mjs**
Exercises queued navigation, acknowledgement, serialization, and
community-switch behavior.

**desktop/src/shared/deep-link.ts**
Serializes pending deep-link drains and acknowledges destinations only
after successful navigation.

**desktop/src/shared/styles/globals/markdown.css**
Aligns permalink icon geometry and spacing with agent mention chips.

**desktop/src/shared/ui/markdown.test.mjs**
Adds integration coverage for every permalink chip, authored labels,
fallbacks, icons, and static rendering.

**desktop/src/shared/ui/markdown.tsx**
Routes channel and entity nodes through the shared presentation path
while preserving authored link text.

**desktop/src/shared/ui/markdown/BuzzLinkChip.tsx**
Introduces the shared interactive/static permalink chip and
authored-label inline-link components.

**desktop/src/shared/ui/markdown/ChannelDeepLink.tsx**
Renders channel shares and references with Hash icons, names, and
shortened-ID fallbacks.

**desktop/src/shared/ui/markdown/MessageLinkPill.tsx**
Renders ordinary message shares with message icons and channel/message
context while retaining sent-from-thread behavior.

**desktop/src/shared/ui/markdown/entityLinks.tsx**
Maps repositories, pull requests, and issues to Projects-aligned icons
and contextual labels.

**desktop/src/shared/ui/markdown/nodeCache.ts**
Includes entity-link rendering in cached Markdown node handling.

**desktop/src/shared/ui/markdown/utils.ts**
Allows validated channel links through the Buzz URL transform.

**desktop/src/shared/useMessageDeepLinks.ts**
Drains queued navigation links safely and clears them during teardown.

**desktop/src/testing/e2eBridge.ts**
Extends the mock bridge with pending-navigation command behavior.

**desktop/tests/e2e/community-rail.spec.ts**
Verifies queued links do not cross community boundaries.

**desktop/tests/e2e/navigation.spec.ts**
Covers channel/message deep-link navigation during startup and active
sessions.

**desktop/tests/helpers/bridge.ts**
Adds reusable deep-link mock state and acknowledgement helpers.


</details>

## Reproduction steps
1. Run the desktop app and open a channel containing bare
`buzz://channel`, `buzz://message`, `buzz://repo`, `buzz://pr`, and
`buzz://issue` URLs.
2. Confirm each bare URL renders as one cohesive chip with a type icon,
a useful name or shortened identifier, and no duplicated channel `#`
character.
3. Add an authored Markdown link such as `[design
discussion](buzz://issue?...)` and confirm the supplied label remains an
ordinary link rather than becoming a chip.
4. Select channel and message links and confirm they navigate correctly
in warm and cold-start states.

## Screenshots / demos
Houston dark theme with custom purple accent (`#a855f7`), captured from
rebased visual implementation `ad411cc06`; current head `0aafa144f` only
adjusts E2E expectations for the visible mention-label behavior shown
here.

**Composer — channel, message, repository, pull request, and issue
pills**

![Composer with all Buzz permalink pill types in Houston dark theme and
purple
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5638/composer-all-permalink-pills-dark-purple.png)

**Message list — channel, message, repository, pull request, and issue
pills**

![Message list with all Buzz permalink pill types in Houston dark theme
and purple
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/5638/message-list-all-pill-types-dark-purple.png)

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-14 10:18:18 -07:00
17d2147eca Fix video comment effect wrapping (#5748)
## What changed

- render video-review timecode chips inside the first Markdown paragraph
so comment text wraps naturally around them
- reuse the canonical video-review chip treatment across the timeline,
Inbox previews, and Inbox detail
- preserve video-review context in Inbox so timestamp chips remain
interactive

## Why

Video comments now support Markdown-like effects, but non-player
surfaces rendered the timestamp beside a separate text layout. That kept
the chip and comment from sharing the same inline flow and made Inbox
behavior inconsistent with the player.

## Validation

- `pnpm --dir desktop check`
- 100 focused Markdown, timecode, video-review, and Inbox unit tests
- `pnpm --dir desktop build:e2e`
- focused `video-attachment.spec.ts` Playwright scenario
- pre-push desktop typecheck and 4,761-test desktop suite
- native Builderlab staging with the configured profile

Focused timeline and Inbox snapshots will be attached in a PR comment.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz>
Co-authored-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz>
2026-08-14 17:16:28 +01:00
1d51081b8a Teach agents to inherit Buzz product intent (#5875)
## Summary

- make `VISION.md`, relevant `VISION_*.md`, and applicable testing
guides explicit planning and review inputs for non-trivial Buzz changes
- teach managed agents to load repository-root and path-local
`AGENTS.md` files after selecting a checkout
- distinguish CI evidence from exercising the live workflow for
user-visible and integration behavior
- turn repeatable mistakes into same-session durable lessons, keeping
only load-bearing rules in core memory and promoting shared lessons to
team guidance
- pin the new managed-agent prompt invariants in tests
- preserve the exact display name shown in Buzz when mentioning or
addressing someone; never infer or look up a surname merely to sound
more complete

### Related issue

None found after searching `block/buzz` issues and PRs for agent
instruction, vision, and product-intent routing.

### Testing

At commit `07ef705b42f58d3be6981165c6959d541ada0ba7`:

- `cargo fmt --all -- --check`
- `cargo test -p buzz-acp agent_draft_prompt_tests` (4 passed)
- mandatory pre-push hooks passed on the exact pushed head:
`branch-skew`, `desktop-check`, `desktop-typecheck`, `mobile-test`,
`desktop-test`, `rust-tests`, and `desktop-tauri-checks`
- `git diff --check origin/main...HEAD`

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-14 09:03:37 -07:00
caa64b5e8f feat(desktop): one relative date ladder across chat and the Inbox (#3769)
Slice 6 of #2216. Independent of #3642 — cut from `main`, no shared
files in conflict.

## Why

Five surfaces formatted the same thing five ways, and none of them
matched the writing standard's Today / Yesterday / weekday / date
progression.

| Surface | Before |
|---|---|
| Chat day divider | `Monday, March 31st` — ordinal suffix, which the
standard says to avoid |
| Inbox section header | `Yesterday`, but never `Today`; always printed
the year |
| Inbox list row | A third implementation |
| Inbox thread pane header | `Jul 8, 2026, 2:34 PM` — always absolute,
always with the year, never relative at any distance |
| Channel message header | `9:05 AM` — a bare clock, so a message from
last week has nothing to anchor it once its day divider scrolls away |

There were three separate date implementations doing this, which is the
symptom worth naming: **two different jobs were being solved ad hoc at
each call site.** A header that labels a *group* of items needs a
different label than an individual item's own timestamp.

## What

`shared/lib/datetime.ts` owns both ladders:

```
formatDayGroupLabel          formatItemTimestamp
(day divider, section header) (list row, message header)

Today       → Today           withTime:false   withTime:true
Yesterday   → Yesterday       2:34 PM          2:34 PM
2–6 days    → Monday          Yesterday        Yesterday at 2:34 PM
this year   → June 20         Monday           Monday at 2:34 PM
older       → June 20, 2025   Jun 20           Jun 20 at 2:34 PM
                              Jun 20, 2025     Jun 20, 2025 at 2:34 PM
```

## Two deliberate deviations from the standard

Both are documented at the definition, not just here.

**The oldest band keeps the day.** The standard collapses anything over
ten months to month-and-year (`Aug 2022`). A group label has to
*identify* its day — collapsing would give every day in a month the same
divider, so scrolling old history would show a run of identical headers
with no way to tell one day from the next. Only the year is conditional.
There's a test asserting three consecutive 2022 dates produce three
distinct labels.

**Roomy surfaces keep the time of day at every band.** `Yesterday at
9:05 AM`, not `Yesterday`. This is a chat and collaboration workspace
rather than a transactional product — where you read conversation, the
time is content, not chrome. Narrow list rows still drop it (`withTime:
false`) and rely on the existing hover tooltip, which stays the absolute
value. `withTime` is a surface decision, not a preference.

Today needs no date word in either mode: a bare clock already reads as
today, and "Today at 2:34 PM" is longer without saying more.

## Derived rather than captured

`MessageTimestamp` now takes only `createdAt` and derives both of its
labels, instead of receiving a pre-formatted `time` string. A relative
label captured when the message list was formatted would be frozen at
that wording; deriving it means each render recomputes.

This does not make it live — `MessageRow` is memoized, so a row already
on screen when the clock passes midnight keeps saying "Today" until
something re-renders it. The day divider above it has always had the
same property, and both correct themselves on the next message, scroll,
or navigation. Called out in the component doc so the next person
doesn't read "derived" as "reactive".

The memo comparator moved from `message.time` to `message.createdAt`.
Behavior-identical — `time` was a pure function of `createdAt` — but it
now names the prop the row actually reads.

The 36px continuation hover gutter stays clock-only. A relative label
doesn't fit in `w-9`.

## Middot between metadata segments

`managed by you 9:53 AM` ran two unrelated facts together as if they
were one phrase. Now `managed by you · 9:53 AM`.

- `aria-hidden` — punctuation for the eye only. The header already reads
as separate nodes to a screen reader, and `MessageAgentOwner` supplies
its own "Agent managed by" label.
- Grouped with the segment it precedes, so it can't wrap to the start of
a line on its own — as loose siblings in a `flex-wrap` row, an orphaned
divider is exactly what happens.
- No margin; spacing comes from the container gap.
- **No separator after the author name.** "Alice 9:53 AM" already reads
as a name followed by a time. Dividers go between metadata segments
only.

Middot is already the app's separator for this —
`MessageThreadSummaryRow`, the mention list, project rows, 46 files in
total.

Applied to the channel message header, channel system rows, and the
Inbox thread pane. Left-side Inbox activity rows deliberately unchanged.

## Verified

Screenshots taken through `just desktop-screenshot`:

- `#agents` — `nadia 🤖 managed by you · 10:20 AM`, and the `Today`
divider with clock-only rows
- Inbox thread pane — `alice 🤖 owner unavailable · 12:00 PM`

**Gap worth naming:** every mock channel message is same-day, so the
past-day labels (`Yesterday at 9:05 AM`, `Jun 20 at 2:34 PM`) are
covered by unit tests rather than by a rendered screenshot. Happy to add
a spec that seeds an older `created_at` if a reviewer wants to see them.

## Validation

- `pnpm check`, `pnpm typecheck` — clean
- Unit: **3800/3800**, including 17 new tests in
`shared/lib/datetime.test.mjs` and 4 in
`messageTimestampContract.test.mjs`

The datetime tests pin the things that are easy to regress:
Today/Yesterday as *calendar* boundaries rather than 24-hour windows (a
message 15 hours old across midnight is "Yesterday"; one 22 hours old on
the same day is "Today"), the weekday band bounded at both ends so a
future timestamp from clock skew never gets labelled with a past
weekday, no ordinals across all the tricky days
(1/2/3/11/12/13/21/22/23/31), the year omitted within the current year,
and compact labels staying ≤12 chars for a narrow row.

- Smoke E2E: **783 passed, 2 failed, 1 skipped**

Both failures are pre-existing and unrelated, confirmed by re-running
each against a clean tree:

1. `video-attachment.spec.ts:223` — fails deterministically on clean
`main`
2. `community-rail.spec.ts:797` (keyboard drag-and-drop reorder) — flaky
on clean `main`: 2/5 failures there vs 3/5 with this branch, i.e. noise

## Mobile

Mobile had the same divergence, so it moves with desktop rather than
drifting until the next pass.
`mobile/lib/features/channels/date_formatters.dart`:

| Before | After |
|---|---|
| `formatDayHeading` → Today / Yesterday / `Tuesday, March 31, 2026` |
Today / Yesterday / `Tuesday` / `March 31` / `March 31, 2025` |
| `formatThreadSummaryLastReplyTime` → `on May 19th` | `on May 19` |

Same two departures from the standard as desktop, documented at the
definition and cross-referenced to `datetime.ts` so the next person
editing one finds the other. Day comparison also moved to a rounded
start-of-day difference, so a DST transition counts as one calendar day
rather than zero — Dart's `Duration.inDays` truncates.

**Message timestamps stay clock-only on mobile.** Desktop message
headers now read `Yesterday at 9:05 AM`; mobile keeps `9:05 AM` at every
band. That's the compact side of the same surface split the desktop
change makes — a mobile timestamp sits inside a chat bubble on a narrow
screen with the day divider a short scroll away, where a date word costs
width it doesn't earn. Recorded as a decision at `formatMessageTime` so
it doesn't read as an oversight.

Mobile needs no middot work: message headers have no "managed by"
segment, and the mention suggestion list already uses `\u00b7`.

Validation: `dart format` clean, `flutter analyze` no issues, `flutter
test` **911 passed, 1 skipped** — 8 new day-heading tests covering the
weekday band, the year boundary, ordinals across
1/2/3/11/12/13/21/22/23/31, distinct labels for consecutive days in the
oldest band, and calendar-day rather than 24-hour bands.

## Out of scope

- **Search results.** `SearchResultItem.tsx` and `TopbarSearch.tsx`
hand-roll a `5m ago` elapsed format. That's a third *kind* of label —
elapsed rather than relative-calendar — and deciding whether search
should switch is a separate call.
- **`formatThreadSummaryLastReplyTime`** keeps its own "3 hours ago"
elapsed scale on both platforms; only its old-reply fallback lost the
ordinal (`on May 19th` → `on May 19`).
- **Mobile search.** `relativeTime` returns `7/31/2026` past a week,
matching the desktop search format that's also out of scope above. Both
should change together or not at all.

---------

Signed-off-by: Clay Delk <clay.delk@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 08:39:53 -07:00
17977814d3 fix(desktop): amortize observer journal eviction with a low-water mark (#5808)
Refs #5718.

## What happens

`appendAgentEvents` evicts the per-agent live observer journal back to
*exactly* `MAX_OBSERVER_EVENTS`:

```ts
const trimmed = sorted.length > MAX_OBSERVER_EVENTS;
const final = trimmed ? sorted.slice(sorted.length - MAX_OBSERVER_EVENTS) : sorted;
```

Once an agent's journal reaches 3000, `current.length` is 3000 forever,
every later append makes `sorted.length >= 3001`, and `trimmed` is
`true` on every call. That permanently disables the incremental-fold
gate:

```ts
if (allAtEnd && !trimmed) { /* incremental fold */ }
else { transcriptByAgent.set(key, buildTranscriptState(final)); }
```

So every steady-state append then replays the whole retained window
through `buildTranscriptState`, which is itself O(streamed-text) because
streaming chunks fold as uncapped string concat. Nothing shrinks
`eventsByAgent` except a store reset, so the state is permanent for the
life of the renderer process, per agent. At ~90 frames/min an agent
crosses the cap in ~33 minutes; from then on live CPU escalates (issue
receipts: 188x on a headless ingest, renderer CPU climbing to 119% of a
core after five minutes idle).

This is not an off-by-one — a cap of 3000 does want `>`. The defect is
that trimming *to* the cap re-arms eviction on the very next append, and
eviction is what forces the replay.

## Fix

Evict to a low-water mark below the cap:

```ts
const OBSERVER_EVENTS_LOW_WATER = Math.floor(MAX_OBSERVER_EVENTS * 0.9);
```

The journal still never exceeds `MAX_OBSERVER_EVENTS`; it now has to be
refilled by ~300 ordinary appends before the next eviction, so one
replay is amortized across the appends that refill it. Retention
semantics (newest-N at trim time) and the derived transcript are
unchanged. The mark is a **fraction of the cap** rather than a fixed
count so the math stays correct if the cap is ever made per-agent — a
fixed headroom could exceed a smaller cap and drive the slice length
negative.

### Eviction floor

Low-water eviction leaves headroom below the cap, and the dedup set is
built only from the *retained* array — so once eviction discards the
oldest frames, the journal no longer remembers them. A relay reconnect
replaying a pre-eviction frame (normal relay behavior, and the reason
the dedup set exists) would be re-admitted into the headroom, and a
later refill to the cap would then trim away up to 300 legitimate
retained events with **no new activity** — a bounded display-window loss
plus rebuild churn that partially defeats the amortization.

To close that, each agent carries an **eviction floor**: the ordering
key of the newest event eviction has ever discarded
(`evictionFloorByAgent`, recorded at trim time as the entry just below
the retained window). `appendAgentEvents` rejects any arrival at or
before the floor (`isObserverEventAfter`, so an equal key is rejected —
the floor event itself was evicted); a stale-only batch returns `false`
with no rebuild and no notify. Out-of-order frames *newer* than the
floor are still admitted via the rebuild fallback, so the fold-gate
semantics are unchanged. The floor is cleared in
`resetAgentObserverStore` alongside the other per-agent maps.

## Evidence

`observerTranscriptRetention.test.mjs` asserts the retention window's
**shape** — the observable signal for which ingest path runs, since
transcript *content* is identical on both paths by design — plus
boundary cases and the invariant that the derived transcript still
equals a full replay of the retained window.

Against the pre-fix trim-to-cap shape, three tests fail on the mechanism
itself (`test_append_crossing_cap_trims_to_exactly_low_water`,
`test_headroom_refills_before_next_eviction`,
`test_single_batch_larger_than_cap_trims_to_low_water` — each expects
headroom the old shape never leaves), and the cost shows up directly in
runtime:

| | `observerTranscriptRetention.test.mjs` (single-event appends past
the cap) |
|---|---|
| trim-to-cap (pre-fix) | **429,105 ms** |
| this branch | **16,221 ms** |

~26x on this workload, consistent with the 188x the issue measured on a
heavier one (their events accumulate streaming text; these do not, so
this understates it).

Three further tests pin the **eviction floor** against reconnect replay:
a replay of already-evicted frames leaves the retained window
byte-identical and notifies no listener; a pre-floor frame arriving
after a refill to the cap drops no retained events; and an out-of-order
frame *newer* than the floor is still admitted. Deleting the floor check
turns exactly the first two red while the out-of-order case stays green
— confirming the tests pin the floor's rejection without
over-constraining legitimate out-of-order delivery.

## Merge-order note

This PR collides with #5596 (bounded renderer accumulators) on
`observerRelayStore.ts` by design — #5596 refactors this exact eviction
into `mergeObserverEventBatch` in a new `observerEventOrdering.ts` and
adds a second, unpinned-agent tier (`truncateUnpinnedAgentWindow`,
`UNPINNED_AGENT_EVENT_TAIL`). This PR merges first; #5596 rebases over
it, porting the low-water cap-math **and the per-agent eviction floor**
into `mergeObserverEventBatch`, and applying the same headroom to the
unpinned-tier truncate (which must also record a floor when it trims).
The fraction-of-cap form makes the low-water port mechanical — it feeds
either the 3000 pinned cap or the 100 unpinned tail without a
fixed-count underflow.

## Credits

Supersedes #5767 (Chessing234's low-water-mark approach and the runtime
measurements).

Closes #5718. Issue receipts from the reporter, GeneralJah215 (188x
headless, 119%/core after 5min idle).

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-14 11:38:18 -04:00
34a7f2fb91 Unify agent profile content (#5788)
## Summary

- remove synthetic preview runtime and configuration data so profiles
show only real agent content
- simplify model settings to the effective values and restore bare
section icons
- make owned-agent profiles resolve to the same current persona instance
from every entry point

## Why

Agent profiles opened from DMs or channels could fall back to a partial
declared-owner view instead of the full managed-agent profile shown on
the Agents page. Test preview content and configuration provenance also
remained visible after the redesign.

## User impact

Owned agent profiles now expose the same actions, runtime, channels,
memories, and configuration regardless of where they are opened.
Profiles no longer synthesize preview data, and model settings use the
same simple title/value hierarchy as the rest of the panel.

## Validation

- `pnpm --dir desktop check`
- `pnpm --dir desktop build:e2e`
- focused unit tests: 10 passed
- profile entry-point integration tests: 2 passed
- configuration screenshot suite: 7 passed, with six visually distinct
captures

Snapshots are attached in a PR comment.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Watcher <bb7abfd757d0af7b66569d02ab9c0316b616f9d0c151ecf5b964344c462e7f8f@buzz.block.builderlab.xyz>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
Co-authored-by: Watcher <bb7abfd757d0af7b66569d02ab9c0316b616f9d0c151ecf5b964344c462e7f8f@buzz.block.builderlab.xyz>
2026-08-14 16:29:50 +01:00
43e53fc349 Standardize settings section layout (#5855)
## Summary

- move Settings section labels outside their framed containers and
centralize the spacing
- apply the shared hierarchy across Appearance, Notifications, Voice,
Agents, Shortcuts, Members, and Profile
- give Identity and Sign out complete section treatments while removing
redundant in-cell labels

## Testing

- desktop pre-push checks, including 4,791 tests
- focused Settings layout and sign-out Playwright coverage

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
2026-08-14 16:25:40 +01:00
8b8445f5ef fix(desktop): share one timer across same-interval useNow consumers (#5861)
## Summary

Every `useNow(1000)` consumer owned its own `setInterval`. With dozens
of "agent working" surfaces mounted (sidebar channel badges, tray menu,
agent session panels, managed-agent rows), each ticked on its own
unaligned 1 s timer — a render/composite pass per consumer per second.
On a machine running ~23 agent sessions this pinned a sustained **~25%
of a core** in `com.apple.WebKit.WebContent` while the app sat idle.

This PR makes same-interval `useNow` consumers share one timer: all of
them tick in a single `setInterval` callback, so React batches the state
updates into one render pass. The last unsubscriber tears the timer
down; the visibility gate (pause while hidden, snap fresh on return) is
unchanged.

Attribution receipts (live dev build, 23 acp sessions): the shimmer was
the original suspect from `sample` stacks, but probing `animation: none`
left CPU flat (~25%), while clamping `useNow` intervals dropped it
immediately. Repeated A/B with this exact change: **~25% → ~3–9%**
webview CPU under the same agent load (ambient variance from live agent
activity; the delta reproduced across three alternations).

### Related issue

None found — follow-up to the presence-firehose investigation (#5830
fixed the subscription side; this is the remaining local render cost).

### Testing

- `pnpm test` — 4792/4792 pass, including a new test asserting N
same-interval consumers create exactly one timer and the last unmount
releases it
- `pnpm typecheck`, `biome check` — clean
- Live-local per TESTING.md: hot-patched into a running dev desktop with
23 active acp sessions; webview CPU dropped from ~25% sustained to ~3–9%
(A/B/A alternation, `ps` sampling over 30 s windows)

Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
mobile-v0.11.0-rc.2
2026-08-14 10:16:10 -04:00
ea0960f8d0 Clarify immediate spoken huddle replies (#5863)
## Summary
- state that only `buzz messages send` messages are spoken in a huddle
- require the first tool call after being addressed to be a brief spoken
pickup
- explicitly override the normal no-bare-acknowledgment rule and bound
follow-up speech
- pin those invariants in the prompt test

## Test plan
- `cargo test --workspace` from `desktop/src-tauri`
- pre-push `desktop-tauri-checks` (clippy and full workspace tests)

Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
2026-08-14 10:14:59 -04:00
df9e773a13 Scope desktop presence subscriptions to active demand (#5830)
## Summary

- replace the desktop's global kind-20001 presence subscription with one
author-filtered subscription derived from active TanStack presence
queries
- reconcile changing demand without a delivery gap: promote only after
relay EOSE, keep the last confirmed subscription on failure, discard
stale opens, and close entirely when demand is empty
- preserve REST presence as the initial seed and TTL/crash-recovery
backstop
- add transport-seam and lifecycle tests for readiness, normalization,
churn, retries, close failures, reconnect ownership assumptions, and
disposal

## Why

The desktop currently receives presence heartbeats from every identity
on the relay. A live tap measured roughly 2,700 events/minute (45/sec),
about 1 MB/minute and 71.5% of readable traffic, from approximately
1,300 distinct fleet identities. Most are discarded only after
WebSocket, Tauri IPC, and JS parsing.

This change applies normal Nostr author filtering at relay fan-out,
before those costs. It deliberately does not introduce a relay digest
protocol or client-side event batching; relevant-author traffic should
be small after scoping, and the existing signed-delta/REST-TTL model
remains intact.

## Correctness model

- active query observers are the demand source; inactive cached queries
retain no authors
- replacement opens before old closes and is promoted only after EOSE
- timeout/CLOSED rejects and closes the candidate while preserving the
last good subscription
- rapid A→B→C and A→B→A churn cannot unseat current A with stale B
- empty demand never sends an unfiltered subscription
- RelayClient continues to own reconnect replay; the reconciler does not
duplicate subscriptions on reconnect

## Validation

Exact pushed head: `8845093aec0330be16efe52d3459ff67f1000ff4`

Pre-push hooks passed:
- desktop check and file-size ratchet
- desktop TypeScript
- desktop unit suite: 4,791/4,791
- branch-skew check

Focused lifecycle/transport suite: 34/34 passed before commit.
Independent Royal Court review found and blocked two prototype flaws
(timeout-as-success and starvation-prone trailing debounce); both were
fixed and the final worktree was cleared with no remaining correctness
or lifecycle blockers.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-13 20:32:34 -07:00
b30f1f6129 Polish mobile profiles, DMs, and sheets (#5401)
## Summary
- add poster-first, tap-to-toggle animated avatars on profile surfaces
while preserving transparent/static behavior elsewhere
- align mobile DM headers, membership actions, and invisible agent
recipient addressing with established desktop semantics
- polish titled sheets and status editing, preserve native iOS sheet
corners, and batch relay reads to improve review-build responsiveness

## Snapshots

<table>
  <tr>
    <th>Profile avatar</th>
    <th>Agent DM header and composer</th>
  </tr>
  <tr>
<td><img
src="https://raw.githubusercontent.com/block/buzz/e8de7495451dbe1a393ab43c5e204ca7425f2ba5/pr-5401--profile-avatar.png"
width="360" alt="Mobile profile settings with animated avatar
surface"></td>
<td><img
src="https://raw.githubusercontent.com/block/buzz/e8de7495451dbe1a393ab43c5e204ca7425f2ba5/pr-5401--agent-dm.png"
width="360" alt="Agent direct message with masked presence and normal
composer"></td>
  </tr>
  <tr>
    <th>Members sheet</th>
    <th>Status editor</th>
  </tr>
  <tr>
<td><img
src="https://raw.githubusercontent.com/block/buzz/e8de7495451dbe1a393ab43c5e204ca7425f2ba5/pr-5401--members-sheet.png"
width="360" alt="Members bottom sheet with centered title and padded
content"></td>
<td><img
src="https://raw.githubusercontent.com/block/buzz/e8de7495451dbe1a393ab43c5e204ca7425f2ba5/pr-5401--status-sheet.png"
width="360" alt="Status editor bottom sheet with duration and quick
statuses"></td>
  </tr>
  <tr>
    <th colspan="2">Switch Community</th>
  </tr>
  <tr>
<td colspan="2" align="center"><img
src="https://raw.githubusercontent.com/block/buzz/e8de7495451dbe1a393ab43c5e204ca7425f2ba5/pr-5401--switch-community.png"
width="720" alt="Switch Community bottom sheet with centered title and
aligned Edit action"></td>
  </tr>
</table>

## Validation
- `just mobile-check`
- `just mobile-test` (1,283 tests)
- installed and reviewed isolated debug builds on iPhone and Pixel

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-13 20:16:04 -07:00
5743562896 fix(huddle): stop 20 Hz speaker-level churn from re-rendering the whole app (#5825)
## Problem

With a huddle open, Buzz Desktop becomes extremely slow and laggy
(Tyler, live report, 2026-08-14). Root-caused and runtime-convicted on
the instrumented rig in #buzz-conversational-agents:

- The Rust playout loop emits `huddle-speaker-levels` over Tauri IPC
every 50 ms, unconditionally, for the whole life of a huddle
(`playout.rs` `SPEAKER_LEVEL_TICK_MS = 50`).
- Each event deserializes to a fresh object, so `setRemoteSpeakerLevels`
updates state at 20 Hz even in silence.
- `HuddleProvider` wraps the entire main app and its context value was
an inline object literal — never memoized. Every level tick minted a new
context identity, re-rendering **every** `useHuddle()` consumer,
including `ChannelScreen` and message rows.

**Measured (A/B, silent one-participant huddle, same channel/state):**
~41 sustained ChannelScreen renders/sec unsuppressed vs ~4/sec with only
the speaker-level setState suppressed — the 20 Hz path is ~90% of the
load. Receipts: `driver-render-counter-unsuppressed.jsonl` /
`-suppressed.jsonl` on the rig, verified independently. The same
main-thread churn starves the relay client's 16 ms event-flush timer,
which is the delayed/bursty message hydration and thread-panel stalls
seen alongside the lag.

## Fix (minimal, no behavior change for meters)

1. **Split the high-frequency fields** (`micLevel`, `activeSpeakers`,
`speakerLevels`) out of `HuddleContextValue` into a new
`HuddleLevelsContext`, consumed via `useHuddleLevels()` only by the
three meter components (`HuddleBar`, `HuddleRoomHeader`,
`HuddleProfileControl`).
2. **Memoize the main context value** so provider re-renders no longer
mint a new identity for the ~everything that consumes `useHuddle()`.
3. **Extract the mic-level analyser** into `useMicLevelAnalyser` — the
level pipeline now lives in one place, and `HuddleContext.tsx` stays
under the file-size ratchet (977 lines).

Level meters keep their 20-30 Hz updates. Everything else re-renders
only when a value it actually consumes changes.

## Acceptance bar

With this fix, a silent open huddle should hold `ChannelScreen` at idle
render rates (single digits/sec), and message hydration should stay live
during huddles. The rig's render-counter + four-clock instrumentation
can verify on this branch.

## Validation

- `pnpm typecheck` clean
- `biome check` clean (repo leftovers in sidebar tests are preexisting
on main)
- full desktop suite: **4,775 passed, 0 failed** at the final tree
- file-size ratchet passes (was the reason for the analyser extraction)
- lefthook pre-commit (desktop-fix + signoff) passed on commit

Not yet done: live-local A/B rerun on this branch — the rig (Wren/Max)
has the instrumentation ready and can convict/acquit the fix with the
same probe that convicted the bug.

Base: `068a83b0` (main). Co-developed with runtime evidence from Wren
and instrumentation by Max.

Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
2026-08-13 22:26:59 -04:00
0f61f24ad6 Fix channel list scroll interruption (#5815)
## Summary
- keep transparent channel-list gaps in Flutter's gesture arena
- allow a new drag to interrupt active ballistic scrolling immediately
- add a behavioral fling-and-counter-drag regression test

## Scope audit
- audited mobile list and scroll constructors across `mobile/lib`
- channels is the only app scrollable overriding `hitTestBehavior`
- all other lists retain Flutter's default opaque hit testing and do not
share this defect

## Verification
- regression test fails before the production change: ballistic offset
continues from `271.17` to `345.56`
- focused interruption regression passes with the fix
- profile/community control test passes
- pre-commit: Dart formatting and Flutter analyzer pass
- pre-push: complete mobile suite passes, 1323 tests
- simulator: immediate counter-drag from the transparent gutter
interrupts deceleration

Simulator evidence:
`/Users/wesb/.buzz/.scratch/mobile-scroll-videos/interruption-verified.mp4`

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-13 19:13:06 -07:00
morgmartandGitHub eedcd886a0 fix(desktop): match compact link preview thumbnail corners to card shell (#5711)
## Problem

In compact link preview cards with an image, the thumbnail's corners
looked inconsistent — the flush left side and the interior right side
read as different shapes.

## Cause

The `Attachment` shell rounds its corners with a **smooth-corner
(squircle) clip path** via `useSmoothCorners`, not a plain
`border-radius`. In compact image mode the shell has `p-0`, so the
thumbnail sits flush against its left, top, and bottom edges.

That means:

- **Left corners** are carved by the shell's smoothed clip path.
- **Right corners** are drawn by the thumbnail's own plain
`border-radius`.

A circular arc and a smoothed corner of the *same* radius are different
shapes (at 16px the smoothed curve starts 25.6px along the edge instead
of 16px). So the two sides could never match by picking a radius value —
the thumbnail's class has no effect on its left corners at all.

## Fix

Give the thumbnail the same `useSmoothCorners` treatment as the shell,
so both sides share one curve.

- Radius token unchanged: `rounded-2xl` (16px).
- The shell and the shared `Attachment` component are untouched, so no
other `Attachment` consumer changes.

Verified on a rendered card — thumbnail vs shell now agree on all three:
arc radius (16), smoothing (0.6), and curve start (25.6px).

## Hardening

The underlying issue is an invariant that lived nowhere: **a child flush
against a smooth-cornered parent must share its corner treatment.** This
is why the bug was easy to introduce and hard to diagnose.

- Documented the invariant in `smoothCorners.ts`, where anyone reaching
for the hook will see it.
- Added an `expectSmoothCorners()` guard to the existing compact-preview
e2e test. Confirmed it **fails** when the fix is reverted, so it
genuinely bites.

Note: this cannot be a lint rule — "flush" is a runtime layout fact, not
visible in the source.

## Known follow-up (not in this PR)

The composer link preview (`useComposerLinkPreviews.tsx`) has the same
latent issue: a flush thumbnail with a hand-copied `rounded-l-2xl` that
happens to match the shell's current 16px. It is correct today only by
coincidence of two literals agreeing. Left for a separate PR rather than
expanding scope here.

## Screenshots

The same compact card and content before and after the change.

| Before | After |
| --- | --- |
| Original `rounded-xl` (12px) thumbnail: left corners are clipped by
the card’s 16px smooth silhouette while the right corners keep the
thumbnail’s smaller plain radius | `rounded-2xl` (16px) thumbnail with
the same smooth-corner treatment as the card |
| ![Before: compact link preview with the original 12px thumbnail
corners](https://raw.githubusercontent.com/block/buzz/9911631a0698cae25df4d41471dde69faa4169dc/pr-5711--before.png)
| ![After: compact link preview with matching 16px smooth thumbnail
corners](https://raw.githubusercontent.com/block/buzz/9911631a0698cae25df4d41471dde69faa4169dc/pr-5711--after.png)
|

## Verification

- `pnpm exec biome check` on all three touched files
- `pnpm exec tsc --noEmit`
- `node --test src/shared/ui/smoothCorners.test.mjs` — 3 passed
- All 18 link-preview e2e tests pass
- Guard verified to fail without the fix, then pass with it

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
2026-08-13 20:42:24 -05:00
068a83b097 feat(huddle): cut voice-turn time-to-first-audio from ~1.0 s to ~0.35 s (env-gated latency levers) (#5671)
## Overview

**Category:** feat (env-gated experiment + one exact always-on
optimization)
**Problem:** Speech-end -> first TTS audio through the desktop huddle
pipeline measures **924–1087 ms** on an M4 Max with a 0 ms LLM leg.
Voice turns feel sluggish no matter how fast the agent replies. Baseline
breakdown: ~300 ms hardcoded VAD silence flush + ~150–250 ms Parakeet
decode + ~380–550 ms TTS synthesis before the first player append.
**Outcome:** With all levers enabled, e2e time-to-first-audio measures
**347–384 ms** (307–357 ms on a longer utterance) on the same hardware,
harness, and production pipelines. Defaults preserve production behavior
everywhere except one deterministic, bit-exact cache win.

## What's in here

### Levers (all default-off, env-gated)

| Lever | Env | Effect (measured medians, short utterance) |
|---|---|---|
| Speculative Parakeet decode | `BUZZ_STT_SPECULATIVE=1` | STT leg ->
~max(flush, decode) |
| Streaming TTS synthesis | `BUZZ_TTS_STREAMING=1`,
`BUZZ_TTS_EMIT_FRAMES` | first audio 380–550 -> 211–320 ms (emit=12,
bit-exact) |
| ONNX intra-op threads | `BUZZ_STT_THREADS`, `BUZZ_TTS_THREADS` | TTS
first audio 211–320 -> 129–180 ms (4 threads) |

- **Speculative decode** starts the Parakeet decode at the *first*
silent VAD frame, overlapping it with the flush window. Resumed speech
invalidates the result (voiced-frame-count check); held silence emits it
instantly at the flush boundary.
- **Streaming TTS**: new `synth_chunk_streaming` (buzz-voice)
interleaves the Flow LM frame loop with incremental *stateful* Mimi
decoding, emitting PCM deltas to the player via the existing
`PlaybackChunkAudio` decoration. At `emit_frames=12` (the decoder's
native chunk) streamed audio is **bit-identical** to the batch path —
verified by the ignored test
`incremental_stateful_decode_matches_batch_decode` (max|diff|=0).
Smaller deltas are faster but diverge (~23 dB SNR; decoder intra-chunk
lookahead), hence the default of 12.

> **Removed after live testing:** the `BUZZ_STT_FLUSH_MS` flush-window
override. Lowering the silence window below natural mid-sentence pauses
(the fast-path recipe said 150 ms) split single spoken sentences into
multiple messages and confused the listening agents. The window is a
turn-taking quality knob, not a latency lever — it is now fixed at the
production 300 ms value.

### Push-to-talk grouping fix (always-on)

A held push-to-talk shortcut is an explicit "I am not done talking"
signal, so silence never ends the utterance while it is held — even when
the microphone is also manually open. The utterance flushes on shortcut
release (existing transmit-edge flush); a manually open mic with the
shortcut up keeps normal VAD pause flushing. Gate is the pure
`vad_flush_allowed` function with a unit-test truth table.

### Always-on (exact): voice-conditioning cache

Phase profiling (`BUZZ_TTS_PHASE_LOG=1`) showed a fixed ~160 ms
`condition_voice` Flow-LM pass on *every* chunk, re-deriving the same
post-conditioning state for the same reference voice. The state is now
snapshotted after first computation and restored per chunk (dtype-tagged
tensor copies, keyed identically to the existing `cached_voice`).
Deterministic — same tensors in, same tensors out. The default path's
TTS leg drops from 380–550 ms to 225–355 ms with no configuration.

### Bench harness

`huddle::latency_bench` (`#[cfg(test)]` + `#[ignore]`) drives the real
`SttPipeline` and `TtsPipeline`, feeding a 48 kHz WAV in real-time 100
ms batches (AudioWorklet cadence) with a configurable fake LLM in place
of the relay leg, timing speech-end -> transcript -> speak() -> first
accepted player append.

```
BUZZ_STT_SPECULATIVE=1 BUZZ_TTS_STREAMING=1 \
BUZZ_TTS_THREADS=4 BUZZ_STT_THREADS=2 \
BUZZ_BENCH_WAV=<48k f32 mono wav> \
cargo test --release -p buzz-desktop --lib huddle::latency_bench -- --ignored --nocapture
```

## Tradeoffs to weigh before promoting any lever to a default

- **Speculative decode**: the speculative buffer has ~1 silent tail
frame vs ~19; observed one CTC wobble ("fail" vs "failed") in 24 turns.
Mitigation if productionized: zero-pad the speculative buffer to match
the flush-path shape.
- **Threads**: defaults stay 1 pending the min-spec (4-core Intel) A/B
flagged in the existing `STT_NUM_THREADS` comment.
- **Streaming at emit<12** is NOT the same waveform — don't ship below
12 without an ear pass.

## Validation

- Full desktop lib suite: **2408 passed / 0 failed** at this head
(`18fab2e1c`).
- buzz-voice suite green; bit-exactness test passes against the
production batch decode.
- Defaults-only bench rerun stays in the baseline family everywhere
except the exact conditioning-cache win (stt 525–532, tts 225–355).
- `cargo clippy --workspace --all-targets -- -D warnings` + fmt clean
(pre-push hook battery green).

Measurement notes with per-lever logs: Eva's workspace,
`RESEARCH/HUDDLE_E2E_LATENCY_OPTIMIZATION_2026_08_12.md` +
`RESEARCH/HUDDLE_E2E_STT_FAKELLM_TTS_BASELINE_2026_08_12.md`.

## Suggested promotion order

1. Conditioning cache (in this PR, always-on, exact).
2. Streaming TTS at emit=12: bit-exact audio, biggest UX win — needs the
env-gate removed + barge-in soak + an ear pass on a real huddle.
3. Speculative decode with silence padding: near-free ~100–150 ms.
4. Threads: after min-spec A/B.

---------

Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Signed-off-by: tlongwell-block <tlongwell@block.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Co-authored-by: tlongwell-block <tlongwell@block.xyz>
2026-08-13 19:42:54 -04:00
c8da06c5e9 Speed up initial direct messages (#5658)
## Summary

- avoid blocking first-DM navigation on a full channel-list refresh
- publish the initial message through the acknowledged HTTP path instead
of waiting on a missing WebSocket acknowledgement

## Validation

- 4,715 desktop unit tests
- desktop typecheck and checks
- focused new-DM Playwright coverage

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz>
2026-08-13 16:24:48 -07:00