critique 0.1.41 → 0.1.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/bun.lock +34 -10
- package/package.json +7 -3
- package/src/cli.tsx +142 -3
- package/src/image.ts +304 -0
- package/src/review/acp-client.ts +30 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# 0.1.42
|
|
2
|
+
|
|
3
|
+
- New `--image` flag for all diff commands:
|
|
4
|
+
- Generates WebP images of terminal output (saved to /tmp)
|
|
5
|
+
- Splits long diffs into multiple images (70 lines per image)
|
|
6
|
+
- Uses takumi for high-performance image rendering
|
|
7
|
+
- `@takumi-rs/core` and `@takumi-rs/helpers` added as optional dependencies
|
|
8
|
+
- Library export: `import { renderTerminalToImages } from "critique/src/image.ts"`
|
|
9
|
+
- Web output: Use default theme to enable dark/light mode switching based on system preference
|
|
10
|
+
- `review` command:
|
|
11
|
+
- Improved AI prompt: order hunks by code flow, think upfront before writing, split heavy logic across sections
|
|
12
|
+
- Dependencies:
|
|
13
|
+
- Update opentui to `367a9408`
|
|
14
|
+
|
|
1
15
|
# 0.1.41
|
|
2
16
|
|
|
3
17
|
- `review` command:
|
package/bun.lock
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@agentclientprotocol/sdk": "^0.12.0",
|
|
9
9
|
"@clack/prompts": "1.0.0-alpha.9",
|
|
10
|
-
"@opentui/core": "https://pkg.pr.new/anomalyco/opentui/@opentui/core@
|
|
11
|
-
"@opentui/react": "https://pkg.pr.new/anomalyco/opentui/@opentui/react@
|
|
10
|
+
"@opentui/core": "https://pkg.pr.new/anomalyco/opentui/@opentui/core@367a94087821b3b5feedd35bbb57df43b10a286e",
|
|
11
|
+
"@opentui/react": "https://pkg.pr.new/anomalyco/opentui/@opentui/react@367a94087821b3b5feedd35bbb57df43b10a286e",
|
|
12
12
|
"@parcel/watcher": "^2.5.1",
|
|
13
13
|
"bun-pty": "^0.4.7",
|
|
14
14
|
"cac": "^6.7.14",
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"typescript": "^5.9.3",
|
|
28
28
|
"wrangler": "^4.19.1",
|
|
29
29
|
},
|
|
30
|
+
"optionalDependencies": {
|
|
31
|
+
"@takumi-rs/core": "^0.65.0",
|
|
32
|
+
"@takumi-rs/helpers": "^0.65.0",
|
|
33
|
+
},
|
|
30
34
|
},
|
|
31
35
|
},
|
|
32
36
|
"packages": {
|
|
@@ -222,21 +226,21 @@
|
|
|
222
226
|
|
|
223
227
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
|
224
228
|
|
|
225
|
-
"@opentui/core": ["@opentui/core@https://pkg.pr.new/anomalyco/opentui/@opentui/core@
|
|
229
|
+
"@opentui/core": ["@opentui/core@https://pkg.pr.new/anomalyco/opentui/@opentui/core@367a94087821b3b5feedd35bbb57df43b10a286e", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "https://pkg.pr.new/anomalyco/opentui/@opentui/core-darwin-arm64@367a94087821b3b5feedd35bbb57df43b10a286e", "@opentui/core-darwin-x64": "https://pkg.pr.new/anomalyco/opentui/@opentui/core-darwin-x64@367a94087821b3b5feedd35bbb57df43b10a286e", "@opentui/core-linux-arm64": "https://pkg.pr.new/anomalyco/opentui/@opentui/core-linux-arm64@367a94087821b3b5feedd35bbb57df43b10a286e", "@opentui/core-linux-x64": "https://pkg.pr.new/anomalyco/opentui/@opentui/core-linux-x64@367a94087821b3b5feedd35bbb57df43b10a286e", "@opentui/core-win32-arm64": "https://pkg.pr.new/anomalyco/opentui/@opentui/core-win32-arm64@367a94087821b3b5feedd35bbb57df43b10a286e", "@opentui/core-win32-x64": "https://pkg.pr.new/anomalyco/opentui/@opentui/core-win32-x64@367a94087821b3b5feedd35bbb57df43b10a286e", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }],
|
|
226
230
|
|
|
227
|
-
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@https://pkg.pr.new/anomalyco/opentui/@opentui/core-darwin-arm64@
|
|
231
|
+
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@https://pkg.pr.new/anomalyco/opentui/@opentui/core-darwin-arm64@367a94087821b3b5feedd35bbb57df43b10a286e", {}],
|
|
228
232
|
|
|
229
|
-
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@https://pkg.pr.new/anomalyco/opentui/@opentui/core-darwin-x64@
|
|
233
|
+
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@https://pkg.pr.new/anomalyco/opentui/@opentui/core-darwin-x64@367a94087821b3b5feedd35bbb57df43b10a286e", {}],
|
|
230
234
|
|
|
231
|
-
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@https://pkg.pr.new/anomalyco/opentui/@opentui/core-linux-arm64@
|
|
235
|
+
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@https://pkg.pr.new/anomalyco/opentui/@opentui/core-linux-arm64@367a94087821b3b5feedd35bbb57df43b10a286e", {}],
|
|
232
236
|
|
|
233
|
-
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@https://pkg.pr.new/anomalyco/opentui/@opentui/core-linux-x64@
|
|
237
|
+
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@https://pkg.pr.new/anomalyco/opentui/@opentui/core-linux-x64@367a94087821b3b5feedd35bbb57df43b10a286e", {}],
|
|
234
238
|
|
|
235
|
-
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@https://pkg.pr.new/anomalyco/opentui/@opentui/core-win32-arm64@
|
|
239
|
+
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@https://pkg.pr.new/anomalyco/opentui/@opentui/core-win32-arm64@367a94087821b3b5feedd35bbb57df43b10a286e", {}],
|
|
236
240
|
|
|
237
|
-
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@https://pkg.pr.new/anomalyco/opentui/@opentui/core-win32-x64@
|
|
241
|
+
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@https://pkg.pr.new/anomalyco/opentui/@opentui/core-win32-x64@367a94087821b3b5feedd35bbb57df43b10a286e", {}],
|
|
238
242
|
|
|
239
|
-
"@opentui/react": ["@opentui/react@https://pkg.pr.new/anomalyco/opentui/@opentui/react@
|
|
243
|
+
"@opentui/react": ["@opentui/react@https://pkg.pr.new/anomalyco/opentui/@opentui/react@367a94087821b3b5feedd35bbb57df43b10a286e", { "dependencies": { "@opentui/core": "https://pkg.pr.new/anomalyco/opentui/@opentui/core@367a94087821b3b5feedd35bbb57df43b10a286e", "react-reconciler": "^0.32.0" }, "peerDependencies": { "react": ">=19.0.0", "react-devtools-core": "^7.0.1", "ws": "^8.18.0" } }],
|
|
240
244
|
|
|
241
245
|
"@parcel/watcher": ["@parcel/watcher@2.5.4", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.4", "@parcel/watcher-darwin-arm64": "2.5.4", "@parcel/watcher-darwin-x64": "2.5.4", "@parcel/watcher-freebsd-x64": "2.5.4", "@parcel/watcher-linux-arm-glibc": "2.5.4", "@parcel/watcher-linux-arm-musl": "2.5.4", "@parcel/watcher-linux-arm64-glibc": "2.5.4", "@parcel/watcher-linux-arm64-musl": "2.5.4", "@parcel/watcher-linux-x64-glibc": "2.5.4", "@parcel/watcher-linux-x64-musl": "2.5.4", "@parcel/watcher-win32-arm64": "2.5.4", "@parcel/watcher-win32-ia32": "2.5.4", "@parcel/watcher-win32-x64": "2.5.4" } }, "sha512-WYa2tUVV5HiArWPB3ydlOc4R2ivq0IDrlqhMi3l7mVsFEXNcTfxYFPIHXHXIh/ca/y/V5N4E1zecyxdIBjYnkQ=="],
|
|
242
246
|
|
|
@@ -276,6 +280,26 @@
|
|
|
276
280
|
|
|
277
281
|
"@speed-highlight/core": ["@speed-highlight/core@1.2.14", "", {}, "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA=="],
|
|
278
282
|
|
|
283
|
+
"@takumi-rs/core": ["@takumi-rs/core@0.65.0", "", { "optionalDependencies": { "@takumi-rs/core-darwin-arm64": "0.65.0", "@takumi-rs/core-darwin-x64": "0.65.0", "@takumi-rs/core-linux-arm64-gnu": "0.65.0", "@takumi-rs/core-linux-arm64-musl": "0.65.0", "@takumi-rs/core-linux-x64-gnu": "0.65.0", "@takumi-rs/core-linux-x64-musl": "0.65.0", "@takumi-rs/core-win32-arm64-msvc": "0.65.0", "@takumi-rs/core-win32-x64-msvc": "0.65.0" } }, "sha512-lBNG+NRc602ul7Kxy9UohlbnFLVyloQP/DTxQzyNH+8khpdaIQTxpdHouzRzxf6upRDVDIVX5TG8oT16jKUAvQ=="],
|
|
284
|
+
|
|
285
|
+
"@takumi-rs/core-darwin-arm64": ["@takumi-rs/core-darwin-arm64@0.65.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Ii6ILOANG6IeGThsuYjG5azlHXyHWWdVbM1ps3+SJyUg2g4Qn+nTruKGqHQOLNdZ5+37vpTg4PWh79XrUvjXpw=="],
|
|
286
|
+
|
|
287
|
+
"@takumi-rs/core-darwin-x64": ["@takumi-rs/core-darwin-x64@0.65.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-SJGeFVQ2foVHpYL6EiD+jNJnvQmegDLbbJ/SzbaYemJ/kjza2vNDg251urXne6daQ+HoGXLjhQtzW1C5Nyyf5g=="],
|
|
288
|
+
|
|
289
|
+
"@takumi-rs/core-linux-arm64-gnu": ["@takumi-rs/core-linux-arm64-gnu@0.65.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-YbwZzQTugFwvYErXs2W/QsngPSU5H6ZtYJNdbz9/qk8EEU6eB593XiBxE9/1vbq6Sb3mH84mPXElf1BmWJlvzw=="],
|
|
290
|
+
|
|
291
|
+
"@takumi-rs/core-linux-arm64-musl": ["@takumi-rs/core-linux-arm64-musl@0.65.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-iZpzfnD1/Gyof3p+fYUIBxNkDH9vQviOeihZ7yAtPbGmY4AbnOjoUBHDyJoKWE0el/x1W17efvNE7FuvzP0O5Q=="],
|
|
292
|
+
|
|
293
|
+
"@takumi-rs/core-linux-x64-gnu": ["@takumi-rs/core-linux-x64-gnu@0.65.0", "", { "os": "linux", "cpu": "x64" }, "sha512-kL4VclPYz7nmQuadCVSrAFvdy8LaJX5RPymG3upaHPcz0i7A8KmWY8jnQcDDkGQfPIi2lFncnt/sknCiaJ1t4Q=="],
|
|
294
|
+
|
|
295
|
+
"@takumi-rs/core-linux-x64-musl": ["@takumi-rs/core-linux-x64-musl@0.65.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2way2sF3p5bMlhGsDMmQGTPxtc37241POzAJ0bPRKR1fumylEAaADuXvN95qM4x9751iTke/wVhJOL28nV7asw=="],
|
|
296
|
+
|
|
297
|
+
"@takumi-rs/core-win32-arm64-msvc": ["@takumi-rs/core-win32-arm64-msvc@0.65.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-yCZqiu1b3XXhDUE7OaA5RdVg/nYcZU1nzToY7ViE4fR8PqOK54Ad8qOAKYZCTr4FwCB4sWo88/R7K1n0nPFpfA=="],
|
|
298
|
+
|
|
299
|
+
"@takumi-rs/core-win32-x64-msvc": ["@takumi-rs/core-win32-x64-msvc@0.65.0", "", { "os": "win32", "cpu": "x64" }, "sha512-SAw224AhLcKa9+ttO4IxeTquOvyz8pnDeGugzmQZerYO18kOuOCiP1g4VRhM1drpEFN7bbnhSnIvrewg5m4akw=="],
|
|
300
|
+
|
|
301
|
+
"@takumi-rs/helpers": ["@takumi-rs/helpers@0.65.0", "", {}, "sha512-3BsiW0iP3Y7xUPmT/tJBClb59qTxtHEYHQUznXdtDA0qyN6YAWABA6U0DTd3la6XHMEDyKaHV263OL679FyPaA=="],
|
|
302
|
+
|
|
279
303
|
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
|
280
304
|
|
|
281
305
|
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "critique",
|
|
3
3
|
"module": "src/diff.tsx",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.42",
|
|
6
6
|
"private": false,
|
|
7
7
|
"bin": "./src/cli.tsx",
|
|
8
8
|
"scripts": {
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@agentclientprotocol/sdk": "^0.12.0",
|
|
26
26
|
"@clack/prompts": "1.0.0-alpha.9",
|
|
27
|
-
"@opentui/core": "https://pkg.pr.new/anomalyco/opentui/@opentui/core@
|
|
28
|
-
"@opentui/react": "https://pkg.pr.new/anomalyco/opentui/@opentui/react@
|
|
27
|
+
"@opentui/core": "https://pkg.pr.new/anomalyco/opentui/@opentui/core@367a94087821b3b5feedd35bbb57df43b10a286e",
|
|
28
|
+
"@opentui/react": "https://pkg.pr.new/anomalyco/opentui/@opentui/react@367a94087821b3b5feedd35bbb57df43b10a286e",
|
|
29
29
|
"@parcel/watcher": "^2.5.1",
|
|
30
30
|
"bun-pty": "^0.4.7",
|
|
31
31
|
"cac": "^6.7.14",
|
|
@@ -35,5 +35,9 @@
|
|
|
35
35
|
"picocolors": "^1.1.1",
|
|
36
36
|
"react": "^19.2.0",
|
|
37
37
|
"zustand": "^5.0.8"
|
|
38
|
+
},
|
|
39
|
+
"optionalDependencies": {
|
|
40
|
+
"@takumi-rs/core": "^0.65.0",
|
|
41
|
+
"@takumi-rs/helpers": "^0.65.0"
|
|
38
42
|
}
|
|
39
43
|
}
|
package/src/cli.tsx
CHANGED
|
@@ -504,7 +504,8 @@ async function runReviewMode(
|
|
|
504
504
|
|
|
505
505
|
// Write hunks to temp file for the render command
|
|
506
506
|
const hunksFile = writeTempFile(JSON.stringify(hunks), "critique-hunks", ".json");
|
|
507
|
-
|
|
507
|
+
// For web, always use default theme (with auto dark/light inversion) unless explicitly overridden
|
|
508
|
+
const themeName = defaultThemeName;
|
|
508
509
|
|
|
509
510
|
// Calculate rows needed based on hunks
|
|
510
511
|
const totalLines = hunks.reduce((sum, h) => sum + h.lines.length, 0);
|
|
@@ -834,7 +835,8 @@ async function runResumeMode(options: ResumeModeOptions) {
|
|
|
834
835
|
}).join("\n");
|
|
835
836
|
const yamlFile = writeTempFile(yamlContent, "critique-review", ".yaml");
|
|
836
837
|
|
|
837
|
-
|
|
838
|
+
// For web, always use default theme (with auto dark/light inversion) unless explicitly overridden
|
|
839
|
+
const themeName = defaultThemeName;
|
|
838
840
|
const totalLines = review.hunks.reduce((sum, h) => sum + h.lines.length, 0);
|
|
839
841
|
const baseRows = Math.max(200, totalLines * 2 + 100);
|
|
840
842
|
|
|
@@ -918,6 +920,16 @@ interface WebModeOptions {
|
|
|
918
920
|
'--'?: string[];
|
|
919
921
|
}
|
|
920
922
|
|
|
923
|
+
// Image mode handler
|
|
924
|
+
interface ImageModeOptions {
|
|
925
|
+
staged?: boolean;
|
|
926
|
+
commit?: string;
|
|
927
|
+
context?: string;
|
|
928
|
+
filter?: string;
|
|
929
|
+
theme?: string;
|
|
930
|
+
'--'?: string[];
|
|
931
|
+
}
|
|
932
|
+
|
|
921
933
|
async function runWebMode(
|
|
922
934
|
base: string | undefined,
|
|
923
935
|
head: string | undefined,
|
|
@@ -943,9 +955,10 @@ async function runWebMode(
|
|
|
943
955
|
|
|
944
956
|
const desktopCols = options.cols || 230;
|
|
945
957
|
const mobileCols = options.mobileCols || 100;
|
|
958
|
+
// For web, always use default theme (with auto dark/light inversion) unless explicitly overridden via --theme
|
|
946
959
|
const themeName = options.theme && themeNames.includes(options.theme)
|
|
947
960
|
? options.theme
|
|
948
|
-
:
|
|
961
|
+
: defaultThemeName;
|
|
949
962
|
|
|
950
963
|
console.log("Capturing diff output...");
|
|
951
964
|
|
|
@@ -1009,6 +1022,118 @@ async function runWebMode(
|
|
|
1009
1022
|
}
|
|
1010
1023
|
}
|
|
1011
1024
|
|
|
1025
|
+
async function runImageMode(
|
|
1026
|
+
base: string | undefined,
|
|
1027
|
+
head: string | undefined,
|
|
1028
|
+
options: ImageModeOptions
|
|
1029
|
+
) {
|
|
1030
|
+
const { renderTerminalToImages } = await import("./image.ts");
|
|
1031
|
+
const { writeTempFile, cleanupTempFile } = await import("./web-utils.ts");
|
|
1032
|
+
|
|
1033
|
+
const gitCommand = buildGitCommand({
|
|
1034
|
+
staged: options.staged,
|
|
1035
|
+
commit: options.commit,
|
|
1036
|
+
base,
|
|
1037
|
+
head,
|
|
1038
|
+
context: options.context,
|
|
1039
|
+
filter: options.filter,
|
|
1040
|
+
positionalFilters: options['--'],
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
const themeName = options.theme && themeNames.includes(options.theme)
|
|
1044
|
+
? options.theme
|
|
1045
|
+
: persistedState.themeName ?? defaultThemeName;
|
|
1046
|
+
|
|
1047
|
+
console.log("Capturing diff output...");
|
|
1048
|
+
|
|
1049
|
+
// Get the git diff
|
|
1050
|
+
const { stdout: gitDiff } = await execAsync(gitCommand, {
|
|
1051
|
+
encoding: "utf-8",
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
if (!gitDiff.trim()) {
|
|
1055
|
+
console.log("No changes to display");
|
|
1056
|
+
process.exit(0);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Write diff to temp file
|
|
1060
|
+
const diffFile = writeTempFile(gitDiff, "critique-image-diff", ".patch");
|
|
1061
|
+
|
|
1062
|
+
// Build render command for image capture
|
|
1063
|
+
const renderCommand = [
|
|
1064
|
+
process.argv[1]!, // path to cli.tsx
|
|
1065
|
+
"web-render",
|
|
1066
|
+
diffFile,
|
|
1067
|
+
"--theme",
|
|
1068
|
+
themeName,
|
|
1069
|
+
"--cols",
|
|
1070
|
+
"120",
|
|
1071
|
+
"--rows",
|
|
1072
|
+
"10000",
|
|
1073
|
+
];
|
|
1074
|
+
|
|
1075
|
+
console.log("Rendering to images...");
|
|
1076
|
+
|
|
1077
|
+
try {
|
|
1078
|
+
// Capture PTY output
|
|
1079
|
+
const decoder = new TextDecoder();
|
|
1080
|
+
let ansiOutput = "";
|
|
1081
|
+
const cols = 120;
|
|
1082
|
+
const rows = 10000;
|
|
1083
|
+
|
|
1084
|
+
const proc = Bun.spawn(["bun", ...renderCommand], {
|
|
1085
|
+
cwd: process.cwd(),
|
|
1086
|
+
env: {
|
|
1087
|
+
...process.env,
|
|
1088
|
+
TERM: "xterm-256color",
|
|
1089
|
+
},
|
|
1090
|
+
terminal: {
|
|
1091
|
+
cols,
|
|
1092
|
+
rows,
|
|
1093
|
+
data(terminal, data) {
|
|
1094
|
+
ansiOutput += decoder.decode(data, { stream: true });
|
|
1095
|
+
},
|
|
1096
|
+
},
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
await proc.exited;
|
|
1100
|
+
proc.terminal?.close();
|
|
1101
|
+
ansiOutput += decoder.decode();
|
|
1102
|
+
|
|
1103
|
+
// Clean up diff temp file
|
|
1104
|
+
cleanupTempFile(diffFile);
|
|
1105
|
+
|
|
1106
|
+
if (!ansiOutput.trim()) {
|
|
1107
|
+
console.error("No output captured");
|
|
1108
|
+
process.exit(1);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// Strip terminal cleanup sequences
|
|
1112
|
+
const clearIdx = ansiOutput.lastIndexOf("\x1b[H\x1b[J");
|
|
1113
|
+
if (clearIdx > 0) {
|
|
1114
|
+
ansiOutput = ansiOutput.slice(0, clearIdx);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Render to images
|
|
1118
|
+
const result = await renderTerminalToImages(ansiOutput, {
|
|
1119
|
+
cols,
|
|
1120
|
+
themeName,
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
console.log(`\nGenerated ${result.imageCount} image${result.imageCount === 1 ? "" : "s"}:`);
|
|
1124
|
+
for (const path of result.paths) {
|
|
1125
|
+
console.log(` ${path}`);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
process.exit(0);
|
|
1129
|
+
} catch (error: unknown) {
|
|
1130
|
+
cleanupTempFile(diffFile);
|
|
1131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1132
|
+
console.error("Failed to generate images:", message);
|
|
1133
|
+
process.exit(1);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1012
1137
|
// Error boundary component
|
|
1013
1138
|
interface ErrorBoundaryProps {
|
|
1014
1139
|
children: React.ReactNode;
|
|
@@ -1354,6 +1479,7 @@ cli
|
|
|
1354
1479
|
.option("--filter <pattern>", "Filter files by glob pattern (can be used multiple times)")
|
|
1355
1480
|
.option("--theme <name>", "Theme to use for rendering")
|
|
1356
1481
|
.option("--web [title]", "Generate web preview instead of TUI")
|
|
1482
|
+
.option("--image", "Generate images instead of TUI (saved to /tmp)")
|
|
1357
1483
|
.option("--open", "Open in browser (with --web)")
|
|
1358
1484
|
.option("--cols <cols>", "Desktop columns for web render", { default: 240 })
|
|
1359
1485
|
.option("--mobile-cols <cols>", "Mobile columns for web render", { default: 100 })
|
|
@@ -1411,6 +1537,19 @@ cli
|
|
|
1411
1537
|
return;
|
|
1412
1538
|
}
|
|
1413
1539
|
|
|
1540
|
+
// If --image flag, delegate to image generation logic
|
|
1541
|
+
if (options.image) {
|
|
1542
|
+
await runImageMode(base, head, {
|
|
1543
|
+
staged: options.staged,
|
|
1544
|
+
commit: options.commit,
|
|
1545
|
+
context: options.context,
|
|
1546
|
+
filter: options.filter,
|
|
1547
|
+
theme: options.theme,
|
|
1548
|
+
'--': options['--'],
|
|
1549
|
+
});
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1414
1553
|
try {
|
|
1415
1554
|
const gitCommand = buildGitCommand({
|
|
1416
1555
|
staged: options.staged,
|
package/src/image.ts
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// Terminal output to image conversion using takumi.
|
|
2
|
+
// Converts ANSI terminal output to images, splitting into multiple pages if needed.
|
|
3
|
+
// Exports renderTerminalToImages() for library and CLI use.
|
|
4
|
+
|
|
5
|
+
import { tmpdir } from "os"
|
|
6
|
+
import { join } from "path"
|
|
7
|
+
import fs from "fs"
|
|
8
|
+
import { ptyToJson, StyleFlags, type TerminalLine, type TerminalSpan } from "ghostty-opentui"
|
|
9
|
+
import { getResolvedTheme, rgbaToHex } from "./themes.ts"
|
|
10
|
+
|
|
11
|
+
export interface RenderToImagesOptions {
|
|
12
|
+
/** Terminal columns for parsing (default: 120) */
|
|
13
|
+
cols?: number
|
|
14
|
+
/** Terminal rows for parsing (default: 10000, effectively unlimited) */
|
|
15
|
+
rows?: number
|
|
16
|
+
/** Theme name for colors */
|
|
17
|
+
themeName?: string
|
|
18
|
+
/** Image width in pixels (default: 1200) */
|
|
19
|
+
imageWidth?: number
|
|
20
|
+
/** Font size in pixels (default: 14) */
|
|
21
|
+
fontSize?: number
|
|
22
|
+
/** Line height multiplier (default: 1.7) */
|
|
23
|
+
lineHeight?: number
|
|
24
|
+
/** Maximum lines per image before splitting (default: 70) */
|
|
25
|
+
maxLinesPerImage?: number
|
|
26
|
+
/** Output format: webp, png, or jpeg (default: webp) */
|
|
27
|
+
format?: "webp" | "png" | "jpeg"
|
|
28
|
+
/** Quality for lossy formats 0-100 (default: 85) */
|
|
29
|
+
quality?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RenderResult {
|
|
33
|
+
/** Array of image buffers */
|
|
34
|
+
images: Buffer[]
|
|
35
|
+
/** Paths where images were saved */
|
|
36
|
+
paths: string[]
|
|
37
|
+
/** Total number of lines in the output */
|
|
38
|
+
totalLines: number
|
|
39
|
+
/** Number of images generated */
|
|
40
|
+
imageCount: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Convert terminal spans to takumi text nodes.
|
|
45
|
+
* Following shiki-image pattern: only display: inline and color styles.
|
|
46
|
+
*/
|
|
47
|
+
function spanToNode(
|
|
48
|
+
span: TerminalSpan,
|
|
49
|
+
text: typeof import("@takumi-rs/helpers").text
|
|
50
|
+
) {
|
|
51
|
+
const style: Record<string, string | number> = {
|
|
52
|
+
display: "inline",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (span.fg) {
|
|
56
|
+
style.color = span.fg
|
|
57
|
+
}
|
|
58
|
+
if (span.bg) {
|
|
59
|
+
style.backgroundColor = span.bg
|
|
60
|
+
}
|
|
61
|
+
if (span.flags & StyleFlags.BOLD) {
|
|
62
|
+
style.fontWeight = "bold"
|
|
63
|
+
}
|
|
64
|
+
if (span.flags & StyleFlags.ITALIC) {
|
|
65
|
+
style.fontStyle = "italic"
|
|
66
|
+
}
|
|
67
|
+
if (span.flags & StyleFlags.FAINT) {
|
|
68
|
+
style.opacity = 0.5
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return text(span.text, style as any)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Convert a terminal line to a takumi container node.
|
|
76
|
+
* All lines treated the same - render spans with background.
|
|
77
|
+
* Uses negative margin to eliminate gaps (takumi doesn't respect container height for backgrounds).
|
|
78
|
+
*/
|
|
79
|
+
function lineToNode(
|
|
80
|
+
line: TerminalLine,
|
|
81
|
+
container: typeof import("@takumi-rs/helpers").container,
|
|
82
|
+
text: typeof import("@takumi-rs/helpers").text,
|
|
83
|
+
backgroundColor: string,
|
|
84
|
+
lineHeight: number,
|
|
85
|
+
fontSize: number
|
|
86
|
+
) {
|
|
87
|
+
const lineHeightPx = Math.round(fontSize * lineHeight)
|
|
88
|
+
|
|
89
|
+
// Calculate negative margin to eliminate gaps between lines
|
|
90
|
+
// The gap is proportional to the extra space from line-height (lineHeight - 1) * fontSize
|
|
91
|
+
// We use ~50% of that as overlap to close the gaps
|
|
92
|
+
const gapOverlap = Math.round((lineHeight - 1) * fontSize * 0.5)
|
|
93
|
+
|
|
94
|
+
// Get text children from spans, or use a visible character if empty
|
|
95
|
+
let textChildren = line.spans.map((span) => spanToNode(span, text))
|
|
96
|
+
if (textChildren.length === 0) {
|
|
97
|
+
textChildren = [text("X", { color: backgroundColor })]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return container({
|
|
101
|
+
style: {
|
|
102
|
+
display: "flex",
|
|
103
|
+
flexDirection: "row",
|
|
104
|
+
alignItems: "center",
|
|
105
|
+
width: "100%",
|
|
106
|
+
height: lineHeightPx,
|
|
107
|
+
marginBottom: -gapOverlap,
|
|
108
|
+
backgroundColor,
|
|
109
|
+
},
|
|
110
|
+
children: textChildren,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if a line is empty (no spans or only whitespace)
|
|
116
|
+
*/
|
|
117
|
+
function isLineEmpty(line: TerminalLine): boolean {
|
|
118
|
+
if (line.spans.length === 0) return true
|
|
119
|
+
return line.spans.every((span) => span.text.trim() === "")
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Trim empty lines from the end of the lines array
|
|
124
|
+
*/
|
|
125
|
+
function trimEmptyLines(lines: TerminalLine[]): TerminalLine[] {
|
|
126
|
+
let result = [...lines]
|
|
127
|
+
while (result.length > 0 && isLineEmpty(result[result.length - 1]!)) {
|
|
128
|
+
result = result.slice(0, -1)
|
|
129
|
+
}
|
|
130
|
+
return result
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Render terminal output to images.
|
|
135
|
+
* This is the main export for library use.
|
|
136
|
+
*
|
|
137
|
+
* @param ansiOutput - Raw ANSI terminal output string or Buffer
|
|
138
|
+
* @param options - Rendering options
|
|
139
|
+
* @returns Promise with image buffers and saved file paths
|
|
140
|
+
*/
|
|
141
|
+
export async function renderTerminalToImages(
|
|
142
|
+
ansiOutput: string | Buffer,
|
|
143
|
+
options: RenderToImagesOptions = {}
|
|
144
|
+
): Promise<RenderResult> {
|
|
145
|
+
// Try to import takumi - it's an optional dependency
|
|
146
|
+
let takumiCore: typeof import("@takumi-rs/core")
|
|
147
|
+
let takumiHelpers: typeof import("@takumi-rs/helpers")
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
takumiCore = await import("@takumi-rs/core")
|
|
151
|
+
takumiHelpers = await import("@takumi-rs/helpers")
|
|
152
|
+
} catch {
|
|
153
|
+
throw new Error(
|
|
154
|
+
"takumi is not installed. Install it with: bun add @takumi-rs/core @takumi-rs/helpers"
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const { Renderer } = takumiCore
|
|
159
|
+
const { container, text } = takumiHelpers
|
|
160
|
+
|
|
161
|
+
const {
|
|
162
|
+
cols = 120,
|
|
163
|
+
rows = 10000,
|
|
164
|
+
themeName = "tokyonight",
|
|
165
|
+
imageWidth = 1200,
|
|
166
|
+
fontSize = 14,
|
|
167
|
+
lineHeight = 1.9,
|
|
168
|
+
maxLinesPerImage = 70,
|
|
169
|
+
format = "webp",
|
|
170
|
+
quality = 85,
|
|
171
|
+
} = options
|
|
172
|
+
|
|
173
|
+
// Parse ANSI to terminal data
|
|
174
|
+
const data = ptyToJson(ansiOutput, { cols, rows })
|
|
175
|
+
let lines = trimEmptyLines(data.lines)
|
|
176
|
+
|
|
177
|
+
if (lines.length === 0) {
|
|
178
|
+
throw new Error("No content to render")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Get theme colors
|
|
182
|
+
const theme = getResolvedTheme(themeName)
|
|
183
|
+
const backgroundColor = rgbaToHex(theme.background)
|
|
184
|
+
const textColor = rgbaToHex(theme.text)
|
|
185
|
+
|
|
186
|
+
// Calculate dimensions
|
|
187
|
+
const lineHeightPx = Math.round(fontSize * lineHeight)
|
|
188
|
+
const paddingY = 24
|
|
189
|
+
const paddingX = 32
|
|
190
|
+
|
|
191
|
+
// Split lines into chunks
|
|
192
|
+
const chunks: TerminalLine[][] = []
|
|
193
|
+
for (let i = 0; i < lines.length; i += maxLinesPerImage) {
|
|
194
|
+
chunks.push(lines.slice(i, i + maxLinesPerImage))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Create renderer with bundled Geist font (included in takumi)
|
|
198
|
+
const renderer = new Renderer()
|
|
199
|
+
|
|
200
|
+
const images: Buffer[] = []
|
|
201
|
+
const paths: string[] = []
|
|
202
|
+
const timestamp = Date.now()
|
|
203
|
+
|
|
204
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
205
|
+
const chunk = chunks[chunkIndex]!
|
|
206
|
+
const imageHeight = chunk.length * lineHeightPx + paddingY * 2
|
|
207
|
+
|
|
208
|
+
// Build the root container for this chunk
|
|
209
|
+
// Explicit dimensions, lines stack with explicit heights
|
|
210
|
+
const contentHeight = chunk.length * lineHeightPx
|
|
211
|
+
|
|
212
|
+
const rootNode = container({
|
|
213
|
+
style: {
|
|
214
|
+
display: "flex",
|
|
215
|
+
flexDirection: "column",
|
|
216
|
+
gap: 0,
|
|
217
|
+
width: "100%",
|
|
218
|
+
height: "100%",
|
|
219
|
+
backgroundColor,
|
|
220
|
+
color: textColor,
|
|
221
|
+
fontFamily: "monospace",
|
|
222
|
+
fontSize,
|
|
223
|
+
whiteSpace: "pre",
|
|
224
|
+
paddingTop: paddingY,
|
|
225
|
+
paddingBottom: paddingY,
|
|
226
|
+
paddingLeft: paddingX,
|
|
227
|
+
paddingRight: paddingX,
|
|
228
|
+
},
|
|
229
|
+
children: chunk.map((line) => lineToNode(line, container, text, backgroundColor, lineHeight, fontSize)),
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// Render to image
|
|
233
|
+
const imageBuffer = await renderer.render(rootNode, {
|
|
234
|
+
width: imageWidth,
|
|
235
|
+
height: imageHeight,
|
|
236
|
+
format,
|
|
237
|
+
quality,
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
images.push(Buffer.from(imageBuffer))
|
|
241
|
+
|
|
242
|
+
// Save to /tmp
|
|
243
|
+
const filename = `critique-${timestamp}-${chunkIndex + 1}.${format}`
|
|
244
|
+
const filepath = join(tmpdir(), filename)
|
|
245
|
+
fs.writeFileSync(filepath, imageBuffer)
|
|
246
|
+
paths.push(filepath)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
images,
|
|
251
|
+
paths,
|
|
252
|
+
totalLines: lines.length,
|
|
253
|
+
imageCount: chunks.length,
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Capture PTY output and render to images.
|
|
259
|
+
* Similar to captureToHtml but outputs images instead.
|
|
260
|
+
*/
|
|
261
|
+
export async function captureToImages(
|
|
262
|
+
renderCommand: string[],
|
|
263
|
+
options: RenderToImagesOptions & { themeName: string }
|
|
264
|
+
): Promise<RenderResult> {
|
|
265
|
+
const decoder = new TextDecoder()
|
|
266
|
+
let ansiOutput = ""
|
|
267
|
+
|
|
268
|
+
const cols = options.cols ?? 120
|
|
269
|
+
const rows = options.rows ?? 10000
|
|
270
|
+
|
|
271
|
+
// Use Bun.Terminal for real PTY support
|
|
272
|
+
const proc = Bun.spawn(["bun", ...renderCommand], {
|
|
273
|
+
cwd: process.cwd(),
|
|
274
|
+
env: {
|
|
275
|
+
...process.env,
|
|
276
|
+
TERM: "xterm-256color",
|
|
277
|
+
},
|
|
278
|
+
terminal: {
|
|
279
|
+
cols,
|
|
280
|
+
rows,
|
|
281
|
+
data(terminal, data) {
|
|
282
|
+
ansiOutput += decoder.decode(data, { stream: true })
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
await proc.exited
|
|
288
|
+
|
|
289
|
+
// Close terminal and flush decoder
|
|
290
|
+
proc.terminal?.close()
|
|
291
|
+
ansiOutput += decoder.decode()
|
|
292
|
+
|
|
293
|
+
if (!ansiOutput.trim()) {
|
|
294
|
+
throw new Error("No output captured")
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Strip terminal cleanup sequences
|
|
298
|
+
const clearIdx = ansiOutput.lastIndexOf("\x1b[H\x1b[J")
|
|
299
|
+
if (clearIdx > 0) {
|
|
300
|
+
ansiOutput = ansiOutput.slice(0, clearIdx)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return renderTerminalToImages(ansiOutput, options)
|
|
304
|
+
}
|
package/src/review/acp-client.ts
CHANGED
|
@@ -462,16 +462,30 @@ IMPORTANT: Never use emojis or non-ASCII characters except for box-drawing chara
|
|
|
462
462
|
READING ORDER - The Guiding Principle
|
|
463
463
|
═══════════════════════════════════════════════════════════════════════════════
|
|
464
464
|
|
|
465
|
-
|
|
465
|
+
THINK HARD BEFORE WRITING. Before you start editing the YAML file, carefully plan the full order of all hunks. Consider:
|
|
466
|
+
- What is the main story of this change?
|
|
467
|
+
- What should the reader understand first to make everything else click?
|
|
468
|
+
- Which hunks are essential vs supporting details?
|
|
466
469
|
|
|
467
|
-
|
|
468
|
-
2. Types and interfaces (vocabulary)
|
|
469
|
-
3. Core utilities (foundational pieces)
|
|
470
|
-
4. Main implementation (reader now has context)
|
|
471
|
-
5. Integration and usage (how it connects)
|
|
472
|
-
6. Tests and docs (validation)
|
|
470
|
+
The reader reads top to bottom. You control their mental model.
|
|
473
471
|
|
|
474
|
-
|
|
472
|
+
ORDER BY CODE FLOW - Follow how the code actually executes in the app:
|
|
473
|
+
- Start with entry points (what gets called first)
|
|
474
|
+
- Then show what those call (the next layer down)
|
|
475
|
+
- Continue down to the dependencies
|
|
476
|
+
|
|
477
|
+
This creates an intuitive progression: dependants before dependencies. The reader follows the same path the code takes at runtime.
|
|
478
|
+
|
|
479
|
+
Practical ordering:
|
|
480
|
+
1. Entry points and main implementation (routes, handlers, commands - where execution starts)
|
|
481
|
+
2. Business logic (what the entry points call)
|
|
482
|
+
3. Types and interfaces (if needed to understand the above)
|
|
483
|
+
4. Integration points (how it connects to external systems)
|
|
484
|
+
5. Tests and docs (validation)
|
|
485
|
+
6. Utilities and helpers (supporting functions - reader already saw them being used)
|
|
486
|
+
7. Config/infrastructure (setup, boilerplate - least essential)
|
|
487
|
+
|
|
488
|
+
Utils, infra, and config are supporting cast - put them last. The reader already saw them referenced above and can now understand their implementation.
|
|
475
489
|
|
|
476
490
|
═══════════════════════════════════════════════════════════════════════════════
|
|
477
491
|
WHAT TO EXPLAIN - Why Over What
|
|
@@ -568,6 +582,12 @@ SPLITTING RULES - NEVER SHOW MORE THAN 10 LINES AT ONCE
|
|
|
568
582
|
CRITICAL: Never show hunks larger than 10 lines. This is the most important rule.
|
|
569
583
|
Split aggressively to reduce cognitive load. Readers absorb small chunks better.
|
|
570
584
|
|
|
585
|
+
For HEAVY LOGIC - split even one or two lines at a time:
|
|
586
|
+
- When code is dense or complex, show exactly what happens step by step
|
|
587
|
+
- Follow the same order the code executes - reader builds the mental model progressively
|
|
588
|
+
- You can interleave hunks from different files to follow the execution flow
|
|
589
|
+
- Example: show a function call, then the function it calls, then back to the caller
|
|
590
|
+
|
|
571
591
|
For ADDED FILES (new files):
|
|
572
592
|
- Split into logical parts: imports, types, then each function/method separately
|
|
573
593
|
- Each function/class method gets its own chunk with a brief description
|
|
@@ -578,6 +598,8 @@ For MODIFIED FILES:
|
|
|
578
598
|
- Split by logical boundaries (functions, concerns, before/after)
|
|
579
599
|
- Use numbered headers for sequential parts: ## 1. Parse input ## 2. Validate ## 3. Execute
|
|
580
600
|
|
|
601
|
+
You control the order. Reorder and interleave hunks freely to tell the clearest story.
|
|
602
|
+
|
|
581
603
|
Every chunk MUST have a description explaining its purpose, even just one line.
|
|
582
604
|
|
|
583
605
|
Lines use cat -n format (1-based). Use lineRange to reference specific portions of large hunks.
|