pi-diff
A pi extension that replaces the default write and edit tool output with Shiki-powered, syntax-highlighted diffs — side-by-side split view, unified stacked view, and word-level change emphasis, all rendered directly in your terminal.
Status: Early release.
Unified view — stacked single-column diff

Split view — side-by-side comparison

Features
- Syntax-highlighted diffs — full Shiki grammar highlighting (190+ languages) composited with diff background colors
- Split view — side-by-side comparison for
edittool, auto-falls back to unified on narrow terminals - Unified view — stacked single-column layout for
writetool overwrites - Word-level emphasis — changed characters get brighter backgrounds so you see exactly what changed
- New file preview — syntax-highlighted preview when creating files
- Adaptive layout — auto-detects terminal width; wraps intelligently on wide terminals, truncates on narrow ones
- LRU cache — singleton Shiki highlighter with 192-entry cache for fast re-renders
- Large diff fallback — gracefully degrades (skips highlighting, still shows diff structure) for files > 80k chars
- Fully customizable — every color and threshold is overridable via environment variables
- Strict edit safety — delegates matching, uniqueness, overlap checks, mutation queues, aborts, BOM, and EOL preservation to Pi's SDK edit tool
Install
pi install npm:@heyhuynhgiabuu/pi-diff
Latest release: https://github.com/buddingnewinsights/pi-diff/releases/latest
Or load directly for development:
pi -e ./src/index.ts
How It Works
pi-diff wraps the built-in write and edit tools from the pi SDK, including single-edit and multi-edit edit calls. When the agent writes or edits a file:
- Before the write — reads the existing file content
- Delegates to the original SDK tool (file is actually written)
- After the write — computes a structured diff between old and new content
- Renders the diff with syntax highlighting and word-level emphasis
For edit calls, Pi's SDK performs matching, uniqueness and overlap validation, mutation queueing, and the file write. pi-diff adapts the SDK's returned unified patch into its syntax-highlighted renderer, so the preview reflects the actual matched source rather than the requested text.
The rendering pipeline:
Old content ──┐
├── diff (structuredPatch) ── parse ── highlight (Shiki → ANSI)
New content ──┘ │
├── inject diff bg
├── inject word-level bg
└── wrap/fit to terminal
Views
| View | Used by | Description |
|---|---|---|
| Split | edit tool |
Side-by-side with old on left, new on right. Diagonal stripes fill empty slots. Auto-falls back to unified when terminal < 150 cols or > 20% of lines would wrap. |
| Unified | write tool |
Single column with +/- gutter. Compact, works at any terminal width. |
Both views show:
- Colored border bars (
▌) for changed lines - Line numbers in the gutter
- Hunk separators (
··· N unmodified lines ···) - Word-level emphasis on paired add/del lines
Configuration
Disable pi-diff tools
Add disabledTools to pi-diff.json to omit pi-diff tools. Disabling write or edit leaves Pi's built-in tool available; disabling apply_patch removes that custom tool.
{
"disabledTools": ["apply_patch"]
}
See CONFIG.md for the full pi-diff.json reference.
Diff Theme Presets
pi-diff ships with built-in theme presets optimized for different terminal backgrounds. Add to your .pi/settings.json:
{
"theme": "dark",
"diffTheme": "midnight"
}
| Preset | Best for | Description |
|---|---|---|
default |
Dark theme bases (~`#1e1e2e`) | Original pi-diff colors — balanced contrast |
midnight |
Pure black (#000000) terminals |
Subtle tints that don't overwhelm on black |
subtle |
Any dark theme | Minimal backgrounds — barely-there tints for a clean look |
neon |
Low-contrast displays | Higher contrast backgrounds for better visibility |
Per-Color Overrides
Override individual diff colors in .pi/settings.json using hex #RRGGBB values:
{
"theme": "dark",
"diffTheme": "midnight",
"diffColors": {
"bgAdd": "#0d1a12",
"bgDel": "#1a0d0d",
"bgAddHighlight": "#1a3825",
"bgDelHighlight": "#381a1a",
"bgGutterAdd": "#091208",
"bgGutterDel": "#120908",
"bgEmpty": "#080808",
"fgAdd": "#64b478",
"fgDel": "#c86464",
"fgDim": "#404040",
"fgLnum": "#505050",
"fgRule": "#282828",
"fgStripe": "#1e1e1e",
"fgSafeMuted": "#8b949e",
"shikiTheme": "github-dark"
}
}
diffColors overrides take priority over diffTheme presets, so you can start from a preset and tweak individual colors.
Auto-Derive (Default Behavior)
When no diffTheme or diffColors is set, pi-diff automatically derives background colors from your pi theme's diff foreground colors and tool-state backgrounds. Added/context surfaces use toolSuccessBg; removed surfaces use toolErrorBg. This ensures diffs look good with any pi theme and terminal background — no configuration needed.
The auto-derive uses different intensity levels:
- Line backgrounds: 8–10% of the theme's diff fg color mixed into the matching tool-state background (subtle tint)
- Word highlights: 20–22% (more visible for changed characters)
- Gutters: 5–6% (subtler than line backgrounds)
Color Resolution Order
For each color, pi-diff checks (highest priority first):
- Environment variable — e.g.
DIFF_BG_ADD="#1a3320"(backward compatible) diffColorsfrom.pi/settings.json(per-color hex overrides)diffThemepreset from.pi/settings.json(named preset bundle)- Auto-derived from pi theme's
toolDiffAdded/toolDiffRemovedcolors - Hardcoded fallback (original defaults)
Environment Variables
All settings are also controllable via environment variables. Add them to your shell profile or .envrc:
Theme
| Variable | Default | Description |
|---|---|---|
DIFF_THEME |
github-dark |
Shiki theme name (e.g., dracula, one-dark-pro, catppuccin-mocha) |
Colors
Override any diff color with hex #RRGGBB format:
| Variable | Default | Description |
|---|---|---|
DIFF_BG_ADD |
#162620 |
Background for added lines |
DIFF_BG_DEL |
#2d1919 |
Background for removed lines |
DIFF_BG_ADD_HL |
#234b32 |
Word-level emphasis on added text |
DIFF_BG_DEL_HL |
#502323 |
Word-level emphasis on removed text |
DIFF_BG_GUTTER_ADD |
#12201a |
Gutter background for added lines |
DIFF_BG_GUTTER_DEL |
#261616 |
Gutter background for removed lines |
DIFF_FG_ADD |
#64b478 |
Foreground for + signs and add indicators |
DIFF_FG_DEL |
#c86464 |
Foreground for - signs and del indicators |
Layout
| Variable | Default | Description |
|---|---|---|
DIFF_SPLIT_MIN_WIDTH |
150 |
Minimum terminal columns to use split view |
DIFF_SPLIT_MIN_CODE_WIDTH |
60 |
Minimum code columns per side in split view |
Example .envrc
# Use a different Shiki theme
export DIFF_THEME="catppuccin-mocha"
# Brighter add backgrounds
export DIFF_BG_ADD="#1a3a25"
export DIFF_BG_ADD_HL="#2d6040"
# Allow split view on narrower terminals
export DIFF_SPLIT_MIN_WIDTH=120
Architecture
src/
├── index.ts # Extension entry point — wraps write/edit tools with diff rendering
├── core/ # Pure data layer: diff parsing, conflict detection, line resolution
│ ├── config.ts
│ ├── conflicts.ts
│ ├── diff.ts # parseDiff, parsePatchFiles, resolveSepStyle
│ ├── replace.ts # conservative apply_patch matcher and legacy helpers
│ ├── resolve-lines.ts
│ └── *.test.ts
└── review/ # Shared diff-rendering primitives used by the main extension
├── git.ts # read git diffs from disk (execFileSync)
└── hunk-preview.ts # renderSplit, renderUnified, theme helpers (re-exported via __testing)
Key internals
All listed symbols are exposed under the __testing export for unit tests:
| Symbol | Source | Purpose |
|---|---|---|
parseDiff() |
core/diff.ts |
Convert old/new content to structured DiffLine[] (added/removed/chars) |
parsePatchFiles() |
core/diff.ts |
Parse a full git apply patch into per-file diff objects |
resolveSepStyle() |
core/diff.ts |
Pick the per-side separator style (│ vs ╱) for split view |
getSepStyle() |
review/hunk-preview.ts |
Same as above for the shared rendering layer |
computeHunkBlocks() |
review/hunk-preview.ts |
Build add/remove/modify line blocks with column ranges for split view |
renderSplit() |
review/hunk-preview.ts |
Side-by-side renderer with diagonal stripe fillers |
renderUnified() |
review/hunk-preview.ts |
Stacked single-column renderer |
normalizeShikiContrast() |
review/hunk-preview.ts |
Lighten/darken Shiki output to match the surrounding TUI theme |
Rendering constants
| Constant | Value | Description |
|---|---|---|
MAX_PREVIEW_LINES |
60 | Max lines in edit preview (split view) |
MAX_RENDER_LINES |
150 | Max lines in write result (unified view) |
MAX_HL_CHARS |
80,000 | Skip syntax highlighting above this |
CACHE_LIMIT |
192 | LRU cache entries for highlighted blocks |
WORD_DIFF_MIN_SIM |
0.15 | Minimum similarity for word-level emphasis |
Exports
The extension exports a __testing object for unit testing:
import { __testing } from "@heyhuynhgiabuu/pi-diff";
const { parseDiff, renderSplit, renderUnified, normalizeShikiContrast } =
__testing;
Development
git clone https://github.com/buddingnewinsights/pi-diff.git
cd pi-diff
npm install
npm run typecheck # TypeScript validation
npm run lint # Biome linting
npm test # Run tests
Load in pi for testing
# From the pi-diff directory
pi -e ./src/index.ts
# Or install globally
pi install .
How pi Extensions Work
pi-diff is a pi extension — a TypeScript file that exports a default function receiving the pi API:
export default function piDiffExtension(pi: ExtensionAPI): void {
// Wrap the built-in write/edit tools with diff rendering
const origWrite = createWriteTool(cwd);
const origEdit = createEditTool(cwd);
pi.registerTool({
...origWrite,
name: "write",
execute: async (...) => { /* wrap + diff */ },
renderCall: (...) => { /* preview */ },
renderResult: (...) => { /* render diff */ },
});
}
Extensions can:
- Register tools —
pi.registerTool(definition) - Listen to events —
pi.on("session_start" | "input" | "tool_call" | "session_shutdown" | ...)tool_callhandlers canblock: trueto prevent the call from running
- Register commands —
pi.registerCommand("/name", handler)
See the pi docs for the full extension API.
Design notes
How hashline (hashline_read, hashline_edit, edit hashlineChanges) relates to Pi’s stock edit tool (exact edits[], bounded fuzzy match, loud failures) is documented in .pi/artifacts/DECISIONS.md (ADR-001). Summary: stock edit stays the portable default; hashline is an opt-in strict anchor path for agents that adopt it.
License
MIT — huynhgiabuu