@mcuste/pi-diagram

Terminal-native diagram rendering as one safe, model-callable tool for the Pi and Oh My Pi coding agents

Packages

Package details

extension

Install @mcuste/pi-diagram from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@mcuste/pi-diagram
Package
@mcuste/pi-diagram
Version
0.3.0
Published
Aug 22, 2026
Downloads
198/mo · 198/wk
Author
mcuste
License
MIT
Types
extension
Size
466.1 KB
Dependencies
2 dependencies · 2 peers
Pi manifest JSON
{
  "extensions": [
    "./src/index.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

pi-diagram

CI

An extension for the Pi and Oh My Pi terminal coding agents. It gives the model one diagram tool that turns declarative D2 source into a rendered diagram in the transcript, or into documentation artifacts checked into the repository.

If any of those names are new to you:

  • D2 is a declarative diagram language. Text such as client -> gateway -> api compiles 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.

Status: images, text, and files work. Unicode box drawing is the default. Users can opt in to inline images where the terminal supports them. Files land outside the repository unless a destination is named.

Why

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.

This extension gives the model one tool for that. The model writes the meaning: nodes, edges, groups, labels, emphasis. The tool owns the appearance: layout engine, theme, spacing, fonts, and which representation the current terminal can actually display. Diagrams then look consistent across calls, because the model is not styling them one at a time.

Requirements

  • Node.js 22 or newer
  • The D2 CLI, version 0.8.0 or newer, on PATH or named by D2_BIN
brew install d2                          # ships 0.8.1
go install github.com/d2lang/d2@v0.8.1   # or a pinned build from source

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

Install

Pi:

pi install npm:@mcuste/pi-diagram

Oh My Pi:

omp plugin install @mcuste/pi-diagram

Or 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-diagram

What the tool does

One 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 uses the user preference; image and unicode override it; source echoes the D2
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

When the model draws

A described tool gets called when the user asks for a diagram, not when a diagram is the clearer answer. So 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.

What a profile changes

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.

Images in the terminal

Unicode is the default. To persist image rendering for one repository, create .pi/pi-diagram.json for Pi or .omp/pi-diagram.json for OMP:

{
  "render": "image"
}

For every repository, use ~/.pi/agent/pi-diagram.json or ~/.omp/agent/pi-diagram.json. PI_CODING_AGENT_DIR replaces those agent directories when set.

For one run, use the host extension flag or environment variable:

pi --diagram-render image
omp --diagram-render image
PI_DIAGRAM_RENDER=image pi

Precedence is command-line flag, PI_DIAGRAM_RENDER, project configuration, global configuration, then the Unicode default. Configuration files are read before each diagram, so changes apply without restarting the host.

On Kitty, Ghostty, WezTerm, iTerm2, and anything else that speaks a terminal image protocol, auto then shows the drawn diagram. At host startup the extension uses the host's TUI library to detect whether the terminal and its harness support an image protocol. If image rendering is disabled or unavailable, the call shows Unicode and reports Image support is unavailable; generated as Unicode. Explicit render: "unicode" never attempts an image.

A terminal with no image protocol is never sent one. Note that a multiplexer between the terminal and the agent has to forward the protocol: tmux needs allow-passthrough, and herdr needs experimental.kitty_graphics.

The image never enters the model's context. It is written to a private temporary directory and read back when the row is displayed.

The collapsed row is a 60 by 18 cell preview. Press Ctrl+O, the default tool-expansion shortcut, to zoom the image to the available terminal width and up to 60 rows. Press it again to return to the preview.

Where the terminal supports OSC 8 hyperlinks, the title above the diagram links to the full image file. A diagram with no title shows the linked file name under the image instead. 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 it already produces is rasterized locally by resvg, which needs no browser and no network. Labels are drawn with the fonts the SVG carries, so the picture matches the boxes D2 measured. Characters those fonts do not cover, such as CJK, fall back to the fonts on the machine and say so.

What the model gets back

In a terminal this extension draws the row, so the model reads one line: Drew "Request path" as an image. It is on the user's screen, so it is not repeated here. Saved paths and 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 diagram is the result text.

Expanding an image row zooms it and adds the render mode, the profile, the D2 version, the file paths, diagnostics, and the source.

Where files go

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" } } copied into the repository 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:

![Request lifecycle](diagrams/request-lifecycle.svg)

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.

What is deliberately missing

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

Documentation

License

MIT