Skip to content

Inbox refactor - #2045

Merged
delkc merged 12 commits into
mainfrom
claydelk/inbox-refactor
Jul 27, 2026
Merged

Inbox refactor#2045
delkc merged 12 commits into
mainfrom
claydelk/inbox-refactor

Conversation

@delkc

@delkc delkc commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

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

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two user-visible inconsistencies need correction before this is ready:

  1. Show unread only only filters feed conversations. All still 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.
  2. Reminder detail renders DM context as a channel (#Alice) while the All row correctly renders In 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.

Comment thread desktop/src/features/home/ui/InboxListPane.tsx Outdated
Comment thread desktop/src/features/reminders/ui/RemindersPanel.tsx Outdated
@delkc
delkc force-pushed the claydelk/inbox-refactor branch 4 times, most recently from 04d71bf to 1b87f18 Compare July 23, 2026 15:13
@delkc
delkc requested a review from wesbillman July 23, 2026 15:25
@delkc
delkc marked this pull request as ready for review July 23, 2026 15:25
@delkc
delkc requested a review from a team as a code owner July 23, 2026 15:25
@delkc
delkc removed the request for review from wesbillman July 23, 2026 17:47
delkc added a commit that referenced this pull request Jul 24, 2026
@tellaho

tellaho commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

🤖 @delkc This is a thoughtful rework. I’d simplify the scope and ship the strongest parts directly.

Let’s just ship this

Let’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 navigation

Please 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 filters

Preserve 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

  • Focused All feed: Keep DMs, mentions, thread replies, needs-action items, owned-agent replies, due reminders, and active drafts while excluding generic channel traffic.
  • Conversation and unread model: Keep conversation grouping, oldest-unread resume, full context, the New divider, and shared read state.
  • Filters and supporting polish: Keep the simplified filters, dedicated reminder/draft actions, responsive list/detail behavior, metadata, empty states, and unread controls.

Follow-up opportunities

  • Custom and Set as default: Keep both out of this PR. Let the focused experience settle, then reassess whether their value justifies the product, persistence, and testing complexity.
  • Inbox versus Activity: It’s worth a quick conversation with Thomas. If timing would slow the functional work, keep Activity and revisit the label later.
  • A better home for drafts: Your border idea could help distinguish drafts in the meantime, but because drafts are deliberately included in All, resolving their long-term home feels like the more complete solution.

This leaves the PR with one clear thesis: ship a focused, conversation-oriented work feed without customization, preference persistence, or an experiment fork.

@delkc
delkc force-pushed the claydelk/inbox-refactor branch from 7293394 to 28f552a Compare July 24, 2026 19:09
delkc added a commit that referenced this pull request Jul 24, 2026
@tellaho

tellaho commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

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”?

@delkc
delkc force-pushed the claydelk/inbox-refactor branch 2 times, most recently from 95684ec to 61f580f Compare July 27, 2026 14:37
@delkc
delkc requested a review from tellaho July 27, 2026 15:04
tellaho added a commit that referenced this pull request Jul 27, 2026
**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 <taylorkmho@gmail.com>
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>
delkc added a commit that referenced this pull request Jul 27, 2026
@delkc

delkc commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

🤖 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 desktop/tests/e2e/inbox-refactor-screenshots.spec.ts rather than by hand, so the next round regenerates instead of drifting.

Filter menu

All, 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.

01-current-filters

Inbox label and controls

The destination is Inbox with the standard Lucide bell icon. The overflow menu holds Show unread only and Mark all as read.

02-current-controls

DMs group into one conversation row

Three consecutive DMs from alice produce a single row (DM from alice), not three. The detail pane opens the whole conversation with the shared New divider at the unread boundary. The title is a link to the canonical conversation.

03-grouped-dms

Threads resume at the oldest unread reply

A 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.

04-thread-context

@delkc

delkc commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

🤖 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

@wesbillmanShow unread only still showed reminders and drafts. Fixed. InboxListPane now passes empty drafts and reminders arrays into buildInboxListRows whenever unreadOnly is set, so the work queues drop out entirely rather than being filtered downstream. Covered by "Inbox unread-only hides reminders and drafts from mixed All" in channels.spec.ts.

@wesbillman — reminder detail showed a DM source as #name. Fixed in d1ec21269. formatReminderSourceLocation in RemindersPanel.tsx now branches on channelType, rendering DM with <name> for DMs and #channel otherwise. Covered in channels.spec.ts.

@tellaho — the detail pane title should navigate to the conversation. The <h2> in InboxDetailPane.tsx wraps a button that calls onOpenContext(contextChannelId, sourceEventId, contextThreadRootId). The separate ExternalLink icon button is still there for people who prefer an explicit affordance.

@tellaho — switching filters churned the selection, and you asked for coverage of both layouts. resolveInboxFilterSelection preserves the selection when it stays valid, selects the first valid row directly on wide layouts, and returns to the list on narrow layouts — no intermediate content. All four cases are unit-tested in inboxSelection.test.mjs (preserves a conversation that remains visible, wide … immediately selects the first valid row, narrow … returns to the list, empty … clears detail at every width), with an E2E case at channels.spec.ts:2966.

@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; 9c00d2d6e reverts that. The revert is naming-only — file names, identifiers, the sidebar label and tooltip, aria-labels, and the corresponding test names and fixture ids. Three things stay activity on purpose: the feed API contract (activity / agent_activity categories, the types= parameter), plain-noun copy like "No activity yet", and the pre-existing agent/project/profile activity code, which is a different concept.

@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. preview-features.json has no Inbox/Activity entry, and the stale comment in tests/helpers/bridge.ts that implied one existed has been corrected.

@tellaho — CI needed to be green. It is, on 8576cc998. The failures you saw were stale InboxActivity assumptions in the specs; those are resolved by the rename revert.

Before / after

The 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 filters

Before, All mixed in generic channel updates and surfaced no Projects filter.

before-navigation-and-filters

Overflow controls

Before, the toggle was worded generically and the counts sat on the filter button.

before-overflow-controls

DMs

Before, three consecutive DMs from the same person produced three rows, and a reminder appeared as its own generic feed row alongside them.

before-separate-dms

Thread context

Before, opening a thread reply landed on that single reply rather than the thread.

before-thread-context

Verification

  • CI is green on 8576cc998 (run 30298015563), all four smoke shards.
  • Desktop unit suite: 3,663 passed.
  • Full local Playwright smoke project: 732 passed, 1 skipped.
  • The screenshots above are now generated by desktop/tests/e2e/inbox-refactor-screenshots.spec.ts instead of by hand, so they regenerate with the UI rather than going stale.

@wesbillman @tellaho — ready for another look when you have time.

delkc added 8 commits July 27, 2026 15:56
Signed-off-by: Clay Delk <clay.delk@gmail.com>
Signed-off-by: Clay Delk <clay.delk@gmail.com>
Signed-off-by: Clay Delk <clay.delk@gmail.com>
Signed-off-by: Clay Delk <clay.delk@gmail.com>
Signed-off-by: Clay Delk <clay.delk@gmail.com>
Signed-off-by: Clay Delk <clay.delk@gmail.com>
Signed-off-by: Clay Delk <clay.delk@gmail.com>
Signed-off-by: Clay Delk <clay.delk@gmail.com>
delkc and others added 4 commits July 27, 2026 15:56
Signed-off-by: Clay Delk <clay.delk@gmail.com>
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) <noreply@anthropic.com>
Signed-off-by: Clay Delk <clay.delk@gmail.com>
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) <noreply@anthropic.com>
Signed-off-by: Clay Delk <clay.delk@gmail.com>
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) <noreply@anthropic.com>
Signed-off-by: Clay Delk <clay.delk@gmail.com>
@delkc
delkc force-pushed the claydelk/inbox-refactor branch from ca20425 to 77359c4 Compare July 27, 2026 20:05
@delkc
delkc dismissed wesbillman’s stale review July 27, 2026 20:44

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.

