critique 0.1.78 → 0.1.103
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 +11 -0
- package/CHANGELOG.md +171 -0
- package/README.md +60 -4
- package/global.d.ts +5 -0
- package/package.json +5 -4
- package/parsers-config.ts +11 -1
- package/queries/json/highlights.scm +42 -0
- package/scripts/fixtures/json-syntax-example.json +13 -0
- package/scripts/preview-review.tsx +2 -2
- package/src/ansi-output.test.ts +205 -0
- package/src/ansi-output.ts +172 -0
- package/src/assets/favicon-dark-black-bg.png +0 -0
- package/src/cli-scroll.test.tsx +66 -0
- package/src/cli.tsx +487 -88
- package/src/components/diff-view.test.tsx +119 -0
- package/src/components/diff-view.tsx +11 -1
- package/src/diff-utils.ts +54 -1
- package/src/dropdown.test.tsx +126 -0
- package/src/dropdown.tsx +40 -24
- package/src/hooks/use-copy-selection.ts +1 -0
- package/src/image.test.ts +4 -4
- package/src/image.ts +13 -12
- package/src/license.ts +49 -10
- package/src/og-layout.test.ts +1 -1
- package/src/opentui-image.ts +4 -0
- package/src/review/acp-client.ts +37 -21
- package/src/review/fixtures/tool-call-response.json +2 -2
- package/src/review/hunk-parser.ts +65 -2
- package/src/review/index.ts +3 -0
- package/src/review/review-app.test.tsx +316 -213
- package/src/review/review-app.tsx +13 -5
- package/src/themes/cursor.json +35 -35
- package/src/web-utils.test.ts +10 -5
- package/src/web-utils.tsx +272 -150
- package/src/worker.tsx +62 -0
package/AGENTS.md
CHANGED
|
@@ -131,3 +131,14 @@ npx opensrc <owner>/<repo> # GitHub repo (e.g., npx opensrc vercel/ai)
|
|
|
131
131
|
```
|
|
132
132
|
|
|
133
133
|
<!-- opensrc:end -->
|
|
134
|
+
|
|
135
|
+
## opentui fork
|
|
136
|
+
|
|
137
|
+
we are using an opentui fork with name opentuah with my personal PRs merged
|
|
138
|
+
|
|
139
|
+
"@opentui/core": "npm:@opentuah/core@^0.1.80",
|
|
140
|
+
"@opentui/react": "npm:@opentuah/react@^0.1.80",
|
|
141
|
+
|
|
142
|
+
To find my opentui folder with that fork see kimaki projects via kimaki cli, the one named opentui.
|
|
143
|
+
|
|
144
|
+
To apply fixes there you must create a new branch and then merge it in the branch called opentuah. then publish and update the versions here. to publish there is a script specifically for opentuah.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,174 @@
|
|
|
1
|
+
# 0.1.103
|
|
2
|
+
|
|
3
|
+
- Syntax highlighting:
|
|
4
|
+
- JSON: render punctuation (quotes/brackets/separators) using comment color instead of operator red / full-bright
|
|
5
|
+
|
|
6
|
+
# 0.1.102
|
|
7
|
+
|
|
8
|
+
- `critique --web` / `critique web`:
|
|
9
|
+
- Render the upload notice block in a single muted color (no mixed emphasis)
|
|
10
|
+
|
|
11
|
+
# 0.1.101
|
|
12
|
+
|
|
13
|
+
- `critique` / `critique --web` / OG images:
|
|
14
|
+
- Pass `addedBg`/`removedBg` alongside content backgrounds so opentui's word-level highlights don't inherit dark defaults on light themes
|
|
15
|
+
- Keep a small `github-light`-only `addedWordBg` override to make added word highlights visible in images
|
|
16
|
+
|
|
17
|
+
# 0.1.100
|
|
18
|
+
|
|
19
|
+
- Dependencies:
|
|
20
|
+
- Update `@opentui/core` / `@opentui/react` npm aliases to `@opentuah/*@0.1.88`
|
|
21
|
+
- Remove `@opentuah/core-darwin-arm64` optional dependency (core pulls the correct platform binary via its own optional deps)
|
|
22
|
+
|
|
23
|
+
# 0.1.99
|
|
24
|
+
|
|
25
|
+
- `critique` / `critique review` / `critique web` / `critique hunks list`:
|
|
26
|
+
- Define `--filter <pattern>` as an array schema in goke so repeated flags are parsed explicitly as `string[]`
|
|
27
|
+
- `critique review`:
|
|
28
|
+
- Define `--session <id>` as an array schema so repeated session flags are handled natively
|
|
29
|
+
|
|
30
|
+
# 0.1.98
|
|
31
|
+
|
|
32
|
+
- Dependencies:
|
|
33
|
+
- Replace `@xmorse/cac` with `goke` for CLI argument parsing
|
|
34
|
+
|
|
35
|
+
# 0.1.97
|
|
36
|
+
|
|
37
|
+
- `README`:
|
|
38
|
+
- Make the `CodeRabbit` sponsor text clickable while keeping the compact logo link
|
|
39
|
+
|
|
40
|
+
# 0.1.96
|
|
41
|
+
|
|
42
|
+
- `README`:
|
|
43
|
+
- Add a bottom `Sponsors` section with a compact CodeRabbit logo link to `https://coderabbit.link/remorses`
|
|
44
|
+
|
|
45
|
+
# 0.1.95
|
|
46
|
+
|
|
47
|
+
- Syntax highlighting:
|
|
48
|
+
- `json`:
|
|
49
|
+
- Color JSON quote characters (`"`) separately as `punctuation.delimiter` while keeping string text highlighted as `property` (keys) and `string` (values)
|
|
50
|
+
|
|
51
|
+
# 0.1.94
|
|
52
|
+
|
|
53
|
+
- Syntax highlighting:
|
|
54
|
+
- Fix JSON syntax highlighting by using local query file with captures compatible with themes.ts (nvim-treesitter uses incompatible `#set!` and `#eq?` predicates)
|
|
55
|
+
- Fix syntax highlighting in web preview by pre-initializing TreeSitter client before rendering
|
|
56
|
+
|
|
57
|
+
# 0.1.93
|
|
58
|
+
|
|
59
|
+
- Dependencies:
|
|
60
|
+
- Update `@opentui/core` and `@opentui/react` npm aliases to `@opentuah/*@^0.1.81`
|
|
61
|
+
|
|
62
|
+
# 0.1.92
|
|
63
|
+
|
|
64
|
+
- `critique`:
|
|
65
|
+
- Keep dropdown search on `<textarea>` and fix filtering by syncing `plainText` from `onContentChange` in a microtask, plus extract shared filtering logic for deterministic matching across theme/file pickers
|
|
66
|
+
- Make watch-mode loading/empty backgrounds reactive to the selected theme by subscribing to `useAppStore` instead of reading `getState()` once
|
|
67
|
+
- `critique pick`:
|
|
68
|
+
- Use the active global theme for picker UI colors instead of always forcing the default theme
|
|
69
|
+
- `diff rendering`:
|
|
70
|
+
- Remount the `DiffView` wrapper on theme changes so internal diff backgrounds refresh more reliably when switching themes
|
|
71
|
+
- Tests:
|
|
72
|
+
- Add `src/components/diff-view.test.tsx` to verify theme-switch background updates in `DiffView`
|
|
73
|
+
- Add dropdown filtering coverage via exported `filterDropdownOptions` tests in `src/dropdown.test.tsx`
|
|
74
|
+
|
|
75
|
+
# 0.1.91
|
|
76
|
+
|
|
77
|
+
- `critique`:
|
|
78
|
+
- Improve main diff layout scrolling behavior by allowing the scrollbox to shrink within the column layout (`flexShrink: 1`)
|
|
79
|
+
- Tests:
|
|
80
|
+
- Add `src/cli-scroll.test.tsx` using opentui test renderer to verify mouse-wheel scrolling changes visible content in the main diff view
|
|
81
|
+
- Guard CLI entrypoint parsing with `import.meta.main` and export `App`/`AppProps` to support renderer-driven CLI view tests
|
|
82
|
+
|
|
83
|
+
# 0.1.90
|
|
84
|
+
|
|
85
|
+
- `critique`:
|
|
86
|
+
- Fix dropdown search input in file/theme pickers by switching to `<input onInput>` so typed text filters options immediately
|
|
87
|
+
- Ensure diff internals fully refresh on theme change by remounting `<diff>` when `themeName` changes
|
|
88
|
+
- Make loading/empty-state backgrounds reactive to theme changes in watch mode
|
|
89
|
+
- `critique pick`:
|
|
90
|
+
- Use the currently selected global theme instead of always forcing the default theme
|
|
91
|
+
- Tests:
|
|
92
|
+
- Add a dropdown regression test that reproduces and verifies theme search filtering
|
|
93
|
+
- Add a DiffView regression test that verifies diff background colors actually change after a runtime theme switch
|
|
94
|
+
|
|
95
|
+
# 0.1.89
|
|
96
|
+
|
|
97
|
+
- `critique`:
|
|
98
|
+
- Keep the main diff scrollbox focused so mouse-wheel scrolling works reliably in the default diff view
|
|
99
|
+
|
|
100
|
+
# 0.1.88
|
|
101
|
+
|
|
102
|
+
- Dependencies:
|
|
103
|
+
- Keep `@opentui/*` imports/package names and install Jake's fork via npm alias mapping
|
|
104
|
+
- Map `@opentui/core` -> `npm:@opentuah/core@latest` and `@opentui/react` -> `npm:@opentuah/react@latest`
|
|
105
|
+
|
|
106
|
+
# 0.1.87
|
|
107
|
+
|
|
108
|
+
- `hunks`:
|
|
109
|
+
- `critique hunks add <id>` now appends dirty submodule diffs before hunk lookup
|
|
110
|
+
- Fixes mismatch where IDs listed by `critique hunks list` for submodule changes could fail with "Hunk not found"
|
|
111
|
+
|
|
112
|
+
# 0.1.86
|
|
113
|
+
|
|
114
|
+
- New `hunks` commands for non-interactive selective staging:
|
|
115
|
+
- `critique hunks list` - list all hunks with stable IDs
|
|
116
|
+
- `critique hunks add <id>` - stage specific hunks by ID
|
|
117
|
+
- `--staged` flag to list staged hunks
|
|
118
|
+
- `--filter` flag to filter by file pattern
|
|
119
|
+
- Hunk IDs use format `file:@-old,len+new,len` (stable across runs)
|
|
120
|
+
- Uses `diff` npm package for reliable parsing instead of shell/awk
|
|
121
|
+
- Switch to `@xmorse/cac` fork for space-separated subcommand support
|
|
122
|
+
|
|
123
|
+
# 0.1.85
|
|
124
|
+
|
|
125
|
+
- Docs: remove undocumented `--local` option from README (fixes #24)
|
|
126
|
+
|
|
127
|
+
# 0.1.84
|
|
128
|
+
|
|
129
|
+
- New `--scrollback` option: output diff to terminal scrollback instead of interactive TUI
|
|
130
|
+
- Renders using opentui test renderer and converts to ANSI escape sequences
|
|
131
|
+
- Auto-detects terminal color support (truecolor → 256 → 16 → plain text)
|
|
132
|
+
- Respects `FORCE_COLOR` and `NO_COLOR` environment variables
|
|
133
|
+
- Outputs plain text when piped (non-TTY)
|
|
134
|
+
- Add `supports-color` dependency for terminal capability detection
|
|
135
|
+
|
|
136
|
+
# 0.1.83
|
|
137
|
+
|
|
138
|
+
- Internal:
|
|
139
|
+
- Update opentui to 0.1.77
|
|
140
|
+
- Simplify span capture: use opentui's built-in `getSpanLines()` instead of custom implementation
|
|
141
|
+
- Remove ~60 lines of duplicated span capture code in `web-utils.tsx`
|
|
142
|
+
|
|
143
|
+
# 0.1.82
|
|
144
|
+
|
|
145
|
+
- `--web`:
|
|
146
|
+
- Content-fitting for HTML rendering: starts with small buffer (100 rows), doubles until content fits, then shrinks to exact size
|
|
147
|
+
- Reduces memory usage for small diffs while still supporting large ones
|
|
148
|
+
- Rename `CaptureOptions.rows` to `maxRows` to clarify it's the upper bound for buffer growth
|
|
149
|
+
|
|
150
|
+
# 0.1.81
|
|
151
|
+
|
|
152
|
+
- `review`:
|
|
153
|
+
- Fix diagram overflow expanding container width - long diagrams now truncate from right instead of shifting all content left
|
|
154
|
+
- Update AI prompt: diagrams should be under 70 chars wide, prose must be outside code blocks
|
|
155
|
+
|
|
156
|
+
# 0.1.80
|
|
157
|
+
|
|
158
|
+
- OG images:
|
|
159
|
+
- Increased default font size from 16px to 20px for better readability
|
|
160
|
+
- Fixed text clipping by adding flexShrink 0 to all elements
|
|
161
|
+
- Always use github-light theme (no dark mode support in OG protocol)
|
|
162
|
+
- `review --web` now generates OG images from the first few hunks of the diff
|
|
163
|
+
- Fixed `buildPatch` to include `diff --git` header for proper diff parsing
|
|
164
|
+
|
|
165
|
+
# 0.1.79
|
|
166
|
+
|
|
167
|
+
- TUI:
|
|
168
|
+
- `critique`: Esc closes file/theme dropdowns before exiting
|
|
169
|
+
- `critique`: Add dropdown escape handling test coverage
|
|
170
|
+
- `critique review`: Esc closes the theme picker dropdown before exiting
|
|
171
|
+
|
|
1
172
|
# 0.1.78
|
|
2
173
|
|
|
3
174
|
- **Breaking change:** Single positional argument now uses `git diff` instead of `git show`
|
package/README.md
CHANGED
|
@@ -155,6 +155,58 @@ critique pick feature-branch
|
|
|
155
155
|
|
|
156
156
|
Use the interactive UI to select files. Selected files are immediately applied as patches, deselected files are restored.
|
|
157
157
|
|
|
158
|
+
### Selective Hunk Staging
|
|
159
|
+
|
|
160
|
+
Non-interactive hunk staging for scripts and AI agents. Similar to `git add -p` but scriptable.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# List all unstaged hunks with stable IDs
|
|
164
|
+
critique hunks list
|
|
165
|
+
|
|
166
|
+
# List staged hunks
|
|
167
|
+
critique hunks list --staged
|
|
168
|
+
|
|
169
|
+
# Filter by file pattern
|
|
170
|
+
critique hunks list --filter "src/**/*.ts"
|
|
171
|
+
|
|
172
|
+
# Stage specific hunks by ID
|
|
173
|
+
critique hunks add 'src/main.ts:@-10,6+10,7'
|
|
174
|
+
|
|
175
|
+
# Stage multiple hunks
|
|
176
|
+
critique hunks add 'src/main.ts:@-10,6+10,7' 'src/utils.ts:@-5,3+5,4'
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Hunk ID format:** `file:@-oldStart,oldLines+newStart,newLines`
|
|
180
|
+
|
|
181
|
+
The ID is derived from the `@@` header in unified diff format, making it stable across runs (unlike incremental IDs).
|
|
182
|
+
|
|
183
|
+
**Example workflow:**
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# 1. List available hunks
|
|
187
|
+
$ critique hunks list
|
|
188
|
+
src/main.ts:@-10,6+10,7
|
|
189
|
+
@@ -10,6 +10,7 @@
|
|
190
|
+
+import { newFeature } from './feature'
|
|
191
|
+
---
|
|
192
|
+
src/main.ts:@-50,3+51,5
|
|
193
|
+
@@ -50,3 +51,5 @@
|
|
194
|
+
+ newFeature()
|
|
195
|
+
+ return result
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
# 2. Stage just the import hunk
|
|
199
|
+
$ critique hunks add 'src/main.ts:@-10,6+10,7'
|
|
200
|
+
Staged: src/main.ts:@-10,6+10,7
|
|
201
|
+
|
|
202
|
+
# 3. Commit it separately
|
|
203
|
+
$ git commit -m "Add newFeature import"
|
|
204
|
+
|
|
205
|
+
# 4. Stage and commit the usage
|
|
206
|
+
$ critique hunks add 'src/main.ts:@-50,3+51,5'
|
|
207
|
+
$ git commit -m "Use newFeature in main"
|
|
208
|
+
```
|
|
209
|
+
|
|
158
210
|
### Web Preview
|
|
159
211
|
|
|
160
212
|
Generate a shareable web preview of your diff that you can send to anyone - no installation required.
|
|
@@ -187,9 +239,6 @@ critique web -- src/api.ts src/utils.ts
|
|
|
187
239
|
|
|
188
240
|
# Custom title for the HTML page
|
|
189
241
|
critique web --title "Fix authentication bug"
|
|
190
|
-
|
|
191
|
-
# Generate local HTML file instead of uploading
|
|
192
|
-
critique web --local
|
|
193
242
|
```
|
|
194
243
|
|
|
195
244
|
**Features:**
|
|
@@ -208,7 +257,6 @@ critique web --local
|
|
|
208
257
|
| `--commit <ref>` | Show changes from a specific commit | - |
|
|
209
258
|
| `--cols <n>` | Terminal width for rendering | `240` |
|
|
210
259
|
| `--mobile-cols <n>` | Terminal width for mobile version | `100` |
|
|
211
|
-
| `--local` | Save HTML locally instead of uploading | - |
|
|
212
260
|
| `--filter <pattern>` | Filter files by glob (can be used multiple times) | - |
|
|
213
261
|
| `--title <text>` | Custom HTML document title | `Critique Diff` |
|
|
214
262
|
| `--theme <name>` | Theme for rendering (disables auto dark/light mode) | - |
|
|
@@ -270,6 +318,14 @@ Files with more than 6000 lines of diff are also hidden for performance.
|
|
|
270
318
|
- [diff](https://github.com/kpdecker/jsdiff) - Diff algorithm
|
|
271
319
|
- [Hono](https://hono.dev/) - Web framework for the preview worker
|
|
272
320
|
|
|
321
|
+
## Sponsors
|
|
322
|
+
|
|
323
|
+
<a href="https://coderabbit.link/remorses" target="_blank" rel="noopener noreferrer">
|
|
324
|
+
<img src="https://github.com/coderabbitai.png" alt="CodeRabbit" height="24" />
|
|
325
|
+
</a>
|
|
326
|
+
|
|
327
|
+
Sponsored by [CodeRabbit](https://coderabbit.link/remorses).
|
|
328
|
+
|
|
273
329
|
## License
|
|
274
330
|
|
|
275
331
|
MIT
|
package/global.d.ts
ADDED
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.103",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
8
8
|
"bin": "./src/cli.tsx",
|
|
@@ -28,16 +28,17 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@agentclientprotocol/sdk": "^0.13.1",
|
|
30
30
|
"@clack/prompts": "1.0.0-alpha.9",
|
|
31
|
-
"@opentui/core": "
|
|
32
|
-
"@opentui/react": "
|
|
31
|
+
"@opentui/core": "npm:@opentuah/core@0.1.88",
|
|
32
|
+
"@opentui/react": "npm:@opentuah/react@0.1.88",
|
|
33
33
|
"@parcel/watcher": "^2.5.6",
|
|
34
|
-
"
|
|
34
|
+
"goke": "^6.1.3",
|
|
35
35
|
"diff": "^8.0.2",
|
|
36
36
|
"js-yaml": "^4.1.1",
|
|
37
37
|
"marked": "^17.0.1",
|
|
38
38
|
"picocolors": "^1.1.1",
|
|
39
39
|
"react": "^19.2.0",
|
|
40
40
|
"resend": "^6.8.0",
|
|
41
|
+
"supports-color": "^10.2.2",
|
|
41
42
|
"zustand": "^5.0.8"
|
|
42
43
|
},
|
|
43
44
|
"optionalDependencies": {
|
package/parsers-config.ts
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
// Warn: when taking queries from the nvim-treesitter repo, make sure to include the query dependencies as well
|
|
4
4
|
// marked with for example `; inherits: ecma` at the top of the file. Just put the dependencies before the actual query.
|
|
5
5
|
// ALSO: Some queries use breaking changes in the nvim-treesitter repo, that are not compatible with the (web-)tree-sitter parser.
|
|
6
|
+
import { resolve, dirname } from "path"
|
|
7
|
+
import { fileURLToPath } from "url"
|
|
8
|
+
|
|
9
|
+
// Local query files for languages where remote queries have incompatible predicates
|
|
10
|
+
import jsonHighlights from "./queries/json/highlights.scm" with { type: "file" }
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
13
|
+
|
|
6
14
|
export default {
|
|
7
15
|
parsers: [
|
|
8
16
|
{
|
|
@@ -164,7 +172,9 @@ export default {
|
|
|
164
172
|
wasm: "https://github.com/tree-sitter/tree-sitter-json/releases/download/v0.24.8/tree-sitter-json.wasm",
|
|
165
173
|
queries: {
|
|
166
174
|
highlights: [
|
|
167
|
-
|
|
175
|
+
// Local query file - nvim-treesitter and tree-sitter-json queries use predicates/captures
|
|
176
|
+
// incompatible with web-tree-sitter or themes.ts
|
|
177
|
+
resolve(__dirname, jsonHighlights),
|
|
168
178
|
],
|
|
169
179
|
},
|
|
170
180
|
},
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
; JSON highlights query for critique
|
|
2
|
+
; Uses captures compatible with themes.ts mappings
|
|
3
|
+
; No predicates (like #set! or #eq?) that are unsupported by web-tree-sitter
|
|
4
|
+
|
|
5
|
+
(pair
|
|
6
|
+
key: (string
|
|
7
|
+
(string_content) @property))
|
|
8
|
+
|
|
9
|
+
(pair
|
|
10
|
+
value: (string
|
|
11
|
+
(string_content) @string))
|
|
12
|
+
|
|
13
|
+
(array
|
|
14
|
+
(string
|
|
15
|
+
(string_content) @string))
|
|
16
|
+
|
|
17
|
+
(number) @number
|
|
18
|
+
|
|
19
|
+
[
|
|
20
|
+
(true)
|
|
21
|
+
(false)
|
|
22
|
+
] @boolean
|
|
23
|
+
|
|
24
|
+
(null) @constant
|
|
25
|
+
|
|
26
|
+
(escape_sequence) @string
|
|
27
|
+
|
|
28
|
+
; Keep JSON punctuation muted (not operator-red / not full-bright)
|
|
29
|
+
("\"") @comment
|
|
30
|
+
|
|
31
|
+
; JSON separators should render muted, not like operators
|
|
32
|
+
[
|
|
33
|
+
","
|
|
34
|
+
":"
|
|
35
|
+
] @comment
|
|
36
|
+
|
|
37
|
+
[
|
|
38
|
+
"["
|
|
39
|
+
"]"
|
|
40
|
+
"{"
|
|
41
|
+
"}"
|
|
42
|
+
] @comment
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Example JSON for critique syntax highlighting preview.",
|
|
3
|
+
"name": "critique",
|
|
4
|
+
"count": 12,
|
|
5
|
+
"enabled": true,
|
|
6
|
+
"ratio": 0.125,
|
|
7
|
+
"tags": ["diff", "tui", "web"],
|
|
8
|
+
"nested": {
|
|
9
|
+
"a": 1,
|
|
10
|
+
"b": null,
|
|
11
|
+
"url": "https://critique.work/v/example"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -264,7 +264,7 @@ async function main() {
|
|
|
264
264
|
if (webMode) {
|
|
265
265
|
console.log("Capturing preview...")
|
|
266
266
|
|
|
267
|
-
const { htmlDesktop, htmlMobile } = await captureReviewResponsiveHtml({
|
|
267
|
+
const { htmlDesktop, htmlMobile, ogImage } = await captureReviewResponsiveHtml({
|
|
268
268
|
hunks: exampleHunks,
|
|
269
269
|
reviewData: exampleReviewData,
|
|
270
270
|
desktopCols: 200,
|
|
@@ -275,7 +275,7 @@ async function main() {
|
|
|
275
275
|
})
|
|
276
276
|
|
|
277
277
|
console.log("Uploading...")
|
|
278
|
-
const result = await uploadHtml(htmlDesktop, htmlMobile)
|
|
278
|
+
const result = await uploadHtml(htmlDesktop, htmlMobile, ogImage)
|
|
279
279
|
console.log(`\nPreview URL: ${result.url}`)
|
|
280
280
|
console.log("(expires in 7 days)")
|
|
281
281
|
return
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test"
|
|
2
|
+
import { spanToAnsi, frameToAnsi } from "./ansi-output.ts"
|
|
3
|
+
import { RGBA } from "@opentui/core"
|
|
4
|
+
import type { CapturedFrame, CapturedSpan, CapturedLine } from "@opentui/core"
|
|
5
|
+
|
|
6
|
+
const themeBg = RGBA.fromValues(0, 0, 0, 1) // Black background
|
|
7
|
+
|
|
8
|
+
describe("spanToAnsi", () => {
|
|
9
|
+
test("plain text with no colors (level 0)", () => {
|
|
10
|
+
const span: CapturedSpan = {
|
|
11
|
+
text: "hello",
|
|
12
|
+
fg: RGBA.fromValues(1, 1, 1, 1),
|
|
13
|
+
bg: RGBA.fromValues(0, 0, 0, 0),
|
|
14
|
+
attributes: 0,
|
|
15
|
+
width: 5,
|
|
16
|
+
}
|
|
17
|
+
expect(spanToAnsi(span, 0, themeBg)).toMatchInlineSnapshot(`"hello"`)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test("truecolor foreground (level 3)", () => {
|
|
21
|
+
const span: CapturedSpan = {
|
|
22
|
+
text: "red",
|
|
23
|
+
fg: RGBA.fromValues(1, 0, 0, 1), // Pure red
|
|
24
|
+
bg: RGBA.fromValues(0, 0, 0, 0),
|
|
25
|
+
attributes: 0,
|
|
26
|
+
width: 3,
|
|
27
|
+
}
|
|
28
|
+
expect(spanToAnsi(span, 3, themeBg)).toMatchInlineSnapshot(`"\x1B[38;2;255;0;0mred\x1B[0m"`)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test("truecolor foreground and background (level 3)", () => {
|
|
32
|
+
const span: CapturedSpan = {
|
|
33
|
+
text: "styled",
|
|
34
|
+
fg: RGBA.fromValues(1, 1, 1, 1), // White
|
|
35
|
+
bg: RGBA.fromValues(0, 0, 1, 1), // Blue
|
|
36
|
+
attributes: 0,
|
|
37
|
+
width: 6,
|
|
38
|
+
}
|
|
39
|
+
expect(spanToAnsi(span, 3, themeBg)).toMatchInlineSnapshot(`"\x1B[38;2;255;255;255;48;2;0;0;255mstyled\x1B[0m"`)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test("256 colors (level 2)", () => {
|
|
43
|
+
const span: CapturedSpan = {
|
|
44
|
+
text: "256",
|
|
45
|
+
fg: RGBA.fromValues(1, 0, 0, 1), // Red
|
|
46
|
+
bg: RGBA.fromValues(0, 0, 0, 0),
|
|
47
|
+
attributes: 0,
|
|
48
|
+
width: 3,
|
|
49
|
+
}
|
|
50
|
+
expect(spanToAnsi(span, 2, themeBg)).toMatchInlineSnapshot(`"\x1B[38;5;196m256\x1B[0m"`)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("16 colors (level 1)", () => {
|
|
54
|
+
const span: CapturedSpan = {
|
|
55
|
+
text: "basic",
|
|
56
|
+
fg: RGBA.fromValues(1, 0, 0, 1), // Red -> bright red
|
|
57
|
+
bg: RGBA.fromValues(0, 0, 0, 0),
|
|
58
|
+
attributes: 0,
|
|
59
|
+
width: 5,
|
|
60
|
+
}
|
|
61
|
+
expect(spanToAnsi(span, 1, themeBg)).toMatchInlineSnapshot(`"\x1B[31mbasic\x1B[0m"`)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test("bold attribute", () => {
|
|
65
|
+
const span: CapturedSpan = {
|
|
66
|
+
text: "bold",
|
|
67
|
+
fg: RGBA.fromValues(1, 1, 1, 1),
|
|
68
|
+
bg: RGBA.fromValues(0, 0, 0, 0),
|
|
69
|
+
attributes: 1, // BOLD
|
|
70
|
+
width: 4,
|
|
71
|
+
}
|
|
72
|
+
expect(spanToAnsi(span, 3, themeBg)).toMatchInlineSnapshot(`"\x1B[38;2;255;255;255;1mbold\x1B[0m"`)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test("multiple attributes (bold + italic + underline)", () => {
|
|
76
|
+
const span: CapturedSpan = {
|
|
77
|
+
text: "fancy",
|
|
78
|
+
fg: RGBA.fromValues(1, 1, 1, 1),
|
|
79
|
+
bg: RGBA.fromValues(0, 0, 0, 0),
|
|
80
|
+
attributes: 1 | 4 | 8, // BOLD | ITALIC | UNDERLINE
|
|
81
|
+
width: 5,
|
|
82
|
+
}
|
|
83
|
+
expect(spanToAnsi(span, 3, themeBg)).toMatchInlineSnapshot(`"\x1B[38;2;255;255;255;1;3;4mfancy\x1B[0m"`)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test("transparent foreground returns plain text", () => {
|
|
87
|
+
const span: CapturedSpan = {
|
|
88
|
+
text: "transparent",
|
|
89
|
+
fg: RGBA.fromValues(1, 1, 1, 0), // Fully transparent
|
|
90
|
+
bg: RGBA.fromValues(0, 0, 0, 0),
|
|
91
|
+
attributes: 0,
|
|
92
|
+
width: 11,
|
|
93
|
+
}
|
|
94
|
+
expect(spanToAnsi(span, 3, themeBg)).toMatchInlineSnapshot(`"transparent"`)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test("alpha blending with background", () => {
|
|
98
|
+
const span: CapturedSpan = {
|
|
99
|
+
text: "blend",
|
|
100
|
+
fg: RGBA.fromValues(1, 1, 1, 0.5), // 50% white on black bg = gray
|
|
101
|
+
bg: RGBA.fromValues(0, 0, 0, 0),
|
|
102
|
+
attributes: 0,
|
|
103
|
+
width: 5,
|
|
104
|
+
}
|
|
105
|
+
expect(spanToAnsi(span, 3, themeBg)).toMatchInlineSnapshot(`"\x1B[38;2;128;128;128mblend\x1B[0m"`)
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe("frameToAnsi", () => {
|
|
110
|
+
test("single line frame", () => {
|
|
111
|
+
const frame: CapturedFrame = {
|
|
112
|
+
cols: 10,
|
|
113
|
+
rows: 1,
|
|
114
|
+
cursor: [0, 0],
|
|
115
|
+
lines: [
|
|
116
|
+
{
|
|
117
|
+
spans: [
|
|
118
|
+
{ text: "hello", fg: RGBA.fromValues(1, 0, 0, 1), bg: RGBA.fromValues(0, 0, 0, 0), attributes: 0, width: 5 },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
}
|
|
123
|
+
expect(frameToAnsi(frame, themeBg)).toMatchInlineSnapshot(`"hello"`)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test("multi-line frame", () => {
|
|
127
|
+
const frame: CapturedFrame = {
|
|
128
|
+
cols: 10,
|
|
129
|
+
rows: 2,
|
|
130
|
+
cursor: [0, 0],
|
|
131
|
+
lines: [
|
|
132
|
+
{
|
|
133
|
+
spans: [
|
|
134
|
+
{ text: "line1", fg: RGBA.fromValues(1, 0, 0, 1), bg: RGBA.fromValues(0, 0, 0, 0), attributes: 0, width: 5 },
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
spans: [
|
|
139
|
+
{ text: "line2", fg: RGBA.fromValues(0, 1, 0, 1), bg: RGBA.fromValues(0, 0, 0, 0), attributes: 0, width: 5 },
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
}
|
|
144
|
+
expect(frameToAnsi(frame, themeBg)).toMatchInlineSnapshot(`
|
|
145
|
+
"line1
|
|
146
|
+
line2"
|
|
147
|
+
`)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test("trims empty lines from end", () => {
|
|
151
|
+
const frame: CapturedFrame = {
|
|
152
|
+
cols: 10,
|
|
153
|
+
rows: 3,
|
|
154
|
+
cursor: [0, 0],
|
|
155
|
+
lines: [
|
|
156
|
+
{
|
|
157
|
+
spans: [
|
|
158
|
+
{ text: "content", fg: RGBA.fromValues(1, 1, 1, 1), bg: RGBA.fromValues(0, 0, 0, 0), attributes: 0, width: 7 },
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
{ spans: [] }, // Empty line
|
|
162
|
+
{ spans: [{ text: " ", fg: RGBA.fromValues(1, 1, 1, 1), bg: RGBA.fromValues(0, 0, 0, 0), attributes: 0, width: 3 }] }, // Whitespace only
|
|
163
|
+
],
|
|
164
|
+
}
|
|
165
|
+
expect(frameToAnsi(frame, themeBg)).toMatchInlineSnapshot(`"content"`)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test("preserves empty lines when trimEmptyLines is false", () => {
|
|
169
|
+
const frame: CapturedFrame = {
|
|
170
|
+
cols: 10,
|
|
171
|
+
rows: 2,
|
|
172
|
+
cursor: [0, 0],
|
|
173
|
+
lines: [
|
|
174
|
+
{
|
|
175
|
+
spans: [
|
|
176
|
+
{ text: "content", fg: RGBA.fromValues(1, 1, 1, 1), bg: RGBA.fromValues(0, 0, 0, 0), attributes: 0, width: 7 },
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
{ spans: [] },
|
|
180
|
+
],
|
|
181
|
+
}
|
|
182
|
+
expect(frameToAnsi(frame, themeBg, { trimEmptyLines: false })).toMatchInlineSnapshot(`
|
|
183
|
+
"content
|
|
184
|
+
"
|
|
185
|
+
`)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test("multiple spans per line", () => {
|
|
189
|
+
const frame: CapturedFrame = {
|
|
190
|
+
cols: 20,
|
|
191
|
+
rows: 1,
|
|
192
|
+
cursor: [0, 0],
|
|
193
|
+
lines: [
|
|
194
|
+
{
|
|
195
|
+
spans: [
|
|
196
|
+
{ text: "red", fg: RGBA.fromValues(1, 0, 0, 1), bg: RGBA.fromValues(0, 0, 0, 0), attributes: 0, width: 3 },
|
|
197
|
+
{ text: " ", fg: RGBA.fromValues(1, 1, 1, 1), bg: RGBA.fromValues(0, 0, 0, 0), attributes: 0, width: 1 },
|
|
198
|
+
{ text: "green", fg: RGBA.fromValues(0, 1, 0, 1), bg: RGBA.fromValues(0, 0, 0, 0), attributes: 0, width: 5 },
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
}
|
|
203
|
+
expect(frameToAnsi(frame, themeBg)).toMatchInlineSnapshot(`"red green"`)
|
|
204
|
+
})
|
|
205
|
+
})
|