Skip to content

diffy (TypeScript / browser extension) ​

A Manifest-V3 browser extension that replaces GitHub's "Files changed" tab with a full-screen, continuous PR review overlay — fetching the diff through the GitHub API and rendering it with Pierre's @pierre/diffs CodeView, with inline comment threads, batched reviews, and viewed-file tracking.

FieldValue
LanguageTypeScript (React 19, WXT extension framework, Vite)
LicenseMIT (© 2026 Suvesh Moza)
Repositoryhttps://github.com/suveshmoza/diffy
DocumentationREADME.md, CHANGELOG.md; Chrome Web Store listing
CategoryWeb review client (GitHub PR overlay extension)
First releaseNot determinable from the tree (CHANGELOG.md carries no dates; local history squashed)
Latest releasev1.3.0 (package.json + wxt.config.ts manifest; local commit dated 2026-07-23)
Surveyed revision27cdcc9a28c1cf59df3b9e9d84ed0ec1c53acb9c (2026-07-23)

NOTE

The local checkout is a single squashed commit, so "first release" and per-feature dating rely on CHANGELOG.md version headings only. The rendering core — @pierre/diffs 1.2.11 and @pierre/trees 1.0.0-beta.4, the diff/file-tree components open-sourced from the Pierre code review product (Diffs, Trees) — is consumed as npm packages; node_modules is absent from this checkout, so claims about the library internals below are limited to what diffy's own code exercises through its public API.

Overview ​

What it solves ​

GitHub's own PR diff renderer has hard caps and soft failure modes on large PRs: a 20,000-line / 1 MB total-diff cap ("Diff too large to display"), a 406 error from the unified-diff endpoint past 300 files, per-file "Load diff" buttons after 400 lines / 20 KB, and a Files-changed tab that reviewers report as slow and memory-hungry. diffy sidesteps the renderer entirely: a View Diff button (plus a right-click context-menu entry on any PR link) opens a full-screen iframe overlay that fetches the PR through the REST/GraphQL API and renders every file in one continuous scroll, with a searchable file tree, inline review threads, batch reviews, and per-file viewed tracking. The README.md feature table is explicit about the positioning:

"Fetches changes through the GitHub API and renders them in a dedicated viewer, bypassing the web UI diff renderer" — README.md

Its niche is therefore the opposite of terminal differs like delta or git-split-diffs: it never touches a working tree or runs git; it is a review client for server-held PRs, competing with GitHub's own web UI rather than with git diff output pipelines.

Design philosophy ​

Three commitments recur through the source:

  • Rent the renderer, own the data path. All diff parsing and painting is delegated to @pierre/diffs (parsePatchFiles, CodeView); diffy's own code is the GitHub data acquisition cascade, the review workflow, and glue. The README credits the lineage directly: "Powered by Pierre Trees and Diffs" (README.md), and the project is an acknowledged fork of the Linear View Diff extension's idea.

  • Stay small and same-origin. A custom WXT build module (src/modules/shiki-pruner.ts) deletes unused Shiki grammar chunks from the bundle by walking the emitted import graph — that is how "100+ languages, 50+ themes … under 2 MB" (README.md) is achieved. The highlighter worker is copied to a stable extension path because, as src/lib/diff/worker.ts documents:

    "Pierre's portable worker is a single self-contained classic script (no static ES imports), so it can run directly from a same-origin URL. … The default shiki-js highlighter uses a pure-JS regex engine, so the worker never reaches its (conditional) oniguruma WASM import and stays standalone." — src/lib/diff/worker.ts

  • Degrade, never block. Every fetch layer has a fallback (diff endpoint → compare endpoint → .diff web URL → patch reassembled from the files API); review-comment loading failures are carried as a loadError string instead of failing the diff; viewed-state loading "never blocks the diff render" (src/hooks/useViewedFiles.ts).

How it works ​

1. Diff computation & data model ​

