critique 0.1.109 → 0.1.116

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/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.109",
5
+ "version": "0.1.116",
6
6
  "license": "MIT",
7
7
  "private": false,
8
8
  "bin": "./src/cli.tsx",
@@ -45,6 +45,7 @@
45
45
  "marked": "^17.0.1",
46
46
  "picocolors": "^1.1.1",
47
47
  "react": "^19.2.0",
48
+ "string-dedent": "^3.0.2",
48
49
  "strip-ansi": "^7.1.2",
49
50
  "supports-color": "^10.2.2",
50
51
  "zustand": "^5.0.8"
package/src/ansi-html.ts CHANGED
@@ -4,6 +4,10 @@
4
4
 
5
5
  import { TextAttributes, rgbToHex, type RGBA } from "@opentuah/core"
6
6
  import type { CapturedFrame, CapturedLine, CapturedSpan } from "@opentuah/core"
7
+ import dedent from "string-dedent"
8
+
9
+ // Alias for syntax highlighting in editors (tagged template behaves identically)
10
+ const html = dedent
7
11
 
8
12
  export interface ToHtmlOptions {
9
13
  /** Background color for the container */
@@ -20,6 +24,14 @@ export interface ToHtmlOptions {
20
24
  title?: string
21
25
  /** OG image URL for social media previews */
22
26
  ogImageUrl?: string
27
+ /** Custom line renderer - wraps or replaces the default <div class="line"> output per line.
28
+ * Generic hook: receives the default HTML, the captured line data, and the 0-based line index.
29
+ * Return a replacement HTML string. If not provided, the default <div class="line"> is used. */
30
+ renderLine?: (defaultHtml: string, line: CapturedLine, lineIndex: number) => string
31
+ /** Extra CSS injected into the document style block */
32
+ extraCss?: string
33
+ /** Extra JS injected as a separate script block before </body> */
34
+ extraJs?: string
23
35
  }
24
36
 
25
37
  /**
@@ -128,11 +140,14 @@ export function frameToHtml(frame: CapturedFrame, options: ToHtmlOptions = {}):
128
140
  }
129
141
 
130
142
  // Render each line as a div
131
- const htmlLines = lines.map((line) => {
143
+ const htmlLines = lines.map((line, lineIndex) => {
132
144
  const content = lineToHtml(line)
133
145
  // Use a div for each line to ensure proper line breaks
134
146
  // Empty lines get a span with nbsp for consistent flex behavior
135
- return `<div class="line">${content || "<span>&nbsp;</span>"}</div>`
147
+ const defaultHtml = `<div class="line">${content || "<span>&nbsp;</span>"}</div>`
148
+ return options.renderLine
149
+ ? options.renderLine(defaultHtml, line, lineIndex)
150
+ : defaultHtml
136
151
  })
137
152
 
138
153
  return htmlLines.join("\n")
@@ -152,115 +167,160 @@ export function frameToHtmlDocument(frame: CapturedFrame, options: ToHtmlOptions
152
167
  } = options
153
168
 
154
169
  const cols = frame.cols
155
-
156
170
  const content = frameToHtml(frame, options)
157
171
 
158
- // Build OG meta tags
159
- const ogTags = options.ogImageUrl ? `
160
- <meta property="og:title" content="${escapeHtml(title)}">
161
- <meta property="og:type" content="website">
162
- <meta property="og:image" content="${escapeHtml(options.ogImageUrl)}">
163
- <meta property="og:image:width" content="1200">
164
- <meta property="og:image:height" content="630">
165
- <meta name="twitter:card" content="summary_large_image">
166
- <meta name="twitter:title" content="${escapeHtml(title)}">
167
- <meta name="twitter:image" content="${escapeHtml(options.ogImageUrl)}">` : ''
172
+ const ogTags = options.ogImageUrl ? '\n' + html`
173
+ <meta property="og:title" content="${escapeHtml(title)}">
174
+ <meta property="og:type" content="website">
175
+ <meta property="og:image" content="${escapeHtml(options.ogImageUrl)}">
176
+ <meta property="og:image:width" content="1200">
177
+ <meta property="og:image:height" content="630">
178
+ <meta name="twitter:card" content="summary_large_image">
179
+ <meta name="twitter:title" content="${escapeHtml(title)}">
180
+ <meta name="twitter:image" content="${escapeHtml(options.ogImageUrl)}">
181
+ ` : ''
168
182
 
169
- return `<!DOCTYPE html>
170
- <html>
171
- <head>
172
- <meta charset="utf-8">
173
- <meta name="viewport" content="width=device-width, initial-scale=1">
174
- <link rel="icon" href="/favicon-dark.png" media="(prefers-color-scheme: dark)">
175
- <link rel="icon" href="/favicon-light.png" media="(prefers-color-scheme: light)">
176
- <link rel="icon" href="/favicon-dark.png">${ogTags}
177
- <style>
178
- @font-face {
179
- font-family: 'JetBrains Mono Nerd';
180
- src: url('https://critique.work/jetbrains-mono-nerd.woff2') format('woff2');
181
- font-weight: normal;
182
- font-style: normal;
183
- font-display: swap;
184
- }
185
- </style>
186
- <title>${escapeHtml(title)}</title>
187
- <style>
188
- * { margin: 0; padding: 0; box-sizing: border-box; }
189
- html {
190
- -webkit-text-size-adjust: 100%;
191
- text-size-adjust: 100%;
192
- }
193
- html, body {
194
- min-height: 100%;
195
- background-color: ${backgroundColor};
196
- color: ${textColor};
197
- font-family: ${fontFamily};
198
- /*
199
- * Font size scales to fit ${cols} columns within viewport.
200
- * Formula: (viewport - padding) / (cols * char-ratio)
201
- *
202
- * The 0.6 char-ratio is the approximate width of 1ch relative to font-size
203
- * in monospace fonts. Most monospace fonts (JetBrains Mono, Fira Code,
204
- * Monaco, Consolas) have a ch/font-size ratio between 0.55-0.6.
205
- * We use 0.6 as a safe upper bound to prevent overflow.
206
- */
207
- font-size: clamp(4px, calc((100vw - 32px) / (${cols} * 0.6)), 14px);
208
- line-height: 1.7;
209
- }
210
- body {
211
- padding: 16px;
212
- overflow-x: clip;
213
- overflow-y: auto;
214
- max-width: 100vw;
215
- }
216
- #content {
217
- width: fit-content;
218
- margin: 0 auto;
219
- }
220
- .line {
221
- white-space: pre;
222
- display: block;
223
- content-visibility: auto;
224
- contain-intrinsic-block-size: auto round(down, 1.7em, 1px);
225
- background-color: ${backgroundColor};
226
- transform: translateZ(0);
227
- backface-visibility: hidden;
228
- }
229
- .line span {
230
- white-space: pre;
231
- display: inline-block;
232
- line-height: 1.7;
233
- vertical-align: top;
234
- }
235
- /* Disable content-visibility on iOS Safari where it can cause rendering issues */
236
- @supports (-webkit-touch-callout: none) {
237
- .line {
238
- content-visibility: visible;
239
- }
240
- }
241
- ${options.autoTheme ? `@media (prefers-color-scheme: light) {
242
- html {
243
- filter: invert(1) hue-rotate(180deg);
244
- }
245
- }` : ''}\nhtml{scrollbar-width:thin;scrollbar-color:#6b7280 #2d3748;}@media(prefers-color-scheme:light){html{scrollbar-color:#a0aec0 #edf2f7;}}::-webkit-scrollbar{width:12px;}::-webkit-scrollbar-track{background:#2d3748;}::-webkit-scrollbar-thumb{background:#6b7280;border-radius:6px;}::-webkit-scrollbar-thumb:hover{background:#a0aec0;}@media(prefers-color-scheme:light){::-webkit-scrollbar-track{background:#edf2f7;}::-webkit-scrollbar-thumb{background:#a0aec0;}::-webkit-scrollbar-thumb:hover{background:#cbd5e1;}}::-webkit-scrollbar {\n width: 12px;\n}\n::-webkit-scrollbar-track {\n background: #2d3748;\n}\n::-webkit-scrollbar-thumb {\n background: #6b7280;\n border-radius: 6px;\n}\n::-webkit-scrollbar-thumb:hover {\n background: #a0aec0;\n}\n@media (prefers-color-scheme: light) {\n ::-webkit-scrollbar-track {\n background: #edf2f7;\n }\n ::-webkit-scrollbar-thumb {\n background: #a0aec0;\n }\n ::-webkit-scrollbar-thumb:hover {\n background: #cbd5e1;\n }\n}\n</style>\n</head>\n<body>
246
- <div id="content">
247
- ${content}
248
- </div>
249
- <script>
250
- // Redirect mobile devices to ?v=mobile for optimized view
251
- (function() {
252
- const params = new URLSearchParams(window.location.search);
253
- if (!params.has('v')) {
254
- const isMobile = /Mobile|iP(hone|od|ad)|Android|BlackBerry|IEMobile|Kindle|Opera M(obi|ini)|Windows Phone|webOS/i.test(navigator.userAgent);
255
- if (isMobile) {
256
- params.set('v', 'mobile');
257
- window.location.replace(window.location.pathname + '?' + params.toString());
183
+ const autoThemeCss = options.autoTheme ? '\n' + html`
184
+ @media (prefers-color-scheme: light) {
185
+ html {
186
+ filter: invert(1) hue-rotate(180deg);
187
+ }
258
188
  }
259
- }
260
- })();
261
- </script>
262
- </body>
263
- </html>`
189
+ ` : ''
190
+
191
+ const extraJsBlock = options.extraJs
192
+ ? `\n<script>\n${options.extraJs}\n</script>`
193
+ : ''
194
+
195
+ return html`
196
+ <!DOCTYPE html>
197
+ <html>
198
+ <head>
199
+ <meta charset="utf-8">
200
+ <meta name="viewport" content="width=device-width, initial-scale=1">
201
+ <link rel="icon" href="/favicon-dark.png" media="(prefers-color-scheme: dark)">
202
+ <link rel="icon" href="/favicon-light.png" media="(prefers-color-scheme: light)">
203
+ <link rel="icon" href="/favicon-dark.png">${ogTags}
204
+ <style>
205
+ @font-face {
206
+ font-family: 'JetBrains Mono Nerd';
207
+ src: url('https://critique.work/jetbrains-mono-nerd.woff2') format('woff2');
208
+ font-weight: normal;
209
+ font-style: normal;
210
+ font-display: swap;
211
+ }
212
+ </style>
213
+ <title>${escapeHtml(title)}</title>
214
+ <style>
215
+ * { margin: 0; padding: 0; box-sizing: border-box; }
216
+ html {
217
+ -webkit-text-size-adjust: 100%;
218
+ text-size-adjust: 100%;
219
+ }
220
+ html, body {
221
+ min-height: 100%;
222
+ background-color: ${backgroundColor};
223
+ color: ${textColor};
224
+ font-family: ${fontFamily};
225
+ /*
226
+ * Font size scales to fit ${cols} columns within viewport.
227
+ * Formula: (viewport - padding) / (cols * char-ratio)
228
+ *
229
+ * The 0.6 char-ratio is the approximate width of 1ch relative to font-size
230
+ * in monospace fonts. Most monospace fonts (JetBrains Mono, Fira Code,
231
+ * Monaco, Consolas) have a ch/font-size ratio between 0.55-0.6.
232
+ * We use 0.6 as a safe upper bound to prevent overflow.
233
+ */
234
+ font-size: clamp(4px, calc((100vw - 32px) / (${cols} * 0.6)), 14px);
235
+ line-height: 1.7;
236
+ }
237
+ body {
238
+ padding: 16px;
239
+ overflow-x: clip;
240
+ overflow-y: auto;
241
+ max-width: 100vw;
242
+ }
243
+ #content {
244
+ width: fit-content;
245
+ margin: 0 auto;
246
+ }
247
+ .line {
248
+ white-space: pre;
249
+ display: block;
250
+ content-visibility: auto;
251
+ contain-intrinsic-block-size: auto round(down, 1.7em, 1px);
252
+ background-color: ${backgroundColor};
253
+ transform: translateZ(0);
254
+ backface-visibility: hidden;
255
+ }
256
+ .line span {
257
+ white-space: pre;
258
+ display: inline-block;
259
+ line-height: 1.7;
260
+ vertical-align: top;
261
+ }
262
+ /* Disable content-visibility on iOS Safari where it can cause rendering issues */
263
+ @supports (-webkit-touch-callout: none) {
264
+ .line {
265
+ content-visibility: visible;
266
+ }
267
+ }${autoThemeCss}
268
+ html {
269
+ scrollbar-width: thin;
270
+ scrollbar-color: #6b7280 #2d3748;
271
+ }
272
+ @media (prefers-color-scheme: light) {
273
+ html {
274
+ scrollbar-color: #a0aec0 #edf2f7;
275
+ }
276
+ }
277
+ ::-webkit-scrollbar {
278
+ width: 12px;
279
+ }
280
+ ::-webkit-scrollbar-track {
281
+ background: #2d3748;
282
+ }
283
+ ::-webkit-scrollbar-thumb {
284
+ background: #6b7280;
285
+ border-radius: 6px;
286
+ }
287
+ ::-webkit-scrollbar-thumb:hover {
288
+ background: #a0aec0;
289
+ }
290
+ @media (prefers-color-scheme: light) {
291
+ ::-webkit-scrollbar-track {
292
+ background: #edf2f7;
293
+ }
294
+ ::-webkit-scrollbar-thumb {
295
+ background: #a0aec0;
296
+ }
297
+ ::-webkit-scrollbar-thumb:hover {
298
+ background: #cbd5e1;
299
+ }
300
+ }
301
+ ${options.extraCss || ''}
302
+ </style>
303
+ </head>
304
+ <body>
305
+ <div id="content">
306
+ ${content}
307
+ </div>
308
+ <script>
309
+ // Redirect mobile devices to ?v=mobile for optimized view
310
+ (function() {
311
+ const params = new URLSearchParams(window.location.search);
312
+ if (!params.has('v')) {
313
+ const isMobile = /Mobile|iP(hone|od|ad)|Android|BlackBerry|IEMobile|Kindle|Opera M(obi|ini)|Windows Phone|webOS/i.test(navigator.userAgent);
314
+ if (isMobile) {
315
+ params.set('v', 'mobile');
316
+ window.location.replace(window.location.pathname + '?' + params.toString() + window.location.hash);
317
+ }
318
+ }
319
+ })();
320
+ </script>${extraJsBlock}
321
+ </body>
322
+ </html>
323
+ `
264
324
  }
265
325
 
266
326
  export type { CapturedFrame, CapturedLine, CapturedSpan }
@@ -99,6 +99,20 @@ describe("countDelimiter", () => {
99
99
  expect(countDelimiter("x = 'hello'\ny = 'world'", "'''")).toBe(0)
100
100
  })
101
101
  })
102
+
103
+ describe("triple backticks (Markdown)", () => {
104
+ it("counts fenced code block markers", () => {
105
+ expect(countDelimiter("```ts\nconst x = 1\n```", "```")).toBe(2)
106
+ })
107
+
108
+ it("counts single fence marker (unclosed code block)", () => {
109
+ expect(countDelimiter("still inside fence\n```", "```")).toBe(1)
110
+ })
111
+
112
+ it("returns 0 for plain markdown without fences", () => {
113
+ expect(countDelimiter("# Title\n\nSome text with `inline` code", "```")).toBe(0)
114
+ })
115
+ })
102
116
  })
