Skip to content

Add host-owned SourceCode and Diff rendering for the app and plugins - #1876

Merged
ymichael merged 9 commits into
mainfrom
bb/host-owned-sourcecode-and-diff-renderers-thr_5qukx8866e
Aug 20, 2026
Merged

Add host-owned SourceCode and Diff rendering for the app and plugins#1876
ymichael merged 9 commits into
mainfrom
bb/host-owned-sourcecode-and-diff-renderers-thr_5qukx8866e

Conversation

@ymichael

Copy link
Copy Markdown
Collaborator

What was wrong

BB rendered code in two independent places and had no way for a plugin to change either. The file preview drove @pierre/diffs' File view and the diff card drove its FileDiff view, each assembling its own options record, its own line-selection wiring, and its own code-theme lookup. A plugin that wanted to render source or a diff had a third path: import @pierre/diffs directly through the runtime shim and rebuild the host's behavior by hand. plugins/github did exactly that — synthesizing a diff --git header for GitHub's REST patches, calling parsePatchFiles, and running two MutationObservers (one on the root class, one on data-bb-code-theme-*) to keep Pierre's theme in step with BB's.

That is three copies of one capability, and it made "change how bb renders code" impossible to express: there was nothing to replace.

What changed

One host boundary per capability. SourceCodeHost and DiffHost (apps/app/src/components/code/) resolve an exclusive plugin replacement through the same resolveReplacement + PluginReplacementSlot path the sidebar thread list and file openers already use, and otherwise render BB's own renderer. Both BB renderers (BbSourceCode, BbDiff) sit behind lazy(), so a replacement that never delegates never downloads them and experimental_Original costs nothing until it is rendered.

Public API (all experimental_, with entries in docs/api_to_audit.md):

experimental_SourceCode: { content, path, overflow?, highlightedLines?, className? }
experimental_Diff:       { patch, path, view?, overflow?, showLineNumbers?, className? }

app.slots.experimental_sourceCodeRenderer({ id, title, description?, component })
app.slots.experimental_diffRenderer({ id, title, description?, component })

A replacement receives fully resolved semantic props plus a bound experimental_Original, so it can delegate per call without re-entering resolution. Host-only inputs — the pre-parsed ParsedGitDiffFile, the raw patch text, the highlighter cache key, selection-to-composer — never cross the boundary, and no @pierre/diffs, Shiki, FileOptions, or ParsedGitDiffFile type appears in the public contract. experimental_Diff owns patch normalization, so a patch with no diff --git header (GitHub REST, a bare @@ hunk) renders without the caller synthesizing one; content that parses to no hunks degrades to plain monospace text instead of an empty diff.

Migrations. The native file preview, timeline file diffs, and the environment diff panel's file bodies all render through the boundary, so one registration covers BB's surfaces and plugin surfaces alike. plugins/github renders experimental_Diff and drops its @pierre/diffs devDependency along with both MutationObservers; its bundle no longer references the Pierre runtime shim (417.0 KB → 409.0 KB). The shim itself stays for compatibility with existing plugins.

User control. Settings → Appearance gains Source code and Diffs rows beside Sidebar — Automatic / bb (built-in) / each registered provider, per client, each row hidden when no plugin supplies that renderer. A renderer takes over surfaces there is otherwise no route back from, so this is what keeps "installing activates it" reversible. lib/plugin-replacement-preference.ts now owns that automatic/built-in/named-provider rule and the thread list consumes it too, retiring its private copy and the resolveThreadListReplacement wrapper only its own test still called. Registered renderers also appear in the plugin detail's capability list.

Two cleanups fell out. The opaque diffViewOptions: Record<string, string | boolean | number> threaded through five components became the semantic DiffPresentation (view / overflow / showLineNumbers). The renderers own light/dark selection themselves, which made the themeType prop chain from ThreadTimelineSurface down to TimelineFileDiffBlock dead; it and ThreadTimelineTheme are removed.

Docs and generated artifacts. docs/api_to_audit.md gains three entries (components, slots, experimental_Original); the bb-plugin-authoring skill and the bb guide plugins chapter now point at the host components rather than a direct @pierre/diffs import, and the skill's own coverage test is extended so the new slots and their props stay documented. Bundled SDK declarations, the runtime export manifest, and the templates bundle were regenerated with the repository scripts. No wire changes, so no HOST_DAEMON_PROTOCOL_VERSION bump.

How you verified

Regression coverage for what the boundary actually promises — each of these fails without the corresponding behavior:

  • a replacement that never delegates leaves BB's renderer chunk unloaded; delegating through experimental_Original loads it;
  • the replacement receives resolved semantic props only — no parsed file, no cache key, no selection-to-chat;
  • with no patch text in hand the host reconstructs a single-file patch that re-parses to the same file;
  • crash and no-registration paths land on BB's renderer;
  • BB's diff renderer follows applyResolvedCodeTheme live — the behavior plugins/github previously got from a DOM MutationObserver;
  • DiffFileCard, a first-party surface, renders its text body through the same boundary;
  • a built-in pin renders BB's renderer with the plugin still installed and enabled, and an explicit pin survives a later plugin whose id sorts ahead of it (where automatic selection would silently swap the user's renderer).

Commands run:

  • pnpm exec turbo run typecheck --filter=@bb/app --filter=@get-bb/plugin-sdk --filter=@bb/plugin-build --filter=@bb/templates --filter=@bb/server --filter=bb-plugin-github --filter=@bb/cli — passed.
  • pnpm exec turbo run test --filter=@bb/app — 367 files / 2909 tests passed.
  • pnpm exec turbo run test --filter=@bb/server — 182 files / 1730 tests passed.
  • pnpm exec turbo run test --filter=@get-bb/plugin-sdk --filter=@bb/plugin-build --filter=@bb/templates --filter=bb-plugin-github --filter=@bb/cli — 631 tests passed.
  • pnpm exec turbo run lint --filter=@bb/app — 0 errors (the repository's existing 147 react-compiler warnings; several are carried over verbatim with the extracted renderer code).
  • pnpm exec turbo run build --filter=bb-plugin-github — builds; the bundle contains experimental_Diff and no Pierre shim reference.

Bundle impact. Boot payload 1655.6 → 1659.1 KB raw and 448.8 → 453.7 KB brotli against the 1671.0 / 456.4 budget; 40 → 43 chunks. I confirmed this is not new code on the boot path — stubbing out the patch reconstruction moved it by 0.3 KB — it is the extra chunks compressing slightly worse than fewer larger ones. All forbiddenBootPackages still pass. New lazy chunks: BbDiff 2.6 KB raw / 1.1 KB brotli, BbSourceCode 6.1 KB raw / 2.4 KB brotli. Brotli headroom drops from ~7.6 KB to ~2.7 KB, which is worth knowing before the next feature lands on that budget.

Deliberately not migrated. Markdown fenced code blocks are genuine source but run sugar-high — a synchronous ~2 KB highlighter with no worker pool, no shadow DOM, and no async settle, which is what a streaming chat timeline needs. Routing them through the host would either regress that or require a second per-surface default renderer, contradicting one host per capability. Raw-log surfaces (EventCodeBlock, terminal output) are monospace styling, not code rendering, and are untouched.

Open questions for review, all recorded in docs/api_to_audit.md: whether a per-client pin is the right scope for something as visible as every diff (and whether users expect one combined choice rather than two); whether a plugin should be able to change how another plugin's experimental_Diff renders; and whether the silent crash fallback is right where the thread list toasts.

AGENT GENERATED: by Claude Opus 5

@ymichael
ymichael force-pushed the bb/host-owned-sourcecode-and-diff-renderers-thr_5qukx8866e branch 4 times, most recently from 0c6dd35 to 052a6d6 Compare August 20, 2026 03:42
ymichael and others added 9 commits August 19, 2026 23:51
BB had two independent code-rendering call sites — the file preview's
`@pierre/diffs` File view and the diff card's FileDiff view — and plugins
had a third: import `@pierre/diffs` directly and reconstruct the patch
normalization and code-theme wiring by hand (plugins/github did exactly
that, MutationObservers on `data-bb-code-theme-*` included).

Introduce one host boundary per capability. `SourceCodeHost` and
`DiffHost` resolve an exclusive plugin replacement through the same
resolver/PluginReplacementSlot path the sidebar thread list and file
openers use, and otherwise render BB's own renderer. Both BB renderers
sit behind `lazy()`, so a replacement that never delegates never
downloads them and `experimental_Original` costs nothing until rendered.

The renderers now own the code theme and light/dark selection themselves,
which makes the `themeType` prop threaded from ThreadTimelineSurface down
to TimelineFileDiffBlock dead; it is removed along with the opaque
`diffViewOptions` record, replaced by the semantic `DiffPresentation`
(view / overflow / showLineNumbers) that a plugin replacement can also
receive.

Public surface (all experimental, entries follow in docs/api_to_audit.md):
`experimental_SourceCode`, `experimental_Diff`,
`app.slots.experimental_sourceCodeRenderer`, and
`app.slots.experimental_diffRenderer`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The plugin reconstructed the host's diff behavior by hand: synthesize a
`diff --git` header for GitHub's REST patches, call parsePatchFiles, pick
light/dark from a MutationObserver on the root `class`, and read the
resolved code theme from a second MutationObserver on
`data-bb-code-theme-*`. All four are now the host's job, so the plugin
renders `experimental_Diff` and keeps only its own card chrome.

Drops the `@pierre/diffs` devDependency with the direct import: the built
bundle no longer references the Pierre runtime shim at all (417.0 KB →
409.0 KB).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the three api_to_audit entries the new public members need
(components, replacement slots, experimental_Original), points the
bb-plugin-authoring skill and the bb guide at the host components instead
of a direct @pierre/diffs import, and extends the skill's own coverage
test so the two new slots and their resolved props stay documented.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Regression coverage for what the boundary actually promises and what the
old code could not do:

- a replacement that never delegates leaves BB's renderer chunk unloaded,
  and delegating through experimental_Original loads it (the lazy split);
- the replacement receives resolved semantic props only — no pre-parsed
  file, no selection-to-chat, no cache key;
- with no patch text in hand the host reconstructs a complete single-file
  patch that re-parses to the same file;
- crash and no-registration paths land on BB's renderer;
- BB's diff renderer follows applyResolvedCodeTheme live, which is the
  behavior the GitHub plugin used to get from a DOM MutationObserver;
- DiffFileCard — a first-party surface — renders its text body through the
  same boundary, so one registration covers BB and plugins alike.

Also makes experimental_Diff degrade to plain text for content that is not
a patch. Completing a `diff --git` header in front of arbitrary text still
parses, just to a file with zero hunks, so the previous behavior was an
empty diff where the caller expected their content.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Plugin detail lists every slot kind a plugin registers; without entries
  for the two renderer slots a renderer plugin would install showing no
  capability at all.
- FilePreview's `path` and `state.file.name` are not the same string (the
  latter can be a shorter server-supplied display name). Send `path` to the
  host so a copied selection stays labelled the way it was, and keep the
  highlighter's cache keyed on `file.name` as before.
