npm.io
0.8.1 • Published 6d ago

@heyhuynhgiabuu/pi-diff

Licence
MIT
Version
0.8.1
Deps
5
Size
955 kB
Vulns
1
Weekly
492
Stars
41

pi-diff

npm version GitHub release

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

pi-diff unified view

Split view — side-by-side comparison

pi-diff split view

Features

  • Syntax-highlighted diffs — full Shiki grammar highlighting (190+ languages) composited with diff background colors
  • Split view — side-by-side comparison for edit tool, auto-falls back to unified on narrow terminals
  • Unified view — stacked single-column layout for write tool 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:

  1. Before the write — reads the existing file content
  2. Delegates to the original SDK tool (file is actually written)
  3. After the write — computes a structured diff between old and new content
  4. 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):

  1. Environment variable — e.g. DIFF_BG_ADD="#1a3320" (backward compatible)
  2. diffColors from .pi/settings.json (per-color hex overrides)
  3. diffTheme preset from .pi/settings.json (named preset bundle)
  4. Auto-derived from pi theme's toolDiffAdded/toolDiffRemoved colors
  5. 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 toolspi.registerTool(definition)
  • Listen to eventspi.on("session_start" | "input" | "tool_call" | "session_shutdown" | ...)
    • tool_call handlers can block: true to prevent the call from running
  • Register commandspi.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

Keywords