103
117
 
104
118
  // ============================================================================
@@ -334,6 +348,46 @@ describe("balanceDelimiters", () => {
334
348
  })
335
349
  })
336
350
 
351
+ describe("markdown", () => {
352
+ const mdPatch = (hunkLines: string[]) => [
353
+ "--- file.md",
354
+ "+++ file.md",
355
+ "@@ -10,4 +10,4 @@",
356
+ ...hunkLines,
357
+ ].join("\n")
358
+
359
+ it("returns patch unchanged when code fences are balanced", () => {
360
+ const patch = mdPatch([
361
+ " ```ts",
362
+ " const x = 1",
363
+ " ```",
364
+ "+New paragraph",
365
+ ])
366
+ expect(balanceDelimiters(patch, "markdown")).toBe(patch)
367
+ })
368
+
369
+ it("prepends balancing code fence when count is odd", () => {
370
+ const patch = mdPatch([
371
+ " inside fenced block",
372
+ " ```",
373
+ "-old line",
374
+ "+new line",
375
+ ])
376
+ const result = balanceDelimiters(patch, "markdown")
377
+ const lines = result.split("\n")
378
+ expect(lines[3]).toBe(" ```inside fenced block")
379
+ })
380
+
381
+ it("does not modify when only inline code backticks are present", () => {
382
+ const patch = mdPatch([
383
+ " This has `inline` code",
384
+ "-old",
385
+ "+new",
386
+ ])
387
+ expect(balanceDelimiters(patch, "markdown")).toBe(patch)
388
+ })
389
+ })
390
+
337
391
  describe("scala", () => {
338
392
  const scalaPatch = (hunkLines: string[]) => [
339
393
  "--- file.scala",
@@ -1,8 +1,9 @@
1
1
  // Delimiter balancing for syntax highlighting in diff hunks.
2
2
  //
3
3
  // When a diff hunk starts inside a paired delimiter (template literal,
4
- // triple-quoted string, etc.), tree-sitter sees an odd number of that
5
- // delimiter and misparses everything after the first occurrence.
4
+ // triple-quoted string, fenced code block, etc.), tree-sitter sees an
5
+ // odd number of that delimiter and misparses everything after the first
6
+ // occurrence.
6
7
  //
7
8
  // Two-pass fix:
8
9
  // 1. Tokenizer: count delimiter occurrences in each hunk's content,
@@ -27,6 +28,7 @@
27
28
  const LANGUAGE_DELIMITERS: Record<string, string[]> = {
28
29
  typescript: ["`"],
29
30
  python: ['"""', "'''"],
31
+ markdown: ["```"],
30
32
  go: ["`"],
31
33
  scala: ['"""'],
32
34
  swift: ['"""'],
package/src/cli.tsx CHANGED
@@ -43,6 +43,7 @@ import { logger } from "./logger.ts";
43
43
  import { saveStoredLicenseKey } from "./license.ts";
44
44
  import {
45
45
  buildGitCommand,
46
+ filterParsedFilesByPatterns,
46
47
  getFileName,
47
48
  getFileStatus,
48
49
  getOldFileName,
@@ -54,6 +55,7 @@ import {
54
55
  parseGitDiffFiles,
55
56
  getDirtySubmodulePaths,
56
57
  buildSubmoduleDiffCommand,
58
+ getFilterPatterns,
57
59
  IGNORED_FILES,
58
60
  type ParsedFile,
59
61
  type GitCommandOptions,
@@ -136,7 +138,9 @@ async function runReviewMode(
136
138
  if (reviewOptions?.isDefaultMode) {
137
139
  const dirtySubmodules = getDirtySubmodulePaths();
138
140
  if (dirtySubmodules.length > 0) {
139
- const subCmd = buildSubmoduleDiffCommand(dirtySubmodules, reviewOptions.diffOptions || {});
141
+ const subCmd = buildSubmoduleDiffCommand(dirtySubmodules, {
142
+ context: reviewOptions.diffOptions?.context,
143
+ });
140
144
  try {
141
145
  const { stdout: subDiff } = await execAsync(subCmd, { encoding: "utf-8" });
142
146
  if (subDiff.trim()) {
@@ -146,6 +150,8 @@ async function runReviewMode(
146
150
  // Submodule diff failed — skip
147
151
  }
148
152
  }
153
+
154
+ fullDiff = await filterCombinedDiffByPatterns(fullDiff, reviewOptions.diffOptions || {});
149
155
  }
150
156
  const gitDiffResult = fullDiff;
151
157
 
@@ -1404,6 +1410,22 @@ class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundarySta
1404
1410
 
1405
1411
  const execAsync = promisify(exec);
1406
1412
 
1413
+ async function filterCombinedDiffByPatterns(
1414
+ diffContent: string,
1415
+ options: Pick<GitCommandOptions, "filter" | "positionalFilters">,
1416
+ ): Promise<string> {
1417
+ if (!diffContent.trim()) return diffContent;
1418
+ if (getFilterPatterns(options).length === 0) return diffContent;
1419
+
1420
+ const { parsePatch, formatPatch } = await import("diff");
1421
+ const parsedFiles = parseGitDiffFiles(stripSubmoduleHeaders(diffContent), parsePatch);
1422
+ const filteredFiles = filterParsedFilesByPatterns(parsedFiles, options);
1423
+
1424
+ if (filteredFiles.length === 0) return "";
1425
+
1426
+ return filteredFiles.map((file) => formatPatch(file)).join("\n");
1427
+ }
1428
+
1407
1429
  function formatPreviewExpiry(expiresInDays?: number | null): string {
1408
1430
  if (expiresInDays === null) {
1409
1431
  return "(never expires)";
@@ -1822,9 +1844,7 @@ cli
1822
1844
  if (!options.staged) {
1823
1845
  const dirtySubmodules = getDirtySubmodulePaths();
1824
1846
  if (dirtySubmodules.length > 0) {
1825
- const subCmd = buildSubmoduleDiffCommand(dirtySubmodules, {
1826
- filter: options.filter,
1827
- });
1847
+ const subCmd = buildSubmoduleDiffCommand(dirtySubmodules, {});
1828
1848
  try {
1829
1849
  const { stdout: subDiff } = await execAsync(subCmd, { encoding: "utf-8" });
1830
1850
  if (subDiff.trim()) {
@@ -1834,6 +1854,11 @@ cli
1834
1854
  // Submodule diff failed — skip
1835
1855
  }
1836
1856
  }
1857
+
1858
+ fullHunksDiff = await filterCombinedDiffByPatterns(fullHunksDiff, {
1859
+ filter: options.filter,
1860
+ positionalFilters: options['--'],
1861
+ });
1837
1862
  }
1838
1863
 
1839
1864
  if (!fullHunksDiff.trim()) {
@@ -1920,9 +1945,7 @@ cli
1920
1945
  let fullHunksDiff = gitDiff;
1921
1946
  const dirtySubmodules = getDirtySubmodulePaths();
1922
1947
  if (dirtySubmodules.length > 0) {
1923
- const subCmd = buildSubmoduleDiffCommand(dirtySubmodules, {
1924
- filter: filename,
1925
- });
1948
+ const subCmd = buildSubmoduleDiffCommand(dirtySubmodules, {});
1926
1949
  try {
1927
1950
  const { stdout: subDiff } = await execAsync(subCmd, {
1928
1951
  encoding: "utf-8",
@@ -1935,6 +1958,10 @@ cli
1935
1958
  }
1936
1959
  }
1937
1960
 
1961
+ fullHunksDiff = await filterCombinedDiffByPatterns(fullHunksDiff, {
1962
+ filter: filename,
1963
+ });
1964
+
1938
1965
  if (!fullHunksDiff.trim()) {
1939
1966
  console.error(`No unstaged changes in file: ${filename}`);
1940
1967
  process.exit(1);
@@ -2050,8 +2077,6 @@ cli
2050
2077
  if (dirtySubmodules.length > 0) {
2051
2078
  const subCmd = buildSubmoduleDiffCommand(dirtySubmodules, {
2052
2079
  context: options.context,
2053
- filter: options.filter,
2054
- positionalFilters: options['--'],
2055
2080
  });
2056
2081
  try {
2057
2082
  const { stdout: subDiff } = await execAsync(subCmd, {
@@ -2064,6 +2089,11 @@ cli
2064
2089
  // Submodule diff failed (e.g. submodule not initialized) — skip
2065
2090
  }
2066
2091
  }
2092
+
2093
+ diffContent = await filterCombinedDiffByPatterns(diffContent, {
2094
+ filter: options.filter,
2095
+ positionalFilters: options['--'],
2096
+ });
2067
2097
  }
2068
2098
  }
2069
2099
 
@@ -2190,8 +2220,6 @@ cli
2190
2220
  if (dirtySubmodules.length > 0) {
2191
2221
  const subCmd = buildSubmoduleDiffCommand(dirtySubmodules, {
2192
2222
  context: options.context,
2193
- filter: options.filter,
2194
- positionalFilters: options['--'],
2195
2223
  });
2196
2224
  try {
2197
2225
  const { stdout: subDiff } = await execAsync(subCmd, {
@@ -2212,7 +2240,13 @@ cli
2212
2240
  }
2213
2241
 
2214
2242
  const files = parseGitDiffFiles(stripSubmoduleHeaders(fullDiff), parsePatch);
2215
- const processedFiles = processFiles(files, formatPatch);
2243
+ const filteredFiles = isDefaultMode
2244
+ ? filterParsedFilesByPatterns(files, {
2245
+ filter: options.filter,
2246
+ positionalFilters: options['--'],
2247
+ })
2248
+ : files;
2249
+ const processedFiles = processFiles(filteredFiles, formatPatch);
2216
2250
  setParsedFiles(processedFiles);
2217
2251
  } catch (error) {
2218
2252
  setParsedFiles([]);
@@ -2680,8 +2714,6 @@ cli
2680
2714
  if (dirtySubmodules.length > 0) {
2681
2715
  const subCmd = buildSubmoduleDiffCommand(dirtySubmodules, {
2682
2716
  context: options.context,
2683
- filter: options.filter,
2684
- positionalFilters: options['--'],
2685
2717
  });
2686
2718
  try {
2687
2719
  const { stdout: subDiff } = await execAsync(subCmd, { encoding: "utf-8" });
@@ -2692,6 +2724,11 @@ cli
2692
2724
  // Submodule diff failed — skip
2693
2725
  }
2694
2726
  }
2727
+
2728
+ fullWebDiff = await filterCombinedDiffByPatterns(fullWebDiff, {
2729
+ filter: options.filter,
2730
+ positionalFilters: options['--'],
2731
+ });
2695
2732
  }
2696
2733
 
2697
2734
  if (!fullWebDiff.trim()) {