Diff View
A file or a diff rendered by @pierre/diffs — virtualized, syntax highlighted, split or unified — bound to the app's own tokens.
Installation
npx shadcn@latest add miskibin/chat-components/diff-viewAlso installs diff-worker-pool.
Usage
"use client"
import { DiffView } from "@/components/ui/diff-view"
export function Review({ file }: { file: ChangedFile }) {
// The viewer is its own scroll container, so give it a height.
return (
<div className="h-full min-h-0">
<DiffView
path={file.path}
oldText={file.before}
newText={file.after}
mode="split"
wrap={false}
/>
</div>
)
}Examples
Comment on the lines you picked
onLineComment is what turns a viewer into a review surface: the reader drags a range in the gutter, and the host gets the lines and their text. A range dragged across the two columns of a split diff belongs to the side it ended on, so the numbers and the quote always agree.
Drag down the gutter to pick lines — the affordance appears over the selection, and what it hands back carries the text of those lines, so the quote survives leaving the viewer.
Four thousand lines, sixteen changed
Only the rows on screen exist, so the size of the file stops mattering. DiffWorkerPoolProvider moves the highlighting off the main thread as well — mount it once, high in the app; the pool is shared, reference counted, and kept warm for 30s after the last panel closes. Without it a DiffView highlights inline instead, which is slower but never broken.
A whole review, in one scroller
DiffView answers “show me this file”; DiffStack answers “show me what changed”, which is a different question with a different shape. A column of separate viewers would be a column of separate scrollers — the reader lands in one, scrolls it to its end, and stops, with the rest of the review below a boundary the wheel refuses to cross. So this is one viewer holding one item per file: @pierre/diffs virtualizes them together against a single scroll container, which is both the only way the wheel behaves and the reason a hundred-file review costs what one screen costs. File headers are on, because in a stack the filename is the only thing saying which file a hunk belongs to.
Install the pool separately
The provider ships as its own registry item so it can be mounted at the app root, far from any panel.
npx shadcn@latest add miskibin/chat-components/diff-worker-poolNotes
API reference
DiffStack
files*DiffStackFile[]- One entry per file — the same inputs DiffView takes (patch, or oldText/newText, or content), keyed by path. A file with nothing to show drops out rather than rendering an empty frame.
focusPathstring- Scrolls that file to the top of the viewport. Pair it with focusNonce to ask for the same file twice — a file map's rows are exactly that.
focusNoncenumber- Makes a repeat request for the same path a new request.
mode"unified" | "split" = "unified"- Applies to every file in the stack.
wrapboolean = true- Soft-wrap long lines; off, each file scrolls sideways.
emptyLabelReactNode- Shown when there is nothing to draw — a clean worktree, or a change that is all binary.
DiffView
path*string- Names the file, and picks the language when none is given.
patchstring- Unified diff text. Outranks
oldText/newText. A patch that will not parse is shown as its own text with a note above it, rather than as a blank panel — a reader would rather see the patch. oldText / newTextstring- Before and after. The diff is computed for you, in the worker when there is a pool.
contentstring- No diff at all: the file itself, highlighted and virtualized the same way.
startLinenumber = 1- First line of
content, for a partial read. The excerpt is rendered as its own hunk so the gutter keeps the real numbers, with an “N unmodified lines” marker above it. mode"unified" | "split" = "unified"- Side by side or one column. Ignored without a diff.
wrapboolean = true- Soft-wrap long lines; off, the code scrolls sideways instead.
lineNumbersboolean = true- The gutter.
fileHeaderboolean = false- Pierre's own per-file header, with the name and the +/- counts. Off, because the host usually has a header of its own.
highlightLinesIterable<number>- Lines to tint, in the file's own numbering — the lines an agent touched, a search's hits. Emitted as one rule per line into the viewer's stylesheet, so it lands on a virtualized row the moment it mounts; past 400 lines the tint is dropped rather than costing more than it says.
focusLine / focusNoncenumber- Centre a line, once per request. Bump
focusNonceto ask for the same line again — clicking onefile.ts:42chip twice is two requests, and only the host can tell them apart. onLineComment(range: DiffLineCommentRange) => void- Turns on line selection. Drag the gutter and a
Comment on linesbutton appears over the selection; clicking it hands back{ path, startLine, endLine, side?, excerpt }— the numbers in the numbering of the side they were picked on, and the text of those lines, read back out of the patch so the quote survives leaving the viewer. themeDiffsThemeNames | ThemesType = { light: "pierre-light", dark: "pierre-dark" }- Any Shiki theme, or a light/dark pair. The backgrounds, gutter and row tints are overridden with your own tokens either way — only the token colours come from here.
unsafeCSSstring- Appended inside the viewer's shadow root, after the token bindings. The escape hatch for chrome the viewer owns.
optionsDiffViewOptions- Everything
DiffViewdoes not own, forwarded toCodeView. The options it does own are removed from the type, so passing one fails to compile instead of being silently overwritten.
DiffWorkerPoolProvider
poolSizenumber = clamp(cores / 2, 2, 6)- Workers to spawn — enough to keep a scrolling file list highlighted without starving the app.
workerFactory() => Worker- Replaces how a worker is constructed, for a bundler that cannot follow the built-in
new URL("@pierre/diffs/worker/worker.js", import.meta.url). fallbackReact.ReactNode- Shown while the workers start. Defaults to a quiet status line.
Data slots
Every part below is addressable from a parent, so you can restyle internals without forking the component.
diff-viewdiff-view-surfacediff-view-line-commentdiff-view-notediff-view-emptydiff-stackdiff-stack-surfacediff-stack-emptydiff-worker-pool-fallback