@delkc
delkc merged commit 2bd4c24 into main Jul 27, 2026
25 checks passed
@delkc
delkc deleted the claydelk/inbox-refactor branch July 27, 2026 20:45
tlongwell-block pushed a commit that referenced this pull request Jul 28, 2026
* 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 <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
wesbillman added a commit that referenced this pull request Jul 28, 2026
## 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 <wesbillman@users.noreply.github.com>
cameronhotchkies pushed a commit to cameronhotchkies/buzz that referenced this pull request Jul 28, 2026
…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>
delkc added a commit that referenced this pull request Jul 28, 2026
## Why

The Inbox surface was briefly renamed to **Activity** during #2045 and
picked up a bell icon to match. The name was reverted to **Inbox**
before merge, but the icon was not.

A bell says "notification tray." Inbox is a destination — a focused,
conversation-oriented place to catch up on work relevant to you,
including drafts and reminders that have nothing to do with
notifications. The glyph should say that.

## What changed

- Swap the sidebar entry from Lucide `Bell` to Lucide `Inbox`.
- Assert the icon in `inbox-refactor-screenshots.spec.ts`. Nothing
pinned it before, which is exactly how it drifted through a rename.

This also brings desktop back in line with mobile, which already uses
`LucideIcons.inbox300` / `inbox500` for the same destination.

