An extension for the Pi and
Oh My Pi terminal coding agents. It makes agents actively use
visual explanations instead of defaulting to walls of text. The model writes compact declarative
D2 source through one diagram tool. The extension renders that source as
Unicode in the terminal and as reusable SVG and PNG output.
If any of those names are new to you:
- D2 is a declarative diagram language. Text such as
client -> gateway -> apicompiles to a laid-out diagram, and the same language covers containers, sequence diagrams, and SQL tables. - Pi and Oh My Pi are terminal coding agents. An extension is an npm package they load at startup to add tools the model can call.
Coding agents explain spatial things in prose: "A calls B, B queues C, C writes D, D emits an event back to A." The user rebuilds a graph in their head that the agent could have drawn. A fenced Mermaid block moves the problem rather than solving it, because a terminal shows the source instead of the picture.
The goal is to change how agents explain, not just add a renderer. When structure, flow, sequence, data, or relationships are easier to see than to read, the agent leads with a diagram and limits prose to details the picture cannot show.
D2 keeps generation token-efficient: the model describes nodes, edges, groups, and labels instead of spending tokens on layout or SVG coordinates. The tool owns the layout, theme, spacing, and fonts, so the same compact source produces consistent Unicode, SVG, and PNG output.
- Node.js 22 or newer
- The D2 CLI, version 0.8.0 or newer, on
PATHor named byD2_BIN
brew install d2 # ships 0.8.1
go install github.com/d2lang/d2@v0.8.1 # or a pinned build from sourceThe prebuilt binaries on D2's GitHub releases page stop at 0.7.1, which draws SQL tables as empty boxes, so they are below the supported floor.
D2 is an external dependency on purpose. The extension never downloads a renderer during a tool call. Without it, the extension still loads and a call explains how to install it.
Pi:
pi install npm:@mcuste/pi-diagramOh My Pi:
omp plugin install @mcuste/pi-diagramOr through the Oh My Pi marketplace:
/marketplace add mcuste/pi-diagram
/marketplace install pi-diagram@pi-diagram
From a local checkout:
pnpm install
pi install /absolute/path/to/pi-diagram
omp plugin link /absolute/path/to/pi-diagramOne tool named diagram. Give it D2 source and it draws the diagram in the terminal:
edge: Edge { gateway }
core: Core { api; worker }
edge.gateway -> core.api
core.api -> core.worker: enqueue┌───────────────────┐
│ Edge │
│ │
│ ┌──────────┐ │
│ │ gateway │ │
│ └──────────┘ │
│ │ │
└──────────│────────┘
│
┌─────────│─────────┐
│ Core │
│ ▼ │
│ ┌──────┐ │
│ │ api │ │
│ └──────┘ │
│ │ │
│ enqueue │
│ ▼ │
│ ┌─────────┐ │
│ │ worker │ │
│ └─────────┘ │
└───────────────────┘
Containers, sequence diagrams, SQL tables, class shapes, and state flows all work from the same language.
| Field | Purpose |
|---|---|
source |
The diagram, in D2 |
title |
Label shown above the diagram |
render |
auto prepares Unicode and PNG; image, unicode, and source choose a view |
profile |
What the diagram is for. It sets the layout engine, theme, and spacing |
formats |
Files to produce: source, svg, png, txt. Written outside the repository |
save |
Also copy them into the repository. dir is required |
A tool description alone gets called when the user asks for a diagram, not whenever a diagram is the clearer answer. To make visual explanation active, the extension adds a short block to the host system prompt: draw when structure, flow, or relationships are easier to see than to read, call the tool before explaining, keep the prose to what the picture does not show, and never hand-write ASCII art or Mermaid. The block tells the model to check C4, sequence, class, data, dependency, or tree before drawing a generic flow. The tool description contains the detailed profile and shape rules.
The result is fewer walls of text: an answer about how parts connect arrives as a picture with a
few lines around it. The block is appended to whatever the host built, is added once, and is left
out when the diagram tool is not active.
The model says what a diagram is for, and the tool decides how it looks:
| Profile | For | Drawn as |
|---|---|---|
explain |
States, decisions, transformations, or process flow inside an answer | Hand drawn, neutral theme, small margins |
architecture |
Runtime services and infrastructure without C4 levels | Neutral theme, more room between rows so edges stay separable |
data |
Stored records and relations, or types and public APIs | Neutral theme, tight spacing: tables and classes are tall already |
docs |
A saved diagram when no more specific profile fits | Grey theme that prints in greyscale, page-sized margins |
tree |
Folders, call trees, or other parent-child structures | Dagre, which fans children out under their parent |
c4 |
System scope, users, external systems, runnable units, or module responsibilities | Architecture spacing under the C4 palette |
dependency |
Imports, packages, build order, coupling, impact, or cycles | The tightest spacing, cutting the room edges take past nodes |
explain is the default and is drawn by hand: an answer in a conversation is a rough model, and a
crisp diagram claims more precision than it has. Every other profile is crisp.
Every profile also sets a dark theme, so a saved SVG adapts to dark mode.
Only the picture changes. D2 draws text in character cells, so every profile gives the same box drawing.
Every call prepares Unicode, SVG, and PNG from the same D2 source.
Pi shows Unicode in its live tool row. Press Ctrl+O to replace it with the PNG and press it again
to return to Unicode. If the terminal or harness has no image protocol, expansion keeps the Unicode
view and reports This terminal cannot display inline images. A multiplexer between the terminal
and agent must forward the protocol. tmux needs allow-passthrough, and herdr needs
experimental.kitty_graphics.
OMP keeps complete Unicode in the chronological tool result because settled transcript rows are
immutable. Ctrl+O opens the latest diagram's PNG in a viewport-fitted fullscreen overlay. Press
Ctrl+O or Esc to close it and return to the transcript. The Open PNG link remains available
independently.
OMP owns the shared terminal image budget. Closing an overlay does not purge unrelated terminal images; the host evicts cached graphics under its normal budget policy.
Per-call overrides remain available when the user requests another method:
render: "image"shows a compact inline PNG. Pi can zoom it withCtrl+O.render: "unicode"keeps Unicode in both Pi views.render: "source"shows the D2 source.
The PNG never enters the model's context. It is written to a private temporary directory and read only by the display renderer.
Where the terminal supports OSC 8 hyperlinks, OMP shows the linked PNG file name below the Unicode tool result. Pi links the title above the PNG, or the file name below an untitled PNG. Open that link to pan or zoom beyond the terminal view. Ghostty, Kitty, WezTerm, and iTerm2 support these links, some of them on a modified click.
D2 exports PNG by driving a headless browser it downloads on first use, which this tool will not do during a call. Instead the SVG is rasterized locally by resvg, which needs no browser and no network. Labels use the fonts in the SVG. Characters those fonts do not cover, such as CJK, fall back to fonts on the machine and produce a note.
In a terminal this extension draws the row, so the model reads one line: Drew "Request path". It is on the user's screen, so it is not repeated here. Saved paths and generation notes come with
it, so a conversation full of diagrams costs about what a conversation of text costs. Print, RPC,
and JSON modes have no row to draw, so there the Unicode diagram is the result text.
Expanding a result replaces Unicode with PNG where supported and adds the display mode, profile, D2 version, file paths, diagnostics, and source.
Most diagrams explain something in passing and should leave nothing behind. So nothing is written unless it is asked for, and the repository is never the default:
| Call | Files | Approval |
|---|---|---|
{ source } |
none, the diagram is only in the transcript | read |
{ source, formats: ["svg"] } |
an SVG in a private temp directory, path returned | read |
{ source, title, save: { dir: "docs/diagrams" } } |
editable D2 plus an embeddable SVG | write, prompts with the exact files |
save.dir has no default. There is no directory convention that holds across repositories, so
the destination has to be named. Only pass save when the user asked to keep the diagram. A
saved .d2 is put through d2 fmt first, because people edit it later.
A saved pair is editable source plus a viewable rendering:
docs/diagrams/request-lifecycle.d2
docs/diagrams/request-lifecycle.svg
Markdown then needs no D2 or Mermaid runtime:
For documentation prefer svg: it scales, and Markdown renders it anywhere. png exists for
places where SVG support is weak.
Repository paths stay inside the workspace. Absolute paths, .., and symlinks pointing outside
are all refused.
What D2 drew is also kept in a temporary store, so the same diagram drawn again is about ten times faster, including in a later session. A new D2 version, or any change to a profile, is a different entry. The store holds 64 MB for a week.
Layout engine, theme, padding, and font are deliberately not in the schema. They are policy here, and a model given those knobs spends tokens on styling and produces a different look every call.
- Model-drawn images. The model never emits SVG coordinates or ASCII art directly. It writes source, and a renderer lays it out.
- Imports and external assets. Diagram source cannot pull in files, local images, or remote icons. Those turn a rendering request into file and network access.
- Interactive editing. The tool renders; it is not a diagram editor in the terminal.
- Every D2 feature. A safe subset is supported, and anything outside it is reported as an error the model can correct rather than being silently dropped.
