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 +2 -2
- package/dist/review/review-app.test.js +3 -3
- package/dist/web-utils.d.ts +1 -1
- package/dist/web-utils.d.ts.map +1 -1
- package/dist/web-utils.js +65 -27
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +29 -1
- package/package.json +3 -3
- package/src/cli.tsx +2 -2
- package/src/review/review-app.test.tsx +3 -3
- package/src/web-utils.tsx +73 -27
- package/src/worker.tsx +34 -2
- package/src/.test-stdin-pager-tmp/binary.diff +0 -3
- package/src/.test-stdin-pager-tmp/colored.diff +0 -8
- package/src/.test-stdin-pager-tmp/context-only.diff +0 -7
- package/src/.test-stdin-pager-tmp/delete.diff +0 -9
- package/src/.test-stdin-pager-tmp/empty.diff +0 -0
- package/src/.test-stdin-pager-tmp/large.diff +0 -19
- package/src/.test-stdin-pager-tmp/multi.diff +0 -20
- package/src/.test-stdin-pager-tmp/newfile.diff +0 -12
- package/src/.test-stdin-pager-tmp/rename.diff +0 -11
- package/src/.test-stdin-pager-tmp/single.diff +0 -8
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:
|
|
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
|
-
|
|
796
|
+
Database │
|
|
797
797
|
└─────────────┘ └─────────────┘ └─────────────┘
|
|
798
|
-
|
|
798
|
+
└─────────────┘
|
|
799
799
|
|
|
800
800
|
|
|
801
801
|
src/config.ts +1-0
|
package/dist/web-utils.d.ts
CHANGED
|
@@ -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.
|
package/dist/web-utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-utils.d.ts","sourceRoot":"","sources":["../src/web-utils.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAiB,aAAa,EAAE,YAAY,EAA+B,MAAM,gBAAgB,CAAA;
|
|
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
|
-
*
|
|
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
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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
|
|
56
|
-
|
|
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
|
-
|
|
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
|
|
207
|
-
await
|
|
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
|
-
|
|
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
|
|
536
|
-
await
|
|
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 ??
|
|
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 = {
|
package/dist/worker.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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.
|
|
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.
|
|
48
|
-
"@opentuah/react": "0.1.
|
|
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:
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
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
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
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
|
|
133
|
+
async function waitForHighlightAndRenderStabilization(
|
|
108
134
|
renderer: CliRenderer,
|
|
109
135
|
renderOnce: () => Promise<void>,
|
|
110
|
-
|
|
136
|
+
maxMs: number = 2000
|
|
111
137
|
): Promise<void> {
|
|
112
|
-
|
|
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
|
-
|
|
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
|
|
319
|
-
await
|
|
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
|
-
|
|
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
|
|
729
|
-
await
|
|
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 ??
|
|
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
|
-
|
|
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
|
}
|
|
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
|