# Hinge Diff

> 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 · `hinge-diff` · [HTML](https://pi-tui.ratstack.sh/patterns/hinge-diff/) · [JSON](https://pi-tui.ratstack.sh/patterns/hinge-diff.json) · [all patterns](https://pi-tui.ratstack.sh/patterns.md)

Also known as Adaptive diff.

## Intent

Choose compact, unified or split diff presentation from available width.

## Motivation

Tool Display and Dot314 choose edit layouts when narrow terminals cannot support split output.

## Applicability

- Use this when edit output must remain useful in narrow terminals.

## Structure

```text
parsed diff + width -> layout
layout -> compact / unified / split
```

## Participants

- [`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.
- [`truncateToWidth`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model): Clips text to its allotted columns and can pad the result.
- [`visibleWidth`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model): Measures rendered terminal columns rather than string length.
- `Parsed patch`: Supplies the same changes to several width-dependent layouts.

Pi component APIs: [`ToolDefinition.renderResult`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#tool-rendering), [`truncateToWidth`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), [`visibleWidth`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), `ToolExecutionComponent`

## Consequences

- One patch can have compact, unified or split presentation.
- A narrower layout shows less side-by-side context.

## Implementation

- Do not force split panes where their columns do not fit.
- Keep pending preview reads safe and separate from applying an edit.

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

### Split diff

`wide`: Show a synthetic before/after diff where both sides fit.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Hinge Diff, Split diff, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/hinge-diff/wide-40-dark.dc22451e030f.webp) ![Hinge Diff, Split diff, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/hinge-diff/wide-40-light.8d344a4dfc18.webp)
- 60 columns: ![Hinge Diff, Split diff, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/hinge-diff/wide-60-dark.54bd8dbc78d9.webp) ![Hinge Diff, Split diff, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/hinge-diff/wide-60-light.e6d58b2193f1.webp)
- 80 columns: ![Hinge Diff, Split diff, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/hinge-diff/wide-80-dark.af585414d81a.webp) ![Hinge Diff, Split diff, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/hinge-diff/wide-80-light.5449f89e134c.webp)
- 120 columns: ![Hinge Diff, Split diff, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/hinge-diff/wide-120-dark.a4634cd706dd.webp) ![Hinge Diff, Split diff, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/hinge-diff/wide-120-light.939e3865f80d.webp)

### Unified diff

`narrow`: Switch the same patch to unified lines at narrow widths.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Hinge Diff, Unified diff, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/hinge-diff/narrow-40-dark.dc22451e030f.webp) ![Hinge Diff, Unified diff, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/hinge-diff/narrow-40-light.8d344a4dfc18.webp)
- 60 columns: ![Hinge Diff, Unified diff, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/hinge-diff/narrow-60-dark.54bd8dbc78d9.webp) ![Hinge Diff, Unified diff, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/hinge-diff/narrow-60-light.e6d58b2193f1.webp)
- 80 columns: ![Hinge Diff, Unified diff, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/hinge-diff/narrow-80-dark.7273b24e983d.webp) ![Hinge Diff, Unified diff, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/hinge-diff/narrow-80-light.0080028c52c7.webp)
- 120 columns: ![Hinge Diff, Unified diff, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/hinge-diff/narrow-120-dark.c446e221b1f6.webp) ![Hinge Diff, Unified diff, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/hinge-diff/narrow-120-light.435e586e4722.webp)

### Compact diff

`compact`: Show a compact summary with an explicit expansion hint.

Checks at every width and theme:

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

Frames:

- 40 columns: ![Hinge Diff, Compact diff, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/hinge-diff/compact-40-dark.252a1e362401.webp) ![Hinge Diff, Compact diff, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/hinge-diff/compact-40-light.867cebae6b34.webp)
- 60 columns: ![Hinge Diff, Compact diff, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/hinge-diff/compact-60-dark.2e1197255aef.webp) ![Hinge Diff, Compact diff, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/hinge-diff/compact-60-light.93a1f4e6fdc1.webp)
- 80 columns: ![Hinge Diff, Compact diff, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/hinge-diff/compact-80-dark.e6d7125925a5.webp) ![Hinge Diff, Compact diff, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/hinge-diff/compact-80-light.a438b67c67ab.webp)
- 120 columns: ![Hinge Diff, Compact diff, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/hinge-diff/compact-120-dark.fbe015707776.webp) ![Hinge Diff, Compact diff, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/hinge-diff/compact-120-light.6c4e5b6040f6.webp)

## Sample code

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

```ts
// Hinge Diff: fold one parsed patch into split, unified or compact output.
import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
import { ToolExecutionComponent, type ToolRenderers } from "@earendil-works/pi-coding-agent";
import type { PatternStory } from "../../../src/pattern.ts";

export const story: PatternStory = {
  id: "hinge-diff", title: "Hinge Diff", kind: "component",
  apis: ["ToolExecutionComponent", "ToolDefinition.renderResult", "truncateToWidth", "visibleWidth"], rows: 16,
  states: [
    { id: "wide", label: "Split when width permits" },
    { id: "narrow", label: "Unified diff", steps: [{ type: "action", name: "narrow" }] },
    { id: "compact", label: "Compact diff", steps: [{ type: "action", name: "compact" }] },
  ],
  setup({ theme, tui, action }) {
    const patch = [
      { line: 8, before: "const limit = 4;", after: "const limit = 8;" },
      { line: 9, before: "return rows.slice(0, limit);", after: "return rows.slice(-limit);" },
    ];
    let mode: "adaptive" | "unified" | "compact" = "adaptive";
    const renderers: ToolRenderers = {
      renderCall: () => new Text(theme.fg("toolTitle", "edit · src/preview.ts"), 0, 0),
      renderResult() {
        return {
          invalidate() {},
          render(width) {
            if (mode === "compact") return [
              theme.fg("success", "2 lines changed · +2 / -2"),
              theme.fg("muted", "Expand to inspect the patch"),
            ];
            const split = mode === "adaptive" && width >= 72;
            const lines = [theme.fg("dim", split ? "Before  │  After" : "Unified · narrow-safe layout")];
            for (const row of patch) {
              const before = row.line + " - " + row.before;
              const after = row.line + " + " + row.after;
              if (split) {
                const leftWidth = Math.floor((width - 3) / 2);
                const left = truncateToWidth(before, leftWidth, "…", true);
                const rightWidth = width - visibleWidth(left) - 3;
                lines.push(theme.fg("toolDiffRemoved", left) + theme.fg("border", " │ ") +
                  theme.fg("toolDiffAdded", truncateToWidth(after, rightWidth)));
              } else {
                lines.push(theme.fg("toolDiffRemoved", truncateToWidth(before, width)));
                lines.push(theme.fg("toolDiffAdded", truncateToWidth(after, width)));
              }
            }
            return lines;
          },
        };
      },
    };
    const tool = new ToolExecutionComponent("edit", "example-edit-1", {},
      undefined, renderers, tui, "/workspace/example");
    tool.updateResult({ content: [], isError: false }, false);
    action("narrow", () => { mode = "unified"; tool.invalidate(); tui.requestRender(); });
    action("compact", () => { mode = "compact"; tool.invalidate(); tui.requestRender(); });
    return tool;
  },
};
```

## Known uses: seen in Nico's repos

- [**dot314**](https://github.com/nicobailon/dot314): Choose diff layout from available width
  - [extensions/pi-codex-apply-patch-display/diff-renderer.ts:1210-1238](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/pi-codex-apply-patch-display/diff-renderer.ts#L1210-L1238) @17cce138
- [**pi-tool-display**](https://github.com/nicobailon/pi-tool-display): Choose diff layout from available width
  - [src/diff-renderer.ts:1-35](https://github.com/nicobailon/pi-tool-display/blob/fca8c858a0989b63eba18ab935f3d8ed78354c3a/src/diff-renderer.ts#L1-L35) @fca8c858
  - [src/diff-renderer.ts:2390-2435](https://github.com/nicobailon/pi-tool-display/blob/fca8c858a0989b63eba18ab935f3d8ed78354c3a/src/diff-renderer.ts#L2390-L2435) @fca8c858
  - [src/pending-diff-preview.ts:1-35](https://github.com/nicobailon/pi-tool-display/blob/fca8c858a0989b63eba18ab935f3d8ed78354c3a/src/pending-diff-preview.ts#L1-L35) @fca8c858
  - [src/pending-diff-preview.ts:160-245](https://github.com/nicobailon/pi-tool-display/blob/fca8c858a0989b63eba18ab935f3d8ed78354c3a/src/pending-diff-preview.ts#L160-L245) @fca8c858

## For agents: choose and check

Choose Hinge Diff 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.
- [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.
- [Renderer Chain](https://pi-tui.ratstack.sh/patterns/renderer-chain.md): Add tool rendering while preserving an existing renderer or fallback.
- [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 [`ToolDefinition.renderResult`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/extensions.md#tool-rendering), [`truncateToWidth`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), [`visibleWidth`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/coding-agent/docs/tui.md#understand-the-component-model), `ToolExecutionComponent` before you use them.
- Every reference frame gets the verdict its state expects.

Next actions: `related({ id: "hinge-diff" })` lists what to read next, and `states({ id: "hinge-diff", 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

- [Hinge Panel](https://pi-tui.ratstack.sh/patterns/hinge-panel.md): uses the same width-driven rearrangement

## Linked from

- [Word Spotlight](https://pi-tui.ratstack.sh/patterns/word-spotlight.md): chooses the enclosing diff layout
