Skip to content

Expose exact captured-message reads - #3264

Merged
kazemek21 merged 13 commits into
mainfrom
kylez_NEXUS-4479_exact-captured-unread
Nov 11, 2026
Merged

kazemek21 merged 13 commits into
mainfrom
kylez_NEXUS-4479_exact-captured-unread

Conversation

@kazemek21

@kazemek21 kazemek21 commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR

This lets retained readers explicitly clear only a captured set of unread messages without including other messages or newer unread intent.
It adds one optional host method that saves reads for verified message IDs.

Why

Existing read methods can include a subtree or move a timeline boundary. A caller that already has an exact set needs a host operation that keeps that boundary exact.

What

UnreadCapability now has optional markCapturedMessagesRead(messageIds). Older hosts can omit it.

How

The method revalidates each message and saves only per-message keys in the captured set. After that save succeeds, it clears only those IDs' visit-local forced overlays and leaves the shared channel force hint untouched.

Risk

No caller or UI changes. Default read behavior remains unchanged.

Testing

No manual testing.

Generated with Codex (gpt-6-luna) on behalf of kylez

kazemek21 and others added 10 commits November 10, 2026 22:26
Co-authored-by: Codex <[email protected]>
Signed-off-by: Kyle Zemek <[email protected]>
Signed-off-by: Kyle Zemek <[email protected]>
Co-authored-by: Codex <[email protected]>
…e card (#2239)

## Summary

Lets plugins open file attachments the way
#1469 opens Markdown, without
bundling a previewer into Buzz.

File attachments in message rows now go through attachment renderers,
like images and audio already do. For files, a renderer also receives
`FileCard`: the host's own file card. Rendering it with `onPreview`
makes clicking the card open the preview, while a separate trailing
button keeps the host's native or browser download. Plugins can't reach
the native download themselves, so this keeps Download working for them
on desktop.

Renderers also receive `placement` (`"message"` or `"composer"`), so a
plugin can offer the preview only on sent messages and leave the
composer's attachment review alone. In message rows they also receive
`message`, the message that holds the attachment, so a preview can name
who shared it and where.

The first consumer is an external PDF viewer plugin: clicking a PDF card
opens it in the conversation side pane.

## Changes

- `contracts.ts` (FOUNDATION): adds optional `placement`, `message` and
`FileCard` to `AttachmentRendererProps`, plus `FileCardProps`.
- `MessageRow.tsx`: file attachments go through `AttachmentView` with
`placement="message"`, `message` and `FileCard`. Audio and image
renderers also get `placement`.
- `ComposerAttachmentViewer.tsx`, `ComposerAttachments.tsx`: pass
`placement="composer"`. The draft viewer also passes `FileCard`.
- `FileAttachment.tsx`, `Messages.module.css`: the click-to-preview card
with a separate Download action, taken from
#1469 without its summary variant.
- `docs/plugin-architecture.md`: documents `registerAttachment`,
`placement` and `FileCard`.

With no plugin claiming a file, message rows render exactly as before.

## Relationship to #1469

This takes only the host plumbing from
#1469 (the renderer routing,
`placement`, and the preview card) and leaves out the bundled File
Preview plugin. If #1469 lands later, its File Preview can render
`FileCard` the same way, and the overlapping pieces should rebase
cleanly onto this. Co-authored with htian-dev, whose code this is.

## Test plan

- `pnpm check` passes.
- `vitest run src/features/messages src/features/conversation
src/bundled`: 6941 passed.
- New tests:
- `FileAttachment.test.tsx`: preview and native download are separate
keyboard actions.
- `MessageRow.test.tsx`: a plugin renders `FileCard` in message
placement and receives the message; clicking it opens the plugin's
target; Download keeps the media address; an unclaimed file keeps the
plain download card.

### Manual test steps

1. Run `just desktop` from this branch and sign in.
2. Without any plugin, post a PDF and a `.zip`. Both show the usual file
card, and clicking either downloads it.
3. Install a plugin whose PDF renderer draws `FileCard` with `onPreview`
opening a side-pane target (Settings → Plugins → Manage plugins). I
tested with a PDF viewer plugin built this way.
4. Click the PDF card. The plugin's preview opens as a tab in the side
pane.
5. Click the download button at the right edge of the PDF card. The file
saves to Downloads, and the card shows "Saved to Downloads".
6. Tab to the card: focus lands on the card first, then on Download.
Enter on each does the same as clicking it.
7. The `.zip` card still downloads on click.
8. Attach a PDF in the composer and open it from the draft. The draft
view shows the plain card, with no side-pane preview.
9. Check both themes and a narrow window.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Helen Tian <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Joah Gerstenberg <[email protected]>
Add an optional host capability that saves reads for exactly the captured verified message IDs and clears only their visit-local forced overlays after persistence. Preserve independent shared channel markers and newer read intent.

