# Renderer Chain

> For agents: start with the [agent guide](https://pi-tui.ratstack.sh/llms.txt). Every page here has a Markdown twin, and every pattern has a JSON record.

Presentation / Tool and message output · `renderer-chain` · [HTML](https://pi-tui.ratstack.sh/patterns/renderer-chain/) · [JSON](https://pi-tui.ratstack.sh/patterns/renderer-chain.json) · [all patterns](https://pi-tui.ratstack.sh/patterns.md)

Also known as Renderer decoration.

## Intent

Add tool rendering while preserving an existing renderer or fallback.

## Motivation

Tool Display preserves existing renderers unless its explicit override policy requests replacement.

## Applicability

- Use this when rendering extensions must compose rather than overwrite each other.

## Structure

```text
resolver -> next() -> prior renderer
prior / fallback -> component
```

## Participants

- [`pi.registerToolRenderer`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#tool-rendering): Composes tool-renderer resolution through next.
- [`ToolDefinition.renderCall`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#tool-rendering): Renders the call before or during execution.
- [`ToolDefinition.renderResult`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#tool-rendering): Renders partial or final tool output.
- `Prior renderer`: Provides output to preserve, wrap or fall back from.

Pi component APIs: [`pi.registerToolRenderer`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#tool-rendering), [`ToolDefinition.renderCall`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#tool-rendering), [`ToolDefinition.renderResult`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#tool-rendering), `ToolRendererResolver`, `ToolExecutionComponent`, `Container`

## Consequences

- Current resolver composition can retain or wrap an existing renderer.
- The old registerTool interception and teardown strategy must not be copied as the default.

## Implementation

- Use the current resolver and next() rather than copying registerTool interception.
- The source adapter's teardown and override policy are historical implementation details.

## States

Each state was rendered at 40, 60, 80 and 120 columns in Pi's dark and light themes. Each frame links one WebP. An animated frame links its first image, then the animation.

### No existing renderer

`fallback`: Show a synthetic tool using the supplied fallback renderer.

Checks at every width and theme:

- width: ✓ pass
- style-leak: ✓ pass
- hard-coded-colour: ✓ pass
- height: ✓ pass

Frames:

- 40 columns: ![Renderer Chain, No existing renderer, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/renderer-chain/fallback-40-dark.cd11dd96c73a.webp) ![Renderer Chain, No existing renderer, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/renderer-chain/fallback-40-light.989286f2dedc.webp)
- 60 columns: ![Renderer Chain, No existing renderer, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/renderer-chain/fallback-60-dark.4b1f865a1eab.webp) ![Renderer Chain, No existing renderer, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/renderer-chain/fallback-60-light.7443c68a0b45.webp)
- 80 columns: ![Renderer Chain, No existing renderer, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/renderer-chain/fallback-80-dark.fbabb775feba.webp) ![Renderer Chain, No existing renderer, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/renderer-chain/fallback-80-light.ef127dcca8cb.webp)
- 120 columns: ![Renderer Chain, No existing renderer, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/renderer-chain/fallback-120-dark.ae6ac8608601.webp) ![Renderer Chain, No existing renderer, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/renderer-chain/fallback-120-light.e3931a71241b.webp)

### Existing renderer

`preserved`: Keep an earlier renderer's output intact.

Checks at every width and theme:

- width: ✓ pass
- style-leak: ✓ pass
- hard-coded-colour: ✓ pass
- height: ✓ pass

Frames:

- 40 columns: ![Renderer Chain, Existing renderer, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/renderer-chain/preserved-40-dark.3c00589da6d8.webp) ![Renderer Chain, Existing renderer, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/renderer-chain/preserved-40-light.197316da6e6e.webp)
- 60 columns: ![Renderer Chain, Existing renderer, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/renderer-chain/preserved-60-dark.097567875c29.webp) ![Renderer Chain, Existing renderer, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/renderer-chain/preserved-60-light.7c50e1866458.webp)
- 80 columns: ![Renderer Chain, Existing renderer, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/renderer-chain/preserved-80-dark.cb11e875f46e.webp) ![Renderer Chain, Existing renderer, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/renderer-chain/preserved-80-light.4287f2d41890.webp)
- 120 columns: ![Renderer Chain, Existing renderer, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/renderer-chain/preserved-120-dark.67080898d8ba.webp) ![Renderer Chain, Existing renderer, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/renderer-chain/preserved-120-light.77d19421ed4e.webp)

### Decorated output

`decorated`: Wrap an existing renderer's component with a small extra label.

Checks at every width and theme:

- width: ✓ pass
- style-leak: ✓ pass
- hard-coded-colour: ✓ pass
- height: ✓ pass

Frames:

- 40 columns: ![Renderer Chain, Decorated output, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/renderer-chain/decorated-40-dark.b3ef81983ed2.webp) ![Renderer Chain, Decorated output, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/renderer-chain/decorated-40-light.d698dd2f5e64.webp)
- 60 columns: ![Renderer Chain, Decorated output, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/renderer-chain/decorated-60-dark.c9faa96ee6ff.webp) ![Renderer Chain, Decorated output, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/renderer-chain/decorated-60-light.5cfb4775add4.webp)
- 80 columns: ![Renderer Chain, Decorated output, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/renderer-chain/decorated-80-dark.f8e5b3b26501.webp) ![Renderer Chain, Decorated output, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/renderer-chain/decorated-80-light.d9d90e312003.webp)
- 120 columns: ![Renderer Chain, Decorated output, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/renderer-chain/decorated-120-dark.bcbe9cbae23b.webp) ![Renderer Chain, Decorated output, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/renderer-chain/decorated-120-light.b24ba594e030.webp)

## Sample code

`stories/patterns/presentation/renderer-chain.ts`, the story the frames above were rendered from.

```ts
// Renderer Chain: preserve next() or decorate it; use a fallback only if absent.
import { Container, Text } from "@earendil-works/pi-tui";
import { ToolExecutionComponent, type ToolRenderers, type ToolRendererResolver } from "@earendil-works/pi-coding-agent";
import type { PatternStory } from "../../../src/pattern.ts";

export const story: PatternStory = {
  id: "renderer-chain", title: "Renderer Chain", kind: "component",
  apis: ["ToolRendererResolver", "ToolExecutionComponent", "Container"], rows: 12,
  states: [
    { id: "fallback", label: "No existing renderer" },
    { id: "preserved", label: "Existing renderer", steps: [{ type: "action", name: "preserve" }] },
    { id: "decorated", label: "Decorated output", steps: [{ type: "action", name: "decorate" }] },
  ],
  setup({ theme, tui, action }) {
    let existing = false, decorate = false;
    const prior: ToolRenderers = {
      renderCall: () => new Text(theme.fg("toolTitle", "lint_preview · original renderer"), 0, 0),
      renderResult: () => new Text(theme.fg("success", "Original result: 6 checks passed"), 0, 0),
    };
    const fallback: ToolRenderers = {
      renderCall: () => new Text(theme.fg("toolTitle", "lint_preview · fallback renderer"), 0, 0),
      renderResult: () => new Text(theme.fg("muted", "Fallback result: no prior renderer"), 0, 0),
    };
    // Register this typed callback with pi.registerToolRenderer in an extension.
    // The component harness has no extension registry; it drives the callback.
    const resolver: ToolRendererResolver = (toolName, next) => {
      const previous = next();
      if (toolName !== "lint_preview") return previous;
      if (!previous) return fallback;
      if (!decorate || !previous.renderResult) return previous;
      const renderResult = previous.renderResult;
      return {
        ...previous,
        renderResult(result, options, activeTheme, context) {
          const decorated = new Container();
          decorated.addChild(new Text(activeTheme.fg("accent", "Review badge · output below unchanged"), 0, 0));
          decorated.addChild(renderResult(result, options, activeTheme, context));
          return decorated;
        },
      };
    };
    const slot = new Container();
    function mount() {
      const renderers = resolver("lint_preview", () => existing ? prior : undefined);
      const tool = new ToolExecutionComponent("lint_preview", "example-lint-1", {},
        undefined, renderers, tui, "/workspace/example");
      tool.updateResult({ content: [], isError: false }, false);
      slot.children = [tool];
      tui.requestRender();
    }
    mount();
    action("preserve", () => { existing = true; mount(); });
    action("decorate", () => { decorate = true; mount(); });
    return slot;
  },
};
```

## Known uses: seen in Nico's repos

- [**pi-tool-display**](https://github.com/nicobailon/pi-tool-display): Composable renderer adapters
  - [src/tool-overrides.ts:1500-1538](https://github.com/nicobailon/pi-tool-display/blob/fca8c858a0989b63eba18ab935f3d8ed78354c3a/src/tool-overrides.ts#L1500-L1538) @fca8c858
  - [src/tool-overrides.ts:2040-2072](https://github.com/nicobailon/pi-tool-display/blob/fca8c858a0989b63eba18ab935f3d8ed78354c3a/src/tool-overrides.ts#L2040-L2072) @fca8c858

## For agents: choose and check

Choose Renderer Chain when your job matches its intent and applicability above. Its neighbours in Tool and message output are listed below. Read the one whose intent fits your job more closely before you commit.

- [Detail Fold](https://pi-tui.ratstack.sh/patterns/detail-fold.md): Show compact progress and summaries with bounded expanded tool detail.
- [Call Capsule](https://pi-tui.ratstack.sh/patterns/call-capsule.md): Retain a display shell through call and result renders of one tool execution.
- [Hinge Diff](https://pi-tui.ratstack.sh/patterns/hinge-diff.md): Choose compact, unified or split diff presentation from available width.
- [Word Spotlight](https://pi-tui.ratstack.sh/patterns/word-spotlight.md): Emphasize changed words inside parsed patch lines.
- [Error Digest](https://pi-tui.ratstack.sh/patterns/error-digest.md): Project structured errors into compact and expanded diagnostic output.
- [Message Fold](https://pi-tui.ratstack.sh/patterns/message-fold.md): Render custom message content as a preview with expanded detail.
- [Image Parachute](https://pi-tui.ratstack.sh/patterns/image-parachute.md): Render terminal images where supported and text placeholders otherwise.

Check your version:

- Render your version at 40, 60, 80 and 120 columns in the dark and light themes.
- Check width: no rendered line is wider than the terminal.
- Check style-leak: no line ends with colour, bold or a link still switched on.
- Check hard-coded-colour: every colour on screen comes from the active theme.
- Check height: the output fits in the rows the terminal has.
- Read the Pi 1.0.3 docs for [`pi.registerToolRenderer`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#tool-rendering), [`ToolDefinition.renderCall`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#tool-rendering), [`ToolDefinition.renderResult`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#tool-rendering), `ToolRendererResolver`, `ToolExecutionComponent`, `Container` before you use them.
- Every reference frame gets the verdict its state expects.

Next actions: `related({ id: "renderer-chain" })` lists what to read next, and `states({ id: "renderer-chain", width: 40 })` returns every frame and verdict at one width. The [build-pi-tui-component skill](https://pi-tui.ratstack.sh/skills/build-pi-tui-component.md) walks through the whole loop.

## Related

- [Call Capsule](https://pi-tui.ratstack.sh/patterns/call-capsule.md): retains per-execution renderer state

## Linked from

- [Call Capsule](https://pi-tui.ratstack.sh/patterns/call-capsule.md): selects or wraps the renderer
