Add host-owned SourceCode and Diff rendering for the app and plugins - #1876
Merged
ymichael merged 9 commits intoAug 20, 2026
Merged
Conversation
ymichael
force-pushed
the
bb/host-owned-sourcecode-and-diff-renderers-thr_5qukx8866e
branch
4 times, most recently
from
August 20, 2026 03:42
0c6dd35 to
052a6d6
Compare
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
force-pushed
the
bb/host-owned-sourcecode-and-diff-renderers-thr_5qukx8866e
branch
from
August 20, 2026 07:00
e6e1c81 to
2ecb000
Compare
ymichael
deleted the
bb/host-owned-sourcecode-and-diff-renderers-thr_5qukx8866e
branch
August 20, 2026 07:14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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'Fileview and the diff card drove itsFileDiffview, 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/diffsdirectly through the runtime shim and rebuild the host's behavior by hand.plugins/githubdid exactly that — synthesizing adiff --githeader for GitHub's REST patches, callingparsePatchFiles, and running two MutationObservers (one on the rootclass, one ondata-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.
SourceCodeHostandDiffHost(apps/app/src/components/code/) resolve an exclusive plugin replacement through the sameresolveReplacement+PluginReplacementSlotpath the sidebar thread list and file openers already use, and otherwise render BB's own renderer. Both BB renderers (BbSourceCode,BbDiff) sit behindlazy(), so a replacement that never delegates never downloads them andexperimental_Originalcosts nothing until it is rendered.Public API (all
experimental_, with entries indocs/api_to_audit.md):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-parsedParsedGitDiffFile, the raw patch text, the highlighter cache key, selection-to-composer — never cross the boundary, and no@pierre/diffs, Shiki,FileOptions, orParsedGitDiffFiletype appears in the public contract.experimental_Diffowns patch normalization, so a patch with nodiff --githeader (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/githubrendersexperimental_Diffand drops its@pierre/diffsdevDependency 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.tsnow owns that automatic/built-in/named-provider rule and the thread list consumes it too, retiring its private copy and theresolveThreadListReplacementwrapper 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 semanticDiffPresentation(view/overflow/showLineNumbers). The renderers own light/dark selection themselves, which made thethemeTypeprop chain fromThreadTimelineSurfacedown toTimelineFileDiffBlockdead; it andThreadTimelineThemeare removed.Docs and generated artifacts.
docs/api_to_audit.mdgains three entries (components, slots,experimental_Original); thebb-plugin-authoringskill and thebb guideplugins chapter now point at the host components rather than a direct@pierre/diffsimport, 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 noHOST_DAEMON_PROTOCOL_VERSIONbump.How you verified
Regression coverage for what the boundary actually promises — each of these fails without the corresponding behavior:
experimental_Originalloads it;applyResolvedCodeThemelive — the behaviorplugins/githubpreviously got from a DOM MutationObserver;DiffFileCard, a first-party surface, renders its text body through the same boundary;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 containsexperimental_Diffand 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
forbiddenBootPackagesstill pass. New lazy chunks:BbDiff2.6 KB raw / 1.1 KB brotli,BbSourceCode6.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'sexperimental_Diffrenders; and whether the silent crash fallback is right where the thread list toasts.