critique 0.1.9 → 0.1.11

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/AGENTS.md CHANGED
@@ -8,7 +8,12 @@ ALWAYS!
8
8
 
9
9
  ## bun
10
10
 
11
- NEVER run bun run index.tsx. You cannot directly run the tui app. it will hang. instead ask me to do so.
11
+ NEVER run the interactive TUI (e.g. `bun run src/cli.tsx` without arguments). It will hang. Instead ask the user to run it.
12
+
13
+ The `web` command is safe to run - it generates HTML and exits:
14
+ ```bash
15
+ bun run src/cli.tsx web
16
+ ```
12
17
 
13
18
  NEVER use require. just import at the top of the file with esm
14
19
 
package/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ # 0.1.11
2
+
3
+ - All commands:
4
+ - Add support for passing file filters as positional args after `--` (e.g. `critique web -- src/cli.tsx`)
5
+ - Web command:
6
+ - Add `--title` option for custom HTML document title
7
+ - Add scrollbar styling to HTML output (dark/light mode aware)
8
+
9
+ # 0.1.10
10
+
11
+ - Default command:
12
+ - Add `--filter <pattern>` option to filter files by glob pattern (e.g. `critique --filter 'src/**/*.ts'`)
13
+ - Web command:
14
+ - Add support for comparing two refs: `critique web <base> <head>`
15
+ - Add `--filter <pattern>` option to filter files by glob pattern
16
+ - Add auto light/dark mode based on system preference (uses CSS `prefers-color-scheme`)
17
+ - Disabled when `--theme` is specified
18
+ - Fix browser rendering for Safari/Chrome subpixel issues
19
+ - Themes:
20
+ - Change default theme to `github` (dark)
21
+ - Fix opencode theme line number contrast (was nearly invisible on dark background)
22
+
1
23
  # 0.1.9
2
24
 
3
25
  - Performance:
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A beautiful terminal UI for reviewing git diffs with syntax highlighting, split view, and word-level diff.
4
4
 
5
- ![Diff Viewer Demo](diff-viewer-demo.png)
5
+ ![Diff Viewer Demo](screenshot.png)
6
6
 
7
7
  ## Installation
8
8
 
@@ -27,16 +27,26 @@ critique
27
27
  # View staged changes
28
28
  critique --staged
29
29
 
30
+ # View the last commit (works whether pushed or unpushed)
31
+ critique HEAD
32
+
30
33
  # View a specific commit
31
34
  critique --commit HEAD~1
32
35
  critique abc1234
33
36
 
37
+ # View combined changes from last N commits
38
+ critique HEAD~3 HEAD # shows all changes from 3 commits ago to now
39
+
34
40
  # Compare two branches (PR-style, shows what head added since diverging from base)
35
41
  critique main feature-branch # what feature-branch added vs main
36
42
  critique main HEAD # what current branch added vs main
37
43
 
38
44
  # Watch mode - auto-refresh on file changes
39
45
  critique --watch
