Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
778fbdf
Split the TUI's renderer-free theme and stream code out (AI-assisted)
sidkmenon Oct 9, 2026
acf3882
TUI: light closed folds under the pointer and keep pseudocode beside …
sidkmenon Oct 9, 2026
f87b783
Move the TUI under diffr/packages (AI-assisted)
sidkmenon Oct 9, 2026
d0f3596
TUI: parse theme files with smol-toml instead of Bun.TOML (AI-assisted)
sidkmenon Oct 9, 2026
f829f0f
Move the TUI's renderer-free code into @diffr/viewer (AI-assisted)
sidkmenon Oct 9, 2026
5a3fa62
Plan the TUI's code cells in @diffr/viewer (AI-assisted)
sidkmenon Oct 9, 2026
d741501
Click the rail the pointer arms, even beside a fold's label (AI-assis…
sidkmenon Oct 9, 2026
1fea674
Move the TUI's viewer state and keys into @diffr/viewer (AI-assisted)
sidkmenon Oct 9, 2026
9b1cdde
Move foldBackground from the theme to the document rows (AI-assisted)
sidkmenon Oct 9, 2026
d6d79ec
Make the terminal span helpers generic over any styled span (AI-assis…
sidkmenon Oct 9, 2026
51d5af3
Find visible rows with a direct search instead of Hunk's row windowin…
sidkmenon Oct 9, 2026
de133d9
Delete terminal helpers vendored from Hunk that nothing calls (AI-ass…
sidkmenon Oct 9, 2026
7fe0bab
Name a comparison's ends once, in the protocol (AI-assisted)
sidkmenon Oct 9, 2026
f6215c2
Copy a selection for an agent with Y (AI-assisted)
sidkmenon Oct 9, 2026
5d57cab
Mark files viewed in the TUI (AI-assisted)
sidkmenon Oct 9, 2026
a149203
Search with / and walk matches with n and N (AI-assisted)
sidkmenon Oct 9, 2026
5a67d98
Go to a changed file with Ctrl-P, ranked by fuzzysort (AI-assisted)
sidkmenon Oct 9, 2026
3fc9603
Search and picker polish, ]c and [c, and a status line that keeps ? k…
sidkmenon Oct 9, 2026
b671a8b
Close search, the picker and other open views when the viewer loses f…
sidkmenon Oct 9, 2026
106a740
Lay out files of a million lines without overflowing the stack (AI-as…
sidkmenon Oct 9, 2026
8f9f4ed
Show a selection bar with one action (AI-assisted)
sidkmenon Oct 9, 2026
facee41
Select both sides of a diff and hand agents a Git patch (AI-assisted)
sidkmenon Oct 9, 2026
e95df6e
Widen fold rail and caret hit areas with matching hover feedback (AI-…
sidkmenon Oct 9, 2026
8a74216
Test the TUI and @diffr/viewer in CI (AI-assisted)
sidkmenon Oct 9, 2026
b9b6208
Leave diffr's packages to Diffr Test in Whiteboard CI (AI-assisted)
sidkmenon Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/diffr-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ jobs:
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: 24
- uses: dtolnay/rust-toolchain@stable
- name: Build and package
env:
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/diffr-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,33 @@ jobs:
pnpm build
pnpm test

typescript_packages:
name: TypeScript frontends
permissions:
contents: read
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: 24
cache: pnpm
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14
- run: pnpm install --frozen-lockfile
- working-directory: diffr/packages/viewer
run: |
pnpm typecheck
pnpm test
- working-directory: diffr/packages/tui
run: |
pnpm typecheck
pnpm test

whiteboard_integration:
name: Whiteboard integration with diffr
runs-on: ubuntu-22.04
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/review-desktop-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,16 @@ jobs:
run: pnpm --filter @dev.fast/whiteboard check:tutorial

# The build already built workspace dependencies and typechecked Code OSS.
# Diffr Test checks diffr's packages, with the Bun and Claude Code types they need.
- name: Typecheck
run: |
pnpm -r --filter '!@dev.fast/review-desktop' --workspace-concurrency=1 --if-present typecheck
pnpm -r --filter '!@dev.fast/review-desktop' --filter '!./diffr/**' --workspace-concurrency=1 --if-present typecheck

# Typecheck prepared the shared tutorial assets before concurrent pretests.
- name: Test
run: |
node --test "scripts/**/*.test.mjs"
pnpm -r --filter '!@dev.fast/diffr' --no-sort --workspace-concurrency=3 --if-present test
pnpm -r --filter '!./diffr/**' --no-sort --workspace-concurrency=3 --if-present test

- name: Upload browser test diagnostics
if: failure()
Expand Down
8 changes: 4 additions & 4 deletions diffr/NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ difftastic
Copyright (c) 2021-2025 Wilfred Hughes. MIT License (see LICENSE).
diffr forked difftastic at commit 274d0a8f5 (2026-09-08).

hunk (terminal frontend under tui/)
hunk (terminal frontend under packages/tui/ and packages/viewer/)
https://github.com/modem-dev/hunk
Copyright (c) Modem Labs Inc. MIT License (see tui/LICENSE).
Copyright (c) Modem Labs Inc. MIT License (see packages/tui/LICENSE).
Import details in TUI_UPSTREAM.md.

Helix editor themes (tui/themes/)
Helix editor themes (packages/viewer/themes/)
https://github.com/helix-editor/helix/tree/master/runtime/themes
Mozilla Public License 2.0 (see tui/themes/LICENSE).
Mozilla Public License 2.0 (see packages/viewer/themes/LICENSE).

GitHub Linguist language data (plugins/classify/rust/src/linguist/)
https://github.com/github-linguist/linguist
Expand Down
2 changes: 1 addition & 1 deletion diffr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ cargo install diffr-cli --locked

Windows x64: download `diffr-<version>-<target>.tar.gz` from [Whiteboard Releases](https://github.com/devdotfast/whiteboard/releases) and extract it with `tar -xzf`, or use `cargo binstall`.

From a Whiteboard checkout: `cd diffr && cargo xtask install` (requires Rust and [Bun](https://bun.sh)).
TODO(smenon):UPDATEME!

## History

Expand Down
2 changes: 1 addition & 1 deletion diffr/TUI_UPSTREAM.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Terminal frontend provenance

Imported unchanged from https://github.com/modem-dev/hunk at f86a04ed0325e641d7d96067992f14849016ec8b.
Upstream MIT license is retained in tui/LICENSE.
Upstream MIT license is retained in packages/tui/LICENSE.
Import and subsequent refactoring performed with AI assistance.

Additional restored primitives: `ui/lib/sidebar.ts` is copied unchanged from Hunk;
Expand Down
2 changes: 1 addition & 1 deletion diffr/diffr-ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Keep these files in sync:
|---|---|
| `crates/diffr-core/src/protocol/mod.rs`, `crates/diffr-core/src/pairing.rs`, `diffr-ts/src/contract.ts` | Contract tests against the binary; Rust version check |
| `docs/streaming.md` | Review against the wire format |
| `tui/packages/hunk/src/diffr/wire.ts` (v3 only) | TUI fixture tests |
| `packages/viewer/src/protocol/wire.ts` (v3 only) | TUI fixture tests |
| `../packages/review-protocol/package.json` and `../packages/review/package.json` | Exact package version pins |

## Release
Expand Down
2 changes: 1 addition & 1 deletion diffr/diffr-ts/src/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ describe("every record the binary writes validates", () => {
});

test("the terminal UI's committed v3 fixture still validates", () => {
const text = readFileSync(join(repositoryRoot, "tui/test/fixtures/comparison.ndjson"), "utf8");
const text = readFileSync(join(repositoryRoot, "packages/tui/test/fixtures/comparison.ndjson"), "utf8");
const events = text.split("\n").filter((line) => line.length > 0).map(decodeStructuralDiffEvent);
expect(events[0]).toMatchObject({ type: "start", version: STRUCTURAL_DIFF_WIRE_VERSION });
expect(events.at(-1)).toMatchObject({ type: "complete", failed: 0 });
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
11 changes: 6 additions & 5 deletions diffr/tui/package.json → diffr/packages/tui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@
"license": "MIT",
"type": "module",
"scripts": {
"start": "bun run packages/hunk/src/main.tsx",
"start": "bun run src/main.tsx",
"typecheck": "tsc --noEmit",
"test": "bun test packages"
"test": "bun test src"
},
"dependencies": {
"@diffr/viewer": "workspace:*",
"@opentui/core": "0.5.6",
"@opentui/react": "0.5.6",
"react": "19.2.0",
"get-east-asian-width": "1.4.0",
"string-width": "8.1.0",
"zod": "4.1.12"
"react-devtools-core": "7.0.1",
"web-tree-sitter": "0.25.10",
"ws": "8.21.3"
},
"devDependencies": {
"@types/bun": "1.3.14",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect, test } from "bun:test";
import { readDiffStream } from "./stream";
import { createTestDiffFile, fold, leaf, root } from "./fixture";
import { fileVisibility, type FileChange } from "./wire";
import { createTestDiffFile, fold, leaf, root } from "@diffr/viewer/protocol/fixture";
import { fileVisibility, type FileChange } from "@diffr/viewer/protocol/wire";
const manifest = (file: ReturnType<typeof createTestDiffFile>): FileChange =>
({ file: file.file, status: "modified", tags: [] });
const start = {
Expand Down Expand Up @@ -86,7 +86,7 @@ test("results arrive in any order", async () => {
test("a stream recorded from diffr parses, with manifest tags and each record's visibility", async () => {
// Recorded with `diffr <base> <head> --format ndjson --syntax --jobs 1` from a repository with
// one source file and one test file; the test file is hidden by the hide-files plugin.
const bytes = await Bun.file(new URL("../../../../test/fixtures/comparison.ndjson", import.meta.url)).bytes();
const bytes = await Bun.file(new URL("../../test/fixtures/comparison.ndjson", import.meta.url)).bytes();
async function* chunks() {
yield bytes;
}
Expand Down
12 changes: 12 additions & 0 deletions diffr/packages/tui/src/diffr/stream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { parseDiffEvents } from "@diffr/viewer/protocol/events";
import type { DiffEvent } from "@diffr/viewer/protocol/wire";
export async function* readDiffStream(
chunks: AsyncIterable<Uint8Array>,
): AsyncGenerator<DiffEvent> {
const decoder = new TextDecoder("utf-8", { fatal: true });
async function* texts() {
for await (const chunk of chunks) yield decoder.decode(chunk, { stream: true });
yield decoder.decode();
}
yield* parseDiffEvents(texts());
}
13 changes: 13 additions & 0 deletions diffr/packages/tui/src/diffr/theme.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { expect, test } from "bun:test";
import { themeConfig } from "@diffr/viewer/theme/palette";
import { themesFromConfig } from "./theme";
test("the theme set follows diffr's config and errors on a missing section", () => {
expect(themeConfig({ theme: { name: "gruvbox", path: null } })).toEqual({ name: "gruvbox", path: null });
expect(() => themeConfig({ folds: {} })).toThrow("theme section");
const set = themesFromConfig({ name: "gruvbox", path: null });
expect(set.initial.name).toBe("gruvbox");
expect(set.dark.name).toBe("default-dark");
expect(set.light.name).toBe("default-light");
expect(() => themesFromConfig({ name: "nope", path: null })).toThrow("Unknown theme");
expect(() => themesFromConfig({ name: "default-dark", path: "/nonexistent/theme.toml" })).toThrow();
});
17 changes: 17 additions & 0 deletions diffr/packages/tui/src/diffr/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { readFileSync } from "node:fs";
import { paletteFromHelix, type Palette } from "@diffr/viewer/theme/palette";
import { loadBundledTheme, parseHelixTheme } from "@diffr/viewer/theme/themes";
export function loadThemeFile(path: string): Palette {
return paletteFromHelix(parseHelixTheme(readFileSync(path, "utf8"), path));
}
export interface ThemeSet {
initial: Palette;
dark: Palette;
light: Palette;
}
/** Resolve the theme diffr's config names, plus the two defaults the `t` key toggles between. */
export function themesFromConfig(config: { name: string; path: string | null }): ThemeSet {
const dark = loadBundledTheme("default-dark"), light = loadBundledTheme("default-light");
const initial = config.path ? loadThemeFile(config.path) : loadBundledTheme(config.name);
return { initial, dark, light };
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import { spawn } from "node:child_process";
import { createCliRenderer } from "@opentui/core";
import { createRoot } from "@opentui/react";
import { readDiffStream } from "./diffr/stream";
import { DiffStore } from "./diffr/store";
import { DiffStore } from "@diffr/viewer/protocol/store";
import { cliClient, flattenSchema, type Setting } from "./diffr/config";
import { loadBundledTheme, loadThemeFile, themeConfig, themesFromConfig, type ThemeSet } from "./diffr/theme";
import { loadThemeFile, themesFromConfig, type ThemeSet } from "./diffr/theme";
import { loadBundledTheme } from "@diffr/viewer/theme/themes";
import { themeConfig } from "@diffr/viewer/theme/palette";
import { App } from "./ui/App";
import { Settings } from "./ui/Settings";
const args = process.argv.slice(2),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import { act, Profiler } from "react";
import { TextRenderable, type BaseRenderable } from "@opentui/core";

import { App } from "./App";
import { DiffStore } from "../diffr/store";
import { createTestDiffFile, leaf, line, manifestEntry, startFor, withIdenticalLines, root } from "../diffr/fixture";
import type { DiffEvent, DiffFile } from "../diffr/wire";
import { createBundledDiffFile, createFoldedDiffFile } from "../diffr/regions.test";
import { loadBundledTheme } from "../diffr/theme";
import { DiffStore } from "@diffr/viewer/protocol/store";
import { createBundledDiffFile, createFoldedDiffFile, createTestDiffFile, leaf, line, manifestEntry, startFor, withIdenticalLines, root } from "@diffr/viewer/protocol/fixture";
import type { DiffEvent, DiffFile } from "@diffr/viewer/protocol/wire";
import { loadBundledTheme } from "@diffr/viewer/theme/themes";
const themes = { initial: loadBundledTheme("default-dark"), dark: loadBundledTheme("default-dark"), light: loadBundledTheme("default-light") };
// Wait for the stream batch to be published inside React's act boundary.
async function accept(store: DiffStore, ...events: DiffEvent[]) {
Expand Down
Loading
Loading