@pierre/trees + @pierre/diffs integration
SkillFiles & storageUse Pierre Computer Company's `@pierre/trees` (path-first file tree) and `@pierre/diffs` (shiki-based code/diff renderer) in Kolu. Pierre ships Preact/vanilla cores with optional React wrappers — Kolu consumes the vanilla classes and wraps them in thin SolidJS components. Trigger when: wiring up a file tree, rendering unified diffs or syntax-highlighted files, replacing `@git-diff-view`, or any mention of `@pierre/trees` / `@pierre/diffs` / "pierre library".
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the @pierre/trees + @pierre/diffs integration skill
What this skill tells your AI
The instructions your AI receives, as published by juspay/kolu in .apm/skills/pierre/SKILL.md and read by ahel’s review.
Kolu uses two Pierre packages for code-review surfaces (CodeTab):
@pierre/trees— virtualized, path-first file-tree UI with search, git status, drag-and-drop, context menus, icons, themes.@pierre/diffs— shiki-backed unified/split diff renderer with annotations, line selection, virtualization, custom hunk separators. Since 1.2.x, the top-levelCodeViewclass hosts one or more file/diff items in a single virtualized scroll viewport (advanced mode) and supersedes the per-fileFile/FileDiff/Virtualizertrio for kolu's consumer code.
Both publish a vanilla class API (Preact-rendered internally) plus optional React wrappers. Kolu consumes the vanilla core from SolidJS — no React.
- Source for study:
git clone https://github.com/pierrecomputer/pierre /tmp/pierre - npm:
@pierre/trees@1.0.0-beta.4,@pierre/diffs@1.2.1
Why Pierre over hand-rolled
Pierre's libraries encapsulate three things Kolu used to own:
- Tree layout and virtualization (
buildFileTree.ts,FileTree.tsx— removed). Pierre handles sort, collapse, sticky folders, keyboard nav, virtualization, search, and git status in one pass. - Diff parsing and rendering (
@git-diff-view/solid— removed). Pierre parses raw unified-diff strings withparsePatchFiles()and renders with syntax highlighting via Shiki. - Theming. Pierre reads Shiki themes directly and exposes CSS variables for
host-page overrides (
--trees-*,--diffs-*).
SolidJS wrapping pattern
The vanilla classes own their DOM. SolidJS wrappers are thin — they only mount, update options reactively, and clean up. No re-render loop, no framework-internal state.
FileTree wrapper (shape)
// packages/solid-pierre/src/FileTree.tsx (sketch)
import { FileTree, type GitStatusEntry } from "@pierre/trees";
import { createEffect, onCleanup, on } from "solid-js";
export type FileTreeProps = {
paths: string[]; // canonical repo-relative
gitStatus?: GitStatusEntry[];
selectedPath?: string | null;
onSelect?: (path: string) => void;
};
export const FileTree: Component<FileTreeProps> = (props) => {
let container!: HTMLDivElement;
let tree: FileTreeClass | undefined;
// Mount once — class owns its DOM. Don't recreate on prop changes.
queueMicrotask(() => {
tree = new FileTreeClass({
paths: props.paths,
initialExpansion: "open",
search: true,
gitStatus: props.gitStatus,
onSelectionChange: (paths) => props.onSelect?.(paths[0] ?? ""),
});
tree.render({ containerWrapper: container });
});
// Reactively push updates via setters — `resetPaths`, `setGitStatus` patch
// in place without rerenders.
createEffect(
on(
() => props.paths,
(paths) => tree?.resetPaths(paths),
{ defer: true },
),
);
createEffect(
on(
() => props.gitStatus,
(g) => tree?.setGitStatus(g),
{ defer: true },
),
);
onCleanup(() => tree?.cleanUp());
return <div ref={container!} class="h-full" />;
};
Key points:
- Mount in
queueMicrotask(oronMount) — the container div must be in the DOM beforerender(). - Pass callbacks through
props.onSelect?.()— don't capture at mount time, the prop ref may change. Pierre calls back through the current closure. - Use setters for updates (
resetPaths,setGitStatus,setIcons,setComposition) — never reconstructFileTreeon prop change. defer: trueon the effects so the initial mount doesn't fire them.onCleanup(() => tree?.cleanUp())is mandatory — leaks the shadow root otherwise.
Git status mapping
Kolu's GitChangeStatus is a single letter (M / A / D / R / C / U / T / ?).
Pierre's GitStatus is a word (modified, added, deleted, renamed,
untracked, ignored). Map at the call site:
const MAP: Record<GitChangeStatus, GitStatus> = {
M: "modified",
A: "added",
D: "deleted",
R: "renamed",
C: "renamed",
U: "modified",
T: "modified",
"?": "untracked",
};
No hand-built tree needed. Pass the flat files.map(f => f.path) directly to
paths and files.map(f => ({ path: f.path, status: MAP[f.status] })) to
gitStatus. Pierre handles hierarchy, collapse-chains, and sort.
File-browser (lazy load) mode
@pierre/trees expects the full path list up front. For a lazy "browse the
whole repo" mode, there are two options:
- Eagerly list all paths once — simpler, fine up to ~50k files. Use the
server's
fs.listDirrecursively or add anfs.listAllendpoint. - Synthesize paths on demand and call
tree.add(path)/tree.batch([...])as directories expand. Subscribe to the tree's expand events via the controller; requires deeper API reading.
The prototype replacement started with (1) — simpler, matches Pierre's path-first model.
CodeView wrapper (shape)
Since @pierre/diffs@1.2, the top-level CodeView class is the rendering
substrate for both files and diffs. Kolu's @kolu/solid-pierre exposes a
single <CodeView> Solid wrapper around it; the older per-class
<FileView> / <FileDiff> / <Virtualizer> trio is retired.
// packages/solid-pierre/src/CodeView.tsx (sketch)
import {
CodeView as CodeViewClass,
type CodeViewItem,
type CodeViewLineSelection,
type CodeViewOptions,
DEFAULT_THEMES,
} from "@pierre/diffs";
export type CodeViewProps = {
items: readonly CodeViewItem[];
theme: "light" | "dark";
diffStyle?: "unified" | "split";
overflow?: "scroll" | "wrap";
enableLineSelection?: boolean;
selectedLines?: CodeViewLineSelection | null;
onSelectedLinesChange?: (selection: CodeViewLineSelection | null) => void;
onError: (err: Error) => void;
class?: string;
style?: JSX.CSSProperties;
};
export const CodeView: Component<CodeViewProps> = (props) => {
let root!: HTMLDivElement;
let instance: CodeViewClass | undefined;
const buildOptions = (): CodeViewOptions<undefined> => ({
theme: DEFAULT_THEMES,
themeType: props.theme,
diffStyle: props.diffStyle ?? "unified",
overflow: props.overflow ?? "wrap",
enableLineSelection: props.enableLineSelection ?? false,
onSelectedLinesChange: (s) => props.onSelectedLinesChange?.(s),
});
onMount(() => {
instance = new CodeViewClass(buildOptions());
instance.setup(root); // root IS the scroll container
instance.setItems(props.items);
});
createEffect(on(() => props.items, (items) => instance?.setItems(items), { defer: true }));
createEffect(on(() => props.theme, () => instance?.setOptions(buildOptions()), { defer: true }));
onCleanup(() => instance?.cleanUp());
return <div ref={root} class={props.class} style={props.style} />;
};
Key points:
- The root
<div>IS Pierre's scroll container.setup(root)registers it as the scroller. Putoverflow-autoand sizing on it directly; do not nest another scroller inside. itemsis typed. Pass{ id, type: "file", file: { name, contents } }for a file or{ id, type: "diff", fileDiff }for a diff. Get theFileDiffMetadataviaparsePatchFiles(raw)[0]?.files[0]from the call site —parsePatchFilesis the top-level Pierre API for raw unified-diff strings.- Single-file case is just
items={[oneItem]}. Pierre virtualizes unconditionally; there is no vanilla branch to opt into. - Selection is item-scoped.
CodeViewLineSelectionis{ id, range }— theidmust match one of the items currently in the viewport. When the rendered item changes (path swap), updateselectedLinesto either point at the new id or null. - Version tracking. Pierre's
reconcileItemskeeps the previous record when an item'sversionis unchanged — passing a freshfileDiff/filefor the sameidwithout bumpingversionleaves stale content on screen. The wrapper diffs incoming items by reference and bumpsversioninternally so callers don't deal with the field. If you build your own wrapper, replicate that bump. - Advanced mode bypasses legacy bugs.
VirtualizedFile.setVisibilityandVirtualizedFileDiff.setVisibilityboth early-return in advanced mode (isAdvancedMode()check), so thesetVisibilityupstream-bug workaround the old wrappers carried is no longer needed underCodeView.
Peer dependencies
Both packages declare react, react-dom as peer dependencies. They're
only needed for the ./react entry points. The vanilla core bundles preact +
preact-render-to-string as regular deps, so no React install is required
when consuming @pierre/trees / @pierre/diffs directly from SolidJS.
Suppress peer-dep warnings in pnpm-workspace.yaml or .npmrc:
# pnpm-workspace.yaml
packageExtensions:
"@pierre/trees@*":
peerDependenciesMeta:
react: { optional: true }
react-dom: { optional: true }
"@pierre/diffs@*":
peerDependenciesMeta:
react: { optional: true }
react-dom: { optional: true }
Theming hookup
Trees reads CSS variables; expose kolu's theme tokens by setting them on the host element's inline style (or a wrapper class):
.pierre-trees-host {
--trees-fg-override: theme(colors.fg);
--trees-selected-bg-override: theme(colors.surface.2);
--trees-border-color-override: theme(colors.edge);
}
For diffs, prefer DEFAULT_THEMES (pierre-dark/pierre-light) initially. Move
to Kolu-branded Shiki themes later via registerCustomTheme() if needed.
What to port, what to keep
Removed in earlier passes (no longer in the tree):
packages/client/src/ui/FileTree.tsxpackages/client/src/ui/buildFileTree.tspackages/client/src/ui/buildFileTree.test.ts@git-diff-view/soliddephighlight.jsdep — the file browser's content viewer now runs through@pierre/diffs'sCodeView(file item), same shiki pipeline.
Removed when CodeView landed:
packages/solid-pierre/src/Virtualizer.tsx— context-based dispatcher for vanilla vs. virtualized; advanced mode makes virtualization unconditional.packages/solid-pierre/src/FileView.tsx— single-file viewer wrapper. The rAF + line-height-estimatescrollToLinefallback and thesetVisibilityupstream-bug patch retired with it.packages/solid-pierre/src/FileDiff.tsx— single-diff viewer wrapper.
Keep (not replaced by pierre):
packages/common/src/contract.ts—GitDiffOutputSchemastill carries the raw unified diff. Consumers nowparsePatchFiles()it instead of handing parsed hunks to@git-diff-view.- Sub-tab state (
local/branch/browse) inuseRightPanel— pierre doesn't know about Kolu's diff modes.
Gotchas
- Shadow DOM: both libs render into a shadow root for CSS isolation.
Tailwind classes on children won't pierce in. Style via CSS variables or
unsafeCSSoption, not Tailwind utilities inside the tree rows.CodeViewcreates one<diffs-container>shadow root per item — selection-walkers must descend recursively (seepackages/client/src/comments/shadowWalk.ts). - Path-first identity: pierre's public API is keyed on path strings. Do
not store or compare internal numeric IDs.
CodeViewItem.idis also path-keyed by convention. - Async load: pierre's diff renderer loads shiki WASM lazily. First render
of a new language is async; the
render()call returns immediately and the rows paint in a later frame. Do not race cleanup. setGitStatus([])clears statuses — passundefinedto leave alone.resetPathsdiscards expansion state unless you passinitialExpandedPathsin the reset options.setItemsreconciles by id; same-id same-version is treated as no-change. Bumpversionwhen content swaps for an existing id.- Nix
fetchPnpmDepshash must be regenerated after adding new deps; see thenix-typescriptskill.
Development tips
- Pierre's benchmarks live in
/tmp/pierre/packages/trees/scripts/and/tmp/pierre/packages/diffs/scripts/— useful to understand expected usage at scale. - Pierre's own demo:
cd /tmp/pierre && bun install && bun run demo:dev(but their demo is Preact-based; reference only). - React wrappers in
/tmp/pierre/packages/diffs/src/react/are the clearest reference for "what the intended consumer does" — especiallyreact/CodeView.tsx, which mirrors the SolidJS wrapper's prop shape.
Signals
- GitHub stars
- 78
- Forks
- 12
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
pierre- Source
- github.com/juspay/kolu