46
+
47
+ # Filter files by glob pattern (can be used multiple times)
48
+ critique --filter "src/**/*.ts"
49
+ critique --filter "src/**/*.ts" --filter "lib/**/*.js"
40
50
  ```
41
51
 
42
52
  ### Navigation
@@ -85,9 +95,15 @@ critique web
85
95
  # View staged changes
86
96
  critique web --staged
87
97
 
98
+ # View the last commit (works whether pushed or unpushed)
99
+ critique web HEAD
100
+
88
101
  # View a specific commit
89
102
  critique web --commit HEAD~1
90
103
 
104
+ # View combined changes from last N commits
105
+ critique web HEAD~3 HEAD
106
+
91
107
  # Generate local HTML file instead of uploading
92
108
  critique web --local
93
109
 
@@ -112,6 +128,7 @@ critique web --cols 100 --rows 2000
112
128
  | `--cols <n>` | Terminal width for rendering | `240` |
113
129
  | `--rows <n>` | Terminal height for rendering | `2000` |
114
130
  | `--local` | Save HTML locally instead of uploading | - |
131
+ | `--filter <pattern>` | Filter files by glob (can be used multiple times) | - |
115
132
 
116
133
  **Tips:**
117
134
 
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.9",
5
+ "version": "0.1.11",
6
6
  "private": false,
7
7
  "bin": "./src/cli.tsx",
8
8
  "scripts": {
package/screenshot.png ADDED
Binary file
package/src/ansi-html.ts CHANGED
@@ -13,6 +13,10 @@ export interface AnsiToHtmlOptions {
13
13
  fontSize?: string
14
14
  /** Trim empty lines from the end */
15
15
  trimEmptyLines?: boolean
16
+ /** Enable auto light/dark mode based on system preference */
17
+ autoTheme?: boolean
18
+ /** HTML document title */
19
+ title?: string
16
20
  }
17
21
 
18
22
  /**
@@ -127,6 +131,7 @@ export function ansiToHtmlDocument(input: string | Buffer, options: AnsiToHtmlOp
127
131
  textColor = "#1a1a1a",
128
132
  fontFamily = "Monaco, Menlo, 'Ubuntu Mono', Consolas, monospace",
129
133
  fontSize = "14px",
134
+ title = "Critique Diff",
130
135
  } = options
131
136
 
132
137
  const content = ansiToHtml(input, options)
@@ -141,7 +146,7 @@ export function ansiToHtmlDocument(input: string | Buffer, options: AnsiToHtmlOp
141
146
  <head>
142
147
  <meta charset="utf-8">
143
148
  <meta name="viewport" content="width=device-width, initial-scale=1">
144
- <title>Critique Diff</title>
149
+ <title>${escapeHtml(title)}</title>
145
150
  <style>
146
151
  * { margin: 0; padding: 0; box-sizing: border-box; }
147
152
  html {
@@ -156,7 +161,10 @@ html, body {
156
161
  color: ${textColor};
157
162
  font-family: ${fontFamily};
158
163
  font-size: ${fontSize};
159
- line-height: 1.6;
164
+ line-height: 1.5;
165
+ -webkit-font-smoothing: antialiased;
166
+ -moz-osx-font-smoothing: grayscale;
167
+ text-rendering: optimizeLegibility;
160
168
  }
161
169
  body {
162
170
  display: flex;
@@ -172,15 +180,19 @@ body {
172
180
  white-space: pre;
173
181
  display: flex;
174
182
  content-visibility: auto;
175
- contain-intrinsic-block-size: auto 1lh;
183
+ contain-intrinsic-block-size: auto 1.5em;
176
184
  background-color: ${backgroundColor};
185
+ transform: translateZ(0);
186
+ backface-visibility: hidden;
177
187
  }
178
188
  .line span {
179
189
  white-space: pre;
180
190
  }
181
- </style>
182
- </head>
183
- <body>
191
+ ${options.autoTheme ? `@media (prefers-color-scheme: light) {
192
+ html {
193
+ filter: invert(1) hue-rotate(180deg);
194
+ }
195
+ }` : ''}\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>
184
196
  <div id="content">
185
197
  ${content}
186
198
  </div>
@@ -195,7 +207,10 @@ ${content}
195
207
  function adjustFontSize() {
196
208
  const viewportWidth = window.innerWidth;
197
209
  const calculatedSize = (viewportWidth - padding) / (cols * charRatio);
198
- const fontSize = Math.max(minFontSize, Math.min(maxFontSize, calculatedSize));
210
+ // Round to nearest even integer to prevent subpixel rendering issues
211
+ // (with line-height: 1.5, even font-size always yields integer line-height)
212
+ const clamped = Math.max(minFontSize, Math.min(maxFontSize, calculatedSize));
213
+ const fontSize = Math.round(clamped / 2) * 2;
199
214
  document.body.style.fontSize = fontSize + 'px';
200
215
  }
201
216
 
package/src/cli.tsx CHANGED
@@ -562,20 +562,26 @@ cli
562
562
  .option("--commit <ref>", "Show changes from a specific commit")
563
563
  .option("--watch", "Watch for file changes and refresh diff")
564
564
  .option("--context <lines>", "Number of context lines (default: 3)")
565
+ .option("--filter <pattern>", "Filter files by glob pattern (can be used multiple times)")
565
566
  .action(async (base, head, options) => {
566
567
  try {
567
568
  const contextArg = options.context ? `-U${options.context}` : "";
569
+ // Combine --filter options with positional args after --
570
+ const filterOptions = options.filter ? (Array.isArray(options.filter) ? options.filter : [options.filter]) : [];
571
+ const positionalFilters = options['--'] || [];
572
+ const filters = [...filterOptions, ...positionalFilters];
573
+ const filterArg = filters.length > 0 ? `-- ${filters.map((f: string) => `"${f}"`).join(" ")}` : "";
568
574
  const gitCommand = (() => {
569
575
  if (options.staged)
570
- return `git diff --cached --no-prefix ${contextArg}`.trim();
576
+ return `git diff --cached --no-prefix ${contextArg} ${filterArg}`.trim();
571
577
  if (options.commit)
572
- return `git show ${options.commit} --no-prefix ${contextArg}`.trim();
578
+ return `git show ${options.commit} --no-prefix ${contextArg} ${filterArg}`.trim();
573
579
  // Two refs: compare base...head (three-dot, shows changes since branches diverged, like GitHub PRs)
574
580
  if (base && head)
575
- return `git diff ${base}...${head} --no-prefix ${contextArg}`.trim();
581
+ return `git diff ${base}...${head} --no-prefix ${contextArg} ${filterArg}`.trim();
576
582
  // Single ref: show that commit's changes
577
- if (base) return `git show ${base} --no-prefix ${contextArg}`.trim();
578
- return `git add -N . && git diff --no-prefix ${contextArg}`.trim();
583
+ if (base) return `git show ${base} --no-prefix ${contextArg} ${filterArg}`.trim();
584
+ return `git add -N . && git diff --no-prefix ${contextArg} ${filterArg}`.trim();
579
585
  })();
580
586
 
581
587
  const shouldWatch = options.watch && !base && !head && !options.commit;
@@ -1010,7 +1016,7 @@ const WORKER_URL =
1010
1016
  process.env.CRITIQUE_WORKER_URL || "https://critique.work";
1011
1017
 
1012
1018
  cli
1013
- .command("web [ref]", "Generate web preview of diff")
1019
+ .command("web [base] [head]", "Generate web preview of diff")
1014
1020
  .option("--staged", "Show staged changes")
1015
1021
  .option("--commit <ref>", "Show changes from a specific commit")
1016
1022
  .option(
@@ -1027,25 +1033,35 @@ cli
1027
1033
  .option("--open", "Open in browser after generating")
1028
1034
  .option("--context <lines>", "Number of context lines (default: 3)")
1029
1035
  .option("--theme <name>", "Theme to use for rendering")
1030
- .action(async (ref, options) => {
1036
+ .option("--filter <pattern>", "Filter files by glob pattern (can be used multiple times)")
1037
+ .option("--title <title>", "HTML document title")
1038
+ .action(async (base, head, options) => {
1031
1039
  const pty = await import("@xmorse/bun-pty");
1032
1040
  const { ansiToHtmlDocument } = await import("./ansi-html.ts");
1033
1041
 
1034
1042
  const contextArg = options.context ? `-U${options.context}` : "";
1043
+ // Combine --filter options with positional args after --
1044
+ const filterOptions = options.filter ? (Array.isArray(options.filter) ? options.filter : [options.filter]) : [];
1045
+ const positionalFilters = options['--'] || [];
1046
+ const filters = [...filterOptions, ...positionalFilters];
1047
+ const filterArg = filters.length > 0 ? `-- ${filters.map((f: string) => `"${f}"`).join(" ")}` : "";
1035
1048
  const gitCommand = (() => {
1036
1049
  if (options.staged)
1037
- return `git diff --cached --no-prefix ${contextArg}`.trim();
1050
+ return `git diff --cached --no-prefix ${contextArg} ${filterArg}`.trim();
1038
1051
  if (options.commit)
1039
- return `git show ${options.commit} --no-prefix ${contextArg}`.trim();
1040
- if (ref) return `git show ${ref} --no-prefix ${contextArg}`.trim();
1041
- return `git add -N . && git diff --no-prefix ${contextArg}`.trim();
1052
+ return `git show ${options.commit} --no-prefix ${contextArg} ${filterArg}`.trim();
1053
+ // Two refs: compare base...head (three-dot, shows changes since branches diverged, like GitHub PRs)
1054
+ if (base && head)
1055
+ return `git diff ${base}...${head} --no-prefix ${contextArg} ${filterArg}`.trim();
1056
+ // Single ref: show that commit's changes
1057
+ if (base) return `git show ${base} --no-prefix ${contextArg} ${filterArg}`.trim();
1058
+ return `git add -N . && git diff --no-prefix ${contextArg} ${filterArg}`.trim();
1042
1059
  })();
1043
1060
 
1044
1061
  const desktopCols = parseInt(options.cols) || 240;
1045
1062
  const mobileCols = parseInt(options.mobileCols) || 100;
1046
- const themeName = options.theme && themeNames.includes(options.theme)
1047
- ? options.theme
1048
- : defaultThemeName;
1063
+ const customTheme = options.theme && themeNames.includes(options.theme);
1064
+ const themeName = customTheme ? options.theme : defaultThemeName;
1049
1065
 
1050
1066
  console.log("Capturing diff output...");
1051
1067
 
@@ -1124,7 +1140,7 @@ cli
1124
1140
  const backgroundColor = rgbaToHex(theme.background);
1125
1141
  const textColor = rgbaToHex(theme.text);
1126
1142
 
1127
- return ansiToHtmlDocument(ansiOutput, { cols, rows: renderRows, backgroundColor, textColor });
1143
+ return ansiToHtmlDocument(ansiOutput, { cols, rows: renderRows, backgroundColor, textColor, autoTheme: !customTheme, title: options.title });
1128
1144
  }
1129
1145
 
1130
1146
  // Generate desktop and mobile versions in parallel
@@ -1367,5 +1383,4 @@ cli
1367
1383
 
1368
1384
  cli.help();
1369
1385
  cli.version("1.0.0");
1370
- // comment
1371
1386
  cli.parse();
@@ -138,8 +138,8 @@
138
138
  "light": "lightStep2"
139
139
  },
140
140
  "diffLineNumber": {
141
- "dark": "darkStep3",
142
- "light": "lightStep3"
141
+ "dark": "darkStep8",
142
+ "light": "lightStep8"
143
143
  },
144
144
  "diffAddedLineNumberBg": {
145
145
  "dark": "#1b2b34",
package/src/themes.ts CHANGED
@@ -237,7 +237,7 @@ export function getSyntaxTheme(
237
237
 
238
238
  export const themeNames = Object.keys(THEME_FILES).sort();
239
239
 
240
- export const defaultThemeName = "github-light";
240
+ export const defaultThemeName = "github";
241
241
 
242
242
  // Helper to convert RGBA to hex string
243
243
  export function rgbaToHex(rgba: RGBA): string {
Binary file