Skip to content

feat(messages): let attachment renderers open files from the host file card - #2239

Merged
joahg merged 3 commits into
block:mainfrom
ericm-square:ericm/file-attachment-card
Nov 11, 2026
Merged

joahg merged 3 commits into
block:mainfrom
ericm-square:ericm/file-attachment-card

Conversation

@ericm-square

@ericm-square ericm-square commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

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 [buzz-app] Preview Markdown and common text attachments #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

@joahg

joahg commented Nov 3, 2026

Copy link
Copy Markdown
Member

🤖 We'd like to build on this. Block's Markdown reader plugin (squareup/agents#8965) would claim .md file cards with it, so clicking the card opens the reader in the side pane. Buzz itself keeps no Markdown code. The contract looks right to us. Could you mark this ready for review?

Two notes:

  1. Attachment renderers resolve by first active match, so when two plugins claim the same file (say, a Markdown reader and a future summarizer), activation order decides. Panels and links already use order for this. I'll open a small follow-up that gives registerAttachment the same order rule.
  2. onPreview only applies to relay-hosted sources. An external https: file keeps its plain link card. Is that intended?

I'm testing this branch with md-reader in the desktop app and will post results here.

@joahg

joahg commented Nov 3, 2026

Copy link
Copy Markdown
Member

🤖 Follow-up for note 1: #2284 gives registerAttachment the same order rule that panels and links use. It doesn't depend on this PR, and either can merge first.

@joahg

joahg commented Nov 3, 2026

Copy link
Copy Markdown
Member

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

  • md-reader registered an attachment renderer that draws FileCard with onPreview. Clicking the .md card opened the reader as a side-pane tab, and the Download link stayed on the card. With no page errors, md-reader's own "Open in reader" fallback card could step aside.
  • One gap: renderers get the attachment but not the message it belongs to. md-reader's reader header uses the channel, message and author for "shared by … in #channel", so opening from the card drops that context. An optional message prop on AttachmentRendererProps (absent in the composer) would cover it. I'm happy to add it here or as a follow-up.
  • Nit: the default meta reads "MARKDOWN file". That's fine, since plugins can pass typeLabel.

Not yet tried in the native desktop app with a real buzz-media: source. I'll do that next.

@ericm-square
ericm-square marked this pull request as ready for review November 3, 2026 21:03
@ericm-square
ericm-square requested a review from a team as a code owner November 3, 2026 21:03
@ericm-square

Copy link
Copy Markdown
Contributor Author

🤖 @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?

  1. message prop: added in fc7eb11. Message rows now pass message, the message that holds the attachment, to every attachment renderer (file, audio and drawn image). The composer leaves it out. It's documented next to placement in docs/plugin-architecture.md, and the MessageRow test checks it.
  2. External https: files: yes, intended for now. Those cards open the link in the browser, and a plugin would need its own network access to read most external hosts, so the preview stays on relay-hosted files. Happy to widen it if a renderer needs it.
  3. feat(plugins): resolve attachment renderers by order, like panels and links #2284: looks good to us. The two PRs touch nearby lines in contracts.ts and docs/plugin-architecture.md, so whichever lands second may need a small rebase. I'm happy to do it on this side.
  4. "MARKDOWN file": agreed, plugins can pass typeLabel. The PDF viewer passes "PDF".

The PDF viewer that uses this is https://github.com/squareup/agents/pull/8960.

@joahg joahg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 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:

  1. Files now reach registerAttachment matchers. A plugin whose matcher ignores attachment.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 check kind, so nothing breaks today. A sentence in docs/plugin-architecture.md telling matchers to check attachment.kind would cover it.
  2. 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.
  3. onClick={onPreview} passes the click event to the callback. onClick={() => onPreview()} keeps a plugin that writes onPreview={open} from receiving a MouseEvent as its target.

joahg added a commit that referenced this pull request Nov 3, 2026
… 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]>
arividar pushed a commit to minniai/minni-buzz-app that referenced this pull request Nov 4, 2026
… 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]>
@joahg

joahg commented Nov 6, 2026

Copy link
Copy Markdown
Member

🤖 @ericm-square #2284 has landed, and this branch now conflicts with main in docs/plugin-architecture.md (that's the only conflict). Could you rebase when you get a chance? I'll re-approve after the push.

ericm-square and others added 2 commits November 9, 2026 16:21
…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]>
@ericm-square
ericm-square force-pushed the ericm/file-attachment-card branch from fc7eb11 to ac20f7d Compare November 9, 2026 22:26
@ericm-square

Copy link
Copy Markdown
Contributor Author

@joahg Can you run the CI checks?

@joahg joahg closed this Nov 10, 2026
@joahg joahg reopened this Nov 10, 2026
@joahg
joahg merged commit 367e4c4 into block:main Nov 11, 2026
70 checks passed
joahg added a commit to joahg/buzz-app that referenced this pull request Nov 11, 2026
* 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
bostonaholic added a commit that referenced this pull request Nov 11, 2026
…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)
  ...
tlongwell-block added a commit that referenced this pull request Nov 11, 2026
* 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>
joahg added a commit to joahg/buzz-app that referenced this pull request Nov 11, 2026
* 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
joahg added a commit that referenced this pull request Nov 14, 2026
…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>
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.

2 participants