Skip to content

Feature request: plugin slot to render fenced code blocks in assistant messages (app.slots.experimental_codeFenceRenderer) #5268

Description

@ComicBit

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.
  • messageDirective is 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 messageDirective activates. User messages, file previews and plugin Markdown keep BB's code block.
  • Shares the core path: MarkdownPreview's code renderer, the same pipeline used by messageDirective and the built-in mermaid special case.
  • SDK testing harness: loadPluginApp(...) exposes codeFenceRenderers, renderable with renderSlot(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 ```ts fence between two ```ui fences 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().openThreadPanel works inside the component, as for messageDirective.

Fallback behavior

  • Original renders BB's code block for that fence (syntax highlighting, copy button; mermaid diagram for mermaid), 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 messageDirective with 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)

  • MarkdownCode already special-cases one fence language (mermaid) inside message Markdown:
    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">
  • messageDirective resolution (registry, collision handling, crash fallback via the slot boundary) is the pattern this slot follows:
    export function remarkMessageDirectives(
  • Current directive registration contract:
    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

  1. added
    pluginsPlugin SDK, runtime, marketplace
    uiApp shell, sidebar, composer, rendering
    on Oct 9, 2026
  2. bb-slop-cop commented on Oct 9, 2026

    @bb-slop-cop
    Contributor

    🚨 SLOP COP 🚨 · new-issue-autopilot

    Applied missing classifications:

    • Type: Feature
    • Priority: Medium
    • Effort: High
    • Labels: plugins, ui

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    pluginsPlugin SDK, runtime, marketplaceuiApp shell, sidebar, composer, rendering

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions