@demon-utils/playwright 0.1.3 → 0.1.6

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/index.js CHANGED
@@ -1,3 +1,377 @@
1
+ var __create = Object.create;
2
+ var __getProtoOf = Object.getPrototypeOf;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __toESM = (mod, isNodeMode, target) => {
7
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
8
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
9
+ for (let key of __getOwnPropNames(mod))
10
+ if (!__hasOwnProp.call(to, key))
11
+ __defProp(to, key, {
12
+ get: () => mod[key],
13
+ enumerable: true
14
+ });
15
+ return to;
16
+ };
17
+ var __export = (target, all) => {
18
+ for (var name in all)
19
+ __defProp(target, name, {
20
+ get: all[name],
21
+ enumerable: true,
22
+ configurable: true,
23
+ set: (newValue) => all[name] = () => newValue
24
+ });
25
+ };
26
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
27
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
28
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
29
+ }) : x)(function(x) {
30
+ if (typeof require !== "undefined")
31
+ return require.apply(this, arguments);
32
+ throw Error('Dynamic require of "' + x + '" is not supported');
33
+ });
34
+
35
+ // node:path
36
+ var exports_path = {};
37
+ __export(exports_path, {
38
+ sep: () => sep,
39
+ resolve: () => resolve,
40
+ relative: () => relative,
41
+ posix: () => posix,
42
+ parse: () => parse,
43
+ normalize: () => normalize,
44
+ join: () => join,
45
+ isAbsolute: () => isAbsolute,
46
+ format: () => format,
47
+ extname: () => extname,
48
+ dirname: () => dirname,
49
+ delimiter: () => delimiter,
50
+ default: () => path_default,
51
+ basename: () => basename,
52
+ _makeLong: () => _makeLong
53
+ });
54
+ function assertPath(path) {
55
+ if (typeof path !== "string")
56
+ throw TypeError("Path must be a string. Received " + JSON.stringify(path));
57
+ }
58
+ function normalizeStringPosix(path, allowAboveRoot) {
59
+ var res = "", lastSegmentLength = 0, lastSlash = -1, dots = 0, code;
60
+ for (var i = 0;i <= path.length; ++i) {
61
+ if (i < path.length)
62
+ code = path.charCodeAt(i);
63
+ else if (code === 47)
64
+ break;
65
+ else
66
+ code = 47;
67
+ if (code === 47) {
68
+ if (lastSlash === i - 1 || dots === 1)
69
+ ;
70
+ else if (lastSlash !== i - 1 && dots === 2) {
71
+ if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) {
72
+ if (res.length > 2) {
73
+ var lastSlashIndex = res.lastIndexOf("/");
74
+ if (lastSlashIndex !== res.length - 1) {
75
+ if (lastSlashIndex === -1)
76
+ res = "", lastSegmentLength = 0;
77
+ else
78
+ res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
79
+ lastSlash = i, dots = 0;
80
+ continue;
81
+ }
82
+ } else if (res.length === 2 || res.length === 1) {
83
+ res = "", lastSegmentLength = 0, lastSlash = i, dots = 0;
84
+ continue;
85
+ }
86
+ }
87
+ if (allowAboveRoot) {
88
+ if (res.length > 0)
89
+ res += "/..";
90
+ else
91
+ res = "..";
92
+ lastSegmentLength = 2;
93
+ }
94
+ } else {
95
+ if (res.length > 0)
96
+ res += "/" + path.slice(lastSlash + 1, i);
97
+ else
98
+ res = path.slice(lastSlash + 1, i);
99
+ lastSegmentLength = i - lastSlash - 1;
100
+ }
101
+ lastSlash = i, dots = 0;
102
+ } else if (code === 46 && dots !== -1)
103
+ ++dots;
104
+ else
105
+ dots = -1;
106
+ }
107
+ return res;
108
+ }
109
+ function _format(sep, pathObject) {
110
+ var dir = pathObject.dir || pathObject.root, base = pathObject.base || (pathObject.name || "") + (pathObject.ext || "");
111
+ if (!dir)
112
+ return base;
113
+ if (dir === pathObject.root)
114
+ return dir + base;
115
+ return dir + sep + base;
116
+ }
117
+ function resolve() {
118
+ var resolvedPath = "", resolvedAbsolute = false, cwd;
119
+ for (var i = arguments.length - 1;i >= -1 && !resolvedAbsolute; i--) {
120
+ var path;
121
+ if (i >= 0)
122
+ path = arguments[i];
123
+ else {
124
+ if (cwd === undefined)
125
+ cwd = process.cwd();
126
+ path = cwd;
127
+ }
128
+ if (assertPath(path), path.length === 0)
129
+ continue;
130
+ resolvedPath = path + "/" + resolvedPath, resolvedAbsolute = path.charCodeAt(0) === 47;
131
+ }
132
+ if (resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute), resolvedAbsolute)
133
+ if (resolvedPath.length > 0)
134
+ return "/" + resolvedPath;
135
+ else
136
+ return "/";
137
+ else if (resolvedPath.length > 0)
138
+ return resolvedPath;
139
+ else
140
+ return ".";
141
+ }
142
+ function normalize(path) {
143
+ if (assertPath(path), path.length === 0)
144
+ return ".";
145
+ var isAbsolute = path.charCodeAt(0) === 47, trailingSeparator = path.charCodeAt(path.length - 1) === 47;
146
+ if (path = normalizeStringPosix(path, !isAbsolute), path.length === 0 && !isAbsolute)
147
+ path = ".";
148
+ if (path.length > 0 && trailingSeparator)
149
+ path += "/";
150
+ if (isAbsolute)
151
+ return "/" + path;
152
+ return path;
153
+ }
154
+ function isAbsolute(path) {
155
+ return assertPath(path), path.length > 0 && path.charCodeAt(0) === 47;
156
+ }
157
+ function join() {
158
+ if (arguments.length === 0)
159
+ return ".";
160
+ var joined;
161
+ for (var i = 0;i < arguments.length; ++i) {
162
+ var arg = arguments[i];
163
+ if (assertPath(arg), arg.length > 0)
164
+ if (joined === undefined)
165
+ joined = arg;
166
+ else
167
+ joined += "/" + arg;
168
+ }
169
+ if (joined === undefined)
170
+ return ".";
171
+ return normalize(joined);
172
+ }
173
+ function relative(from, to) {
174
+ if (assertPath(from), assertPath(to), from === to)
175
+ return "";
176
+ if (from = resolve(from), to = resolve(to), from === to)
177
+ return "";
178
+ var fromStart = 1;
179
+ for (;fromStart < from.length; ++fromStart)
180
+ if (from.charCodeAt(fromStart) !== 47)
181
+ break;
182
+ var fromEnd = from.length, fromLen = fromEnd - fromStart, toStart = 1;
183
+ for (;toStart < to.length; ++toStart)
184
+ if (to.charCodeAt(toStart) !== 47)
185
+ break;
186
+ var toEnd = to.length, toLen = toEnd - toStart, length = fromLen < toLen ? fromLen : toLen, lastCommonSep = -1, i = 0;
187
+ for (;i <= length; ++i) {
188
+ if (i === length) {
189
+ if (toLen > length) {
190
+ if (to.charCodeAt(toStart + i) === 47)
191
+ return to.slice(toStart + i + 1);
192
+ else if (i === 0)
193
+ return to.slice(toStart + i);
194
+ } else if (fromLen > length) {
195
+ if (from.charCodeAt(fromStart + i) === 47)
196
+ lastCommonSep = i;
197
+ else if (i === 0)
198
+ lastCommonSep = 0;
199
+ }
200
+ break;
201
+ }
202
+ var fromCode = from.charCodeAt(fromStart + i), toCode = to.charCodeAt(toStart + i);
203
+ if (fromCode !== toCode)
204
+ break;
205
+ else if (fromCode === 47)
206
+ lastCommonSep = i;
207
+ }
208
+ var out = "";
209
+ for (i = fromStart + lastCommonSep + 1;i <= fromEnd; ++i)
210
+ if (i === fromEnd || from.charCodeAt(i) === 47)
211
+ if (out.length === 0)
212
+ out += "..";
213
+ else
214
+ out += "/..";
215
+ if (out.length > 0)
216
+ return out + to.slice(toStart + lastCommonSep);
217
+ else {
218
+ if (toStart += lastCommonSep, to.charCodeAt(toStart) === 47)
219
+ ++toStart;
220
+ return to.slice(toStart);
221
+ }
222
+ }
223
+ function _makeLong(path) {
224
+ return path;
225
+ }
226
+ function dirname(path) {
227
+ if (assertPath(path), path.length === 0)
228
+ return ".";
229
+ var code = path.charCodeAt(0), hasRoot = code === 47, end = -1, matchedSlash = true;
230
+ for (var i = path.length - 1;i >= 1; --i)
231
+ if (code = path.charCodeAt(i), code === 47) {
232
+ if (!matchedSlash) {
233
+ end = i;
234
+ break;
235
+ }
236
+ } else
237
+ matchedSlash = false;
238
+ if (end === -1)
239
+ return hasRoot ? "/" : ".";
240
+ if (hasRoot && end === 1)
241
+ return "//";
242
+ return path.slice(0, end);
243
+ }
244
+ function basename(path, ext) {
245
+ if (ext !== undefined && typeof ext !== "string")
246
+ throw TypeError('"ext" argument must be a string');
247
+ assertPath(path);
248
+ var start = 0, end = -1, matchedSlash = true, i;
249
+ if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
250
+ if (ext.length === path.length && ext === path)
251
+ return "";
252
+ var extIdx = ext.length - 1, firstNonSlashEnd = -1;
253
+ for (i = path.length - 1;i >= 0; --i) {
254
+ var code = path.charCodeAt(i);
255
+ if (code === 47) {
256
+ if (!matchedSlash) {
257
+ start = i + 1;
258
+ break;
259
+ }
260
+ } else {
261
+ if (firstNonSlashEnd === -1)
262
+ matchedSlash = false, firstNonSlashEnd = i + 1;
263
+ if (extIdx >= 0)
264
+ if (code === ext.charCodeAt(extIdx)) {
265
+ if (--extIdx === -1)
266
+ end = i;
267
+ } else
268
+ extIdx = -1, end = firstNonSlashEnd;
269
+ }
270
+ }
271
+ if (start === end)
272
+ end = firstNonSlashEnd;
273
+ else if (end === -1)
274
+ end = path.length;
275
+ return path.slice(start, end);
276
+ } else {
277
+ for (i = path.length - 1;i >= 0; --i)
278
+ if (path.charCodeAt(i) === 47) {
279
+ if (!matchedSlash) {
280
+ start = i + 1;
281
+ break;
282
+ }
283
+ } else if (end === -1)
284
+ matchedSlash = false, end = i + 1;
285
+ if (end === -1)
286
+ return "";
287
+ return path.slice(start, end);
288
+ }
289
+ }
290
+ function extname(path) {
291
+ assertPath(path);
292
+ var startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0;
293
+ for (var i = path.length - 1;i >= 0; --i) {
294
+ var code = path.charCodeAt(i);
295
+ if (code === 47) {
296
+ if (!matchedSlash) {
297
+ startPart = i + 1;
298
+ break;
299
+ }
300
+ continue;
301
+ }
302
+ if (end === -1)
303
+ matchedSlash = false, end = i + 1;
304
+ if (code === 46) {
305
+ if (startDot === -1)
306
+ startDot = i;
307
+ else if (preDotState !== 1)
308
+ preDotState = 1;
309
+ } else if (startDot !== -1)
310
+ preDotState = -1;
311
+ }
312
+ if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
313
+ return "";
314
+ return path.slice(startDot, end);
315
+ }
316
+ function format(pathObject) {
317
+ if (pathObject === null || typeof pathObject !== "object")
318
+ throw TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject);
319
+ return _format("/", pathObject);
320
+ }
321
+ function parse(path) {
322
+ assertPath(path);
323
+ var ret = { root: "", dir: "", base: "", ext: "", name: "" };
324
+ if (path.length === 0)
325
+ return ret;
326
+ var code = path.charCodeAt(0), isAbsolute2 = code === 47, start;
327
+ if (isAbsolute2)
328
+ ret.root = "/", start = 1;
329
+ else
330
+ start = 0;
331
+ var startDot = -1, startPart = 0, end = -1, matchedSlash = true, i = path.length - 1, preDotState = 0;
332
+ for (;i >= start; --i) {
333
+ if (code = path.charCodeAt(i), code === 47) {
334
+ if (!matchedSlash) {
335
+ startPart = i + 1;
336
+ break;
337
+ }
338
+ continue;
339
+ }
340
+ if (end === -1)
341
+ matchedSlash = false, end = i + 1;
342
+ if (code === 46) {
343
+ if (startDot === -1)
344
+ startDot = i;
345
+ else if (preDotState !== 1)
346
+ preDotState = 1;
347
+ } else if (startDot !== -1)
348
+ preDotState = -1;
349
+ }
350
+ if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {
351
+ if (end !== -1)
352
+ if (startPart === 0 && isAbsolute2)
353
+ ret.base = ret.name = path.slice(1, end);
354
+ else
355
+ ret.base = ret.name = path.slice(startPart, end);
356
+ } else {
357
+ if (startPart === 0 && isAbsolute2)
358
+ ret.name = path.slice(1, startDot), ret.base = path.slice(1, end);
359
+ else
360
+ ret.name = path.slice(startPart, startDot), ret.base = path.slice(startPart, end);
361
+ ret.ext = path.slice(startDot, end);
362
+ }
363
+ if (startPart > 0)
364
+ ret.dir = path.slice(0, startPart - 1);
365
+ else if (isAbsolute2)
366
+ ret.dir = "/";
367
+ return ret;
368
+ }
369
+ var sep = "/", delimiter = ":", posix, path_default;
370
+ var init_path = __esm(() => {
371
+ posix = ((p) => (p.posix = p, p))({ resolve, normalize, isAbsolute, join, relative, _makeLong, dirname, basename, extname, format, parse, sep, delimiter, win32: null, posix: null });
372
+ path_default = posix;
373
+ });
374
+
1
375
  // src/commentary.ts
