critique 0.1.121 → 0.1.123

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/dist/cli.js CHANGED
@@ -909,7 +909,7 @@ async function runWebMode(diffContent, options) {
909
909
  // after the URL is printed, so the user sees the URL faster.
910
910
  const { htmlDesktop, htmlMobile, ogImage } = await captureResponsiveHtml(diffContent, { desktopCols, mobileCols, baseRows, themeName, title: options.title, skipOgImage: true });
911
911
  log("Uploading...");
912
- const result = await uploadHtml(htmlDesktop, htmlMobile);
912
+ const result = await uploadHtml(htmlDesktop, htmlMobile, undefined, diffContent);
913
913
  log(`\nPreview URL: ${result.url}`);
914
914
  log(formatPreviewExpiry(result.expiresInDays));
915
915
  if (typeof result.expiresInDays === "number") {
@@ -937,7 +937,7 @@ async function runWebMode(diffContent, options) {
937
937
  const { renderDiffToOgImage } = await import("./image.js");
938
938
  const ogImg = await renderDiffToOgImage(diffContent, {
939
939
  themeName: "github-light",
940
- stabilizeMs: 100,
940
+ stabilizeMs: 2000,
941
941
  });
942
942
  if (ogImg) {
943
943
  await uploadOgImage(result.id, ogImg);
@@ -791,11 +791,11 @@ The diagram above should not wrap.`,
791
791
  Architecture
792
792
 
793
793
  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
794
- ┌─────────────┐
794
+ ┌─────────────┐
795
795
  │ Client │────▶│ Router │────▶│ Handler │────▶│
796
- Database │
796
+ Database │
797
797
  └─────────────┘ └─────────────┘ └─────────────┘
798
- └─────────────┘
798
+ └─────────────┘
799
799
 
800
800
 
801
801
  src/config.ts +1-0
@@ -110,7 +110,7 @@ export declare function captureReviewResponsiveHtml(options: {
110
110
  /**
111
111
  * Upload HTML to the critique.work worker
112
112
  */
113
- export declare function uploadHtml(htmlDesktop: string, htmlMobile: string, ogImage?: Buffer | null): Promise<UploadResult>;
113
+ export declare function uploadHtml(htmlDesktop: string, htmlMobile: string, ogImage?: Buffer | null, patch?: string): Promise<UploadResult>;
114
114
  /**
115
115
  * Upload OG image to an existing diff via PATCH.
116
116
  * Called in the background after the initial upload returns the URL.
@@ -1 +1 @@
1
- {"version":3,"file":"web-utils.d.ts","sourceRoot":"","sources":["../src/web-utils.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"web-utils.d.ts","sourceRoot":"","sources":["../src/web-utils.tsx"],"names":[],"mappings":"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"}
package/dist/web-utils.js CHANGED
@@ -8,6 +8,7 @@ import fs from "fs";
8
8
  import { tmpdir } from "os";
9
9
  import { join } from "path";
10
10
  import { getResolvedTheme, rgbaToHex } from "./themes.js";
11
+ import { DiffRenderable } from "@opentuah/core";
11
12
  import { loadStoredLicenseKey, loadOrCreateOwnerSecret } from "./license.js";
12
13
  const execAsync = promisify(exec);
13
14
  // Worker URL for uploading HTML previews
@@ -41,39 +42,70 @@ function getContentHeight(root) {
41
42
  return Math.ceil(maxBottom);
42
43
  }
43
44
  /**
44
- * Wait for async rendering (tree-sitter highlighting etc.) to stabilize.
45
+ * Find all DiffRenderable instances in the renderer tree.
46
+ * Walks the tree recursively checking instanceof DiffRenderable.
47
+ */
48
+ function findDiffRenderables(root) {
49
+ const results = [];
50
+ function walk(node) {
51
+ if (!node.getChildren)
52
+ return;
53
+ for (const child of node.getChildren()) {
54
+ if (child instanceof DiffRenderable) {
55
+ results.push(child);
56
+ }
57
+ walk(child);
58
+ }
59
+ }
60
+ walk(root);
61
+ return results;
62
+ }
63
+ /**
64
+ * Wait for tree-sitter syntax highlighting to complete on all diff elements,
65
+ * then wait for rendering to stabilize (no more requestRender calls).
45
66
  *
46
- * Uses an idle+max model:
47
- * - Exits when no render requests for `idleMs` (tree-sitter is done)
48
- * - Hard cap at `maxMs` to prevent unbounded waits on huge diffs
49
- * - Polls at `pollMs` granularity (finer than the idle threshold)
67
+ * Two-phase approach:
68
+ * 1. Wait for DiffRenderable.isHighlighting to become false on all diffs
69
+ * (deterministic, exits as soon as tree-sitter is done)
70
+ * 2. Wait for render idle no new requestRender calls for idleMs
71
+ * (catches deferred rebuilds like DiffRenderable.requestRebuild which
72
+ * uses queueMicrotask to schedule buildView + requestRender after
73
+ * highlighting completes)
50
74
  *
51
- * The old approach used a single `stabilizeMs` with 100ms polling, which
52
- * meant stabilizeMs=100 gave only one polling window and could miss late
53
- * highlights, while stabilizeMs=500 was wastefully slow for small diffs.
75
+ * Phase 2 is critical because DiffRenderable's split view rebuild happens
76
+ * asynchronously via microtask AFTER isHighlighting goes false. Without it,
77
+ * the captured frame may have concealed (unhighlighted) content on one side.
54
78
  */
55
- async function waitForRenderStabilization(renderer, renderOnce, stabilizeMs = 500) {
56
- // Derive idle/max from the legacy parameter:
57
- // - Interactive TUI (500ms): idleMs=200, maxMs=800
58
- // - Web/batch (100ms): idleMs=80, maxMs=400
59
- const idleMs = Math.max(Math.round(stabilizeMs * 0.4), 60);
60
- const maxMs = Math.max(stabilizeMs, Math.round(stabilizeMs * 4));
79
+ async function waitForHighlightAndRenderStabilization(renderer, renderOnce, maxMs = 2000) {
80
+ const startTime = Date.now();
61
81
  const pollMs = 20;
82
+ const idleMs = 80;
83
+ // Track render requests to detect when rendering has quiesced
62
84
  let lastRenderTime = Date.now();
63
- const startTime = lastRenderTime;
64
85
  const originalRequestRender = renderer.root.requestRender.bind(renderer.root);
65
86
  renderer.root.requestRender = function () {
66
87
  lastRenderTime = Date.now();
67
88
  originalRequestRender();
68
89
  };
69
- while (true) {
90
+ // Do one render cycle to kick off highlighting
91
+ await renderOnce();
92
+ // Phase 1: wait for isHighlighting to become false on all diffs
93
+ while (Date.now() - startTime < maxMs) {
94
+ const diffs = findDiffRenderables(renderer.root);
95
+ if (diffs.length === 0 || diffs.every(d => !d.isHighlighting)) {
96
+ break;
97
+ }
98
+ await new Promise(resolve => setTimeout(resolve, pollMs));
99
+ await renderOnce();
100
+ }
101
+ // Phase 2: wait for render to stabilize (catches deferred rebuilds)
102
+ // Reset the render timestamp so we wait for any post-highlight renders
103
+ lastRenderTime = Date.now();
104
+ await renderOnce();
105
+ while (Date.now() - startTime < maxMs) {
70
106
  const now = Date.now();
71
- // Exit if idle long enough (no render requests for idleMs)
72
107
  if (now - lastRenderTime >= idleMs)
73
108
  break;
74
- // Exit if hard cap reached
75
- if (now - startTime >= maxMs)
76
- break;
77
109
  await new Promise(resolve => setTimeout(resolve, pollMs));
78
110
  await renderOnce();
79
111
  }
@@ -203,8 +235,8 @@ async function renderDiffToFrameWithSectionPositions(diffContent, options) {
203
235
  resize(options.cols, finalHeight);
204
236
  await renderOnce();
205
237
  }
206
- // Wait for async highlighting to complete (only once at the end)
207
- await waitForRenderStabilization(renderer, renderOnce, options.stabilizeMs ?? 500);
238
+ // Wait for tree-sitter highlighting + render stabilization
239
+ await waitForHighlightAndRenderStabilization(renderer, renderOnce, options.stabilizeMs ?? 2000);
208
240
  const sectionPositions = [];
209
241
  for (let idx = 0; idx < fileNames.length; idx++) {
210
242
  const section = fileSectionRefs.get(idx);
@@ -419,7 +451,9 @@ export async function captureResponsiveHtml(diffContent, options) {
419
451
  // These act as upper bounds to prevent runaway memory usage
420
452
  const desktopRows = Math.max(options.baseRows * 3, 5000);
421
453
  const mobileRows = Math.max(Math.ceil(desktopRows * (options.desktopCols / options.mobileCols)), 10000);
422
- const stabilizeMs = options.stabilizeMs ?? 100;
454
+ // With deterministic isHighlighting detection, stabilizeMs is just a safety cap.
455
+ // The function exits instantly when highlighting completes, so 2000ms is fine.
456
+ const stabilizeMs = options.stabilizeMs ?? 2000;
423
457
  // Run all renders in parallel: desktop HTML, mobile HTML, and OG image
424
458
  const ogImagePromise = options.skipOgImage
425
459
  ? Promise.resolve(null)
@@ -532,8 +566,8 @@ export async function renderReviewToFrame(options) {
532
566
  resize(options.cols, finalHeight);
533
567
  await renderOnce();
534
568
  }
535
- // Wait for async highlighting to complete (only once at the end)
536
- await waitForRenderStabilization(renderer, renderOnce, options.stabilizeMs ?? 500);
569
+ // Wait for tree-sitter highlighting + render stabilization
570
+ await waitForHighlightAndRenderStabilization(renderer, renderOnce, options.stabilizeMs ?? 2000);
537
571
  // Capture the final frame
538
572
  const buffer = renderer.currentRenderBuffer;
539
573
  const cursorState = renderer.getCursorState();
@@ -575,7 +609,7 @@ export async function captureReviewResponsiveHtml(options) {
575
609
  // These act as upper bounds to prevent runaway memory usage
576
610
  const desktopRows = Math.max(options.baseRows * 3, 5000);
577
611
  const mobileRows = Math.max(Math.ceil(desktopRows * (options.desktopCols / options.mobileCols)), 10000);
578
- const stabilizeMs = options.stabilizeMs ?? 100;
612
+ const stabilizeMs = options.stabilizeMs ?? 2000;
579
613
  // Generate OG image from first few hunks' raw diff (in parallel with HTML renders)
580
614
  const ogImagePromise = options.skipOgImage
581
615
  ? Promise.resolve(null)
@@ -623,7 +657,7 @@ export async function captureReviewResponsiveHtml(options) {
623
657
  /**
624
658
  * Upload HTML to the critique.work worker
625
659
  */
626
- export async function uploadHtml(htmlDesktop, htmlMobile, ogImage) {
660
+ export async function uploadHtml(htmlDesktop, htmlMobile, ogImage, patch) {
627
661
  const body = {
628
662
  html: htmlDesktop,
629
663
  htmlMobile,
@@ -632,6 +666,10 @@ export async function uploadHtml(htmlDesktop, htmlMobile, ogImage) {
632
666
  if (ogImage) {
633
667
  body.ogImage = ogImage.toString("base64");
634
668
  }
669
+ // Include raw unified diff (patch) for programmatic access
670
+ if (patch) {
671
+ body.patch = patch;
672
+ }
635
673
  const licenseKey = loadStoredLicenseKey();
636
674
  const ownerSecret = loadOrCreateOwnerSecret();
637
675
  const headers = {
@@ -1 +1 @@
1
- {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.tsx"],"names":[],"mappings":"AAAA,gCAAgC;AAMhC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAG3B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAQ5D,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAEnD,KAAK,QAAQ,GAAG;IACd,WAAW,EAAE,WAAW,CAAA;IACxB,iBAAiB,EAAE,MAAM,CAAA;IACzB,qBAAqB,EAAE,MAAM,CAAA;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE;QAAE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG;YAAE,QAAQ,IAAI,MAAM,CAAA;SAAE,CAAC;QAAC,GAAG,CAAC,EAAE,EAAE,GAAG,GAAG;YAAE,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;SAAE,CAAA;KAAE,CAAA;CAChI,CAAA;AAUD,QAAA,MAAM,GAAG;cAAwB,QAAQ;eAAa;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE;yCAAK,CAAA;AAo2B7E,eAAe,GAAG,CAAA"}
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.tsx"],"names":[],"mappings":"AAAA,gCAAgC;AAMhC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAG3B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAQ5D,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAEnD,KAAK,QAAQ,GAAG;IACd,WAAW,EAAE,WAAW,CAAA;IACxB,iBAAiB,EAAE,MAAM,CAAA;IACzB,qBAAqB,EAAE,MAAM,CAAA;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE;QAAE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG;YAAE,QAAQ,IAAI,MAAM,CAAA;SAAE,CAAC;QAAC,GAAG,CAAC,EAAE,EAAE,GAAG,GAAG;YAAE,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;SAAE,CAAA;KAAE,CAAA;CAChI,CAAA;AAUD,QAAA,MAAM,GAAG;cAAwB,QAAQ;eAAa;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE;yCAAK,CAAA;AAo4B7E,eAAe,GAAG,CAAA"}
package/dist/worker.js CHANGED
@@ -147,6 +147,12 @@ class CritiqueKv {
147
147
  async setMobileHtml(id, html, ttlSeconds) {
148
148
  await this.kv.put(`${id}-mobile`, html, ttlSeconds ? { expirationTtl: ttlSeconds } : undefined);
149
149
  }
150
+ async getPatch(id) {
151
+ return this.kv.get(`${id}-patch`);
152
+ }
153
+ async setPatch(id, patch, ttlSeconds) {
154
+ await this.kv.put(`${id}-patch`, patch, ttlSeconds ? { expirationTtl: ttlSeconds } : undefined);
155
+ }
150
156
  async getOgImage(id) {
151
157
  return this.kv.get(`og-${id}`, "arrayBuffer");
152
158
  }
@@ -182,6 +188,7 @@ class CritiqueKv {
182
188
  await Promise.all([
183
189
  this.kv.delete(id),
184
190
  this.kv.delete(`${id}-mobile`),
191
+ this.kv.delete(`${id}-patch`),
185
192
  this.kv.delete(`og-${id}`),
186
193
  this.kv.delete(`owner:${id}`),
187
194
  ]);
@@ -336,6 +343,10 @@ app.post("/upload", async (c) => {
336
343
  if (htmlMobile && typeof htmlMobile === "string") {
337
344
  await kv.setMobileHtml(id, htmlMobile, ttlSeconds);
338
345
  }
346
+ // Store raw patch (unified diff) if provided
347
+ if (body.patch && typeof body.patch === "string") {
348
+ await kv.setPatch(id, body.patch, ttlSeconds);
349
+ }
339
350
  const viewUrl = `${url.origin}/v/${id}`;
340
351
  return c.json({
341
352
  id,
@@ -539,8 +550,25 @@ app.post("/stripe/webhook", async (c) => {
539
550
  // Query params: ?v=desktop or ?v=mobile to select version
540
551
  // Server redirects mobile devices to ?v=mobile, client JS also handles redirect
541
552
  async function handleView(c) {
542
- const id = c.req.param("id");
553
+ let id = c.req.param("id") || "";
543
554
  const kv = new CritiqueKv(c.env.CRITIQUE_KV);
555
+ // Serve raw patch (unified diff) when URL ends with .patch
556
+ // e.g. /v/abc123.patch → strips suffix, returns text/plain
557
+ if (id.endsWith(".patch")) {
558
+ id = id.slice(0, -".patch".length);
559
+ if (!id || !/^[a-f0-9]{16,32}$/.test(id)) {
560
+ return c.text("Invalid ID", 400);
561
+ }
562
+ const patch = await kv.getPatch(id);
563
+ if (!patch) {
564
+ return c.text("Not found", 404);
565
+ }
566
+ return c.text(patch, 200, {
567
+ "Content-Type": "text/plain; charset=utf-8",
568
+ "Content-Disposition": `inline; filename="${id}.patch"`,
569
+ "Cache-Control": "public, max-age=86400",
570
+ });
571
+ }
544
572
  if (!id || !/^[a-f0-9]{16,32}$/.test(id)) {
545
573
  return c.text("Invalid ID", 400);
546
574
  }
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.121",
5
+ "version": "0.1.123",
6
6
  "license": "MIT",
7
7
  "private": false,
8
8
  "bin": "./dist/cli.js",
@@ -44,8 +44,8 @@
44
44
  "dependencies": {
45
45
  "@agentclientprotocol/sdk": "^0.13.1",
46
46
  "@clack/prompts": "1.0.0-alpha.9",
47
- "@opentuah/core": "0.1.96",
48
- "@opentuah/react": "0.1.96",
47
+ "@opentuah/core": "0.1.97",
48
+ "@opentuah/react": "0.1.97",
49
49
  "@parcel/watcher": "^2.5.6",
50
50
  "diff": "^8.0.2",
51
51
  "goke": "^6.1.3",
package/src/cli.tsx CHANGED
@@ -1156,7 +1156,7 @@ async function runWebMode(
1156
1156
 
1157
1157
  log("Uploading...");
1158
1158
 
1159
- const result = await uploadHtml(htmlDesktop, htmlMobile);
1159
+ const result = await uploadHtml(htmlDesktop, htmlMobile, undefined, diffContent);
1160
1160
 
1161
1161
  log(`\nPreview URL: ${result.url}`);
1162
1162
  log(formatPreviewExpiry(result.expiresInDays));
@@ -1187,7 +1187,7 @@ async function runWebMode(
1187
1187
  const { renderDiffToOgImage } = await import("./image.js");
1188
1188
  const ogImg = await renderDiffToOgImage(diffContent, {
1189
1189
  themeName: "github-light",
1190
- stabilizeMs: 100,
1190
+ stabilizeMs: 2000,
1191
1191
  });
1192
1192
  if (ogImg) {
1193
1193
  await uploadOgImage(result.id, ogImg);
@@ -928,11 +928,11 @@ The diagram above should not wrap.`,
928
928
  Architecture
929
929
 
930
930
  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
931
- ┌─────────────┐
931
+ ┌─────────────┐
932
932
  │ Client │────▶│ Router │────▶│ Handler │────▶│
933
- Database │
933
+ Database │
934
934
  └─────────────┘ └─────────────┘ └─────────────┘
935
- └─────────────┘
935
+ └─────────────┘
936
936
 
937
937
 
938
938
  src/config.ts +1-0
package/src/web-utils.tsx CHANGED
@@ -9,6 +9,7 @@ import { tmpdir } from "os"
9
9
  import { join } from "path"
10
10
  import { getResolvedTheme, rgbaToHex } from "./themes.js"
11
11
  import type { BoxRenderable, CapturedFrame, CapturedLine, RootRenderable, CliRenderer } from "@opentuah/core"
12
+ import { DiffRenderable } from "@opentuah/core"
12
13
  import type { IndexedHunk, ReviewYaml } from "./review/types.js"
13
14
  import { loadStoredLicenseKey, loadOrCreateOwnerSecret } from "./license.js"
14
15
 
@@ -93,43 +94,80 @@ function getContentHeight(root: RootRenderable): number {
93
94
  }
94
95
 
95
96
  /**
96
- * Wait for async rendering (tree-sitter highlighting etc.) to stabilize.
97
+ * Find all DiffRenderable instances in the renderer tree.
98
+ * Walks the tree recursively checking instanceof DiffRenderable.
99
+ */
100
+ function findDiffRenderables(root: RootRenderable): InstanceType<typeof DiffRenderable>[] {
101
+ const results: InstanceType<typeof DiffRenderable>[] = []
102
+
103
+ function walk(node: { getChildren?: () => any[] }) {
104
+ if (!node.getChildren) return
105
+ for (const child of node.getChildren()) {
106
+ if (child instanceof DiffRenderable) {
107
+ results.push(child)
108
+ }
109
+ walk(child)
110
+ }
111
+ }
112
+
113
+ walk(root)
114
+ return results
115
+ }
116
+
117
+ /**
118
+ * Wait for tree-sitter syntax highlighting to complete on all diff elements,
119
+ * then wait for rendering to stabilize (no more requestRender calls).
97
120
  *
98
- * Uses an idle+max model:
99
- * - Exits when no render requests for `idleMs` (tree-sitter is done)
100
- * - Hard cap at `maxMs` to prevent unbounded waits on huge diffs
101
- * - Polls at `pollMs` granularity (finer than the idle threshold)
121
+ * Two-phase approach:
122
+ * 1. Wait for DiffRenderable.isHighlighting to become false on all diffs
123
+ * (deterministic, exits as soon as tree-sitter is done)
124
+ * 2. Wait for render idle no new requestRender calls for idleMs
125
+ * (catches deferred rebuilds like DiffRenderable.requestRebuild which
126
+ * uses queueMicrotask to schedule buildView + requestRender after
127
+ * highlighting completes)
102
128
  *
103
- * The old approach used a single `stabilizeMs` with 100ms polling, which
104
- * meant stabilizeMs=100 gave only one polling window and could miss late
105
- * highlights, while stabilizeMs=500 was wastefully slow for small diffs.
129
+ * Phase 2 is critical because DiffRenderable's split view rebuild happens
130
+ * asynchronously via microtask AFTER isHighlighting goes false. Without it,
131
+ * the captured frame may have concealed (unhighlighted) content on one side.
106
132
  */
107
- async function waitForRenderStabilization(
133
+ async function waitForHighlightAndRenderStabilization(
108
134
  renderer: CliRenderer,
109
135
  renderOnce: () => Promise<void>,
110
- stabilizeMs: number = 500
136
+ maxMs: number = 2000
111
137
  ): Promise<void> {
112
- // Derive idle/max from the legacy parameter:
113
- // - Interactive TUI (500ms): idleMs=200, maxMs=800
114
- // - Web/batch (100ms): idleMs=80, maxMs=400
115
- const idleMs = Math.max(Math.round(stabilizeMs * 0.4), 60)
116
- const maxMs = Math.max(stabilizeMs, Math.round(stabilizeMs * 4))
138
+ const startTime = Date.now()
117
139
  const pollMs = 20
140
+ const idleMs = 80
118
141
 
142
+ // Track render requests to detect when rendering has quiesced
119
143
  let lastRenderTime = Date.now()
120
- const startTime = lastRenderTime
121
144
  const originalRequestRender = renderer.root.requestRender.bind(renderer.root)
122
145
  renderer.root.requestRender = function() {
123
146
  lastRenderTime = Date.now()
124
147
  originalRequestRender()
125
148
  }
126
149
 
127
- while (true) {
150
+ // Do one render cycle to kick off highlighting
151
+ await renderOnce()
152
+
153
+ // Phase 1: wait for isHighlighting to become false on all diffs
154
+ while (Date.now() - startTime < maxMs) {
155
+ const diffs = findDiffRenderables(renderer.root)
156
+ if (diffs.length === 0 || diffs.every(d => !d.isHighlighting)) {
157
+ break
158
+ }
159
+ await new Promise(resolve => setTimeout(resolve, pollMs))
160
+ await renderOnce()
161
+ }
162
+
163
+ // Phase 2: wait for render to stabilize (catches deferred rebuilds)
164
+ // Reset the render timestamp so we wait for any post-highlight renders
165
+ lastRenderTime = Date.now()
166
+ await renderOnce()
167
+
168
+ while (Date.now() - startTime < maxMs) {
128
169
  const now = Date.now()
129
- // Exit if idle long enough (no render requests for idleMs)
130
170
  if (now - lastRenderTime >= idleMs) break
131
- // Exit if hard cap reached
132
- if (now - startTime >= maxMs) break
133
171
  await new Promise(resolve => setTimeout(resolve, pollMs))
134
172
  await renderOnce()
135
173
  }
@@ -315,8 +353,8 @@ async function renderDiffToFrameWithSectionPositions(
315
353
  await renderOnce()
316
354
  }
317
355
 
318
- // Wait for async highlighting to complete (only once at the end)
319
- await waitForRenderStabilization(renderer, renderOnce, options.stabilizeMs ?? 500)
356
+ // Wait for tree-sitter highlighting + render stabilization
357
+ await waitForHighlightAndRenderStabilization(renderer, renderOnce, options.stabilizeMs ?? 2000)
320
358
 
321
359
  const sectionPositions: FileSectionPosition[] = []
322
360
  for (let idx = 0; idx < fileNames.length; idx++) {
@@ -585,7 +623,9 @@ export async function captureResponsiveHtml(
585
623
  // These act as upper bounds to prevent runaway memory usage
586
624
  const desktopRows = Math.max(options.baseRows * 3, 5000)
587
625
  const mobileRows = Math.max(Math.ceil(desktopRows * (options.desktopCols / options.mobileCols)), 10000)
588
- const stabilizeMs = options.stabilizeMs ?? 100
626
+ // With deterministic isHighlighting detection, stabilizeMs is just a safety cap.
627
+ // The function exits instantly when highlighting completes, so 2000ms is fine.
628
+ const stabilizeMs = options.stabilizeMs ?? 2000
589
629
 
590
630
  // Run all renders in parallel: desktop HTML, mobile HTML, and OG image
591
631
  const ogImagePromise = options.skipOgImage
@@ -725,8 +765,8 @@ export async function renderReviewToFrame(
725
765
  await renderOnce()
726
766
  }
727
767
 
728
- // Wait for async highlighting to complete (only once at the end)
729
- await waitForRenderStabilization(renderer, renderOnce, options.stabilizeMs ?? 500)
768
+ // Wait for tree-sitter highlighting + render stabilization
769
+ await waitForHighlightAndRenderStabilization(renderer, renderOnce, options.stabilizeMs ?? 2000)
730
770
 
731
771
  // Capture the final frame
732
772
  const buffer = renderer.currentRenderBuffer
@@ -792,7 +832,7 @@ export async function captureReviewResponsiveHtml(
792
832
  // These act as upper bounds to prevent runaway memory usage
793
833
  const desktopRows = Math.max(options.baseRows * 3, 5000)
794
834
  const mobileRows = Math.max(Math.ceil(desktopRows * (options.desktopCols / options.mobileCols)), 10000)
795
- const stabilizeMs = options.stabilizeMs ?? 100
835
+ const stabilizeMs = options.stabilizeMs ?? 2000
796
836
 
797
837
  // Generate OG image from first few hunks' raw diff (in parallel with HTML renders)
798
838
  const ogImagePromise = options.skipOgImage
@@ -845,7 +885,8 @@ export async function captureReviewResponsiveHtml(
845
885
  export async function uploadHtml(
846
886
  htmlDesktop: string,
847
887
  htmlMobile: string,
848
- ogImage?: Buffer | null
888
+ ogImage?: Buffer | null,
889
+ patch?: string,
849
890
  ): Promise<UploadResult> {
850
891
  const body: Record<string, string> = {
851
892
  html: htmlDesktop,
@@ -857,6 +898,11 @@ export async function uploadHtml(
857
898
  body.ogImage = ogImage.toString("base64")
858
899
  }
859
900
 
901
+ // Include raw unified diff (patch) for programmatic access
902
+ if (patch) {
903
+ body.patch = patch
904
+ }
905
+
860
906
  const licenseKey = loadStoredLicenseKey()
861
907
  const ownerSecret = loadOrCreateOwnerSecret()
862
908
  const headers: Record<string, string> = {
package/src/worker.tsx CHANGED
@@ -200,6 +200,14 @@ class CritiqueKv {
200
200
  await this.kv.put(`${id}-mobile`, html, ttlSeconds ? { expirationTtl: ttlSeconds } : undefined)
201
201
  }
202
202
 
203
+ async getPatch(id: string): Promise<string | null> {
204
+ return this.kv.get(`${id}-patch`)
205
+ }
206
+
207
+ async setPatch(id: string, patch: string, ttlSeconds?: number): Promise<void> {
208
+ await this.kv.put(`${id}-patch`, patch, ttlSeconds ? { expirationTtl: ttlSeconds } : undefined)
209
+ }
210
+
203
211
  async getOgImage(id: string): Promise<ArrayBuffer | null> {
204
212
  return this.kv.get(`og-${id}`, "arrayBuffer")
205
213
  }
@@ -245,6 +253,7 @@ class CritiqueKv {
245
253
  await Promise.all([
246
254
  this.kv.delete(id),
247
255
  this.kv.delete(`${id}-mobile`),
256
+ this.kv.delete(`${id}-patch`),
248
257
  this.kv.delete(`og-${id}`),
249
258
  this.kv.delete(`owner:${id}`),
250
259
  ])
@@ -376,7 +385,7 @@ function extractTitle(html: string): string {
376
385
  app.post("/upload", async (c) => {
377
386
  try {
378
387
  const kv = new CritiqueKv(c.env.CRITIQUE_KV)
379
- const body = await c.req.json<{ html: string; htmlMobile?: string; ogImage?: string }>()
388
+ const body = await c.req.json<{ html: string; htmlMobile?: string; ogImage?: string; patch?: string }>()
380
389
 
381
390
  if (!body.html || typeof body.html !== "string") {
382
391
  return c.json({ error: "Missing or invalid 'html' field" }, 400)
@@ -433,6 +442,11 @@ app.post("/upload", async (c) => {
433
442
  await kv.setMobileHtml(id, htmlMobile, ttlSeconds)
434
443
  }
435
444
 
445
+ // Store raw patch (unified diff) if provided
446
+ if (body.patch && typeof body.patch === "string") {
447
+ await kv.setPatch(id, body.patch, ttlSeconds)
448
+ }
449
+
436
450
  const viewUrl = `${url.origin}/v/${id}`
437
451
 
438
452
  return c.json({
@@ -705,10 +719,28 @@ app.post("/stripe/webhook", async (c) => {
705
719
  // Query params: ?v=desktop or ?v=mobile to select version
706
720
  // Server redirects mobile devices to ?v=mobile, client JS also handles redirect
707
721
  async function handleView(c: any) {
708
- const id = c.req.param("id")
722
+ let id: string = c.req.param("id") || ""
709
723
 
710
724
  const kv = new CritiqueKv(c.env.CRITIQUE_KV)
711
725
 
726
+ // Serve raw patch (unified diff) when URL ends with .patch
727
+ // e.g. /v/abc123.patch → strips suffix, returns text/plain
728
+ if (id.endsWith(".patch")) {
729
+ id = id.slice(0, -".patch".length)
730
+ if (!id || !/^[a-f0-9]{16,32}$/.test(id)) {
731
+ return c.text("Invalid ID", 400)
732
+ }
733
+ const patch = await kv.getPatch(id)
734
+ if (!patch) {
735
+ return c.text("Not found", 404)
736
+ }
737
+ return c.text(patch, 200, {
738
+ "Content-Type": "text/plain; charset=utf-8",
739
+ "Content-Disposition": `inline; filename="${id}.patch"`,
740
+ "Cache-Control": "public, max-age=86400",
741
+ })
742
+ }
743
+
712
744
  if (!id || !/^[a-f0-9]{16,32}$/.test(id)) {
713
745
  return c.text("Invalid ID", 400)
714
746
  }
@@ -1,3 +0,0 @@
1
- diff --git a/logo.png b/logo.png
2
- new file mode 100644
3
- Binary files /dev/null and b/logo.png differ
@@ -1,8 +0,0 @@
1
- diff --git a/src/hello.ts b/src/hello.ts
2
- --- a/src/hello.ts
3
- +++ b/src/hello.ts
4
- @@ -1,3 +1,3 @@
5
- const greeting = 'hello'
6
- -console.log(greeting)
7
- +console.log(greeting + ' world')
8
- export default greeting
@@ -1,7 +0,0 @@
1
- diff --git a/readme.md b/readme.md
2
- --- a/readme.md
3
- +++ b/readme.md
4
- @@ -1,3 +1,3 @@
5
- # My Project
6
-
7
- Some description
@@ -1,9 +0,0 @@
1
- diff --git a/src/deprecated.ts b/src/deprecated.ts
2
- deleted file mode 100644
3
- --- a/src/deprecated.ts
4
- +++ /dev/null
5
- @@ -1,4 +0,0 @@
6
- -// This module is no longer needed
7
- -export function oldHelper() {
8
- - return 'deprecated'
9
- -}
File without changes
@@ -1,19 +0,0 @@
1
- diff --git a/config.json b/config.json
2
- --- a/config.json
3
- +++ b/config.json
4
- @@ -1,9 +1,11 @@
5
- {
6
- - "name": "my-app",
7
- + "name": "my-awesome-app",
8
- - "version": "1.0.0",
9
- + "version": "2.0.0",
10
- "description": "A sample app",
11
- - "main": "index.js",
12
- + "main": "dist/index.js",
13
- + "types": "dist/index.d.ts",
14
- "scripts": {
15
- - "build": "tsc"
16
- + "build": "tsc --project tsconfig.build.json",
17
- + "test": "bun test"
18
- }
19
- }
@@ -1,20 +0,0 @@
1
- diff --git a/src/index.ts b/src/index.ts
2
- --- a/src/index.ts
3
- +++ b/src/index.ts
4
- @@ -1,4 +1,6 @@
5
- import { App } from './app'
6
- +import { Logger } from './logger'
7
-
8
- const app = new App()
9
- +const logger = new Logger()
10
- app.start()
11
- diff --git a/src/logger.ts b/src/logger.ts
12
- new file mode 100644
13
- --- /dev/null
14
- +++ b/src/logger.ts
15
- @@ -0,0 +1,5 @@
16
- +export class Logger {
17
- + log(msg: string) {
18
- + console.log(`[LOG] ${msg}`)
19
- + }
20
- +}
@@ -1,12 +0,0 @@
1
- diff --git a/src/utils.ts b/src/utils.ts
2
- new file mode 100644
3
- --- /dev/null
4
- +++ b/src/utils.ts
5
- @@ -0,0 +1,7 @@
6
- +export function clamp(value: number, min: number, max: number): number {
7
- + return Math.min(Math.max(value, min), max)
8
- +}
9
- +
10
- +export function identity<T>(x: T): T {
11
- + return x
12
- +}
@@ -1,11 +0,0 @@
1
- diff --git a/src/old-name.ts b/src/new-name.ts
2
- similarity index 90%
3
- rename from src/old-name.ts
4
- rename to src/new-name.ts
5
- --- a/src/old-name.ts
6
- +++ b/src/new-name.ts
7
- @@ -1,3 +1,3 @@
8
- -export const name = 'old'
9
- +export const name = 'new'
10
- export const version = 1
11
- export default name
@@ -1,8 +0,0 @@
1
- diff --git a/src/hello.ts b/src/hello.ts
2
- --- a/src/hello.ts
3
- +++ b/src/hello.ts
4
- @@ -1,3 +1,3 @@
5
- const greeting = 'hello'
6
- -console.log(greeting)
7
- +console.log(greeting + ' world')
8
- export default greeting