Repository navigation
Inbox refactor - #2045
Inbox refactor#2045
Conversation
wesbillman
left a comment
There was a problem hiding this comment.
Two user-visible inconsistencies need correction before this is ready:
Show unread onlyonly filters feed conversations.Allstill appends every pending reminder and active draft, so enabling the toggle can leave a list full of rows that have no unread state and cannot be marked read. Either define/filter personal-row unread state or make the control's scope explicit and keep those rows out of the unread result; add an E2E assertion covering mixed messages + reminders + drafts.- Reminder detail renders DM context as a channel (
#Alice) while the All row correctly rendersIn DM with Alice, contradicting this PR's stated location semantics. Use the channel type in detail and cover the DM case.
The branch is also behind current main; after these changes, rebase and require fresh CI before the final approval gate.
04d71bf to
1b87f18
Compare
|
🤖 @delkc This is a thoughtful rework. I’d simplify the scope and ship the strongest parts directly. Let’s just ship thisLet’s remove the Activity experiment and make these improvements directly to the existing destination. Maintaining two paths adds implementation and testing complexity without much benefit. Restore title navigationPlease make the channel/DM title link to the canonical conversation again. The external-link icon can remain, but it shouldn’t replace the larger, expected navigation target. Avoid detail-pane churn when switching filtersPreserve the selected conversation when it remains valid in the destination filter. Otherwise, transition directly to the first result on wide layouts—or the list on narrow layouts—without flashing intermediate content. Coverage for both cases would help lock this in. What we’d keep
Follow-up opportunities
This leaves the PR with one clear thesis: ship a focused, conversation-oriented work feed without customization, preference persistence, or an experiment fork. |
7293394 to
28f552a
Compare
|
Jumping online real quick to keep this moving along. I’m seeing failing checks from stale "Inbox" → "Activity" assumptions, plus failures in the new filter-transition coverage and live-mention flows. We’ll need those resolved and CI green before merging. Also, what did you think about the note re: chatting with Thomas before renaming this to “Activity”? |
95684ec to
61f580f
Compare
**Category:** improvement **User Impact:** Mobile users can scan Activity as a focused conversation inbox and open the exact unread message or thread represented by each item. ## Context Mobile's Activity tab had not kept pace with Desktop: it presented isolated event headlines, advertised categories that were often empty, and opened a channel without clearly landing on the selected item. This PR brings the Mobile surface toward the conversation-oriented direction explored in Clay Delk's Desktop [Inbox refactor PR #2045](#2045), while adapting it to Mobile rather than copying the Desktop split-pane implementation. The related product/UX discussion is captured in the originating [Buzz thread](buzz://message?channel=a9bbc0e5-d25d-4740-849c-93c34bb578a4&id=a7d9a4d33dcd8c6bf0dc67d81c328892b9e38dedaa8548920224ef388301b6ab). ## UX decisions in this PR - **Conversation-oriented, not event-oriented:** related updates collapse into one row per thread/DM conversation, represented by the latest update and ordered by latest activity. Separate top-level conversations in the same channel remain separate rows. - **Resume at the oldest unread:** tapping a grouped row opens the represented canonical message/thread/DM at its oldest unread item, rather than merely opening the channel at an arbitrary position. - **Desktop-aligned row hierarchy:** rows lead with a full avatar and sender, followed by contextual location/type metadata, unread dot + time, and a two-line preview. A **New** boundary separates unread and read content. - **Mobile-native navigation:** Mobile keeps a single-column `Activity → canonical conversation → Back` flow. It does not introduce Desktop's persistent detail pane. - **Compact filtering:** the old horizontal chip rail becomes a compact filter menu so the source set fits a phone viewport without horizontal scanning. Filters are All, Mentions, Threads, Needs Action, Activity, Agents, Reminders, and Drafts. - **Focused source semantics:** All covers personally relevant work—DMs, mentions, thread replies, needs-action events, owned-agent activity, due reminders, and active drafts—rather than becoming a generic stream of every channel message. Mobile's standalone Activity source is currently limited to DM traffic because it does not have Desktop's aggregated channel-activity feed. - **Shared read behavior:** rows project canonical channel/thread/message markers, support unread-only and mark-all-read, and use local overrides only where canonical markers cannot represent an item. - **Reminders and drafts are real data:** reminders use the same encrypted NIP-ER events as Desktop. Drafts persist device-local composer state, restore on return, survive failed sends, and clear after successful sends. - **Explain navigation failures:** an unavailable destination produces an explanatory message rather than silently doing nothing or falling back to an unrelated channel position. ## Implementation summary - Adds a Mobile inbox model for conversation grouping, category priority, contextual labels, sorting, filtering, and oldest-unread targets. - Expands relay-backed sources for mentions, approvals, owned-agent lifecycle events, and DM traffic. - Adds fail-closed NIP-ER reminder decryption and device-local compose-draft persistence. - Redesigns Activity rows, boundaries, filters, unread controls, and empty/loading states. - Routes rows through Mobile's existing canonical channel/thread screens with precise target IDs. - Adds model, provider, widget, reminder, read-state, draft-lifecycle, and deep-link coverage. ## Reproduction steps 1. Run Mobile and open **Activity**. 2. Confirm full avatars, sender-first rows, context labels, unread indicators, timestamps, two-line previews, and the compact filter control. 3. Open the filter menu and verify All, Mentions, Threads, Needs Action, Activity, Agents, Reminders, and Drafts. 4. Tap a grouped thread row and confirm the canonical conversation opens at its oldest unread message. 5. Mark rows read/unread, enable unread-only mode, and use mark-all-read; confirm state agrees with the channel/thread destination. 6. Type without sending in a channel or thread, leave, and confirm the draft appears in Activity and restores in the composer. ## Screenshots | Before — merge-base `dd222a509` | After — PR head `52ad40aee` | |---|---| | <img width="1206" height="2622" alt="image" src="https://github.com/user-attachments/assets/ae961b08-bf8a-4bd5-b487-f6321ae8d85b" /> | <img width="1206" height="2622" alt="image" src="https://github.com/user-attachments/assets/bcbe7ab0-552a-417c-9e85-7a85eb4592ee" /> | Recaptured on the same authenticated iPhone 17 simulator, account, theme, and Activity view, at this PR's current merge-base (`dd222a509`) and head (`52ad40aee`). Both frames were taken within a few minutes on the same live feed, so the visible conversation set overlaps closely (the recent Ned/Bart/Tommy items appear in both). The compared change is the row *structure*: Before leads with an `@ Mention` headline over a small inline avatar and a horizontal chip rail; After leads with a full avatar, a compact `labelMedium` sender label, contextual "Mentioned in" metadata, and a filter menu. The sender username now renders at the same compact scale the old `@ Mention` label used. ## Verification - Current rebased head: `5bd87f4f4` on `origin/main` at `dd222a509`; GitHub reports the PR mergeable. - `flutter analyze` — clean at `5bd87f4f4`. - Full Mobile suite — 698 passed, 1 skipped, 4 failed; all four failures reproduce identically on clean `origin/main` (`channels_page_test` create-channel sheet and three `compose_bar_test` agent-mention cases). - The prior PR-specific `home_page_test` failures were fixed by providing the Activity local-state dependency in that harness. - Independent code and simulator UI review — approved. - Post-rebase GitHub checks are running. --------- Signed-off-by: Taylor Ho <[email protected]> Signed-off-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> Co-authored-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
|
🤖 Refreshed screenshots. The earlier set was stale twice over — it predated the Projects filter and still showed the surface labeled Activity. Please review these instead of the older comment, which I've deleted. These are now generated by Filter menuAll, Projects, Mentions, Threads, Needs action, and Agents are message filters. A subtle divider separates them from the Reminders and Drafts work queues — no second labeled section. Counts sit beside their own filter label (Drafts shows 2 here), not on the generic filter button. Inbox label and controlsThe destination is Inbox with the standard Lucide bell icon. The overflow menu holds Show unread only and Mark all as read. DMs group into one conversation rowThree consecutive DMs from alice produce a single row ( Threads resume at the oldest unread replyA thread with three replies is one row. Opening it loads the full thread — including the root the reader has already seen — and selects the oldest unread reply, so intervening context isn't skipped. |
|
🤖 Status summary — every review item on this PR has been addressed. The requested changes on record predate the last two rounds of work, so here is where each one stands. Review items@wesbillman — @wesbillman — reminder detail showed a DM source as @tellaho — the detail pane title should navigate to the conversation. The @tellaho — switching filters churned the selection, and you asked for coverage of both layouts. @tellaho — you asked about talking to Thomas before renaming. @delkc has since settled it in the other direction: the surface stays Inbox, so the label question no longer blocks anything. An interim pass in this branch had renamed it to Activity; @tellaho — drop the experiment fork. Done. The preview-feature gate, Custom view, and default-view controls are all out of this PR; the focused behavior ships directly on the existing destination. @tellaho — CI needed to be green. It is, on Before / afterThe pre-refactor shots below are unchanged and still show what this PR moves away from. The "after" shots are the regenerated ones from the comment above. Navigation and filtersBefore, All mixed in generic channel updates and surfaced no Projects filter. Overflow controlsBefore, the toggle was worded generically and the counts sat on the filter button. DMsBefore, three consecutive DMs from the same person produced three rows, and a reminder appeared as its own generic feed row alongside them. Thread contextBefore, opening a thread reply landed on that single reply rather than the thread. Verification
@wesbillman @tellaho — ready for another look when you have time. |
Signed-off-by: Clay Delk <[email protected]>
Signed-off-by: Clay Delk <[email protected]>
Signed-off-by: Clay Delk <[email protected]>
Signed-off-by: Clay Delk <[email protected]>
Signed-off-by: Clay Delk <[email protected]>
Signed-off-by: Clay Delk <[email protected]>
Signed-off-by: Clay Delk <[email protected]>
Signed-off-by: Clay Delk <[email protected]>
Signed-off-by: Clay Delk <[email protected]>
Reverts the surface name to Inbox. Behavior is unchanged — this is naming only. Renamed files: - home/lib/activityListRows.ts -> inboxListRows.ts (+ test) - home/lib/activitySelection.ts -> inboxSelection.ts (+ test) - home/ui/ActivityFilterMenu.tsx -> InboxFilterMenu.tsx - home/ui/HomePersonalActivityDetail.tsx -> HomePersonalInboxDetail.tsx - home/useHomePersonalActivity.ts -> useHomePersonalInbox.ts Renamed identifiers: ActivityListRow, buildActivityListRows, resolveActivityFilterSelection, ACTIVITY_FILTER_OPTIONS, ACTIVITY_(UNREAD_)EMPTY_STATE_TITLES, ActivityLabel, isMixedActivityView, filterActivityInboxItems, matchesActivityAllView, and the RemindersPanel "activity-list" presentation variant. User-facing copy: sidebar label and tooltip, the "Inbox options" and "Filter inbox:" aria-labels, and two console error strings. Left alone deliberately: - the feed API contract (category "activity"/"agent_activity", the feed.activity/agentActivity keys, the types= query param) - "activity" where it reads as a plain English noun (empty states such as "No activity yet", latestActivityAt, PROJECT_ACTIVITY_KINDS) - pre-existing agent/project/profile activity code, which is unrelated Also restores the accurate preview-feature comments in tests/helpers/bridge.ts: they had been rewritten to describe an Activity feature gate that does not exist in preview-features.json, and seedPreviewFeaturesEnabled seeds every desktop feature. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Signed-off-by: Clay Delk <[email protected]>
The E2E screenshot-spec section told agents to run `pnpm run build` before re-running Playwright. That strips the mock Tauri bridge, which main.tsx compiles in only under `--mode e2e`, so every mock-mode spec fails with "Cannot read properties of undefined (reading 'invoke')" and the app renders "Community connection failed" instead of the UI under test — a build mistake that reads as a product bug. Point at `pnpm build:e2e` and recommend the `test:e2e:*` scripts, which already pick the right build. The justfile recipes (desktop-screenshot, desktop-e2e-changed) were already correct. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Signed-off-by: Clay Delk <[email protected]>
The PR screenshots for the Inbox refactor were generated by hand and went stale twice: once when the surface was briefly renamed to Activity, and again when the Projects filter landed. Capturing them in a spec means the next round regenerates instead of drifting. Covers the filter menu (including Projects and the Reminders/Drafts divider with counts), the Inbox sidebar label plus overflow controls, DM grouping into a single conversation row, and thread context anchored at the oldest unread reply. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Signed-off-by: Clay Delk <[email protected]>
ca20425 to
77359c4
Compare
Dismissing as stale. This review was submitted 2026-07-17 against commit 53cdf80, which is no longer in the branch after rebasing onto main. Both items it raised are fixed and both of its inline threads are resolved and outdated: (1) Show unread only now passes empty drafts/reminders into buildInboxListRows so the work queues drop out entirely — covered by "Inbox unread-only hides reminders and drafts from mixed All" in channels.spec.ts; (2) reminder DM sources render as "DM with " via formatReminderSourceLocation in RemindersPanel.tsx (d1ec212) — covered at channels.spec.ts:2861. tellaho has since approved at the current HEAD (77359c4) and CI is green there across all 25 checks. @wesbillman, please re-review if you would like another look.
* origin/main: (70 commits) feat(relay): make Postgres pool size configurable, default 50 (#3191) Publish symbol-bearing debug relay images (#3250) feat(tracing): add datastore tracing plumbing (#2760) fix(buzz-acp): accept id-keyed config options when resolving model switch (#2795) fix(desktop): probe legacy Goose install dir on Windows (#3248) refactor(desktop): extract install command execution into install_exec (#3251) Polish composer activity layout and transitions (#3151) feat(invites): add use-limited invite links (#3141) fix(node): bump Buzz-supplied Node runtimes past OpenClaw's >=24.15.0 floor (#3218) fix(desktop): preserve thread anchor through layout reflow (#3212) feat(search): parse from:/in:/after:/before: and pass them in the filter (#2871) fix(desktop): fetch join policies through native networking (#2862) fix(desktop): republish agent identity records when a persona rename propagates (#2607) fix(desktop): keep project Inbox previews compact (#3193) Inbox refactor (#2045) Fix composer selection formatting and drop overlay (#3172) Refine pending message status (#3153) feat(admin): show reported message content in report detail (#3149) fix(desktop): recover full local storage on startup (#3182) Replace mobile reconnect banners with skeleton shimmer (#3143) ... Co-authored-by: Tyler Longwell <[email protected]> Signed-off-by: Tyler Longwell <[email protected]>
## Buzz Desktop release v0.5.0 ### Changes since v0.4.26: - feat(invites): add use-limited invite links ([#3141](#3141)) ([`d500c2d5c`](d500c2d)) - fix(node): bump Buzz-supplied Node runtimes past OpenClaw's >=24.15.0 floor ([#3218](#3218)) ([`98a7b1334`](98a7b13)) - fix(desktop): preserve thread anchor through layout reflow ([#3212](#3212)) ([`9810d8545`](9810d85)) - feat(search): parse from:/in:/after:/before: and pass them in the filter ([#2871](#2871)) ([`cb2a265b5`](cb2a265)) - fix(desktop): fetch join policies through native networking ([#2862](#2862)) ([`0019f8076`](0019f80)) - fix(desktop): republish agent identity records when a persona rename propagates ([#2607](#2607)) ([`7ca0bbd94`](7ca0bbd)) - fix(desktop): keep project Inbox previews compact ([#3193](#3193)) ([`de1396050`](de13960)) - Inbox refactor ([#2045](#2045)) ([`2bd4c24b7`](2bd4c24)) - Fix composer selection formatting and drop overlay ([#3172](#3172)) ([`99da5b7eb`](99da5b7)) - Refine pending message status ([#3153](#3153)) ([`75588eaff`](75588ea)) - fix(desktop): recover full local storage on startup ([#3182](#3182)) ([`174c38e4b`](174c38e)) - fix(desktop): keep collapsed table separators out of spoilers ([#3169](#3169)) ([`4d8b676bb`](4d8b676)) - feat(desktop): redesign agent runtime settings ([#3093](#3093)) ([`d98da7389`](d98da73)) - fix(desktop): use forward slashes for git credential.helper on Windows ([#3023](#3023)) ([`899531684`](8995316)) - chore(desktop): add AgentCreationPreview file-size override to unblock main CI ([#3154](#3154)) ([`b92a1f4bf`](b92a1f4)) - fix(desktop): make the test loader work on Windows ([#2758](#2758)) ([`8bb43d519`](8bb43d5)) - fix(desktop): make lint and unit-test gates work on Windows ([#2943](#2943)) ([`545bb46b8`](545bb46)) - feat(desktop): add search to agent emoji picker ([#2630](#2630)) ([`313f793c8`](313f793)) - fix(desktop): keep identity key help dialog readable in dark mode ([#2854](#2854)) ([`be275cfc6`](be275cf)) - feat(acp): title agent sessions from the agent and channel name ([#3028](#3028)) ([`f2fe3b63c`](f2fe3b6)) - feat(git): use agent display name as git author name ([#3040](#3040)) ([`18eef633d`](18eef63)) - fix(deps): bump nostr to 0.44.6 for RUSTSEC-2026-0216 (NIP-44 remote DoS) ([#3135](#3135)) ([`31e2de196`](31e2de1)) - fix(desktop): read the newest pair-scoped harness log ([#3134](#3134)) ([`654f38490`](654f384)) - feat(desktop): handle project work from Inbox ([#3117](#3117)) ([`c5c4f390b`](c5c4f39)) - fix(desktop): clarify identity key button when key exists ([#2357](#2357)) ([`87b3fcd3c`](87b3fcd)) - Restore Goose and Buzz Agent to onboarding harness selection ([#2731](#2731)) ([`7fc0cc82d`](7fc0cc8)) - fix(desktop): render rich project work item content ([#3100](#3100)) ([`afb272bb7`](afb272b)) - feat(acp): bring your own harness (BYOH) — generic ACP runtime seam + settings gallery ([#2773](#2773)) ([`95fdf9788`](95fdf97)) - feat(desktop): use collective mesh routing for Auto ([#2825](#2825)) ([`16d4ec335`](16d4ec3)) - fix(desktop): strip legacy baked team instructions from stored prompts ([#3035](#3035)) ([`aee631448`](aee6314)) - feat(agents): lower default agent parallelism from 24 to 10 ([#3038](#3038)) ([`5d8ede446`](5d8ede4)) - Polish community rail and mobile pairing ([#2972](#2972)) ([`e6c90bb7c`](e6c90bb)) - fix(desktop): remove bundled libsystemd from AppImage ([#2353](#2353)) ([`a31fc4d2f`](a31fc4d)) - fix(desktop): make agent definition authoritative for model/provider/prompt ([#1968](#1968)) ([`8c0e8cb16`](8c0e8cb)) - chore(desktop): delete dead persona catalog UI cluster ([#2886](#2886)) ([`8e67cf399`](8e67cf3)) - fix(desktop): surface install failures hidden by curl-pipe exit codes ([#2892](#2892)) ([`166c6655e`](166c665)) - Refactor managed-agent runtime into cohesive modules ([#2974](#2974)) ([`74b63e184`](74b63e1)) - fix(desktop): make Linux AppImage GStreamer work on non-Debian distros ([#2176](#2176)) ([`cc6c4d347`](cc6c4d3)) - refactor(desktop): remove Agent directory section from Agents page ([#2290](#2290)) ([`5d1233e84`](5d1233e)) - fix(desktop): enable arboard Wayland backend so Linux copies reach the Wayland clipboard ([#2904](#2904)) ([`ab7aa8b12`](ab7aa8b)) - fix(desktop): supervise and re-arm relay-mesh runtime ([#2823](#2823)) ([`aa51dab9d`](aa51dab)) - fix(agents): run live Databricks discovery instead of the fallback list ([#2890](#2890)) ([`8eb6e3eb6`](8eb6e3e)) - fix(desktop): retire prepend mode on every reader wheel ([#2913](#2913)) ([`07d0265cf`](07d0265)) - fix(desktop): consolidate prepend scroll correction ([#2855](#2855)) ([`25e7864b3`](25e7864)) - fix(desktop): track concurrent agent turns up to the harness maximum ([#2882](#2882)) ([`20bff5910`](20bff59)) - fix(relay): preserve reconnect backoff ([#2759](#2759)) ([`499c5d349`](499c5d3)) - refactor(relay): expose reconnect timing policy ([#2310](#2310)) ([`2f0041595`](2f00415)) - fix(desktop): clear stale working badges on agent stop/restart ([#2803](#2803)) ([`a64cc71f6`](a64cc71)) - fix(desktop): surface agent rename relay profile sync failure as a warning toast ([#2279](#2279)) ([`5e3d2e484`](5e3d2e4)) - fix(discovery): inject PATH into Codex adapter planning ([#2767](#2767)) ([`6ab3835f3`](6ab3835)) **To release:** merge this PR. The tag and build will happen automatically. Signed-off-by: Wes <[email protected]>
…y-to-env * origin/main: (22 commits) Polish mobile message and search layouts (block#3121) Add mobile message image galleries (block#3312) chore(release): release Buzz Desktop version 0.5.0 (block#3213) feat(relay): make Postgres pool size configurable, default 50 (block#3191) Publish symbol-bearing debug relay images (block#3250) feat(tracing): add datastore tracing plumbing (block#2760) fix(buzz-acp): accept id-keyed config options when resolving model switch (block#2795) fix(desktop): probe legacy Goose install dir on Windows (block#3248) refactor(desktop): extract install command execution into install_exec (block#3251) Polish composer activity layout and transitions (block#3151) feat(invites): add use-limited invite links (block#3141) fix(node): bump Buzz-supplied Node runtimes past OpenClaw's >=24.15.0 floor (block#3218) fix(desktop): preserve thread anchor through layout reflow (block#3212) feat(search): parse from:/in:/after:/before: and pass them in the filter (block#2871) fix(desktop): fetch join policies through native networking (block#2862) fix(desktop): republish agent identity records when a persona rename propagates (block#2607) fix(desktop): keep project Inbox previews compact (block#3193) Inbox refactor (block#2045) Fix composer selection formatting and drop overlay (block#3172) Refine pending message status (block#3153) ... Signed-off-by: npub1x4hk035p3p9q39a3fcrd2fe30lpkrhr5dwe0cqzzjphxyyh8m0gsq4vqap <356f67c681884a0897b14e06d527317fc361dc746bb2fc0042906e6212e7dbd1@buzz.block.builderlab.xyz> Co-authored-by: npub1x4hk035p3p9q39a3fcrd2fe30lpkrhr5dwe0cqzzjphxyyh8m0gsq4vqap <356f67c681884a0897b14e06d527317fc361dc746bb2fc0042906e6212e7dbd1@buzz.block.builderlab.xyz>
## 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 <[email protected]> Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
* Add mobile message image galleries (#3312) ## What - group uploaded photos into full-width message carousels - add a fullscreen viewer with pinch zoom, double-tap reset, swipe-down dismissal, a centered filmstrip, and image actions - preload nearby display-sized images for smoother swiping and keep each upload as its own avatar-backed message ## Validation - `just mobile-check` - `flutter test test/features/channels/message_content_test.dart` - iOS 26.5 simulator gesture pass --------- Signed-off-by: kenny lopez <[email protected]> * Polish mobile message and search layouts (#3121) ## Summary - align message typography, avatars, metadata, and spacing across mobile surfaces - improve message follow behavior, touch feedback, and Activity popover motion - refine Search motion, gutters, and explicit recent-search history ## Snapshots ### Home  ### Activity  ### Search  ## Testing - `just mobile-check` - `just mobile-test` (749 passed, 1 skipped) --------- Signed-off-by: Taylor Ho <[email protected]> Signed-off-by: kenny lopez <[email protected]> Signed-off-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz> Signed-off-by: Wes <[email protected]> Signed-off-by: npub15w828kxsxu2684ynste0uah2jwkgatd99flt7ds4523hzm8ju6cshdr8hh <a38ea3d8d03715a3d49382f2fe76ea93ac8eada52a7ebf3615a2a3716cf2e6b1@buzz.block.builderlab.xyz> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> Co-authored-by: Taylor Ho <[email protected]> Co-authored-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz> Co-authored-by: Wes <[email protected]> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> Co-authored-by: npub15w828kxsxu2684ynste0uah2jwkgatd99flt7ds4523hzm8ju6cshdr8hh <a38ea3d8d03715a3d49382f2fe76ea93ac8eada52a7ebf3615a2a3716cf2e6b1@buzz.block.builderlab.xyz> * Refine mobile attachment picking (#3313) ## What - morph the composer plus button into the attachment menu, camera, and photo surfaces - add ordered multi-select with inline recent photos and system picker fallback - add native iOS attachment/photo popovers and align the Android camera treatment ## Stack - follows #3312 ## Validation - `just mobile-check` - `flutter test test/features/channels/compose_bar_test.dart` - full mobile pre-push suite --------- Signed-off-by: kenny lopez <[email protected]> * feat(chart): add relay pod extension points (#3322) ## Why Allow operators to install wrapper binaries and override the relay entrypoint without maintaining a duplicated Deployment outside the OSS chart. `extraManifests` can create independent resources but cannot extend the chart-managed relay Pod. ## What - Add opt-in init-container, volume, volume-mount, command, and args extension points - Preserve image defaults when extensions are empty and compose generic init containers with the MinIO readiness gate - Document the distinction from `extraManifests`, add schema coverage, and release chart 0.1.7 ## Risk Assessment Low — all new values are opt-in, and default rendered manifests are unchanged apart from version-derived metadata. Merge publishes a new chart version without modifying existing installations. ## References - [OpenTelemetry Collector Pod extensions](https://github.com/open-telemetry/opentelemetry-helm-charts/blob/main/charts/opentelemetry-collector/templates/_pod.tpl) alongside [extraManifests](https://github.com/open-telemetry/opentelemetry-helm-charts/blob/main/charts/opentelemetry-collector/templates/extraManifests.yaml) - [Argo CD extraObjects](https://github.com/argoproj/argo-helm/blob/main/charts/argo-cd/templates/extra-manifests.yaml) alongside component-scoped Pod extension hooks - `helm unittest` 0.8.2: 43/43 tests passed - Helm lint, schema validation, fixture renders, and chart packaging passed - Oracle review found no functional issues; its literal no-`tpl` regression test recommendation is included Generated with Amp --------- Signed-off-by: David Grochowski <[email protected]> Co-authored-by: Amp <[email protected]> * fix(composer): scope multiline block formatting (#3246) **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 <[email protected]> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> * feat(cli): add users set-status command for NIP-38 profile status (#3253) ## Summary The desktop client renders a persistent user status (NIP-38 kind:30315, `d:general`) as the status line on profiles, but the CLI had no way to set it — only ephemeral presence (`set-presence`, kind:20001). Integrations that want a scriptable, durable status line (for example a now-playing music bridge that shows the current TIDAL track on a profile) had no entry point. ## Screenshots <img width="1455" height="960" alt="1" src="https://github.com/user-attachments/assets/f1669ec6-212b-4f6e-ad53-07df9aacffc9" /> <img width="1455" height="960" alt="2" src="https://github.com/user-attachments/assets/5bf70f47-e5b5-4eb0-a426-b5f1ef90d2ec" /> This adds: ```bash buzz users set-status --text "Working on the relay" --emoji "🔧" buzz users set-status --text "" --emoji "🎶" # intentional emoji-only status buzz users set-status --clear # removes the status ``` - Signs and submits the replaceable kind:30315 event via the HTTP bridge (no WS needed — unlike presence, user status is a stored event). - Uses the `d:general` coordinate the desktop client already reads for the profile status line, and the same `emoji` tag shape `SetStatusDialog` publishes. - Event construction lives in `buzz_sdk::build_user_status()`, keyed off `buzz_core::kind::KIND_USER_STATUS`, so the CLI command is a thin sign/submit wrapper. Text and emoji are trimmed; a blank emoji is omitted rather than emitted as an empty tag. - Clearing is the explicit `--clear` flag, mutually exclusive with `--text`/`--emoji`. It publishes an empty-content event carrying only `d:general`, which the desktop treats as no status. `--text ""` with an `--emoji` is an emoji-only status, not a clear. --------- Signed-off-by: Kagan Yaldizkaya <[email protected]> Signed-off-by: Will Pfleger <[email protected]> Co-authored-by: Will Pfleger <[email protected]> * fix(desktop): gate codex-acp on a minimum supported version (#3254) 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 <[email protected]> * fix(desktop): restore the inbox icon in the sidebar (#3341) ## 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 <[email protected]> Co-authored-by: Claude Opus 5 (1M context) <[email protected]> * Unify mobile loading spinners (#3314) ## What - add the shared desktop-style arc spinner for mobile - replace app loading indicators with the shared component - preserve a static pose when reduced motion is enabled ## Stack - follows #3313 ## Validation - `just mobile-check` - focused spinner and pairing widget tests --------- Signed-off-by: kenny lopez <[email protected]> * docs: restructure DCO guidance into scannable subsection (#3337) Extracts the dense inline DCO paragraph from the "Before You Open a PR" section into a dedicated `### Sign Your Commits` subsection. ## What changed - Adds a `### Sign Your Commits` heading directly below the Conventional Commits paragraph - Leads with the command (`git commit -s`) in a code block - Follows with a plain-English explainer of what the sign-off does - Adds linkable `#### Fix unsigned commits already pushed` and `#### Auto-setup for future commits` subheadings - Removes the old inline paragraph (content preserved, structure only changed) ## Why The existing guidance was buried mid-paragraph; contributors may not find it until CI blocks them. This makes the requirement and its fix immediately visible and actionable. ## Notes Docs-only change, no code modified. Signed-off-by: Cameron Hotchkies <[email protected]> Co-authored-by: npub1ep9tf72jk6xgwamqj5m2j0xvqvwm9vdu3zxlz7cesxg53x52tkkqf6pa42 <c84ab4f952b68c8777609536a93ccc031db2b1bc888df17b198191489a8a5dac@buzz.block.builderlab.xyz> * fix(desktop): keep drafts out of the Inbox All view (#3217) ## 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 <[email protected]> * feat(desktop): refine agent catalog sharing (#2439) ## Summary - add custom-agent catalog sharing and hide built-ins from discovery - let owners publish later catalog updates from Share or while saving edits — the save always persists locally, and the publish reports `published` or `queued` (flushed automatically once the relay is reachable again) - preserve agent type, model, and runtime across snapshot import/export - simplify agent and team entry points and tighten catalog layout - migrate the legacy global retention queue into the owner's active scope so pending catalog publishes survive the upgrade - keep the agent list and edits usable in recovery mode by degrading to unshared projections when scope resolution or the retention DB fails - track catalog provenance on copied personas, so adding an already-added foreign agent resolves to the existing copy instead of creating a duplicate - scope inbound persona events to the community relay they arrived on - page the catalog read past the relay's 1,000-row query clamp - unify the share dialog's memory-level choice into a single "What's included" selector that drives both DM-send and copy-link delivery (all six combinations preserved), group the delivery rows above the option rows, and label the catalog toggle "Not shared" / "Shared" - keep emoji avatars on catalog entries — they persist as inline percent-encoded SVG, which the catalog projection's http(s)-only URL guard used to drop, so a shared agent showed initials instead of its avatar - drop the "Active in communities" card from Agents settings, superseded by the per-channel runtime controls in the members sidebar ## Screenshots ### Agent actions  ### Team avatar stack  ### Catalog sharing  ### Publish while editing  ### Publish from Share  ### Catalog details  --------- Signed-off-by: kenny lopez <[email protected]> Signed-off-by: Will Pfleger <[email protected]> Co-authored-by: Will Pfleger <[email protected]> * chore(compose): remove stale typesense env vars (#3332) Search migrated to Postgres FTS (commit f8bbe6efc). The Typesense container was removed from compose.yml and the Helm chart, but the cleanup missed two template/config files: - `deploy/compose/.env.example`: `TYPESENSE_API_KEY` and `TYPESENSE_PORT` are dead — no typesense service exists in compose.yml and the relay binary no longer reads `TYPESENSE_API_KEY`. The `CHANGE_ME_RANDOM_API_KEY` placeholder was never consumed, so removing it also unbreaks the sed loop in the blog draft (one fewer no-op secret to generate). - `benchmarks/harbor-buzz-orchestra/scripts/benchmark.py`: generates a typesense_api_key in state and writes `TYPESENSE_API_KEY` to the .env file it creates. - *Editing this file caused the https://github.com/block/buzz/blob/main/.github/workflows/benchmark-harbor.yml linter ci checks to run, which seemingly haven't run before, so I needed fix the lint issues to pass this.* --------- Signed-off-by: Kalvin Chau <[email protected]> Co-authored-by: npub1c4alndp82zyt9veaklm5d965quss79vlhk9awv7qu5erwhmf42qqlvc25c <c57bf9b4275088b2b33db7f746975407210f159fbd8bd733c0e532375f69aa80@buzz.block.builderlab.xyz> * feat(desktop): add custom harness inline from agent dialogs (#3252) 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 <[email protected]> * fix(acp): disable goose cron scheduler in managed agent children (#3144) A Buzz install with a scheduled goose recipe fires each cron entry once per `goose acp` child instead of once, because every child unconditionally starts its own cron scheduler over the shared `~/.local/share/goose/schedule.json`. With a pool of N children per harness and multiple harnesses, one scheduled recipe fans out to N × harness_count executions — each running under the managed agent's identity rather than the operator's, and racing the operator's own standalone goose over the same schedule file. This injects `GOOSE_ACP_SCHEDULER_DISABLED=true` into every child spawned by `AcpClient::spawn`, so a managed agent never owns the operator's cron schedule. ## Placement The `cmd.env` call is set last — after the `extra_env` operator-wins loop and after the `CODEX_CONFIG` merge — deliberately with no escape hatch. Managed children not running the operator's schedule is a correctness invariant rather than an operator-tunable default, so the injection must beat both a conflicting persona `extra_env` entry and any value inherited from the parent process. It is injected for all agents, not just goose. Agent builds that don't recognize the variable ignore it. ## Sequencing The goose-side flag that reads this variable and skips scheduler startup lands separately (repo TBD). Until it does, this change is a forward-compatible no-op: it sets an environment variable nothing currently reads. Merging it first means no coordinated release is needed — the fix takes effect as soon as the goose side ships. Related: https://github.com/aaif-goose/goose/pull/10738 Signed-off-by: Will Pfleger <[email protected]> * fix(desktop): paint community rail full height (#3382) ## 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 <[email protected]> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> * docs(contributing): document the Linux system libraries just ci requires (#3396) ## Problem The prerequisites table lists language toolchains (Rust, Node, pnpm, Flutter, Docker, `just`) but no system libraries. Hermit pins the former and not the latter, so following the setup section exactly on Linux still leaves `just ci` unable to run: it fails partway through its first dependency, `just check`, at `desktop-tauri-clippy`. ``` The system library `gdk-pixbuf-2.0` required by crate `gdk-pixbuf-sys` was not found. The file `gdk-pixbuf-2.0.pc` needs to be installed and the PKG_CONFIG_PATH environment variable must contain its parent directory. ``` The desktop crates link against GTK and WebKitGTK. CI installs those packages explicitly, so it never sees this — which is exactly why the gap is invisible from the maintainer side. Since `check` runs first in the `ci` chain, the failure also masks everything after it (`test-unit`, `desktop-test`, `web-build`, `mobile-test` never run), which makes it read as a broken repo rather than a missing dependency. ## Change Adds a `#### Linux: Tauri system libraries` subsection under Prerequisites with: - The apt list copied from `.github/workflows/ci.yml`, so a local run matches CI rather than drifting from it - A pointer to [Tauri's prerequisites](https://tauri.app/start/prerequisites/) for non-Debian distributions - A note that server-side contributors can skip it — `just fmt-check`, `just clippy`, `just test-unit`, and `just test` need no GTK Docs only. No TOC entry needed, since the TOC lists `##` headings and this is a `####` subsection. ## How I hit it Running `just ci` before pushing #3372, on Ubuntu under WSL2 with the Hermit toolchain active and all Docker services healthy. Everything the guide asks for was in place. The four `check` steps before `desktop-tauri-clippy` (`fmt-check`, `clippy`, `desktop-check`, `desktop-tauri-fmt-check`) passed, which is what makes the failure point specific rather than a general build problem. ## Closest existing work None found. I searched open and closed issues and PRs for `gdk-pixbuf`, `libgtk`, `webkit2gtk`, `system dependencies`, `prerequisites`, `just ci`, and `linux setup`. The Linux/GTK issues that exist (#2604, #2643, #2982, #2811, #2562) are all runtime bugs in shipped builds, not setup-path failures. ## Verification The package list is transcribed from `.github/workflows/ci.yml:152-163`; the same list appears in `release.yml` and `linux-canary.yml`. I have not installed the packages on my machine, so I can confirm the failure and the source of the fix but not that the list is exhaustive on a clean box — worth a second pair of eyes from anyone who has done a fresh Linux setup recently. Signed-off-by: Kyler Cao <[email protected]> * fix(desktop): stabilize flaky DM expansion E2E ordering assertions (#2004) ## 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 <[email protected]> Co-authored-by: Goose <[email protected]> * feat(desktop): apply WebKit rendering workarounds at startup on Linux (#3271) 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 <[email protected]> * feat(acp): steer claude-code and codex agents via _session/steering (#3007) Mid-turn steering was reachable only through goose's `_goose/unstable/session/steer`, which requires an `expectedRunId` sourced from `_meta.goose.activeRunId`. claude-agent-acp and codex-acp never emit a run id, so every mid-turn mention to those harnesses bailed at the run-id guard before writing a byte and degraded to cancel + merge, destroying in-flight tool calls. Both adapters ship `_session/steering` (params `{sessionId, prompt}`, result `{outcome}`) and advertise it as `_meta.steering.supported` on the `initialize` response. This adds it as a second steer transport selected at write time, reusing the existing withhold/release, ack routing, and cancel+merge fallback machinery unchanged. ## Transport selection | `active_run_id` | `steering_supported` | Transport | |---|---|---| | `Some(run_id)` | any | `_goose/unstable/session/steer` + `expectedRunId` (unchanged) | | `None` | `true` | `_session/steering` with `{sessionId, prompt}` | | `None` | `false` | ack `ExpectedRunIdMissing`, write nothing (unchanged) | goose keeps priority when both are present — `expectedRunId` is strictly more precise about *which* run is being steered. ## Two load-bearing safety properties **The advertised capability is the only gate — never error-code probing.** codex-acp's `extMethod` answers unrecognized extension methods with a bare `{}`, which is a JSON-RPC *success* rather than `-32601`. Buzz maps a steer success to `queue.remove_event`, so probing an unknown method would silently delete the user's message with no error, no fallback, and no log line. **An `outcome` must be positively recognized.** Only `injected` and `startedNewTurn` count as delivery. Anything else — codex's `failed`, an unknown value, or a missing `outcome` entirely — is `SteerError::OutcomeRejected`, which releases the withheld event and fires the cancel+merge fallback. This makes the silent-loss path above unreachable even if an adapter mis-advertises. `startedNewTurn` acks `Success`, because the message really was delivered and must not be redelivered, but deliberately does **not** renew the read loop's hard deadline: the turn Buzz was awaiting had already settled, and renewing would extend the clock on a finished turn. ## Notes for reviewers - `SteerError::OutcomeRejected` needs no new arm in the `PoolEvent::SteerAck` match — the existing catch-all `Ok(SteerAck::Err(_)) => (true, false, true)` already gives release + fallback, and the two `AgentError` arms above it match that variant specifically, so they do not shadow it. - Comments that described the old goose-only "try-and-tolerate" `-32601` behavior are corrected; that assumption was never valid for codex-acp. - No CI job runs `buzz-acp` tests. The full package suite was run locally: **617 passing, 0 failing**. Signed-off-by: Will Pfleger <[email protected]> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> * release(chart): publish 0.1.7 (#3393) ## Why Publish chart 0.1.7 after the feature PR merged from a fork and therefore intentionally skipped the internal-branch auto-tag job. ## What - Trigger the `chart-release/0.1.7` release lane - Update the quickstart example to reference chart 0.1.7 ## Risk Assessment Low — the chart implementation is already merged and tested; this PR creates its immutable release tag and OCI artifact. ## References - Chart implementation: https://github.com/block/buzz/pull/3322 - `helm unittest` 0.8.2: 43/43 tests passed - Local pre-push checks passed Generated with Amp Signed-off-by: David Grochowski <[email protected]> Co-authored-by: Amp <[email protected]> * chore(ci): bump desktop smoke E2E timeout to 30 minutes (#3409) Three main-branch runs today had shards killed at exactly 20m17s ("exceeded the maximum execution time of 20m0s"); the killed shard was actively passing tests seconds before the cap. Shard runtime has grown to the limit. 30 matches the other desktop jobs in the same workflow. Signed-off-by: Will Pfleger <[email protected]> * fix(ci): ratchet file sizes against the base tree (#3352) ## 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 <[email protected]> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> * fix(desktop): clear stale thread new-message pill (#3411) ## 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 <[email protected]> * feat: add explicit entry for claude-opus-5 in model config (#2831) 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 <[email protected]> Co-authored-by: Apurva Shaw <[email protected]> * fix(relay): avoid subscription lock inversion (#3413) ## Summary - drop the `subs` DashMap guard before mutating subscription indexes - snapshot fan-out candidate vectors so index guards are dropped before looking up `subs` - add concurrent fan-out/replacement regression coverage ## Why `fan_out_scoped` previously held an index guard while `push_match` acquired `subs`, while CLOSE and same-ID replacement held `subs` while removing from an index. The reverse ordering made an AB/BA deadlock reachable and could synchronously park all Tokio workers. ## Validation - `rustup run 1.95.0 cargo test -p buzz-relay` — 769 library tests passed, 33 ignored; 11 binary tests passed; doc tests passed - push hooks with pinned Rust 1.95 — branch-skew, repository Rust suites, and desktop Tauri suite passed - `git diff --check` ## Residual risk Fan-out now clones bounded candidate vectors before matching. This adds allocation/copy cost proportional to the indexed candidate set, in exchange for eliminating nested DashMap guards. This fixes the concrete lock cycle but does not prove every observed production wedge had this cause. --------- Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> * fix(acp): per-runtime env defaults at spawn — isolate Hermes from configured MCP startup (#3420) ## Summary - add a generic per-runtime env-defaults table, `config::default_agent_env()`, mirroring the existing `default_agent_args()` / `codex_network_env()` precedent, and merge it once in `AcpClient::spawn` with the established precedence: **runtime defaults < persona `extra_env` < inherited parent env** - first (and only) row: Buzz-owned Hermes processes get `HERMES_ACP_SKIP_CONFIGURED_MCP=1`, so Hermes does not preload unrelated profile-configured MCP servers before answering ACP `initialize` (fixes the 10s model-discovery timeout in #3355 — Buzz supplies session MCP servers explicitly through `session/new`, per Hermes's documented host-integration contract for this variable) - normalize Windows `.cmd`/`.bat` shims alongside `.exe` in `normalize_agent_command_identity` (npm installs resolve to those wrappers) - switch the `extra_env` parent-presence check from `var()` to `var_os()` so non-UTF-8 parent values are honored Replaces the runtime-specific approach in #3386: same behavior, but the mechanism is generic runtime spawn metadata in `config.rs` rather than a Hermes/ACP special case in `acp.rs`, and the seam covers every launch path (Desktop spawn, `buzz-acp models`, CLI) because they all funnel through `AcpClient::spawn`. ~15 lines of production code. Fixes #3355 ## Testing - `cargo test -p buzz-acp` — **639 passed, 0 failed** (full package, includes the new `default_agent_env_recognizes_hermes_identities` unit test and `spawn_applies_runtime_env_defaults_with_extra_env_precedence` integration test covering default injection, extra_env override, and non-Hermes exclusion) - `cargo fmt --all -- --check`, `cargo clippy -p buzz-acp --all-targets -- -D warnings` — clean - live-local with real Hermes v0.19.0 (`hermes-acp`): `buzz-acp models` returned **13 models / currentModelId in 2.6–3.0s** (was a 10.0s timeout on the first cold run without isolation); a wrapper probe confirmed the child received `HERMES_ACP_SKIP_CONFIGURED_MCP=1` by default and `0` when the parent env set it explicitly (operator wins) - lefthook pre-push suite green: rust-tests, desktop-check, desktop-test, desktop-tauri-test, mobile-test, branch-skew No UI changes; subprocess environment behavior only. Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: Tyler Longwell <[email protected]> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Tyler Longwell <[email protected]> Co-authored-by: mr-r0b0t.eth <[email protected]> * Fix mobile attachment and gallery polish (#3370) ## Summary - align mobile message metadata and enlarge attachment-menu content - smooth keyboard-to-camera/photo transitions and initialize the iOS photo grid at the intended scale - fix horizontal gallery loading, edge overflow, and end spacing ## Why The attachment surfaces were reacting to keyboard and compact-menu geometry during presentation, while gallery clipping and image lifecycle behavior caused misalignment and occasional blank previews. ## Testing - `just mobile-check` - `flutter test` (881 passed, 1 skipped) - native `RunnerTests` (17 passed) - verified standalone Release build on a physical iPhone --------- Signed-off-by: kenny lopez <[email protected]> * feat(agent): fix Anthropic prompt caching with Databricks (+ MCP proxy/TLS passthrough) (#3463) > On 8 tasks matched by name across the two runs, cost fell $8.36 → $1.77 (4.71×) and wall-clock 12,423 s → 1,085 s (11.45×). ## Summary Two independent, self-contained fixes to `buzz-agent`/`buzz-acp`, split out of the benchmark branch so they can land while the harness work continues: 1. **Request and surface Anthropic prompt caching.** buzz never sent a `cache_control` breakpoint, so on the Databricks Anthropic route `cache_read_input_tokens` was **structurally always 0** and the ~10× cache-read discount was never claimed. This teaches `anthropic_body()` to mark the cacheable prefix, and plumbs the cache split end-to-end so accounting can price it. 2. **Pass proxy + TLS-trust env into MCP tool subprocesses**, so agent tools on a proxy-only host stop reporting a live network as offline. ## Why the caching gap matters The Anthropic Messages API does **not** cache unless the request carries a `cache_control` breakpoint, and the Databricks AI Gateway — a third-party proxy in front of the model, in the same category as Bedrock/Vertex — does **not** auto-cache (only the first-party Anthropic API and Claude-on-AWS do zero-config caching). So every request was billed cold. Measured live against the Databricks gateway (`databricks-claude-opus-5`, 2026-07-28), the same call with and without a single `cache_control` marker: | Run | `input_tokens` | `cache_creation` | `cache_read` | latency | |---|---|---|---|---| | No `cache_control`, two byte-identical calls | 121,625 | 0 | **0** | ~9.3 s | | With one marker — cold (write) | 4 | 121,625 | 0 | 9.3 s | | With one marker — warm (read) | 4 | 0 | **121,625** | **4.5 s** | One marker moved 121,625 tokens from full-price input to a 0.1× cache read and roughly halved latency (a clean, isolated ~2.07× prefill speedup on this single-threaded microbenchmark). The gateway honours `cache_control`; buzz simply never sent it. At fleet scale this was a real budget item. Across matched Terminal-Bench solo sweeps (89 tasks, `-n 20`, before the fix), the two OpenAI-route models independently landed at ~86–87% cache reads — the expected shape for an agentic loop, where system + tools + append-only history repeat every turn — while the Anthropic route returned a hard 0% on every receipt: | Condition | Route | Input tokens | Cache reads | Cost | Cost if uncached | Discount | |---|---|---|---|---|---|---| | luna (`gpt-5-6`) | OpenAI | 20,320,818 | **17.7M (87.0%)** | $6.96 | $22.87 | **3.28×** | | sol (`gpt-5-6`) | OpenAI | 22,312,290 | **19.2M (85.9%)** | $37.07 | $123.35 | **3.33×** | | opus (`claude-opus-5`) | Anthropic | 12,459,822 | **0 (0.0%)** | $81.31 | $81.31 | **1.00×** | Applying luna's measured 87% read rate to the opus token counts at list prices (`input $5/M`, `cached_input $0.5/M`, `output $25/M`) puts the opus run at **~$32.53 vs the $81.31 actually paid — a ~60% overspend on those 49 trials (~$89 on a full sweep)**. That is an upper bound (it prices every cached token at the 0.1× read rate and ignores the 1.25× write premium), and the opus discount is structurally smaller than luna/sol's because opus emits ~3.5× more uncacheable output per trial, which sets a floor on what caching can recover. There is also a plausible **second-order effect**: Databricks appears to meter its per-minute rate limit on *uncached* input tokens, so the missing cache also cost rate-limit headroom — the opus endpoint lost 63% of its trials to fatal 429s while running alone at one-third of a GPT endpoint's raw throughput. This is a hypothesis, not a proven mechanism (the only zero-cache condition is also the only Anthropic endpoint), but it is the reading that explains the throttling with one rule instead of two. ## Post-fix results (provisional — first trials of an in-flight re-run) On 8 tasks matched by name across the two runs, cost fell **$8.36 → $1.77 (4.71×)** and wall-clock **12,423 s → 1,085 s (11.45×)**. | Metric | before (`4a955a858`) | after (`3bef1f6a`) | |---|---|---| | Cache reads as % of input | **0.0%** | **78.7%** (still climbing toward the ~86% steady state) | | `cost_usd_no_cache_discount / cost_usd` | **1.00×** | **2.18×** (tracking the projected ~2.5×) | | Trials with a fatal 429 (same `-n 20`) | **63%** | **15–19%** | To be clear about attribution: **~2× of that is the clean prefill saving from caching itself**; the rest is second-order — cached requests burn far less rate-limit budget, so they stall less and redo less destroyed work. The 11.45× is a system-level result specific to this throttled workspace, not a caching benchmark. Quality held (7/8 solved in each run). A controlled low-`-n` A/B (neither arm hitting a 429), which the `BUZZ_AGENT_PROMPT_CACHING` opt-out exists to enable, is still owed before this becomes a published claim. ## What changed ### 1. Request caching (`llm.rs`, `config.rs`) `anthropic_body()` emits ephemeral `cache_control` breakpoints, gated by `BUZZ_AGENT_PROMPT_CACHING` (**default on**, `=0` to opt out): - **Static prefix** — marker on the `system` block. Prefix order is `tools → system → messages`, so this single marker caches **tools + system** together. Byte-identical on every turn of a run, and survives a context handoff (system/tools come from cfg/mcp, not `self.history`). - **Rolling tail + leapfrog** — marker on the last block of the last **two** messages. The append-only history re-reads the prior turn's prefix from cache; marking two messages (not one) keeps consecutive breakpoints inside Anthropic's **20-block lookback window** even as tool parallelism rises, avoiding a silent full-price miss. An empty system prompt stays a bare string (Anthropic rejects empty text blocks), and below-threshold prefixes are silently not cached, so the flag is safe on by default. ### 2. Surface the cache split end-to-end — the plumbing (`types.rs`, `llm.rs`, `agent.rs`, `lib.rs`, `usage.rs`, `acp.rs`) This is the part that makes gaps like the one above **visible** instead of silent. A consumer that prices all of `input_tokens` at the full rate can't tell a route that's caching from one that isn't — the total looks right either way. So: - `LlmResponse` gains `cached_input_tokens` (a **subset** of `input_tokens`, never an addition); `parse_anthropic` / `parse_openai` / `parse_responses` each populate it. - A `usage_first()` helper reads the cache count wherever a provider hides it — flat `cache_read_input_tokens` (Anthropic), `prompt_tokens_details.cached_tokens` (OpenAI chat), `input_tokens_details.cached_tokens` (Responses) — taking the **first present value, never a sum**. Reading only flat keys is exactly why the OpenAI route's nested `cached_tokens` had *also* been going unclaimed: `prompt_tokens` is already inclusive, so the total looked correct while the discount silently went unreported. - The per-turn/per-session accumulators and the goose `usage_update` payload now carry `accumulatedCachedInputTokens`; `buzz-acp` deserializes it (`serde` default `0` for goose, which doesn't send it) and logs `cached=<n>`. ### 3. Fix a Databricks MLflow-route double-count (`llm.rs`) The Databricks MLflow route reports the flat Anthropic-spelled `cache_read_input_tokens` *alongside* an already-inclusive `prompt_tokens`, so the old code summed them and nearly doubled the count — inflating both the context-budget gate and cost. `openai_chat_input_tokens()` now reads `prompt_tokens` alone. Verified on a live `databricks-glm-5-2` response where `prompt_tokens + completion == total` proves inclusivity. (Anthropic's native route genuinely *excludes* the cache fields and is still summed — the two never collide, because `claude*` models route to the Anthropic path.) ### 4. Proxy + TLS-trust passthrough into MCP tools (`mcp.rs`) — independent fix `buzz-agent` `env_clear()`s each MCP child, and the allowlist carried no proxy/TLS vars. On a proxy-only host that doesn't degrade the tools, it **blinds** them: apt, curl, pip, git connect directly, the egress firewall resets the socket, and the agent reports "Connection reset by peer" — indistinguishable from a genuinely offline task. Adds both spellings of `HTTP(S)_PROXY`/`NO_PROXY`/`ALL_PROXY` (curl/git read lowercase; Go/Python read uppercase; libcurl ignores uppercase `HTTP_PROXY`) plus `SSL_CERT_FILE`/`SSL_CERT_DIR` for TLS-terminating proxies that present their own CA. ## Testing - `cargo fmt --all -- --check`, `cargo clippy -p buzz-agent -p buzz-acp --all-targets -- -D warnings` — clean. - `cargo test -p buzz-agent -p buzz-acp` — **all green** (632 + 299 lib tests plus integration suites, 0 failures). New tests cover: the three breakpoints and the disabled/empty-system/single-message edge cases; nested-vs-flat cache parsing for all three routes; the Databricks inclusive-`prompt_tokens` fix; wire deserialization of `accumulatedCachedInputTokens`; and the proxy/TLS passthrough allowlist. - Pre-push lefthook suite green (branch-skew, rust-tests, test, desktop-check/test/tauri). ## Relationship to the benchmark branch These are the non-`benchmarks/` changes from `benchmark/harness-accounting-and-solo`, lifted onto a clean base off `main` so they can merge independently. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Atish Patel <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> * Polish mobile typing indicator (#3528) ## Summary - Present channel and thread typing status in a composer-matched container. - Animate the strip so the message list moves smoothly as typing begins and ends. - Increase typing-label contrast and avatar/padding for readability. ## Pixel 10 snapshot  ## Validation - `flutter test test/features/channels/channel_detail_page_test.dart` - `flutter analyze` Signed-off-by: kenny lopez <[email protected]> * Refine community invite limits (#3529) ## 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 <[email protected]> * feat(agent): route Claude/GPT model families to their native gateway wire (#3538) ## Summary Databricks v2 chooses the gateway wire format — OpenAI Responses, Anthropic Messages, or MLflow chat — purely from substrings in the endpoint name. There is no family field on the endpoint to key off, so the substring set *is* the routing contract. The matcher only recognised `gpt-5`/`gpt5` and `claude`, which makes correct billing depend on every Claude endpoint happening to be named with the literal string "claude". ## Why this matters Getting a Claude model onto the Anthropic Messages route is exactly what lets buzz attach the `cache_control` breakpoint (the fix in #3463). If a Claude endpoint's catalog name omits "claude" — an alias, a bare `opus-5`, a `goose-opus-5` — it silently falls through to the MLflow (OpenAI-wire) path, where Anthropic prompt caching is **structurally impossible**. The result is the same failure #3463 fixed: 0% cache reads, the full ~10x read discount lost, and no error — a naming convention quietly holding up a billing-correctness invariant. ## What changed `databricks_v2_route_for_model` (`crates/buzz-agent/src/llm.rs`) now matches broader, case-insensitive marker sets: - **Claude → Anthropic Messages:** `claude`, `opus`, `sonnet`, `haiku`, `mythos`, `fable` — the Claude family names and release code names, so a Claude endpoint reaches the cache-capable route regardless of how it's named. - **GPT → OpenAI Responses:** the `gpt` family (now `gpt` on its own, not just `gpt-5`) plus the GPT-5 launch code names `sol`, `luna`, `terra`. OpenAI markers are evaluated first, preserving the prior `gpt-5`-first precedence for any name that could carry both. Names matching neither set still fall through to the MLflow chat route. ## Testing - `cargo fmt`, `cargo clippy -p buzz-agent --all-targets -- -D warnings` — clean. - `cargo test -p buzz-agent` — all green (299 lib + integration suites, 0 failures). The `databricks_v2_routes_by_model_family` test was expanded to cover each new marker, the GPT-5 code names, case-insensitivity, and the unchanged MLflow fallback (including `gemini`). ## Relationship to #3463 #3463 taught the Anthropic path to request caching; this makes sure Claude models actually land on that path. Follow-up still open: surfacing `cache_creation_input_tokens` end-to-end so a persistent `reads == 0 && writes == 0` reveals a disabled cache regardless of which wire a model takes — happy to do that next. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Atish Patel <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> * Polish mobile navigation and menus (#3486) ## Summary - Add a shared footer fade behind the floating tabs on Home, Activity, and Search. - Use a shared anchored popover for Activity filters and section actions, with working section move controls. - Polish message grouping/press states and remove the initial Search back button. <img width="630" height="1368" alt="Screenshot 2026-07-29 at 08 49 37" src="https://github.com/user-attachments/assets/9e787adf-0bb3-49c6-8224-5819e8cfb1ad" /> ### Testing - `flutter analyze` - `flutter test` - Release build installed and checked on a connected iPhone ### Screenshots A real-device Activity baseline showing the original solid footer is attached in a PR comment. The updated review build was checked on the connected iPhone. --------- Signed-off-by: kenny lopez <[email protected]> * fix(desktop): preserve shared agent fidelity (#3553) ## 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 <[email protected]> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> * perf(desktop): move observer-feed archive and decrypt commands off main thread (#3415) 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 <[email protected]> * Run Tauri clippy in pre-push (#3555) ## Summary - run Desktop Tauri clippy from pre-push for every path that can affect the Tauri crate - reuse `just desktop-tauri-clippy`, keeping the local command identical to Desktop Core CI - leave the existing Tauri test hook unchanged ## Why PR #3553 exposed a hook gap: `cargo test` allowed an unused-import warning that CI's `clippy -D warnings` correctly rejected. Running the same recipe before push catches that class of failure locally without duplicating CI flags in Lefthook. ## Validation - `lefthook run pre-push --command desktop-tauri-clippy --force` - confirmed it invokes `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --all-targets -- -D warnings` - command passed Signed-off-by: Wes <[email protected]> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> * chore(release): release Buzz Desktop version 0.5.1 (#3566) ## Buzz Desktop release v0.5.1 ### Changes since v0.5.0: - perf(desktop): move observer-feed archive and decrypt commands off main thread ([#3415](https://github.com/block/buzz/pull/3415)) ([`294c8c821`](https://github.com/block/buzz/commit/294c8c821de51442a8c384c0bdb66b1a10224ca0)) - fix(desktop): preserve shared agent fidelity ([#3553](https://github.com/block/buzz/pull/3553)) ([`f7a3988ba`](https://github.com/block/buzz/commit/f7a3988ba13b590d9a55a7e8413fc3fb5ffbef18)) - feat(agent): route Claude/GPT model families to their native gateway wire ([#3538](https://github.com/block/buzz/pull/3538)) ([`6438dedf8`](https://github.com/block/buzz/commit/6438dedf83a9dbe1853e484326911bf6c7f1618c)) - Refine community invite limits ([#3529](https://github.com/block/buzz/pull/3529)) ([`24d90d128`](https://github.com/block/buzz/commit/24d90d1280a9325c6cbcf8eea30ac54db5afd2cb)) - feat(agent): fix Anthropic prompt caching with Databricks (+ MCP proxy/TLS passthrough) ([#3463](https://github…
**Category:** improvement **User Impact:** Mobile users can scan Activity as a focused conversation inbox and open the exact unread message or thread represented by each item. ## Context Mobile's Activity tab had not kept pace with Desktop: it presented isolated event headlines, advertised categories that were often empty, and opened a channel without clearly landing on the selected item. This PR brings the Mobile surface toward the conversation-oriented direction explored in Clay Delk's Desktop [Inbox refactor PR block#2045](block#2045), while adapting it to Mobile rather than copying the Desktop split-pane implementation. The related product/UX discussion is captured in the originating [Buzz thread](buzz://message?channel=a9bbc0e5-d25d-4740-849c-93c34bb578a4&id=a7d9a4d33dcd8c6bf0dc67d81c328892b9e38dedaa8548920224ef388301b6ab). ## UX decisions in this PR - **Conversation-oriented, not event-oriented:** related updates collapse into one row per thread/DM conversation, represented by the latest update and ordered by latest activity. Separate top-level conversations in the same channel remain separate rows. - **Resume at the oldest unread:** tapping a grouped row opens the represented canonical message/thread/DM at its oldest unread item, rather than merely opening the channel at an arbitrary position. - **Desktop-aligned row hierarchy:** rows lead with a full avatar and sender, followed by contextual location/type metadata, unread dot + time, and a two-line preview. A **New** boundary separates unread and read content. - **Mobile-native navigation:** Mobile keeps a single-column `Activity → canonical conversation → Back` flow. It does not introduce Desktop's persistent detail pane. - **Compact filtering:** the old horizontal chip rail becomes a compact filter menu so the source set fits a phone viewport without horizontal scanning. Filters are All, Mentions, Threads, Needs Action, Activity, Agents, Reminders, and Drafts. - **Focused source semantics:** All covers personally relevant work—DMs, mentions, thread replies, needs-action events, owned-agent activity, due reminders, and active drafts—rather than becoming a generic stream of every channel message. Mobile's standalone Activity source is currently limited to DM traffic because it does not have Desktop's aggregated channel-activity feed. - **Shared read behavior:** rows project canonical channel/thread/message markers, support unread-only and mark-all-read, and use local overrides only where canonical markers cannot represent an item. - **Reminders and drafts are real data:** reminders use the same encrypted NIP-ER events as Desktop. Drafts persist device-local composer state, restore on return, survive failed sends, and clear after successful sends. - **Explain navigation failures:** an unavailable destination produces an explanatory message rather than silently doing nothing or falling back to an unrelated channel position. ## Implementation summary - Adds a Mobile inbox model for conversation grouping, category priority, contextual labels, sorting, filtering, and oldest-unread targets. - Expands relay-backed sources for mentions, approvals, owned-agent lifecycle events, and DM traffic. - Adds fail-closed NIP-ER reminder decryption and device-local compose-draft persistence. - Redesigns Activity rows, boundaries, filters, unread controls, and empty/loading states. - Routes rows through Mobile's existing canonical channel/thread screens with precise target IDs. - Adds model, provider, widget, reminder, read-state, draft-lifecycle, and deep-link coverage. ## Reproduction steps 1. Run Mobile and open **Activity**. 2. Confirm full avatars, sender-first rows, context labels, unread indicators, timestamps, two-line previews, and the compact filter control. 3. Open the filter menu and verify All, Mentions, Threads, Needs Action, Activity, Agents, Reminders, and Drafts. 4. Tap a grouped thread row and confirm the canonical conversation opens at its oldest unread message. 5. Mark rows read/unread, enable unread-only mode, and use mark-all-read; confirm state agrees with the channel/thread destination. 6. Type without sending in a channel or thread, leave, and confirm the draft appears in Activity and restores in the composer. ## Screenshots | Before — merge-base `dd222a509` | After — PR head `52ad40aee` | |---|---| | <img width="1206" height="2622" alt="image" src="https://github.com/user-attachments/assets/ae961b08-bf8a-4bd5-b487-f6321ae8d85b" /> | <img width="1206" height="2622" alt="image" src="https://github.com/user-attachments/assets/bcbe7ab0-552a-417c-9e85-7a85eb4592ee" /> | Recaptured on the same authenticated iPhone 17 simulator, account, theme, and Activity view, at this PR's current merge-base (`dd222a509`) and head (`52ad40aee`). Both frames were taken within a few minutes on the same live feed, so the visible conversation set overlaps closely (the recent Ned/Bart/Tommy items appear in both). The compared change is the row *structure*: Before leads with an `@ Mention` headline over a small inline avatar and a horizontal chip rail; After leads with a full avatar, a compact `labelMedium` sender label, contextual "Mentioned in" metadata, and a filter menu. The sender username now renders at the same compact scale the old `@ Mention` label used. ## Verification - Current rebased head: `5bd87f4f4` on `origin/main` at `dd222a509`; GitHub reports the PR mergeable. - `flutter analyze` — clean at `5bd87f4f4`. - Full Mobile suite — 698 passed, 1 skipped, 4 failed; all four failures reproduce identically on clean `origin/main` (`channels_page_test` create-channel sheet and three `compose_bar_test` agent-mention cases). - The prior PR-specific `home_page_test` failures were fixed by providing the Activity local-state dependency in that harness. - Independent code and simulator UI review — approved. - Post-rebase GitHub checks are running. --------- Signed-off-by: Taylor Ho <[email protected]> Signed-off-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> Co-authored-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
## Why The Inbox mixed overlapping feed categories with personal work queues, so **All** was not actually comprehensive and several filters did not make it clear why an item appeared. Threads and DMs could produce one row per event instead of one row per conversation, drafts were hidden until selected, and reminders appeared through multiple competing presentations. This refactor makes the **Inbox** a focused, conversation-oriented place to catch up on work relevant to you. It is intentionally not a mirror of every unread event in every channel. ## What changed - Keep the destination named **Inbox** and use the standard Lucide bell icon. - Refocus **All** on DMs, mentions, thread replies, needs-action items, replies from agents the user owns or controls, due reminders, and active drafts. - Exclude generic top-level channel traffic and updates from agents the user does not own or control. - Group each thread or DM into one row, sorted by latest activity. - Resume an unread conversation at its oldest unread message while opening the full thread or DM in the detail pane. - Reuse the existing **New** divider at the unread boundary. - Make the detail title a direct link to the canonical conversation. - Give Reminders and Drafts the same list/detail interaction and location metadata as conversation rows. - Separate Reminders and Drafts from message filters with a subtle divider, without adding another labeled section. - Put reminder and draft counts beside their corresponding filter labels instead of on the generic filter button. - Preserve the selected conversation when switching filters if it remains valid; otherwise select a valid replacement without flashing stale detail. - Use filter-specific empty states and rename the options toggle to **Show unread only**. - Ship the focused behavior directly. The earlier experiment gate, Custom view, and default-view controls have been removed from this PR to keep the first pass focused. ## Filter model | Filter | What appears | | --- | --- | | **All** | One row per personally relevant conversation, plus due reminders and active drafts. Includes DMs, mentions, thread replies, explicit needs-action items, and replies from agents the current user owns or controls. Excludes generic top-level channel traffic, other agents' updates, and reminders that are not due yet. | | **Mentions** | Conversations containing a direct mention. Each conversation appears once and opens with full context. | | **Threads** | Conventional threaded replies, grouped to one row per thread. Broadcast replies are not treated as conventional thread replies. | | **Needs action** | Feed items explicitly classified as requiring action. | | **Agents** | Conversations whose representative response was authored by an agent the current user owns or controls, including top-level DM responses. If a human replies afterward, the conversation leaves this filter until an owned agent responds again. | | **Reminders** | All pending reminders, including upcoming reminders that stay out of **All** until they are due. | | **Drafts** | Active drafts, ordered by their last real edit time. | ## Grouping, ordering, and state - A thread or DM creates one Inbox row rather than one row per event. - An unread conversation resumes at its oldest unread message so intervening context is not skipped. - Conversation rows still sort by their latest activity. - The detail pane opens the full available conversation and shows the shared **New** divider before the first unread message. - Upcoming reminders appear only in **Reminders**. - When a reminder becomes due, it enters **All** at its trigger time. If its source conversation is already represented, the reminder state merges into that row instead of creating a duplicate; otherwise it appears as a standalone reminder row. - A due reminder can enrich a row in another relative filter when that conversation already qualifies for the filter. Reminder lifecycle remains separate from message read state. - Drafts appear in **All** by their last real edit time. Opening an unchanged draft does not move it to the top. - Reminder and draft rows show their location as `In #channel` or `In DM with <name>`. - **Show unread only** hides reminder and draft work queues because they do not share message unread semantics. ## Removed or narrowed - **Remove the old Activity filter.** It overlapped with All while still omitting items All now includes. - **Narrow Agents.** It no longer gathers every agent participating in a shared thread or subsequent human follow-ups. - **Remove duplicate reminder presentations.** The aggregate pending-reminders jump and duplicate generic feed rows are replaced by one list/detail model. - **Remove Custom and default-view settings from this pass.** They added considerable state and UI before the core model had been validated. - **Do not add section labels for Reminders and Drafts.** A divider communicates the distinction without creating another hierarchy in the menu. ## Risk assessment Medium implementation risk because this changes composition, grouping, ordering, read behavior, and personal queues in a primary desktop view. The implementation is scoped to the desktop UI and its local feed projection; it does not change relay schemas or public APIs. ## Testing - Desktop formatting, lint, file-size, text-size, and TypeScript checks passed. - Desktop unit suite: **3,663 passed, 0 failed**. - Desktop E2E production build passed. - Playwright smoke coverage across every spec touching this surface (`channels`, `smoke`, `profile`, `project-inbox`, `community-rail`, `integration`, `drafts-screenshots`): **118 passed, 0 failed**. - Full Playwright smoke project: **732 passed, 1 skipped**. Three local failures were investigated and cleared — `community-rail` keyboard reorder passed on re-run (flaky), while `relay-reconnect:97` and `video-attachment:223` are untouched by this commit (the only change to shared `tests/helpers/bridge.ts` is a comment) and pass in CI. - Unit coverage includes focused All matching, owned-agent filtering, conversation grouping, oldest-unread selection, selection stability, chronological reminder/draft composition, trigger-time reminder ordering, and duplicate reminder suppression. ## Update: July 27, 2026 The naming decision is settled: the surface stays **Inbox**. An earlier pass in this branch had renamed it to **Activity**; that rename has been reverted in `9c00d2d6e`, which is naming-only and changes no behavior. The revert covers file names, component/hook/type/constant identifiers, the sidebar label and tooltip, the `Inbox options` and `Filter inbox:` aria-labels, and the corresponding test names, test ids, and fixture ids. Three things were deliberately left as `activity`: - **The feed API contract** — the `activity` / `agent_activity` categories, the `feed.activity` and `feed.agentActivity` keys, and the `types=` query parameter. These are the server's names, not the surface's. - **Plain-noun usage** — empty states such as "No activity yet", plus `latestActivityAt` and `PROJECT_ACTIVITY_KINDS`. - **Pre-existing agent, project, and profile activity code**, which refers to a different concept entirely. The earlier experiment-gate approach has also been dropped, so `tests/helpers/bridge.ts` no longer claims that an Activity preview feature exists — `preview-features.json` has no such entry and the seed helper enables every desktop feature. Generated with Codex --------- Signed-off-by: Clay Delk <[email protected]> Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
## Buzz Desktop release v0.5.0 ### Changes since v0.4.26: - feat(invites): add use-limited invite links ([block#3141](block#3141)) ([`d500c2d5c`](block@d500c2d)) - fix(node): bump Buzz-supplied Node runtimes past OpenClaw's >=24.15.0 floor ([block#3218](block#3218)) ([`98a7b1334`](block@98a7b13)) - fix(desktop): preserve thread anchor through layout reflow ([block#3212](block#3212)) ([`9810d8545`](block@9810d85)) - feat(search): parse from:/in:/after:/before: and pass them in the filter ([block#2871](block#2871)) ([`cb2a265b5`](block@cb2a265)) - fix(desktop): fetch join policies through native networking ([block#2862](block#2862)) ([`0019f8076`](block@0019f80)) - fix(desktop): republish agent identity records when a persona rename propagates ([block#2607](block#2607)) ([`7ca0bbd94`](block@7ca0bbd)) - fix(desktop): keep project Inbox previews compact ([block#3193](block#3193)) ([`de1396050`](block@de13960)) - Inbox refactor ([block#2045](block#2045)) ([`2bd4c24b7`](block@2bd4c24)) - Fix composer selection formatting and drop overlay ([block#3172](block#3172)) ([`99da5b7eb`](block@99da5b7)) - Refine pending message status ([block#3153](block#3153)) ([`75588eaff`](block@75588ea)) - fix(desktop): recover full local storage on startup ([block#3182](block#3182)) ([`174c38e4b`](block@174c38e)) - fix(desktop): keep collapsed table separators out of spoilers ([block#3169](block#3169)) ([`4d8b676bb`](block@4d8b676)) - feat(desktop): redesign agent runtime settings ([block#3093](block#3093)) ([`d98da7389`](block@d98da73)) - fix(desktop): use forward slashes for git credential.helper on Windows ([block#3023](block#3023)) ([`899531684`](block@8995316)) - chore(desktop): add AgentCreationPreview file-size override to unblock main CI ([block#3154](block#3154)) ([`b92a1f4bf`](block@b92a1f4)) - fix(desktop): make the test loader work on Windows ([block#2758](block#2758)) ([`8bb43d519`](block@8bb43d5)) - fix(desktop): make lint and unit-test gates work on Windows ([block#2943](block#2943)) ([`545bb46b8`](block@545bb46)) - feat(desktop): add search to agent emoji picker ([block#2630](block#2630)) ([`313f793c8`](block@313f793)) - fix(desktop): keep identity key help dialog readable in dark mode ([block#2854](block#2854)) ([`be275cfc6`](block@be275cf)) - feat(acp): title agent sessions from the agent and channel name ([block#3028](block#3028)) ([`f2fe3b63c`](block@f2fe3b6)) - feat(git): use agent display name as git author name ([block#3040](block#3040)) ([`18eef633d`](block@18eef63)) - fix(deps): bump nostr to 0.44.6 for RUSTSEC-2026-0216 (NIP-44 remote DoS) ([block#3135](block#3135)) ([`31e2de196`](block@31e2de1)) - fix(desktop): read the newest pair-scoped harness log ([block#3134](block#3134)) ([`654f38490`](block@654f384)) - feat(desktop): handle project work from Inbox ([block#3117](block#3117)) ([`c5c4f390b`](block@c5c4f39)) - fix(desktop): clarify identity key button when key exists ([block#2357](block#2357)) ([`87b3fcd3c`](block@87b3fcd)) - Restore Goose and Buzz Agent to onboarding harness selection ([block#2731](block#2731)) ([`7fc0cc82d`](block@7fc0cc8)) - fix(desktop): render rich project work item content ([block#3100](block#3100)) ([`afb272bb7`](block@afb272b)) - feat(acp): bring your own harness (BYOH) — generic ACP runtime seam + settings gallery ([block#2773](block#2773)) ([`95fdf9788`](block@95fdf97)) - feat(desktop): use collective mesh routing for Auto ([block#2825](block#2825)) ([`16d4ec335`](block@16d4ec3)) - fix(desktop): strip legacy baked team instructions from stored prompts ([block#3035](block#3035)) ([`aee631448`](block@aee6314)) - feat(agents): lower default agent parallelism from 24 to 10 ([block#3038](block#3038)) ([`5d8ede446`](block@5d8ede4)) - Polish community rail and mobile pairing ([block#2972](block#2972)) ([`e6c90bb7c`](block@e6c90bb)) - fix(desktop): remove bundled libsystemd from AppImage ([block#2353](block#2353)) ([`a31fc4d2f`](block@a31fc4d)) - fix(desktop): make agent definition authoritative for model/provider/prompt ([block#1968](block#1968)) ([`8c0e8cb16`](block@8c0e8cb)) - chore(desktop): delete dead persona catalog UI cluster ([block#2886](block#2886)) ([`8e67cf399`](block@8e67cf3)) - fix(desktop): surface install failures hidden by curl-pipe exit codes ([block#2892](block#2892)) ([`166c6655e`](block@166c665)) - Refactor managed-agent runtime into cohesive modules ([block#2974](block#2974)) ([`74b63e184`](block@74b63e1)) - fix(desktop): make Linux AppImage GStreamer work on non-Debian distros ([block#2176](block#2176)) ([`cc6c4d347`](block@cc6c4d3)) - refactor(desktop): remove Agent directory section from Agents page ([block#2290](block#2290)) ([`5d1233e84`](block@5d1233e)) - fix(desktop): enable arboard Wayland backend so Linux copies reach the Wayland clipboard ([block#2904](block#2904)) ([`ab7aa8b12`](block@ab7aa8b)) - fix(desktop): supervise and re-arm relay-mesh runtime ([block#2823](block#2823)) ([`aa51dab9d`](block@aa51dab)) - fix(agents): run live Databricks discovery instead of the fallback list ([block#2890](block#2890)) ([`8eb6e3eb6`](block@8eb6e3e)) - fix(desktop): retire prepend mode on every reader wheel ([block#2913](block#2913)) ([`07d0265cf`](block@07d0265)) - fix(desktop): consolidate prepend scroll correction ([block#2855](block#2855)) ([`25e7864b3`](block@25e7864)) - fix(desktop): track concurrent agent turns up to the harness maximum ([block#2882](block#2882)) ([`20bff5910`](block@20bff59)) - fix(relay): preserve reconnect backoff ([block#2759](block#2759)) ([`499c5d349`](block@499c5d3)) - refactor(relay): expose reconnect timing policy ([block#2310](block#2310)) ([`2f0041595`](block@2f00415)) - fix(desktop): clear stale working badges on agent stop/restart ([block#2803](block#2803)) ([`a64cc71f6`](block@a64cc71)) - fix(desktop): surface agent rename relay profile sync failure as a warning toast ([block#2279](block#2279)) ([`5e3d2e484`](block@5e3d2e4)) - fix(discovery): inject PATH into Codex adapter planning ([block#2767](block#2767)) ([`6ab3835f3`](block@6ab3835)) **To release:** merge this PR. The tag and build will happen automatically. Signed-off-by: Wes <[email protected]>
## Why The Inbox surface was briefly renamed to **Activity** during block#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 <[email protected]> Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
…-core * origin/main: (48 commits) fix(buzz-acp): accept id-keyed config options when resolving model switch (block#2795) fix(desktop): probe legacy Goose install dir on Windows (block#3248) refactor(desktop): extract install command execution into install_exec (block#3251) Polish composer activity layout and transitions (block#3151) feat(invites): add use-limited invite links (block#3141) fix(node): bump Buzz-supplied Node runtimes past OpenClaw's >=24.15.0 floor (block#3218) fix(desktop): preserve thread anchor through layout reflow (block#3212) feat(search): parse from:/in:/after:/before: and pass them in the filter (block#2871) fix(desktop): fetch join policies through native networking (block#2862) fix(desktop): republish agent identity records when a persona rename propagates (block#2607) fix(desktop): keep project Inbox previews compact (block#3193) Inbox refactor (block#2045) Fix composer selection formatting and drop overlay (block#3172) Refine pending message status (block#3153) feat(admin): show reported message content in report detail (block#3149) fix(desktop): recover full local storage on startup (block#3182) Replace mobile reconnect banners with skeleton shimmer (block#3143) fix(desktop): keep collapsed table separators out of spoilers (block#3169) chore(deps): update plugin org.jetbrains.kotlin.android to v2.2.21 (block#3058) resolve findings (block#3150) ... Signed-off-by: Michael Neale <[email protected]> # Conflicts: # crates/buzz-agent/src/mcp.rs
…-core * origin/main: (48 commits) fix(buzz-acp): accept id-keyed config options when resolving model switch (block#2795) fix(desktop): probe legacy Goose install dir on Windows (block#3248) refactor(desktop): extract install command execution into install_exec (block#3251) Polish composer activity layout and transitions (block#3151) feat(invites): add use-limited invite links (block#3141) fix(node): bump Buzz-supplied Node runtimes past OpenClaw's >=24.15.0 floor (block#3218) fix(desktop): preserve thread anchor through layout reflow (block#3212) feat(search): parse from:/in:/after:/before: and pass them in the filter (block#2871) fix(desktop): fetch join policies through native networking (block#2862) fix(desktop): republish agent identity records when a persona rename propagates (block#2607) fix(desktop): keep project Inbox previews compact (block#3193) Inbox refactor (block#2045) Fix composer selection formatting and drop overlay (block#3172) Refine pending message status (block#3153) feat(admin): show reported message content in report detail (block#3149) fix(desktop): recover full local storage on startup (block#3182) Replace mobile reconnect banners with skeleton shimmer (block#3143) fix(desktop): keep collapsed table separators out of spoilers (block#3169) chore(deps): update plugin org.jetbrains.kotlin.android to v2.2.21 (block#3058) resolve findings (block#3150) ... Signed-off-by: Michael Neale <[email protected]> # Conflicts: # crates/buzz-agent/src/mcp.rs
**Category:** improvement **User Impact:** Mobile users can scan Activity as a focused conversation inbox and open the exact unread message or thread represented by each item. ## Context Mobile's Activity tab had not kept pace with Desktop: it presented isolated event headlines, advertised categories that were often empty, and opened a channel without clearly landing on the selected item. This PR brings the Mobile surface toward the conversation-oriented direction explored in Clay Delk's Desktop [Inbox refactor PR block#2045](block#2045), while adapting it to Mobile rather than copying the Desktop split-pane implementation. The related product/UX discussion is captured in the originating [Buzz thread](buzz://message?channel=a9bbc0e5-d25d-4740-849c-93c34bb578a4&id=a7d9a4d33dcd8c6bf0dc67d81c328892b9e38dedaa8548920224ef388301b6ab). ## UX decisions in this PR - **Conversation-oriented, not event-oriented:** related updates collapse into one row per thread/DM conversation, represented by the latest update and ordered by latest activity. Separate top-level conversations in the same channel remain separate rows. - **Resume at the oldest unread:** tapping a grouped row opens the represented canonical message/thread/DM at its oldest unread item, rather than merely opening the channel at an arbitrary position. - **Desktop-aligned row hierarchy:** rows lead with a full avatar and sender, followed by contextual location/type metadata, unread dot + time, and a two-line preview. A **New** boundary separates unread and read content. - **Mobile-native navigation:** Mobile keeps a single-column `Activity → canonical conversation → Back` flow. It does not introduce Desktop's persistent detail pane. - **Compact filtering:** the old horizontal chip rail becomes a compact filter menu so the source set fits a phone viewport without horizontal scanning. Filters are All, Mentions, Threads, Needs Action, Activity, Agents, Reminders, and Drafts. - **Focused source semantics:** All covers personally relevant work—DMs, mentions, thread replies, needs-action events, owned-agent activity, due reminders, and active drafts—rather than becoming a generic stream of every channel message. Mobile's standalone Activity source is currently limited to DM traffic because it does not have Desktop's aggregated channel-activity feed. - **Shared read behavior:** rows project canonical channel/thread/message markers, support unread-only and mark-all-read, and use local overrides only where canonical markers cannot represent an item. - **Reminders and drafts are real data:** reminders use the same encrypted NIP-ER events as Desktop. Drafts persist device-local composer state, restore on return, survive failed sends, and clear after successful sends. - **Explain navigation failures:** an unavailable destination produces an explanatory message rather than silently doing nothing or falling back to an unrelated channel position. ## Implementation summary - Adds a Mobile inbox model for conversation grouping, category priority, contextual labels, sorting, filtering, and oldest-unread targets. - Expands relay-backed sources for mentions, approvals, owned-agent lifecycle events, and DM traffic. - Adds fail-closed NIP-ER reminder decryption and device-local compose-draft persistence. - Redesigns Activity rows, boundaries, filters, unread controls, and empty/loading states. - Routes rows through Mobile's existing canonical channel/thread screens with precise target IDs. - Adds model, provider, widget, reminder, read-state, draft-lifecycle, and deep-link coverage. ## Reproduction steps 1. Run Mobile and open **Activity**. 2. Confirm full avatars, sender-first rows, context labels, unread indicators, timestamps, two-line previews, and the compact filter control. 3. Open the filter menu and verify All, Mentions, Threads, Needs Action, Activity, Agents, Reminders, and Drafts. 4. Tap a grouped thread row and confirm the canonical conversation opens at its oldest unread message. 5. Mark rows read/unread, enable unread-only mode, and use mark-all-read; confirm state agrees with the channel/thread destination. 6. Type without sending in a channel or thread, leave, and confirm the draft appears in Activity and restores in the composer. ## Screenshots | Before — merge-base `3367e2304` | After — PR head `52ad40aee` | |---|---| | <img width="1206" height="2622" alt="image" src="https://github.com/user-attachments/assets/ae961b08-bf8a-4bd5-b487-f6321ae8d85b" /> | <img width="1206" height="2622" alt="image" src="https://github.com/user-attachments/assets/bcbe7ab0-552a-417c-9e85-7a85eb4592ee" /> | Recaptured on the same authenticated iPhone 17 simulator, account, theme, and Activity view, at this PR's current merge-base (`3367e2304`) and head (`52ad40aee`). Both frames were taken within a few minutes on the same live feed, so the visible conversation set overlaps closely (the recent Ned/Bart/Tommy items appear in both). The compared change is the row *structure*: Before leads with an `@ Mention` headline over a small inline avatar and a horizontal chip rail; After leads with a full avatar, a compact `labelMedium` sender label, contextual "Mentioned in" metadata, and a filter menu. The sender username now renders at the same compact scale the old `@ Mention` label used. ## Verification - Current rebased head: `5bd87f4f4` on `origin/main` at `3367e2304`; GitHub reports the PR mergeable. - `flutter analyze` — clean at `5bd87f4f4`. - Full Mobile suite — 698 passed, 1 skipped, 4 failed; all four failures reproduce identically on clean `origin/main` (`channels_page_test` create-channel sheet and three `compose_bar_test` agent-mention cases). - The prior PR-specific `home_page_test` failures were fixed by providing the Activity local-state dependency in that harness. - Independent code and simulator UI review — approved. - Post-rebase GitHub checks are running. --------- Signed-off-by: Taylor Ho <[email protected]> Signed-off-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> Co-authored-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
## Why The Inbox mixed overlapping feed categories with personal work queues, so **All** was not actually comprehensive and several filters did not make it clear why an item appeared. Threads and DMs could produce one row per event instead of one row per conversation, drafts were hidden until selected, and reminders appeared through multiple competing presentations. This refactor makes the **Inbox** a focused, conversation-oriented place to catch up on work relevant to you. It is intentionally not a mirror of every unread event in every channel. ## What changed - Keep the destination named **Inbox** and use the standard Lucide bell icon. - Refocus **All** on DMs, mentions, thread replies, needs-action items, replies from agents the user owns or controls, due reminders, and active drafts. - Exclude generic top-level channel traffic and updates from agents the user does not own or control. - Group each thread or DM into one row, sorted by latest activity. - Resume an unread conversation at its oldest unread message while opening the full thread or DM in the detail pane. - Reuse the existing **New** divider at the unread boundary. - Make the detail title a direct link to the canonical conversation. - Give Reminders and Drafts the same list/detail interaction and location metadata as conversation rows. - Separate Reminders and Drafts from message filters with a subtle divider, without adding another labeled section. - Put reminder and draft counts beside their corresponding filter labels instead of on the generic filter button. - Preserve the selected conversation when switching filters if it remains valid; otherwise select a valid replacement without flashing stale detail. - Use filter-specific empty states and rename the options toggle to **Show unread only**. - Ship the focused behavior directly. The earlier experiment gate, Custom view, and default-view controls have been removed from this PR to keep the first pass focused. ## Filter model | Filter | What appears | | --- | --- | | **All** | One row per personally relevant conversation, plus due reminders and active drafts. Includes DMs, mentions, thread replies, explicit needs-action items, and replies from agents the current user owns or controls. Excludes generic top-level channel traffic, other agents' updates, and reminders that are not due yet. | | **Mentions** | Conversations containing a direct mention. Each conversation appears once and opens with full context. | | **Threads** | Conventional threaded replies, grouped to one row per thread. Broadcast replies are not treated as conventional thread replies. | | **Needs action** | Feed items explicitly classified as requiring action. | | **Agents** | Conversations whose representative response was authored by an agent the current user owns or controls, including top-level DM responses. If a human replies afterward, the conversation leaves this filter until an owned agent responds again. | | **Reminders** | All pending reminders, including upcoming reminders that stay out of **All** until they are due. | | **Drafts** | Active drafts, ordered by their last real edit time. | ## Grouping, ordering, and state - A thread or DM creates one Inbox row rather than one row per event. - An unread conversation resumes at its oldest unread message so intervening context is not skipped. - Conversation rows still sort by their latest activity. - The detail pane opens the full available conversation and shows the shared **New** divider before the first unread message. - Upcoming reminders appear only in **Reminders**. - When a reminder becomes due, it enters **All** at its trigger time. If its source conversation is already represented, the reminder state merges into that row instead of creating a duplicate; otherwise it appears as a standalone reminder row. - A due reminder can enrich a row in another relative filter when that conversation already qualifies for the filter. Reminder lifecycle remains separate from message read state. - Drafts appear in **All** by their last real edit time. Opening an unchanged draft does not move it to the top. - Reminder and draft rows show their location as `In #channel` or `In DM with <name>`. - **Show unread only** hides reminder and draft work queues because they do not share message unread semantics. ## Removed or narrowed - **Remove the old Activity filter.** It overlapped with All while still omitting items All now includes. - **Narrow Agents.** It no longer gathers every agent participating in a shared thread or subsequent human follow-ups. - **Remove duplicate reminder presentations.** The aggregate pending-reminders jump and duplicate generic feed rows are replaced by one list/detail model. - **Remove Custom and default-view settings from this pass.** They added considerable state and UI before the core model had been validated. - **Do not add section labels for Reminders and Drafts.** A divider communicates the distinction without creating another hierarchy in the menu. ## Risk assessment Medium implementation risk because this changes composition, grouping, ordering, read behavior, and personal queues in a primary desktop view. The implementation is scoped to the desktop UI and its local feed projection; it does not change relay schemas or public APIs. ## Testing - Desktop formatting, lint, file-size, text-size, and TypeScript checks passed. - Desktop unit suite: **3,663 passed, 0 failed**. - Desktop E2E production build passed. - Playwright smoke coverage across every spec touching this surface (`channels`, `smoke`, `profile`, `project-inbox`, `community-rail`, `integration`, `drafts-screenshots`): **118 passed, 0 failed**. - Full Playwright smoke project: **732 passed, 1 skipped**. Three local failures were investigated and cleared — `community-rail` keyboard reorder passed on re-run (flaky), while `relay-reconnect:97` and `video-attachment:223` are untouched by this commit (the only change to shared `tests/helpers/bridge.ts` is a comment) and pass in CI. - Unit coverage includes focused All matching, owned-agent filtering, conversation grouping, oldest-unread selection, selection stability, chronological reminder/draft composition, trigger-time reminder ordering, and duplicate reminder suppression. ## Update: July 27, 2026 The naming decision is settled: the surface stays **Inbox**. An earlier pass in this branch had renamed it to **Activity**; that rename has been reverted in `9c00d2d6e`, which is naming-only and changes no behavior. The revert covers file names, component/hook/type/constant identifiers, the sidebar label and tooltip, the `Inbox options` and `Filter inbox:` aria-labels, and the corresponding test names, test ids, and fixture ids. Three things were deliberately left as `activity`: - **The feed API contract** — the `activity` / `agent_activity` categories, the `feed.activity` and `feed.agentActivity` keys, and the `types=` query parameter. These are the server's names, not the surface's. - **Plain-noun usage** — empty states such as "No activity yet", plus `latestActivityAt` and `PROJECT_ACTIVITY_KINDS`. - **Pre-existing agent, project, and profile activity code**, which refers to a different concept entirely. The earlier experiment-gate approach has also been dropped, so `tests/helpers/bridge.ts` no longer claims that an Activity preview feature exists — `preview-features.json` has no such entry and the seed helper enables every desktop feature. Generated with Codex --------- Signed-off-by: Clay Delk <[email protected]> Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
## Buzz Desktop release v0.5.0 ### Changes since v0.4.26: - feat(invites): add use-limited invite links ([block#3141](block#3141)) ([`b87b0f8e5`](block@b87b0f8)) - fix(node): bump Buzz-supplied Node runtimes past OpenClaw's >=24.15.0 floor ([block#3218](block#3218)) ([`26fa4b998`](block@26fa4b9)) - fix(desktop): preserve thread anchor through layout reflow ([block#3212](block#3212)) ([`1fec177fe`](block@1fec177)) - feat(search): parse from:/in:/after:/before: and pass them in the filter ([block#2871](block#2871)) ([`a70b605f9`](block@a70b605)) - fix(desktop): fetch join policies through native networking ([block#2862](block#2862)) ([`9d4c1db67`](block@9d4c1db)) - fix(desktop): republish agent identity records when a persona rename propagates ([block#2607](block#2607)) ([`0b3ff0ca2`](block@0b3ff0c)) - fix(desktop): keep project Inbox previews compact ([block#3193](block#3193)) ([`995afd79f`](block@995afd7)) - Inbox refactor ([block#2045](block#2045)) ([`3812fc6d9`](block@3812fc6)) - Fix composer selection formatting and drop overlay ([block#3172](block#3172)) ([`d3b121922`](block@d3b1219)) - Refine pending message status ([block#3153](block#3153)) ([`c244a0032`](block@c244a00)) - fix(desktop): recover full local storage on startup ([block#3182](block#3182)) ([`8ed3e3071`](block@8ed3e30)) - fix(desktop): keep collapsed table separators out of spoilers ([block#3169](block#3169)) ([`c0c971c88`](block@c0c971c)) - feat(desktop): redesign agent runtime settings ([block#3093](block#3093)) ([`4597ad753`](block@4597ad7)) - fix(desktop): use forward slashes for git credential.helper on Windows ([block#3023](block#3023)) ([`5ede34513`](block@5ede345)) - chore(desktop): add AgentCreationPreview file-size override to unblock main CI ([block#3154](block#3154)) ([`60da1f8e9`](block@60da1f8)) - fix(desktop): make the test loader work on Windows ([block#2758](block#2758)) ([`7de427e92`](block@7de427e)) - fix(desktop): make lint and unit-test gates work on Windows ([block#2943](block#2943)) ([`49bae5d4f`](block@49bae5d)) - feat(desktop): add search to agent emoji picker ([block#2630](block#2630)) ([`72c572f78`](block@72c572f)) - fix(desktop): keep identity key help dialog readable in dark mode ([block#2854](block#2854)) ([`6d533b24c`](block@6d533b2)) - feat(acp): title agent sessions from the agent and channel name ([block#3028](block#3028)) ([`f89c51480`](block@f89c514)) - feat(git): use agent display name as git author name ([block#3040](block#3040)) ([`f57ea796e`](block@f57ea79)) - fix(deps): bump nostr to 0.44.6 for RUSTSEC-2026-0216 (NIP-44 remote DoS) ([block#3135](block#3135)) ([`05af0ba4d`](block@05af0ba)) - fix(desktop): read the newest pair-scoped harness log ([block#3134](block#3134)) ([`1f134f8cd`](block@1f134f8)) - feat(desktop): handle project work from Inbox ([block#3117](block#3117)) ([`cb2982b22`](block@cb2982b)) - fix(desktop): clarify identity key button when key exists ([block#2357](block#2357)) ([`ca5fde1ff`](block@ca5fde1)) - Restore Goose and Buzz Agent to onboarding harness selection ([block#2731](block#2731)) ([`bcd15f640`](block@bcd15f6)) - fix(desktop): render rich project work item content ([block#3100](block#3100)) ([`6037a701a`](block@6037a70)) - feat(acp): bring your own harness (BYOH) — generic ACP runtime seam + settings gallery ([block#2773](block#2773)) ([`46e6ca471`](block@46e6ca4)) - feat(desktop): use collective mesh routing for Auto ([block#2825](block#2825)) ([`6c0e93e1f`](block@6c0e93e)) - fix(desktop): strip legacy baked team instructions from stored prompts ([block#3035](block#3035)) ([`02227d761`](block@02227d7)) - feat(agents): lower default agent parallelism from 24 to 10 ([block#3038](block#3038)) ([`8baed7af9`](block@8baed7a)) - Polish community rail and mobile pairing ([block#2972](block#2972)) ([`4c47d7af5`](block@4c47d7a)) - fix(desktop): remove bundled libsystemd from AppImage ([block#2353](block#2353)) ([`05394d35c`](block@05394d3)) - fix(desktop): make agent definition authoritative for model/provider/prompt ([block#1968](block#1968)) ([`50a3dd7c4`](block@50a3dd7)) - chore(desktop): delete dead persona catalog UI cluster ([block#2886](block#2886)) ([`7575f8d1c`](block@7575f8d)) - fix(desktop): surface install failures hidden by curl-pipe exit codes ([block#2892](block#2892)) ([`806feb9ed`](block@806feb9)) - Refactor managed-agent runtime into cohesive modules ([block#2974](block#2974)) ([`b1a983734`](block@b1a9837)) - fix(desktop): make Linux AppImage GStreamer work on non-Debian distros ([block#2176](block#2176)) ([`53e1752d3`](block@53e1752)) - refactor(desktop): remove Agent directory section from Agents page ([block#2290](block#2290)) ([`f756b5828`](block@f756b58)) - fix(desktop): enable arboard Wayland backend so Linux copies reach the Wayland clipboard ([block#2904](block#2904)) ([`3f2f3964e`](block@3f2f396)) - fix(desktop): supervise and re-arm relay-mesh runtime ([block#2823](block#2823)) ([`c9607bd0f`](block@c9607bd)) - fix(agents): run live Databricks discovery instead of the fallback list ([block#2890](block#2890)) ([`7d64ca7a8`](block@7d64ca7)) - fix(desktop): retire prepend mode on every reader wheel ([block#2913](block#2913)) ([`dba1ba5ae`](block@dba1ba5)) - fix(desktop): consolidate prepend scroll correction ([block#2855](block#2855)) ([`797933e4d`](block@797933e)) - fix(desktop): track concurrent agent turns up to the harness maximum ([block#2882](block#2882)) ([`3115a39a0`](block@3115a39)) - fix(relay): preserve reconnect backoff ([block#2759](block#2759)) ([`cd949e75a`](block@cd949e7)) - refactor(relay): expose reconnect timing policy ([block#2310](block#2310)) ([`94c91f110`](block@94c91f1)) - fix(desktop): clear stale working badges on agent stop/restart ([block#2803](block#2803)) ([`da2580e51`](block@da2580e)) - fix(desktop): surface agent rename relay profile sync failure as a warning toast ([block#2279](block#2279)) ([`4d91195e9`](block@4d91195)) - fix(discovery): inject PATH into Codex adapter planning ([block#2767](block#2767)) ([`795f2d12e`](block@795f2d1)) **To release:** merge this PR. The tag and build will happen automatically. Signed-off-by: Wes <[email protected]>
## Why The Inbox surface was briefly renamed to **Activity** during block#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 <[email protected]> Co-authored-by: Claude Opus 5 (1M context) <[email protected]>








Why
The Inbox mixed overlapping feed categories with personal work queues, so All was not actually comprehensive and several filters did not make it clear why an item appeared. Threads and DMs could produce one row per event instead of one row per conversation, drafts were hidden until selected, and reminders appeared through multiple competing presentations.
This refactor makes the Inbox a focused, conversation-oriented place to catch up on work relevant to you. It is intentionally not a mirror of every unread event in every channel.
What changed
Filter model
Grouping, ordering, and state
In #channelorIn DM with <name>.Removed or narrowed
Risk assessment
Medium implementation risk because this changes composition, grouping, ordering, read behavior, and personal queues in a primary desktop view. The implementation is scoped to the desktop UI and its local feed projection; it does not change relay schemas or public APIs.
Testing
channels,smoke,profile,project-inbox,community-rail,integration,drafts-screenshots): 118 passed, 0 failed.community-railkeyboard reorder passed on re-run (flaky), whilerelay-reconnect:97andvideo-attachment:223are untouched by this commit (the only change to sharedtests/helpers/bridge.tsis a comment) and pass in CI.Update: July 27, 2026
The naming decision is settled: the surface stays Inbox. An earlier pass in this branch had renamed it to Activity; that rename has been reverted in
9c00d2d6e, which is naming-only and changes no behavior.The revert covers file names, component/hook/type/constant identifiers, the sidebar label and tooltip, the
Inbox optionsandFilter inbox:aria-labels, and the corresponding test names, test ids, and fixture ids.Three things were deliberately left as
activity:activity/agent_activitycategories, thefeed.activityandfeed.agentActivitykeys, and thetypes=query parameter. These are the server's names, not the surface's.latestActivityAtandPROJECT_ACTIVITY_KINDS.The earlier experiment-gate approach has also been dropped, so
tests/helpers/bridge.tsno longer claims that an Activity preview feature exists —preview-features.jsonhas no such entry and the seed helper enables every desktop feature.Generated with Codex