Repository navigation
feat(messages): let attachment renderers open files from the host file card - #2239
Conversation
|
🤖 We'd like to build on this. Block's Markdown reader plugin (squareup/agents#8965) would claim Two notes:
I'm testing this branch with md-reader in the desktop app and will post results here. |
|
🤖 Follow-up for note 1: #2284 gives |
|
🤖 I tested this end to end with md-reader. I used the buzz-plugin-demos web shell (seeded fictional community), with this PR merged with #2284.
Not yet tried in the native desktop app with a real |
|
🤖 @joahg thanks for testing this with md-reader! This is ready for review. I can't request reviewers on this repo from a fork, so could you add yourself, or whoever should review it?
The PDF viewer that uses this is https://github.com/squareup/agents/pull/8960. |
joahg
left a comment
There was a problem hiding this comment.
🤖 Reviewed at fc7eb11 and tested it end to end with md-reader (squareup/agents#9003) plus #2284 in the demo shell. Clicking the card opens the reader with its message context, and Download still works. Approving. None of the following blocks merging:
- Files now reach
registerAttachmentmatchers. A plugin whose matcher ignoresattachment.kind(for example, a test on the name alone) will now claim file cards too. If its component renders without throwing, the host card disappears. The bundled matchers checkkind, so nothing breaks today. A sentence indocs/plugin-architecture.mdtelling matchers to checkattachment.kindwould cover it. - The failed-download text sits inside the Preview button (
FileAttachment.tsx). The card's meta text reads "Couldn't download · Retry", but clicking that text opens the preview. Retry lives on the trailing icon. Dropping "· Retry" on this path would avoid the confusion. onClick={onPreview}passes the click event to the callback.onClick={() => onPreview()}keeps a plugin that writesonPreview={open}from receiving aMouseEventas its target.
… links (#2284) ## Summary When several plugins claim the same attachment, a precedence rule now picks the one that draws it, instead of activation order. `registerAttachment` accepts the optional `order` that panels and link renderers already take: a number, or a function of the `Attachment`. Lower wins, and equal orders fall to the contribution key. This makes #2239 safe to build on with more than one file plugin installed. #2239 routes file cards through attachment renderers. Once it lands, a Markdown reader (squareup/agents#8965) and a future plugin that also handles `.md` (a summarizer, say) would both match the same card. Without `order`, whichever activated first wins, and disabling and re-enabling a plugin can flip the result. This PR applies the same bands as panels and links: `-10` specialises, `0` is the default, `100` is a catch-all. Tracks AGNTOPS-704. ## Changes - `src/plugins/contributions.ts`: `resolveMatch` and `MatchOrder` take the target type as a parameter (default `string`), so attachments can use the same resolver. Existing callers are unchanged. - `src/features/conversation/contracts.ts` (FOUNDATION; Joah approved this change): `AttachmentRenderer` gains `order?: MatchOrder<Attachment>`. - `src/features/conversation/service.tsx`: `registerAttachment` rejects an `order` that is neither a number nor a function, as `registerLink` does. - `src/features/conversation/AttachmentView.tsx`: picks the renderer with `resolveMatch` instead of the first match. - `docs/plugin-architecture.md`: the Resolution order section now covers attachment renderers. No bundled renderer sets `order`, so with one renderer per attachment nothing changes. ## Test plan - New `AttachmentView.test.tsx` covers four cases: - the lowest order wins, whatever the registry order; - equal orders fall to the contribution key; - an `order` function receives the attachment, and a throwing matcher is skipped; - with no match, the host card stays. - `service.test.tsx`: an invalid `order` is rejected, and a function `order` registers. - `vitest run src/features/conversation src/plugins src/features/messages src/bundled`: 7154 passed, 16 skipped. - `pnpm check` and the pre-push types-and-related-tests hook pass. ### Manual test steps 1. Run `just desktop` from this branch, with #2239 merged in so file cards use renderers. 2. Install two folder plugins that both match `.md` file attachments. One sets `order: -10` and draws a "Reader" label; the other uses the default and draws "Other". 3. Post a `.md` file. The card shows "Reader". 4. In Settings, disable both plugins, then enable the `order: -10` plugin last. The card still shows "Reader". 5. Remove `order` from the first plugin and reload it. The card shows whichever plugin's key sorts first, and toggling the plugins doesn't change it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Joah Gerstenberg <[email protected]> Co-authored-by: Hertz <c6741012416ef5c49a865db79682113385c5fb998e8c890234fb442b59e22748@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <[email protected]>
… links (block#2284) ## Summary When several plugins claim the same attachment, a precedence rule now picks the one that draws it, instead of activation order. `registerAttachment` accepts the optional `order` that panels and link renderers already take: a number, or a function of the `Attachment`. Lower wins, and equal orders fall to the contribution key. This makes block#2239 safe to build on with more than one file plugin installed. block#2239 routes file cards through attachment renderers. Once it lands, a Markdown reader (squareup/agents#8965) and a future plugin that also handles `.md` (a summarizer, say) would both match the same card. Without `order`, whichever activated first wins, and disabling and re-enabling a plugin can flip the result. This PR applies the same bands as panels and links: `-10` specialises, `0` is the default, `100` is a catch-all. Tracks AGNTOPS-704. ## Changes - `src/plugins/contributions.ts`: `resolveMatch` and `MatchOrder` take the target type as a parameter (default `string`), so attachments can use the same resolver. Existing callers are unchanged. - `src/features/conversation/contracts.ts` (FOUNDATION; Joah approved this change): `AttachmentRenderer` gains `order?: MatchOrder<Attachment>`. - `src/features/conversation/service.tsx`: `registerAttachment` rejects an `order` that is neither a number nor a function, as `registerLink` does. - `src/features/conversation/AttachmentView.tsx`: picks the renderer with `resolveMatch` instead of the first match. - `docs/plugin-architecture.md`: the Resolution order section now covers attachment renderers. No bundled renderer sets `order`, so with one renderer per attachment nothing changes. ## Test plan - New `AttachmentView.test.tsx` covers four cases: - the lowest order wins, whatever the registry order; - equal orders fall to the contribution key; - an `order` function receives the attachment, and a throwing matcher is skipped; - with no match, the host card stays. - `service.test.tsx`: an invalid `order` is rejected, and a function `order` registers. - `vitest run src/features/conversation src/plugins src/features/messages src/bundled`: 7154 passed, 16 skipped. - `pnpm check` and the pre-push types-and-related-tests hook pass. ### Manual test steps 1. Run `just desktop` from this branch, with block#2239 merged in so file cards use renderers. 2. Install two folder plugins that both match `.md` file attachments. One sets `order: -10` and draws a "Reader" label; the other uses the default and draws "Other". 3. Post a `.md` file. The card shows "Reader". 4. In Settings, disable both plugins, then enable the `order: -10` plugin last. The card still shows "Reader". 5. Remove `order` from the first plugin and reload it. The card shows whichever plugin's key sorts first, and toggling the plugins doesn't change it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Joah Gerstenberg <[email protected]> Co-authored-by: Hertz <c6741012416ef5c49a865db79682113385c5fb998e8c890234fb442b59e22748@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <[email protected]>
|
🤖 @ericm-square #2284 has landed, and this branch now conflicts with main in |
…e card File attachments in message rows now go through attachment renderers. Renderers receive placement and the host FileCard, so a plugin can make the card open a preview while the host keeps native download. Co-Authored-By: Helen Tian <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
fc7eb11 to
ac20f7d
Compare
|
@joahg Can you run the CI checks? |
* origin/main: feat(messages): let attachment renderers open files from the host file card (block#2239) feat(charts): bundle a Charts code-block renderer (block#3226) Expose native reply grouping through Conversation.ui (block#3245) NEXUS-4163: Keep plugin-owned chat deletion in Solo (block#2787) NEXUS-4156: Expose Solo Workspaces to plugin regions (block#2786) NEXUS-4022: Start region chats in the New Chat editor (block#2784) feat(link-previews): unfurl web links below messages by default (block#3225) fix(messages): keep names in literal Markdown as written when editing (block#2834) feat(plugins): fetch a packaged managed plugin policy from a URL (block#2504) Signed-off-by: Joah Gerstenberg <[email protected]> # Conflicts: # crates/plugin-manager/tests/management.rs # docs/plugin-architecture.md
…y-untagged-reactions * origin/main: (324 commits) fix(github): show private-repo body images from signed links (#3252) NEXUS-4029: Preserve semantic chips in the core composer (#2303) Make plugin signature enforcement opt-in (#2212) NEXUS-4156: Add workspace Leads and choose a chat's workspace (#2788) feat(plugins): report conclusively retired update providers (#3263) feat(messages): let attachment renderers open files from the host file card (#2239) feat(charts): bundle a Charts code-block renderer (#3226) Expose native reply grouping through Conversation.ui (#3245) NEXUS-4163: Keep plugin-owned chat deletion in Solo (#2787) NEXUS-4156: Expose Solo Workspaces to plugin regions (#2786) NEXUS-4022: Start region chats in the New Chat editor (#2784) feat(link-previews): unfurl web links below messages by default (#3225) fix(messages): keep names in literal Markdown as written when editing (#2834) feat(plugins): fetch a packaged managed plugin policy from a URL (#2504) fix(goose): enable deferred tool loading for goose agents (#3227) fix(unread): preserve manual intent in captured explicit reads (#3228) NEXUS-4337: Expose retained plugin update reviews (#3075) feat(conversation): add a link preview slot below messages (#3219) feat(plugins): let link previews read a public page's preview fields (#3217) Let sidebar row plugins decorate ordinary Solo chats (#3222) ...
* origin/main: (121 commits) Expose Solo region state before a component mounts (#3296) fix(builderlab): say a 401 was refused, not expired, and show each sign-in failure once (#3241) feat(agents): file new Solo chats in explicit personal workspaces (#2857) perf(css): limit broad style invalidation (#3267) Delay and fade in the Bestie badge celebration (#3292) fix(github): show private-repo body images from signed links (#3252) NEXUS-4029: Preserve semantic chips in the core composer (#2303) Make plugin signature enforcement opt-in (#2212) NEXUS-4156: Add workspace Leads and choose a chat's workspace (#2788) feat(plugins): report conclusively retired update providers (#3263) feat(messages): let attachment renderers open files from the host file card (#2239) feat(charts): bundle a Charts code-block renderer (#3226) Expose native reply grouping through Conversation.ui (#3245) NEXUS-4163: Keep plugin-owned chat deletion in Solo (#2787) NEXUS-4156: Expose Solo Workspaces to plugin regions (#2786) NEXUS-4022: Start region chats in the New Chat editor (#2784) feat(link-previews): unfurl web links below messages by default (#3225) fix(messages): keep names in literal Markdown as written when editing (#2834) feat(plugins): fetch a packaged managed plugin policy from a URL (#2504) fix(goose): enable deferred tool loading for goose agents (#3227) ... Signed-off-by: Tyler Longwell <[email protected]> Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
* origin/main: NEXUS-4029: Offer folder chips in the composer (block#2789) perf(css): narrow effort button invalidation (block#3297) NEXUS-4433: Expose saved plugin installation source kind (block#3242) fix(identity): keep an unbacked key safe on Replace and unblock a corrupt saved key (block#3233) Expose Solo region state before a component mounts (block#3296) fix(builderlab): say a 401 was refused, not expired, and show each sign-in failure once (block#3241) feat(agents): file new Solo chats in explicit personal workspaces (block#2857) perf(css): limit broad style invalidation (block#3267) Delay and fade in the Bestie badge celebration (block#3292) fix(github): show private-repo body images from signed links (block#3252) NEXUS-4029: Preserve semantic chips in the core composer (block#2303) Make plugin signature enforcement opt-in (block#2212) NEXUS-4156: Add workspace Leads and choose a chat's workspace (block#2788) feat(plugins): report conclusively retired update providers (block#3263) feat(messages): let attachment renderers open files from the host file card (block#2239) feat(charts): bundle a Charts code-block renderer (block#3226) Expose native reply grouping through Conversation.ui (block#3245) Signed-off-by: Joah Gerstenberg <[email protected]> # Conflicts: # src/features/conversation/ComposerCompletions.test.tsx # src/features/conversation/ComposerCompletions.tsx # src/features/messages/MessageComposer.tsx
…xt and CSV (#3338) Adapted from a Media viewer plugin by Krish Ray (@LordMelkor), with its PDF view from a PDF viewer plugin by Eric Morgan (@ericm-square). The long-file reader, outline, highlighting and PDF text were ported by @LordMelkor in joahg#1. ## Why Files shared in a conversation open only as downloads. With #2239 the host file card can hand its click to an attachment renderer. This PR bundles a viewer for the common text formats and PDFs, so a shared file opens in a tab beside the thread. ## What - New bundled plugin `buzz.file-viewer` (`src/bundled/file-viewer/`), **default off** for fresh installs: `enabledByDefault: false` in `src/bundled/index.ts`, left out of the Rust `enabled_by_default()` list, and listed in the default-off assertions in `management.rs` and `pages.integration.test.mjs`. - An attachment renderer wraps the host `FileCard`. In a message, its preview opens a `buzz-file:?url=…&name=…&mime=…&time=…` panel target. The card's download button is unchanged, and composer cards get no preview. - A `File viewer` panel (`keepOpen`, `deferUntilShown`) renders: - **Markdown, code and text of any length up to the 20 MB read limit, in parts.** A file over 150,000 characters reads in parts of at most 12,500 characters: each part formats as it comes near the view, a few stay prepared, and the rest are dropped, without the view jumping. A single line longer than a part is cut between words outside code spans, links and emphasis; if no such point exists, the two parts beside the cut show as plain text. Markdown over 64 KB is indexed in a worker; leaving the file ends it, and after 10 s, or if the worker fails, the file shows as plain text with a note. Code and text index linearly on the main thread (20 MB in about 50 ms). - **Markdown** through the shared `MarkdownDocument` with the document profile used by Canvas and Notes: no raw HTML, and only Buzz or safe links. The viewer adds heading ids, footnotes carried across parts, `#` links that move within the file, and images, which load only from the current community's media; any other image shows as its alt text. - **An outline** for a Markdown file with three or more h1-h3 headings. Jumps from it, and from `#` links, reach parts not yet shown. - **Code** highlighted with lowlight, using the host's neutral text roles, with line numbers that carry across parts. Each file has a time budget: after a second highlight call over 50 ms the rest stays plain, and a part with an unbroken run of more than 1,000 word characters skips highlighting. - **CSV and TSV** as a table, with an RFC 4180 parser capped at 5,000 rows, 200 columns and 50,000 cells. - **PDFs** with PDF.js. - **Copy text** for documents, tables and PDFs. A PDF's text is read only when the button is pressed. A scan says "No text to copy", and text cut at 5,000,000 characters says so. - A header with the message time, plus reading time and word count, or lines, for the whole file. - Renderer and panel both claim at `order: 100`, the band `plugin-architecture.md` gives bundled catch-alls. A plugin built for a specific format (the default order 0), or a fuller installed viewer at 50, keeps its files. It's named File viewer so Settings never lists two plugins called Media viewer, and so it isn't confused with the host's image media viewer. - Only `/media/` URLs on the current community's origin open, for files and Markdown images alike. They're read through `session.media()` and must resolve to the host media proxy. Any other file shows "Only files shared in this community open here" and is never fetched. Reads stop at 20 MB for text and 100 MB for PDFs, checked against `Content-Length` and again while streaming. - PDF.js (`pdfjs-dist`, pinned at 6.4.299, legacy build) loads through `lazy()` on the first PDF and runs in a worker served from `'self'`, so the CSP doesn't change. Its CMaps, standard fonts and image decoders ship as bundled assets served through a `BinaryDataFactory`, so a PDF needs nothing from the network beyond the file. Pages are laid out up to 1,000, and a page is drawn only once it nears the viewport. The view has page and zoom controls (‹ ›, the arrow keys, − +, Fit to width). - The panel tab takes the file's name. - Host changes: - `MarkdownProfile` gains optional `rehype` transforms, which `MarkdownDocument` passes to react-markdown. - `SharedLink` is exported, so a screen can wrap the shared link. - Vite resolves `decode-named-character-reference` to its table entry (`scripts/entity-decoder-alias.mjs`). The browser entry needs `document`, so it fails in the indexing worker. - New dependencies, pinned exactly and listed in NOTICE.md and `check-icons.mjs`: `pdfjs-dist`, `highlight.js`, `lowlight`, `github-slugger`, `unified`, `remark-parse` and `micromark-util-decode-string`, plus `@types/hast` and `remark-rehype` for tests only. - Docs: a "Reading shared files beside a conversation" section in `docs/buzz-help.md`, a paragraph and the defaults list in `docs/plugin-architecture.md`, and NOTICE attribution. ### Not in this PR The marketplace plugin also reads Office files, HTML pages, SVG and notebooks, plus GitHub Markdown links and message bodies. Those are left for follow-up PRs. ## Validation At the head of this PR, merged with current `main`: - `tests/browser/file-viewer.spec.mjs` passes on Chromium and WebKit (14 tests). It runs the real plugin over a fake relay, with the host `FileAttachment` as the card: - Opens Markdown (heading, task list, raw `<img onerror>` not rendered), CSV (quoted comma cell), Rust source, and a generated 2-page PDF. For the PDF it checks that the canvas has ink, then uses Next page and Zoom in. An unclaimed `.zip` stays a plain card, and every media request goes to the community's `/media/` path. - The viewer pane never scrolls sideways, at 1280px light and at 420px dark with 150% text. - A long file reads in parts and jumps into a part not yet shown without the view moving. Reopening the outline shows the current section, and End and Home focus rows that aren't drawn yet. - A community image draws in Markdown, and a remote one shows as its alt text. - A 500 from the media proxy shows a load error, and a PDF without text says it has none to copy. - A PDF in a non-embedded Japanese font draws, and its `UniJIS-UCS2-H` CMap loads from the app's own assets. - Vitest: all tests across `src/bundled/file-viewer`, `src/features/markdown`, `src/features/messages`, `src/bundled/channels`, `src/app` and discovery all pass. This includes the Markdown trust tests, and unit tests for parts (including cuts inside one long line), footnote link attributes, the part buffer, the outline, highlighting budgets, PDF text, Copy text, formats, targets, the community guard and CSV caps. - Indexing benchmarks (Node, `indexDocument`): | Input | Time | | --- | --- | | Text or code, 20 MB | 27 to 54 ms | | Markdown with nested lists, 4 MB | 1.1 s (in the worker) | | Markdown headings, 4 MB | 1.8 s (in the worker) | | `ini` highlighting of pathological 12.5k-character runs | under 1 ms | - `biome check --error-on-warnings`, `typecheck`, `design:check`, `check-icons`, `cargo test -p buzzodz-plugins` and `pnpm build` (which emits the indexing worker chunk) all pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Joah Gerstenberg <[email protected]> Signed-off-by: LordMelkor <[email protected]> Co-authored-by: Hertz <c6741012416ef5c49a865db79682113385c5fb998e8c890234fb442b59e22748@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: LordMelkor <[email protected]> Co-authored-by: Sigil <06114f072cf21e50fcd4133ab21346cdb1f74ee16d853a9729995fa6c03a251c@buzz.block.builderlab.xyz> Co-authored-by: Clod <c7b21872e9ddb79ca62967d0e21e6a4518a669eb04648e963cdfb2426e62fafb@buzz.block.builderlab.xyz>
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 withonPreviewmakes 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 receivemessage, 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 optionalplacement,messageandFileCardtoAttachmentRendererProps, plusFileCardProps.MessageRow.tsx: file attachments go throughAttachmentViewwithplacement="message",messageandFileCard. Audio and image renderers also getplacement.ComposerAttachmentViewer.tsx,ComposerAttachments.tsx: passplacement="composer". The draft viewer also passesFileCard.FileAttachment.tsx,Messages.module.css: the click-to-preview card with a separate Download action, taken from [buzz-app] Preview Markdown and common text attachments #1469 without its summary variant.docs/plugin-architecture.md: documentsregisterAttachment,placementandFileCard.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 renderFileCardthe same way, and the overlapping pieces should rebase cleanly onto this. Co-authored with htian-dev, whose code this is.Test plan
pnpm checkpasses.vitest run src/features/messages src/features/conversation src/bundled: 6941 passed.FileAttachment.test.tsx: preview and native download are separate keyboard actions.MessageRow.test.tsx: a plugin rendersFileCardin 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
just desktopfrom this branch and sign in..zip. Both show the usual file card, and clicking either downloads it.FileCardwithonPreviewopening a side-pane target (Settings → Plugins → Manage plugins). I tested with a PDF viewer plugin built this way..zipcard still downloads on click.🤖 Generated with Claude Code