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 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.46",
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.12.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.1",
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 { captureResponsiveHtml, uploadHtml } from "../src/web-utils.ts"
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, then open in browser
213
+ // Web mode: capture and upload HTML using test renderer
214
214
  if (webMode) {
215
215
  console.log("Capturing preview...")
216
216
 
217
- const scriptPath = import.meta.path
218
- const { htmlDesktop, htmlMobile } = await captureResponsiveHtml(
219
- ["run", scriptPath, "--capture"],
220
- {
221
- desktopCols: 200,
222
- mobileCols: 80,
223
- baseRows: 200,
224
- themeName: "github",
225
- title: "Review Preview",
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
- // ANSI terminal output to HTML converter for web preview generation.
2
- // Uses ghostty-opentui to parse PTY output and generates responsive HTML documents
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 { ptyToJson, StyleFlags, type TerminalData, type TerminalLine, type TerminalSpan } from "ghostty-opentui"
5
+ import { TextAttributes, rgbToHex, type RGBA } from "@opentui/core"
6
+ import type { CapturedFrame, CapturedLine, CapturedSpan } from "@opentui/core"
6
7
 
7
- export interface AnsiToHtmlOptions {
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: TerminalSpan): string {
46
+ function spanToHtml(span: CapturedSpan): string {
42
47
  const styles: string[] = []
43
48
 
44
- if (span.fg) {
45
- styles.push(`color:${span.fg}`)
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 (span.bg) {
48
- styles.push(`background-color:${span.bg}`)
55
+ if (bg) {
56
+ styles.push(`background-color:${bg}`)
49
57
  }
50
58
 
51
- // Handle style flags
52
- if (span.flags & StyleFlags.BOLD) {
59
+ // Handle style flags using TextAttributes
60
+ if (span.attributes & TextAttributes.BOLD) {
53
61
  styles.push("font-weight:bold")
54
62
  }
55
- if (span.flags & StyleFlags.ITALIC) {
63
+ if (span.attributes & TextAttributes.ITALIC) {
56
64
  styles.push("font-style:italic")
57
65
  }
58
- if (span.flags & StyleFlags.UNDERLINE) {
66
+ if (span.attributes & TextAttributes.UNDERLINE) {
59
67
  styles.push("text-decoration:underline")
60
68
  }
61
- if (span.flags & StyleFlags.STRIKETHROUGH) {
69
+ if (span.attributes & TextAttributes.STRIKETHROUGH) {
62
70
  styles.push("text-decoration:line-through")
63
71
  }
64
- if (span.flags & StyleFlags.FAINT) {
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: TerminalLine): string {
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: TerminalLine): boolean {
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 ANSI terminal output to styled HTML.
99
- * Uses ptyToJson for parsing and renders HTML line by line.
106
+ * Converts captured frame to styled HTML.
107
+ * Renders HTML line by line from the CapturedFrame structure.
100
108
  */
101
- export function ansiToHtml(input: string | Buffer, options: AnsiToHtmlOptions = {}): string {
102
- const { cols = 500, rows = 256, trimEmptyLines = true } = options
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 = data.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, idx) => {
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 ANSI input.
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 ansiToHtmlDocument(input: string | Buffer, options: AnsiToHtmlOptions = {}): string {
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 content = ansiToHtml(input, options)
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 { TerminalData }
243
+ export type { CapturedFrame, CapturedLine, CapturedSpan }