How are diffs and trees libraries integrated in diffshub?

HEAD
aed9bb5[diffshub] Add local :prod command for diffshub (#772)
This post might have stale content, as HEAD is 24 commits ahead.

tell me in diffshub how does the diffs and trees libraries are integrated

Avatar of openai/gpt-5.5
GPT 5.5
commented

In Diffshub, @pierre/diffs and @pierre/trees are not coupled inside the libraries themselves. They are integrated at the app layer in apps/docs/app/(diffshub)/(view)/_components.

The basic shape is:

GitHub patch
   ↓
@pierre/diffs parses patch → FileDiffMetadata[]
   ↓
Diffshub accumulator builds:
   1. CodeViewItem[]        → rendered by @pierre/diffs/react CodeView
   2. tree paths/statuses   → rendered by @pierre/trees/react FileTree
   ↓
File tree selection maps path → CodeView item id → scroll CodeView

##1. The docs/Diffshub app depends on both packages

apps/docs/package.json depends on both:

"@pierre/diffs": "workspace:*",
"@pierre/trees": "workspace:*"

But the packages themselves are independent: packages/diffs/package.json does not depend on @pierre/trees, and packages/trees/package.json does not depend on @pierre/diffs. Diffshub is the place where they meet.

##2. Patch parsing comes from @pierre/diffs

The core integration starts in:

apps/docs/app/(diffshub)/(view)/_components/codeViewDataAccumulator.ts

It imports diff types and parsers from @pierre/diffs:

import {
  type ChangeTypes,
  type CodeViewItem,
  type FileDiffMetadata,
  parsePatchFiles,
} from '@pierre/diffs';

And tree status types from @pierre/trees:

import type { FileTreeGitStatusPatch, GitStatusEntry } from '@pierre/trees';

For full patch loads, buildCodeViewData() calls parsePatchFiles():

const parsedPatches = parsePatchFiles(
  patchContent,
  encodeURIComponent(githubPath)
);

Then every parsed fileDiff is passed into appendFileDiffToCodeViewData().

For streaming loads, usePatchLoader.ts uses processFile() from @pierre/diffs per streamed file:

const fileDiff = processFile(fileText, {
  cacheKey: `${cacheKeyPrefix}-0-${accumulator.fileIndex}`,
  isGitDiff: true,
});

Then that fileDiff is also appended to the same accumulator.

##3. One accumulator builds data for both CodeView and FileTree

The key bridge is appendFileDiffToCodeViewData() in codeViewDataAccumulator.ts.

For each FileDiffMetadata, it creates a CodeViewItem for the diff viewer:

const item: CodeViewItem<CommentMetadata> = {
  id,
  type: 'diff',
  fileDiff,
  version: 0,
};
accumulator.items.push(item);

At the same time, it derives the tree path and tree status:

const path = fileDiff.name;
const treePath = treePathPrefix == null ? path : `${treePathPrefix}/${path}`;

Then:

accumulator.paths.push(treePath);
accumulator.pathToItemId.set(treePath, id);
updateGitStatusByPath(accumulator, treePath, fileDiff.type, ...);

So the same parsed diff file produces:

  • a CodeViewItem for @pierre/diffs/react
  • a path entry for @pierre/trees/react
  • a path → itemId map used to connect tree clicks to diff scrolling
  • git status data for added/deleted/renamed indicators in the tree

##4. Diff change types are mapped to tree git statuses

@pierre/diffs exposes ChangeTypes. The file tree expects GitStatus.

Diffshub maps between them in:

apps/docs/app/(diffshub)/(view)/_components/utils.ts

The comment describes it clearly:

// Translates the diff-level change type surfaced by @pierre/diffs into the
// git-status vocabulary the file tree understands.

Function:

export function mapChangeTypeToGitStatus(type: ChangeTypes): GitStatus {
  switch (type) {
    ...
  }
}

This is used by updateGitStatusByPath() in the accumulator.

##5. CodeView renders the diffs

The diff viewer is rendered in:

apps/docs/app/(diffshub)/(view)/_components/CodeViewWrapper.tsx

It imports CodeView from @pierre/diffs/react:

import {
  CodeView,
  type CodeViewHandle,
  useStableCallback,
} from '@pierre/diffs/react';

Then renders:

<CodeView<CommentMetadata>
  ref={handleViewerRef}
  containerRef={scrollRef}
  initialItems={initialItems}
  options={options}
  selectedLines={selectedLines}
  onSelectedLinesChange={handleSetSelection}
  renderAnnotation={renderCommentAnnotation}
  renderHeaderPrefix={renderHeaderPrefix}
/>

The options passed to CodeView include Diffshub UI controls:

{
  layout: CODE_VIEW_LAYOUT,
  themeType,
  diffStyle,
  diffIndicators,
  overflow,
  disableBackground: !showBackgrounds,
  disableLineNumbers: !lineNumbers,
  enableLineSelection: true,
  enableGutterUtility: true,
  stickyHeaders: true,
  unsafeCSS: CODE_VIEW_CUSTOM_CSS,
}

So @pierre/diffs owns the actual patch/diff rendering, virtualization, selections, annotations, scrolling APIs, etc.

##6. FileTree renders the sidebar

The tree is rendered in:

apps/docs/app/(diffshub)/(view)/_components/CodeViewFileTree.tsx

It imports from @pierre/trees/react:

import { FileTree, useFileTree } from '@pierre/trees/react';

It builds the tree model with useFileTree():

const { model } = useFileTree({
  ...BASE_FILE_TREE_OPTIONS,
  gitStatus: source.gitStatus,
  paths: initialPathsRef.current,
  sort: PRESERVE_INPUT_ORDER_SORT,
  onSelectionChange,
  itemHeight: CODE_VIEW_FILE_TREE_ITEM_HEIGHT,
  initialVisibleRowCount,
});

Then renders:

<FileTree
  className="h-full min-h-0 overflow-auto ..."
  model={model}
  style={themeStyles}
/>

source.paths and source.gitStatus come from the same accumulator that created the CodeView items.

##7. Tree selection scrolls the diff viewer

The important link is pathToItemId.

In CodeViewFileTree.tsx, when the tree selection changes:

const onSelectionChange = useStableCallback(
  (selectedPaths) => {
    if (selectedPaths.length !== 1) return;

    const [path] = selectedPaths;
    const itemId = sourceRef.current.pathToItemId.get(path);

    if (itemId != null) {
      onSelectItem(itemId);
    }
  }
);

Then ReviewUI.tsx handles that selected item id:

const handleSelectTreeItem = useCallback((itemId: string) => {
  setFileTreeOverlayOpen(false);

  const viewer = viewerRef.current;
  if (viewer == null) return;

  const item = viewer.getItem(itemId);
  if (item != null && item.collapsed === true) {
    item.collapsed = false;
    item.version = typeof item.version === 'number' ? item.version + 1 : 1;
    viewer.updateItem(item);
  }

  viewer.scrollTo({
    type: 'item',
    id: itemId,
    align: 'start',
    behavior: 'smooth',
  });
}, []);

So clicking a file in the @pierre/trees sidebar maps to a @pierre/diffs CodeView item and scrolls to it.

##8. Streaming integration is incremental for both libraries

In usePatchLoader.ts, streamed files are parsed one by one with processFile(), appended into the accumulator, and then published in batches.

For the diff viewer:

viewer.addItems(pendingItems);

For the tree:

setTreeSource(snapshotCodeViewTreeSource(accumulator));

The tree source snapshots are designed for incremental updates. types.ts documents this:

// Streamed publishes link successive snapshots through `previousSource` so the
// tree consumer can recognize append-only growth and apply the delta as
// `model.batch` adds instead of rebuilding the entire path store.

Then CodeViewFileTree.tsx applies the delta:

if (
  source.previousSource != null &&
  source.previousSource === previousSource
) {
  const operations: FileTreeBatchOperation[] = [];

  for (let index = previousPathCount; index < source.pathCount; index++) {
    operations.push({ type: 'add', path: source.paths[index] });
  }

  model.batch(operations);

  if (source.gitStatusPatch != null) {
    model.applyGitStatusPatch(source.gitStatusPatch);
  }
} else {
  model.resetPaths(source.paths.slice(0, source.pathCount));
  model.setGitStatus(source.gitStatus);
}

So streaming does not rebuild the entire tree every time. It appends new paths with model.batch() and patches statuses with model.applyGitStatusPatch().

##9. Theme integration: diff themes are converted into tree styles

Diffshub also makes the tree visually match the diff viewer theme.

This happens in:

apps/docs/app/(diffshub)/(view)/_components/useResolvedTreeThemeStyles.ts

It imports theme resolution from @pierre/diffs:

import { type DiffsThemeNames, getResolvedOrResolveTheme } from '@pierre/diffs';

And tree theme conversion from @pierre/trees:

import { themeToTreeStyles, type TreeThemeStyles } from '@pierre/trees';

The resolved Shiki/diff theme is converted into tree CSS variables:

const treeStyles = themeToTreeStyles(theme);

Then CodeViewFileTree.tsx applies those styles to the FileTree:

const activeStyles = useResolvedTreeThemeStyles(lightTheme, darkTheme);
const themeStyles = useMemo(
  () => ({ ...activeStyles, ...DENSITY_OVERRIDE_STYLES }),
  [activeStyles]
);

<FileTree model={model} style={themeStyles} />

So the sidebar tree follows the same light/dark Shiki theme selection as the diff viewer.


In short: Diffshub uses @pierre/diffs as the patch parser and diff viewer, @pierre/trees as the sidebar file navigator, and a small app-level adapter layer converts FileDiffMetadata into both CodeViewItem[] and FileTree inputs. The two are connected by stable item ids: tree path → CodeView item id → viewer.scrollTo(...).


END OF POST

How are diffs and trees libraries integrated in diffshub? — pierrecomputer/pierre