diffy computes no diff itself — the diff is server-provided by GitHub and parsed client-side. fetchPullRequestDiffData (src/lib/github/api.ts) loads, in parallel, the paginated pulls.listFiles list (100/page, with progress callbacks) and all review comments, then acquires one aggregate unified-diff text via a fallback cascade:

  1. pulls.get with mediaType: { format: 'diff' } — skipped up front when changed_files > 300 (GITHUB_MAX_AGGREGATE_DIFF_FILES, "GitHub rejects unified diffs above this file count");
  2. on a 406 too_large error: repos.compareCommitsWithBasehead (base.sha...head.sha, diff format);
  3. the raw web endpoint https://github.com/{owner}/{repo}/pull/{n}.diff with browser credentials;
  4. final fallback buildPatchFromFiles: the per-file patch fragments from the files API are re-wrapped with synthesized diff --git headers (correct new file mode / deleted file mode / rename headers per file.status), and pure renames/copies with no hunks get a synthetic similarity index 100% stanza so the parser still emits a file entry.

The text is parsed by parsePatchFiles from @pierre/diffs (src/lib/code-view/build-items.ts) into FileDiffMetadata values — the visible shape (constructed manually for media stubs) is { name, prevName, type, hunks, splitLineCount, unifiedLineCount, isPartial, deletionLines, additionLines }, i.e. a hunk-list model that pre-computes per-layout line counts and per-side line indices. Granularity of diffy's own model is file + hunk + line; anything finer lives inside the library. Parsed results become an ordered CodeViewItem[] (type: 'diff' | 'file', following the PR's file order so media files interleave correctly) and are memoized in a module-level Map keyed by owner/repo#N@headSha@{count:idSum:newestUpdatedAt} — a content fingerprint that includes the review-comment set, so a new comment invalidates exactly one PR's built items.

2. Rendering & layout ​

Rendering is Pierre's CodeView React component (wrapped by src/components/diff/ThemedCodeView.tsx), driven by user preferences persisted in browser.storage.sync (src/lib/diff/display-prefs.ts):

  • Split or unified layout toggle (DiffLayoutToggle.tsx); the parsed model's splitLineCount/unifiedLineCount indicates the library pre-computes row alignment for both.
  • Diff indicators: 'classic' (+/- signs), 'bars' (default), or 'none'; hunk separators in four styles (simple, metadata, line-info (default), line-info-basic).
  • Line numbers toggleable; wrap vs horizontal scroll per preference (overflow: 'wrap' default); sticky per-file headers; custom code/tree fonts with size, line-height, and OpenType font-feature settings (Google Fonts allowed by the extension CSP).
  • Syntax highlighting is Shiki, run in a worker pool (src/providers/PersistentWorkerPoolShell.tsx): pool size clamp(1..4, hardwareConcurrency/2), a 200-entry totalASTLRUCacheSize render cache, and the full pruned language list (src/lib/diff/lang-ids.json, regenerated by the build module). Themes come from @pierre/theme/@shikijs/themes with separate light/dark picks and an auto mode (src/lib/theming/).
  • Image diffs reuse the annotation channel (see §3 for the mechanism): before/after images are placed as annotations on the deletions/additions sides of a synthetic zero-line diff, "so split view is truly side-by-side" while "unified view keeps both slots in one row" (src/lib/code-view/build-items.ts); a lightbox offers 2-up, swipe, and onion-skin compare modes. Non-image binaries get a placeholder annotation linking to GitHub.

3. Intra-line & noise handling ​

