critique 0.1.11 → 0.1.13

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,3 +1,17 @@
1
+ # 0.1.13
2
+
3
+ - Docs:
4
+ - Enhance web preview section in README
5
+ - Add `/v/` short URL alias documentation
6
+
7
+ # 0.1.12
8
+
9
+ - Web preview:
10
+ - Add client-side mobile detection with redirect to `?v=mobile`
11
+ - Simplify worker: redirect mobile devices instead of content negotiation
12
+ - Remove `Vary` header - URL now determines content, better caching
13
+ - Increase cache max-age to 24h (was 1h)
14
+
1
15
  # 0.1.11
2
16
 
3
17
  - All commands:
package/README.md CHANGED
@@ -86,7 +86,13 @@ Use the interactive UI to select files. Selected files are immediately applied a
86
86
 
87
87
  ### Web Preview
88
88
 
89
- Generate a shareable web preview of your diff that you can send to anyone - no installation required:
89
+ Generate a shareable web preview of your diff that you can send to anyone - no installation required.
90
+
91
+ **Example:** [critique.work/v/b8faf4362c247bfc46f5098a028e00f0](https://critique.work/v/b8faf4362c247bfc46f5098a028e00f0)
92
+
93
+ Great for background agents that can't render terminal UIs, like [kimaki.xyz](https://kimaki.xyz) which runs OpenCode in Discord.
94
+
95
+ ![Web Preview](screenshot-web.png)
90
96
 
91
97
  ```bash
92
98
  # Upload to critique.work and get a shareable URL
@@ -95,29 +101,32 @@ critique web
95
101
  # View staged changes
96
102
  critique web --staged
97
103
 
98
- # View the last commit (works whether pushed or unpushed)
104
+ # View the last commit
99
105
  critique web HEAD
100
106
 
101
107
  # View a specific commit
102
108
  critique web --commit HEAD~1
103
109
 
104
- # View combined changes from last N commits
105
- critique web HEAD~3 HEAD
110
+ # Compare branches (PR-style diff)
111
+ critique web main feature-branch
112
+
113
+ # Filter specific files
114
+ critique web -- src/api.ts src/utils.ts
115
+
116
+ # Custom title for the HTML page
117
+ critique web --title "Fix authentication bug"
106
118
 
107
119
  # Generate local HTML file instead of uploading
108
120
  critique web --local
109
-
110
- # Adjust rendering size (use ~100 cols for mobile-friendly output)
111
- critique web --cols 100 --rows 2000
112
121
  ```
113
122
 
114
- **How it works:**
123
+ **Features:**
115
124
 
116
- 1. Captures the terminal UI output using a PTY (pseudo-terminal)
117
- 2. Converts ANSI escape codes to styled HTML with syntax highlighting
118
- 3. Uploads the HTML to [critique.work](https://critique.work) (Cloudflare Worker + KV storage)
119
- 4. Returns a shareable URL that expires after 7 days
120
- 5. Automatically opens the preview in your browser
125
+ - **Mobile optimized** - Automatically detects mobile devices and serves a unified diff view optimized for smaller screens. Add `?v=mobile` to any URL to force mobile view.
126
+ - **Dark/Light mode** - Automatically adapts to your system's color scheme preference using CSS `prefers-color-scheme`.
127
+ - **Syntax highlighting** - Full syntax highlighting for 18+ languages, same as the terminal UI.
128
+ - **Split view** - Side-by-side diff on desktop, unified view on mobile.
129
+ - **Fast loading** - HTML is streamed for quick initial render, cached for 24 hours.
121
130
 
122
131
  **Options:**
123
132
 
@@ -126,14 +135,24 @@ critique web --cols 100 --rows 2000
126
135
  | `--staged` | Show staged changes | - |
127
136
  | `--commit <ref>` | Show changes from a specific commit | - |
128
137
  | `--cols <n>` | Terminal width for rendering | `240` |
129
- | `--rows <n>` | Terminal height for rendering | `2000` |
138
+ | `--mobile-cols <n>` | Terminal width for mobile version | `100` |
130
139
  | `--local` | Save HTML locally instead of uploading | - |
131
140
  | `--filter <pattern>` | Filter files by glob (can be used multiple times) | - |
141
+ | `--title <text>` | Custom HTML document title | `Critique Diff` |
142
+ | `--theme <name>` | Theme for rendering (disables auto dark/light mode) | - |
143
+
144
+ **How it works:**
145
+
146
+ 1. Captures the terminal UI output using a PTY (pseudo-terminal)
147
+ 2. Converts ANSI escape codes to styled HTML with syntax highlighting
148
+ 3. Generates both desktop (240 cols, split view) and mobile (100 cols, unified view) versions
149
+ 4. Uploads to [critique.work](https://critique.work) (Cloudflare Worker + KV storage)
150
+ 5. Returns a shareable URL that expires after 7 days
132
151
 
133
152
  **Tips:**
134
153
 
135
- - Use `--cols 100` for mobile-friendly output (switches to unified diff view instead of split view)
136
154
  - The URL is based on a SHA-256 hash of the content, so identical diffs produce the same URL (deduplication)
155
+ - Use `?v=desktop` or `?v=mobile` query params to force a specific version
137
156
  - If upload fails, critique automatically saves the HTML locally as a fallback
138
157
 
139
158
  ## Features
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.11",
5
+ "version": "0.1.13",
6
6
  "private": false,
7
7
  "bin": "./src/cli.tsx",
8
8
  "scripts": {
Binary file
package/src/ansi-html.ts CHANGED
@@ -204,6 +204,17 @@ ${content}
204
204
  const minFontSize = 4;
205
205
  const maxFontSize = 16;
206
206
 
207
+ // Redirect mobile devices to ?v=mobile for optimized view
208
+ // Only redirect if not already on a forced version
209
+ const params = new URLSearchParams(window.location.search);
210
+ if (!params.has('v')) {
211
+ const isMobile = /Mobile|iP(hone|od|ad)|Android|BlackBerry|IEMobile|Kindle|Opera M(obi|ini)|Windows Phone|webOS/i.test(navigator.userAgent);
212
+ if (isMobile) {
213
+ params.set('v', 'mobile');
214
+ window.location.replace(window.location.pathname + '?' + params.toString());
215
+ }
216
+ }
217
+
207
218
  function adjustFontSize() {
208
219
  const viewportWidth = window.innerWidth;
209
220
  const calculatedSize = (viewportWidth - padding) / (cols * charRatio);
package/src/worker.ts CHANGED
@@ -80,7 +80,7 @@ app.post("/upload", async (c) => {
80
80
  }
81
81
 
82
82
  const url = new URL(c.req.url)
83
- const viewUrl = `${url.origin}/view/${id}`
83
+ const viewUrl = `${url.origin}/v/${id}`
84
84
 
85
85
  return c.json({ id, url: viewUrl })
86
86
  } catch (error) {
@@ -89,21 +89,31 @@ app.post("/upload", async (c) => {
89
89
  })
90
90
 
91
91
  // View HTML content with streaming
92
- // GET /view/:id
93
- // Query params: ?v=desktop or ?v=mobile to force a version
94
- app.get("/view/:id", async (c) => {
92
+ // GET /v/:id (short) or /view/:id (legacy)
93
+ // Query params: ?v=desktop or ?v=mobile to select version
94
+ // Server redirects mobile devices to ?v=mobile, client JS also handles redirect
95
+ async function handleView(c: any) {
95
96
  const id = c.req.param("id")
96
97
 
97
98
  if (!id || !/^[a-f0-9]{16,32}$/.test(id)) {
98
99
  return c.text("Invalid ID", 400)
99
100
  }
100
101
 
101
- // Check for forced version via query param
102
- const forcedVersion = c.req.query("v")
103
- const isMobile = forcedVersion === "mobile" || (forcedVersion !== "desktop" && isMobileDevice(c))
102
+ // Check for version query param
103
+ const version = c.req.query("v")
104
+
105
+ // If no version specified and mobile device detected, redirect to ?v=mobile
106
+ // This is a fallback - client JS also handles this redirect
107
+ if (!version && isMobileDevice(c)) {
108
+ const url = new URL(c.req.url)
109
+ url.searchParams.set("v", "mobile")
110
+ return c.redirect(url.toString(), 302)
111
+ }
104
112
 
105
- // Try to get the appropriate version
113
+ // Serve the appropriate version based on query param
114
+ const isMobile = version === "mobile"
106
115
  let html: string | null = null
116
+
107
117
  if (isMobile) {
108
118
  // Try mobile version first, fall back to desktop
109
119
  html = await c.env.CRITIQUE_KV.get(`${id}-mobile`)
@@ -120,11 +130,9 @@ app.get("/view/:id", async (c) => {
120
130
 
121
131
  // Stream the HTML content for faster initial load
122
132
  return stream(c, async (s) => {
123
- // Set content type header
124
133
  c.header("Content-Type", "text/html; charset=utf-8")
125
- // Vary by User-Agent and Sec-CH-UA-Mobile for proper caching
126
- c.header("Vary", "User-Agent, Sec-CH-UA-Mobile")
127
- c.header("Cache-Control", "public, max-age=3600")
134
+ // Cache is now safe - URL determines content, no Vary needed
135
+ c.header("Cache-Control", "public, max-age=86400")
128
136
 
129
137
  // Stream in chunks for better performance
130
138
  const chunkSize = 16 * 1024 // 16KB chunks
@@ -136,7 +144,10 @@ app.get("/view/:id", async (c) => {
136
144
  offset += chunkSize
137
145
  }
138
146
  })
139
- })
147
+ }
148
+
149
+ app.get("/v/:id", handleView)
150
+ app.get("/view/:id", handleView)
140
151
 
141
152
  // Get raw HTML content (for debugging/API access)
142
153
  // GET /raw/:id
@@ -159,8 +170,8 @@ app.get("/raw/:id", async (c) => {
159
170
  })
160
171
 
161
172
  // Check if content exists
162
- // HEAD /view/:id
163
- app.on("HEAD", "/view/:id", async (c) => {
173
+ // HEAD /v/:id or /view/:id
174
+ async function handleHead(c: any) {
164
175
  const id = c.req.param("id")
165
176
 
166
177
  if (!id || !/^[a-f0-9]{16,32}$/.test(id)) {
@@ -175,6 +186,9 @@ app.on("HEAD", "/view/:id", async (c) => {
175
186
 
176
187
  c.header("Content-Length", String(html.length))
177
188
  return c.body(null, 200)
178
- })
189
+ }
190
+
191
+ app.on("HEAD", "/v/:id", handleHead)
192
+ app.on("HEAD", "/view/:id", handleHead)
179
193
 
180
194
  export default app