Co-authored-by: Codex <[email protected]>
Signed-off-by: Kyle Zemek <[email protected]>
Signed-off-by: Kyle Zemek <[email protected]>
Co-authored-by: Codex <[email protected]>
@kazemek21
kazemek21 marked this pull request as ready for review November 11, 2026 17:48
@kazemek21
kazemek21 requested a review from a team as a code owner November 11, 2026 17:48

@gilgtm gilgtm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 No P0/P1 findings in the reviewed comparison at d35017315bd61cf18f3cce298b5dd8d4c2a27a93.

Posted by duel-review skill [driver: codex(high), mode: deep-dual, review synthesized by codex(high)]

Comment thread src/features/relay/read-state.ts Resolved
Comment thread src/features/relay/read-state.ts Resolved
@dr0uleau
dr0uleau requested a review from a team November 11, 2026 18:41
Comment thread src/features/relay/read-state.ts Resolved
@dr0uleau
dr0uleau requested a review from a team November 11, 2026 19:56

@dr0uleau dr0uleau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 no blocking findings in the completed review at eb2a0e836094c06e6f996113dd16ab6e715e7460

@kazemek21
kazemek21 merged commit 0397581 into main Nov 11, 2026
105 of 115 checks passed
@kazemek21
kazemek21 deleted the kylez_NEXUS-4479_exact-captured-unread branch November 11, 2026 22:25
joahg added a commit to joahg/buzz-app that referenced this pull request Nov 11, 2026
* origin/main:
  fix(identity): show a backed-up key as backed up, and drop dead retries (block#3223)
  fix(browser): open Builderlab account connections in the system browser (block#3287)
  Expose exact captured-message reads (block#3264)
  docs(agents): keep internal discussion out of public repo content (block#3258)
  fix(identity): clear a copied private key, name restore failures, fix storage copy (block#3236)
  fix(channels): list workspaces that load after `#` opens (block#3277)
  feat(profiles): show a larger avatar in the hover profile preview (block#3232)
  fix(identity): ask for credentials on the next sign-in after signing out (block#3231)
  fix(relay): keep saved channels when the community refuses sign-in (block#3215)

Signed-off-by: Joah Gerstenberg <[email protected]>
kazemek21 added a commit that referenced this pull request Nov 12, 2026
## TL;DR

This supports retained readers that need to undo an explicit read action
without overwriting newer unread intent. It offers Undo only for saved
local-unread markers removed by that action.

## How

Undo checks the current journal revision inside the storage transaction
and revalidates access, visit, and captured-message evidence. It
restores only markers removed by that exact saved action.

## Boundary and risk

Undo never restores forced-message overlays or rewinds or publishes read
frontiers. Results omit Undo if the completed read also cleared
forced-message overlays, or if force work was queued while the read
saved.

The handle expires after any later journal save, even if unrelated or
quiet. This whole-journal lifetime is deliberately conservative. Older
hosts may omit the optional handle.

## Related PRs

This change is independent of
[#3264](#3264),
[#2918](#2918),
[#2939](#2939),
[#1666](#1666), and
[#2956](#2956).

Generated with Codex (gpt-6-luna) on behalf of kylez

Signed-off-by: Kyle Zemek <[email protected]>
Co-authored-by: Codex (gpt-6-luna) <[email protected]>
joahg added a commit to joahg/buzz-app that referenced this pull request Nov 12, 2026
* origin/main: (38 commits)
  Fix reminder counts with compact dock corner tabs (block#3126)
  Add conditional undo for explicit reads (block#3291)
  fix(composer): edit channel links as atomic chips (block#3312)
  fix(agents): show remote instruction limit (block#3325)
  NEXUS-4033: Offer session skills in the core task composer (block#2790)
  feat(github): name pasted repository and commit links (block#3268)
  Let plugins give standing instructions to every agent turn (block#3285)
  fix(relay): read a channel's creation once, not per metadata version (block#3244)
  fix(messages): remove unconfirmed sends from this device (block#3317)
  fix: expose proven non-delivery for artifacts (block#3300)
  feat(plugins): let plugins mark a person's name in @mention suggestions (block#3260)
  fix(mcp): strip metadata from images agents attach (block#3311)
  fix(identity): name a linked key that isn't here, and keep Restore after a failed backup check (block#3238)
  feat(changesets): show the files a local agent changed on its reply (block#3214)
  fix(identity): show a backed-up key as backed up, and drop dead retries (block#3223)
  fix(browser): open Builderlab account connections in the system browser (block#3287)
  Expose exact captured-message reads (block#3264)
  docs(agents): keep internal discussion out of public repo content (block#3258)
  fix(identity): clear a copied private key, name restore failures, fix storage copy (block#3236)
  fix(channels): list workspaces that load after `#` opens (block#3277)
  ...
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.

5 participants