Repository navigation
Feature request: plugin slot to render fenced code blocks in assistant messages (app.slots.experimental_codeFenceRenderer) #5268
Description
Workflow I could not complete
I want a plugin to render inline generative UI inside an agent's reply: the agent writes a fenced block such as
```ui
$seats = 10
Card("Monthly estimate", [Slider($seats, 1, 50, label: "Seats"), Metric("Total", "$" + 29 * $seats)])
```and the plugin renders it as a live component while the reply streams, in the spirit of ChatGPT's "Intelligent UI" (https://www.openui.com/blog/how-chatgpt-intelligent-ui-works). A second language (```app) carries a self-contained HTML document that the plugin runs in a sandboxed iframe.
Why current behavior falls short
- Plugins can only scrape BB's rendered DOM for
pre > code.language-*(e.g. from a content script) and overlay their own UI. This is fragile (depends on internal markup and class names), flickers as BB re-renders the code block on every streamed chunk, and loses local component state. messageDirectiveis single-line leaf syntax (::name{k="v"}), so it cannot carry a multi-line body, and incomplete directives stay literal until closed — it cannot render progressively while the body streams.
Proposed API (@get-bb/plugin-sdk/app)
app.slots.experimental_codeFenceRenderer(registration: ExperimentalCodeFenceRendererRegistration): void;
interface ExperimentalCodeFenceRendererRegistration {
id: string;
languages: readonly string[];
component: ComponentType<ExperimentalCodeFenceRendererProps>;
}
interface ExperimentalCodeFenceRendererProps {
code: string;
language: string;
index: number;
complete: boolean;
message: PluginMessageDirectiveMessage;
Original: ComponentType;
}id: unique within the plugin; letters, digits,-,_.languages: non-empty; each lowercase/^[a-z0-9][a-z0-9+#._-]*$/; a language may be claimed once per plugin (duplicates across that plugin's registrations are rejected at load).- Activates only in assistant and nested-agent message Markdown — exactly where
messageDirectiveactivates. User messages, file previews and plugin Markdown keep BB's code block. - Shares the core path:
MarkdownPreview's code renderer, the same pipeline used bymessageDirectiveand the built-inmermaidspecial case. - SDK testing harness:
loadPluginApp(...)exposescodeFenceRenderers, renderable withrenderSlot(app.codeFenceRenderers[0]!, props).
Streaming semantics
code: the fence body so far; grows while the reply streams.complete: true once the closing fence delimiter is present in the message source (derived per block).index: zero-based ordinal among fences of the same language in the same message (a```tsfence between two```uifences does not shift it); stable while streaming. Plugins key persistent state on${message.id}#${language}#${index}.- The component keeps its React identity while an open fence grows, so local state survives streaming. After the fence closes it may remount once (when the reply's settled region moves past it, or when the reply finishes); state that must survive is keyed as above.
useBbNavigate().openThreadPanelworks inside the component, as formessageDirective.
Fallback behavior
Originalrenders BB's code block for that fence (syntax highlighting, copy button; mermaid diagram formermaid), so a component can opt out per block.- Two plugins claiming one language: the first in deterministic slot order (plugin id order) wins; BB logs a console warning naming the claimants.
- Missing or disabled plugin → BB's code block.
- Crashing component → that renderer is disabled for the session and all its fences render BB's code block (same crash boundary as
messageDirective).
Alternatives considered
- DOM scraping + overlay from a content script (what plugins do today): fragile, flickers, no state retention, no streaming signal.
- Extending
messageDirectivewith a block/container form (:::name ... :::): new syntax the model must learn, and it conflicts with the existing "code-fenced directives stay literal" rule. Fences are what models already emit for structured payloads. - Replacing BB's whole Markdown renderer: far too broad; loses BB's handling for everything else.
experimental_sourceCodeRenderer: exclusive and global (file previews, diffs), not per-language and not message-scoped.
Where it plugs in today (base commit d474d2932dea14b5ccdfa305760c547c03a4d251)
MarkdownCodealready special-cases one fence language (mermaid) inside message Markdown:bb/apps/app/src/components/ui/markdown-preview.tsx
Lines 753 to 790 in d474d29
function MarkdownCode({ className: codeClassName, children, imagePolicy, linkRouting, node: _node, preferredTheme, rewriteLocalhostLinks, ...props }: MarkdownCodeRendererProps) { const codeText = typeof children === "string" ? children.replace(/\n$/, "") : ""; const language = getMarkdownCodeLanguage({ className: codeClassName }); const isBlock = isMarkdownCodeBlock({ codeText, language }); const [softWrap, setSoftWrap] = useState(false); const highlightedHtml = useMemo( () => isBlock && language !== "mermaid" ? highlightMarkdownCode({ code: codeText, language }) : null, [isBlock, language, codeText], ); const highlightedMarkup = useMemo( () => (highlightedHtml === null ? null : { __html: highlightedHtml }), [highlightedHtml], ); if (isBlock) { if (language === "mermaid" && imagePolicy === "render") { return ( <MarkdownMermaidDiagram preferredTheme={preferredTheme} source={codeText} /> ); } return ( <div className="my-2 overflow-hidden rounded-md border border-border bg-surface-recessed"> messageDirectiveresolution (registry, collision handling, crash fallback via the slot boundary) is the pattern this slot follows:export function remarkMessageDirectives( - Current directive registration contract:
bb/packages/plugin-sdk/src/app-contract.ts
Lines 1507 to 1513 in d474d29
export interface PluginMessageDirectiveRegistration { /** * The directive name. Lowercase kebab-case beginning with a letter. */ id: string; component: ComponentType<PluginMessageDirectiveProps>; }
Duplicate search (gh search issues --repo get-bb/bb): "code fence", "generative UI", "fenced code plugin", "codeFenceRenderer", "messageDirective multi-line", "inline UI plugin message", "render code block plugin" — no matching feature request found.
Prototype
Fork branch feat/code-fence-renderer: https://github.com/ComicBit/bb/tree/feat/code-fence-renderer
The prototype implements the API above end to end (SDK contract and collector validation, host slot store, MarkdownPreview wiring with per-piece fence ordinals across the incremental and streaming split renderers, Plugin Guide card, authoring docs, docs/api_to_audit.md entry). Unit/DOM tests cover collector validation; assistant vs. user message activation; index across the settled/tail streaming split; complete for unclosed, closed, list-nested, blockquoted, tilde, and over-indented fences; identity kept while an open fence grows; crash fallback; collision; and claims surviving the sanitized HTML pipeline.
Live check: pnpm dev from the branch, with a plugin registering ui and app and an omp agent thread. A ```ui reply rendered through the slot (no DOM overlay; slider interaction re-evaluated locally), and an ```app reply rendered in the plugin's sandboxed iframe. That check found one bug, now fixed in the branch: assistant messages containing < go through MarkdownHtml, whose sanitize schema stripped the claim attributes from code. Because the mobile app is a WebView shell around the same web app, the slot also applies there; not run on a device.
Per CONTRIBUTING.md, no PR will be opened until this is aligned.
AGENT GENERATED
Activity
bb-slop-cop commentedon Oct 9, 2026
🚨 SLOP COP 🚨 ·
new-issue-autopilotApplied missing classifications:
plugins,uiThis requests a new plugin rendering capability rather than unexpected existing behavior. It enables a meaningful workflow with a workaround; the public interaction surface and streaming/state contracts warrant High effort. No existing classification values needed preservation. Verified all values after writing, including final Type.
No reproduction report or pull request: this is a feature requiring API and product design alignment, not a bug eligible for an automatic fix.
Add a code fence renderer plugin slot and rebuild inline-vis on it (g…