@abelfubu/dv 0.1.0

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.
Files changed (118) hide show
  1. package/dist/ansi-html.d.ts +42 -0
  2. package/dist/ansi-html.d.ts.map +1 -0
  3. package/dist/ansi-html.js +327 -0
  4. package/dist/ansi-output.d.ts +22 -0
  5. package/dist/ansi-output.d.ts.map +1 -0
  6. package/dist/ansi-output.js +154 -0
  7. package/dist/balance-delimiters.d.ts +25 -0
  8. package/dist/balance-delimiters.d.ts.map +1 -0
  9. package/dist/balance-delimiters.js +539 -0
  10. package/dist/balance-delimiters.test.d.ts +2 -0
  11. package/dist/balance-delimiters.test.d.ts.map +1 -0
  12. package/dist/balance-delimiters.test.js +1029 -0
  13. package/dist/cli-copy-notification.test.d.ts +2 -0
  14. package/dist/cli-copy-notification.test.d.ts.map +1 -0
  15. package/dist/cli-copy-notification.test.js +80 -0
  16. package/dist/cli-scroll.test.d.ts +2 -0
  17. package/dist/cli-scroll.test.d.ts.map +1 -0
  18. package/dist/cli-scroll.test.js +283 -0
  19. package/dist/cli.d.ts +9 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +976 -0
  22. package/dist/clipboard.d.ts +16 -0
  23. package/dist/clipboard.d.ts.map +1 -0
  24. package/dist/clipboard.js +128 -0
  25. package/dist/components/diff-view.d.ts +32 -0
  26. package/dist/components/diff-view.d.ts.map +1 -0
  27. package/dist/components/diff-view.js +123 -0
  28. package/dist/components/diff-view.test.d.ts +5 -0
  29. package/dist/components/diff-view.test.d.ts.map +1 -0
  30. package/dist/components/diff-view.test.js +312 -0
  31. package/dist/components/directory-tree-view.d.ts +33 -0
  32. package/dist/components/directory-tree-view.d.ts.map +1 -0
  33. package/dist/components/directory-tree-view.js +262 -0
  34. package/dist/components/index.d.ts +4 -0
  35. package/dist/components/index.d.ts.map +1 -0
  36. package/dist/components/index.js +5 -0
  37. package/dist/components/toast.d.ts +21 -0
  38. package/dist/components/toast.d.ts.map +1 -0
  39. package/dist/components/toast.js +47 -0
  40. package/dist/diff-cursor-utils.d.ts +20 -0
  41. package/dist/diff-cursor-utils.d.ts.map +1 -0
  42. package/dist/diff-cursor-utils.js +105 -0
  43. package/dist/diff-cursor-utils.test.d.ts +2 -0
  44. package/dist/diff-cursor-utils.test.d.ts.map +1 -0
  45. package/dist/diff-cursor-utils.test.js +40 -0
  46. package/dist/diff-surface-copy.d.ts +23 -0
  47. package/dist/diff-surface-copy.d.ts.map +1 -0
  48. package/dist/diff-surface-copy.js +64 -0
  49. package/dist/diff-surface-copy.test.d.ts +5 -0
  50. package/dist/diff-surface-copy.test.d.ts.map +1 -0
  51. package/dist/diff-surface-copy.test.js +142 -0
  52. package/dist/diff-utils.d.ts +196 -0
  53. package/dist/diff-utils.d.ts.map +1 -0
  54. package/dist/diff-utils.js +682 -0
  55. package/dist/diff-utils.test.d.ts +2 -0
  56. package/dist/diff-utils.test.d.ts.map +1 -0
  57. package/dist/diff-utils.test.js +727 -0
  58. package/dist/directory-tree.d.ts +72 -0
  59. package/dist/directory-tree.d.ts.map +1 -0
  60. package/dist/directory-tree.js +161 -0
  61. package/dist/directory-tree.test.d.ts +2 -0
  62. package/dist/directory-tree.test.d.ts.map +1 -0
  63. package/dist/directory-tree.test.js +383 -0
  64. package/dist/dropdown.d.ts +26 -0
  65. package/dist/dropdown.d.ts.map +1 -0
  66. package/dist/dropdown.js +172 -0
  67. package/dist/dropdown.test.d.ts +2 -0
  68. package/dist/dropdown.test.d.ts.map +1 -0
  69. package/dist/dropdown.test.js +106 -0
  70. package/dist/filter-submodule.e2e.test.d.ts +2 -0
  71. package/dist/filter-submodule.e2e.test.d.ts.map +1 -0
  72. package/dist/filter-submodule.e2e.test.js +109 -0
  73. package/dist/hooks/use-copy-selection.d.ts +29 -0
  74. package/dist/hooks/use-copy-selection.d.ts.map +1 -0
  75. package/dist/hooks/use-copy-selection.js +46 -0
  76. package/dist/kv-codec.d.ts +16 -0
  77. package/dist/kv-codec.d.ts.map +1 -0
  78. package/dist/kv-codec.js +36 -0
  79. package/dist/license.d.ts +14 -0
  80. package/dist/license.d.ts.map +1 -0
  81. package/dist/license.js +63 -0
  82. package/dist/logger.d.ts +9 -0
  83. package/dist/logger.d.ts.map +1 -0
  84. package/dist/logger.js +78 -0
  85. package/dist/monochrome.d.ts +34 -0
  86. package/dist/monochrome.d.ts.map +1 -0
  87. package/dist/monochrome.js +613 -0
  88. package/dist/monotone.d.ts +22 -0
  89. package/dist/monotone.d.ts.map +1 -0
  90. package/dist/monotone.js +185 -0
  91. package/dist/parsers-config.d.ts +19 -0
  92. package/dist/parsers-config.d.ts.map +1 -0
  93. package/dist/parsers-config.js +271 -0
  94. package/dist/patch-terminal-dimensions.d.ts +2 -0
  95. package/dist/patch-terminal-dimensions.d.ts.map +1 -0
  96. package/dist/patch-terminal-dimensions.js +45 -0
  97. package/dist/stdin-pager.test.d.ts +2 -0
  98. package/dist/stdin-pager.test.d.ts.map +1 -0
  99. package/dist/stdin-pager.test.js +497 -0
  100. package/dist/store.d.ts +16 -0
  101. package/dist/store.d.ts.map +1 -0
  102. package/dist/store.js +48 -0
  103. package/dist/themes/github.json +247 -0
  104. package/dist/themes.d.ts +59 -0
  105. package/dist/themes.d.ts.map +1 -0
  106. package/dist/themes.js +248 -0
  107. package/dist/tree-icons.d.ts +4 -0
  108. package/dist/tree-icons.d.ts.map +1 -0
  109. package/dist/tree-icons.js +18 -0
  110. package/dist/utils.d.ts +2 -0
  111. package/dist/utils.d.ts.map +1 -0
  112. package/dist/utils.js +13 -0
  113. package/dist/web-utils.d.ts +56 -0
  114. package/dist/web-utils.d.ts.map +1 -0
  115. package/dist/web-utils.js +363 -0
  116. package/package.json +37 -0
  117. package/public/jetbrains-mono-nerd.ttf +0 -0
  118. package/public/jetbrains-mono-nerd.woff2 +0 -0
