critique 0.1.46 → 0.1.49
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 +30 -0
- package/package.json +5 -10
- package/scripts/preview-review.tsx +11 -13
- package/src/ansi-html.ts +41 -35
- package/src/cli.tsx +85 -399
- package/src/components/directory-tree-view.tsx +1 -1
- package/src/directory-tree.test.tsx +9 -0
- package/src/hooks/use-copy-selection.ts +131 -0
- package/src/review/review-app.test.tsx +213 -140
- package/src/review/review-app.tsx +74 -18
- package/src/review/stream-display.test.tsx +7 -0
- package/src/themes.ts +3 -1
- package/src/web-utils.test.ts +99 -0
- package/src/web-utils.ts +358 -57
- package/src/ansi-html.test.ts +0 -107
- package/src/image.ts +0 -304
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,38 @@
|
|
|
1
|
+
# 0.1.49
|
|
2
|
+
|
|
3
|
+
- Add copy selection on mouseup: text selected with the mouse is automatically copied to clipboard when released
|
|
4
|
+
- Works across all TUI modes (diff viewer, review, loading states)
|
|
5
|
+
- Uses native clipboard commands (pbcopy, xclip, wl-copy) with OSC52 fallback for SSH
|
|
6
|
+
- New `useCopySelection` hook in `src/hooks/use-copy-selection.ts`
|
|
7
|
+
- Disable click-to-scroll on directory tree files (conflicts with copy selection)
|
|
8
|
+
|
|
9
|
+
# 0.1.48
|
|
10
|
+
|
|
11
|
+
- Web rendering improvements:
|
|
12
|
+
- Fix review height estimation to use same row multiplier as regular diff capture (prevents scrollbars)
|
|
13
|
+
- Fix diagram wrapping by passing `renderer` prop to enable custom `renderNode` with `wrapMode: "none"`
|
|
14
|
+
- Remove `ghostty-opentui` dependency, use opentui test renderer directly for HTML generation
|
|
15
|
+
- `review` command:
|
|
16
|
+
- Diagram code blocks (`lang="diagram"`) now use `wrapMode: "none"` to prevent line wrapping
|
|
17
|
+
- Added `flexShrink: 0` and `overflow: "hidden"` to diagram line boxes
|
|
18
|
+
- Internal: Remove `web-render` and `review-web-render` CLI subcommands (replaced by test renderer approach)
|
|
19
|
+
|
|
20
|
+
# 0.1.47
|
|
21
|
+
|
|
22
|
+
- Add vim-style keyboard navigation:
|
|
23
|
+
- `G` (Shift+g) - scroll to bottom
|
|
24
|
+
- `gg` (double-tap g) - scroll to top
|
|
25
|
+
- `Ctrl+D` - half page down
|
|
26
|
+
- `Ctrl+U` - half page up
|
|
27
|
+
- `review` command: change debug console toggle from `Ctrl+D` to `Ctrl+Z` (consistent with main viewer)
|
|
28
|
+
- Fix theme loading on Windows by using `fileURLToPath` for proper path conversion
|
|
29
|
+
|
|
1
30
|
# 0.1.46
|
|
2
31
|
|
|
3
32
|
- Add directory tree view at top of diff TUIs (default, review, web commands)
|
|
4
33
|
- Switch opentui packages to npm releases (from pkg.pr.new preview URLs)
|
|
5
34
|
- Add missing `marked` dependency
|
|
35
|
+
- Fix Q and Escape keys not working to exit when there are no changes to display (fixes #16)
|
|
6
36
|
|
|
7
37
|
# 0.1.45
|
|
8
38
|
|
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.49",
|
|
6
6
|
"private": false,
|
|
7
7
|
"bin": "./src/cli.tsx",
|
|
8
8
|
"scripts": {
|
|
@@ -22,22 +22,17 @@
|
|
|
22
22
|
"wrangler": "^4.19.1"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agentclientprotocol/sdk": "^0.
|
|
25
|
+
"@agentclientprotocol/sdk": "^0.13.1",
|
|
26
26
|
"@clack/prompts": "1.0.0-alpha.9",
|
|
27
|
-
"@opentui/core": "^0",
|
|
28
|
-
"@opentui/react": "^0",
|
|
29
|
-
"@parcel/watcher": "^2.5.
|
|
27
|
+
"@opentui/core": "^0.1.75",
|
|
28
|
+
"@opentui/react": "^0.1.75",
|
|
29
|
+
"@parcel/watcher": "^2.5.6",
|
|
30
30
|
"cac": "^6.7.14",
|
|
31
31
|
"diff": "^8.0.2",
|
|
32
|
-
"ghostty-opentui": "^1.3.12",
|
|
33
32
|
"js-yaml": "^4.1.1",
|
|
34
33
|
"marked": "^17.0.1",
|
|
35
34
|
"picocolors": "^1.1.1",
|
|
36
35
|
"react": "^19.2.0",
|
|
37
36
|
"zustand": "^5.0.8"
|
|
38
|
-
},
|
|
39
|
-
"optionalDependencies": {
|
|
40
|
-
"@takumi-rs/core": "^0.65.0",
|
|
41
|
-
"@takumi-rs/helpers": "^0.65.0"
|
|
42
37
|
}
|
|
43
38
|
}
|
|
@@ -9,7 +9,7 @@ import * as React from "react"
|
|
|
9
9
|
import { ReviewApp, ReviewAppView } from "../src/review/review-app.tsx"
|
|
10
10
|
import { createHunk } from "../src/review/hunk-parser.ts"
|
|
11
11
|
import type { ReviewYaml } from "../src/review/types.ts"
|
|
12
|
-
import {
|
|
12
|
+
import { captureReviewResponsiveHtml, uploadHtml } from "../src/web-utils.ts"
|
|
13
13
|
import fs from "fs"
|
|
14
14
|
import { tmpdir } from "os"
|
|
15
15
|
import { join } from "path"
|
|
@@ -210,21 +210,19 @@ All tests pass and coverage is at 95%.`,
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
async function main() {
|
|
213
|
-
// Web mode: capture and upload HTML
|
|
213
|
+
// Web mode: capture and upload HTML using test renderer
|
|
214
214
|
if (webMode) {
|
|
215
215
|
console.log("Capturing preview...")
|
|
216
216
|
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
)
|
|
217
|
+
const { htmlDesktop, htmlMobile } = await captureReviewResponsiveHtml({
|
|
218
|
+
hunks: exampleHunks,
|
|
219
|
+
reviewData: exampleReviewData,
|
|
220
|
+
desktopCols: 200,
|
|
221
|
+
mobileCols: 80,
|
|
222
|
+
baseRows: 200,
|
|
223
|
+
themeName: "github",
|
|
224
|
+
title: "Review Preview",
|
|
225
|
+
})
|
|
228
226
|
|
|
229
227
|
console.log("Uploading...")
|
|
230
228
|
const result = await uploadHtml(htmlDesktop, htmlMobile)
|
package/src/ansi-html.ts
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Uses
|
|
1
|
+
// Terminal output to HTML converter for web preview generation.
|
|
2
|
+
// Uses opentui's test renderer to capture structured span data and generates responsive HTML documents
|
|
3
3
|
// with proper font scaling to fit terminal content within viewport width.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { TextAttributes, rgbToHex, type RGBA } from "@opentui/core"
|
|
6
|
+
import type { CapturedFrame, CapturedLine, CapturedSpan } from "@opentui/core"
|
|
6
7
|
|
|
7
|
-
export interface
|
|
8
|
-
cols?: number
|
|
9
|
-
rows?: number
|
|
8
|
+
export interface ToHtmlOptions {
|
|
10
9
|
/** Background color for the container */
|
|
11
10
|
backgroundColor?: string
|
|
12
11
|
/** Text color for the container */
|
|
13
12
|
textColor?: string
|
|
14
13
|
/** Font family for the output */
|
|
15
14
|
fontFamily?: string
|
|
16
|
-
/** Font size for the output */
|
|
17
|
-
fontSize?: string
|
|
18
15
|
/** Trim empty lines from the end */
|
|
19
16
|
trimEmptyLines?: boolean
|
|
20
17
|
/** Enable auto light/dark mode based on system preference */
|
|
@@ -34,34 +31,45 @@ function escapeHtml(text: string): string {
|
|
|
34
31
|
.replace(/"/g, """)
|
|
35
32
|
}
|
|
36
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Convert RGBA to hex string, returning null for transparent colors
|
|
36
|
+
*/
|
|
37
|
+
function rgbaToHexOrNull(rgba: RGBA): string | null {
|
|
38
|
+
if (rgba.a === 0) return null
|
|
39
|
+
return rgbToHex(rgba)
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
/**
|
|
38
43
|
* Convert a single span to HTML
|
|
39
44
|
* Always wraps in span for consistent inline-block sizing
|
|
40
45
|
*/
|
|
41
|
-
function spanToHtml(span:
|
|
46
|
+
function spanToHtml(span: CapturedSpan): string {
|
|
42
47
|
const styles: string[] = []
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
const fg = rgbaToHexOrNull(span.fg)
|
|
50
|
+
const bg = rgbaToHexOrNull(span.bg)
|
|
51
|
+
|
|
52
|
+
if (fg) {
|
|
53
|
+
styles.push(`color:${fg}`)
|
|
46
54
|
}
|
|
47
|
-
if (
|
|
48
|
-
styles.push(`background-color:${
|
|
55
|
+
if (bg) {
|
|
56
|
+
styles.push(`background-color:${bg}`)
|
|
49
57
|
}
|
|
50
58
|
|
|
51
|
-
// Handle style flags
|
|
52
|
-
if (span.
|
|
59
|
+
// Handle style flags using TextAttributes
|
|
60
|
+
if (span.attributes & TextAttributes.BOLD) {
|
|
53
61
|
styles.push("font-weight:bold")
|
|
54
62
|
}
|
|
55
|
-
if (span.
|
|
63
|
+
if (span.attributes & TextAttributes.ITALIC) {
|
|
56
64
|
styles.push("font-style:italic")
|
|
57
65
|
}
|
|
58
|
-
if (span.
|
|
66
|
+
if (span.attributes & TextAttributes.UNDERLINE) {
|
|
59
67
|
styles.push("text-decoration:underline")
|
|
60
68
|
}
|
|
61
|
-
if (span.
|
|
69
|
+
if (span.attributes & TextAttributes.STRIKETHROUGH) {
|
|
62
70
|
styles.push("text-decoration:line-through")
|
|
63
71
|
}
|
|
64
|
-
if (span.
|
|
72
|
+
if (span.attributes & TextAttributes.DIM) {
|
|
65
73
|
styles.push("opacity:0.5")
|
|
66
74
|
}
|
|
67
75
|
|
|
@@ -78,7 +86,7 @@ function spanToHtml(span: TerminalSpan): string {
|
|
|
78
86
|
/**
|
|
79
87
|
* Convert a single line to HTML
|
|
80
88
|
*/
|
|
81
|
-
function lineToHtml(line:
|
|
89
|
+
function lineToHtml(line: CapturedLine): string {
|
|
82
90
|
if (line.spans.length === 0) {
|
|
83
91
|
return ""
|
|
84
92
|
}
|
|
@@ -88,22 +96,20 @@ function lineToHtml(line: TerminalLine): string {
|
|
|
88
96
|
/**
|
|
89
97
|
* Check if a line is empty (no spans or only whitespace content)
|
|
90
98
|
*/
|
|
91
|
-
function isLineEmpty(line:
|
|
99
|
+
function isLineEmpty(line: CapturedLine): boolean {
|
|
92
100
|
if (line.spans.length === 0) return true
|
|
93
101
|
// Check if all spans contain only whitespace
|
|
94
102
|
return line.spans.every(span => span.text.trim() === "")
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
/**
|
|
98
|
-
* Converts
|
|
99
|
-
*
|
|
106
|
+
* Converts captured frame to styled HTML.
|
|
107
|
+
* Renders HTML line by line from the CapturedFrame structure.
|
|
100
108
|
*/
|
|
101
|
-
export function
|
|
102
|
-
const {
|
|
103
|
-
|
|
104
|
-
const data = ptyToJson(input, { cols, rows })
|
|
109
|
+
export function frameToHtml(frame: CapturedFrame, options: ToHtmlOptions = {}): string {
|
|
110
|
+
const { trimEmptyLines = true } = options
|
|
105
111
|
|
|
106
|
-
let lines =
|
|
112
|
+
let lines = frame.lines
|
|
107
113
|
|
|
108
114
|
// Trim empty lines from the end
|
|
109
115
|
if (trimEmptyLines) {
|
|
@@ -113,7 +119,7 @@ export function ansiToHtml(input: string | Buffer, options: AnsiToHtmlOptions =
|
|
|
113
119
|
}
|
|
114
120
|
|
|
115
121
|
// Render each line as a div
|
|
116
|
-
const htmlLines = lines.map((line
|
|
122
|
+
const htmlLines = lines.map((line) => {
|
|
117
123
|
const content = lineToHtml(line)
|
|
118
124
|
// Use a div for each line to ensure proper line breaks
|
|
119
125
|
// Empty lines get a span with nbsp for consistent flex behavior
|
|
@@ -124,21 +130,21 @@ export function ansiToHtml(input: string | Buffer, options: AnsiToHtmlOptions =
|
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
/**
|
|
127
|
-
* Generates a complete HTML document from
|
|
133
|
+
* Generates a complete HTML document from captured frame.
|
|
128
134
|
* Includes proper styling for terminal output display.
|
|
129
135
|
* Font size automatically adjusts to fit content within viewport.
|
|
130
136
|
*/
|
|
131
|
-
export function
|
|
137
|
+
export function frameToHtmlDocument(frame: CapturedFrame, options: ToHtmlOptions = {}): string {
|
|
132
138
|
const {
|
|
133
|
-
cols = 500,
|
|
134
139
|
backgroundColor = "#ffffff",
|
|
135
140
|
textColor = "#1a1a1a",
|
|
136
141
|
fontFamily = "'JetBrains Mono Nerd', 'JetBrains Mono', 'Fira Code', Monaco, Menlo, 'Ubuntu Mono', Consolas, monospace",
|
|
137
|
-
fontSize = "14px",
|
|
138
142
|
title = "Critique Diff",
|
|
139
143
|
} = options
|
|
140
144
|
|
|
141
|
-
const
|
|
145
|
+
const cols = frame.cols
|
|
146
|
+
|
|
147
|
+
const content = frameToHtml(frame, options)
|
|
142
148
|
|
|
143
149
|
return `<!DOCTYPE html>
|
|
144
150
|
<html>
|
|
@@ -234,4 +240,4 @@ ${content}
|
|
|
234
240
|
</html>`
|
|
235
241
|
}
|
|
236
242
|
|
|
237
|
-
export type {
|
|
243
|
+
export type { CapturedFrame, CapturedLine, CapturedSpan }
|