mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
91aea2db96b9b75a911a874210f00716957fba4c
1067
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
50f615ecbd |
Merge remote-tracking branch 'origin/main' into duncan/workspace-scoped-agent-store
* origin/main: chore(release): release Buzz Desktop version 0.5.5 (#4809) feat: paste composer text without formatting (#4801) Revert "chore(release): release Buzz Desktop version 0.5.5" (#4808) Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
25a9cf1be6 |
feat: paste composer text without formatting (#4801)
## Summary - handle Cmd+Shift+V on macOS and Ctrl+Shift+V on Windows/Linux in the message composer - read plain text through the native Tauri/arboard clipboard path in packaged builds, with a browser-only Clipboard API fallback - re-enter ProseMirror's paste pipeline with populated `text/plain` clipboard data so selection, undo, multiline behavior, and paste observers remain intact - cover both platform mappings with rendered composer E2E tests that assert the native command path ## Testing - `pnpm test` — 4,286 passed - `pnpm check` - `pnpm typecheck` - `pnpm exec playwright test composer-selection-formatting.spec.ts --project=smoke` — 26 passed - `cargo check --manifest-path desktop/src-tauri/Cargo.toml --workspace --all-targets --target aarch64-apple-darwin` - `just desktop-tauri-test` — 2,206 core tests plus integration and doc-test groups passed - full pre-push hooks passed ## Manual verification Physical packaged-app clipboard verification remains recommended on macOS, Windows, and Linux. The automated E2E uses mocked Tauri IPC but asserts the native `read_clipboard_text` command is invoked. Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> |
||
|
|
4634fe1b39 |
chore(desktop): merge origin/main into workspace-scoped-agent-store
Adopt spawn_hash → spawn_snapshot rename from main; migrate spawn_config_hash: 0 test fixtures to prospective_spawn_config_snapshot() calls in global_agent_config tests, epoch tests, and runtime_commands tests. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
a1d78f2959 |
feat: Buzz entity links — rich preview cards + in-app navigation for repos, PRs, and issues (#4695)
## Summary
Gives Buzz-hosted git entities the same "GitHub-style" chat experience
GitHub links already get: rich preview cards, real titles, and
click-through — except clicks navigate **in-app** to the Projects view
instead of a browser.
- **Spec**: `docs/buzz-entity-links.md` — link scheme, slices, and
deferred work (`buzz://project`, OS deep links, web routes).
- **Canonical `buzz://` deep links**: new
`desktop/src/shared/lib/entityLink.ts` with builders + strict parser for
`buzz://pr?id=…&owner=…&d=…`, `buzz://issue?…`, and
`buzz://repo?owner=…&d=…`, mirrored by a Rust module
(`crates/buzz-cli/src/links.rs`) with a shared golden-format test so the
two implementations can't drift.
- **Preview cards**: `linkPreview.ts` recognizes `buzz://` entity links
*and* HTTPS relay clone URLs (`{origin}/git/<pubkey>/<repo>`, the shape
agents paste today). Both normalize onto the canonical `buzz://` href,
so the two spellings of a repo dedupe to one `Buzz`-provider card
(`BuzzMark` logo) rendered by `link-preview-attachment.tsx`.
- **Title enrichment**: PR/issue cards fetch the real subject from the
relay event (`subject` tag or first content line) via
`useResolvedLinkPreviews.ts`; the cache is community-scoped and reset in
`resetCommunityState()`.
- **In-app navigation**: clicking a card or inline anchor (including
HTTPS relay clone URLs whose origin matches the active relay) routes to
the canonical `30617:<owner>:<d>` coordinate via `goProject()`
(`markdown/entityLinks.tsx`). **Merge dependency: #4671 must merge
first** — route resolution for `30617:` coordinates is implemented on
that branch (`feat/multi-repository-projects`). Entity-link and
external-anchor logic were extracted out of `markdown.tsx` to stay under
the file-size ratchet.
- **Agent side**: `buzz pr open`, `buzz issues create`, and `buzz repos
create` now return a ready-made `link` field (omitted when the relay
returns `accepted: false`), and `base_prompt.md` instructs agents to
paste it verbatim when announcing work.
## Test plan
- [x] Desktop unit tests: pass, including new `entityLink.test.mjs` and
`linkPreview.test.mjs` coverage (golden formats, malformed-link
rejection, clone-URL/`buzz://` dedupe, origin-gated anchor behavior,
label-must-win invariant, cache epoch)
- [x] Rust: `cargo test -p buzz-cli` golden-format test +
accepted/rejected link guard assertions, clippy + fmt clean
- [x] Biome + `tsc --noEmit` clean; pre-push hooks
(desktop-tauri-checks, rust-tests, desktop-test) pass
- [ ] Manual: paste a relay clone URL and a `buzz://pr` link in a
channel — verify one card each, real PR title, and in-app navigation to
the Projects view
Related: [#4671](https://github.com/block/buzz/pull/4671)
---------
Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
|
||
|
|
e30db7028f |
feat(projects): support multiple repositories (#4671)
## Summary - adopt the finalized NIP-MP project model so one project can enumerate and switch between multiple NIP-34 repositories - add project and repository navigation, activity summaries, existing-repository attachment, and repository access-channel management - preserve privacy-safe activation provenance for agent-authored patches, pull requests, issues, and associated commits ## Test plan - [x] Run desktop typecheck and unit tests - [x] Run focused NIP-MP, repository access, and provenance tests - [x] Run Rust formatting and desktop lint checks - [x] Run the complete pre-push suite after merging current `main` - [ ] Manually verify project creation, repository attachment, switching, and access repair on staging - [ ] Manually verify public-channel and private-agent origin labels on newly created Git activity Related: [#4695](https://github.com/block/buzz/pull/4695) --------- Signed-off-by: Thomas Petersen <thomasp@squareup.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
65f7a10035 |
fix(desktop): wait for terminal frame before splash (#4781)
## Summary - keep the first-open Buzz Term splash pending until the active PTY delivers its first frame - retrigger the splash effect when that readiness gate changes - cover the real bootstrap path so startup latency cannot consume the animation invisibly ## Verification - Wes manually verified the first-open animation in the worktree - `pnpm --dir desktop typecheck` - `pnpm --dir desktop test` — 4,195 passed - `pnpm exec biome check src/features/terminal/TerminalBootstrap.tsx src/features/terminal/TerminalSubstrate.tsx src/features/terminal/TerminalBootstrap.test.mjs` - pre-push hooks — branch skew, desktop check, and 4,195 desktop tests passed The repository-wide `pnpm --dir desktop check` still reports pre-existing diagnostics in `personaCatalogRelay.test.mjs` and `terminal.css`; the three changed files pass Biome directly. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
8b8d86c5d2 |
fix(desktop): integer-align custom reaction emoji (#4779)
## Summary - remove the fractional half-pixel translation from custom reaction emoji - preserve the existing 28px reaction pill, 14×14 glyph box, and `object-fit: contain` - add real-app Playwright coverage for integer centering and non-square intrinsic dimensions ### Related issue None found. Follow-up to the Buzz emoji-warp investigation. ### Testing - `cd desktop && pnpm exec playwright test tests/e2e/custom-emoji.spec.ts --project=smoke` (15 passed) - `cd desktop && pnpm test` (4,171 passed) - `cd desktop && pnpm lint` (passed; two pre-existing informational `useTemplate` diagnostics) - `cd desktop && pnpm typecheck` (passed) - `cd desktop && pnpm exec biome check src/features/messages/ui/MessageReactions.tsx tests/e2e/custom-emoji.spec.ts` (passed) Independent review also mutation-tested the regression coverage by restoring the half-pixel transform and confirming the new test fails. No after screenshot is included because the patch preserves dimensions and fixes subpixel raster alignment; the real-app test asserts the mechanism directly. Validated at `bc95969b21b58d83b7f94de4ad25e499e52b35fb`. Signed-off-by: Kalvin Chau <kalvin@block.xyz> Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz> |
||
|
|
ce3cf3cd25 |
Polish Huddle voice controls (#4694)
## Summary - add a visible Stop control for interrupting agent speech - make push-to-talk available by default while preserving manual mute controls - refine agent management, muted audio states, drawer layering, and return navigation - suppress duplicate notification sounds for Huddle messages ## Why Huddles could trap users behind long agent speech, hide useful agent controls, and leave temporary Huddle state visible after the call. The drawer also regressed when the terminal substrate began painting behind the rounded app surface. ## Validation - `just desktop-ci` - focused Huddle Playwright coverage for the drawer, speech interruption, agent picker, and leave navigation --------- Signed-off-by: kenny lopez <klopez4212@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> |
||
|
|
5179726737 |
fix(local-archive): default both archive settings to enabled (#4750)
## Overview
Both local archive settings — "Archive my agents' observer frames" (kind
24200) and "Archive my agents' turn metrics" (kind 44200) — previously
defaulted to OFF in OSS builds, controlled by build-time env vars. This
had an irreversible cost: observer frames are ephemeral (not stored by
the relay), so any missed events are permanently unrecoverable. This PR
makes both settings default to enabled for all builds and removes the
build-time flag machinery entirely.
## What changed
### Rust
- `observer_archive_default_enabled()` — returns `true` unconditionally;
removed `option_env!("BUZZ_DESKTOP_BUILD_OBSERVER_ARCHIVE_DEFAULT")`
check and `nest_is_dev()` runtime fallback.
- `agent_metric_archive_default_enabled()` — returns `true`
unconditionally; removed
`option_env!("BUZZ_DESKTOP_BUILD_AGENT_METRIC_ARCHIVE_DEFAULT")` check
and its OSS-build test.
- `build.rs` — removed both `rerun-if-env-changed` declarations
(`BUZZ_BUILD_OBSERVER_ARCHIVE_DEFAULT`,
`BUZZ_BUILD_AGENT_METRIC_ARCHIVE_DEFAULT`) and the two baked-env
emitting blocks.
### Build / CI
- `Justfile` — removed `desktop-tauri-test-compiled-flags` recipe (the
dual-compile test machinery).
- `.github/workflows/ci.yml` — removed the "Desktop Tauri compiled-flag
verification" CI step.
### TypeScript
- `useObserverArchiveSeed.ts` — removed `observerArchiveDefaultEnabled`
dep from `ObserverArchiveSeedDeps` and the `policyOn` gate in
`reconcileObserverArchive`; the function now unconditionally calls
`mergeSaveSubscriptionKinds`.
- `useAgentMetricArchiveSeed.ts` — removed
`agentMetricArchiveDefaultEnabled` dep from `AgentMetricArchiveSeedDeps`
and the `defaultOn` flag-check path in `maybeSeed`; the
`hasExplicitChoice` guard is preserved as the sole gate against
re-seeding.
- `LocalArchiveSettingsCard.tsx` — removed `policy` prop,
`observerPolicy` state, and `observerArchiveDefaultEnabled` fetch from
`ObserverArchiveSection`; toggle is now always enabled (just `toggling`
disables it); removed the stale "Always on for internal builds" copy
branch; removed the `observerPolicy !== false` guard from
`handleObserverToggle`.
- `tauriArchive.ts` — updated JSDoc on both default-enabled functions to
reflect always-true.
- `e2eBridge.ts` — changed both mock defaults from `?? false` to `??
true` so E2E tests without an explicit mock override exercise the real
default behavior.
### Tests
- `useObserverArchiveSeed.test.mjs` — replaced `policyOn` dep with
direct merge dep; updated `test_oss_policy_off_no_merge` →
`test_reconcile_always_seeds_24200`; all cancellation, identity-switch,
and ordering tests adapted.
- `useAgentMetricArchiveSeed.test.mjs` — removed `defaultOn` dep and
`test_oss_build_does_not_seed`; updated
`test_internal_build_unset_seeds_*` → `test_default_enabled_*`;
`hasExplicitChoice` guard tests unchanged.
## Preservation of explicit opt-outs
Users who have previously toggled the setting off are unaffected:
- `useAgentMetricArchiveSeed` skips seeding when
`hasExplicitChoice(pubkey)` returns true (localStorage-persisted per
identity).
- Observer archive reconciliation now unconditionally calls
`mergeSaveSubscriptionKinds`, but a user who already deleted the
subscription can turn it off via the Settings toggle, which calls
`removeSaveSubscriptionKind` — this is the existing explicit opt-out
path, and the toggle is now always enabled (not locked by a policy
flag).
## Result
- No `BUZZ_BUILD_*_ARCHIVE_DEFAULT` /
`BUZZ_DESKTOP_BUILD_*_ARCHIVE_DEFAULT` references remain outside
CHANGELOG/history.
- Desktop node tests: 4168 pass, 0 fail.
- `just desktop-tauri-check`: clean.
- `just desktop-tauri-test`: all pass.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
e5efd04705 |
fix(desktop): close reconnect gaps that previously required CMD+R (#4737)
> Opened by Brain (agent) on behalf of @wesbillman. ## Problem Users report the desktop app doesn't reliably reconnect and can wedge in states where only CMD+R (or a full restart) restores connectivity (thread `c2205e2b` in #desktop-reconnecting). Pinky's empirical light-switch matrix (real `buzz-relay`, SIGTERM/1012 + SIGKILL × 1s/45s/3min, at `f18a9cb10`) passed 4/4 — the backoff state machine recovers cleanly from ordinary relay loss. That isolates the user-stuck states to four special cases a reload resets but the auto flow never did. ## Fixes | Gap | Change | |---|---| | **G1** — recovery rode solely on the backoff timer (max 30s), throttled by WKWebView in occluded/background windows; nothing fired on network return or wake | New `useRelayResumeTriggers`: `online`, window focus, and visibility→visible call `preconnect()` when the session is `reconnecting`/`stalled`, rate-limited to one attempt per 5s (`relayResumeTriggerPolicy.ts`). Deliberately inert for the terminal `disconnected` state. | | **G2** — any AUTH `OK false` latched the session terminal forever, though the relay also rejects for transient causes (duplicate-AUTH "already authenticated" race, ±60s clock skew, fail-closed allowlist DB errors) | New `AuthOkTracker` (`relayAuthPolicy.ts`): "already authenticated" resolves as success; transient rejections retry with normal backoff; latch only on `restricted:` or after 3 consecutive rejections. | | **G3** — an `auth-required:` CLOSED (REQ racing AUTH after reconnect) permanently deleted the live subscription with no UI signal — frozen channel while state reads "connected" | Reclassified `auth-required:` as retryable in `relayClosedPolicy.ts`. Genuinely terminal classes (`restricted:`, `invalid:`, …) still delete. Can't loop: a truly unauthenticated session latches terminal at the connection level. | | **G4** — `useRelayAutoHeal` observed the 2s-debounced connection hook, so sub-2s flaps never triggered the heal even though `resetConnection` had already rejected every in-flight query | Auto-heal now observes the raw connection-state emitter. The existing 15s heal rate-limit still guards against flap storms. | Each fix is a colocated pure-policy module + unit tests, matching the existing `relayReconnectPolicy`/`relayClosedPolicy` pattern. ## Validation - Full desktop unit suite: **4151 pass, 0 fail** (at branch tip, `pnpm -C desktop test`) - `pnpm -C desktop typecheck` and `pnpm -C desktop check` clean (file-size ratchet respected — `relayClientSession.ts` net −2 lines despite the tracker wiring) - Evidence trail: `RESEARCH/DESKTOP_RECONNECT_CMDR_GAP_AUDIT.md` (audit), `RESEARCH/DESKTOP_RECONNECT_LIGHT_SWITCH_RESULTS.md` (Pinky's matrix) ## Not covered / follow-ups - Native macOS sleep-wake was not automated (would kill the harness session); G1's focus trigger is the mechanism that covers wake in practice, but a manual sleep-wake verification on a real build is worthwhile. - G3 terminal-CLOSED classes (`restricted:` etc.) still silently delete subs with no UI signal — surfacing that is a separate UX decision. - Stall-watchdog latency (60s idle + 10s check) left unchanged; G1 triggers largely mask it. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Signed-off-by: npub1gjuws2a2dc8z2nszprtg7v6u9q7ffeah3hgl5yx45jwn7y7aqs6s5e9xj6 <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz> Co-authored-by: npub1yxv5wk0u0fh6dwt925wntn7h397jvteyj4r87ttcd9xae7n2t3lqqj9jmm <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz> Co-authored-by: npub1gjuws2a2dc8z2nszprtg7v6u9q7ffeah3hgl5yx45jwn7y7aqs6s5e9xj6 <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz> |
||
|
|
cb4a73e17d |
Dock Buzz Term within channel workspace (#4724)
## Summary - replace Buzz Term's full-app takeover with a resizable bottom dock inside the channel content surface - add a discoverable channel-header button plus hide and maximize/restore controls - create PTYs lazily and keep separate, persistent terminal workspaces per channel - capture immutable channel/thread context on every terminal session ## Multiple-channel behavior The dock is a single surface, but its tabs are partitioned by channel. Switching channels swaps to that channel's sessions without terminating background PTYs; returning restores them. New tabs capture the currently visible channel/thread context. ## Verification At commit `7ca087f8e08c80528387684364a65bf4ccd6315f`: - `pnpm --dir desktop typecheck` - `pnpm --dir desktop test` — 4,129 passed - pre-push repository hooks — desktop check/test, Tauri checks, terminal Rust suites all passed --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> Co-authored-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
e1287c92cc |
Refine community invite links (#4734)
## Summary - Separate direct invites from link sharing with a labeled divider. - Show the generated invite URL inline with truncation and a copy control. - Use shared loading feedback and a restrained copy-status resize. ## Validation - `pnpm -C desktop exec playwright test tests/e2e/invite-link-copy.spec.ts tests/e2e/invites-settings-screenshots.spec.ts` (7 passed) --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
0afeac8a7c |
feat(desktop): persist sidebar observed-unread across webview reload (#3976)
## Problem `Command+R` (webview reload) wipes the two in-memory refs driving sidebar channel unread badges: `observedUnreadEventsByChannelRef` and `latestByChannelRef`. The boot catch-up REQ can only fetch events newer than each channel's NIP-RS frontier, so thread replies that arrived before the frontier was passively advanced (the common case) are never re-discovered. Inbox is unaffected because it rebuilds candidates from a relay feed query and checks fine-grained `thread:`/`msg:` markers. The sidebar badge path lacks an equivalent recovery mechanism. ## Solution Persist the sidebar's per-event candidate set to localStorage as a disposable, versioned projection cache (`buzz-observed-unread.v1:<relay>:<pubkey>`) and hydrate it on boot before the catch-up REQ runs. ### New files **`observedUnreadStorage.ts`** — storage module for the cache: - Keyed `buzz-observed-unread.v1:<normalizedRelayUrl>:<normalizedPubkey>` (relay-scoped to prevent cross-community leakage, matching `threadActivityStorage`) - Stores validated per-event `ObservedUnreadEvent` rows; `latestByChannel` is derived at hydration — no divergent dual aggregate - Age pruning (7d = `READ_STATE_HORIZON_SECONDS`), per-channel cap (1000), global cap (5000) across all channels in a scope bucket - Payload `updatedAt` for LRU ordering; registered in `PURE_CACHE_KEY_PREFIXES` for 2 MiB eviction budget - Field-level validation on decode; write failure is non-fatal (session-only degradation) - Snapshot-owning timers: `scheduleObservedUnreadWrite` deep-clones the events map at schedule time — a late A-scope timer can never read B's mutable refs or write under B's key **`useObservedUnreadPersistence.ts`** — hook that owns all persistence lifecycle: - Scope fence: `normalized pubkey + normalized relay` identity; `isScopeLoaded()` callback guards both projection (`rawUnread`) and every **observed-cache mutation** (`recordUnreadEvent`, `removeChannel`, `clearAll`) before touching refs or storage. Note: stale-scope calls to `markChannelRead`/`markAllChannelsRead` can still affect `forcedUnreadRef` and NIP-RS markers, which are pre-existing on `main` and deferred to the NIP-RS arc (see Deferred below). - Synchronous `pagehide` flush closes the Cmd+R timing gap (`useReloadShortcut.ts` reloads within 500ms of teardown, before the 1-second debounce fires) - Identity-reset effect: flushes old scope, resets refs, hydrates from storage, stamps loaded scope — all atomic; cleanup flushes on unmount - `clearAll` cancels the pending timer, resets both in-memory refs, and clears storage in a single transactional operation; `removeChannel` deletes the channel from both refs and replaces any pending snapshot with the current full map — never cancel-without-replacement, preserving sibling-channel events on reload - Marker-prune effect on `readStateVersion`: evaluates each retained event with `observedUnreadEventReadAt()` (the same evaluator used by the projection memo) and removes covered events, rederiving per-channel latest — never clears a whole channel for a single thread/msg marker - Returns a stable `useMemo`-wrapped API object keyed on actual deps so unrelated re-renders do not restart the catch-up REQ - `isScopeLoaded` is a `useCallback` (not a memoized boolean) — always reads the ref at call time, never stale ### Modified files **`useUnreadChannels.ts`** — hook integration: - Calls `useObservedUnreadPersistence` with all persistence wired through the returned API - `rawUnread`: `isScopeLoaded()` guard suppresses A-scope refs from projecting under B - `recordUnreadEvent`: `isScopeLoaded()` fence before touching refs; schedules a debounced write on each successful record - `markChannelRead` clearObserved path: calls `removeChannel` so the cleared state survives reload - `markAllChannelsRead`: delegates to the owner's fenced `clearAll` — the parent does not reset the observed refs directly; `clearAll` owns the transactional clear of both refs and storage, preventing a stale scope-A callback from corrupting scope B **`localStorageQuota.ts`** — registers `buzz-observed-unread.v1:` in `PURE_CACHE_KEY_PREFIXES` ## Design constraints The cache is a **disposable projection**: versioned key, read-through only, safe to delete wholesale. It does not touch `ReadStateManager`, marker semantics, or `forcedUnreadStore`. Zero overlap with the NIP-RS manual mark-read/unread protocol work in progress in another channel; migration path when that lands is "stop reading the key." ## Test coverage **`observedUnreadStorage.test.mjs`** covers storage primitives: - Key normalization, relay-scoped isolation, round-trip correctness - Age-prune and per-channel cap on read and write; global cap across channels - `deriveLatestByChannel` correctness - Thread-marker prune leaves sibling thread events persisted and lit - Scope-isolation state machine: A rows visible in A, absent in B, restored on A again; late A-scope write does not overwrite B's bucket - Malformed structures/fields, relay/pubkey isolation, quota failure degradation **`useObservedUnreadPersistence.test.mjs`** exercises the real hook via `createRoot` + `act`: - pagehide flush: event recorded within debounce window survives reload (headline regression) - Unmount with pending write flushes before teardown - `clearAll` cancels pending debounce so no resurrection after reload - `removeChannel` replaces pending snapshot so sibling channel B survives reload (two-channel repro) - Marker prune: thread and channel markers prune covered events; sibling channels survive - `isScopeLoaded` returns false before identity-reset effect commits, true after - A→B scope switch: pending A-timer is cancelled by flush, A data persisted synchronously (hydration round-trip) - Stale `clearAll` from scope A rejects after scope B loads (observed-cache scope fence) - Stale `removeChannel` from scope A rejects after scope B loads (observed-cache scope fence) - API object identity stable across unrelated re-renders (catch-up stability) **`useUnreadChannels.test.mjs`** exercises the full parent-to-owner seam with real hook mounts: - Stale `markChannelRead` from scope A does not corrupt B's observed bucket after flush - Stale `markAllChannelsRead` from scope A does not overwrite B's bucket after flush ## Deferred Issues deferred to the NIP-RS arc (`#unread-messages-ux`) or future hardening — not regressions introduced by this PR: - **Stale-scope `forcedUnreadRef` / `markContextRead` exposure**: a stale scope-A `markChannelRead` or `markAllChannelsRead` still deletes B's `forcedUnreadRef` entries and advances B's NIP-RS markers via `markContextRead` before the observed-cache fence rejects. This is pre-existing on `origin/main` (identical shape at lines 316/330). Fix requires touching `forcedUnreadStore` and marker paths — out of scope for Fix A. Deferred to the NIP-RS work. - **`isScopeLoaded` empty-scope hardening**: `isScopeLoaded()` returns `true` when `pubkey` and `relay` are empty strings (no active session). A guard could assert non-empty identity before stamping scope-loaded. Low risk in practice since the hook is only mounted after auth, but could be tightened. - **Catch-up batch scheduling**: `handleChannelMessage` and the catch-up loop each clone the full events map per event via `scheduleObservedUnreadWrite`. For channels with large backlogs this produces O(n) snapshot clones per catch-up batch. A batch-schedule API (single snapshot at end of batch) would reduce allocations. Not observable in normal use; deferred as a performance optimization. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
f86dfc5883 |
feat(desktop): surface config diff in restart-required badge (#3637)
The "Restart required" badge reports that an agent's running config has
drifted from its spawn-time config, but never says what changed. This
ships the full feature: a typed Rust diff engine and a TS/UI layer that
renders it at every badge site.
## Rust core (spawn-snapshot diff engine)
Replaces the lossy `u64` `spawn_config_hash` with a typed
`SpawnConfigSnapshot`. The snapshot is stamped from the already-resolved
command/env/config values immediately before `spawn()`, closing the race
window where a mid-spawn config edit would suppress the badge.
`SpawnConfigSnapshot::canonical()` is the single JSON projection shared
by the badge and the diff. Drift is `to_value(stamped) !=
to_value(current)`; the diff is a generic leaf walk over those same two
values, so badge-on and diff-non-empty are structurally guaranteed.
Adding a snapshot field reaches the UI with no code change to the diff
engine — `mutation_table_covers_every_serialized_field` fails CI if a
new field arrives without a mutation row.
`eligible_restart_diff(persona_orphaned, Option<TrackedSpawnState>)`
returns the final vector — snapshot walk entries plus a synthetic
`adapter_availability` entry. It returns empty for an orphaned instance
(spawning one would fail) and for agents with no tracked spawn state
(never stamped, can never have drifted). `needs_restart =
!restart_diff.is_empty()` derives from that vector and nothing else.
Redaction policy (`policy_for(path)`) is shared by the wire diff and the
snapshot's manual `Debug` via `is_safe_to_reveal()` from
`managed_agents::env_vars` as the single authority for env-key masking:
| Policy | Paths | Rendering |
|---|---|---|
| `Text` | `system_prompt`, `team_instructions` | character counts only
|
| `MaskedBare` | `args`, `relay_url` | `••••`, no suffix |
| `MaskedSuffix` | non-allowlisted `env.*` | `••••` + last 4 chars when
longer than 8 |
| `Plain` | allowlisted `env.*` (`BUZZ_AGENT_THINKING_EFFORT`,
`BUZZ_AGENT_PROVIDER`, `BUZZ_AGENT_MODEL`, `DATABRICKS_HOST/MODEL`) and
everything else | verbatim |
Default-deny: every env key not in the explicit allowlist stays masked.
`is_safe_to_reveal()` is the single allowlist authority for both the
baked-env display and the diff.
`restart_diff` is omitted from the wire when empty
(`skip_serializing_if`).
## TypeScript / UI layer
New `restartDiff.ts` module defines `RestartDiffEntry`, `RestartChange`,
`JsonValue`; `tauri.ts` and `types.ts` re-export and add `restart_diff`
/ `restartDiff` fields (Rust omission → `restartDiff: []`).
**`RestartDiffBadge`** — hover tooltip capped at 6 entries + "and N
more", `asChild` span trigger (never inside a `<button>`), auto-restart
blurb below the diff list (on/off variant from `autoRestartEnabled`
prop; same `AUTO_RESTART_ON_BLURB` / `AUTO_RESTART_OFF_BLURB` constants
shared with the Runtime-tab banner). **`RestartDiffList`** renders the
full uncapped list for the Runtime-tab banner with `tooltip`/`inline`
presentation variants for correct foreground in both surfaces.
**`ManagedAgentRow` B4 fix** — badge moved to a sibling `div` of the row
expansion button; tooltip trigger has no `button` ancestor.
**`UnifiedAgentsSection`** — both badge sites render
`<RestartDiffBadge>` instead of a raw `<Badge>`, with
`autoRestartEnabled` threaded from `agent.autoRestartOnConfigChange`.
**Side-panel fix** — `RestartDiffBadge` rendered tab-independently in
the `ProfileSummaryView` hero area (was Runtime-tab only — root cause of
the ~50% inconsistency Will reported). Hero badge is `self-center` in
the flex column. `ProfileRuntimeTabContent` early-return checks
`needsRestart` so the banner is never dropped when all other content is
empty. Auto-restart blurb in the Runtime-tab banner uses the shared
constants.
## Wire shape
```jsonc
"restart_diff": [
{ "field": "model", "change": { "kind": "value", "before": "gpt-5", "after": "claude-4" } },
{ "field": "system_prompt", "change": { "kind": "text", "before_chars": 1234, "after_chars": 1410 } },
{ "field": "env.OPENAI_API_KEY", "change": { "kind": "masked", "before": "••••bc12", "after": "••••xyz9" } },
{ "field": "env.BUZZ_AGENT_THINKING_EFFORT", "change": { "kind": "value", "before": "medium", "after": "high" } }
]
```
`added`/`removed` occur only for dynamic-map keys; nullable struct
fields always serialize as `null`; arrays are atomic leaves (`args`,
never `args.0`).
## Tests
**Rust** — 1902 passing: snapshot mutation coverage, diff entry
serialization, allowlist-aware env masking
(`allowlisted_env_key_shows_plain_value`,
`allowlisted_env_key_is_case_insensitive`,
`non_allowlisted_env_key_stays_masked`),
`unstamped_agent_yields_no_badge_and_no_entries` (both orphan values),
`summary_without_drift_omits_restart_diff_from_the_wire`,
`unstamped_availability_is_not_drift`. Clippy clean, fmt clean.
**TypeScript** — `needs-restart-screenshots.spec.ts`: 11 E2E cases
registered in the smoke project — all three badge sites, tooltip +
keyboard focus, DOM no-button-ancestor assertion, 6+1 truncation,
uncapped Runtime list, unknown field humanisation, side-panel badge on
default Info tab, inactive/friendly-error Runtime opening path.
Consolidates [#3652](https://github.com/block/buzz/pull/3652)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
540b58920c |
Polish sidebar unread hierarchy (#4573)
## Summary - Keep selected sidebar rows regular by default; manually unread rows become bold immediately. - Apply a clearer dark-mode hierarchy: standard inactive rows at 75%, muted rows at 45%, and unread rows at full emphasis. - Keep hover text color stable while retaining the selected-row and unread cues. ## Validation - `pnpm typecheck` - `pnpm build:e2e` - Playwright: sidebar badge and channel-mute coverage ## Screenshots Posted in the PR comments. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
d0d4acd4fa |
fix(desktop): show cached display names on startup (#3317)
## Why Buzz restores cached channels and messages before profile lookups complete. On launch, that briefly exposes pubkey-derived labels in place of familiar display names. ## What - Persist a bounded, relay-scoped cache of last-known display names, NIP-01 names, and NIP-05 handles - Seed batch profile queries from those labels immediately, while keeping them stale so the existing relay request revalidates them - Keep cached data presentation-only: avatars and ownership metadata are not persisted or used to seed profile-detail caches - Remove cleared or missing profiles, purge a relay's labels when its community is removed, and include the cache in local-storage quota recovery - Add unit coverage for parsing, bounds, eviction, malformed data, and cleared profiles - Add an E2E regression that delays the relay profile response and verifies the cached name is rendered first ## Risk Assessment Low. The cache is disposable, capped at 1,000 entries per relay, scoped by normalized relay URL, and always revalidated. It contains only public label fields and does not restore avatars, agent ownership, or authorization state. ## Verification - `just ci` - `pnpm typecheck` - `pnpm test` — 3,727 passed - `pnpm exec playwright test tests/e2e/channels.spec.ts --grep "cached profile labels"` — passed Generated with Codex |
||
|
|
d0af845a1d |
Remove blur from Welcome composer guidance (#4691)
## Summary - Keep the Welcome composer prompt above the dock blur so it stays readable. - Remove blur from the prompt and persona-motion paths. - Cover the crisp, correctly layered banner in the onboarding browser test. ## Validation - `pnpm -C desktop exec biome check src/features/channels/ui/WelcomeComposerBanner.tsx tests/e2e/onboarding.spec.ts` - `pnpm -C desktop build:e2e` - `pnpm -C desktop exec playwright test tests/e2e/onboarding.spec.ts --grep "finishing onboarding creates starter channels and focuses welcome-everyone for a new member" --project=integration` --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
a5bf3c5ae1 |
Refine desktop timeline activity presentation (#4582)
## Summary - Make channel join/leave activity use the selected inline avatar-stack treatment. - Group related membership activity for one hour and preserve profile/overflow-name interactions. - Restore the virtualized day-divider handoff and align the sticky date behavior with the message timeline. ## Validation - `pnpm check` - `pnpm test` - `cargo test --manifest-path desktop/src-tauri/Cargo.toml` - Visual desktop screenshot captured with seeded membership activity --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
f18a9cb106 |
Defer desktop media uploads until send (#4522)
## Summary - send desktop messages immediately while media uploads continue in background state across channel navigation - show immediate progress above the composer and keep Jump to latest above it - report the real media stages as Preparing, Processing, Converting, Uploading, and Finishing - use Buzz's shared spinner during local media work, then switch to the real percentage when byte transfer begins - animate phase-label and status-suffix changes without overlap or layout jumps - keep cancel, progress fill, message publication, and community-reset behavior coordinated with the background task - use raw Tauri IPC for large browser files so renderer-side byte serialization does not block initial feedback ## Why Desktop previously blocked sending while attachments uploaded in the composer. Large videos could also pause the renderer before progress appeared, and the progress pill said Uploading while native media processing was still underway. This makes the initial response immediate and describes the work actually happening. ## Validation - `cd desktop && pnpm check` - `cd desktop && pnpm typecheck` - `cd desktop && pnpm test` (3,931 passed) - `cd desktop && pnpm exec vite build --mode e2e` - `cd desktop && pnpm exec playwright test tests/e2e/file-attachment.spec.ts --project=smoke` (11 passed) - focused native media tests (80 passed) - native Clippy with all targets and features - pre-push native suite (2,107 passed, 14 ignored; 3 diagnostics passed) Updated phase snapshots are included in the PR comments. Split from #4512 so the desktop and mobile changes can be reviewed independently. --------- Signed-off-by: kenny lopez <klopez4212@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> |
||
|
|
ddcf0aef9f |
fix(desktop): stop clipping focus ring on channel intro action cards (#2392) (#4374)
## What Fixes #2392 — the action cards in the empty-channel intro ("Create agent", "Add people") had their `focus-visible` ring clipped by the surrounding scroll container. ## Root cause The cards sit in a `flex ... overflow-x-auto pb-1` row. Setting `overflow-x` (without `overflow-y`) makes the browser compute `overflow-y: auto` as well, so the container clips anything painted outside its padding box — including the cards' `focus-visible:ring-2` box-shadow. With only `pb-1` padding, the top/left/right of the ring were cut off when Tabbing to a card. ## Change `desktop/src/features/messages/ui/ChannelIntroBlock.tsx` — `pb-1` → `p-1` on the action-cards scroll container, reserving 4px on all four sides so the focus ring renders fully inside the scroll container's padding box. - 1 file, 1 line. No behavior change for mouse users or layout. ## Verification - `pnpm typecheck` — clean - `pnpm exec biome check src/features/messages/ui/ChannelIntroBlock.tsx` — clean - `pnpm check:file-sizes` — clean - Desktop unit suite — **3906/3906 pass** Signed-off-by: Sarthak Singh <sarthak.singh@juspay.in> Signed-off-by: Sarthak Singh <sarthak.singh@juspay.in> |
||
|
|
ff16a80b5b |
fix(desktop): pass-2 review corrections (round 3) — captured respawn, behavioral tests, residue
Global-config respawn: replace live-scope wrappers with one transition epoch.
restart_under_captured_epoch acquires managed_agent_runtime_transition +
managed_agents_store_lock, validates captured generation, stops, builds a
captured spawn context (records/personas/global config/owner hex/scope_id
from captured_scope), spawns/registers, saves — no live wrapper past the stop.
persist_last_error takes captured_scope, validates generation under its own
acquired store lock, never called while that lock is held, fails closed on
poison. Two tauri::test::mock_app() tests: fresh-scope no-runtime → Skipped
(proves generation guard passes + path proceeds), stale-scope → Skipped at
generation step (switch-between-stop-and-spawn test, calls production fn).
Behavioral tests on production paths: tauri = { version = "2", features = ["test"] }
added to dev-dependencies. compensate_drain_for extracted with injectable
start_fn; tests call it directly (compensate_drain_for is the production core).
capture_agent_snapshot_import_entry and capture_team_snapshot_import_entry
extracted as testable entry guards; tests call the real production functions
(no-scope reject, owner-mismatch reject, matching-owner passes + relay verified,
stale-generation rejects validate_scope_generation). registerNestNotifications
extracted from useNestNotifications; test imports and calls the real function,
asserts all three event registrations, workspace-degraded toast payload,
unlisten cleanup per event. fail_if_client_mesh_active and mesh_stop_client
tested via tauri::test::mock_builder() with real AppHandle (no-runtime,
client-runtime, no-runtime stop paths). Generalized fail_if_client_mesh_active
and mesh_stop_client to tauri::Runtime to allow mock_app usage.
Residue: _compensation_gate_removed placeholder deleted from AppState.
identity.rs:346-348 comment corrected (guard passed by value, not dropped).
mesh_llm_scope.rs:59-64 stale re-arm comment replaced with accurate Option A note.
Duplicate generation bump at identity.rs:544 removed (clear_active_scope
calls next_scope_generation internally; no second bump needed).
File-size gate: AgentSnapshotImportEntry + capture_agent_snapshot_import_entry
extracted to import_entry.rs (import.rs: 995 lines); TeamSnapshotImportEntry +
capture_team_snapshot_import_entry extracted to team_snapshot_entry.rs
(team_snapshot.rs: 997 lines). Both within the 1000-line ratchet.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
|
||
|
|
6dc4e29155 |
Merge origin/main into duncan/workspace-scoped-agent-store
Resolves two conflicts against main:
AppShell.tsx: HEAD had const { activeCommunity, reinitKey } = communitiesHook
for the composite workspace key; origin/main added useHuddlePresentation()
destructuring from the Huddle redesign (#4281). Resolution keeps both: the
composite key is required for useManagedAgentRuntimeReconciliation, and the
Huddle hooks are needed for the new Huddle UI.
MeshComputeSettingsCard.tsx: HEAD had the Stop using shared compute affordance
plus the legacy inline model section; origin/main (#3735) replaced the inline
model section with the MeshModelPicker component. Resolution keeps the Stop
button block and adopts the MeshModelPicker layout, discarding the replaced
inline model controls.
Also corrects the false comment at runtime_commands_tests.rs:342-345 that
claimed compensate_drain is covered by the desktop integration test suite.
The compensation round-trip requires an AppHandle; the codebase has no
tauri::test harness and no AppHandle mock. The honest coverage statement is:
drain-prefix contract proven by unit test, restart path integration-only.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
|
||
|
|
40607d4a88 |
fix(desktop): pass-1 review corrections (round 2) — compensation, Mesh stop, scope, CI
Item 1 — Compensation primitive: replace AtomicBool gate with lock-owning
compensate_drain that takes the caller's already-held rt_transition guard
by value, re-acquires only the store lock, validates captured scope generation,
then restores journal entries via start_pair_under_held_locks. Split
start_pair_under_held_locks out of start_pair so both the normal and
compensation paths share the spawn-and-register body. execute_drain_journal
refactored to accept an injectable stop_fn via drain_journal_with_stop;
execute_drain_journal_with_stop_fn exposed for test injection. Tests: live
SIGKILL drain, structural lock-release proof, deterministic partial-failure
test with injected stop error covering stopped prefix and remaining tail.
Item 2 — Client stop + start serialization: mesh_stop_client Tauri command
in mesh_llm_scope.rs stops only a client-mode runtime; serve/absent are
no-ops. Client start (ensure_relay_mesh_for_record) acquires
workspace_transition through runtime installation to serialize against
apply_workspace, which holds workspace_transition from before the Option A
preflight through commit. fail_if_client_mesh_active preflight runs under
workspace_transition so no new client can start in the check→commit gap.
UI: 'Stop using shared compute' button in MeshComputeSettingsCard shown when
isConsuming; calls new meshStopClient() in tauriMesh.ts. e2eBridge mock for
mesh_stop_client added.
Item 3 — Fallible migrations + atomic marker: rename_provider_to_runtime_in_personas
propagates Result; migrate_agent_keys_to_dev_service_at returns Result and
propagates from copy_agent_keys_between_stores. run_scoped_migrations uses ?
on persona-provider step. _ready marker written via temp+rename (atomic).
dev-key migration skipped in unit-test builds (#[cfg(not(test))]) to avoid
macOS Keychain dialogs. Tests: old-marker upgrade (no scope deletion), partial
migration failure withholds v1 until repair succeeds.
Item 4 — Global-config captured respawn: Phase 2 restart validates captured
scope generation under store lock before stop, and again before respawn via
start_local_agent_pairs_with_preflight_at (new captured variant using
definitions_dir). persist_last_error validates generation under store lock.
Item 5 — Snapshot imports captured operation context: both confirm_agent_snapshot_import
and confirm_team_snapshot_import capture owner keys at entry, verify against
captured_scope.owner_pubkey immediately, thread captured keys through all
mint/retention/engram phases. Re-verify owner key under store lock before
Phase 3a write. Outbound profile/memory phases use captured_scope.relay_url.
Item 6 — CI red: rustfmt applied (agents_scoped.rs, import.rs); clippy
needless_borrow at team_snapshot.rs:793 fixed; e2eBridge apply_workspace mock
returns { applied: true, degraded: [] } in both immediate and delayed branches;
mesh_stop_client mock case added. Stale 3-line doc fragment removed from
mesh_llm.rs; visibility of re-exported agents_scoped fns bumped to pub(crate).
Item 7 — Listener behavioral test: useNestNotifications.test.mjs exercises
workspace-degraded toast payload, unlisten cleanup, and boundary payloads
without requiring a real Tauri runtime. Doc comment updated: event-sync dispatch
failure does not emit workspace-degraded (shutdown-time, no toast surface).
Item 8 — Dead code (minor): backfill_persona_snapshots AppHandle shim removed;
stale scope_init.rs:388-393 comment corrected; make_base_dir test helper removed.
File-size gate: mesh_llm.rs (999), import.rs (999), team_snapshot.rs (999).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
|
||
|
|
631b05c883 |
feat: ship Buzz Term (#4347)
## Summary - ship **Buzz Term** end to end: the terminal engine/runtime, mounted desktop substrate, and user-visible naming - add Quinn's tape-deck-inspired banner: a beveled chassis filled by the `buzz term` wordmark, surrounded by a complete-hex field - derive the wordmark's three-stop sweep from each theme's terminal palette so primary, secondary, and accent roles remain visibly distinct across all 62 shipped themes, including light themes - paint the banner once on its own pointer-transparent canvas; PTY rendering beneath it remains unchanged ## Banner behavior - uses the renderer's shared `8.4 × 17` cell metrics and production aspect ratio `2.0238` - regenerates only for viewport/theme changes; palette switches repaint correctly while the banner is visible - dismisses on non-empty output from the active terminal session; empty output and inactive sessions do not dismiss it - fails closed below **70 columns** rather than squeezing or clipping the wordmark - adds **8 lines** to `terminalRenderer.ts` for shared cell metrics and **zero lines inside `paint()`** ## Screenshots | Buzz (light) | Buzz Dark | |---|---| |  |  | | Kanagawa Lotus (light) | Red | |---|---| |  |  | Additional production-aspect finals: [Vesper](https://buzz.block.builderlab.xyz/media/9ca6514b63f8cfb2107a85ca46f16a940c0883848e6fbc718e411af94aa13100.png), [Min Dark](https://buzz.block.builderlab.xyz/media/f67bd2970e5d64ffb07b1ae78ab58c847e6ebc23e7e7a48e067eb024dba64ec8.png), and [Dark Plus](https://buzz.block.builderlab.xyz/media/290fee08924f37d064abc687ecf3e9526ab05b87e8e56d610f23048949793dbe.png). The screenshot harness was checked against the shipped painter at this exact head: all **2,541 draw calls** matched on color, glyph, x, and y; four deliberate divergence controls fired. ## Verification at `98ebc8f9048bd5f0ceb7e843b67874d642f0b7fd` - desktop tests: **3,946 / 3,946** - TypeScript: clean - checks: pass (two pre-existing informational `useTemplate` notices only) - integration/e2e: PASS (independent exact-SHA lane; artifacts recorded in the originating Buzz thread) - artifact/dead-path sweep: clean - redteam G1–G7: PASS - all six named banner emitter-deletion mutants die - independent handwritten five-row full-wordmark fixture kills Quinn's seven-mutant battery, including a one-pixel glyph change - real `112 × 46` canvas-rect dismissal tests separately cover active non-empty, active empty, and inactive non-empty output - layer-drop and zero-draw painter mutants die; z-order and pointer-events verified - CI's `tsc && vite build` includes all three banner modules - performance at DPR 2 (worst-case measured envelope): - one-time content paint: **~0.7–0.8 ms**, paid only when the banner is built or its palette changes - busy compositor, CSS `1277 × 697`, backing `2554 × 1394`: **470–497 µs/frame** for the full banner (**2.82–2.98%** of a 60 Hz frame) - busy compositor, CSS `1920 × 1080`, backing `3840 × 2160`: **1,139–1,212 µs/frame** (**6.83–7.27%**) - empty, one-glyph, and full-banner controls converge: compositor cost follows backing-layer area and DPR rather than painted-cell count - in the actual idle welcome state, cost is below both vsync-clamped rigs' resolution; it is not claimed as zero - **Pane cross-rig spread: resolved at matched loop rate.** Two independent rigs initially differed 2.3× (58–68 vs 136 µs/Mpx of backing store; pane, CSS 1277×697 / backing 2554×1394, DPR 2). The cause of *that* spread is rAF loop rate: the higher figure came from a free-running loop at ~1600fps. Throttled to ~200–236fps, both rigs read 58–68 µs/Mpx (1.25–1.44% of a 60Hz frame). The busy-composite figures quoted above remain the **unthrottled worst case** and are conservative by ~2.3× at the pane. Not established: the mechanism and sign of free-running distortion (one rig under-charges ~15%, the other over-charges 2.3×), and the 1080p figure has not been re-measured throttled. - the layer paints only on generation/theme/resize and dismisses on first non-empty active-session output, so the measurable busy cost is a short-lived worst case rather than a persistent PTY paint-path tax ## Follow-ups in this PR These are intentionally subsequent commits after the certified static-banner head, not claims about `98ebc8f90`: 1. close the compositor metrology: remeasure the 1080p point throttled and characterize the opposite-sign free-running rAF distortion, with each measurement regime stated 2. add Tyler's animated honeycomb color waves, gated by `prefers-reduced-motion`, a full 62-theme phase-sweep contrast check, and DPR-2 per-tick performance certification 3. land the already-proven mounted theme-switch regression probe from `RESEARCH/BUZZ_TERM_G3A_PROBE/` 4. bound the slow/hang-shaped G1-c mutant `waitFor` 5. optionally trim the generator to its ink bounding box, reducing the minimum viewport from 70 to 62 columns --------- Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6 <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@buzz.block.builderlab.xyz> Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> |
||
|
|
b42b093613 |
feat(mobile): sync per-group channel sorting (#4231)
**Category:** improvement **User Impact:** Mobile users can sort each channel group by recent activity or A–Z, with their choices synchronized with desktop. **Problem:** Desktop supports persistent per-group channel sorting, but mobile shows the same groups without equivalent controls or shared preferences. The earlier mobile attempt coupled sorting to unsafe dirty-state behavior that could overwrite newer cross-client changes. **Solution:** Add mobile sorting controls and encrypted NIP-78 synchronization using the existing desktop `channel-sort` contract, while retaining ordinary whole-blob last-write-wins behavior. Local state is scoped by identity and normalized relay, startup closes fetch/subscription gaps, and both clients use the same deterministic ordering rules. <details> <summary>File changes</summary> **desktop/src/features/sidebar/lib/channelSortPreference.test.mjs** Updates ordering coverage for the deterministic, cross-client A–Z comparison rule. **desktop/src/features/sidebar/lib/channelSortPreference.ts** Aligns desktop channel-name collation with mobile so synchronized preferences produce the same visible order. **mobile/lib/features/channels/channel_sort/channel_sort_manager.dart** Adds encrypted relay synchronization with safe startup gap handling, clock checks, and ordinary last-write-wins conflicts. **mobile/lib/features/channels/channel_sort/channel_sort_provider.dart** Scopes sort state to the active identity and community lifecycle. **mobile/lib/features/channels/channel_sort/channel_sort_storage.dart** Defines the desktop-compatible payload, relay-scoped cache and migration, cleanup, and shared ordering behavior. **mobile/lib/features/channels/channels_page.dart** Connects sort state to the channel page. **mobile/lib/features/channels/channels_page/body.dart** Applies each selected order to Starred, custom groups, Channels, and DMs. **mobile/lib/features/channels/channels_page/sections.dart** Adds checked Recent and A–Z actions using the existing anchored-popover UI. **mobile/test/features/channels/channel_sort/channel_sort_manager_test.dart** Covers payload adoption, encrypted publication, conflicts, timestamps, retries, and cleanup. **mobile/test/features/channels/channel_sort/channel_sort_storage_test.dart** Covers parsing, relay isolation, migration, cleanup, and ordering modes. **mobile/test/features/channels/channels_page_test.dart** Verifies the group controls expose both choices. </details> ### Reproduction steps 1. Open the mobile channel list with populated built-in and custom groups. 2. Open a group menu and choose **Sort: Recent**; confirm active channels move to the top. 3. Choose **Sort: A–Z**; confirm deterministic alphabetical ordering returns. 4. Repeat for Starred, a custom group, Channels, and DMs. 5. Open desktop with the same identity and community and confirm each synchronized preference. 6. Switch communities and confirm cached preferences do not bleed across relays. ### Screenshots Approved `live` custom-section flow with `research` kept offscreen. | Recent selected | A–Z result | A–Z selected | |---|---|---| |  |  |  | ### Validation - Mobile `flutter analyze` — clean - Focused mobile sort and channel-page suites — 37/37 passed - Desktop full suite — 3906/3906 passed - Mobile full suite — 1034 passed, 1 skipped, 1 unrelated baseline failure reproduced at `ac4fa13b8` <!-- Originating Buzz channel: 2a16a2bb-6fd3-4d69-8182-2afcb21b2d14 --> --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> |
||
|
|
b29c8cdaa4 |
feat(desktop): redesign the Huddle experience (#4281)
## Summary - open Huddles in a focused companion window with a clean handoff back to the in-app drawer and backing channel - redesign the participant film strip, sidebar control, transcript surface, and themed shell treatment - preserve microphone and device control across windows, start agent voice on the first reply, and show agent speaking activity in the film strip - give each agent a distinct session voice, beginning with the configured default, plus compact per-agent text-to-speech and voice controls - enroll only agents explicitly mentioned or deliberately added through an agent panel into the live Huddle roster - keep temporary Huddle channels out of the sidebar unless the user explicitly brings one into the main app - remove Huddle-only avatar policy badges and filter short silence or noise segments before speech-to-text posts ## Why The previous flow exposed the temporary channel as product UI, obscured who was present or speaking, and split transcript and audio state between the main and companion windows. This keeps backing channels as implementation details unless a user explicitly brings a Huddle into the app, while sharing the live conversation and audio lifecycle across both surfaces. Agent participants now join only after an explicit invitation, distinct voices make multi-agent Huddles easier to follow, and short microphone noise no longer becomes stray transcript messages. ## Validation - `pnpm check` - `pnpm build:e2e` - `pnpm exec playwright test tests/e2e/huddle-transcription.spec.ts --project=smoke` (13 passed) - Huddle sidebar visibility unit coverage (4 passed) - focused managed-agent and persona-mention E2E coverage (2 passed) - `pnpm test` (3,910 passed) - `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --all-targets -- -D warnings` - `cargo test --manifest-path desktop/src-tauri/Cargo.toml` (2,093 passed, 14 ignored; 3 diagnostics passed) --------- Signed-off-by: kenny lopez <klopez4212@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> |
||
|
|
985cdcc6ea |
feat(agents): model-tuning parity in global Agent Defaults editor (#4578)
## Overview
The global Agent Defaults surface (Settings card, defaults modal,
onboarding) exposed structured controls for Effort but left Max Output
Tokens, Context Limit, and Max Rounds as raw env vars. Per-agent dialogs
had structured numeric fields but only for `isBuzzAgentRuntime` —
incorrectly excluding Goose. This PR unifies numeric-tuning capability
across all surfaces, fixes a pre-existing dual-editor defect, and adds
full test coverage.
## What changed
### Phase 1 — Catalog projection
- Add `max_rounds_env_var` to `KnownAcpRuntime` in `runtime_metadata.rs`
(`Some("BUZZ_AGENT_MAX_ROUNDS")` for buzz-agent, `None` elsewhere).
- Project all three numeric env-var fields (`max_tokens_env_var`,
`context_limit_env_var`, `max_rounds_env_var`) end-to-end:
`AcpRuntimeCatalogEntry` Rust struct, TS `types.ts`,
`RawAcpRuntimeCatalogEntry` + `fromRawAcpRuntimeCatalogEntry` in
`tauri.ts`, and the e2e mock bridge (`withMockRuntimeConfigMetadata`).
### Phase 2 — Field model
- `deriveAgentConfigFieldModel` now derives `maxOutputTokens` /
`contextLimit` / `maxRounds` descriptors from catalog-projected fields.
- `structuredEnvKeys(descriptors)` — exported helper that takes the
**rendered** descriptor set (not the whole model). Hidden keys follow
what is actually rendered per surface: global hides effort + all three
numeric keys for buzz-agent / two for Goose; per-agent buzz-agent hides
effort + three numeric keys; per-agent Goose hides only its two numeric
keys. `BUZZ_AGENT_THINKING_EFFORT` stays a visible generic env row
per-agent because no effort control renders there.
### Phase 3 — UI
- Extract `NumericTuningFields` from `buzzAgentModelTuningFields.tsx` as
a shared descriptor-driven component (`descriptors`, `envVars`,
`inheritedEnvVars`, `onEnvVarChange`). Kind-specific minima:
`NUMERIC_KIND_MIN` map (`maxOutputTokens`/`contextLimit`: 1,
`maxRounds`: 0) applied to `<input min>`.
- **Global surface** (`AgentConfigFields.tsx`): deduplicate the
previously duplicated Advanced env-editor block; render
`NumericTuningFields` below the env editor when descriptors exist;
`hiddenKeys` and `bakedGenericRows` exclusions use `structuredEnvKeys`
so structured keys are never double-rendered. Under 1000 lines.
- **Per-agent surfaces** (`EditAgentAdvancedFields`,
`PersonaAdvancedFields`): replace `isBuzzAgentRuntime` as the
numeric-field gate with `deriveNumericDescriptors(selectedRuntime)` from
`agentConfigCore`; hidden keys come from
`structuredEnvKeys(numericDescriptors)` — the same rendered descriptor
set, no local rebuilding (fixes pre-existing dual-editor defect).
Catalog status carried as `RuntimeCatalogStatus` (`loading | ready |
error`); both error and loading withhold structured controls and leave
saved values visible as generic rows, making error distinguishable from
"runtime not capable" (`ready` + no runtime).
- **Dialogs** (`AgentDefinitionDialog`, `AgentInstanceEditDialog`,
callers): `AgentDefinitionDialog` accepts `runtimeCatalogStatus?:
"loading" | "ready" | "error"` (replaces separate
`runtimesLoading`/`runtimesError` booleans); all call sites —
`AgentManagementDialogs`, `AgentsView`, `RequestedAgentCreateDialogs`,
`UserProfilePersonaDialogs` — compute and pass the status.
### Phase 4 — Tests
- `buildRecord` exported from `EnvVarsEditor.tsx` as a pure `(nextRows,
value, requiredKeys, hiddenKeys) => Record<string, string>` helper for
isolation testing.
- **17 new node tests** in `agentConfigCore.test.mjs`:
`deriveNumericDescriptors` (all three fields, partial, undefined
runtime, matches field-model subset); `structuredEnvKeys` per surface
including discriminating Goose per-agent effort-key invariant;
`NUMERIC_KIND_MIN` values.
- **4 new node tests** in `EnvVarsEditor.test.mjs`: hidden tuning key
preserved through generic row edits; runtime-switch then generic edit
(derives both descriptor sets, asserts new-runtime hidden key survives
`buildRecord` via `hiddenKeys` and old-runtime key survives via generic
rows); baked numeric key excluded via `filterBakedGenericRows` with
`numericTuningPlaceholder` assertion; clearing a structured override —
`numericTuningPlaceholder` verifies placeholder text.
- **5 new Playwright tests** in `agent-numeric-tuning.spec.ts` (added to
smoke project `testMatch`): global numeric fields visible for
buzz-agent; global: non-capable runtime hides numeric controls; Goose
per-agent shows `Inherit (16384)` after saving global value through the
UI; delayed catalog: saved values visible as generic rows while loading
then structured controls appear after settle; failed catalog: saved
values remain visible as generic rows (never the "unsupported" empty
state).
## Result
- buzz-agent global defaults: Max output tokens, Context limit, Max
rounds as structured inputs with `Inherit (N)` placeholders from baked
env.
- Goose global defaults: Max output tokens, Context limit as structured
inputs.
- A Goose global value surfaces as `Inherit (<value>)` in the per-agent
Goose edit dialog.
- No structured key is editable in two places on any surface; no
persisted key has zero editors.
- No `runtime.id === "buzz-agent"` comparison decides numeric-field
visibility anywhere — capability flows catalog →
`AcpRuntimeCatalogEntry` → field model → UI.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
027a74a61c |
Polish Share Compute settings (#3735)
## Summary - Refresh Share Compute with the shared agent-style model controls. - Reveal sharing details and advanced options only while sharing. - Remove the preview-only mesh API path. ## Validation - `pnpm check` - `pnpm test` - `pnpm exec playwright test tests/e2e/mesh-compute.spec.ts` Snapshots are attached in a follow-up comment. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
7981597848 |
fix(reactions): wrap long popover names (#3834)
**Category:** fix **User Impact:** Long custom emoji names now stay contained inside reaction popovers and remain fully readable. **Problem:** An unbroken custom emoji name could force a reaction popover beyond its intended maximum width and overflow the message view. **Solution:** Give the reaction popover a definite 288px width and allow the complete emoji name to wrap within it without truncation or ellipsis. Short names retain the same content and interaction behavior. <details> <summary>File changes</summary> **desktop/src/features/messages/ui/MessageReactions.tsx** Bounds the reaction popover width and allows long names to break across lines while preserving the full shortcode. **desktop/tests/e2e/reaction-names.spec.ts** Covers fixed width, full text preservation, and wrapping for the maximum supported colon-wrapped reaction name, with deterministic seeded Picsum visual fixtures and explicit image-load waits. </details> ## Reproduction Steps 1. Open a message with a custom emoji reaction whose name is 64 characters. 2. Hover or focus the reaction pill to open its details popover. 3. Confirm the popover remains 288px wide and the complete name wraps within it without ellipsis. 4. Open a short-name reaction and confirm its popover remains readable and unchanged in behavior. ## Screenshots | Before | After | | --- | --- | |  |  | **Short-name regression check**  ## Verification - `pnpm test` in `desktop`: 3,858 passed - Focused reaction-name E2E with seeded Picsum captures: 2 passed - Desktop checks and commit hooks passed Originating Buzz channel: `f2ec9671-d78e-4cde-894c-9f4c458c7f1f` --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> |
||
|
|
d4a4570b97 |
fix(desktop): clarify inherited agent parallelism (#4010)
## Summary - show an unambiguous `App default (10)` inherited state for parallelism in create and edit forms - explain that blank inherits the app default and suppress create-form number steppers that could silently set `1` - align the E2E mint fallback with production while preserving explicit input → definition → app-default precedence ## Why The forms displayed `1` even though an untouched field is omitted and desktop minting materializes `10`. The create-form spinner could also turn blank/inherited into an explicit `1` with one click while leaving the field looking nearly unchanged. ## Testing - `pnpm test` (desktop: 3,886 passed) - `pnpm typecheck` (desktop) - `pnpm check` (desktop) - pre-push `desktop-check` and `desktop-test` --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
5c98932c59 |
feat(desktop): make onboarding model defaults skippable (#3968)
**Category:** improvement **User Impact:** Users can skip default model configuration during onboarding and finish it later in Settings → Agents. **Problem:** Requiring model defaults during onboarding can block users who are not ready to choose a harness, provider, or model. Skipping also needs to leave existing configuration untouched rather than persisting partial selections. **Solution:** Stage onboarding edits locally and persist them only when users choose Next or Back. A delayed Skip action advances without any configuration write, while a footer hint points users to the settings location for completing setup later. <details> <summary>File changes</summary> **desktop/src/features/onboarding/ui/DefaultConfigStep.tsx** Adds the skip action and future-settings hint, and makes model configuration transactional so Skip discards staged changes while Next and Back preserve the intended save behavior. **desktop/src/testing/e2eBridge.ts** Exposes model-config setter call counts so tests can distinguish a true zero-write skip from a write-and-rollback implementation. **desktop/tests/e2e/onboarding-agent-defaults.spec.ts** Covers skipping during loading and after staged edits, verifies zero persistence calls, and confirms Next and Back still commit changes. </details> ## Reproduction steps 1. Start fresh onboarding and continue through harness setup to **Configure your default model settings**. 2. Change the selected harness or model, then choose **Skip for now**. 3. Confirm onboarding advances to **Join or create a community** and the prior global model configuration remains unchanged. 4. Return through onboarding and confirm **Next** saves the staged selection; confirm **Back** also preserves staged changes before returning. 5. Confirm the footer says model defaults can be configured later in **Settings → Agents**. --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> |
||
|
|
ad3230b480 |
fix(desktop): resume-pass corrections — Option A Mesh, versioned ready, captured scope, lock-aware compensation
Eight corrections from the resumed loop (fresh 3-pass budget, Option A ruling):
1. Option A Mesh: delete pre-prepare drain_mesh_client_if_stale and rollback
restore_mesh_sharing (compensated a drain that no longer happens). Replace
with fail_if_client_mesh_active preflight in both apply_workspace and
identity import. Journaled Mesh recipe deferred as tracked follow-up.
2. Versioned _ready: scope_is_ready now reads marker content and compares
against READY_MARKER_VERSION ("v1"); old unversioned markers return false
and force re-run through run_pre_ready_family. Delete log-only post-ready
best-effort guards (backfill + retention migration) from apply_workspace.
Add test: old marker -> pipeline re-runs -> version advances.
3. Snapshot outbound phases use captured scope relay: both
confirm_agent_snapshot_import (Phase 3b profile) and
confirm_team_snapshot_import (Phases 4/5 profile + memory) now use
captured_scope.relay_url instead of relay_ws_url_with_override.
Add test proving outbound relay is captured-scope, not live-state.
4. Generation checks atomic with writes: global_agent_config Phase 1 validates
scope generation inside the store lock before writing config; Phase 2
(restart_local_agent_on_config_change) validates under lock before stop.
collect_restart_candidates renamed to collect_restart_candidates_at with
definitions_dir parameter. Mesh recovery helpers (persist_mesh_last_error_at,
clear_mesh_last_error_if_set_at) take captured_scope and validate generation
inside the store lock.
5. Lock-aware compensation gate: AtomicBool
managed_agent_drain_compensation_in_progress added to AppState.
compensate_drain sets it true (Release) before restarting entries, false
after. start_pair loads it (Acquire) before taking the transition lock and
returns Err if set. Closes the drop-then-compensate interleave window
without recursive locking. Add deterministic partial-drain test.
6. Pre-scope migrations deleted: migrate_agent_keys_to_dev_service (AppHandle
variant) removed from storage.rs. Pre-scope calls removed from
run_boot_migrations_inner. Scoped variants in run_pre_ready_family are
authoritative.
7. Degradation wired to UI: workspace-degraded Tauri event listener added to
useNestNotifications.ts (toast.error with payload as description). False
comment about emit_workspace_degradation removed from event_sync.rs.
backfill_persona_snapshots_at (dead lock-taking wrapper) deleted.
8. e2e test docs: test_two_workspace_relay_partition comment corrected --
Direction 3 asserts len==1 (B's event), not zero. Explicit note added that
this test does not cover desktop workspaces or substitute for the live probe.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
|
||
|
|
c1b88af8d7 |
feat(desktop): improve channel template discovery (#4549)
## Summary - move **Channel templates** from Communities to Personal settings - always expose the template picker in New Channel, using **None** as the no-template value - create a channel template directly from the picker and select it on return - preview the selected template's current visibility, canvas, agents, and teams - order the channel-creation controls as **Type / Visibility / Template** and mark Template **Optional** - cover populated and empty libraries, inline creation, selection, visibility overrides, mixed agent/team inventory, field order, optional labeling, and settings navigation in Playwright ## Validation Validated at desktop-only tip `76442270c88aa1d533ddca5de9f87cd615183919` with a clean worktree: - focused channel-template Playwright: 2/2 passed - Type / Visibility / Template ordering and muted Optional treatment visually inspected in the replacement screenshot - `git diff --check origin/main...HEAD` passed - PR diff contains exactly nine Desktop files and no Mobile files The pre-push hook was bypassed only for the corrected history push because the inherited Mobile test `keeps follow mode off while a tall newest message stays visible` passes in Linux CI but fails on macOS because its offscreen-child mounting assertion is platform-sensitive. No Mobile code or tests are changed by this PR. ## Screenshot  Originating Buzz channel: `efba7343-e147-48b7-a2aa-15a5f04abc57` --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
b0c6d6f744 |
Add channel activity hover menu (#3935)
## Summary - show relevant unread threads and active agents when hovering a channel - keep channel-level unread emphasis separate from thread activity dots - make activity rows navigate to the thread and remove demo-only data ## Test plan - `just ci` (all stages passed except the final duplicate native check, which ran out of disk after its earlier clippy pass) - `cd desktop && pnpm exec playwright test tests/e2e/channel-activity-popover.spec.ts --project=smoke` --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
f865c0054b |
feat(desktop): show saved Run on settings when editing an agent (#4539)
## What When editing an agent, show where it runs. The edit dialog previously showed nothing about the backend; the "Where to run" section only existed in the create flow. This adds a read-only **Run on** section to `AgentInstanceEditDialog`: - **Local agents:** "This computer". - **Provider agents (e.g. Kubernetes):** the provider id plus its saved config rows — context, namespace, image, resources, etc. — with labels humanized from the stored keys and rows in provider-schema order (locators first, request/limit pairs adjacent, alphabetical spillover for unknown providers). - Copy states these are the settings **saved at creation** and that the run location can't be changed afterwards (a new agent is required). ## Design decisions (from thread review with @Wren + @Sami) - **No provider probe on edit.** `info` is executable work, and its schema reflects the plugin *today* (including a freshly generated random namespace default) — not what this agent was deployed with. The stored record is the only honest source. - **Saved settings, not effective settings.** Optional fields a record omits (e.g. `service_account`) are defaulted by the provider at deploy time; we render only what was persisted and never synthesize today's defaults. - **Safe rendering of opaque provider config.** Values render as safe scalars only; arrays/objects degrade to a summary row (React throws on object children — a hand-edited record must not crash the dialog). Falsy-but-present values (`0`, `false`) render honestly. Secret-shaped keys are redacted using the same word-split heuristic as the create-time `validate_provider_config` gate — one definition of "looks like a secret". The gate already blocks such keys on every app write path; display-side redaction is screenshot hygiene and covers hand-edited records. - **`backendAgentId` intentionally excluded:** deploy-time runtime state written on start, not saved creation intent. - **Read-only, no form state.** The backend is immutable post-create (`UpdateManagedAgentRequest` has no backend field), so the section renders straight from `agent.backend` with no reset effect. - `ADVANCED_FIELDS_MOTION_TRANSITION` was duplicated in both agent dialogs; hoisted to `agentConfigOptions` (also keeps the edit dialog inside the file-size ratchet). ## Testing - Unit contract for `summarizeRunOn` (9 tests): scalar honesty incl. `0`/`false`, structured-value fallback, secret redaction fail-safe, preferred ordering with spillover, key humanization. - Playwright spec (4 tests, registered in the smoke project): kubernetes agent with the exact eight-key record a real create flow persisted, local agent, blox agent (`workstation_name`), and redacted secret-shaped keys from a hypothetical future provider. - `pnpm typecheck`, `pnpm check`, full `pnpm test` (3937 pass) green at this head. - Live screenshots posted in the originating Buzz thread. --------- Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> |
||
|
|
5e0efb0bb9 |
fix(desktop): disambiguate provider API key labels and annotate mint key (#4406)
Two different credentials were presented under the same name throughout the app. The top-level credential field for non-Anthropic providers (OpenAI, OpenAI-compatible, OpenRouter) was labeled "OpenAI API Key" via a hardcoded binary ternary repeated in three dialogs. The card-minting key (`OPENAI_API_KEY`) and the runtime credential (`OPENAI_COMPAT_API_KEY`) have independent endpoint namespaces and consumers (`OPENAI_COMPAT_BASE_URL`/`OPENAI_COMPAT_API_KEY` for runtime, `OPENAI_BASE_URL`/`OPENAI_API_KEY` for minting) and must remain separate — either may require a different credential. This PR makes them impossible to confuse in the UI. ## Changes **Provider-accurate labels from the credential table.** `PROVIDER_CREDENTIAL_CONFIG` entries now carry an `apiKeyLabel` paired with `secretEnvVar` as a discriminated union (both present or neither — a future provider cannot ship a secret field with no label). `getProviderApiKeyLabel(providerId)` is the single source of truth. The three hardcoded ternaries in `AgentConfigFields`, `AgentInstanceEditDialog`, and `AgentDefinitionDialog` are replaced by this helper. Labels: `openai` → "OpenAI Runtime API Key", `openai-compat` → "OpenAI-compatible Runtime API Key", `openrouter` → "OpenRouter API Key" (was incorrectly "OpenAI API Key"), `anthropic` → "Anthropic API Key" (unchanged). **Field names its backing env var.** `PersonaProviderApiKeyField` renders the env var name as a monospace hint beneath the label with `aria-describedby` wiring. All three call sites pass their `secretEnvVar`. A user who sees `OPENAI_API_KEY` in the mint dialog can now confirm at a glance that the credential field shows `OPENAI_COMPAT_API_KEY` — a different key. **Signpost visible at the decision point.** `CARD_MINT_KEY_ANNOTATIONS` is exported from `agentConfigOptions.tsx` (single source) and passed as `keyAnnotations` to all three generic env editors: both `EnvVarsEditor` branches in Agent Defaults, `EditAgentAdvancedFields`, and `PersonaAdvancedFields`. `CardMintKeyCue` — a new small component — renders an always-visible muted cue beneath the Advanced toggle when `OPENAI_API_KEY` is present in global env (Advanced is collapsed by default, so the per-row annotation is invisible until the cue guides the user to open it). **Model discovery error copy.** The `OPENAI_COMPAT_API_KEY required` message now reads "Enter an OpenAI runtime API key (OPENAI_COMPAT_API_KEY) to load OpenAI models." — naming the env var explicitly so it cannot be confused with the mint key. ## Tests - `getProviderApiKeyLabel` helper: pinned correct label per provider including the new distinct labels for `openai` and `openai-compat` - `PersonaProviderApiKeyField` render: semantic label present; env-var hint rendered when `envVarName` provided; `aria-describedby` wired to hint id; hint and describedby absent when prop omitted - `EnvVarsEditor` render: annotation appears exactly once on the matching row; absent for non-matching rows - `personaModelDiscoveryStatus`: pinned new copy naming `OPENAI_COMPAT_API_KEY` explicitly - Playwright: stale `"OpenAI API Key"` selectors updated; new `card-mint-key-cue-visible-and-annotation-in-advanced` test covers Will's exact path (databricks_v2 global provider + saved `OPENAI_API_KEY` → cue visible before opening Advanced → annotation present after opening) ## File sizes (post-format) | File | Lines | |------|-------| | `AgentConfigFields.tsx` | 994 (≤ 996) | | `AgentInstanceEditDialog.tsx` | 1228 (≤ 1228) | | `AgentDefinitionDialog.tsx` | 1045 (≤ 1047) | Related: [#4140](https://github.com/block/buzz/pull/4140) --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1ng3jzsaqxdhrfq22dg85j3lpr0zsh3jp7g2h9jyxl59wraayapnsu6kvfg <9a232143a0336e34814a6a0f4947e11bc50bc641f21572c886fd0ae1f7a4e867@buzz.block.builderlab.xyz> |
||
|
|
f810a2f49e |
fix(desktop): make OpenAI key re-enterable after first save in card mint dialog (#4140)
Fixes a write-once dead-end in the card mint dialog where a user with an
expired OpenAI key had no way to replace it.
**Source-aware key status (Rust + TypeScript).** `card_mint_key_status`
returns a layer discriminant (`"none" | "global" | "persona" | "agent" |
"process"`) instead of a boolean. A pure `resolve_key_layer()` helper in
`card.rs` owns the classification logic; `card_mint_key_status`
delegates to it, so the production path is under direct test with no
duplicate logic.
**Mint form always reachable.** The key panel replaces the mint form
only for `none` (first-time setup) or when the user explicitly opens the
edit panel (`editingKey`). Keys from agent/persona/process layers show
an inline provenance row on the mint form with a "Why?" affordance;
clicking it shows the read-only redirect in a panel with a Cancel button
that returns to the mint form — never a terminal state.
**Precise auth-error matching.** The 401 handling in `cardMintStore.ts`
matches `startsWith("Card mint failed (HTTP 401 ")` plus the specific
`Incorrect API key` text, so avatar-fetch 401 errors pass through
unchanged.
**Tri-state key status row.** "Using your saved OpenAI key · Update"
renders only when `keyLayer === "global"` (confirmed writable key).
Query pending or errored hides the row without asserting key existence.
**Real tests.** Panel visibility derivations live in
`cardMintKeyUtils.ts`, which `AgentCardMintDialog.tsx` imports directly.
Tests cover all layers including the mint-reachability invariant (Mint
reachable for every resolved layer; only `none` gates setup).
- `card.rs` — new `resolve_key_layer()` pure helper;
`card_mint_key_status` delegates to it; 999 lines (under the 1000-line
ratchet)
- `card/tests.rs` — precedence test calls `resolve_key_layer()` directly
(no test-local closure); adds process-layer and blank-value cases
- `tauriPersonas.ts` — `CardMintKeyLayer` type; updated
`cardMintKeyStatus` signature
- `cardMintKeyUtils.ts` — `showKeyPanel`, `showReadOnlyRow`,
`showCancelButton`, `keyPanelTitle`, and helpers; component imports all
of them
- `AgentCardMintDialog.tsx` — inline provenance rows for all key
sources; key panel only for setup/edit; no unused variables
- `cardMintStore.ts` — precise 401 prefix matching
- `e2eBridge.ts` — `card_mint_key_status` stub returns `"global"` (not
boolean)
- Tests: 3959 JS passing, 2089 Rust passing, `tsc --noEmit` clean
Related: [block/buzz#4406](https://github.com/block/buzz/pull/4406)
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1ng3jzsaqxdhrfq22dg85j3lpr0zsh3jp7g2h9jyxl59wraayapnsu6kvfg <9a232143a0336e34814a6a0f4947e11bc50bc641f21572c886fd0ae1f7a4e867@buzz.block.builderlab.xyz>
|
||
|
|
be95a8a986 |
fix(config-bridge): add harness-definition env tier and fix equal-value model override (#3580)
All seven normalized config fields resolve through sanitized
`InheritedConfigTiers` passed wholesale to `read_config_surface`. The
reader's precedence tiers now match spawn's Layer 2b exactly — including
harness-definition env — and the equal-value model-override regression
is fixed.
## Changes
**`config_bridge/types.rs`** — add `InheritedConfigTiers`: persona env,
global env, harness definition env, structured model/provider/prompt for
both tiers. Add `HarnessDefault` `ConfigOrigin` variant for
harness-definition env values.
**`commands/agent_config.rs`** — `build_inherited_tiers` now resolves
the harness definition env using the same lookup path as spawn
(`record.runtime` → `persona.runtime` → empty string) and applies
`sanitize_inherited_env` to it. `resolve_config_surface` is unchanged in
shape — tiers passed to the reader now include `definition_env`.
**`config_bridge/reader.rs`** — `env_candidates` extended to 4-element
return (record, persona, global, definition). All five field builders
that use env candidates now include the definition-env slot below global
env and above the structured block, matching spawn Layer 2b. Magic
`configured[..6]` slice replaced with `configured[..configured.len()-1]`
(named split: all non-file candidates). Equal-value model-override arm
falls through to the normal resolve path instead of early-returning
`RuntimeOverride`, so the panel shows the baseline origin (e.g.
`BuzzExplicit`) rather than a spurious "Live override" label for a no-op
switch.
**`config_bridge/reader_tests_ext.rs`** — three new Layer 2b tests:
definition env beats structured persona model, global env beats
definition env, reserved-key-absent fallthrough.
**`commands/agent_config_tests.rs`** —
`genuine_explicit_live_switch_to_same_model_yields_clean_field` updated
to assert `origin == BuzzExplicit` (not `RuntimeOverride`); wrapped in
`with_no_goose_config` for hermeticity. New
`reserved_key_in_definition_env_shaped_map_is_stripped_by_sanitize` test
pins the shared sanitization contract.
**`AgentConfigPanel.tsx` / `types.ts`** — `HarnessDefault` origin
variant wired end-to-end: TS union type and provenance sentence
("Inherited from harness definition").
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
2c0ac24674 |
fix(desktop): stop the create-agent provider config probe from erasing keystrokes (#4411)
## What
Fixes the create-agent dialog's "Run on" provider config fields eating
keystrokes — reported by Tyler in buzz-remote-agents (channel
`29414326`, thread `db76677a`): the Kubernetes **Kubeconfig context**
field would not accept typing.
## Why it happened (the Typewriter Eraser, shipped in #4289)
`WhereToRunSection`'s probe `useEffect` depended on the whole `draft`:
1. every keystroke changed the draft → effect re-fired → provider binary
re-probed;
2. each probe result is a fresh object written into the draft → the
effect re-triggered **itself**, respawning the provider binary in a loop
for as long as the dialog sat on a provider;
3. every probe resolution reset `providerConfig` to schema defaults —
erasing whatever was typed. A field with no schema default (`context`)
snapped back to empty, i.e. "won't let me type". Unrelated to how many
kubeconfig contexts you have.
## Fix
- **Probe once per provider selection**, keyed on the provider's stable
`binaryPath` — not the draft, not the provider object (a
`useBackendProvidersQuery` refresh must not reprobe an unchanged
selection).
- **Latest-state resolution** via `React.useEffectEvent` + a new pure
`applyProbeResult` helper: schema defaults merge **beneath** the current
`providerConfig`, so a probe landing after the user typed can never
clobber in-flight input (per Wren's pre-patch red-team: changing deps
alone leaves a stale closure).
Existing `cancelled` cleanup keeps provider-switch/unmount safe;
selection reset (`emptyWhereToRunDraft`) and the fail-closed probe-error
path are unchanged.
## Tests
- **Unit** (`whereToRunIntent.test.mjs`): `applyProbeResult` merge
semantics — defaults under typed values, user-cleared fields stay
cleared, schema-less results, unrelated fields preserved.
- **E2E** (new `where-to-run-config.spec.ts`, added to the smoke
project, **red-first verified**: all 3 fail against the unfixed
component):
- typing into a defaultless provider field sticks, and
`probe_backend_provider` fires exactly once per selection;
- the config form is gated on probe resolution (slow probe: no
half-rendered form, defaults prefill once);
- provider → local → provider re-probes and resets cleanly.
- Mock bridge gains `backendProviders` / `backendProviderProbeResult` /
`backendProviderProbeDelayMs` seams (defaults preserve prior behavior).
## Verification at
|
||
|
|
3ea3d70cfe |
fix(desktop): use null-coalescing fallback in AppShell composite reconcile key
Without the '?? "none"' fallback, a null activeCommunity?.id produces the string "undefined-N" rather than "none-N". Both are valid change signals, but the explicit fallback matches the existing pattern at line 233 of the same file and satisfies Thufir's pass-2 finding C7. The line expands to 3 lines after biome formatting (88 chars), landing AppShell.tsx at exactly 1000 gate-counted lines — still within the 1000-line ratchet (gate condition is > 1000). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
372b0fabd9 |
fix(desktop): pass-2 correction C7 — degraded results observable + AppShell composite key
spawn_event_sync returns Result<(), String> so dispatch failure can be captured
by the workspace-apply post-commit section rather than being silently ignored.
The value is always Ok(()) since tauri::async_runtime::spawn is infallible; this
establishes the typed interface for future signaling.
try_regenerate_nest returns Result<(), String> instead of swallowing errors.
All fire-and-forget callers updated to .ok() to explicitly discard the Result.
apply_workspace post-commit:
- try_regenerate_nest moved out of the spawn_blocking closure into the async
post-commit section so its Result can populate the degraded vec.
- spawn_event_sync Result captured; dispatch failure pushed to degraded.
- Nest failure reported as 'nest context regeneration failed: ...' degradation.
useCommunityInit.ts: post-commit degraded items now emit a toast.warning (8 s)
via sonner so the user sees partial failures. Previously only console.warn.
AppShell.tsx: useManagedAgentRuntimeReconciliation key changed from
String(reinitKey) to `${activeCommunity?.id}-${reinitKey}`. A same-relay
identity swap (new communityId, unchanged reinitKey) now correctly re-triggers
runtime reconciliation. Destructured activeCommunity and reinitKey from
communitiesHook and updated two other call sites for consistency.
dead pub use exports in migration.rs removed (fold, backfill, detach, strip,
materialize — all now accessed only through scoped _in_dir/_at variants).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
|
||
|
|
370de88dac |
chore(desktop): trim file-size ratchet violations to pass desktop-check
Nine files were over the gate limit after the workspace-scoped store implementation. Trims/extractions to get all under limit: - app_state_tests.rs: move scope lifecycle tests to app_state_scope_tests.rs - app_state.rs: move pending_owned_channels methods to identity_storage.rs - AppShell.tsx: inline reconciliation key as String(reinitKey) (1 line vs 3) - tauri.ts: type alias ApplyWorkspaceResult + biome-ignore format to keep the applyCommunity body under the limit - runtime.rs: restructure scope capture to save a net line - storage.rs: trim doc comments on _at variants to single-liners - mesh_llm.rs: make scope_impl pub(crate) mod; fold scope relay capture inside check_mesh_runtime_relay_scope; remove verbose comments - migration.rs: extract scoped migration helpers to migration_scope.rs via include!(); trim SHARED_AGENT_FILES/DIRS block comments - migration_tests.rs: trim explanatory comments to save net 33 lines Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
91859ea806 |
feat(desktop): plumb WorkspaceApplyResult through frontend boundary (Phase 3e)
Surfaces the degraded-apply result from the backend all the way to the UI:
tauri.ts:
- Add WorkspaceApplyResult interface { applied: boolean; degraded: string[] }
- applyCommunity() now returns Promise<WorkspaceApplyResult> instead of
Promise<void>; passes typed result through from apply_workspace command.
useCommunityInit.ts:
- Consumes applyResult from applyCommunity instead of discarding void.
- applied: false (drain-failed) → park on the loading gate so the user
can retry via a workspace switch; the specific degradation messages are
shown as the error.
- applied: true with degraded entries → console.warn (informational;
workspace IS active; post-commit step failed gracefully).
- Existing catch block still handles genuine Tauri errors (poisoned lock,
invalid nsec, etc.) unchanged.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
|
||
|
|
00a3381902 |
feat(desktop): implement Phase 3 runtime ownership + Mesh scope rules
Phase 3 of workspace-scoped agent store:
3a: reconcile_managed_agent_runtimes loses its `communities` parameter.
The backend derives the sole target relay from the captured active scope;
cross-scope fan-out is no longer representable at the API level.
- runtime_commands.rs: capture active scope relay; remove communities Vec
- runtime_types.rs: remove ManagedAgentCommunityTarget struct
- tauriManagedAgents.ts: reconcileManagedAgentRuntimes() takes no args
- managedAgentRuntimeHooks.ts: bootstrapManagedAgentRuntimePairs calls
parameterless reconcile; drop communities list construction
- useManagedAgentRuntimeReconciliation.ts: rewritten to track a single
activeCommunityKey instead of per-relay state; simplified retry logic
- AppShell.tsx: pass `${activeCommunity?.id}-${reinitKey}` as the key
3b: Mesh relay-match reuse rule + fail-closed serve preflight + watchdog.
- mesh_llm.rs: ensure_relay_mesh_for_record captures scope relay at entry;
a live runtime is only reused when its relay matches the scope relay;
serve-mode mismatch fails closed with a precise 'Share Compute is
currently pinned to <relay>' error; client-mode mismatch falls through
to re-arm; drain_mesh_client_if_stale drains a client whose relay
differs from the incoming workspace relay (Layer-1 async, non-fatal).
- recovery.rs: rearm_relay_mesh_for_running_agents captures one scope per
pass; Live early-return only taken on relay match; serve-mode Live
mismatch skips the pass (machine-level pinning).
- personas.rs: add scoped load_personas_at / save_personas_at variants.
3c: Drain journal + compensation + apply_workspace rewrite.
- runtime_commands.rs: DrainJournalEntry struct, drain_scope_runtimes
(snapshot journal + stop all live runtimes, returns stopped/remaining/
first_error), compensate_drain (restart exactly the stopped entries).
- workspace.rs: apply_workspace return type changed from () to
WorkspaceApplyResult. Layer-1 async drains the Mesh client before
spawn_blocking. Drain stage acquires managed_agent_runtime_transition,
calls drain_scope_runtimes; on failure calls compensate_drain and
returns applied:false. Per-transition restore replaces the launch-only
managed_agent_restore_pending one-shot. Post-commit failures (event
sync, restore) surface as degraded entries on WorkspaceApplyResult.
3d: Scope-tagged runtime map entries.
- runtime_types.rs: ManagedAgentPairRuntime gains scope_id: Option<String>
- starting() constructor takes scope_id; captured from active scope at
spawn time in runtime_commands.rs, restore.rs, and runtime.rs.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
|
||
|
|
7ff5fc3189 |
feat(acp): deliver system prompt via _meta.systemPrompt for claude-agent-acp (#4395)
`claude-agent-acp` (since v0.6.0 / PR #91) accepts `_meta.systemPrompt: {append: text}` on `session/new` to append to the adapter's native preset while keeping its tool-use prompt intact — the same non-standard extension pattern as `_session/steering` was before it was standardised. ## What changes **Rust (`crates/buzz-acp/`)** - Adds `SystemPromptTransport` enum to `acp.rs`: `Field(&str)` (ACP protocol v2, unchanged) vs `ClaudeMeta(&str)` (new `_meta.systemPrompt: {append: text}`). When both `ClaudeMeta` and `session_title` are present the two `_meta` members are merged into one object so neither clobbers the other. - Gates on exact adapter identity `@agentclientprotocol/claude-agent-acp` in `pool.rs`: `session_new_system_prompt()` routes that name to `ClaudeMeta` regardless of reported `protocolVersion` (CC declares v1). `has_system_prompt_support()` gains the same name check so user-message `[Base]`/`[System]` framing is suppressed for CC sessions. - All other paths — goose post-hoc method, protocol-v2 `Field`, legacy user-message framing — are byte-identical to before. **Desktop (`desktop/src/features/agents/ui/`)** - `agentSessionTranscript.ts`: the `session/new` extractor now checks `params._meta.systemPrompt.append` as a fallback when bare `params.systemPrompt` is absent. Bare field takes precedence. Net line count stays at 1173 (ratchet limit). - `agentSessionTranscript.test.mjs`: two new tests — one verifying the `_meta` transport produces the identical standalone card (same five sections, same `turnId: null`, same placement before the first turn) as the bare-field transport; one proving bare field wins when both transports are present. ## Gate claim `@agentclientprotocol/claude-agent-acp` implies `_meta.systemPrompt` support because the feature landed in v0.6.0 (Oct 2025, commit `ea796f3`) before the `@zed-industries/claude-code-acp` → `@agentclientprotocol/claude-agent-acp` package rename (Mar 2026, commit `b409782`). The new name is therefore a reliable capability gate; the old name falls through to the protocol-version gate (status quo, no regression). ## Tests - Rust: Claude append serialization; `_meta` coexistence with `sessionTitle`; protocol-v2 bare field byte-identical; codex/old-zed omission; claude-name support/suppression gate; old `@zed-industries` name falls through to protocol-version gate. - Desktop: `_meta` transport → identical standalone card; bare field wins over `_meta` when both present. ## Pre-existing failures `just mobile-check` and `just mobile-test` fail identically on clean `origin/main` (5 `compose_bar` / `channels_page` tests + 3 Flutter lint warnings) — not caused by this change. All other `just ci` jobs are green. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
f86cfc7369 |
fix(desktop): back/forward via keyboard chords, mouse X1/X2 buttons, and swipe gestures (#3778)
## Problem Two related gaps in global back/forward navigation. Fixes #3775. 1. The keyboard shortcuts almost never fire in real use — users fall back to clicking the toolbar chevrons and assume the shortcuts don't exist. 2. On macOS, mouse back/forward buttons (X1/X2) and horizontal swipe gestures do nothing, although they navigate in every browser and in Slack. **Duplicate check:** searched open PRs and issues — none found beyond #3775 (filed alongside this fix). #3078 / #3377 are next/previous-*channel* navigation, a different feature. ## Root causes **Keyboard:** `useBackForwardControls`'s keydown handler bailed whenever the event target was editable — but `useComposerAutofocus` deliberately focuses the message composer (a ProseMirror contenteditable) on mount and on every channel switch. In steady state focus almost always lives in the composer, so the chords were silently swallowed. Invisible to CI because `navigation.spec.ts` only ever clicked the `global-back` / `global-forward` buttons, never pressed the keys. **Mouse/swipe:** on macOS, WKWebView never delivers X1/X2 button events or swipe gestures to the page (Safari handles them natively in the app layer, not in page JS), and Buzz had no native handler. ## Fix ### Keyboard chords (web layer) Match the existing platform chord regardless of the event target and drop the editable-target guard: - `⌘[` / `⌘]` have no text-editing semantics in macOS text fields, and the TipTap/StarterKit editor config binds no `Mod-[` / `Mod-]` shortcuts (checked `useRichTextEditor.ts` — list indentation is Tab/Shift-Tab). - `preventDefault()` keeps the chord out of the editor — asserted in the e2e test. This matches browsers and Slack, where back/forward chords work while a text field is focused. Chord matching is extracted into a pure helper, `app/navigation/backForwardChords.ts`, so it can be unit tested; behavior (bindings, modifier exclusivity, `code`-based matching for non-US layouts) is unchanged. ### macOS mouse buttons and swipe gestures (native layer) An NSEvent local monitor in `mouse_nav.rs` catches what the webview can't see and emits a `mouse-nav` Tauri event to the main window (`emit_to`, so navigation stays scoped if multi-window ever lands) that the frontend acts on. Two AppKit event shapes map to navigation: - `otherMouseUp` with button 3/4 — mice whose X1/X2 buttons arrive as plain button events. These are swallowed after emitting so nothing downstream double-handles them. - `swipe` with a horizontal delta — AppKit's page-swipe gesture (`swipeWithEvent:`): `deltaX > 0` back, `deltaX < 0` forward. Sent by mouse drivers that synthesize a page-swipe gesture for the back/forward buttons instead of button-3/4 events (the hardware this was verified on). Stock Apple trackpad and Magic Mouse swipes arrive as phased scroll-wheel events instead, which this PR does not handle — that path (`ScrollWheel` + `trackSwipeEventWithOptions:`, which also needs scroll-edge detection) is deferred to a follow-up. Swipes are passed through (swallowing mid-gesture events could confuse AppKit gesture tracking). The swipe path was verified end to end on hardware whose back/forward buttons emit only swipe gestures, never button-3/4 events — an instrumented event monitor confirmed the events arrive as `NSEventType::Swipe` with `deltaX ±1`, and navigation worked after mapping them. ## Tests - **13 unit tests** for the web-side chord matcher (`backForwardChords.test.mjs`): supported chords, modifier exclusivity, `code` fallback, and preservation of line-editing shortcuts. - **6 Rust unit tests** for the native mapping helpers (`mouse_nav.rs`): button 3/4 directions, other buttons ignored, swipe delta sign → direction, zero-delta (gesture-begin) ignored. - **e2e regression case** in `navigation.spec.ts`: presses the platform chord *while the composer is focused* — the missing coverage. Verified it fails against the pre-fix implementation and passes with the fix. - Full desktop unit suite: 3832/3832 pass. Full Rust suite (`cargo test`, buzz-desktop): 1888 passed / 0 failed. `pnpm typecheck`, `biome check`, `pnpm check`, `cargo fmt --check`, `cargo clippy`: clean (no new warnings). - Full Playwright e2e: 958 passed; 6 failures are relay-infrastructure tests (live relay seeding / relay state seam) that fail identically without this change — `navigation.spec.ts` is fully green. ## Manual test 1. Open a channel, then another (composer autofocuses on each switch). 2. `⌘[` — returns to the previous channel; `⌘]` — forward again. Typing `[` / `]` in the composer inserts normally. 3. Mouse back/forward buttons navigate the same way, from anywhere in the window (verified on macOS on hardware using both event shapes). ## Update — 2026-07-31 Removed the redundant DOM mouse-button handler after verifying it was unnecessary. The native macOS path remains unchanged and was revalidated manually. --------- Signed-off-by: npub1yvnq5equak5errqpku8stskushny9wsvt0fc2ywcpwt79yslwaqswe7tse <23260a641ceda9918c01b70f05c2dc85e642ba0c5bd38511d80b97e2921f7741@buzz.block.builderlab.xyz> Signed-off-by: Matheus Iser <matheusiser@squareup.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1yvnq5equak5errqpku8stskushny9wsvt0fc2ywcpwt79yslwaqswe7tse <23260a641ceda9918c01b70f05c2dc85e642ba0c5bd38511d80b97e2921f7741@buzz.block.builderlab.xyz> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
6530b58a61 |
feat(k8s): Kubernetes backend plugin + desktop deploy path (#4289)
# Kubernetes backend plugin (crates/buzz-backend-kubernetes) + desktop deploy path Implements docs/remote-agents.md (merged @ |
||
|
|
8e81afa431 |
fix(desktop): keep thread-open affordance in archived channels (#4012)
## Problem
Threaded replies "disappeared" from archived Buzz channels: the **"N
replies →"** summary row and the huddle-started **"View thread"** button
vanished, so existing threads were unreachable from the channel
timeline. The thread data was intact — this was a UI gate, not data
loss.
## Root cause
A single `onReply` prop drove two distinct affordances:
- the **compose** affordances (hover "Reply" button, inline reply
target), and
- the **view** affordances ("N replies →" summary row, huddle "View
thread").
`ChannelPane` nulls `onReply` on archived channels to keep them
read-only. That correctly hid composing — but also hid the view
affordances, since they keyed off the same prop.
## Fix
Two independent props, one per concern:
- **`onReply`** drives the compose affordances and is gated on
`archivedAt` — nulled on archived channels, so no new replies can be
started.
- **`onOpenThread`** drives the view affordances and is passed
regardless of archived state, threaded `ChannelPane → MessageTimeline →
TimelineMessageList → MessageRow`.
Opening a thread on an archived channel is read-only: the thread panel's
composer is independently gated via `isComposerDisabled` (includes
`archivedAt !== null`, `ChannelPane.tsx:318`).
### Before
<img width="811" height="794" alt="Screenshot 2026-07-31 at 20 26 00"
src="https://github.com/user-attachments/assets/670d9db4-30da-4c6d-97dc-275b5dbebca8"
/>
### After
<img width="873" height="791" alt="Screenshot 2026-07-31 at 20 28 04"
src="https://github.com/user-attachments/assets/88525231-2539-4eb3-8117-8e58a0cb3855"
/>
## Validation
- `pnpm typecheck` clean
- biome lint clean on touched files
- full `pnpm test` suite green (3885 tests)
- pre-push `branch-skew` / `desktop-check` / `desktop-test` hooks passed
Signed-off-by: Trey Wood <treyw@squareup.com>
Co-authored-by: npub14h0tw3uj7jm77qfxcwn6um2s5h55l0klrt2w9srzp3m3yvjc0mpsjsuk6e <addeb74792f4b7ef0126c3a7ae6d50a5e94fbedf1ad4e2c0620c771232587ec3@buzz.block.builderlab.xyz>
|
||
|
|
45314fc504 |
fix(desktop): preserve first huddle speech (#3962)
## Context
On the first huddle after launching Buzz Desktop, a live agent reply can
arrive after agent membership is known but before the initial
TTS-enabled state has loaded. The subscription previously released
buffered messages at the membership boundary, so that first reply was
evaluated while speech was still disabled and was silently skipped.
Later replies worked, and later huddles usually worked because the state
was already warm.
## Summary
Hold initial live agent replies until both authoritative agent
membership and the initial TTS state are known. This preserves the first
eligible reply after a cold app launch without changing live-only
routing, ordering, or fail-closed behavior.
## Changes
- Replace the membership-only startup gate with a two-signal readiness
gate for membership and TTS state.
- Release buffered live messages in arrival order only after both
signals resolve.
- Drop buffered messages if either initial lookup fails.
- Add a deterministic regression for the observed ordering: membership
resolves first, TTS enables second, and the first reply is spoken.
## Related issue
None found.
## Testing
Manual validation in the daily-driver build confirmed that the first
agent reply is spoken in the first huddle after a fresh app launch.
The regression scenario was also run against both revisions:
```text
main: FAIL — actual spoken replies: []; expected: ["first agent reply"]
PR: PASS — 10 passed, 0 failed
```
## Screenshots
N/A, nonvisual speech behavior.
## Reviewer-reproducible examples
1. Quit Buzz Desktop completely.
2. Reopen it with Pocket TTS enabled.
3. Start the first huddle of the session with a running agent.
4. Send a prompt that produces a spoken agent reply immediately after
the huddle starts.
5. Confirm the first reply is spoken, not only the second reply.
6. Stop the huddle, start another one, and confirm subsequent huddles
retain the same behavior.
For a deterministic red/green check, run the same
membership-before-TTS-state ordering from `desktop/`.
On `main`:
```bash
node --import ./test-loader.mjs --experimental-strip-types --input-type=module -e '
import assert from "node:assert/strict";
import { createInitialMembershipGate, createOrderedSpeaker } from "./src/features/huddle/lib/ttsLiveMessages.ts";
const spoken = [];
const speaker = createOrderedSpeaker(async text => spoken.push(text), error => { throw error; }, false);
const gate = createInitialMembershipGate(text => speaker.enqueue(text, 1));
gate.push("first agent reply");
gate.succeed();
speaker.setEnabled(true);
await new Promise(resolve => setTimeout(resolve, 0));
console.log("spoken:", JSON.stringify(spoken));
assert.deepEqual(spoken, ["first agent reply"]);
'
```
Observed failure:
```text
spoken: []
AssertionError: Expected values to be strictly deep-equal
```
On this PR branch:
```bash
node --import ./test-loader.mjs --experimental-strip-types --input-type=module -e '
import assert from "node:assert/strict";
import { createInitialTtsReadinessGate, createOrderedSpeaker } from "./src/features/huddle/lib/ttsLiveMessages.ts";
const spoken = [];
const speaker = createOrderedSpeaker(async text => spoken.push(text), error => { throw error; }, false);
const gate = createInitialTtsReadinessGate(text => speaker.enqueue(text, 1));
gate.push("first agent reply");
gate.markMembershipKnown();
speaker.setEnabled(true);
gate.markTtsStateKnown();
await new Promise(resolve => setTimeout(resolve, 0));
console.log("spoken:", JSON.stringify(spoken));
assert.deepEqual(spoken, ["first agent reply"]);
'
```
Observed output:
```text
spoken: ["first agent reply"]
```
---------
Signed-off-by: John Tennant <jtennant@squareup.com>
|
||
|
|
eb049ddf81 |
feat(desktop): Agent Trading Cards — mintable agent-snapshot card PNGs with optional NIP-44 lock (#3278)
## Agent Trading Cards "Create Agent Card" action in the agent panel that mints an AI-generated trading card PNG which **is** the agent: the card carries the `buzz_agent_snapshot` tEXt chunk and is drag-in importable like any snapshot PNG. ### What's in here - **Mint pipeline (Rust):** one OpenAI Responses call — `gpt-5.6-sol` as card designer with `gpt-image-2` via the `image_generation` tool (~2–3 min). New `mint_agent_card` / `save_agent_card` commands; preview with reroll; save or send as `.agent.png` with round-trip verification before any bytes leave the app. - **Snapshot/chunk work stays in Rust,** reusing the existing encoder/decoder seams (byte-compat golden vector proves the plain path is identical to the pre-envelope encoder for placeholder, PNG-injection, and JPEG-transcode paths). - **Locked cards (NIP-44):** optional `buzz-agent-snapshot-encrypted` envelope encrypted to the (owner, agent) pair. `parse_canonical_pubkey` performs lift-x curve validation before any API spend; wrong-key decrypt returns a fixed refusal; the plain decoder refuses locked cards. - **Guardrails:** 10 MiB ceiling on final bytes, memory structurally `none` in the snapshot, full-manifest import disclosure, API-key hygiene via env layering (record > persona > global > process), fail-early validation ordering (all key/lock/NIP-44-cap checks before Responses spend). - **Import side:** full-manifest disclosure dialog, locked-card import disclosure, bounded avatar fetch. ### Review Code reviewed by Wren across the full arc; final locked-card cross-review **APPROVED 9/9/9** at exactly this head (`64f819dc8`), with independent same-SHA verification: Rust lib 1,843/1,843, clippy `--all-targets -D warnings`, desktop file-size gate. ### Live-mint evidence (real API, shipping seams, this SHA) - **Plain (Honey):** 188s, 1500x2250, 5,101,503 bytes (< 10 MiB); decoded manifest == built manifest; memory=none. - **Locked (Fizz):** 176s, 4,670,184 bytes; owner-key and agent-key decrypt both verified via logical manifest compare; wrong-key refusal exact; plain decoder refuses. - **Live finding:** built-in agents' ~171 KB inline avatars exceed the NIP-44 65,535-byte plaintext cap and the fail-early guard fires before API spend — clean error path, noted as a UX follow-up for large-avatar agents choosing lock. Full evidence (cards + dialog screenshots) posted in the originating thread. --------- Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> |