@@ -0,0 +1,497 @@
1
+ // Integration tests for --stdin pager mode (lazygit integration).
2
+ // Reproduces https://github.com/remorses/critique/issues/25
3
+ //
4
+ // Uses tuistory to launch critique in a PTY (exactly like lazygit does),
5
+ // pipes a real diff via stdin, and verifies the output is plain scrollback
6
+ // text — not interactive TUI escape sequences.
7
+ //
8
+ // tuistory spawns a PTY where isTTY=true, which is exactly how lazygit
9
+ // runs its pager (via github.com/creack/pty). This makes the test
10
+ // realistic: it catches the original bug where --stdin + TTY incorrectly
11
+ // entered interactive TUI mode instead of scrollback mode.
12
+ import { describe, test, expect, afterAll, beforeAll } from "bun:test";
13
+ import { launchTerminal } from "tuistory";
14
+ import fs from "fs";
15
+ import path from "path";
16
+ const TEMP_DIR = path.join(import.meta.dir, ".test-stdin-pager-tmp");
17
+ function tempFile(name, content) {
18
+ const p = path.join(TEMP_DIR, name);
19
+ fs.writeFileSync(p, content);
20
+ return p;
21
+ }
22
+ function launchCritique(diffPath, opts) {
23
+ return launchTerminal({
24
+ command: "bash",
25
+ args: ["-c", `cat "${diffPath}" | bun run src/cli.tsx --stdin`],
26
+ cols: opts?.cols ?? 100,
27
+ rows: opts?.rows ?? 30,
28
+ cwd: process.cwd(),
29
+ env: {
30
+ PATH: process.env.PATH,
31
+ HOME: process.env.HOME,
32
+ TERM: "xterm-256color",
33
+ },
34
+ });
35
+ }
36
+ // Auto-detect piped stdin (no --stdin flag).
37
+ // Spawns critique with a real pipe (not a PTY) so process.stdin.isTTY is undefined.
38
+ async function runCritiqueAutoStdin(diffContent) {
39
+ const proc = Bun.spawn({
40
+ cmd: ["bun", "run", "src/cli.tsx"],
41
+ cwd: process.cwd(),
42
+ env: {
43
+ ...process.env,
44
+ TERM: "xterm-256color",
45
+ },
46
+ stdin: "pipe",
47
+ stdout: "pipe",
48
+ stderr: "pipe",
49
+ });
50
+ const writer = proc.stdin;
51
+ writer.write(diffContent);
52
+ writer.end();
53
+ const output = new TextDecoder().decode(await Bun.readableStreamToArrayBuffer(proc.stdout));
54
+ const stderr = new TextDecoder().decode(await Bun.readableStreamToArrayBuffer(proc.stderr));
55
+ const code = await proc.exited;
56
+ if (code !== 0) {
57
+ throw new Error(`critique exited with ${code}: ${stderr}`);
58
+ }
59
+ return output;
60
+ }
61
+ // -- Sample diffs --
62
+ const SINGLE_FILE_DIFF = [
63
+ "diff --git a/src/hello.ts b/src/hello.ts",
64
+ "--- a/src/hello.ts",
65
+ "+++ b/src/hello.ts",
66
+ "@@ -1,3 +1,3 @@",
67
+ " const greeting = 'hello'",
68
+ "-console.log(greeting)",
69
+ "+console.log(greeting + ' world')",
70
+ " export default greeting",
71
+ ].join("\n");
72
+ // Empty patch — lazygit sends this when there are no changes for a file
73
+ const EMPTY_DIFF = "";
74
+ // Diff with only context lines and no actual changes (can happen with -U999)
75
+ const CONTEXT_ONLY_DIFF = [
76
+ "diff --git a/readme.md b/readme.md",
77
+ "--- a/readme.md",
78
+ "+++ b/readme.md",
79
+ "@@ -1,3 +1,3 @@",
80
+ " # My Project",
81
+ " ",
82
+ " Some description",
83
+ ].join("\n");
84
+ // Multiple files in a single diff
85
+ const MULTI_FILE_DIFF = [
86
+ "diff --git a/src/index.ts b/src/index.ts",
87
+ "--- a/src/index.ts",
88
+ "+++ b/src/index.ts",
89
+ "@@ -1,4 +1,6 @@",
90
+ " import { App } from './app'",
91
+ "+import { Logger } from './logger'",
92
+ " ",
93
+ " const app = new App()",
94
+ "+const logger = new Logger()",
95
+ " app.start()",
96
+ "diff --git a/src/logger.ts b/src/logger.ts",
97
+ "new file mode 100644",
98
+ "--- /dev/null",
99
+ "+++ b/src/logger.ts",
100
+ "@@ -0,0 +1,5 @@",
101
+ "+export class Logger {",
102
+ "+ log(msg: string) {",
103
+ "+ console.log(`[LOG] ${msg}`)",
104
+ "+ }",
105
+ "+}",
106
+ ].join("\n");
107
+ // Deletion-only diff
108
+ const DELETE_ONLY_DIFF = [
109
+ "diff --git a/src/deprecated.ts b/src/deprecated.ts",
110
+ "deleted file mode 100644",
111
+ "--- a/src/deprecated.ts",
112
+ "+++ /dev/null",
113
+ "@@ -1,4 +0,0 @@",
114
+ "-// This module is no longer needed",
115
+ "-export function oldHelper() {",
116
+ "- return 'deprecated'",
117
+ "-}",
118
+ ].join("\n");
119
+ // Addition-only diff (new file)
120
+ const NEW_FILE_DIFF = [
121
+ "diff --git a/src/utils.ts b/src/utils.ts",
122
+ "new file mode 100644",
123
+ "--- /dev/null",
124
+ "+++ b/src/utils.ts",
125
+ "@@ -0,0 +1,7 @@",
126
+ "+export function clamp(value: number, min: number, max: number): number {",
127
+ "+ return Math.min(Math.max(value, min), max)",
128
+ "+}",
129
+ "+",
130
+ "+export function identity<T>(x: T): T {",
131
+ "+ return x",
132
+ "+}",
133
+ ].join("\n");
134
+ // Large hunk with many changes
135
+ const LARGE_HUNK_DIFF = [
136
+ "diff --git a/config.json b/config.json",
137
+ "--- a/config.json",
138
+ "+++ b/config.json",
139
+ "@@ -1,9 +1,11 @@",
140
+ " {",
141
+ '- "name": "my-app",',
142
+ '+ "name": "my-awesome-app",',
143
+ '- "version": "1.0.0",',
144
+ '+ "version": "2.0.0",',
145
+ ' "description": "A sample app",',
146
+ '- "main": "index.js",',
147
+ '+ "main": "dist/index.js",',
148
+ '+ "types": "dist/index.d.ts",',
149
+ ' "scripts": {',
150
+ '- "build": "tsc"',
151
+ '+ "build": "tsc --project tsconfig.build.json",',
152
+ '+ "test": "bun test"',
153
+ " }",
154
+ " }",
155
+ ].join("\n");
156
+ // Rename diff (common in lazygit)
157
+ const RENAME_DIFF = [
158
+ "diff --git a/src/old-name.ts b/src/new-name.ts",
159
+ "similarity index 90%",
160
+ "rename from src/old-name.ts",
161
+ "rename to src/new-name.ts",
162
+ "--- a/src/old-name.ts",
163
+ "+++ b/src/new-name.ts",
164
+ "@@ -1,3 +1,3 @@",
165
+ "-export const name = 'old'",
166
+ "+export const name = 'new'",
167
+ " export const version = 1",
168
+ " export default name",
169
+ ].join("\n");
170
+ // Colored diff (lazygit uses --color=always by default, issue #28)
171
+ const COLORED_DIFF = [
172
+ "\x1B[1mdiff --git a/src/hello.ts b/src/hello.ts\x1B[m",
173
+ "\x1B[1m--- a/src/hello.ts\x1B[m",
174
+ "\x1B[1m+++ b/src/hello.ts\x1B[m",
175
+ "\x1B[36m@@ -1,3 +1,3 @@\x1B[m",
176
+ " const greeting = 'hello'",
177
+ "\x1B[31m-console.log(greeting)\x1B[m",
178
+ "\x1B[32m+console.log(greeting + ' world')\x1B[m",
179
+ " export default greeting",
180
+ ].join("\n");
181
+ // Binary file diff (lazygit shows these)
182
+ const BINARY_DIFF = [
183
+ "diff --git a/logo.png b/logo.png",
184
+ "new file mode 100644",
185
+ "Binary files /dev/null and b/logo.png differ",
186
+ ].join("\n");
187
+ describe("--stdin pager mode (lazygit issue #25)", () => {
188
+ beforeAll(() => {
189
+ fs.mkdirSync(TEMP_DIR, { recursive: true });
190
+ });
191
+ afterAll(() => {
192
+ try {
193
+ fs.rmSync(TEMP_DIR, { recursive: true });
194
+ }
195
+ catch { }
196
+ });
197
+ test("single file change", async () => {
198
+ const diffPath = tempFile("single.diff", SINGLE_FILE_DIFF);
199
+ const session = await launchCritique(diffPath);
200
+ await session.waitForText("hello", { timeout: 15000 });
201
+ const trimmed = await session.text({ trimEnd: true });
202
+ expect(trimmed).toMatchInlineSnapshot(`
203
+ "
204
+ 󰝰 src
205
+ 󰛦 hello.ts (+1,-1)
206
+
207
+
208
+ src/hello.ts +1-1
209
+
210
+ 1 const greeting = 'hello'
211
+ 2 - console.log(greeting)
212
+ 2 + console.log(greeting + ' world')
213
+ 3 export default greeting"
214
+ `);
215
+ const lines = trimmed.split("\n").filter((l) => l.trim().length > 0);
216
+ expect(lines.length).toBeGreaterThan(0);
217
+ expect(lines.length).toBeLessThan(25);
218
+ session.close();
219
+ }, 30000);
220
+ test("empty diff produces no crash", async () => {
221
+ const diffPath = tempFile("empty.diff", EMPTY_DIFF);
222
+ const session = await launchCritique(diffPath);
223
+ // Empty diff should cause critique to exit quickly.
224
+ // Wait a bit for it to process and exit.
225
+ await new Promise((r) => setTimeout(r, 5000));
226
+ const trimmed = await session.text({ trimEnd: true, immediate: true });
227
+ // Should either be empty or show an error message — not a TUI
228
+ expect(trimmed).toMatchInlineSnapshot(`
229
+ "
230
+ No changes to display"
231
+ `);
232
+ session.close();
233
+ }, 15000);
234
+ test("context-only diff (no actual changes)", async () => {
235
+ const diffPath = tempFile("context-only.diff", CONTEXT_ONLY_DIFF);
236
+ const session = await launchCritique(diffPath);
237
+ await new Promise((r) => setTimeout(r, 5000));
238
+ const trimmed = await session.text({ trimEnd: true, immediate: true });
239
+ expect(trimmed).toMatchInlineSnapshot(`
240
+ "
241
+ 󰍔 readme.md ()
242
+
243
+
244
+ readme.md +0-0
245
+
246
+ 1 # My Project
247
+ 2
248
+ 3 Some description"
249
+ `);
250
+ session.close();
251
+ }, 15000);
252
+ test("multiple files in one diff", async () => {
253
+ const diffPath = tempFile("multi.diff", MULTI_FILE_DIFF);
254
+ const session = await launchCritique(diffPath);
255
+ await session.waitForText("Logger", { timeout: 15000 });
256
+ const trimmed = await session.text({ trimEnd: true });
257
+ expect(trimmed).toMatchInlineSnapshot(`
258
+ "
259
+ 󰝰 src
260
+ 󰛦 index.ts (+2)
261
+ 󰛦 logger.ts (+5)
262
+
263
+
264
+ src/index.ts +2-0
265
+
266
+ 1 import { App } from './app'
267
+ 2 + import { Logger } from './logger'
268
+ 3
269
+ 4 const app = new App()
270
+ 5 + const logger = new Logger()
271
+ 6 app.start()
272
+
273
+
274
+ src/logger.ts +5-0
275
+
276
+ 1 + export class Logger {
277
+ 2 + log(msg: string) {
278
+ 3 + console.log(\`[LOG] \${msg}\`)
279
+ 4 + }
280
+ 5 + }"
281
+ `);
282
+ // Should contain both filenames
283
+ expect(trimmed).toContain("index.ts");
284
+ expect(trimmed).toContain("logger.ts");
285
+ // Should not show the privacy notice
286
+ expect(trimmed).not.toContain("URL is private");
287
+ const lines = trimmed.split("\n").filter((l) => l.trim().length > 0);
288
+ expect(lines.length).toBeLessThan(40);
289
+ session.close();
290
+ }, 30000);
291
+ test("deleted file", async () => {
292
+ const diffPath = tempFile("delete.diff", DELETE_ONLY_DIFF);
293
+ const session = await launchCritique(diffPath);
294
+ await session.waitForText("deprecated", { timeout: 15000 });
295
+ const trimmed = await session.text({ trimEnd: true });
296
+ expect(trimmed).toMatchInlineSnapshot(`
297
+ "
298
+ 󰝰 src
299
+ 󰛦 deprecated.ts (-4)
300
+
301
+
302
+ src/deprecated.ts +0-4
303
+
304
+ 1 - // This module is no longer needed
305
+ 2 - export function oldHelper() {
306
+ 3 - return 'deprecated'
307
+ 4 - }"
308
+ `);
309
+ expect(trimmed).toContain("deprecated");
310
+ expect(trimmed).not.toContain("URL is private");
311
+ session.close();
312
+ }, 30000);
313
+ test("new file", async () => {
314
+ const diffPath = tempFile("newfile.diff", NEW_FILE_DIFF);
315
+ const session = await launchCritique(diffPath);
316
+ await session.waitForText("clamp", { timeout: 15000 });
317
+ const trimmed = await session.text({ trimEnd: true });
318
+ expect(trimmed).toMatchInlineSnapshot(`
319
+ "
320
+ 󰝰 src
321
+ 󰛦 utils.ts (+7)
322
+
323
+
324
+ src/utils.ts +7-0
325
+
326
+ 1 + export function clamp(value: number, min: number, max: number): number {
327
+ 2 + return Math.min(Math.max(value, min), max)
328
+ 3 + }
329
+ 4 +
330
+ 5 + export function identity<T>(x: T): T {
331
+ 6 + return x
332
+ 7 + }"
333
+ `);
334
+ expect(trimmed).toContain("clamp");
335
+ expect(trimmed).toContain("identity");
336
+ expect(trimmed).not.toContain("URL is private");
337
+ session.close();
338
+ }, 30000);
339
+ test("large hunk with many changes", async () => {
340
+ const diffPath = tempFile("large.diff", LARGE_HUNK_DIFF);
341
+ const session = await launchCritique(diffPath);
342
+ await session.waitForText("config.json", { timeout: 15000 });
343
+ const trimmed = await session.text({ trimEnd: true });
344
+ expect(trimmed).toMatchInlineSnapshot(`
345
+ "
346
+ 󰘦 config.json (+6,-4)
347
+
348
+
349
+ config.json +6-4
350
+
351
+ 1 {
352
+ 2 - "name": "my-app",
353
+ 3 - "version": "1.0.0",
354
+ 2 + "name": "my-awesome-app",
355
+ 3 + "version": "2.0.0",
356
+ 4 "description": "A sample app",
357
+ 5 - "main": "index.js",
358
+ 5 + "main": "dist/index.js",
359
+ 6 + "types": "dist/index.d.ts",
360
+ 7 "scripts": {
361
+ 7 - "build": "tsc"
362
+ 8 + "build": "tsc --project tsconfig.build.json",
363
+ 9 + "test": "bun test"
364
+ 10 }
365
+ 11 }"
366
+ `);
367
+ expect(trimmed).toContain("config.json");
368
+ expect(trimmed).toContain("my-awesome-app");
369
+ expect(trimmed).not.toContain("URL is private");
370
+ session.close();
371
+ }, 30000);
372
+ test("file rename with changes", async () => {
373
+ const diffPath = tempFile("rename.diff", RENAME_DIFF);
374
+ const session = await launchCritique(diffPath);
375
+ await session.waitForText("new-name", { timeout: 15000 });
376
+ const trimmed = await session.text({ trimEnd: true });
377
+ expect(trimmed).toMatchInlineSnapshot(`
378
+ "
379
+ 󰝰 src
380
+ 󰛦 new-name.ts (+1,-1)
381
+
382
+
383
+ src/old-name.ts → src/new-name.ts +1-1
384
+
385
+ 1 - export const name = 'old'
386
+ 1 + export const name = 'new'
387
+ 2 export const version = 1
388
+ 3 export default name"
389
+ `);
390
+ // Should show the rename
391
+ expect(trimmed).toContain("old-name");
392
+ expect(trimmed).toContain("new-name");
393
+ expect(trimmed).not.toContain("URL is private");
394
+ session.close();
395
+ }, 30000);
396
+ test("binary file diff", async () => {
397
+ const diffPath = tempFile("binary.diff", BINARY_DIFF);
398
+ const session = await launchCritique(diffPath);
399
+ // Binary diffs may not render content — wait for exit or timeout
400
+ await new Promise((r) => setTimeout(r, 5000));
401
+ const trimmed = await session.text({ trimEnd: true, immediate: true });
402
+ expect(trimmed).toMatchInlineSnapshot(`
403
+ "
404
+ 󰈙 unknown ()
405
+
406
+
407
+ unknown +0-0"
408
+ `);
409
+ expect(trimmed).not.toContain("URL is private");
410
+ session.close();
411
+ }, 15000);
412
+ test("colored diff from lazygit (issue #28)", async () => {
413
+ const diffPath = tempFile("colored.diff", COLORED_DIFF);
414
+ const session = await launchCritique(diffPath);
415
+ await session.waitForText("hello", { timeout: 15000 });
416
+ const trimmed = await session.text({ trimEnd: true });
417
+ // Should strip ANSI codes and parse the diff correctly
418
+ // (not show "unknown +0-0")
419
+ expect(trimmed).toContain("hello.ts");
420
+ expect(trimmed).not.toContain("unknown");
421
+ expect(trimmed).toMatchInlineSnapshot(`
422
+ "
423
+ 󰝰 src
424
+ 󰛦 hello.ts (+1,-1)
425
+
426
+
427
+ src/hello.ts +1-1
428
+
429
+ 1 const greeting = 'hello'
430
+ 2 - console.log(greeting)
431
+ 2 + console.log(greeting + ' world')
432
+ 3 export default greeting"
433
+ `);
434
+ session.close();
435
+ }, 30000);
436
+ test("narrow terminal (40 cols) forces unified view", async () => {
437
+ const diffPath = tempFile("narrow.diff", SINGLE_FILE_DIFF);
438
+ const session = await launchCritique(diffPath, { cols: 40 });
439
+ await session.waitForText("hello", { timeout: 15000 });
440
+ const trimmed = await session.text({ trimEnd: true });
441
+ expect(trimmed).toMatchInlineSnapshot(`
442
+ "
443
+ 󰝰 src
444
+ 󰛦 hello.ts (+1,-1)
445
+
446
+
447
+ src/hello.ts +1-1
448
+
449
+ 1 const greeting = 'hello'
450
+ 2 - console.log(greeting)
451
+ 2 + console.log(greeting + ' world')
452
+ 3 export default greeting"
453
+ `);
454
+ expect(trimmed).toContain("hello");
455
+ expect(trimmed).not.toContain("URL is private");
456
+ // Every non-empty line should fit within 40 cols
457
+ const lines = trimmed.split("\n").filter((l) => l.trim().length > 0);
458
+ for (const line of lines) {
459
+ expect(line.length).toBeLessThanOrEqual(40);
460
+ }
461
+ session.close();
462
+ }, 30000);
463
+ });
464
+ describe("auto-detect piped stdin (git diff | critique)", () => {
465
+ test("single file change without --stdin flag", async () => {
466
+ const output = await runCritiqueAutoStdin(SINGLE_FILE_DIFF);
467
+ const trimmed = output.replace(/\r/g, "").trim();
468
+ expect(trimmed).toContain("hello.ts");
469
+ expect(trimmed).toContain("console.log(greeting)");
470
+ expect(trimmed).toContain("console.log(greeting + ' world')");
471
+ expect(trimmed).not.toContain("unknown");
472
+ }, 30000);
473
+ test("empty diff falls back to git repo check (errors outside repo)", async () => {
474
+ // Empty piped stdin with no --stdin should fall back to git diff.
475
+ // Since we're running this inside the critique repo which may have no
476
+ // uncommitted changes, the output should be "No changes to display".
477
+ const output = await runCritiqueAutoStdin("");
478
+ const trimmed = output.replace(/\r/g, "").trim();
479
+ expect(trimmed).toContain("No changes to display");
480
+ }, 30000);
481
+ test("multiple files in one diff without --stdin", async () => {
482
+ const output = await runCritiqueAutoStdin(MULTI_FILE_DIFF);
483
+ const trimmed = output.replace(/\r/g, "").trim();
484
+ expect(trimmed).toContain("index.ts");
485
+ expect(trimmed).toContain("logger.ts");
486
+ expect(trimmed).toContain("Logger");
487
+ expect(trimmed).not.toContain("unknown");
488
+ }, 30000);
489
+ test("colored diff auto-detected and stripped", async () => {
490
+ const output = await runCritiqueAutoStdin(COLORED_DIFF);
491
+ const trimmed = output.replace(/\r/g, "").trim();
492
+ expect(trimmed).toContain("hello.ts");
493
+ expect(trimmed).not.toContain("unknown");
494
+ // ANSI codes should be stripped
495
+ expect(trimmed).not.toContain("\x1B[");
496
+ }, 30000);
497
+ });
@@ -0,0 +1,16 @@
1
+ export interface PersistedState {
2
+ themeName?: string;
3
+ italicsEnabled?: boolean;
4
+ transparentBackground?: boolean;
5
+ }
6
+ export declare function loadPersistedState(): PersistedState;
7
+ export declare function savePersistedState(state: PersistedState): void;
8
+ declare const persistedState: PersistedState;
9
+ export interface AppState {
10
+ themeName: string;
11
+ italicsEnabled: boolean;
12
+ transparentBackground: boolean;
13
+ }
14
+ export declare const useAppStore: import("zustand").UseBoundStore<import("zustand").StoreApi<AppState>>;
15
+ export { persistedState };
16
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC;AAED,wBAAgB,kBAAkB,IAAI,cAAc,CAOnD;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAS9D;AAGD,QAAA,MAAM,cAAc,gBAAuB,CAAA;AAE3C,MAAM,WAAW,QAAQ;IAEvB,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,OAAO,CAAA;IACvB,qBAAqB,EAAE,OAAO,CAAA;CAC/B;AAED,eAAO,MAAM,WAAW,uEAIrB,CAAA;AAYH,OAAO,EAAE,cAAc,EAAE,CAAA"}
package/dist/store.js ADDED
@@ -0,0 +1,48 @@
1
+ // Global Zustand store for persistent application state.
2
+ // Manages theme selection and italics toggle with automatic persistence to ~/.config/dv/state.json.
3
+ // Shared between main diff view and review view components.
4
+ import { create } from "zustand";
5
+ import fs from "fs";
6
+ import { join } from "path";
7
+ import { homedir } from "os";
8
+ import { defaultThemeName } from "./themes.js";
9
+ // State persistence
10
+ const STATE_DIR = join(homedir(), ".config", "dv");
11
+ const STATE_FILE = join(STATE_DIR, "state.json");
12
+ export function loadPersistedState() {
13
+ try {
14
+ const data = fs.readFileSync(STATE_FILE, "utf-8");
15
+ return JSON.parse(data);
16
+ }
17
+ catch {
18
+ return {};
19
+ }
20
+ }
21
+ export function savePersistedState(state) {
22
+ try {
23
+ if (!fs.existsSync(STATE_DIR)) {
24
+ fs.mkdirSync(STATE_DIR, { recursive: true });
25
+ }
26
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state));
27
+ }
28
+ catch {
29
+ // Ignore write errors
30
+ }
31
+ }
32
+ // Load initial state
33
+ const persistedState = loadPersistedState();
34
+ export const useAppStore = create(() => ({
35
+ themeName: persistedState.themeName ?? defaultThemeName,
36
+ italicsEnabled: persistedState.italicsEnabled ?? true,
37
+ transparentBackground: persistedState.transparentBackground ?? false,
38
+ }));
39
+ // Subscribe to persist state changes
40
+ useAppStore.subscribe((state) => {
41
+ savePersistedState({
42
+ themeName: state.themeName,
43
+ italicsEnabled: state.italicsEnabled,
44
+ transparentBackground: state.transparentBackground,
45
+ });
46
+ });
47
+ // Re-export persisted state for initial reads
48
+ export { persistedState };