- Restore the standard primary CTA treatment for general onboarding steps.
- Add a dedicated inverted CTA style for dark backup-security surfaces.
- Apply the white button treatment only to backup creation, file selection, verification, and password-reset actions.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
- Give shared in-step primary actions a white background with dark labels.
- Add coordinated hover colors that preserve contrast across onboarding surfaces.
- Update the shared CTA documentation to describe the inverted treatment.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
- Add a key-to-password-to-lock timeline to the onboarding backup form.
- Animate four connector dots in a downward pulse with reduced-motion support.
- Preserve the compact settings variant and hide decoration in short windows.
- Keep balanced spacing and semantic security-theme colors across the illustration.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
- Increase contrast for the spotlight password field border and focus ring against its white surface.
- Apply dark default and hover colors to the password generator and visibility controls.
- Remove the obsolete dark-surface icon treatment from the light password field.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
- Give the spotlight password field a white background, subtle border, rounded corners, and dark password and placeholder text.
- Keep the saved-password mask legible against the light input surface.
- Render password generator controls in the standard popover surface instead of the dark textured security treatment.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
- Use the resolved identity storage backend to name the system keychain in the initial backup guidance.
- Keep local-file and unknown storage states accurate without exposing boot-time identity creation.
- Explain in the backup options card that the computer may request the user's password when Buzz needs to read the key.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
- Add generated compact light and dark noisy surfaces with reusable tone and size options for cards, popovers, and confirmation dialogs.
- Keep the dark backup option and password panels unboxed, reduce their padding, and top-align parallel card titles for a clearer visual hierarchy.
- Explain identity keys and password-protected backup files in plain language while preserving distinct device, password-manager, and portable-backup choices.
- Normalize onboarding primary, secondary, and icon controls and add reduced-motion-safe transitions between backup creation and verification stages.
- Update focused onboarding E2E coverage for current navigation, compact texture contracts, title alignment, and narrow viewport geometry.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
- Track whether the active identity is stored in the system keyring, local fallback file, environment, or ephemeral memory.
- Expose storage metadata through the Tauri identity response and shared TypeScript identity model.
- Add a responsive first information card beside the two backup actions without revealing boot-time identity creation.
- Show keychain password education only when secure storage succeeded and accurate neutral copy for fallback states.
- Extend identity persistence tests and split storage types into focused modules to preserve file-size limits.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
- Replace the password-backup surface's onboarding Skip and Next actions with a dedicated Back control.
- Keep Backup key and Back in the existing bottom-docked CTA location while preserving inline settings behavior.
- Route Back to Backup options with the correct backward transition instead of advancing onboarding.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
- Replace the standalone password-backup step with copy and encrypted-file options inside the identity-key stage.
- Add a midnight security theme, return-to-onboarding control, and downward transition back into the core yellow flow.
- Restore seven-page onboarding pagination and keep backup progress available when users revisit the security view.
- Update onboarding E2E coverage and screenshots for the inline options, direct setup path, and password-backup return behavior.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Conflict resolutions:
- relay.rs: took main's submit refactor (submit_signed_event_at_with_keys
funnel replaces the removed submit_signed_event); the persona flush loop
on main already publishes through the guarded funnel.
- egress_guard boundary 3 retargeted to the pre-signed funnel entry, and
the /events inventory rows updated for main's relay.rs / sharing.rs
layout (sharing.rs site is its in-file mock-relay test fixture; its
production publish goes through the guarded boundary-1 funnel).
- personas/snapshot/import.rs + e2eBridge.ts: kept both sides' additions.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
## Problem
`common_binary_paths()` probes mise shims, `~/.local/bin`, volta, asdf,
and (further down `resolve_command_uncached`) nvm's default bin dir —
but not bun's global bin directory, `~/.bun/bin`.
bun's installer appends its bin dir to `~/.zshrc` / `~/.bashrc`, which
are **interactive**-only. A login shell never sources them, so
`find_via_login_shell()` can't recover the path either. That's the same
failure mode already called out in this file for nvm:
```rust
// Check nvm's default Node.js bin directory — nvm initializes via
// ~/.zshrc (interactive) which is not loaded by a login shell, so
// `node`, `npm`, and npm-global shims installed there are otherwise
// invisible.
```
So for a GUI-launched desktop app, every rung of the resolution ladder
misses a bun-installed CLI:
1. workspace dev dirs — no
2. `command_looks_like_path` — no, presets use bare names
3. Buzz-managed npm/node dirs — no
4. current process PATH — launchd's minimal PATH on a Finder launch
5. `find_via_login_shell` — `.zshrc` not sourced
6. `common_binary_paths()` — **`~/.bun/bin` absent**
7. nvm default bin — no
This matters because bun is a common install route for the agent CLIs
Buzz targets. Kimi Code in particular ships as an npm package
(`@moonshot-ai/kimi-code`), so `bun add -g` puts it at `~/.bun/bin/kimi`
— exactly where discovery doesn't look.
## Reproduction
On macOS with `codex` and `kimi` installed via bun, launching Buzz from
Finder:
- Kimi Code shows **"CLI needed"**
- both CLIs run fine in an interactive terminal
Probing the way `find_via_login_shell` does, in a clean environment:
```console
$ env -i HOME=$HOME /bin/zsh -l -c 'command -v -- codex; command -v -- kimi'
(nothing)
```
Launching the app with the bun dir on PATH resolves both immediately:
```console
$ env PATH="$HOME/.bun/bin:$PATH" /Applications/Buzz.app/Contents/MacOS/buzz-desktop
```
## Change
One entry appended to the home-relative list in `common_binary_paths()`.
It goes **last** so it cannot shadow a directory that already resolves —
the change can only add resolutions, never alter existing ones.
## Testing
`cargo fmt --check` passes.
I was not able to run the full `just ci` gate locally: `ring 0.17.14`
fails to build in this environment against the macOS 26.2 SDK (`cc`
error compiling `p256-nistz.c`), which is unrelated to this change.
Relying on CI for the rest — the diff adds one `PathBuf` to an existing
`Vec<PathBuf>` and introduces no new API.
## Notes
- Related to #3084, which adds `~/.kimi-code/bin` for the same class of
GUI-launch discovery failure. That covers Kimi's standalone installer;
this covers the bun/npm-global install route. They're complementary —
I've left a note on that PR.
- Only `~/.bun/bin` is added. bun's global packages live under
`~/.bun/install/global/node_modules` but are symlinked into
`~/.bun/bin`, so the single directory is sufficient.
- Worth noting `~/.bun/bin` contains no `node`/`npm`/`npx`, so appending
it can't shadow a system Node toolchain.
Signed-off-by: Xule Lin <43122877+linxule@users.noreply.github.com>
**Category:** improvement
**User Impact:** Users can create a password-protected key backup, save
it locally, and prove that it works before finishing onboarding or
signing out.
**Problem:** Tyler's NIP-49 foundation made encrypted local backup
possible, but the end-to-end ceremony still needed clearer progression,
trustworthy verification, and a Settings treatment consistent with the
rest of Identity. **Solution:** This stack adds a guided
create/download/test flow, verifies the actual saved backup through Rust
without returning secret key material, minimizes password lifetime in
the webview, and restores warm, plain-language presentation across
onboarding and Settings.
> [!NOTE]
> This PR is intentionally stacked on #2937 (`eva/nip49-local-backup`).
Review only the 19 commits in this stack.
<details>
<summary>File changes</summary>
**desktop/src-tauri/src/commands/identity.rs**
Adds off-thread verification of NIP-49 backups and returns public
identity details only.
**desktop/src-tauri/src/commands/identity_key_backup_tests.rs**
Covers verification, identity matching, and failure behavior at the
command boundary.
**desktop/src-tauri/src/egress_guard_tests.rs**
Keeps the NIP-49 source inventory aligned with the new verification
path.
**desktop/src-tauri/src/key_backup.rs**
Supports real backup decryption for verification while zeroizing
submitted passwords.
**desktop/src-tauri/src/key_backup_tests.rs**
Exercises valid, wrong-password, damaged, and different-identity
backups.
**desktop/src-tauri/src/lib.rs**
Registers the verification command with the desktop runtime.
**desktop/src/features/communities/ui/WelcomeSetup.tsx**
Routes community onboarding through the revised backup ceremony.
**desktop/src/features/onboarding/lib/encryptedBackup.test.mjs**
Covers the hardened backup state model and password-clearing behavior.
**desktop/src/features/onboarding/lib/encryptedBackup.ts**
Models creation and verification with opaque request correlation and
short-lived passwords.
**desktop/src/features/onboarding/ui/BackupStep.tsx**
Presents the backup choice clearly and advances into the dedicated
download step.
**desktop/src/features/onboarding/ui/BackupTestFlow.tsx**
Adds the polished select-file, enter-password, verification, error, and
success experience.
**desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx**
Connects community onboarding to the updated backup steps.
**desktop/src/features/onboarding/ui/DownloadKeyStep.tsx**
Adds the dedicated encrypted-backup download step and pre-creation skip
escape hatch.
**desktop/src/features/onboarding/ui/EncryptedBackupCreator.tsx**
Guides password generation, backup creation, native saving, safe retry,
and re-download.
**desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx**
Sequences chooser, download, exact-file test, and setup progression.
**desktop/src/features/onboarding/ui/NostrKeyImportForm.tsx**
Aligns encrypted-key import behavior with the backup flow.
**desktop/src/features/onboarding/ui/NsecMaskedDisplay.tsx**
Improves masked/revealed key presentation and safely wraps long private
keys.
**desktop/src/features/onboarding/ui/OnboardingChrome.tsx**
Supports the revised onboarding layout and transitions.
**desktop/src/features/onboarding/ui/SetupStep.tsx**
Integrates the completed backup ceremony with final setup.
**desktop/src/features/onboarding/ui/onboardingFlowSteps.test.mjs**
Updates flow-level assertions for the new step sequence.
**desktop/src/features/settings/ui/EncryptedBackupRow.tsx**
Adds sibling Create and Test rows that match the surrounding Identity
settings rhythm.
**desktop/src/features/settings/ui/ProfileSettingsCard.tsx**
Places password-backup controls in the Identity card.
**desktop/src/features/settings/ui/SignOutSection.tsx**
Uses explicit backup self-attestation plus the existing wipe phrase
without requiring a raw-key reveal first.
**desktop/src/shared/api/tauriIdentity.ts**
Exposes typed backup verification to the frontend.
**desktop/src/shared/lib/ncryptsecSourceScan.test.mjs**
Updates the frontend source allowlist for the verification path.
**desktop/src/testing/e2eBridge.ts**
Models creation, saving, and verification for browser coverage.
**desktop/tests/e2e/onboarding-backup.spec.ts**
Covers backup creation, exact-file testing, errors, retries, and
completion.
**desktop/tests/e2e/onboarding-docked-cta-screenshots.spec.ts**
Updates docked CTA visual coverage for the revised progression.
**desktop/tests/e2e/onboarding.spec.ts**
Keeps broader onboarding coverage aligned with the backup ceremony.
**desktop/tests/e2e/profile-nsec-reveal.spec.ts**
Covers Settings create/test behavior and updated success copy.
**desktop/tests/e2e/signout-confirmation.spec.ts**
Covers backup self-attestation, wipe phrase confirmation, and reset
behavior.
**desktop/tests/helpers/fileDrag.ts**
Adds reusable file-drop support for backup test coverage.
**desktop/tests/helpers/onboarding.ts**
Routes tests through chooser, download, and skip behavior consistently.
</details>
## Reproduction steps
1. Start fresh onboarding and choose the password-protected backup path.
2. Continue to **Backup your key**, create a password, save the
`.ncryptsec` file, and confirm that **Next** remains gated until testing
succeeds.
3. Select that exact file, enter its password, and verify the **Your
backup works!** success state.
4. Open **Settings → Identity** and confirm that **Create password
backup** and **Test password backup** appear as sibling rows.
5. Test a valid backup, then test wrong-password and different-identity
cases to confirm clear, non-secret-bearing outcomes.
6. Open sign out and confirm that backup self-attestation plus typing
`wipe all my data` remain required.
## Screenshots
### Onboarding flow
| 1. Creating your key | 2. Key created | 3. Create backup password |
|---|---|---|
| <img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/fe5ebf60-02ad-4584-8e64-ff137bafac02"
/> | <img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/0b8c7c6c-2ae5-494e-bb81-0a7b47ea1b66"
/> | <img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/9fa8f6c0-832a-46ec-9af8-55dcae0adbb8"
/> |
| **4. Select the saved backup** | **5. Enter its password** | **6.
Verification succeeds** |
| <img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/7ed1528c-6a1f-4411-bd3e-c57413320cdf"
/> | <img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/088ec3ed-2735-4b9a-a0c5-6d176b3f06c5"
/> | <img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/7081ce32-9371-46b2-9692-23ff77c39d35"
/> |
### Settings flow
| Create and Test tools | Tested backup success |
|---|---|
| <img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/978ab672-5510-46d8-9116-6df4f1bda34c"
/> | <img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/56a1764d-3c3e-40e2-9f4b-962c5e96a36c"
/> |
## Verification
- Desktop typecheck
- Full desktop JS: 3,733/3,733
- `pnpm check`
- Rust desktop lib: 1,862 passed, 14 ignored; diagnostic: 3/3
- Focused Playwright: 16/16 with one worker
Native save/cancel remains the residual smoke-test risk: browser E2E
mocks the KDF/native save-dialog boundary, while real decrypt and
identity matching are covered in Rust.
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
🤖
## Summary
Agent-authored mentions currently depend on matching visible `@Name`
text to channel profiles. That makes notification delivery ambiguous
when names collide or profiles change, and it encourages an extra
post-send lookup just to confirm that the intended `p` tags were
emitted.
This change makes `buzz messages send` mirror Desktop's existing model:
the message keeps a readable name in its content while the recipient
pubkey is supplied separately.
```bash
buzz messages send \
--channel <UUID> \
--content '@Alice could you review this?' \
--mention <alice-hex-or-npub>
```
`--mention` is repeatable. The CLI normalizes and deduplicates explicit
pubkeys, merges them with any names it can resolve from the channel, and
gives explicit identities priority under the existing 50-mention limit.
Before uploading attachments, signing, or publishing, the command checks
every resulting pubkey against the channel's current membership:
- Members are mentioned normally.
- Non-members stop the send and produce an actionable error.
- `--allow-non-member-mentions` deliberately sends notifying `p` tags
without adding anyone to the channel.
Sending a message never changes membership. On success,
`mention_pubkeys` is read from the exact signed event and returned with
the relay response, so callers can verify the emitted recipients without
another query.
Managed-agent guidance teaches this single-command mention flow. Desktop
mention behavior and the Nostr event schema are unchanged. Forum
guidance is intentionally handled separately in #3596.
### Related issue
None found. This replaces the earlier guidance-only approach in this PR
with the underlying CLI behavior it required.
### Testing
- `cargo test -p buzz-sdk`
- `cargo test -p buzz-cli`
- `cargo test -p buzz-acp`
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml`
---------
Signed-off-by: npub1fdupjvyregj3z2tx7gx5x6py04zw89jm5usef9lyea4f3vcgh8qq9zgkdz <4b78193083ca25112966f20d4368247d44e3965ba7219497e4cf6a98b308b9c0@buzz.block.builderlab.xyz>
Signed-off-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Co-authored-by: npub1fdupjvyregj3z2tx7gx5x6py04zw89jm5usef9lyea4f3vcgh8qq9zgkdz <4b78193083ca25112966f20d4368247d44e3965ba7219497e4cf6a98b308b9c0@buzz.block.builderlab.xyz>
Co-authored-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
🤖
## Summary
- Keep the relay reconnect notification dismissed across repeated
connection retries during one continuous outage.
- Re-arm the notification after recovery or relay lifecycle replacement,
including switches between communities that use the same relay URL.
- Preserve the existing dedicated path for authentication and other
application-level errors.
### How an outage is tracked
The AppShell-owned relay-card hook treats an outage as one contiguous
runtime episode rather than assigning it a persisted ID. A hook-local
`outageActiveRef` is armed by the first qualifying unreachable/degraded
observation. While it is armed, intermediate retry states (`connecting`,
`reconnecting`, `stalled`, and `disconnected`) belong to that same
episode, so retry churn cannot clear dismissal or emit another
notification.
The hook receives the same lifecycle identity used by community
initialization: community ID plus `reinitKey`. This distinguishes
multiple communities even when they share a relay URL, and it also
changes when the active community is explicitly reinitialized. The latch
and dismissal are reset when that identity changes or when the relay
singleton reports its authoritative `idle` teardown state. A successful
`connected` state also closes the episode and re-arms the next outage.
These boundaries deliberately bias toward re-notifying rather than
suppressing a later outage: recovery, community switch/reinit, or relay
teardown cannot leave the hook stuck believing an old outage is still
active. No outage state is persisted beyond the mounted hook lifecycle.
### Related issue
None found.
### Testing
- `pnpm --dir desktop typecheck`
- `pnpm --dir desktop test` — 3,769 passed
- `pnpm --dir desktop check`
- `pnpm --dir desktop build:e2e`
- `pnpm --dir desktop exec playwright test
tests/e2e/sidebar-relay-card.spec.ts --project=integration` — 11 passed
---------
Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Co-authored-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
## Summary
- reconcile stale native-scroll anchors when a reply arrives at the
physical floor
- clear the thread new-message affordance instead of incrementing it
from stale cached state
- preserve the existing mid-history path and add direct lifecycle
regression coverage
## Why
PR #3411 fixed geometry-driven reconciliation, but the reply-arrival
branch still trusted a cached `message` anchor without checking the
rendered position. Native anchoring could return a short thread to the
floor without another scroll/resize callback, then the next reply
incremented the pill anyway.
## Verification
- Desktop checks passed
- Desktop typecheck passed
- focused lifecycle test passed (6/6)
- push hook full Desktop unit suite passed (3,770/3,770)
- `git diff --check` passed
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Summary
- Show all colon emoji autocomplete matches
- Rank exact and prefix shortcodes before weaker matches
- Add a regression test and screenshot
## Validation
- `pnpm test`
- `pnpm build`
- `pnpm exec playwright test --project=smoke
tests/e2e/custom-emoji.spec.ts --grep "exact standard shortcode"`
- `just desktop-tauri-clippy`
Native Tauri tests were attempted but could not link because the local
disk filled during compilation.
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
> Carl is updating this pull request on Wes's behalf.
## Summary
- upload an embedded raster avatar through the existing authenticated
media pipeline before minting or persisting an imported shared agent
- store and publish only the resulting hosted URL so agent kind:0
profiles remain within content limits
## Root cause
Snapshot import recovered raster avatar pixels as a large inline base64
data URL. That value was persisted and placed into the agent's kind:0
profile. The relay rejected the oversized profile, so other clients
could not resolve the imported agent's avatar.
## Scope
This is intentionally the forward fix only. It changes two Desktop files
and does **not** add migration or reconciliation behavior for previously
imported agents. Existing affected imports must be re-imported or fixed
manually.
## Validation
- successful pre-push Desktop suite: 1,863 passed, 14 ignored, 0 failed
- all pre-push Rust/Desktop gates green, including all-target clippy
- valid >256 KiB PNG import → production MIME detection/sanitization →
bounded signed kind:0 containing only the hosted URL
- upload failure, malformed data, and URL-only avatar cases covered
- independent fresh review by Princess Donut: clean, no blocking
findings
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Summary
- render existing shared personas whose catalog avatar is a bounded
inline PNG, JPEG, GIF, or WebP data URL
- keep rejecting arbitrary, malformed, unsupported, and oversized
`data:` URLs
- preserve hosted relay URLs as the forward format; catalog browsing
remains read-only
## Root cause
Paul's live shared kind:30175 head contains a 144,878-character
`data:image/png;base64,...` avatar. The catalog projection accepted
HTTP(S) URLs and bounded percent-encoded SVG emoji avatars only, so it
projected Paul's avatar to `null` before `ProfileAvatar` rendered it.
The owner still saw the local persona avatar, producing the reported
owner/viewer mismatch.
This patch accepts only four raster MIME types with strict base64 shape
and a 256 KiB total URL cap at the existing catalog parsing boundary. It
repairs already-signed heads such as Paul without viewer-side uploads or
publication side effects.
Hosted media remains the canonical forward path. #3578 uploads inline
raster avatars during snapshot import, preventing the known source from
creating future inline persona/profile values; existing signed catalog
heads still need this compatibility path until their owners republish.
## Agent instruction finding
The catalog publishes `AgentDefinition.system_prompt` verbatim as the
user-authored **Agent instruction**, as documented by the sharing UI and
NIP-AP. No Buzz base/core/runtime prompt is concatenated in the publish,
catalog, or import path. This PR therefore does not remove authored
instructions and accidentally strip copied agents of their behavior.
## Validation
- targeted `personaCatalogRelay.test.mjs`: 24 passed
- Desktop typecheck: passed
- pre-push Desktop frontend suite: 3,771 passed
- pre-push Desktop checks: passed
- `git diff --check`: passed
- independent review: no blockers, 9.4/10
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Opening the agent observer feed could beachball the app. In Tauri 2, a
sync (`pub fn`) command body runs on the **main thread** — only `async
fn` commands run on the runtime pool. Five commands on the observer-feed
open path were sync, so panel open ran SQLite I/O and secp256k1 work on
the macOS main thread:
| Command | Main-thread work |
|---|---|
| `decrypt_observer_event` | Schnorr ID + signature verify, then NIP-44
decrypt — once per frame |
| `read_archived_observer_events_for_channel` | Opens the archive DB,
runs the channel-index JOIN, returns up to 200 raw JSON blobs per page |
| `read_unindexed_observer_rows` | Opens the DB, returns **all**
not-yet-indexed kind-24200 rows in one shot |
| `index_observer_channel_id` | Opens the DB, loops N upserts |
| `delete_save_subscription` | Opens the DB, one delete |
Eager hydration loads up to 10 pages × 200 frames on panel open, so
that's up to 10 main-thread DB reads plus up to 2,000 sequential
verify+decrypt calls before any scrolling. The one-shot backfill makes
it worse on the first open after history accumulates: one read of every
unindexed row, a decrypt per row, then a batch upsert — all on the main
thread, and all proportional to archive size.
The four archive commands now route their DB work through the existing
`run_archive_db_task` helper (`spawn_blocking` + `open_db`), matching
`list_save_subscriptions`, `read_archived_events`, and `archive_events`
directly around them. `decrypt_observer_event` becomes `async fn` +
`tauri::async_runtime::spawn_blocking`, with `state.signing_keys()`
extracted before the spawn since `State` is not `Send` — the same
pattern `sign_event` uses from #1222.
No frontend changes: `invoke` is already promise-based, so the TS
wrappers in `tauriArchive.ts` and `tauriObserver.ts` are unchanged.
This removes the freeze, not the work. Eager hydration still takes the
same wall time — the feed shows a loading state instead of blocking the
UI. Batching the per-frame decrypt IPC (2,000 round-trips into one
command) would cut the latency itself; that's deliberately out of scope
here.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary
Fixes two distinct fidelity failures in direct agent sharing:
- The sender now puts the same effective avatar shown on the agent card
into People-share and file-export snapshot PNGs, including
profile/kind:0 fallback avatars.
- The importer now persists the visible PNG body as the portable avatar
instead of ignoring it in favor of sender-local manifest references.
- Export materializes inherited runtime, provider, and model identifiers
verbatim, while preserving explicit definition values. It does not
translate or substitute configuration for a different recipient setup.
- Sharing waits for a profile-only fallback avatar query, preventing an
early-click race.
The PNG import path keeps the existing safety invariant: decode is
capped at 2048×2048 / 32 MiB and re-encoded avatars above the 2 MiB
inline limit fall back to the manifest reference. The exact transparent
1×1 no-avatar placeholder is ignored.
The original Tyler↔Wes screenshot demonstrates both stages: Wren's
attachment had an avatar that disappeared after **Add agent**
(receiver/import failure), while Pinky's attachment was already blank
(sender/projection failure).
### Related issue
N/A — reported and traced in the linked Buzz conversation.
### Testing
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml
commands::personas::snapshot` — 57 passed
- `pnpm exec tsc --noEmit`
- Biome check on changed frontend/E2E files
- Pre-push hooks:
- desktop check
- desktop tests
- desktop Tauri tests — 1853 passed, 14 ignored
- file-size ratchet
The People-share E2E regression asserts that a profile-only avatar
reaches `avatarPngDataUrl` in the real encode command payload.
---------
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Summary
- Simplify the community invite dialog around link sharing.
- Add matching expiry and use-limit dropdowns, with sensible preset use
caps.
- Cover the default unlimited and selected-limit invite payloads.
## Validation
- `pnpm -C desktop run build:e2e`
- `pnpm -C desktop exec playwright test
tests/e2e/invite-link-copy.spec.ts
tests/e2e/invites-settings-screenshots.spec.ts --project=smoke`
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Fixes#2787
- Added `claude-opus-5` to `config.rs` model classification and adaptive
effort helpers.
- Updated fixture test configurations to cover `claude-opus-5`.
- Verified with `cargo test` and JS unit tests.
Signed-off-by: Apurva Shaw <apurvashaw@Apurvas-MacBook-Air.local>
Co-authored-by: Apurva Shaw <apurvashaw@Apurvas-MacBook-Air.local>
## Summary
- reconcile anchored-scroll state when passive layout changes put a
thread at its physical floor
- route thread composer-padding growth and shrink through the same
hook-owned settlement path
- preserve pinned thread targets while clearing stale new-message state
## Root cause
Thread bottom state was updated primarily by native `scroll` events.
Deferred replies, viewport changes, and composer-overlay padding can
finish changing geometry after the user's last scroll—or after the
initial open pin—without another scroll event. The thread could visibly
reach the floor while `isAtBottom` and `newMessageCount` remained stale,
leaving the “N new messages” pill visible.
## Verification
- `pnpm check`
- `pnpm typecheck`
- `pnpm test` — 3,768 passed
- push hook: branch-skew, Desktop check, and Desktop full unit suite
passed
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
## Summary
- replace the whole-tree file-size gate with a stateless differential
ratchet
- allow inherited files over 1,000 lines to hold or shrink, but never
grow
- delete the 44-entry numeric override ledger and run the same policy
across Desktop, Web, and Mobile CI
- fail closed when the local base cannot be resolved and cover policy,
Git status parsing, and base resolution in unit tests
This removes the shared mutable policy state that caused unrelated PRs
to fail after neighboring merges. It does **not** by itself prevent two
stale green PRs from becoming invalid when combined; that requires merge
queue or up-to-date branch enforcement.
### Related issue
None found. This follows the design discussion in the linked Buzz
channel.
### Testing
- `node --test scripts/check-file-sizes-core.test.mjs` (6/6)
- Desktop, Web, and Mobile ratchet entrypoints
- `just desktop-check`
- `just web-check`
- Mobile analysis
- `git diff --check`
The repository pre-push suite also exposed an unrelated existing Mobile
widget failure in `ChannelDetailPage keeps follow mode off while a tall
newest message stays visible`; it reproduces in isolation and this
branch does not touch Mobile widget behavior.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
On some Linux GPU/driver/compositor combinations, WebKitGTK's dmabuf
renderer aborts the web process during startup, so Buzz comes up with no
window at all and the user has no way to fix it. Setting
`WEBKIT_DISABLE_DMABUF_RENDERER=1` avoids the abort by falling back to
the shared-memory buffer path.
WebKit reads each of its rendering variables exactly once per process,
so the choice has to be made before anything initializes — there is no
runtime toggle and no second chance later in the same process. This
decides up front from two cheap preflight signals rather than reacting
to a crash:
- **NVIDIA GPU** — any DRM device under `/sys/class/drm` reporting PCI
vendor `0x10de`, the driver family behind most upstream reports.
- **AppImage** — the `APPIMAGE` environment variable. linuxdeploy's
AppRun hook pins `GDK_BACKEND=x11`, and the dmabuf renderer buys nothing
on that XWayland path.
Either signal disables the dmabuf renderer. Neither signal leaves the
environment untouched.
## Escape hatches
`--safe-rendering` forces the safest configuration for one launch —
`WEBKIT_DISABLE_DMABUF_RENDERER` plus `WEBKIT_DISABLE_COMPOSITING_MODE`
— for a machine neither signal recognises.
Any user assignment of a variable this module may set stands the
heuristic down **wholesale**. Presence is the test, not truthiness, so
`VAR=0` and `VAR=` both count: a user asking for the dmabuf renderer
*on* gets it, even on a machine the heuristic would have opted out.
`--safe-rendering` against such an assignment is refused with a
diagnostic naming both the assignment and the key to unset, and exits
non-zero — the flag and the environment are two incompatible answers to
one question, and neither is guessed.
## Placement
`webkit_rendering::apply()` runs at the top of `fn main()`, before
`buzz_lib::run()`. That is the only point where the process is still
single threaded with no GTK object alive, which is what makes
`std::env::set_var` sound; the module doc and the call site both say so.
The whole module is `#[cfg(target_os = "linux")]` — macOS and Windows
compile none of it.
The decision is a pure function of argv, an injected environment lookup,
and an injected DRM root, so all of it is unit-testable without mutating
the process environment.
Closes#2338. Upstream:
[tauri#9394](https://github.com/tauri-apps/tauri/issues/9394). Same
approach and same variable as
[clash-verge-rev](https://github.com/clash-verge-rev/clash-verge-rev/blob/main/src-tauri/src/utils/linux/workarounds.rs)
`workarounds.rs` and
[screenpipe](https://github.com/screenpipe/screenpipe/blob/main/apps/screenpipe-app-tauri/src-tauri/src/linux_webkit_env.rs)
`linux_webkit_env.rs`.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary
Fixes 4 flaky DM expansion E2E tests in Desktop Smoke shard 1 that were
failing non-deterministically on CI (also reproducing on `main` at run
`29526844596`).
**Failing tests:**
- `channels.spec.ts:652` — creates the DM before preparing a persona
mention
- `channels.spec.ts:760` — routes an agent mention from an existing DM
to the expanded conversation
- `channels.spec.ts:815` — routes a relay-agent mention from an existing
DM to the expanded conversation
- `channels.spec.ts:940` — drops an expanded DM after the first message
fails
## Root Cause
Race condition: under fast CI execution, mock command completions
(create_managed_agent, open_dm) can resolve in non-deterministic order,
causing assertions to observe stale or mid-transition state.
## Fix
- **:652** — Move the `new-message-recipient-popover` hidden assertion
after `chat-title` settles (both names present), so it runs
post-transition rather than mid-transition.
- **:760, :940** — Add `createManagedAgentDelayMs: 100` to ensure
persona provisioning doesn't collapse into the same tick as the
expanded-DM open/start sequence.
- **:815** — Add `openDmDelayMs: 100` so the two open_dm calls resolve
in deterministic order.
## Validation
All 4 tests pass with `--repeat-each=3` (12/12 green) locally. Biome
lint clean.
## Scope
Test-only change: 12 insertions, 1 deletion in
`desktop/tests/e2e/channels.spec.ts`.
---
Investigated by Ferret, reviewed by Grumplestiltzkin.
Signed-off-by: Cameron Hotchkies <chotchkies@block.xyz>
Co-authored-by: Goose <opensource@block.xyz>
## Summary
- paint the community rail across the full app height instead of
exposing the parent background through external margins
- preserve the existing community-button alignment and balanced
horizontal gutters by moving vertical spacing inside the rail
- update the rail geometry coverage to require full-height paint
ownership
## Root cause
PR #2972 aligned the rail box with the inset content by adding top and
bottom margins to the `bg-sidebar` element. Margins are outside the
painted box, so flat light and dark themes exposed a differently colored
app background above and below the rail.
## Validation
- pre-push `desktop-check`
- pre-push desktop unit suite: 3,751 passed
- `git diff --check`
Local Playwright/E2E was not run; CI owns the full browser matrix.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Registering a custom ACP harness works today, but only from Settings →
Agents. Anyone whose first touchpoint is "New agent" has no way to
discover the custom path — the dropdown just lists the baked-in presets
plus whatever was registered earlier. This adds an inline "Add custom
harness…" entry to the harness dropdown in all three agent surfaces:
create, edit-definition (`AgentDefinitionDialog`), and instance edit
(`AgentInstanceEditDialog`).
The entry is a sentinel option (`ADD_CUSTOM_HARNESS_VALUE`, NUL-prefixed
so it can never collide with a real harness id — backend ids match
`[a-z0-9_][a-z0-9_-]*`), mirroring the `CUSTOM_ENTRY_ID` trick already
used in `HarnessCatalogDialog`. Picking it never writes into form state;
it opens `AddCustomHarnessDialog`, a thin modal wrapper hosting the
existing `CustomHarnessForm` in `chromeless` mode. `CustomHarnessForm`'s
`onSaved` now carries the saved `definition.id` (the form may rewrite
it); the two existing call sites ignore the argument, so their behavior
is unchanged.
Selection after save is deferred rather than immediate.
`usePendingHarnessSelection` holds the saved id until the runtime
catalog actually publishes it via discovery, then selects it exactly
once — so the dialog never selects an id it cannot render, and
back-to-back registrations resolve correctly. The wait is scoped to the
owning dialog's `open` state: both host dialogs stay mounted when
closed, so an unpublished id is dropped on close rather than selecting
into reset form state when discovery later catches up. Selection is
routed through each dialog's normal dropdown change handler, so
provider/model reset (and command pinning in the instance dialog) behave
identically to a hand-picked harness. Dismissing the modal leaves the
previous selection untouched. `AgentInstanceEditDialog`'s existing
"Custom command" option is a different feature (ad-hoc command override
vs. a registered reusable harness) and is untouched.
Coverage is 16 unit tests in `addCustomHarness.test.mjs` (real React
mount, following the existing `.test.mjs` pattern) plus 4 Playwright
specs in `inline-custom-harness.spec.ts` covering all three surfaces
end-to-end. Both suites were mutation-verified: treating the sentinel as
a real selection, selecting before the catalog publishes, never clearing
the pending id, ignoring the dialog's open state, and reversing
latest-save-wins each turn the unit tests red; reverting the two dialog
diffs turns all four e2e specs red. The `check-file-sizes.mjs` overrides
for the two dialogs are ratcheted to their exact new counts (1048 and
1229) — verified tight in both directions, N passes and N−1 fails, so no
headroom is introduced.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary
Drafts were showing up in the Home Inbox **All** view, mixed in with
messages and reminders (reported in `#buzz-bugs`). Drafts are private
composer state, not inbox activity — they now appear only under the
dedicated **Drafts** filter.
## Changes
- **`inboxListRows.ts`** — drop the `draft` row variant from
`buildInboxListRows`; the mixed view builds only `inbox` + `reminder`
rows.
- **`InboxListPane.tsx`** — remove the draft branch of the All-view
render path; `PersonalItemRow` now renders reminders only.
- **`useHomePersonalInbox.ts`** — stop enabling draft selection (and its
root-status relay probing) for the mixed view; draft selection is scoped
to the Drafts filter.
- Drafts filter behavior is unchanged: the filter badge count,
`DraftsPanel` list, and `DraftDetailPane` all still work.
## Testing
- `pnpm test` (desktop unit suite): 3697 passed, 0 failed.
- `pnpm exec biome check src/features/home tests`: clean.
- Updated `inboxListRows.test.mjs` for the two-variant row model.
- Updated the e2e test (`channels.spec.ts`) to assert All never lists
drafts and that the draft is still reachable under the Drafts filter.
- Added `drafts-all-fix-screenshots.spec.ts` capturing both states
(screenshots below).
### All view — draft is gone, messages/reminders unaffected

### Drafts filter — the draft is still listed and editable

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
## Why
The Inbox surface was briefly renamed to **Activity** during #2045 and
picked up a bell icon to match. The name was reverted to **Inbox**
before merge, but the icon was not.
A bell says "notification tray." Inbox is a destination — a focused,
conversation-oriented place to catch up on work relevant to you,
including drafts and reminders that have nothing to do with
notifications. The glyph should say that.
## What changed
- Swap the sidebar entry from Lucide `Bell` to Lucide `Inbox`.
- Assert the icon in `inbox-refactor-screenshots.spec.ts`. Nothing
pinned it before, which is exactly how it drifted through a rename.
This also brings desktop back in line with mobile, which already uses
`LucideIcons.inbox300` / `inbox500` for the same destination.
## Deliberately unchanged
The bell on **reminder** rows in the list pane (`InboxListPane.tsx`,
reminders → bell, drafts → file) stays. A bell is the right glyph for a
reminder; that one was never about the surface's identity.
## Verification
- The new assertion is a real guard, not a no-op: with `Bell` restored
the test fails with `Expected: 1, Received: 0` on `svg.lucide-inbox`.
Confirmed before committing.
- `biome` and `tsc` clean.
- Playwright smoke: `inbox-refactor-screenshots` 4 passed; `smoke`,
`navigation`, `channels`, `sidebar-more-unread-overlap`,
`home-collapsed-top-chrome`, `workspace-rail` — 107 passed, 1 skipped.
- Screenshot below is the regenerated `02-current-controls` shot from
the spec.
Signed-off-by: Clay Delk <clay.delk@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The codex adapter version gate accepted any `major >= 1`, so a 1.x
`codex-acp` older than the version that fixes outbound relay access for
`buzz` CLI subprocesses classified as `Available` and was never offered
a reinstall. Only the 0.16.x `@zed-industries/codex-acp` adapter — which
fails `--version` outright — was caught.
`probe_codex_acp_version` now returns the full `(major, minor, patch)`
triple and `codex_adapter_availability` compares it against a new
`MIN_CODEX_ACP_VERSION` floor of `1.1.7`, the current npm latest. An
adapter below the floor classifies as `AdapterOutdated`, which routes it
through the existing uninstall-then-install reinstall plan.
The parse requires exactly three numeric dot-separated components.
Partial versions (`1.2`) and prerelease tags (`1.2.0-rc1`) return `None`
and therefore classify as `AdapterOutdated` — a version Buzz cannot
compare against the floor fails closed, offering a reinstall rather than
running an adapter of unknown vintage. Both the floor's bump policy and
the strict-parse behavior are stated in doc comments rather than left
implicit.
Supersedes [#3097](https://github.com/block/buzz/pull/3097) by
@Bharathchinneni, whose semver floor and behavior tests this carries.
That PR could not land as written: the two
`probe_codex_acp_major_version` compatibility wrappers it kept had no
non-test callers, which is a hard `clippy -D warnings` failure. The
wrappers are deleted here and their call sites collapsed onto
`probe_codex_acp_version`.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
**Category:** fix
**User Impact:** Composer block formatting now applies to the intended
line or selection without collapsing multiline content.
**Problem:** Block formatting from a Shift+Enter line could convert the
entire draft, selected visual lines could collapse into one list item,
and code conversion could lose line breaks. **Solution:** Scope caret
formatting to its hard-break-delimited line and normalize explicit
selections for the destination block type while preserving neighboring
content and visual line boundaries.
<details>
<summary>File changes</summary>
**desktop/src/features/messages/lib/selectionBlockFormatting.ts**
Scopes collapsed-caret block actions to the active visual line and
normalizes multiline selections for lists and code blocks.
**desktop/src/features/messages/lib/selectionBlockFormatting.test.mjs**
Adds unit coverage for caret-line isolation across line positions and
selection directions.
**desktop/src/features/messages/ui/FormattingToolbar.tsx**
Routes list, quote, and code-block actions through the selection-aware
formatting transaction.
**desktop/tests/e2e/composer-selection-formatting.spec.ts**
Covers caret-only formatting, multiline list conversion, list-to-code
conversion, preserved hard breaks, Markdown output, and backward
selections.
</details>
## Reproduction steps
1. In the desktop composer, enter several lines using Shift+Enter and
place the caret on one line.
2. Apply a bullet list, ordered list, quote, or code block; only the
caret line should change.
3. Select several Shift+Enter lines and apply a list; each visual line
should become its own item.
4. Select several list items and apply Code block; they should become
one multiline code block while unselected neighbors remain intact.
5. Select several Shift+Enter lines and apply Code block; each line
break should remain visible.
## Screenshots/Demos
<img width="508" height="222" alt="Screen Recording 2026-07-27 at 5 29
19 PM"
src="https://github.com/user-attachments/assets/35640dea-0cfb-44f1-9b0b-a993c69cb55f"
/>
Expected multiline code-block result:
https://buzz.block.builderlab.xyz/media/d2e2668093af3b67d896a32e9799daccd236da9fc9e24ec56ddb4ebf7d01dd96.png
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Merging main (7a6a2cda5) brought in two smoke specs written against the
old raw-key backup page, which this branch replaces with an
encrypted-by-default step: Next stays disabled until a backup exists,
and the raw nsec (nsec-reveal-toggle) sits behind an explicit 'Show raw
key instead' click. Both specs timed out in CI at 72f00afa8.
- harness-management.spec.ts (More-harnesses nav, from #3093): route
through the existing passThroughBackupStep helper instead of clicking
a disabled Next.
- onboarding-docked-cta-screenshots.spec.ts: click backup-show-raw-key
before the reveal toggle; the raw-key card is the surface the
screenshot exists to capture.
Test-only change; no product code touched.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Max's clean-room pass found the wipe -> import-from-file leg unreachable:
the file input accepted only .key,text/plain (macOS greys out the
identity.ncryptsec our own save dialog emits), and the first-launch
spotlight variant rendered no file input at all, leaving paste-only
recovery exactly where a returning user holds a backup file.
Accept .ncryptsec in the shared hidden file input, hoist it out of the
default-variant branch, and give the spotlight variant an 'Import from
a file' button wired to the same picker. Drop-zone behavior in the
default variant is unchanged. E2E covers the spotlight file path end to
end (accept attribute asserted explicitly, since setInputFiles bypasses
it) through decrypt-and-continue.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Tyler's spec names the settings entry "Password Backup". Rename the
row title and align its description (password, not passphrase) plus
the two doc comments that referenced the old row name. No behavior
change; test ids unchanged.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Goose's pre-[#2680](https://github.com/block/buzz/pull/2680) Windows
installer unpacked the CLI to `%USERPROFILE%\goose\goose.exe`. That
directory is on no standard `PATH`, and `common_binary_paths()` never
probed it, so users who installed Goose with the legacy installer stayed
permanently undiscovered — the residual half of #2239.
`resolve_command_uncached` finds binaries outside `PATH` only by
scanning `common_binary_paths()`, so adding the directory there is the
whole fix: Windows basename expansion already supplies
`goose.exe`/`.cmd`/`.bat`, and discovery, readiness probes, and spawn
all route through the same shared resolver. No Goose-specific resolution
path is introduced. The entry sits beside the existing Codex
`%LOCALAPPDATA%\Programs\OpenAI\Codex\bin` probe in the same
`#[cfg(windows)]` block.
The regression test is `#[cfg(windows)]` and is CI-reachable, not dead
code — the `desktop-build-windows` job runs `cargo test --manifest-path
desktop/src-tauri/Cargo.toml --target $env:TARGET` on `windows-latest`.
It asserts the probe list rather than planting a binary:
`common_binary_paths` is a process-lifetime `OnceLock`, so a test cannot
deterministically re-seed `USERPROFILE`, and planting an executable
under the real user profile is not an acceptable side effect. Verified
locally by widening the `cfg` to build on macOS — the test passes with
the probe and fails without it.
The `check-file-sizes.mjs` override for `managed_agents/discovery.rs`
moves 1835 → 1841, the exact post-`cargo fmt` gate count. Verified both
directions: 1841 passes, 1840 fails.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
`commands/agent_discovery.rs` was pinned at its 2167-line file-size
ceiling with zero headroom, blocking the install-supervision and
install-log work queued behind it. Install command *execution* is a
clean seam and moves into `commands/agent_discovery/install_exec.rs`
together with its tests, matching the existing `managed_node.rs` /
`post_install_verification.rs` split under the same module.
Moved: `INSTALL_MAX_ATTEMPTS`, `run_install_command_with_retry`,
`run_install_with_retry`, `install_failure_is_retryable`,
`install_retry_backoff`, `annotate_retry_attempts`,
`run_install_command`, `truncate_output`, `floor_char_boundary`, and the
install-retry test block. Command *construction*
(`install_shell_command`, `install_powershell_command`,
`build_install_command`) stays in the parent — the new module owns only
what happens once a `Command` exists. Public surface is exactly one
`pub(super) fn run_install_command_with_retry`.
The extraction is behavior-preserving, verified by diffing the moved
text against the original line ranges: the parent is original-minus-cuts
plus the intended edits, and the moved code is byte-identical except for
the `pub(super)` marker, the `build_install_command` →
`prepare_install_command` call site, and the new function described
below. Two parent imports (`std::io::Read`, `InstallStepResult`) became
unused and were dropped.
### Install working directory (#2245)
Absorbed from #3090. A packaged desktop launch inherits `/` as its
working directory, so installers that write relative to the CWD fail on
a read-only root. The new `prepare_install_command` builds the command
and applies `default_agent_workdir()`, and it is the only builder
`run_install_command` calls — so no spawn path can bypass the workdir.
This differs from #3090 in the test: that version spawned `pwd` through
the real install shell and deleted
`test_install_shell_command_returns_ok_on_unix` to make room. Here the
prepared `Command` is asserted directly via `get_current_dir()` —
hermetic, no shell spawn — and the existing test is kept.
### Tests
Four new, on top of the moved retry block:
- `test_prepared_install_command_uses_default_workdir` — every install
child carries `default_agent_workdir()`.
- `test_truncate_output_leaves_short_output_untouched` — under the cap,
byte-for-byte passthrough.
- `test_truncate_output_keeps_head_and_tail_with_marker` — over the cap,
both ends survive and the marker names the omitted byte count.
- `test_truncate_output_does_not_split_multibyte_characters` — the
boundary floor prevents a mid-codepoint cut.
`truncate_output` had no coverage anywhere before this.
### File-size gate
`check-file-sizes.mjs` override for `agent_discovery.rs` moves 2167 →
1808, the exact post-`cargo fmt` gate count — verified both directions
(1808 passes, 1807 fails). `install_exec.rs` is 458 lines and needs no
override; the default 1000-line limit covers it.
Related: #3090, #2245
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
## Summary
- Replace the permanently reserved composer activity row with a
conditional, content-driven accessory for channel and thread composers.
- Keep the composer dock geometrically stable while activity appears, so
the composer’s bottom edge resizes smoothly without shifting the
conversation.
- Preserve translucent backdrop blur during the resize with a stable
dock-level blur layer, and keep thread overlay/focus-mode alignment
consistent.
- Keep bottom-pinned virtualized conversations fully visible as composer
content, zoom, or viewport height changes without repinning readers who
have scrolled into history.
- Refine the activity lockup with aligned avatars/text and a subtle,
reduced-motion-safe shimmer.
- Centralize the dock’s quiet inset, activity rail, released space, and
activity offset in one CSS-variable geometry contract.
This takes a different, systemic route from the spacing reduction
proposed in #2602 and supersedes that approach.
### Related issue
Related PR: #2602
### Testing
- `./scripts/check-branch-skew.sh`
- `just desktop-check`
- `just desktop-test` — 3,638 passing
- `useAnchoredScroll.test.mjs` — virtualized viewport resize follows the
explicit bottom state
- Focused desktop smoke E2E — 6 passing across stable dock geometry,
multiline growth, viewport resize, reduced motion, blur ownership, and
thread overlay alignment
- Pre-push hooks passed for organization safety, branch skew, desktop
checks/tests, Rust tests, workspace tests, and desktop Tauri tests
- Mobile pre-push is independently red on latest `main`; the exact
`activity_page_test.dart` compiler failure reproduces on untouched
`origin/main`
- Visually tested channel and thread composers across quiet/activity
states, multiline composer growth, and thread overlay/focus mode
https://github.com/user-attachments/assets/ed0b08d9-18e0-4061-b272-ab509dbcd8ee
---------
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
## Problem
User report:
```
Buzz Node mismatch: Buzz supplies Node 24.14.0; OpenClaw requires >=24.15.0. All 10 ACP workers immediately crash.
```
Buzz supplies Node to agent processes from two places, and both were
below OpenClaw's floor:
| Supply path | Was | Now |
|---|---|---|
| hermit dev env (`bin/.node-*.pkg`) — the 24.14.0 in the report |
24.14.0 | **24.15.0** (newest hermit publishes; satisfies `>=24.15.0`) |
| Desktop managed runtime (`managed_node.rs` / `managed_node_paths.rs`)
| v24.11.0 | **v24.18.0** (current latest v24) |
The managed runtime sits **first** on the worker PATH
(`managed_agents/runtime/path.rs`), so a user-installed newer Node can't
mask a stale managed one — the pin itself has to move.
## Verification
- SHA-256 digests for all six platform artifacts taken from
`https://nodejs.org/dist/v24.18.0/SHASUMS256.txt`; darwin-arm64
independently re-verified by downloading the tarball (hash match),
extracting, and running `bin/node --version` → `v24.18.0`.
- All artifacts within `MANAGED_NODE_MAX_BYTES` (largest linux-x64 at 57
MB < 90 MB cap); tar.gz layout keeps the `node-vX-platform/bin/node`
shape `verify_node_tree` expects.
- `cargo test --lib` in `desktop/src-tauri`: **1801 passed, 0 failed**
at this commit; `cargo fmt --check` + `cargo clippy --lib -D warnings`
clean.
- Existing readiness check (`node --version == MANAGED_NODE_VERSION`)
makes upgrade automatic: installed v24.11.0 trees fail readiness and the
installer stages v24.18.0 atomically (rename with `.old` rollback —
existing logic, unchanged).
Note: CI `node-version: 24.14.1` pins in
`release.yml`/`windows-canary.yml` are build-env only (already `>=`
nothing OpenClaw touches) and left alone to keep this minimal.
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
## Summary
- keep a thread presentation-switch anchor pinned while focus/split
width reflow settles
- retire the temporary anchor only after resize correction and a
following paint confirm the row is visible
- preserve the existing external-target resolution behavior and viewport
E2E contract
## Root cause
The focus and split wrappers intentionally retain the same thread
surface, but switching wrappers also changes the message column width.
`useAnchoredScroll` centered the captured message once and immediately
cleared the one-shot layout target. A later text reflow could then move
that message outside the viewport with no remaining target to correct
it.
## Verification
- `pnpm check`
- `pnpm typecheck`
- `pnpm test` — 3,699 passed
- `pnpm build:e2e`
- `pnpm exec playwright test tests/e2e/thread-focus-mode.spec.ts
--project=smoke --repeat-each=10` — 20 passed
- push hook: branch-skew, Desktop check, and Desktop full unit suite
passed
---------
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Summary
- Adds a pure-TS operator parser (`from:` / `in:` / `after:` /
`before:`) with unit tests. Invalid date tokens stay in the FTS text.
- Extends `search_messages` so the desktop can send `authors` / `since`
/ `until` (and existing `#h`) on the filter the relay already
understands.
- Wires topbar search to strip operators from the prefix query, resolve
`in:` against local channels and `from:` against hex pubkeys / known
agents, then pass the structured fields through.
This is part 1 of #2853 (parser + command plumbing). Autocomplete chips
/ richer `from:@name` resolution can follow in a second PR.
## Test plan
- [x] `node --import ./test-loader.mjs --experimental-strip-types --test
src/features/search/lib/parseSearchOperators.test.mjs`
- [x] Added `search_messages_filter_emits_operator_fields` unit test
(full `buzz-desktop` crate build needs local sidecar binaries in this
environment)
- [ ] Manual: topbar `deploy from:<hex> after:2024-01-01` emits
authors/since on the bridge filter and returns narrowed hits
Made with [Cursor](https://cursor.com)
---------
Signed-off-by: Jatinder Mahajan <jatinder.mahajan@certifyos.com>
Co-authored-by: Jatinder Mahajan <jatinder.mahajan@certifyos.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
## Context
Adding an existing community by relay URL could fail with `Community
rejected: Load failed` even when its WebSocket endpoint was reachable.
The Add Community flow fetched `/api/join-policy` from the WebView, so a
relay without a matching CORS allowance blocked the policy request
before the app could join it.
## Summary
This bug fix fetches join policies through Tauri's native networking
layer for direct URL joins. Invite-code discovery, policy acceptance,
and signed invite claims remain on the WebView path so those operations
can migrate together later.
## Changes
- Uses native networking for Add Community and first-community direct
URL join-policy requests.
- Validates relay schemes, rejects URLs containing credentials, and
refuses redirects.
- Bounds declared and chunked native responses before JSON parsing.
- Preserves existing `404`, non-success status, malformed JSON, and
absent-policy behavior.
- Requires every join-policy caller to choose its transport explicitly.
Public relays using Buzz's default permissive CORS configuration are not
known to be affected.
### Related issue
Related to #2872.
### Testing
#### Reviewer-reproducible examples
End-to-end red/green requires a relay with restrictive CORS and a Buzz
identity authorized to join it.
##### Red: `main`
From a clean checkout of `main`:
```bash
. ./bin/activate-hermit
just staging
```
In Buzz Desktop:
1. Add another community so the restrictive-CORS relay can be removed.
2. Remove that relay.
3. Open Add Community and enter the relay's WebSocket URL.
4. Select Add Community.
Observed result:
```text
Community rejected: Load failed
```
##### Green: this PR
From a clean checkout of this branch:
```bash
. ./bin/activate-hermit
just staging
```
Repeat the same steps above.
Observed result:
```text
The community rejoins successfully.
```
Supporting checks:
- Six native join-policy tests, including oversized declared and chunked
responses.
- Four TypeScript API tests, including the native command contract.
- E2E build and four focused onboarding and sidebar Playwright tests.
- Full `just ci` and pre-push suites.
- Builderbot, Kalvin, and minimize-diff review fanout found no
actionable issues after the final rebase.