- The GitHub plugin's DiffPatch wrapper became a pass-through once the host
  owned normalization and theming; call the host component directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A renderer replacement takes over surfaces the user has no other route
back from — the file preview, every diff in the app — so installing a
plugin that registers one was effectively irreversible short of disabling
the whole plugin. The sidebar thread list already solved this with an
automatic/built-in/named-provider pin; source and diff renderers now get
the same one, as two independent rows beside it.

Rather than a third copy of that rule, lib/plugin-replacement-preference
owns it: the two sentinel keys, the provider key, the storage-atom
factory, and the resolution. The thread list consumes it too, which
retires its private copy and the resolveThreadListReplacement wrapper
that only its own test still called. Its exported names are unchanged.

Both hosts read the pin instead of resolving slots directly, so a pin
covers BB's own preview, timeline diffs, and diff panel as well as plugin
experimental_Diff calls. Two host tests hold that down: a built-in pin
renders BB's renderer with the plugin still enabled, and an explicit pin
survives a later plugin whose id sorts ahead of the pinned one — the case
where automatic selection would silently swap the renderer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Timeline file-change diffs rendered as an empty 32px card: the header and
its +/- tally, then nothing. The diff panel was fine.

Unifying the diff options behind the host boundary moved
`expansionLineCount: 30` — which main set only in the diff panel's base
options — onto every diff. The timeline never had it, deliberately: its
rows carry a hunk-only patch synthesized from the file-change event, with
no way to fetch the full file. Handing pierre an expansion budget for a
partial patch makes it render zero lines rather than ignore the option.

Tie the option to the capability instead of the surface: the card sends it
only when `contextExpansion` is available, which is exactly when a content
fetcher is wired. It stays host-only and never reaches a plugin
replacement, since context expansion is a BB renderer capability rather
than part of the semantic contract.

Verified against origin/main in the dev app on the same thread: timeline
1696px / 76 rendered lines and panel 4 containers / 181 lines, both
identical before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pin scope, cross-plugin scope, and the silent crash fallback were posed as
open questions. All three are decided as shipped, so state them as decisions
with their reasons instead of leaving a future auditor to re-litigate them.
The parts still genuinely open — one combined "code rendering" choice, and
whether source and diff stay separately replaceable — stay open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
main's #2007 applied usePierreStrictModeRecoveryOptions to the built-in diff
card and to the plugin @pierre/diffs/react shim. This branch routes both of
those through the host renderers instead — the built-in card and the GitHub
plugin now render via BbDiff, and experimental_SourceCode renders via
BbSourceCode — so without moving the hook the dev-only highlighting bug would
have come straight back for every consumer the host boundary took over.

BbSourceCode is now the single implementation behind BB's file preview and the
public experimental_SourceCode, so the plugin-surface fix necessarily reaches
the file preview too. The hook is a no-op outside import.meta.env.DEV.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ymichael
ymichael force-pushed the bb/host-owned-sourcecode-and-diff-renderers-thr_5qukx8866e branch from e6e1c81 to 2ecb000 Compare August 20, 2026 07:00
@ymichael
ymichael merged commit ea61d17 into main Aug 20, 2026
13 checks passed
@ymichael
ymichael deleted the bb/host-owned-sourcecode-and-diff-renderers-thr_5qukx8866e branch August 20, 2026 07:14
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.

1 participant