2
376
  var TOOLTIP_ID = "demon-commentary-tooltip";
3
377
  async function showCommentary(page, options) {
@@ -17,7 +391,7 @@ async function showCommentary(page, options) {
17
391
  @keyframes demon-commentary-in {
18
392
  from {
19
393
  opacity: 0;
20
- transform: translateY(8px);
394
+ transform: translateY(var(--demon-slide-y, 8px));
21
395
  }
22
396
  to {
23
397
  opacity: 1;
@@ -31,10 +405,11 @@ async function showCommentary(page, options) {
31
405
  }
32
406
  to {
33
407
  opacity: 0;
34
- transform: translateY(8px);
408
+ transform: translateY(var(--demon-slide-y, 8px));
35
409
  }
36
410
  }
37
411
  #${tooltipId} {
412
+ --demon-slide-y: 8px;
38
413
  position: fixed;
39
414
  z-index: 2147483647;
40
415
  background: #1a1a2e;
@@ -53,12 +428,21 @@ async function showCommentary(page, options) {
53
428
  `;
54
429
  document.querySelector("style[data-demon-commentary]")?.remove();
55
430
  document.head.appendChild(style);
56
- const top = rect.bottom + 10;
57
- const left = rect.left + rect.width / 2;
431
+ tooltip.style.visibility = "hidden";
432
+ document.body.appendChild(tooltip);
433
+ const tooltipRect = tooltip.getBoundingClientRect();
434
+ const tooltipWidth = tooltipRect.width;
435
+ const tooltipHeight = tooltipRect.height;
436
+ const viewportWidth = window.innerWidth;
437
+ let top = rect.bottom + 10;
438
+ if (top + tooltipHeight > window.innerHeight && rect.top - 10 - tooltipHeight >= 0) {
439
+ top = rect.top - 10 - tooltipHeight;
440
+ tooltip.style.setProperty("--demon-slide-y", "-8px");
441
+ }
442
+ const left = Math.max(4, Math.min(rect.left + rect.width / 2 - tooltipWidth / 2, viewportWidth - 4 - tooltipWidth));
58
443
  tooltip.style.top = `${top}px`;
59
444
  tooltip.style.left = `${left}px`;
60
- tooltip.style.transform = `translateX(-50%)`;
61
- document.body.appendChild(tooltip);
445
+ tooltip.style.visibility = "";
62
446
  }, { selector: options.selector, text: options.text, tooltipId: TOOLTIP_ID });
63
447
  }
64
448
  async function hideCommentary(page) {
@@ -74,9 +458,732 @@ async function hideCommentary(page) {
74
458
  }, TOOLTIP_ID);
75
459
  await page.waitForTimeout(300);
76
460
  }
461
+ // src/review.ts
462
+ var GIT_DIFF_MAX_CHARS = 50000;
463
+ function buildReviewPrompt(options) {
464
+ const { filenames, stepsMap, gitDiff, guidelines } = options;
465
+ const demoEntries = filenames.map((f) => {
466
+ const steps = stepsMap[f] ?? [];
467
+ const stepLines = steps.map((s) => `- [${s.timestampSeconds}s] ${s.text}`).join(`
468
+ `);
469
+ return `Video: ${f}
470
+ Recorded steps:
471
+ ${stepLines || "(no steps recorded)"}`;
472
+ });
473
+ const sections = [];
474
+ if (guidelines && guidelines.length > 0) {
475
+ sections.push(`## Coding Guidelines
476
+
477
+ ${guidelines.join(`
478
+
479
+ `)}`);
480
+ }
481
+ if (gitDiff) {
482
+ let diff = gitDiff;
483
+ if (diff.length > GIT_DIFF_MAX_CHARS) {
484
+ diff = diff.slice(0, GIT_DIFF_MAX_CHARS) + `
485
+
486
+ ... (diff truncated at 50k characters)`;
487
+ }
488
+ sections.push(`## Git Diff
489
+
490
+ \`\`\`diff
491
+ ${diff}
492
+ \`\`\``);
493
+ }
494
+ sections.push(`## Demo Recordings
495
+
496
+ ${demoEntries.join(`
497
+
498
+ `)}`);
499
+ return `You are a code reviewer. You are given a git diff, coding guidelines, and demo recordings that show the feature in action.
500
+
501
+ ${sections.join(`
502
+
503
+ `)}
504
+
505
+ ## Task
506
+
507
+ Review the code changes and demo recordings. Generate a JSON object matching this exact schema:
508
+
509
+ {
510
+ "demos": [
511
+ {
512
+ "file": "<filename>",
513
+ "summary": "<a meaningful sentence describing what this demo showcases based on the steps>"
514
+ }
515
+ ],
516
+ "review": {
517
+ "summary": "<2-3 sentence overview of the changes>",
518
+ "highlights": ["<positive aspect 1>", "<positive aspect 2>"],
519
+ "verdict": "approve" | "request_changes",
520
+ "verdictReason": "<one sentence justifying the verdict>",
521
+ "issues": [
522
+ {
523
+ "severity": "major" | "minor" | "nit",
524
+ "description": "<what the issue is and how to fix it>"
525
+ }
526
+ ]
527
+ }
528
+ }
529
+
530
+ Rules:
531
+ - Return ONLY the JSON object, no markdown fences or extra text.
532
+ - Include one entry in "demos" for each filename, in the same order.
533
+ - "file" must exactly match the provided filename.
534
+ - "verdict" must be exactly "approve" or "request_changes".
535
+ - Use "request_changes" if there are any "major" issues.
536
+ - "severity" must be exactly "major", "minor", or "nit".
537
+ - "major": bugs, security issues, broken functionality, guideline violations.
538
+ - "minor": code quality, readability, missing edge cases.
539
+ - "nit": style, naming, trivial improvements.
540
+ - "highlights" must have at least one entry.
541
+ - "issues" can be an empty array if there are no issues.
542
+ - Verify that demo steps demonstrate the acceptance criteria being met.`;
543
+ }
544
+ async function invokeClaude(prompt, options) {
545
+ const spawnFn = options?.spawn ?? defaultSpawn;
546
+ const agent = options?.agent ?? "claude";
547
+ const proc = spawnFn([agent, "-p", prompt]);
548
+ const reader = proc.stdout.getReader();
549
+ const chunks = [];
550
+ for (;; ) {
551
+ const { done, value } = await reader.read();
552
+ if (done)
553
+ break;
554
+ chunks.push(value);
555
+ }
556
+ const exitCode = await proc.exitCode;
557
+ const output = new TextDecoder().decode(concatUint8Arrays(chunks));
558
+ if (exitCode !== 0) {
559
+ throw new Error(`claude process exited with code ${exitCode}: ${output.trim()}`);
560
+ }
561
+ return output.trim();
562
+ }
563
+ var VALID_VERDICTS = new Set(["approve", "request_changes"]);
564
+ var VALID_SEVERITIES = new Set(["major", "minor", "nit"]);
565
+ function extractJson(raw) {
566
+ try {
567
+ JSON.parse(raw);
568
+ return raw;
569
+ } catch {}
570
+ const start = raw.indexOf("{");
571
+ const end = raw.lastIndexOf("}");
572
+ if (start === -1 || end === -1 || end <= start) {
573
+ throw new Error(`No JSON object found in LLM response: ${raw.slice(0, 200)}`);
574
+ }
575
+ return raw.slice(start, end + 1);
576
+ }
577
+ function parseLlmResponse(raw) {
578
+ const jsonStr = extractJson(raw);
579
+ let parsed;
580
+ try {
581
+ parsed = JSON.parse(jsonStr);
582
+ } catch {
583
+ throw new Error(`Invalid JSON from LLM: ${raw.slice(0, 200)}`);
584
+ }
585
+ if (typeof parsed !== "object" || parsed === null || !("demos" in parsed)) {
586
+ throw new Error("Missing 'demos' array in review metadata");
587
+ }
588
+ const obj = parsed;
589
+ if (!Array.isArray(obj["demos"])) {
590
+ throw new Error("'demos' must be an array");
591
+ }
592
+ for (const demo of obj["demos"]) {
593
+ if (typeof demo !== "object" || demo === null) {
594
+ throw new Error("Each demo must be an object");
595
+ }
596
+ const d = demo;
597
+ if (typeof d["file"] !== "string") {
598
+ throw new Error("Each demo must have a 'file' string");
599
+ }
600
+ if (typeof d["summary"] !== "string") {
601
+ throw new Error("Each demo must have a 'summary' string");
602
+ }
603
+ }
604
+ if (typeof obj["review"] !== "object" || obj["review"] === null) {
605
+ throw new Error("Missing 'review' object in response");
606
+ }
607
+ const review = obj["review"];
608
+ if (typeof review["summary"] !== "string") {
609
+ throw new Error("review.summary must be a string");
610
+ }
611
+ if (!Array.isArray(review["highlights"])) {
612
+ throw new Error("review.highlights must be an array");
613
+ }
614
+ if (review["highlights"].length === 0) {
615
+ throw new Error("review.highlights must not be empty");
616
+ }
617
+ for (const h of review["highlights"]) {
618
+ if (typeof h !== "string") {
619
+ throw new Error("Each highlight must be a string");
620
+ }
621
+ }
622
+ if (typeof review["verdict"] !== "string" || !VALID_VERDICTS.has(review["verdict"])) {
623
+ throw new Error("review.verdict must be 'approve' or 'request_changes'");
624
+ }
625
+ if (typeof review["verdictReason"] !== "string") {
626
+ throw new Error("review.verdictReason must be a string");
627
+ }
628
+ if (!Array.isArray(review["issues"])) {
629
+ throw new Error("review.issues must be an array");
630
+ }
631
+ for (const issue of review["issues"]) {
632
+ if (typeof issue !== "object" || issue === null) {
633
+ throw new Error("Each issue must be an object");
634
+ }
635
+ const i = issue;
636
+ if (typeof i["severity"] !== "string" || !VALID_SEVERITIES.has(i["severity"])) {
637
+ throw new Error("Each issue severity must be 'major', 'minor', or 'nit'");
638
+ }
639
+ if (typeof i["description"] !== "string") {
640
+ throw new Error("Each issue must have a 'description' string");
641
+ }
642
+ }
643
+ return parsed;
644
+ }
645
+ function defaultSpawn(cmd) {
646
+ const [command, ...args] = cmd;
647
+ const proc = Bun.spawn([command, ...args], {
648
+ stdout: "pipe",
649
+ stderr: "pipe"
650
+ });
651
+ return {
652
+ exitCode: proc.exited,
653
+ stdout: proc.stdout
654
+ };
655
+ }
656
+ function concatUint8Arrays(arrays) {
657
+ const totalLength = arrays.reduce((sum, a) => sum + a.length, 0);
658
+ const result = new Uint8Array(totalLength);
659
+ let offset = 0;
660
+ for (const a of arrays) {
661
+ result.set(a, offset);
662
+ offset += a.length;
663
+ }
664
+ return result;
665
+ }
666
+ // src/git-context.ts
667
+ var {readFileSync} = (() => ({}));
668
+ var defaultExec = async (cmd, cwd) => {
669
+ const proc = Bun.spawnSync(cmd, { cwd });
670
+ if (proc.exitCode !== 0) {
671
+ const stderr = proc.stderr.toString().trim();
672
+ throw new Error(`Command failed (exit ${proc.exitCode}): ${cmd.join(" ")}${stderr ? `: ${stderr}` : ""}`);
673
+ }
674
+ return proc.stdout.toString();
675
+ };
676
+ var defaultReadFile = (path) => {
677
+ return readFileSync(path, "utf-8");
678
+ };
679
+ async function getRepoContext(demosDir, options) {
680
+ const exec = options?.exec ?? defaultExec;
681
+ const readFile = options?.readFile ?? defaultReadFile;
682
+ const gitRoot = (await exec(["git", "rev-parse", "--show-toplevel"], demosDir)).trim();
683
+ let gitDiff;
684
+ const workingDiff = (await exec(["git", "diff", "HEAD"], gitRoot)).trim();
685
+ if (workingDiff.length > 0) {
686
+ gitDiff = workingDiff;
687
+ } else {
688
+ gitDiff = (await exec(["git", "diff", "HEAD~1..HEAD"], gitRoot)).trim();
689
+ }
690
+ const lsOutput = (await exec(["git", "ls-files"], gitRoot)).trim();
691
+ const files = lsOutput.split(`
692
+ `).filter((f) => f.length > 0);
693
+ const guidelinePatterns = ["CLAUDE.md", "SKILL.md"];
694
+ const guidelines = [];
695
+ for (const file of files) {
696
+ const basename = file.split("/").pop() ?? "";
697
+ if (guidelinePatterns.includes(basename)) {
698
+ const fullPath = `${gitRoot}/${file}`;
699
+ const content = readFile(fullPath);
700
+ guidelines.push(`# ${file}
701
+ ${content}`);
702
+ }
703
+ }
704
+ return { gitDiff, guidelines };
705
+ }
706
+ // src/html-generator.ts
707
+ function escapeHtml(s) {
708
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
709
+ }
710
+ function escapeAttr(s) {
711
+ return escapeHtml(s);
712
+ }
713
+ function renderReviewSection(review) {
714
+ const bannerClass = review.verdict === "approve" ? "approve" : "request-changes";
715
+ const verdictLabel = review.verdict === "approve" ? "Approved" : "Changes Requested";
716
+ const highlightsHtml = review.highlights.map((h) => `<li>${escapeHtml(h)}</li>`).join(`
717
+ `);
718
+ const issuesHtml = review.issues.length > 0 ? review.issues.map((issue) => {
719
+ const badgeLabel = issue.severity.toUpperCase();
720
+ return `<div class="issue ${issue.severity}"><span class="severity-badge">${badgeLabel}</span> <span class="issue-text">${escapeHtml(issue.description)}</span><button class="feedback-add-issue" data-issue="${escapeAttr(issue.description)}">+</button></div>`;
721
+ }).join(`
722
+ `) : '<p class="no-issues">No issues found.</p>';
723
+ return `<section class="review-section">
724
+ <div class="verdict-banner ${bannerClass}">
725
+ <strong>${verdictLabel}</strong>: ${escapeHtml(review.verdictReason)}
726
+ </div>
727
+ <div class="review-body">
728
+ <h2>Summary</h2>
729
+ <p>${escapeHtml(review.summary)}</p>
730
+ <h2>Highlights</h2>
731
+ <ul class="highlights-list">
732
+ ${highlightsHtml}
733
+ </ul>
734
+ <h2>Issues</h2>
735
+ ${issuesHtml}
736
+ </div>
737
+ </section>`;
738
+ }
739
+ function generateReviewHtml(options) {
740
+ const { metadata, title = "Demo Review" } = options;
741
+ if (metadata.demos.length === 0) {
742
+ throw new Error("metadata.demos must not be empty");
743
+ }
744
+ const firstDemo = metadata.demos[0];
745
+ const demoButtons = metadata.demos.map((demo, i) => {
746
+ const activeClass = i === 0 ? ' class="active"' : "";
747
+ return `<li><button data-index="${i}"${activeClass}>${escapeHtml(demo.file)}</button></li>`;
748
+ }).join(`
749
+ `);
750
+ const metadataJson = JSON.stringify(metadata).replace(/<\//g, "<\\/");
751
+ const reviewHtml = metadata.review ? renderReviewSection(metadata.review) : "";
752
+ const hasReview = !!metadata.review;
753
+ const defaultTab = hasReview ? "summary" : "demos";
754
+ return `<!DOCTYPE html>
755
+ <html lang="en">
756
+ <head>
757
+ <meta charset="UTF-8">
758
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
759
+ <title>${escapeHtml(title)}</title>
760
+ <style>
761
+ * { margin: 0; padding: 0; box-sizing: border-box; }
762
+ body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; }
763
+ header { padding: 1rem 2rem; background: #16213e; border-bottom: 1px solid #0f3460; }
764
+ header h1 { font-size: 1.4rem; color: #e94560; }
765
+ .tab-bar { display: flex; gap: 0; background: #16213e; border-bottom: 2px solid #0f3460; padding: 0 2rem; }
766
+ .tab-btn { padding: 0.7rem 1.5rem; background: none; border: none; border-bottom: 3px solid transparent; color: #999; font-size: 0.95rem; cursor: pointer; font-family: inherit; transition: all 0.15s; margin-bottom: -2px; }
767
+ .tab-btn:hover { color: #e0e0e0; }
768
+ .tab-btn.active { color: #e94560; border-bottom-color: #e94560; }
769
+ .tab-panel { display: none; }
770
+ .tab-panel.active { display: block; }
771
+ .review-section { padding: 1.5rem 2rem; }
772
+ .verdict-banner { padding: 1rem 1.5rem; border-radius: 6px; font-size: 1rem; margin-bottom: 1.5rem; }
773
+ .verdict-banner.approve { background: #1b4332; border: 1px solid #2d6a4f; color: #95d5b2; }
774
+ .verdict-banner.request-changes { background: #4a1520; border: 1px solid #842029; color: #f5c6cb; }
775
+ .review-body { max-width: 900px; }
776
+ .review-body h2 { font-size: 1.1rem; color: #e94560; margin: 1.2rem 0 0.5rem; }
777
+ .review-body p { font-size: 0.95rem; line-height: 1.6; color: #ccc; }
778
+ .highlights-list { list-style: disc; padding-left: 1.5rem; margin-bottom: 0.5rem; }
779
+ .highlights-list li { font-size: 0.95rem; line-height: 1.5; color: #95d5b2; margin-bottom: 0.3rem; }
780
+ .issue { padding: 0.6rem 0.8rem; margin-bottom: 0.5rem; border-radius: 4px; font-size: 0.9rem; line-height: 1.4; }
781
+ .issue.major { background: rgba(132, 32, 41, 0.3); border-left: 4px solid #dc3545; }
782
+ .issue.minor { background: rgba(255, 193, 7, 0.1); border-left: 4px solid #ffc107; }
783
+ .issue.nit { background: rgba(108, 117, 125, 0.2); border-left: 4px solid #6c757d; }
784
+ .severity-badge { display: inline-block; font-size: 0.7rem; font-weight: bold; padding: 0.15rem 0.4rem; border-radius: 3px; margin-right: 0.5rem; vertical-align: middle; }
785
+ .issue.major .severity-badge { background: #dc3545; color: #fff; }
786
+ .issue.minor .severity-badge { background: #ffc107; color: #000; }
787
+ .issue.nit .severity-badge { background: #6c757d; color: #fff; }
788
+ .no-issues { color: #95d5b2; font-style: italic; }
789
+ .demos-section { padding: 1rem 0; }
790
+ .review-layout { display: flex; height: 600px; }
791
+ .video-panel { flex: 4; padding: 1rem; display: flex; align-items: center; justify-content: center; background: #0f0f23; }
792
+ .video-wrapper { position: relative; width: 100%; max-height: 100%; display: flex; flex-direction: column; }
793
+ .video-wrapper video { width: 100%; max-height: calc(100% - 36px); border-radius: 4px 4px 0 0; display: block; cursor: pointer; }
794
+ .video-controls { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: #16213e; border-radius: 0 0 4px 4px; }
795
+ .video-controls button { background: none; border: none; color: #e0e0e0; cursor: pointer; font-size: 1rem; padding: 0; width: 20px; display: flex; align-items: center; justify-content: center; }
796
+ .video-controls button:hover { color: #e94560; }
797
+ .video-controls input[type="range"] { flex: 1; height: 4px; accent-color: #e94560; cursor: pointer; }
798
+ .video-controls .vc-time { font-size: 0.75rem; color: #999; white-space: nowrap; font-variant-numeric: tabular-nums; }
799
+ .side-panel { flex: 1; min-width: 260px; max-width: 360px; padding: 1rem; overflow-y: auto; background: #16213e; border-left: 1px solid #0f3460; }
800
+ .side-panel h2 { font-size: 1rem; margin-bottom: 0.5rem; color: #e94560; }
801
+ .side-panel section { margin-bottom: 1.5rem; }
802
+ #demo-list { list-style: none; }
803
+ #demo-list li { margin-bottom: 0.25rem; }
804
+ #demo-list button { width: 100%; text-align: left; padding: 0.4rem 0.6rem; background: #1a1a2e; color: #e0e0e0; border: 1px solid #0f3460; border-radius: 4px; cursor: pointer; font-size: 0.85rem; }
805
+ #demo-list button:hover { background: #0f3460; }
806
+ #demo-list button.active { background: #e94560; color: #fff; border-color: #e94560; }
807
+ #summary-text { font-size: 0.9rem; line-height: 1.5; color: #ccc; }
808
+ #steps-list { list-style: none; }
809
+ #steps-list li { margin-bottom: 0.3rem; }
810
+ #steps-list button { width: 100%; text-align: left; padding: 0.4rem 0.6rem; background: transparent; color: #53a8b6; border: none; border-left: 3px solid transparent; cursor: pointer; font-size: 0.85rem; transition: all 0.2s; }
811
+ #steps-list button:hover { color: #e94560; }
812
+ #steps-list button.step-active { background: rgba(233, 69, 96, 0.15); color: #e94560; border-left-color: #e94560; }
813
+ .timestamp { font-weight: bold; margin-right: 0.4rem; color: #e94560; }
814
+ .issue { position: relative; }
815
+ .feedback-add-issue { position: absolute; right: 0.5rem; top: 50%; transform: translateY(-50%); background: none; border: 1px solid #53a8b6; color: #53a8b6; border-radius: 4px; cursor: pointer; font-size: 0.85rem; padding: 0.1rem 0.45rem; line-height: 1; }
816
+ .feedback-add-issue:hover { background: #53a8b6; color: #1a1a2e; }
817
+ #feedback-selection-btn { display: none; position: absolute; z-index: 1000; padding: 0.35rem 0.7rem; background: #e94560; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 0.8rem; white-space: nowrap; }
818
+ .feedback-layout { display: flex; gap: 1.5rem; padding: 1.5rem 2rem; }
819
+ .feedback-left { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1rem; }
820
+ .feedback-right { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.5rem; }
821
+ #feedback-list { list-style: none; padding: 0; }
822
+ #feedback-list li { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.6rem; background: #16213e; border: 1px solid #0f3460; border-radius: 4px; margin-bottom: 0.4rem; font-size: 0.9rem; }
823
+ #feedback-list li span { flex: 1; }
824
+ .feedback-remove { background: none; border: none; color: #dc3545; cursor: pointer; font-size: 0.9rem; padding: 0 0.3rem; }
825
+ .feedback-remove:hover { color: #ff6b7a; }
826
+ #feedback-general { width: 100%; min-height: 100px; background: #16213e; color: #e0e0e0; border: 1px solid #0f3460; border-radius: 4px; padding: 0.6rem; font-family: inherit; font-size: 0.9rem; resize: vertical; }
827
+ #feedback-preview { background: #0f0f23; color: #ccc; border: 1px solid #0f3460; border-radius: 4px; padding: 1rem; white-space: pre-wrap; font-size: 0.85rem; line-height: 1.5; flex: 1; min-height: 200px; overflow-y: auto; }
828
+ #feedback-copy { align-self: flex-end; padding: 0.5rem 1rem; background: none; border: 1px solid #53a8b6; color: #53a8b6; border-radius: 4px; cursor: pointer; font-size: 0.85rem; }
829
+ #feedback-copy:hover { background: #53a8b6; color: #1a1a2e; }
830
+ </style>
831
+ </head>
832
+ <body>
833
+ <header>
834
+ <h1>${escapeHtml(title)}</h1>
835
+ </header>
836
+ <nav class="tab-bar">
837
+ ${hasReview ? `<button class="tab-btn${defaultTab === "summary" ? " active" : ""}" data-tab="summary">Summary</button>` : ""}
838
+ <button class="tab-btn${defaultTab === "demos" ? " active" : ""}" data-tab="demos">Demos</button>
839
+ ${hasReview ? `<button class="tab-btn" data-tab="feedback">Feedback</button>` : ""}
840
+ </nav>
841
+ <main>
842
+ ${hasReview ? `<div id="tab-summary" class="tab-panel${defaultTab === "summary" ? " active" : ""}">
843
+ ${reviewHtml}
844
+ </div>` : ""}
845
+ <div id="tab-demos" class="tab-panel${defaultTab === "demos" ? " active" : ""}">
846
+ <section class="demos-section">
847
+ <div class="review-layout">
848
+ <div class="video-panel">
849
+ <div class="video-wrapper">
850
+ <video id="review-video" src="${escapeAttr(firstDemo.file)}"></video>
851
+ <div class="video-controls">
852
+ <button id="vc-play" aria-label="Play">&#9654;</button>
853
+ <input id="vc-seek" type="range" min="0" max="100" value="0" step="0.1">
854
+ <span class="vc-time" id="vc-time">0:00 / 0:00</span>
855
+ </div>
856
+ </div>
857
+ </div>
858
+ <div class="side-panel">
859
+ <section>
860
+ <h2>Demos</h2>
861
+ <ul id="demo-list">
862
+ ${demoButtons}
863
+ </ul>
864
+ </section>
865
+ <section>
866
+ <h2>Summary</h2>
867
+ <p id="summary-text"></p>
868
+ </section>
869
+ <section id="steps-section">
870
+ <h2>Steps</h2>
871
+ <ul id="steps-list"></ul>
872
+ </section>
873
+ </div>
874
+ </div>
875
+ </section>
876
+ </div>
877
+ ${hasReview ? `<div id="tab-feedback" class="tab-panel">
878
+ <div class="feedback-layout">
879
+ <div class="feedback-left">
880
+ <h2>Feedback Items</h2>
881
+ <ul id="feedback-list"></ul>
882
+ <h2>General Feedback</h2>
883
+ <textarea id="feedback-general" placeholder="Add general feedback here..."></textarea>
884
+ </div>
885
+ <div class="feedback-right">
886
+ <h2>Preview</h2>
887
+ <pre id="feedback-preview"></pre>
888
+ <button id="feedback-copy">Copy to clipboard</button>
889
+ </div>
890
+ </div>
891
+ </div>` : ""}
892
+ </main>
893
+ ${hasReview ? `<button id="feedback-selection-btn">Add to feedback</button>` : ""}
894
+ <script>
895
+ (function() {
896
+ // Tab switching
897
+ var tabBtns = document.querySelectorAll(".tab-btn");
898
+ var tabPanels = document.querySelectorAll(".tab-panel");
899
+ tabBtns.forEach(function(btn) {
900
+ btn.addEventListener("click", function() {
901
+ var target = btn.getAttribute("data-tab");
902
+ tabBtns.forEach(function(b) { b.classList.toggle("active", b === btn); });
903
+ tabPanels.forEach(function(p) { p.classList.toggle("active", p.id === "tab-" + target); });
904
+ });
905
+ });
906
+
907
+ var metadata = ${metadataJson};
908
+ var video = document.getElementById("review-video");
909
+ var summaryText = document.getElementById("summary-text");
910
+ var stepsList = document.getElementById("steps-list");
911
+ var demoButtons = document.querySelectorAll("#demo-list button");
912
+
913
+ function esc(s) {
914
+ var d = document.createElement("div");
915
+ d.appendChild(document.createTextNode(s));
916
+ return d.innerHTML;
917
+ }
918
+
919
+ function formatTime(seconds) {
920
+ var m = Math.floor(seconds / 60);
921
+ var s = Math.floor(seconds % 60);
922
+ return m + ":" + (s < 10 ? "0" : "") + s;
923
+ }
924
+
925
+ function selectDemo(index) {
926
+ var demo = metadata.demos[index];
927
+ video.src = demo.file;
928
+ video.load();
929
+ summaryText.textContent = demo.summary;
930
+
931
+ demoButtons.forEach(function(btn, i) {
932
+ btn.classList.toggle("active", i === index);
933
+ });
934
+
935
+ var stepsHtml = "";
936
+ demo.steps.forEach(function(step) {
937
+ stepsHtml += '<li><button data-time="' + step.timestampSeconds + '">' +
938
+ '<span class="timestamp">' + esc(formatTime(step.timestampSeconds)) + '</span>' +
939
+ esc(step.text) + '</button></li>';
940
+ });
941
+ stepsList.innerHTML = stepsHtml;
942
+ }
943
+
944
+ demoButtons.forEach(function(btn) {
945
+ btn.addEventListener("click", function() {
946
+ selectDemo(parseInt(btn.getAttribute("data-index"), 10));
947
+ });
948
+ });
949
+
950
+ stepsList.addEventListener("click", function(e) {
951
+ var btn = e.target.closest("button[data-time]");
952
+ if (btn) {
953
+ video.currentTime = parseFloat(btn.getAttribute("data-time"));
954
+ video.play();
955
+ }
956
+ });
957
+
958
+ video.addEventListener("timeupdate", function() {
959
+ var buttons = document.querySelectorAll("#steps-list button[data-time]");
960
+ var ct = video.currentTime;
961
+ var activeIdx = -1;
962
+ buttons.forEach(function(btn, i) {
963
+ if (parseFloat(btn.getAttribute("data-time")) <= ct) activeIdx = i;
964
+ btn.classList.remove("step-active");
965
+ });
966
+ if (activeIdx >= 0) buttons[activeIdx].classList.add("step-active");
967
+ });
968
+
969
+ selectDemo(0);
970
+
971
+ // Custom video controls
972
+ var playBtn = document.getElementById("vc-play");
973
+ var seekBar = document.getElementById("vc-seek");
974
+ var timeDisplay = document.getElementById("vc-time");
975
+ var seeking = false;
976
+
977
+ function fmtTime(sec) {
978
+ var m = Math.floor(sec / 60);
979
+ var s = Math.floor(sec % 60);
980
+ return m + ":" + (s < 10 ? "0" : "") + s;
981
+ }
982
+
983
+ function updateTime() {
984
+ var cur = video.currentTime || 0;
985
+ var dur = video.duration || 0;
986
+ timeDisplay.textContent = fmtTime(cur) + " / " + fmtTime(dur);
987
+ if (!seeking && dur) seekBar.value = (cur / dur) * 100;
988
+ }
989
+
990
+ function updatePlayBtn() {
991
+ playBtn.innerHTML = video.paused ? "&#9654;" : "&#9646;&#9646;";
992
+ }
993
+
994
+ playBtn.addEventListener("click", function() {
995
+ video.paused ? video.play() : video.pause();
996
+ });
997
+ video.addEventListener("click", function() {
998
+ video.paused ? video.play() : video.pause();
999
+ });
1000
+ video.addEventListener("play", updatePlayBtn);
1001
+ video.addEventListener("pause", updatePlayBtn);
1002
+ video.addEventListener("ended", updatePlayBtn);
1003
+ video.addEventListener("timeupdate", updateTime);
1004
+ video.addEventListener("loadedmetadata", updateTime);
1005
+
1006
+ seekBar.addEventListener("input", function() {
1007
+ seeking = true;
1008
+ if (video.duration) {
1009
+ video.currentTime = (seekBar.value / 100) * video.duration;
1010
+ }
1011
+ });
1012
+ seekBar.addEventListener("change", function() { seeking = false; });
1013
+
1014
+ // Feedback tab logic
1015
+ if (document.getElementById("tab-feedback")) {
1016
+ var feedbackItems = [];
1017
+ var feedbackList = document.getElementById("feedback-list");
1018
+ var feedbackGeneral = document.getElementById("feedback-general");
1019
+ var feedbackPreview = document.getElementById("feedback-preview");
1020
+ var feedbackCopy = document.getElementById("feedback-copy");
1021
+ var selectionBtn = document.getElementById("feedback-selection-btn");
1022
+
1023
+ function addFeedbackItem(text) {
1024
+ var trimmed = text.trim();
1025
+ if (!trimmed) return;
1026
+ for (var i = 0; i < feedbackItems.length; i++) {
1027
+ if (feedbackItems[i] === trimmed) return;
1028
+ }
1029
+ feedbackItems.push(trimmed);
1030
+ renderFeedback();
1031
+ }
1032
+
1033
+ function removeFeedbackItem(index) {
1034
+ feedbackItems.splice(index, 1);
1035
+ renderFeedback();
1036
+ }
1037
+
1038
+ function renderFeedback() {
1039
+ var html = "";
1040
+ feedbackItems.forEach(function(item, i) {
1041
+ html += '<li><span>' + esc(item) + '</span><button class="feedback-remove" data-index="' + i + '">X</button></li>';
1042
+ });
1043
+ feedbackList.innerHTML = html;
1044
+ updatePreview();
1045
+ }
1046
+
1047
+ function updatePreview() {
1048
+ var lines = "";
1049
+ feedbackItems.forEach(function(item, i) {
1050
+ lines += (i + 1) + ". Address: " + item + "\\n";
1051
+ });
1052
+ var general = feedbackGeneral.value.trim();
1053
+ if (general) {
1054
+ lines += "\\nGeneral feedback:\\n" + general;
1055
+ }
1056
+ feedbackPreview.textContent = lines;
1057
+ }
1058
+
1059
+ // Issue "+" buttons
1060
+ var summaryTab = document.getElementById("tab-summary");
1061
+ if (summaryTab) {
1062
+ summaryTab.addEventListener("click", function(e) {
1063
+ var btn = e.target.closest(".feedback-add-issue");
1064
+ if (btn) {
1065
+ addFeedbackItem(btn.getAttribute("data-issue"));
1066
+ }
1067
+ });
1068
+ }
1069
+
1070
+ // Text selection floating button
1071
+ var selectionTimeout;
1072
+ document.addEventListener("mouseup", function(e) {
1073
+ clearTimeout(selectionTimeout);
1074
+ selectionTimeout = setTimeout(function() {
1075
+ var sel = window.getSelection();
1076
+ var text = sel ? sel.toString().trim() : "";
1077
+ if (!text) return;
1078
+ var anchor = sel.anchorNode;
1079
+ var inSummary = false;
1080
+ var node = anchor;
1081
+ while (node) {
1082
+ if (node.id === "tab-summary") { inSummary = true; break; }
1083
+ node = node.parentNode;
1084
+ }
1085
+ if (!inSummary) return;
1086
+ selectionBtn.style.display = "block";
1087
+ selectionBtn.style.left = e.pageX + "px";
1088
+ selectionBtn.style.top = (e.pageY - 35) + "px";
1089
+ selectionBtn._selectedText = text;
1090
+ }, 100);
1091
+ });
1092
+
1093
+ selectionBtn.addEventListener("click", function() {
1094
+ if (selectionBtn._selectedText) {
1095
+ addFeedbackItem(selectionBtn._selectedText);
1096
+ }
1097
+ selectionBtn.style.display = "none";
1098
+ window.getSelection().removeAllRanges();
1099
+ });
1100
+
1101
+ document.addEventListener("mousedown", function(e) {
1102
+ if (e.target !== selectionBtn) {
1103
+ selectionBtn.style.display = "none";
1104
+ }
1105
+ });
1106
+
1107
+ // Remove buttons
1108
+ feedbackList.addEventListener("click", function(e) {
1109
+ var btn = e.target.closest(".feedback-remove");
1110
+ if (btn) {
1111
+ removeFeedbackItem(parseInt(btn.getAttribute("data-index"), 10));
1112
+ }
1113
+ });
1114
+
1115
+ // Textarea input
1116
+ feedbackGeneral.addEventListener("input", updatePreview);
1117
+
1118
+ // Copy button
1119
+ feedbackCopy.addEventListener("click", function() {
1120
+ var text = feedbackPreview.textContent;
1121
+ function onCopied() {
1122
+ feedbackCopy.textContent = "Copied!";
1123
+ setTimeout(function() { feedbackCopy.textContent = "Copy to clipboard"; }, 1500);
1124
+ }
1125
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1126
+ navigator.clipboard.writeText(text).then(onCopied, onCopied);
1127
+ } else {
1128
+ onCopied();
1129
+ }
1130
+ });
1131
+
1132
+ renderFeedback();
1133
+ }
1134
+ })();
1135
+ </script>
1136
+ </body>
1137
+ </html>`;
1138
+ }
1139
+ // src/recorder.ts
1140
+ class DemoRecorder {
1141
+ steps = [];
1142
+ startTime;
1143
+ showCommentaryFn;
1144
+ testStepFn;
1145
+ constructor(options) {
1146
+ this.startTime = Date.now();
1147
+ this.showCommentaryFn = options?.showCommentary ?? showCommentary;
1148
+ this.testStepFn = options?.testStep;
1149
+ }
1150
+ async step(page, text, options) {
1151
+ const body = async () => {
1152
+ await this.showCommentaryFn(page, {
1153
+ selector: options.selector,
1154
+ text
1155
+ });
1156
+ const timestampSeconds = Math.round((Date.now() - this.startTime) / 100) / 10;
1157
+ this.steps.push({ text, timestampSeconds });
1158
+ };
1159
+ if (this.testStepFn) {
1160
+ await this.testStepFn(text, body);
1161
+ } else {
1162
+ await body();
1163
+ }
1164
+ }
1165
+ getSteps() {
1166
+ return [...this.steps];
1167
+ }
1168
+ async save(outputDir) {
1169
+ const { join: join2 } = await Promise.resolve().then(() => (init_path(), exports_path));
1170
+ const { mkdirSync, writeFileSync } = await import("node:fs");
1171
+ mkdirSync(outputDir, { recursive: true });
1172
+ const filePath = join2(outputDir, "demo-steps.json");
1173
+ writeFileSync(filePath, JSON.stringify(this.steps, null, 2) + `
1174
+ `);
1175
+ }
1176
+ }
77
1177
  export {
78
1178
  showCommentary,
79
- hideCommentary
1179
+ parseLlmResponse,
1180
+ invokeClaude,
1181
+ hideCommentary,
1182
+ getRepoContext,
1183
+ generateReviewHtml,
1184
+ extractJson,
1185
+ buildReviewPrompt,
1186
+ DemoRecorder
80
1187
  };
81
1188
 
82
- //# debugId=308A425302523F0964756E2164756E21
1189
+ //# debugId=160BDBF1FB742F2264756E2164756E21