## Deliberately unchanged

The bell on **reminder** rows in the list pane (`InboxListPane.tsx`,
reminders → bell, drafts → file) stays. A bell is the right glyph for a
reminder; that one was never about the surface's identity.

## Verification

- The new assertion is a real guard, not a no-op: with `Bell` restored
the test fails with `Expected: 1, Received: 0` on `svg.lucide-inbox`.
Confirmed before committing.
- `biome` and `tsc` clean.
- Playwright smoke: `inbox-refactor-screenshots` 4 passed; `smoke`,
`navigation`, `channels`, `sidebar-more-unread-overlap`,
`home-collapsed-top-chrome`, `workspace-rail` — 107 passed, 1 skipped.
- Screenshot below is the regenerated `02-current-controls` shot from
the spec.

Signed-off-by: Clay Delk <clay.delk@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
marccopson added a commit to marccopson/buzz that referenced this pull request Jul 31, 2026
* 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 <klopez4212@gmail.com>

* 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


![Home](https://raw.githubusercontent.com/block/buzz/99aaf9719f68a2813e14c484f503af10c4fca04a/pr-3121--01-home.png)

### Activity


![Activity](https://raw.githubusercontent.com/block/buzz/99aaf9719f68a2813e14c484f503af10c4fca04a/pr-3121--02-activity.png)

### Search


![Search](https://raw.githubusercontent.com/block/buzz/99aaf9719f68a2813e14c484f503af10c4fca04a/pr-3121--03-search.png)

## Testing

- `just mobile-check`
- `just mobile-test` (749 passed, 1 skipped)

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: npub15w828kxsxu2684ynste0uah2jwkgatd99flt7ds4523hzm8ju6cshdr8hh <a38ea3d8d03715a3d49382f2fe76ea93ac8eada52a7ebf3615a2a3716cf2e6b1@buzz.block.builderlab.xyz>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
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 <klopez4212@gmail.com>

* 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 <dgrochowski@squareup.com>
Co-authored-by: Amp <amp@ampcode.com>

* 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 <taylorkmho@gmail.com>
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 <kagan@squareup.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>

* 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 <pfleger.will@gmail.com>

* 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 <clay.delk@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* 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 <klopez4212@gmail.com>

* 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 <chotchkies@block.xyz>
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


![01-all-view-no-drafts](https://raw.githubusercontent.com/block/buzz/12c97624832cef40df951c403982994fea58dd80/pr-3217--01-all-view-no-drafts.png)

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


![02-drafts-filter-still-lists](https://raw.githubusercontent.com/block/buzz/12c97624832cef40df951c403982994fea58dd80/pr-3217--02-drafts-filter-still-lists.png)

Signed-off-by: Thomas Petersen <thomasp@squareup.com>

* 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

![Agent
actions](https://raw.githubusercontent.com/block/buzz/4643581cd0882d8b101b04e3d8be290ea7e39f08/pr-2439--01-agent-menu.png)

### Team avatar stack

![Team avatar
stack](https://raw.githubusercontent.com/block/buzz/4643581cd0882d8b101b04e3d8be290ea7e39f08/pr-2439--02-team-menu.png)

### Catalog sharing

![Catalog
sharing](https://raw.githubusercontent.com/block/buzz/648d6eaf6df7d4daa5b7375948ed221f723793d6/pr-2439--03-share-to-catalog.png)

### Publish while editing

![Publish while
editing](https://raw.githubusercontent.com/block/buzz/bd4eb473ff456f6e665173054dc5a0f764594d1e/pr-2439--01-edit-agent-publish-updates.png)

### Publish from Share

![Publish from
Share](https://raw.githubusercontent.com/block/buzz/648d6eaf6df7d4daa5b7375948ed221f723793d6/pr-2439--02-share-dialog-publish-updates.png)

### Catalog details

![Catalog
details](https://raw.githubusercontent.com/block/buzz/4643581cd0882d8b101b04e3d8be290ea7e39f08/pr-2439--04-agent-catalog.png)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>

* 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 <kalvin@block.xyz>
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 <pfleger.will@gmail.com>

* 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 <pfleger.will@gmail.com>

* 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 <wesbillman@users.noreply.github.com>
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 <kcao@gssmail.com>

* 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 <chotchkies@block.xyz>
Co-authored-by: Goose <opensource@block.xyz>

* 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 <pfleger.will@gmail.com>

* 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 <pfleger.will@gmail.com>
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 <dgrochowski@squareup.com>
Co-authored-by: Amp <amp@ampcode.com>

* 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 <pfleger.will@gmail.com>

* 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 <wesbillman@users.noreply.github.com>
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 <wesbillman@users.noreply.github.com>

* 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 <apurvashaw@Apurvas-MacBook-Air.local>
Co-authored-by: Apurva Shaw <apurvashaw@Apurvas-MacBook-Air.local>

* 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 <tlongwell@block.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: mr-r0b0t.eth <adam.manning@pro-serveinc.com>

* 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 <klopez4212@gmail.com>

* 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 <atish@squareup.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 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

![Typing indicator above the
composer](https://raw.githubusercontent.com/block/buzz/31de9f86a76fe61498bc7f2931d9e574827a9aa2/pr-3528--typing-indicator.png)

## Validation

- `flutter test test/features/channels/channel_detail_page_test.dart`
- `flutter analyze`

Signed-off-by: kenny lopez <klopez4212@gmail.com>

* 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 <klopez4212@gmail.com>

* 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 <atish@squareup.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 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 <klopez4212@gmail.com>

* 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 <wesbillman@users.noreply.github.com>
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 <pfleger.will@gmail.com>

* 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 <wesbillman@users.noreply.github.com>
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…
calvadev pushed a commit to shopstr-eng/buzz that referenced this pull request Aug 3, 2026
**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 <taylorkmho@gmail.com>
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>
calvadev pushed a commit to shopstr-eng/buzz that referenced this pull request Aug 3, 2026
## 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 <clay.delk@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
calvadev pushed a commit to shopstr-eng/buzz that referenced this pull request Aug 3, 2026
## 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 <wesbillman@users.noreply.github.com>
calvadev pushed a commit to shopstr-eng/buzz that referenced this pull request Aug 3, 2026
## 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 <clay.delk@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
mrmoe28 pushed a commit to mrmoe28/buzz-reloaded that referenced this pull request Aug 6, 2026
**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](block/buzz#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 <taylorkmho@gmail.com>
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>
mrmoe28 pushed a commit to mrmoe28/buzz-reloaded that referenced this pull request Aug 6, 2026
## Buzz Desktop release v0.5.0

### Changes since v0.4.26:

- feat(invites): add use-limited invite links
([#3141](block/buzz#3141))
([`a1a21319c`](block/buzz@a1a2131))
- fix(node): bump Buzz-supplied Node runtimes past OpenClaw's >=24.15.0
floor ([#3218](block/buzz#3218))
([`c1cc8d9d1`](block/buzz@c1cc8d9))
- fix(desktop): preserve thread anchor through layout reflow
([#3212](block/buzz#3212))
([`c3e816140`](block/buzz@c3e8161))
- feat(search): parse from:/in:/after:/before: and pass them in the
filter ([#2871](block/buzz#2871))
([`f19d7bbf8`](block/buzz@f19d7bb))
- fix(desktop): fetch join policies through native networking
([#2862](block/buzz#2862))
([`dfe141fd5`](block/buzz@dfe141f))
- fix(desktop): republish agent identity records when a persona rename
propagates ([#2607](block/buzz#2607))
([`d3b18b48c`](block/buzz@d3b18b4))
- fix(desktop): keep project Inbox previews compact
([#3193](block/buzz#3193))
([`31663bffc`](block/buzz@31663bf))
- Inbox refactor ([#2045](block/buzz#2045))
([`e5c883c25`](block/buzz@e5c883c))
- Fix composer selection formatting and drop overlay
([#3172](block/buzz#3172))
([`9d46311fb`](block/buzz@9d46311))
- Refine pending message status
([#3153](block/buzz#3153))
([`4f3794b0a`](block/buzz@4f3794b))
- fix(desktop): recover full local storage on startup
([#3182](block/buzz#3182))
([`ffdaacefd`](block/buzz@ffdaace))
- fix(desktop): keep collapsed table separators out of spoilers
([#3169](block/buzz#3169))
([`8c50f64a6`](block/buzz@8c50f64))
- feat(desktop): redesign agent runtime settings
([#3093](block/buzz#3093))
([`293d57357`](block/buzz@293d573))
- fix(desktop): use forward slashes for git credential.helper on Windows
([#3023](block/buzz#3023))
([`fa4a3190e`](block/buzz@fa4a319))
- chore(desktop): add AgentCreationPreview file-size override to unblock
main CI ([#3154](block/buzz#3154))
([`cba753e4c`](block/buzz@cba753e))
- fix(desktop): make the test loader work on Windows
([#2758](block/buzz#2758))
([`8c5c0b078`](block/buzz@8c5c0b0))
- fix(desktop): make lint and unit-test gates work on Windows
([#2943](block/buzz#2943))
([`35391d5d9`](block/buzz@35391d5))
- feat(desktop): add search to agent emoji picker
([#2630](block/buzz#2630))
([`cc560186a`](block/buzz@cc56018))
- fix(desktop): keep identity key help dialog readable in dark mode
([#2854](block/buzz#2854))
([`329f2176c`](block/buzz@329f217))
- feat(acp): title agent sessions from the agent and channel name
([#3028](block/buzz#3028))
([`f5b3743d9`](block/buzz@f5b3743))
- feat(git): use agent display name as git author name
([#3040](block/buzz#3040))
([`11fdc3dea`](block/buzz@11fdc3d))
- fix(deps): bump nostr to 0.44.6 for RUSTSEC-2026-0216 (NIP-44 remote
DoS) ([#3135](block/buzz#3135))
([`79895680d`](block/buzz@7989568))
- fix(desktop): read the newest pair-scoped harness log
([#3134](block/buzz#3134))
([`07bfb3139`](block/buzz@07bfb31))
- feat(desktop): handle project work from Inbox
([#3117](block/buzz#3117))
([`068e33717`](block/buzz@068e337))
- fix(desktop): clarify identity key button when key exists
([#2357](block/buzz#2357))
([`efc097d6a`](block/buzz@efc097d))
- Restore Goose and Buzz Agent to onboarding harness selection
([#2731](block/buzz#2731))
([`1eb717304`](block/buzz@1eb7173))
- fix(desktop): render rich project work item content
([#3100](block/buzz#3100))
([`a5e457d82`](block/buzz@a5e457d))
- feat(acp): bring your own harness (BYOH) — generic ACP runtime seam +
settings gallery ([#2773](block/buzz#2773))
([`84701fecc`](block/buzz@84701fe))
- feat(desktop): use collective mesh routing for Auto
([#2825](block/buzz#2825))
([`6c0e93e1f`](block/buzz@6c0e93e))
- fix(desktop): strip legacy baked team instructions from stored prompts
([#3035](block/buzz#3035))
([`02227d761`](block/buzz@02227d7))
- feat(agents): lower default agent parallelism from 24 to 10
([#3038](block/buzz#3038))
([`8baed7af9`](block/buzz@8baed7a))
- Polish community rail and mobile pairing
([#2972](block/buzz#2972))
([`4c47d7af5`](block/buzz@4c47d7a))
- fix(desktop): remove bundled libsystemd from AppImage
([#2353](block/buzz#2353))
([`05394d35c`](block/buzz@05394d3))
- fix(desktop): make agent definition authoritative for
model/provider/prompt ([#1968](block/buzz#1968))
([`50a3dd7c4`](block/buzz@50a3dd7))
- chore(desktop): delete dead persona catalog UI cluster
([#2886](block/buzz#2886))
([`7575f8d1c`](block/buzz@7575f8d))
- fix(desktop): surface install failures hidden by curl-pipe exit codes
([#2892](block/buzz#2892))
([`806feb9ed`](block/buzz@806feb9))
- Refactor managed-agent runtime into cohesive modules
([#2974](block/buzz#2974))
([`b1a983734`](block/buzz@b1a9837))
- fix(desktop): make Linux AppImage GStreamer work on non-Debian distros
([#2176](block/buzz#2176))
([`53e1752d3`](block/buzz@53e1752))
- refactor(desktop): remove Agent directory section from Agents page
([#2290](block/buzz#2290))
([`f756b5828`](block/buzz@f756b58))
- fix(desktop): enable arboard Wayland backend so Linux copies reach the
Wayland clipboard ([#2904](block/buzz#2904))
([`3f2f3964e`](block/buzz@3f2f396))
- fix(desktop): supervise and re-arm relay-mesh runtime
([#2823](block/buzz#2823))
([`c9607bd0f`](block/buzz@c9607bd))
- fix(agents): run live Databricks discovery instead of the fallback
list ([#2890](block/buzz#2890))
([`7d64ca7a8`](block/buzz@7d64ca7))
- fix(desktop): retire prepend mode on every reader wheel
([#2913](block/buzz#2913))
([`dba1ba5ae`](block/buzz@dba1ba5))
- fix(desktop): consolidate prepend scroll correction
([#2855](block/buzz#2855))
([`797933e4d`](block/buzz@797933e))
- fix(desktop): track concurrent agent turns up to the harness maximum
([#2882](block/buzz#2882))
([`3115a39a0`](block/buzz@3115a39))
- fix(relay): preserve reconnect backoff
([#2759](block/buzz#2759))
([`cd949e75a`](block/buzz@cd949e7))
- refactor(relay): expose reconnect timing policy
([#2310](block/buzz#2310))
([`94c91f110`](block/buzz@94c91f1))
- fix(desktop): clear stale working badges on agent stop/restart
([#2803](block/buzz#2803))
([`da2580e51`](block/buzz@da2580e))
- fix(desktop): surface agent rename relay profile sync failure as a
warning toast ([#2279](block/buzz#2279))
([`4d91195e9`](block/buzz@4d91195))
- fix(discovery): inject PATH into Codex adapter planning
([#2767](block/buzz#2767))
([`795f2d12e`](block/buzz@795f2d1))

**To release:** merge this PR. The tag and build will happen
automatically.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants