feat(holon): entity DNA, place-safe multi-host agents (Remote Agents + R0–R5) - #1
feat(holon): entity DNA, place-safe multi-host agents (Remote Agents + R0–R5)#1Trevongit wants to merge 74 commits into
Conversation
…g keystrokes (block#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 block#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 8eb7680 - `pnpm check` + `tsc` clean, `pnpm test` 3926/3926; - new spec 3/3 green (and 3/3 red on the unfixed component); - pre-push lefthook: desktop-test, desktop-check, desktop-tauri-checks, rust-tests, mobile-test all green. --------- Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
…block#4524) ## Summary Official Linux desktop packages (`.deb` / AppImage) are built without `--features mesh-llm`, so they ship the `mesh_llm_stubs` backend and Settings → Compute always fails with `mesh-llm feature not enabled`. This PR adds the feature flag to the two Linux build commands: - `release.yml` → `release-linux` job - `linux-canary.yml` → canary build That's the whole diff — 2 lines. Fixes block#3788 (Linux); see also block#3841 (dup with UI-gating PR block#3914) and the Windows twin block#2836/block#3223. ## Why no native prebuild step (unlike the macOS job) The macOS job carries Metal llama prebuild/cache steps from block#798. Linux doesn't need an equivalent: - `mesh-llm-host-runtime` is compiled with `dynamic-native-runtime` and installs the recommended runtime on first use (verified by sha256 checksum over HTTPS; upstream's signature verification path is not yet implemented — default policy is `RequireChecksum`, per `mesh-llm-runtime-install/src/lib.rs`) (`desktop/src-tauri/src/mesh_llm/mod.rs` — `initialize_mesh_native_runtime`), so release builds work on clean machines without bundling llama.cpp. - Upstream publishes Linux x86_64/aarch64 runtime bundles for the pinned `v0.74.0` line, and `scripts/ensure-mesh-native-runtime.sh` already maps `meshllm-native-runtime-linux-x86_64-cpu` / `linux-aarch64-cpu` for local/e2e use. - The unmerged branch `micn/mesh-node-download` (`96f29417a`) treats even the macOS prebuild steps as removable dead weight for the same reason. ## Background The omission is historical drift, not a decision: Linux packaging predates the mesh feature flag (block#693), mesh became opt-in for build-cost/reliability reasons (block#823, block#1183), and block#1221 re-enabled it for releases by editing only the macOS build line. `release-linux` and the later `linux-canary` copy were never revisited. The mesh shutdown hard-exit/relaunch path is gated `all(mesh-llm, target_os = "macos")` because ggml/Metal destructors abort on macOS; ordinary mesh shutdown (`shutdown_mesh_runtime`) is cross-platform, so Linux falls through to the generic path. ## Validation - [x] `./bin/cargo check --manifest-path desktop/src-tauri/Cargo.toml --features mesh-llm` green at base `2c0ac2467` (feature graph compiles at the pinned v0.74.0 line) - [ ] Linux canary run with this change: AppImage/.deb build succeeds and binary contains real `mesh_llm` symbols (not `mesh_llm_stubs`) - [ ] Installed package: cold-start → Settings → Compute → runtime download → serve → clean shutdown The last two need a Linux run/host. **Note (from review):** `linux-canary.yml` is `workflow_dispatch`-only and its `Require main` step rejects non-main refs, so the canary cannot run on this branch pre-merge — and `.github/workflows/**` matches no ci.yml paths-filter, so this PR's own CI does not exercise the changed lines. Validation sequencing is therefore merge → dispatch linux-canary on main → live-package pass, with a trivial 2-line revert as the escape hatch. Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
## Summary
- Refine the mobile composer with compact and expanded states, shared
footer fades, haptics, reliable keyboard dismissal, and full-width
camera and photo surfaces.
- Standardize popovers, filters, and section menus with consistent type,
strokes, radii, spacing, icons, and destructive styling.
- Align message presentation with desktop through consistent system
rows, typing and loading feedback, emoji placement, and predictable
photo viewing.
## Validation
- `just mobile-check`
- `just mobile-test` — 1,037 passed, 1 skipped
- Tested on Pixel 10 and a connected iPhone
## Snapshots
<table>
<tr>
<td align="center">Compact composer</td>
<td align="center">Attachment menu</td>
<td align="center">Recent photos</td>
</tr>
<tr>
<td><img
src="https://raw.githubusercontent.com/block/buzz/9732022cb13bb39ce797c4faaa714fe4c924955f/pr-3918--01-compact-composer.png"
width="260" /></td>
<td><img
src="https://raw.githubusercontent.com/block/buzz/9732022cb13bb39ce797c4faaa714fe4c924955f/pr-3918--02-attachment-menu.png"
width="260" /></td>
<td><img
src="https://raw.githubusercontent.com/block/buzz/9732022cb13bb39ce797c4faaa714fe4c924955f/pr-3918--03-photo-surface.png"
width="260" /></td>
</tr>
</table>
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
…ue model override (block#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>
…int dialog (block#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#4406](block#4406) --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1ng3jzsaqxdhrfq22dg85j3lpr0zsh3jp7g2h9jyxl59wraayapnsu6kvfg <9a232143a0336e34814a6a0f4947e11bc50bc641f21572c886fd0ae1f7a4e867@buzz.block.builderlab.xyz>
…key (block#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: [block#4140](block#4140) --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1ng3jzsaqxdhrfq22dg85j3lpr0zsh3jp7g2h9jyxl59wraayapnsu6kvfg <9a232143a0336e34814a6a0f4947e11bc50bc641f21572c886fd0ae1f7a4e867@buzz.block.builderlab.xyz>
…k#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>
## 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>
**Category:** fix **User Impact:** Users can save password-protected identity backups directly to protected macOS folders such as Downloads. **Problem:** Signed macOS builds could not save a portable `.ncryptsec` backup to Downloads because the atomic writer created an unauthorized sibling temporary file. This surfaced as an “Operation not permitted” error after the user completed backup creation. **Solution:** Portable exports now write only to the exact path authorized by the native Save panel, sync and verify the saved bytes, and refuse to truncate an existing backup. Buzz’s app-managed backup retains its atomic writer and durability guarantees. <details> <summary>File changes</summary> **desktop/src-tauri/src/commands/export_util.rs** Clarifies that secret exports use a dedicated writer compatible with native Save-panel authorization. **desktop/src-tauri/src/commands/identity.rs** Routes portable NIP-49 exports through the Save-panel-compatible writer while preserving canonical app state. **desktop/src-tauri/src/key_backup.rs** Adds an exclusive-create portable writer with owner-only permissions, disk sync, byte verification, and cleanup on failure. Keeps the existing atomic writer for app-managed backups. **desktop/src-tauri/src/key_backup_tests.rs** Covers portable export permissions, absence of sibling files, and preservation of existing backups. </details> ## Reproduction steps 1. Install a signed macOS build containing this change. 2. Open **Settings → Profile → Private key → Create backup** and complete backup creation. 3. Save a fresh `identity.ncryptsec` file into `~/Downloads` and confirm Buzz reports success. 4. Open and verify the saved backup with its password. 5. Repeat the save using an existing filename and confirm Buzz preserves the existing file and asks for a new filename. ## Verification - Full desktop Tauri suite: 2,049 passed, 14 ignored - Diagnostic suite: 3 passed - Focused backup coverage: 30 passed - Tauri clippy (`--all-targets -D warnings`), Rust formatting, and `git diff --check`: passed - Push hooks: org safety, branch skew, and desktop Tauri checks passed Signed-production Downloads smoke remains required after merge because the signing workflow is restricted to `main`. Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
## 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>
…aned Node (block#4382) This PR fixes two Windows-specific install failures: Windows Defender blocking the bare `irm|iex` PowerShell install command, and managed Node shims pointing at a version-bumped (now-absent) Node directory. The Defender block (Trojan:Win32/Commando.A!ml) fires before PowerShell runs and is not clearable via Allow. The Node orphaning means shims in the managed npm prefix resolve but fail at runtime with 'node not recognized' because they reference the deleted old Node path. - Replace all three Windows CLI install commands (Goose, Claude, Codex) with a two-step shape — `Invoke-RestMethod` to a named temp file, then execute — to eliminate the dropper signature; a new `windows_install_command!` macro in `discovery/windows_install.rs` generates all three strings at compile time so the shape cannot drift between runtimes - `$ErrorActionPreference='Stop'` aborts on download failure instead of falling through to a missing-file exit-0; `exit $LASTEXITCODE` propagates the vendor script's own exit code - Add `probe_node(executable, expected_version, timeout)` as a bounded seam: stdout goes to a temp file (not a pipe) so no exit path can block on an inherited handle; the child runs in its own process group on Unix so an unconditional group SIGKILL on every exit path terminates all descendants; on Windows `taskkill /T /F` provides the same tree-wide cleanup; `managed_node_runtime_ready()` is a thin wrapper that resolves the managed Node path and calls the seam - Add `resolve_adapter_path()` in `managed_node.rs`: resolves the candidate first, then calls `should_invalidate_adapter()` — a pure predicate that returns `true` only when the resolved path is under `buzz_managed_npm_bin_dir()` AND the managed Node runtime is orphaned; external adapters outside the managed prefix are always preserved Note: CI cannot reproduce the Defender block (no live Defender ML classifier). Proof of fix is structural — the command shape no longer matches the dropper signature. Canary validation on a real Windows machine with Defender enabled is the definitive check. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…4545) ## The bug buzz-agent emitted its `usage_update` notification in exactly one place: after `ctx.run()` returned. Until that moment a turn's token counters lived only in the prompt task's stack frame. **A turn killed mid-flight reported nothing at all** — the provider had already billed every round it completed, and no consumer ever saw any of it. That is not a corner case for anything that ends a turn on a clock. It is the normal case for a long-horizon benchmark run that relaunches its agent between phases. ## How big Measured against a provider's own billing ledger over one run's window: | | provider ledger | what we recorded | |---|---|---| | the relaunched lead seat | $485 / 348M tok | $98.99 / 90.3M tok | | the two seats that were not relaunched | $29.90 / 856M | $25.81 / 765M — reconciles | 97% of that run's usage rows came back all zeros, against 1–4% for comparable runs that never relaunch. In one 450-phase trial exactly 7 phases recorded any usage — and each of those carries 177k–437k input tokens, a whole session's worth landing in the one phase that happened to end gracefully. Worth being precise about what was *not* wrong, since both were plausible and both were checked: - **Not pricing.** The rates were verified against the provider's endpoints API and match what we charge. - **Not a truncation bug.** The usage files were intact and internally consistent. The tokens were never captured in the first place. ## The fix The run loop now emits a session-cumulative `usage_update` after every usage-bearing provider response, so an interrupted turn has reported everything but its single in-flight request. - **Emitting more than once per turn is already part of the contract.** buzz-acp's `UsageTracker` advances its committed baseline only at publish time, and goose behaves the same way — which is why the tracker was written to tolerate it. - **The turn-start session baseline is snapshotted into `RunCtx`** so the mid-turn figure stays *session*-cumulative. A turn-local number would be discarded by a high-water-mark consumer and lose the turn entirely; there is a test for exactly that. - **Snapshot by value, not a session handle.** The loop reports once per round, and taking the sessions lock on each would serialise concurrent sessions behind one another's provider round-trips. Nothing else advances those counters while the turn holds `busy`, so it cannot go stale. - **One shared `wire::usage_update_payload`** for both call sites, so the mid-turn and end-of-turn shapes cannot drift. A drift there would present as tokens silently vanishing, which is the failure this reporting exists to prevent. ## Why not a SIGTERM handler That was the obvious shape and it does not work. At signal time the counters are not sitting anywhere a handler could reach — they are in the turn's stack frame, and the value the handler would need has not been folded into the session yet. Making usage durable *during* the turn is what actually fixes it; once it is, a handler adds nothing beyond the in-flight request, whose cost is unknown until its response lands. ## Tests - `usage_is_reported_after_each_round_not_only_at_turn_end` — two rounds; asserts the **first** notification carries round 1's counts alone, proving it went out before round 2 returned. - `mid_turn_usage_includes_earlier_turns` — a mid-turn report must be session-cumulative, not turn-local. buzz-agent 18/18 on the `fake_llm` suite, 382 unit. `cargo fmt` / `clippy` / `cargo check --workspace --all-targets` clean. ## Scope Agent-side only, against `main`. The matching harness change — settling usage on the timeout path, which was skipped on the reasoning that an incomplete turn has nothing to flush — is **block#4553**, against the benchmark branch, since that harness does not exist on `main`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Atish Patel <atish@squareup.com> Co-authored-by: Claude Code <noreply@anthropic.com>
## Summary - document exact-head trusted approval as the only desktop tagging authorization - explicitly require `desktop_ref=desktop-v<version>` for the internal desktop handoff - replace the stale `squareup/sprout-releases` repository name with `squareup/buzz-releases` ## Audit coverage Compared `block/buzz` release documentation and automation with `squareup/buzz-releases` `main` (`5b09e5c5d71c80a0849a33458f4e45695df515d7`), including its README, agent guide, Buildkite field hint, desktop validator, release validation tests, and protected updater promotion instructions. ## Validation - `bash scripts/test-release-ref-contract.sh` - `git diff --check origin/main...HEAD` Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Summary - replace a platform-dependent mounted-`RichText` assertion with the production follow-mode boundary predicate - retain the jump-to-latest assertion as the visible consequence of follow mode remaining off - leave production behavior and desktop PR block#4549 unchanged ## Why `ScrollablePositionedList` may keep an offscreen item mounted within cache extent on macOS while Linux does not. Mounting therefore does not establish whether reversed-list item 0 is at the latest boundary. The replacement reads the list's public `itemPositionsNotifier` and applies the same `index == 0 && abs(itemLeadingEdge) < 0.01` contract used by `message_list.dart`. ## Validation At commit `bc88617e61d8e9edf8fea832baa8d918163ee212` on macOS with repo Flutter 3.41.7: - `cd mobile && ../bin/flutter test` — 1088 passed, 1 skipped - `cd mobile && ../bin/flutter analyze` — no issues - pre-push `mobile-test` and `branch-skew` hooks — passed Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Buzz Desktop release v0.5.4 - **Frozen main:** `6de85fe31d781122756aecf954bae7d357a56b9a` - **Reviewed candidate:** `5836cb8f0af478ed3ee3bc6464a20fa4cc91303f` - **Previous desktop release:** `desktop-v0.5.3` - **Proposed immutable tag:** `desktop-v0.5.4` This PR must be **squash merged** only after the Desktop Release Candidate check passes. The branch must remain based directly on current `main`; stale base, payload drift, incomplete notes, or an unauthorized merge produce no tag. The checked-in changelog accounts for every non-merge commit in the release range. Publication remains bound to the immutable candidate tag. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
### Summary Fixes [this issue](buzz://message?channel=e62570dd-33ad-42c5-b92b-75f2689f9694&id=b726c366abfe62429ee3cdcd34d0c0fb98c33c3ea053480585bed71745412b56): > I often don’t see my bot responses until after I post. they’re usually time stamped correctly so I think it’s just a refresh issue? ### What changed? Buzz Mobile now reconnects relay sessions after the app has remained backgrounded beyond the existing 5-second grace period, even when the session still reports a stale `connected` state. This makes resume recovery independent of whether iOS runs the grace timer before or after delivering `resumed`. Reconnection is now based on elapsed background time rather than a direct socket-health probe. - If the app was backgrounded for at least the 5-second grace period, the socket is presumed dead and the session reconnects regardless of reported status. - If it was backgrounded for less than that, a reported `connected` status is still trusted. In the sub-5-second window the socket is either genuinely alive, which is the common case for a momentary background, or it is dead and the client ping detects it within the two-interval worst case described below. That is now a degraded-latency path, not a silent-forever path. The mobile relay socket now uses `IOWebSocketChannel.connect` with a 30-second `pingInterval`. An unanswered ping closes the Dart socket through the existing disconnect and reconnect path. Detection takes up to two ping intervals, so about 60 seconds worst case, not 30. One interval of idleness elapses and a ping is sent, then a second interval elapses with no pong and the socket closes. Any inbound pong restarts the first stage, so the clock measures idleness rather than running on a fixed cadence. ### Why? Buzz iOS can sometimes stop showing new bot or agent responses after a phone has been locked for 5 to 10 minutes. When the user later posts a message, the missing responses can appear all at once. iOS may suspend Buzz before the short delayed cleanup that would normally close its connection has a chance to run. Before this change, Buzz trusted the resulting stale healthy status on resume and skipped reconnecting, so the missing responses stayed hidden until a later post exposed the dead connection. A state-machine test with a stubbed connection reproduced this reported pattern and showed that it matches this failure mode: the failed post triggered a reconnect that fetched the missing messages. The same test also checked the other candidate explanation, the bug tracked in [block#3053](block#3053), where the relay has closed the app's subscription. That state does not produce the pattern. Posting succeeds and the user's own message appears, but nothing looks for the missed messages, so they stay hidden. The test confirmed that the missed messages were still available to fetch in that state, so the missing step was a trigger to fetch them. This was not an end-to-end reproduction on an iOS device or a live relay. The new resume check covers the normal lock and unlock path. If the app was backgrounded for less than the 5-second grace period, it still trusts a connection marked as healthy. A dead connection in that window is instead detected by the ping check, which can take up to about 60 seconds but prevents the app from remaining silently stuck. The ping only runs while iOS is running the app, so it does not detect a connection that died during suspension; the resume check owns the lock and unlock path. A pre-existing path also runs the same resume handling when network connectivity returns while the app is already in the foreground. Because the app was not backgrounded, this change does not alter that path, which still trusts a connection marked as healthy and relies on the slower ping check. Recovery from a subscription that the relay explicitly closes remains in [block#3053](block#3053), and the two changes overlap in one file. Changes to how missed messages are backfilled or replayed are out of scope. ### How is it tested? Full mobile suite at base and head. Both runs have the same known macOS-host-only failure in `ChannelDetailPage keeps follow mode off while a tall newest message stays visible` at line 1053: - Base: 1,021 passed, 1 skipped, 1 failed - Head: 1,025 passed, 1 skipped, 1 failed Added tests: - [`relay_session_test.dart`](https://github.com/block/buzz/tree/main/mobile/test/shared/relay/relay_session_test.dart): long-background resume reconnect and within-grace control - [`relay_socket_liveness_test.dart`](https://github.com/block/buzz/tree/main/mobile/test/shared/relay/relay_socket_liveness_test.dart): silent-peer disconnect and idle-but-healthy control Mutation checks confirm that removing elapsed-background resume recovery fails with one socket instead of two, and removing `pingInterval` leaves the silent peer connected. Restored production code passes both mutations' regression tests and the healthy idle control. Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz>
## Summary Gate 1 only for desktop release caching: - replaces canary `rust-cache` use with explicit exact-key `actions/cache/restore` + `save` - computes keys after `cargo update --workspace`, including platform, target, Rust toolchain, Cargo manifests/locks, profile/features, and native-toolchain inputs - normalizes only the desktop package version so a trusted `main` canary can warm an otherwise identical release tag - excludes Tauri bundle directories, so installers and signed artifacts are never cached - adds a restore-only `cache-proof-*` tag workflow that fails unless tag scope sees the exact default-branch cache - adds contract tests that enforce no release-workflow cache change in Gate 1 `release.yml` is intentionally unchanged. A cache miss remains the current cold canary build; the release path cannot be affected by merging this PR. ## Validation - `scripts/test-desktop-release-cache-key.sh` - `scripts/test-desktop-release-cache-workflow.sh` - `scripts/test-release-ref-contract.sh` - Ruby YAML parse of all four changed workflows - `git diff --check` - pre-push `branch-skew` ## Post-merge proof plan 1. Run each canary cold on trusted `main`, recording cache size/save time and fresh artifact inventory. 2. Run each canary warm, requiring the exact-key hit and recording restore/build time. 3. Create a disposable `cache-proof-*` tag at that same trusted `main` SHA and dispatch **Desktop release cache tag-scope proof** from the tag. 4. Do not begin Gate 2 or modify `release.yml` unless the exact tag-scope restore succeeds and cache transfer economics are favorable. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
**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>
## 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>
**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>
## 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>
…ock#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>
## Overview **Category:** improvement **User Impact:** Mobile users can now access consistent channel and DM actions from both the channel list and conversation header. **Problem:** Mobile channel menus exposed a narrower, inconsistent set of actions than desktop, and the available actions differed by entry point. **Solution:** This change introduces one reusable action sheet with a clear quick-action hierarchy, role-aware lifecycle controls, confirmations for consequential actions, and a deliberately narrower DM menu. ## Changes <details> <summary>File changes</summary> **mobile/lib/features/channels/channel_actions_sheet.dart** Adds the shared channel and DM action-sheet experience used by both entry points, including Star/Unstar and Read/Unread quick actions for channels, section movement, mute, management, inline copy actions, guarded lifecycle actions, confirmations, and a compact DM menu without quick actions. **mobile/lib/features/channels/channel_detail_page.dart** Routes the header ellipsis through the shared action sheet so the in-channel menu matches the channel-list experience, including for DMs. **mobile/lib/features/channels/channel_management_provider.dart** Adds archive and delete operations using the desktop-compatible relay event kinds and refreshes channel state after completion. **mobile/lib/features/channels/channels_page.dart** Makes the shared channel action-sheet entry point available to the channel-list implementation. **mobile/lib/features/channels/channels_page/channel_tile.dart** Replaces the tile-specific long-press menu with the reusable action sheet while preserving read state and section context. **mobile/test/features/channels/channel_actions_sheet_test.dart** Covers action hierarchy, owner/admin/member capability guards, loading and failure states, DM narrowing with no quick-action row, and inline copy actions. **mobile/test/features/channels/channel_detail_page_test.dart** Updates channel-header flows to exercise management through the new shared action sheet. **mobile/test/features/channels/channel_management_provider_test.dart** Verifies archive and delete event tags stay compatible with desktop behavior. </details> ## Reproduction Steps 1. Run the mobile app and open a populated channel list. 2. Long-press a regular channel and verify the Star/Unstar and Read/Unread quick actions appear above Move to section…, Mute, Manage, Copy channel name, and Copy channel ID. 3. Choose either copy action and verify it copies the expected value. 4. Open a channel, tap the header ellipsis, and verify the same action sheet appears. 5. As an admin or owner, verify Archive appears; as an owner, verify Delete also appears. Confirm that lifecycle actions require confirmation. 6. Long-press or open the header menu for a DM and verify it has no quick-action row and starts with Mute, followed by Copy channel name and Copy channel ID. ## Screenshots ### Channel menu | Regular channel — Mark Unread | DM — no quick actions | Archive confirmation | |---|---|---| |  |  |  | --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
## 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>
**Category:** improvement **User Impact:** Mobile readers can jump directly to their oldest unread message and return to the latest message with compact directional controls. **Problem:** Opening an active channel at its newest message makes it easy to miss where unread conversation began, while moving back through history lacks a lightweight route to the live edge. **Solution:** Capture the channel's unread boundary when it opens, offer an accessible up-chevron beneath the app bar to reach that stable target, then reveal the inverse down-chevron at the bottom whenever the reader is away from latest. Deep links retain precedence, and live-follow, pagination, composer resizing, and explicit scroll ownership continue to use the existing timeline behavior. <details> <summary>File changes</summary> **mobile/lib/features/channels/channel_detail_page.dart** Captures the channel's read state at open time and passes a stable unread snapshot into the timeline before the normal deferred read update advances it. **mobile/lib/features/channels/channel_detail_page/message_list.dart** Adds mutually exclusive oldest-unread and latest navigation, with accessible icon controls positioned at opposite edges of the message surface while preserving existing follow and deep-link behavior. **mobile/test/features/channels/channel_detail_page_test.dart** Covers the unread target, compact inverse controls, accessible tooltips, and placement beneath the frosted app bar. </details> ## Reproduction steps 1. Open a Flutter mobile channel that has unread messages without entering through a message or thread deep link. 2. Confirm an up-chevron appears directly below the channel app bar while the timeline remains at latest. 3. Tap the up-chevron and confirm the timeline scrolls to the oldest message that was unread when the channel opened. 4. Confirm the unread control is replaced by a down-chevron at the bottom of the timeline. 5. Tap the down-chevron and confirm the timeline returns to latest and resumes following new messages. ## Screenshots | At latest — up-chevron to oldest unread | Away from latest — down-chevron to latest | |---|---| |  |  | _Real iPhone 17 Pro Simulator captures from the neutral `buzz-mobile-scroll-to` channel._ Originating Buzz thread: `buzz://message?channel=5b16c478-22d8-4ddd-951a-6036e19b81ff&id=6a78af32d7ac6f531b182c4e70dd5a04c503a2dab2ce2c0c74b2c6baa5921741&thread=6a78af32d7ac6f531b182c4e70dd5a04c503a2dab2ce2c0c74b2c6baa5921741` --------- 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>
**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>
## 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>
## Summary - make mobile unread state visible with bold channel names, an animated Inbox badge, and swipe-to-toggle Inbox rows - add directional transitions for top-level mobile navigation - let mobile send while media uploads, with cancellable progress UI - normalize iOS and Android video uploads, attach poster frames, and improve native video playback ## Validation - `just mobile-check` - `just mobile-test` - `cargo test -p buzz-media` - Pixel smoke test - iPhone smoke test Desktop background uploads moved to block#4522 so the two platforms can be reviewed independently. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: leader <71e9f2c44a6932b6772caaaccda1911d010463c3e2c6c40410b8329956046801@buzz.block.builderlab.xyz> Co-authored-by: Tom Brow <tomb@block.xyz>
…lock#2392) (block#4374) ## What Fixes block#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>
## 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 block#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>
## 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>
## 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>
…ock#4792) ## Summary Increases three Playwright assertion timeouts in `tests/e2e/empty-edit-delete.spec.ts` from 5s to 10s to fix a shard-composition flake introduced by PR block#4694. ## Root Cause PR block#4694 added `huddle-transcription.spec.ts` (477 lines, 22+ tests) to the Desktop Smoke E2E suite, shifting shard 2 composition so that `empty-edit-delete` now runs with significantly more accumulated browser state. The three affected assertions all wait for a React state update triggered by pressing Enter in edit mode: - `alertdialog` becoming visible after an empty edit (tests 1 and 2) - `edit-target` hiding after a successful non-empty edit (test 3) These transitions go through the React scheduler. In isolation they complete in milliseconds. In a loaded headless shard with accumulated GC pressure, the 5s window became insufficient — test 3 failed 3/3 times in CI run [30946444168](https://github.com/block/buzz/actions/runs/30946444168) with `edit-target` still visible after Enter. No product code is changed. The empty-edit-delete flow is correct and untouched by block#4694. This is a test-environment timing adjustment only. ## What Changed - `tests/e2e/empty-edit-delete.spec.ts` — three `{ timeout: 5_000 }` → `{ timeout: 10_000 }` for the post-Enter React-update waits ## Validation - `just desktop-check` — passed - `just desktop-test` — 4194 passed, 0 failed Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Summary - derive the current desktop package version in the release cache-key contract test - mutate that version in both `Cargo.toml` and `Cargo.lock` instead of assuming `0.5.4` - prevent desktop release version bumps from failing generic CI ## Context PR block#4788 bumped Desktop to `0.5.5`, exposing the hard-coded fixture. The dedicated release candidate check passed, while generic CI failed with `desktop version changed cache key`. ## Verification - pre-commit hooks passed - pre-push hooks passed - CI will validate the full contract Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## 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: [block#4695](block#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>
## Buzz Desktop release v0.5.5 - **Frozen main:** `383d9e1eafd569b44b9c835200dba69ef7cec9dc` - **Reviewed candidate:** `ac589061ef1009f55384536e483cfe9b1260697b` - **Previous desktop release:** `desktop-v0.5.4` - **Proposed immutable tag:** `desktop-v0.5.5` This PR must be **squash merged** only after the Desktop Release Candidate check passes. The branch must remain based directly on current `main`; stale base, payload drift, incomplete notes, or an unauthorized merge produce no tag. The checked-in changelog accounts for every non-merge commit in the release range. Publication remains bound to the immutable candidate tag. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
## Summary - serialize native `openChannel` tray actions with the camelCase field names consumed by the TypeScript frontend - prevent a valid tray channel ID from becoming `/channels/undefined` - add a Rust serialization contract test covering the complete frontend payload shape ### Root cause `TrayAction` renamed the enum variant to `openChannel`, but its struct fields still serialized as `channel_id` and `community_generation`. The frontend reads `action.channelId`, so tray navigation called `goChannel(undefined)`. ### Testing - manually verified the corrected runtime payload and tray navigation before removing temporary logging - `just desktop-ci` - pre-push hooks (desktop checks/tests, Tauri checks, and Rust tests) --------- Signed-off-by: Kalvin Chau <kalvin@block.xyz> Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz> Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
…repos, PRs, and issues (block#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: block#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: [block#4671](block#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>
## Summary - preserve Databricks catalog 401 responses as authentication failures and retry discovery exactly once after silently refreshing the rejected bearer - preserve runtime OAuth recovery: when discovery has no usable OAuth credential, `session/new` succeeds with only the trimmed configured model so the first `session/prompt` can run the existing browser PKCE flow - reject a rejected configured `DATABRICKS_TOKEN` with actionable, non-interactive guidance; static credentials cannot recover through PKCE - use the configured-model fallback for non-auth discovery failures without caching failed or fallback catalogs, so later sessions retry discovery - keep known Databricks v2 models only for authenticated empty-catalog responses and mark their provenance - resolve discovery before MCP spawn or session registration, preventing failed discovery from leaking resources or consuming session capacity - permit serialized interactive PKCE only from the explicit saved-agent model picker; passive draft discovery never opens a browser ## Runtime flow 1. OAuth discovery attempts cached credentials and silent refresh without opening a browser. 2. If no usable OAuth bearer exists, `session/new` advertises only the configured model and succeeds. 3. The first `session/prompt` uses `TokenSource::bearer()`, which may launch browser PKCE. 4. A later session retries discovery and caches only the authenticated catalog. ## Regression coverage - rejected-but-locally-fresh OAuth bearer performs one refresh and one catalog retry - OAuth mode with no cached token allows `session/new` and returns exactly the trimmed configured model - the OAuth fallback is not cached; a later authenticated session retries discovery and caches the returned catalog - rejected static tokens still reject `session/new` - failed discovery does not consume the sole session slot or spawn the supplied MCP process - Desktop interactive/passive auth intent, static-token redaction, and authenticated empty-catalog provenance ## Verification - `cargo test -p buzz-agent` - `cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib commands::agent_models` - `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --all-targets -- -D warnings` - `cargo fmt --all -- --check` - `git diff --check` - full pre-push hooks ## Review Adversarial review found and drove fixes for session/MCP resource leakage, duplicate concurrent PKCE flows, sensitive error propagation, incorrect 403 reauthentication, missing discovery-level coverage, passive browser launch, and the Desktop file-size ratchet. The final follow-up preserves the existing prompt-time OAuth flow while retaining static-token rejection and pre-allocation discovery ordering. --------- Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
## Buzz Desktop release v0.5.5 - **Frozen main:** `4a2305170eef565bf1836e2859247e67c030f8af` - **Reviewed candidate:** `2d03d37b05b68186b2caad9da79080032be3ac72` - **Previous desktop release:** `desktop-v0.5.4` - **Proposed immutable tag:** `desktop-v0.5.5` This PR must be **squash merged** only after the Desktop Release Candidate check passes. The branch must remain based directly on current `main`; stale base, payload drift, incomplete notes, or an unauthorized merge produce no tag. The checked-in changelog accounts for every non-merge commit in the release range. Publication remains bound to the immutable candidate tag. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
## 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>
## Buzz Desktop release v0.5.5 - **Frozen main:** `25a9cf1be6d245fbd7373cb1160dbc790baf5bd5` - **Reviewed candidate:** `8380c1f8ead8816bcf1f4ea9f66aa08e2441b15a` - **Previous desktop release:** `desktop-v0.5.4` - **Proposed immutable tag:** `desktop-v0.5.5` This PR must be **squash merged** only after the Desktop Release Candidate check passes. The branch must remain based directly on current `main`; stale base, payload drift, incomplete notes, or an unauthorized merge produce no tag. The checked-in changelog accounts for every non-merge commit in the release range. Publication remains bound to the immutable candidate tag. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
Agent co-lab was blocked on skill packs because buzz-cli only allowed image/* and video/mp4. The relay already accepts generic Blossom uploads via a danger denylist (HTML/JS/SVG/executables). Align the CLI with that policy so zip/pdf/text and similar types work while unsafe types stay blocked. Add unit coverage for allow/block and size tiers (100MB generic). Signed-off-by: Trevor P <trev2005@gmail.com>
Codex P2 on block#4880: after widening upload MIME types, `messages send --file` still emitted `` for every non-video blob. Desktop FileCard expects plain `[filename](url)` plus imeta `filename` for zip/pdf/txt. Match Desktop formatImetaMediaLine: images/video stay inline; generic files and agent/team snapshot PNGs use markdown links; escape label metacharacters; include basename on imeta tags. Signed-off-by: Trevor P <trev2005@gmail.com>
Add host-pinned Remote Agents under Desktop Agents (layer-3 seat location), HTTP host-agentd wrapping buzz-host-agents, arm presets including push-nerve, and dogfood docs for laptop tunnel to headless home. Signed-off-by: Trevor P <trev2005@gmail.com>
Expose /v1/location-proof from host-agentd, merge surface/project into Desktop Remote Agent cards, and document hybrid proof dogfood. Signed-off-by: Trevor P <trev2005@gmail.com>
Signed-off-by: Trevor P <trev2005@gmail.com>
Birth cert (pubkey) ≠ face ≠ body ≠ place. Refuse silent dual-spawn; privacy-shaped place_proof.v1; self-location on Desktop + host-agentd; mobile presence snapshot on track; core handoff for block/buzz. Co-lab SoT: #agent-entity-holon · dogfood GREEN on home host-agentd. Signed-off-by: Trevor P <trev2005@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ce36c0c04b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const pk = normalizePubkey(input.agent.pubkey); | ||
| const status = | ||
| input.presenceLookup?.[pk] ?? input.presenceLookup?.[input.agent.pubkey]; | ||
| if (status !== "online" && status !== "away") return; |
There was a problem hiding this comment.
Require presence data on every local-agent start path
The new guard silently permits a start whenever presenceLookup is omitted, but several production callers still omit it: the Agents view calls startManagedAgentWithRules without presence in desktop/src/features/agents/ui/useManagedAgentActions.ts:166-169, and member-sidebar start/respawn paths do the same in desktop/src/features/channels/ui/useMembersSidebarActions.ts:191-194 and 208-213. Starting an agent from those surfaces can therefore launch a second local body while the same pubkey is online elsewhere, defeating the place-safety invariant; make presence mandatory for guarded local starts or wire the query into every caller.
Useful? React with 👍 / 👎.
| limit: pubkeys.length, | ||
| ), | ||
| ]); | ||
| if (generation != _snapshotGeneration) return; |
There was a problem hiding this comment.
Preserve concurrent presence snapshot results
When multiple widgets track different pubkeys before their queries finish—as the DM channel tiles do independently—each call increments _snapshotGeneration, so every earlier successful query returns here without applying its events. Those pubkeys have already been added to _tracked, meaning later renders will not request them again and they remain falsely offline until a live heartbeat arrives. Generation checks should invalidate snapshots only on notifier/session disposal, not merely because another pubkey started a concurrent snapshot.
Useful? React with 👍 / 👎.
| final generation = ++_snapshotGeneration; | ||
| final sessionState = ref.read(relaySessionProvider); | ||
| if (sessionState.status != SessionStatus.connected) return; |
There was a problem hiding this comment.
Retry tracked presence snapshots after connection
If a profile or channel calls track while the relay session is still connecting, the pubkeys are added to _tracked before this early return. When the relay subsequently becomes connected, build() re-subscribes to live updates but never snapshots the existing tracked set, and future track calls consider those pubkeys non-fresh. This makes the newly added startup snapshot consistently disappear for users who open a DM before relay connection, leaving presence offline until the next heartbeat; defer marking them fetched or issue snapshots for _tracked on connection.
Useful? React with 👍 / 👎.
| let snapshot: Snapshot = { mode: "closed", sessionChannelIds: new Set() }; | ||
| const listeners = new Set<() => void>(); |
There was a problem hiding this comment.
Reset the terminal panel singleton on community changes
This module-level snapshot contains community-specific channel IDs and panel state, but its only reset is test-only and it is not wired into resetCommunityState(). If the user switches communities with a terminal open, the remounted app initially inherits the old community's open mode and session-channel set, causing a terminal panel and stale session indicators to appear in the new community until effects overwrite part of the snapshot. Expose a production reset and invoke it at the relay boundary.
AGENTS.md reference: AGENTS.md:L488-L492
Useful? React with 👍 / 👎.
Fetch presence inside start/respawn rules so Agents view and sidebar cannot bypass dual-body refuse. Mobile concurrent track snapshots apply independently; re-snapshot tracked pubkeys on reconnect. Reset terminal panel state on community switch. Signed-off-by: Trevor P <trev2005@gmail.com>
Add Readable Doctrine, locked contracts (C1–C8), architecture and skills roadmap under docs/operators so the fork shows operator DNA beside the holon/remote-agents code. Public-safe wording; skill packs stay private. Signed-off-by: Trevor P <trev2005@gmail.com>
Add WebSocket JSONL watch for one or more channels (NIP-42 + REQ) so operator soft-wake pipelines can consume live events without HTTP poll storms. Diagnostics stay on stderr; unit tests cover dedupe, reconnect overlap, and multi-channel fan-in. Signed-off-by: Trevor P <trev2005@gmail.com>
spin 0.9.8 is yanked but still required by flume 0.11.x (mesh-llm path). spin 0.10.0 already coexists in the lockfile. Set advisories.yanked=warn until flume consumers can move off 0.9.8 so Rust Lint is not blocked. Signed-off-by: Trevor P <trev2005@gmail.com>
Bump messages subcommand count and names list after adding `watch`. Signed-off-by: Trevor P <trev2005@gmail.com>
Use build_imeta_tag when upload basename is absent so the helper is live under -D warnings. Ignore RUSTSEC-2026-0243 (nostr-relay-pool unmaintained) as a mesh-llm → nostr-sdk 0.44 transitive until mesh upgrades. Signed-off-by: Trevor P <trev2005@gmail.com>
Docker builds on the fork were failing after a successful compile when exporting registry cache to ghcr.io/block/* (permission_denied). Default IMAGE_NAME and GATEWAY_IMAGE to ghcr.io/<owner>/… so block/buzz is unchanged and forks write packages under their own namespace. Signed-off-by: Trevor P <trev2005@gmail.com>
GHCR rejects mixed-case repository names. Personal forks with repository_owner like Trevongit failed cache export with invalid reference format. Resolve IMAGE_NAME/GATEWAY_IMAGE per job via tr [:upper:] [:lower:]. Signed-off-by: Trevor P <trev2005@gmail.com>
Summary
Multi-host agents need birth certificates (DNA), not clone faces. This PR lands Remote Agents host control plus the entity-holon vertical (R0–R5): place-safe bodies, dual-spawn refuse, self-location, mobile presence snapshot, and a core handoff doc for block/buzz — plus operator doctrine and CLI soft-wake plumbing.
Treat agent identity as DNA (pubkey), bodies as place-bound instances with leases, presence as status (and place when known), and refuse silent dual-spawn — with public proofs that never leak home paths.
What’s included
place_proof.v1· dual_body 409 · leases · public redaction · host dogfood GREENdocs/metabolic/host-agents/CORE_HANDOFF_ENTITY_HOLON.mdfor core adoptiondocs/operators/— Readable Doctrine, C1–C8 contracts, skills roadmap (private packs stay private)buzz messages watch— WebSocket JSONL stream for soft-wake pipelinesdeny.tomlyanked=warnfor transitivespin 0.9.8via flume 0.11 / mesh-llmOperator doctrine (new)
Landing page for humans evaluating multi-host ops (complements formal remote-agents spec):
docs/operators/README.mdCore handoff
See
docs/metabolic/host-agents/CORE_HANDOFF_ENTITY_HOLON.mdfor vocabulary, I1–I7, public schema, suggested PR stack intoblock/buzz, and mapping to open work.Upstream draft handoff: block#5419
Related upstream themes (humans + agents are already feeling this)
backend_agent_id)Privacy
Public place proofs never include
surface_root, pid, nsec, or tokens.GET …/location-proof?view=public+ PLACE_PROMPT scrub tests.Co-lab / dogfood
#agent-entity-holon,#multi-host-doctrine(operator SOT)Test plan
python3 docs/metabolic/host-agents/test_place_proof.py(incl. dual_body HTTP)flutter test test/features/profile/presence_cache_provider_test.dart(8/8)cargo test -p buzz-cliwatch + subcommand stabilitydocs/operators/README.mdon the branch and skim C2/C8Notes for core reviewers
Prefer reviewing as phased stack per CORE_HANDOFF (docs → host proof → Desktop cards → dual refuse → self-location → mobile snapshot → operator doctrine pointer) rather than one monorepo dump if upstreaming to
block/buzz.