diff --git a/README.md b/README.md
index 10628341..0647d207 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,12 @@ Hunk is a review-first terminal diff viewer for agent-authored changesets, built
npm i -g hunkdiff
```
+To use the reusable OpenTUI component in your own app:
+
+```bash
+npm i hunkdiff
+```
+
Requirements:
- Node.js 18+
@@ -89,6 +95,61 @@ Hunk is optimized for reviewing a full changeset interactively.
## Advanced
+### OpenTUI component
+
+Hunk also publishes a reusable OpenTUI React diff component powered by the same terminal renderer that the app uses.
+
+Runnable source examples live in [`examples/7-opentui-component`](examples/7-opentui-component/README.md).
+
+Import it from `hunkdiff/opentui`:
+
+```tsx
+import { HunkDiffView, parseDiffFromFile } from "hunkdiff/opentui";
+
+const metadata = parseDiffFromFile(
+ {
+ cacheKey: "before",
+ contents: "export const value = 1;\n",
+ name: "example.ts",
+ },
+ {
+ cacheKey: "after",
+ contents: "export const value = 2;\nexport const added = true;\n",
+ name: "example.ts",
+ },
+ { context: 3 },
+ true,
+);
+
+;
+```
+
+Public API:
+
+- `diff?: { id, metadata, language?, path?, patch? }`
+- `layout?: "split" | "stack"`
+- `width: number`
+- `theme?: "graphite" | "midnight" | "paper" | "ember"`
+- `showLineNumbers?: boolean`
+- `showHunkHeaders?: boolean`
+- `wrapLines?: boolean`
+- `horizontalOffset?: number`
+- `highlight?: boolean`
+- `scrollable?: boolean`
+- `selectedHunkIndex?: number`
+
+The component re-exports `parseDiffFromFile`, `parsePatchFiles`, and `FileDiffMetadata` from `@pierre/diffs` so callers can build the `metadata` input without adding a second diff dependency.
+
### Config
You can persist preferences to a config file:
diff --git a/examples/7-opentui-component/README.md b/examples/7-opentui-component/README.md
new file mode 100644
index 00000000..3b15599c
--- /dev/null
+++ b/examples/7-opentui-component/README.md
@@ -0,0 +1,20 @@
+# 7-opentui-component
+
+Two minimal OpenTUI apps that embed `HunkDiffView` directly.
+
+## Run
+
+```bash
+bun run examples/7-opentui-component/from-files.tsx
+bun run examples/7-opentui-component/from-patch.tsx
+```
+
+## What it shows
+
+- embedding `HunkDiffView` inside a normal OpenTUI app shell
+- building `diff.metadata` with `parseDiffFromFile`
+- parsing raw unified diff text with `parsePatchFiles`
+- switching between split and stacked layouts with example shell controls
+- a scrollable terminal diff component that other OpenTUI apps can reuse
+
+The in-repo demos import from `../../src/opentui` so they run from source. Published consumers should import from `hunkdiff/opentui` instead.
diff --git a/examples/7-opentui-component/after.ts b/examples/7-opentui-component/after.ts
new file mode 100644
index 00000000..a621a676
--- /dev/null
+++ b/examples/7-opentui-component/after.ts
@@ -0,0 +1,10 @@
+export interface ReviewSummary {
+ title: string;
+ confidence: number;
+ tags: string[];
+}
+
+export function formatReviewSummary(summary: ReviewSummary) {
+ const tagSuffix = summary.tags.length > 0 ? ` [${summary.tags.join(", ")}]` : "";
+ return `${summary.title} (${summary.confidence})${tagSuffix}`;
+}
diff --git a/examples/7-opentui-component/before.ts b/examples/7-opentui-component/before.ts
new file mode 100644
index 00000000..6acaa833
--- /dev/null
+++ b/examples/7-opentui-component/before.ts
@@ -0,0 +1,8 @@
+export interface ReviewSummary {
+ title: string;
+ confidence: number;
+}
+
+export function summarizeReview(summary: ReviewSummary) {
+ return `${summary.title} (${summary.confidence})`;
+}
diff --git a/examples/7-opentui-component/change.patch b/examples/7-opentui-component/change.patch
new file mode 100644
index 00000000..443264ef
--- /dev/null
+++ b/examples/7-opentui-component/change.patch
@@ -0,0 +1,17 @@
+diff --git a/src/reviewSummary.ts b/src/reviewSummary.ts
+index 1111111..2222222 100644
+--- a/src/reviewSummary.ts
++++ b/src/reviewSummary.ts
+@@ -1,8 +1,10 @@
+ export interface ReviewSummary {
+ title: string;
+ confidence: number;
++ tags: string[];
+ }
+
+-export function summarizeReview(summary: ReviewSummary) {
+- return `${summary.title} (${summary.confidence})`;
++export function formatReviewSummary(summary: ReviewSummary) {
++ const tagSuffix = summary.tags.length > 0 ? ` [${summary.tags.join(", ")}]` : "";
++ return `${summary.title} (${summary.confidence})${tagSuffix}`;
+ }
diff --git a/examples/7-opentui-component/from-files.tsx b/examples/7-opentui-component/from-files.tsx
new file mode 100644
index 00000000..98e2fb63
--- /dev/null
+++ b/examples/7-opentui-component/from-files.tsx
@@ -0,0 +1,33 @@
+#!/usr/bin/env bun
+
+import { parseDiffFromFile } from "../../src/opentui";
+import { readExampleFile, runExample } from "./support";
+
+const path = "src/reviewSummary.ts";
+const before = readExampleFile("before.ts");
+const after = readExampleFile("after.ts");
+const metadata = parseDiffFromFile(
+ {
+ cacheKey: "example:before",
+ contents: before,
+ name: path,
+ },
+ {
+ cacheKey: "example:after",
+ contents: after,
+ name: path,
+ },
+ { context: 3 },
+ true,
+);
+
+await runExample({
+ title: "HunkDiffView from file contents",
+ subtitle: "Built with parseDiffFromFile. Press Ctrl-C to exit.",
+ diff: {
+ id: "example:files",
+ metadata,
+ language: "typescript",
+ path,
+ },
+});
diff --git a/examples/7-opentui-component/from-patch.tsx b/examples/7-opentui-component/from-patch.tsx
new file mode 100644
index 00000000..56445c9b
--- /dev/null
+++ b/examples/7-opentui-component/from-patch.tsx
@@ -0,0 +1,24 @@
+#!/usr/bin/env bun
+
+import { parsePatchFiles } from "../../src/opentui";
+import { readExampleFile, runExample } from "./support";
+
+const patch = readExampleFile("change.patch");
+const parsedPatches = parsePatchFiles(patch, "example:patch", true);
+const metadata = parsedPatches.flatMap((entry) => entry.files)[0];
+
+if (!metadata) {
+ throw new Error("Expected one diff file in examples/7-opentui-component/change.patch.");
+}
+
+await runExample({
+ title: "HunkDiffView from patch text",
+ subtitle: "Built with parsePatchFiles. Press Ctrl-C to exit.",
+ diff: {
+ id: "example:patch",
+ metadata,
+ language: "typescript",
+ path: metadata.name,
+ patch,
+ },
+});
diff --git a/examples/7-opentui-component/support.tsx b/examples/7-opentui-component/support.tsx
new file mode 100644
index 00000000..c01946da
--- /dev/null
+++ b/examples/7-opentui-component/support.tsx
@@ -0,0 +1,103 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { createCliRenderer } from "@opentui/core";
+import { createRoot, useTerminalDimensions } from "@opentui/react";
+import { useState } from "react";
+import type { HunkDiffFile, HunkDiffLayout } from "../../src/opentui";
+import { HunkDiffView } from "../../src/opentui";
+import { fitText } from "../../src/ui/lib/text";
+
+interface ExampleProps {
+ title: string;
+ subtitle: string;
+ diff: HunkDiffFile;
+ layout?: HunkDiffLayout;
+}
+
+/** Read one checked-in example file relative to this folder. */
+export function readExampleFile(name: string) {
+ return readFileSync(path.join(import.meta.dir, name), "utf8");
+}
+
+function LayoutButton({
+ active,
+ label,
+ onPress,
+}: {
+ active: boolean;
+ label: string;
+ onPress: () => void;
+}) {
+ return (
+
+ {` ${label} `}
+
+ );
+}
+
+function ExampleApp({ title, subtitle, diff, layout = "split" }: ExampleProps) {
+ const [activeLayout, setActiveLayout] = useState(layout);
+ const terminal = useTerminalDimensions();
+ const headerWidth = Math.max(1, terminal.width - 2);
+ const diffWidth = Math.max(24, terminal.width - 2);
+
+ return (
+
+
+ {fitText(title, headerWidth)}
+
+
+ {fitText(subtitle, headerWidth)}
+
+
+ layout
+
+ setActiveLayout("split")}
+ />
+
+ setActiveLayout("stack")}
+ />
+
+
+
+
+
+
+ );
+}
+
+/** Launch a tiny OpenTUI app that embeds the exported Hunk diff component. */
+export async function runExample(props: ExampleProps) {
+ const renderer = await createCliRenderer({
+ useAlternateScreen: true,
+ useMouse: true,
+ exitOnCtrlC: true,
+ openConsoleOnError: true,
+ });
+ const root = createRoot(renderer);
+
+ root.render();
+}
diff --git a/examples/README.md b/examples/README.md
index eafc1bcf..fdc4c466 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -1,6 +1,6 @@
# Examples
-Ready-to-run demo diffs for Hunk.
+Ready-to-run demos for Hunk and the exported OpenTUI diff component.
Each folder tells a small review story and includes the exact command to run from the repository root.
@@ -14,9 +14,11 @@ Each folder tells a small review story and includes the exact command to run fro
| `4-ui-polish` | screenshot-friendly TSX diff | `hunk diff examples/4-ui-polish/before.tsx examples/4-ui-polish/after.tsx` |
| `5-pager-tour` | line scrolling, paging, and hunk jumps | `hunk diff --pager examples/5-pager-tour/before.ts examples/5-pager-tour/after.ts` |
| `6-readme-screenshot` | README screenshot with agent notes | `hunk patch examples/6-readme-screenshot/change.patch --agent-context examples/6-readme-screenshot/agent-context.json --mode split --theme midnight` |
+| `7-opentui-component` | embedding `HunkDiffView` in OpenTUI | `bun run examples/7-opentui-component/from-files.tsx` |
## Notes
- The patch-based examples include checked-in `change.patch` files, so you can open them without creating a temporary repo.
- The agent demo also includes an `agent-context.json` sidecar to show inline review notes beside the diff.
- The pager tour is intentionally taller than a typical terminal viewport so you can try `↑`, `↓`, `PageUp`, `PageDown`, `Home`, `End`, and `[` / `]` right away.
+- The OpenTUI component example folder also includes `from-patch.tsx` if you want the same demo driven by raw unified diff text instead of `before` / `after` contents.
diff --git a/package.json b/package.json
index 04ce9b2c..1512f155 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,13 @@
"LICENSE"
],
"type": "module",
+ "exports": {
+ "./opentui": {
+ "types": "./dist/npm/opentui/index.d.ts",
+ "import": "./dist/npm/opentui/index.js"
+ },
+ "./package.json": "./package.json"
+ },
"publishConfig": {
"access": "public"
},
@@ -62,25 +69,30 @@
"bench:large-stream-profile": "bun run benchmarks/large-stream-profile.ts"
},
"dependencies": {
- "@opentui/core": "^0.1.88",
- "@opentui/react": "^0.1.88",
"@pierre/diffs": "^1.1.0",
"bun": "^1.3.10",
"commander": "^14.0.3",
"diff": "^8.0.3",
- "react": "^19.2.4",
"zod": "^4.3.6"
},
"devDependencies": {
+ "@opentui/core": "^0.1.88",
+ "@opentui/react": "^0.1.88",
"@types/bun": "latest",
"@types/react": "^19.2.14",
"lint-staged": "^16.4.0",
"oxfmt": "^0.41.0",
"oxlint": "^1.56.0",
+ "react": "^19.2.4",
"simple-git-hooks": "^2.13.1",
"tuistory": "^0.0.16",
"typescript": "^5.9.3"
},
+ "peerDependencies": {
+ "@opentui/core": "^0.1.88",
+ "@opentui/react": "^0.1.88",
+ "react": "^19.2.4"
+ },
"simple-git-hooks": {
"pre-commit": "bunx lint-staged"
},
diff --git a/scripts/build-npm.sh b/scripts/build-npm.sh
index bb962b11..31697a8a 100644
--- a/scripts/build-npm.sh
+++ b/scripts/build-npm.sh
@@ -3,9 +3,11 @@ set -Eeuo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
outdir="${repo_root}/dist/npm"
+types_outdir="${repo_root}/dist/npm-types"
rm -rf "${outdir}"
-mkdir -p "${outdir}"
+rm -rf "${types_outdir}"
+mkdir -p "${outdir}/opentui"
BUN_TMPDIR="${repo_root}/.bun-tmp" \
BUN_INSTALL="${repo_root}/.bun-install" \
@@ -17,4 +19,26 @@ BUN_INSTALL="${repo_root}/.bun-install" \
chmod 0755 "${outdir}/main.js"
+BUN_TMPDIR="${repo_root}/.bun-tmp" \
+BUN_INSTALL="${repo_root}/.bun-install" \
+ bun build "${repo_root}/src/opentui/index.ts" \
+ --target node \
+ --format esm \
+ --external react \
+ --external react/jsx-runtime \
+ --external react/jsx-dev-runtime \
+ --external @opentui/core \
+ --external @opentui/react \
+ --external @opentui/react/jsx-runtime \
+ --external @opentui/react/jsx-dev-runtime \
+ --external @pierre/diffs \
+ --outdir "${outdir}/opentui" \
+ --entry-naming index.js
+
+bun x tsc -p "${repo_root}/tsconfig.opentui.json"
+
+cp "${types_outdir}/opentui/"*.d.ts "${outdir}/opentui/"
+rm -rf "${types_outdir}"
+
printf 'Built %s\n' "${outdir}/main.js"
+printf 'Built %s\n' "${outdir}/opentui/index.js"
diff --git a/scripts/check-pack.ts b/scripts/check-pack.ts
index 4818c815..934846ab 100644
--- a/scripts/check-pack.ts
+++ b/scripts/check-pack.ts
@@ -43,7 +43,15 @@ if (!pack) {
}
const publishedPaths = new Set(pack.files.map((file) => file.path));
-const requiredPaths = ["bin/hunk.cjs", "dist/npm/main.js", "README.md", "LICENSE", "package.json"];
+const requiredPaths = [
+ "bin/hunk.cjs",
+ "dist/npm/main.js",
+ "dist/npm/opentui/index.d.ts",
+ "dist/npm/opentui/index.js",
+ "README.md",
+ "LICENSE",
+ "package.json",
+];
for (const path of requiredPaths) {
if (!publishedPaths.has(path)) {
@@ -51,7 +59,15 @@ for (const path of requiredPaths) {
}
}
-const forbiddenPrefixes = [".github/", "src/", "test/", "scripts/", "tmp/"];
+const forbiddenPrefixes = [
+ ".github/",
+ "src/",
+ "test/",
+ "scripts/",
+ "tmp/",
+ "dist/npm/core/",
+ "dist/npm/ui/",
+];
const forbiddenPaths = ["AGENTS.md", "bun.lock"];
for (const file of pack.files) {
diff --git a/src/opentui/HunkDiffView.test.tsx b/src/opentui/HunkDiffView.test.tsx
new file mode 100644
index 00000000..e5251684
--- /dev/null
+++ b/src/opentui/HunkDiffView.test.tsx
@@ -0,0 +1,66 @@
+import { describe, expect, test } from "bun:test";
+import { testRender } from "@opentui/react/test-utils";
+import { act } from "react";
+import type { ReactNode } from "react";
+import { HUNK_DIFF_THEME_NAMES, HunkDiffView, parseDiffFromFile } from "./index";
+
+async function captureFrame(node: ReactNode, width = 120, height = 24) {
+ const setup = await testRender(node, { width, height });
+
+ try {
+ await act(async () => {
+ await setup.renderOnce();
+ });
+
+ return setup.captureCharFrame();
+ } finally {
+ await act(async () => {
+ setup.renderer.destroy();
+ });
+ }
+}
+
+describe("HunkDiffView", () => {
+ test("renders a diff through the public OpenTUI entrypoint", async () => {
+ const metadata = parseDiffFromFile(
+ {
+ cacheKey: "before",
+ contents: "export const value = 1;\n",
+ name: "example.ts",
+ },
+ {
+ cacheKey: "after",
+ contents: "export const value = 2;\nexport const added = true;\n",
+ name: "example.ts",
+ },
+ { context: 3 },
+ true,
+ );
+
+ const frame = await captureFrame(
+ ,
+ 92,
+ 12,
+ );
+
+ expect(frame).toContain("@@ -1,1 +1,2 @@");
+ expect(frame).toContain("1 - export const value = 1;");
+ expect(frame).toContain("1 + export const value = 2;");
+ expect(frame).toContain("2 + export const added = true;");
+ });
+
+ test("exports the documented built-in theme names", () => {
+ expect(HUNK_DIFF_THEME_NAMES).toEqual(["graphite", "midnight", "paper", "ember"]);
+ });
+});
diff --git a/src/opentui/HunkDiffView.tsx b/src/opentui/HunkDiffView.tsx
new file mode 100644
index 00000000..2099481e
--- /dev/null
+++ b/src/opentui/HunkDiffView.tsx
@@ -0,0 +1,124 @@
+import { useMemo } from "react";
+import type { DiffFile } from "../core/types";
+import { findMaxLineNumber } from "../ui/diff/codeColumns";
+import { buildSplitRows, buildStackRows } from "../ui/diff/pierre";
+import { diffMessage, DiffRowView, fitText } from "../ui/diff/renderRows";
+import { useHighlightedDiff } from "../ui/diff/useHighlightedDiff";
+import { resolveTheme } from "../ui/themes";
+import type { HunkDiffFile, HunkDiffViewProps } from "./types";
+
+/** Count visible additions and deletions from Pierre metadata for the internal file adapter. */
+function countDiffStats(metadata: HunkDiffFile["metadata"]) {
+ let additions = 0;
+ let deletions = 0;
+
+ for (const hunk of metadata.hunks) {
+ for (const content of hunk.hunkContent) {
+ if (content.type === "change") {
+ additions += content.additions;
+ deletions += content.deletions;
+ }
+ }
+ }
+
+ return { additions, deletions };
+}
+
+/** Adapt the public diff shape into Hunk's internal file model without exposing app-only fields. */
+function toInternalDiffFile(diff: HunkDiffFile): DiffFile {
+ return {
+ agent: null,
+ id: diff.id,
+ language: diff.language,
+ metadata: diff.metadata,
+ patch: diff.patch ?? "",
+ path: diff.path ?? diff.metadata.name,
+ previousPath: diff.metadata.prevName,
+ stats: countDiffStats(diff.metadata),
+ };
+}
+
+/** Render one diff file body with Hunk's terminal-native OpenTUI renderer. */
+export function HunkDiffView({
+ diff,
+ layout = "split",
+ width,
+ theme = "graphite",
+ showLineNumbers = true,
+ showHunkHeaders = true,
+ wrapLines = false,
+ horizontalOffset = 0,
+ highlight = true,
+ scrollable = true,
+ selectedHunkIndex = 0,
+}: HunkDiffViewProps) {
+ const resolvedTheme = resolveTheme(theme, null);
+ const internalDiff = useMemo(() => (diff ? toInternalDiffFile(diff) : undefined), [diff]);
+ const resolvedHighlighted = useHighlightedDiff({
+ file: internalDiff,
+ appearance: resolvedTheme.appearance,
+ shouldLoadHighlight: highlight,
+ });
+ const rows = useMemo(
+ () =>
+ internalDiff
+ ? layout === "split"
+ ? buildSplitRows(internalDiff, resolvedHighlighted, resolvedTheme)
+ : buildStackRows(internalDiff, resolvedHighlighted, resolvedTheme)
+ : [],
+ [internalDiff, layout, resolvedHighlighted, resolvedTheme],
+ );
+ const lineNumberDigits = useMemo(
+ () => String(internalDiff ? findMaxLineNumber(internalDiff) : 1).length,
+ [internalDiff],
+ );
+
+ if (!internalDiff) {
+ return (
+
+ {fitText("No file selected.", Math.max(1, width - 2))}
+
+ );
+ }
+
+ if (internalDiff.metadata.hunks.length === 0) {
+ return (
+
+
+ {fitText(diffMessage(internalDiff), Math.max(1, width - 2))}
+
+
+ );
+ }
+
+ const content = (
+
+ {rows.map((row) => (
+
+
+
+ ))}
+
+ );
+
+ if (!scrollable) {
+ return content;
+ }
+
+ return (
+
+ {content}
+
+ );
+}
diff --git a/src/opentui/index.ts b/src/opentui/index.ts
new file mode 100644
index 00000000..b33cdacb
--- /dev/null
+++ b/src/opentui/index.ts
@@ -0,0 +1,4 @@
+export { parseDiffFromFile, parsePatchFiles, type FileDiffMetadata } from "@pierre/diffs";
+export { HUNK_DIFF_THEME_NAMES, type HunkDiffThemeName } from "./themes";
+export { HunkDiffView } from "./HunkDiffView";
+export type { HunkDiffFile, HunkDiffLayout, HunkDiffViewProps } from "./types";
diff --git a/src/opentui/themes.ts b/src/opentui/themes.ts
new file mode 100644
index 00000000..07b9599a
--- /dev/null
+++ b/src/opentui/themes.ts
@@ -0,0 +1,3 @@
+export const HUNK_DIFF_THEME_NAMES = ["graphite", "midnight", "paper", "ember"] as const;
+
+export type HunkDiffThemeName = (typeof HUNK_DIFF_THEME_NAMES)[number];
diff --git a/src/opentui/types.ts b/src/opentui/types.ts
new file mode 100644
index 00000000..f0af3767
--- /dev/null
+++ b/src/opentui/types.ts
@@ -0,0 +1,28 @@
+import type { FileDiffMetadata } from "@pierre/diffs";
+import type { HunkDiffThemeName } from "./themes";
+
+export type HunkDiffLayout = "split" | "stack";
+
+/** One diff file body that the exported OpenTUI component can render. */
+export interface HunkDiffFile {
+ id: string;
+ metadata: FileDiffMetadata;
+ language?: string;
+ path?: string;
+ patch?: string;
+}
+
+/** Public props for the reusable OpenTUI diff component. */
+export interface HunkDiffViewProps {
+ diff?: HunkDiffFile;
+ layout?: HunkDiffLayout;
+ width: number;
+ theme?: HunkDiffThemeName;
+ showLineNumbers?: boolean;
+ showHunkHeaders?: boolean;
+ wrapLines?: boolean;
+ horizontalOffset?: number;
+ highlight?: boolean;
+ scrollable?: boolean;
+ selectedHunkIndex?: number;
+}
diff --git a/src/ui/diff/useHighlightedDiff.ts b/src/ui/diff/useHighlightedDiff.ts
index e82fcc16..9cac013b 100644
--- a/src/ui/diff/useHighlightedDiff.ts
+++ b/src/ui/diff/useHighlightedDiff.ts
@@ -19,10 +19,53 @@ function enforceCacheLimit() {
}
}
+/** Summarize rendered diff lines without serializing whole arrays into the cache key. */
+function lineSetFingerprint(lines: string[] | undefined) {
+ let totalChars = 0;
+ let hash = 2166136261;
+
+ for (const line of lines ?? []) {
+ totalChars += line.length;
+
+ for (let index = 0; index < line.length; index += 1) {
+ hash ^= line.charCodeAt(index);
+ hash = Math.imul(hash, 16777619);
+ }
+
+ hash ^= 10;
+ hash = Math.imul(hash, 16777619);
+ }
+
+ return `${lines?.length ?? 0}:${totalChars}:${(hash >>> 0).toString(36)}`;
+}
+
+/** Build a fallback fingerprint from parsed metadata when raw patch text is unavailable. */
+function metadataFingerprint(file: DiffFile) {
+ const hunkSummary = file.metadata.hunks
+ .map(
+ (hunk) =>
+ `${hunk.hunkSpecs ?? ""}:${hunk.deletionStart}:${hunk.deletionCount}:${hunk.additionStart}:${hunk.additionCount}:${hunk.hunkContent.length}`,
+ )
+ .join("|");
+
+ return [
+ file.metadata.name,
+ file.metadata.prevName ?? "",
+ file.metadata.type,
+ lineSetFingerprint(file.metadata.deletionLines),
+ lineSetFingerprint(file.metadata.additionLines),
+ hunkSummary,
+ ].join(":");
+}
+
/** Content fingerprint from the diff patch. Changes whenever the underlying diff
* changes, allowing per-file cache invalidation without a global flush. */
function patchFingerprint(file: DiffFile) {
const { patch } = file;
+ if (patch.length === 0) {
+ return metadataFingerprint(file);
+ }
+
const mid = Math.floor(patch.length / 2);
return `${patch.length}:${patch.slice(0, 64)}:${patch.slice(mid, mid + 64)}:${patch.slice(-64)}`;
}
diff --git a/tsconfig.examples.json b/tsconfig.examples.json
new file mode 100644
index 00000000..9ffc4df4
--- /dev/null
+++ b/tsconfig.examples.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "include": [],
+ "files": [
+ "examples/7-opentui-component/support.tsx",
+ "examples/7-opentui-component/from-files.tsx",
+ "examples/7-opentui-component/from-patch.tsx"
+ ]
+}
diff --git a/tsconfig.opentui.json b/tsconfig.opentui.json
new file mode 100644
index 00000000..e1b363a3
--- /dev/null
+++ b/tsconfig.opentui.json
@@ -0,0 +1,12 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "noEmit": false,
+ "declaration": true,
+ "emitDeclarationOnly": true,
+ "outDir": "./dist/npm-types",
+ "rootDir": "./src"
+ },
+ "include": [],
+ "files": ["src/opentui/index.ts"]
+}