diffy itself contains no intra-line refinement and no noise handling: a search across src/ finds no occurrence of word-/char-level diffing, whitespace-ignore, or moved-code logic. Any token-level emphasis is whatever @pierre/diffs does internally (not verifiable from this checkout — node_modules is absent). There is no ignore-whitespace toggle, no formatting-noise classification, and no way to re-request the diff with ignore_whitespace semantics (the GitHub API's diff media type has no such parameter, and diffy adds no client-side pass). Absence is the finding: a tool whose diff arrives as opaque server-rendered patch text has ceded this entire dimension to the forge.

The one adjacent mechanism is the generic annotation channel it builds everything else on: DiffLineAnnotation<Metadata> = { lineNumber, side: 'deletions' | 'additions', metadata }. src/lib/review/comments.ts defines a metadata union (thread | draft | queued | media-image | media-binary) and maps GitHub review comments onto items through it; build-items.ts reuses the same channel for image panes. One typed seam serves comments, drafts, queued batch comments, and media panels.

4. Navigation, folding & scale ​

  • File tree: @pierre/trees FileTree with fuzzy search, comment badges (src/lib/file-tree/comment-badge.ts), and icon theming. Notably the tree library is patched (patches/@pierre__trees.patch, via patch-package) to remember manual expand/collapse overrides made while a search filter is active — a fork-and-fix of the vendored controller's search-visibility state machine.
  • Folding: per-file collapse chevrons, Expand All / Collapse All, and auto-collapse of files marked viewed (DiffOverlay.tsx setItemCollapsed / handleCollapseAll). "Jump to next unviewed" walks the PR's file order from the current selection (findNextUnviewedPath, src/lib/review/viewed-files.ts).
  • Scroll anchoring: src/lib/code-view/scroll-anchor.ts wraps any item mutation (collapse, annotation insert) in capturePendingLayoutAnchor() + a double-requestAnimationFrame scroll restore that re-pins scrollTop if the mutation jumped it by more than 2 px — layout changes never teleport the viewport.
  • Large-PR guard: isLargePullRequestData (> 150 files or > 500 KB patch, src/lib/code-view/build-items.ts) routes item building through requestIdleCallback + React startTransition (src/hooks/useCodeViewItems.ts) so parsing a huge patch does not block first paint; smaller PRs build synchronously in useMemo. Prefetching starts on page load (prefetchPullRequestDiff), before the user clicks View Diff, with a TanStack Query cache (staleTime: Infinity, 30 min GC).
  • No context expansion: the viewer shows exactly the hunks GitHub sent; there is no "expand 20 lines" affordance and no blob fetch for text files (the Contents API is used only for media bytes, capped at 10 MB — src/lib/github/blobs.ts).

5. VCS & review integration ​

No git plumbing at all — the extension's only backends are api.github.com (REST via Octokit, GraphQL via raw fetch) and the github.com web origin. Review features (src/lib/github/review-write.ts, src/lib/github/graphql.ts):

  • Inline comments: immediate single comments (pulls.createReviewComment with line/side and multi-line start_line/start_side derived from the CodeView line selection), replies, edits, deletes. Threads are reconstructed client-side from the flat comment list by chasing in_reply_to_id to a root (cycle-guarded) and sorting by created_at; threads whose anchor has no current line (outdated) are surfaced as an "orphaned comments" badge per file rather than dropped.
  • Batch review: queued comments publish in a single POST /pulls/{n}/reviews with a COMMENT / APPROVE / REQUEST_CHANGES verdict.
  • Viewed-file tracking: GitHub's own markFileAsViewed / unmarkFileAsViewed GraphQL mutations with optimistic updates — so progress state is shared with the native GitHub UI. The README documents a sharp edge: fine-grained PATs cannot call these GraphQL mutations, so the full review flow needs a classic repo-scoped token.
  • Errors are mapped to a typed GitHubReviewWriteError code enum with user-actionable messages; rate-limit state is tracked globally and surfaced in the header.

There is no multi-revision model: the viewer always shows base…head of the current PR head SHA (a Refresh button refetches after new pushes), with no per-commit slicing, no interdiff between force-pushes, and no stacked-PR awareness. No staging, no local apply, no merge/conflict handling — out of scope by construction.

6. Architecture & reuse ​

Process model: a content script on github.com (src/entrypoints/github-pr.content/) installs the button and a full-screen chrome-extension:// iframe overlay (src/lib/overlay/frame.ts) with a postMessage ready/close handshake and page scroll-lock; a background service worker handles the context menu and standalone-tab opens; a popup manages the token (browser.storage.local) with capability hints. The overlay can detach into a standalone tab. Two build-system war stories are instructive: Chrome rejects content scripts containing non-ASCII bytes, so a Vite plugin re-escapes emitted chunks to \uXXXX (toAscii(), wxt.config.ts); and dev-server workers are cross-origin, hence the copied-portable-worker scheme quoted above.

Reusable ideas vs monolith: the diff renderer, file tree, and theming are already external packages (@pierre/diffs, @pierre/trees, @pierre/theme) — diffy is evidence that a standalone, worker-pooled, annotation-extensible diff-rendering library with a clean parsePatchFiles → CodeViewItem[] → CodeView pipeline can carry an entire review product. diffy's own transferable pieces are the GitHub diff-acquisition cascade with synthetic-patch fallback (src/lib/github/api.ts), the thread-reconstruction + annotation mapping (src/lib/review/comments.ts), and the scroll-anchored mutation helpers (src/lib/code-view/scroll-anchor.ts). The React/extension shell is monolith-bound.

Strengths ​

  • Concrete, documented answers to GitHub's diff caps: the four-stage fallback cascade means a 3,000-file PR still renders, where the native tab refuses.
  • One generic annotation seam (side + lineNumber + typed metadata) carries review threads, drafts, batch-queue markers, image panes, and binary placeholders — small API, wide reuse.
  • Review-loop UX is complete, not decorative: batch reviews with verdicts, viewed-state synced through GitHub's own GraphQL mutations (interoperable with the native UI), auto-collapse of viewed files, jump-to-next-unviewed, orphaned-thread surfacing.
  • Serious bundle/runtime discipline for a "wrapper" extension: post-build grammar pruning with import-graph traversal, worker-pool highlighting with an AST LRU cache, idle-callback item building gated on a measured large-PR predicate, content-fingerprinted memoization.
  • Scroll anchoring around layout mutations is handled explicitly rather than hoped for.

Weaknesses ​

  • Entirely GitHub-shaped: no local git, no other forges, and the diff is whatever patch text GitHub emits — no context expansion, no whitespace-ignore, no re-diffing at different granularity, no moved-code detection.
  • Single-snapshot review model: no per-commit or push-to-push (interdiff) views, no stacked-PR support; comments anchor to the head SHA only.
  • The rendering core is a third-party dependency the project does not control (already patched once for a search-state bug), and its intra-line behavior is a black box from this tree.
  • Full review flow requires a classic PAT (repo scope) because GitHub's GraphQL viewed-file mutations reject fine-grained tokens — a platform limitation, but user-visible.
  • No tests anywhere in the tree (no test runner in package.json); correctness of e.g. the synthetic-patch builder rests on manual QA.

Key design decisions and trade-offs ​

DecisionRationaleTrade-off
Consume GitHub's patch text instead of computing diffsZero diff engine to maintain; byte-identical to what GitHub reviewed elsewhereInherits GitHub's granularity and caps; no whitespace/structural options; forge lock-in
Rent @pierre/diffs/@pierre/trees for all renderingA tiny team ships a polished split/unified, highlighted, virtual-feeling viewer immediatelyCore behavior (intra-line, alignment) is upstream's; needed patch-package for a tree bug
Iframe overlay on github.com rather than a separate appOne click from the PR page; inherits the user's session for the .diff fallbackExtension CSP/same-origin gymnastics (portable worker, ASCII-escaped scripts)
Four-stage diff-source fallback ending in synthetic patch assemblySome diff always renders, even past the 300-file / 1 MB API capsFallback output can lack hunks for renames/large files; four code paths to keep honest
Annotations as the single extension channelComments, drafts, batch queue, and image diffs share one typed mechanismMedia-as-annotation is a hack (zero-line diff shells with fabricated FileDiffMetadata)
Batch reviews + viewed state via GitHub's own APIsState interoperates with the native UI; nothing proprietary to syncClassic-PAT requirement for GraphQL mutations; client-side thread reconstruction from flat data
staleTime: Infinity caching keyed by head SHA + comment digestReopening a PR is instant; explicit Refresh action controls stalenessNew pushes/comments invisible until manual refresh or key change

Sources ​

  • Local checkout at /home/petar/code/repos/typescript/diffy, revision 27cdcc9a28c1cf59df3b9e9d84ed0ec1c53acb9c (2026-07-23): README.md, CHANGELOG.md, wxt.config.ts, src/lib/github/api.ts, src/lib/code-view/build-items.ts, src/lib/review/comments.ts, src/lib/github/review-write.ts, src/lib/github/graphql.ts, src/lib/diff/display-prefs.ts, src/lib/diff/worker.ts, src/modules/shiki-pruner.ts, src/providers/PersistentWorkerPoolShell.tsx, src/lib/code-view/scroll-anchor.ts, src/hooks/useCodeViewItems.ts, src/hooks/useViewedFiles.ts, src/components/diff/DiffOverlay.tsx, patches/@pierre__trees.patch
  • diffy repository (upstream)
  • Chrome Web Store listing
  • Pierre Diffs and Pierre Trees (the rendering libraries)
  • @pierre/diffs on npm