@demon-utils/playwright 0.1.5 → 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) {
@@ -85,33 +459,87 @@ async function hideCommentary(page) {
85
459
  await page.waitForTimeout(300);
86
460
  }
87
461
  // src/review.ts
88
- function buildReviewPrompt(filenames) {
89
- const fileList = filenames.map((f) => `- ${f}`).join(`
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(`
90
468
  `);
91
- return `You are given the following .webm demo video filenames:
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(`
92
497
 
93
- ${fileList}
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.
94
500
 
95
- Based on the filenames, generate a JSON object matching this exact schema:
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:
96
508
 
97
509
  {
98
510
  "demos": [
99
511
  {
100
512
  "file": "<filename>",
101
- "summary": "<a short sentence describing what the demo likely shows>",
102
- "annotations": [
103
- { "timestampSeconds": <number>, "text": "<annotation text>" }
104
- ]
513
+ "summary": "<a meaningful sentence describing what this demo showcases based on the steps>"
105
514
  }
106
- ]
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
+ }
107
528
  }
108
529
 
109
530
  Rules:
110
531
  - Return ONLY the JSON object, no markdown fences or extra text.
111
532
  - Include one entry in "demos" for each filename, in the same order.
112
- - Infer the summary and annotations from the filename.
113
- - Each demo should have at least one annotation starting at timestampSeconds 0.
114
- - "file" must exactly match the provided filename.`;
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.`;
115
543
  }
116
544
  async function invokeClaude(prompt, options) {
117
545
  const spawnFn = options?.spawn ?? defaultSpawn;
@@ -132,10 +560,25 @@ async function invokeClaude(prompt, options) {
132
560
  }
133
561
  return output.trim();
134
562
  }
135
- function parseReviewMetadata(raw) {
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);
136
579
  let parsed;
137
580
  try {
138
- parsed = JSON.parse(raw);
581
+ parsed = JSON.parse(jsonStr);
139
582
  } catch {
140
583
  throw new Error(`Invalid JSON from LLM: ${raw.slice(0, 200)}`);
141
584
  }
@@ -157,20 +600,44 @@ function parseReviewMetadata(raw) {
157
600
  if (typeof d["summary"] !== "string") {
158
601
  throw new Error("Each demo must have a 'summary' string");
159
602
  }
160
- if (!Array.isArray(d["annotations"])) {
161
- throw new Error("Each demo must have an 'annotations' array");
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");
162
620
  }
163
- for (const ann of d["annotations"]) {
164
- if (typeof ann !== "object" || ann === null) {
165
- throw new Error("Each annotation must be an object");
166
- }
167
- const a = ann;
168
- if (typeof a["timestampSeconds"] !== "number") {
169
- throw new Error("Each annotation must have a 'timestampSeconds' number");
170
- }
171
- if (typeof a["text"] !== "string") {
172
- throw new Error("Each annotation must have a 'text' string");
173
- }
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");
174
641
  }
175
642
  }
176
643
  return parsed;
@@ -196,6 +663,46 @@ function concatUint8Arrays(arrays) {
196
663
  }
197
664
  return result;
198
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
+ }
199
706
  // src/html-generator.ts
200
707
  function escapeHtml(s) {
201
708
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -203,6 +710,32 @@ function escapeHtml(s) {
203
710
  function escapeAttr(s) {
204
711
  return escapeHtml(s);
205
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
+ }
206
739
  function generateReviewHtml(options) {
207
740
  const { metadata, title = "Demo Review" } = options;
208
741
  if (metadata.demos.length === 0) {
@@ -215,6 +748,9 @@ function generateReviewHtml(options) {
215
748
  }).join(`
216
749
  `);
217
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";
218
754
  return `<!DOCTYPE html>
219
755
  <html lang="en">
220
756
  <head>
@@ -226,9 +762,40 @@ function generateReviewHtml(options) {
226
762
  body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; }
227
763
  header { padding: 1rem 2rem; background: #16213e; border-bottom: 1px solid #0f3460; }
228
764
  header h1 { font-size: 1.4rem; color: #e94560; }
229
- .review-layout { display: flex; height: calc(100vh - 60px); }
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; }
230
791
  .video-panel { flex: 4; padding: 1rem; display: flex; align-items: center; justify-content: center; background: #0f0f23; }
231
- .video-panel video { width: 100%; max-height: 100%; border-radius: 4px; }
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; }
232
799
  .side-panel { flex: 1; min-width: 260px; max-width: 360px; padding: 1rem; overflow-y: auto; background: #16213e; border-left: 1px solid #0f3460; }
233
800
  .side-panel h2 { font-size: 1rem; margin-bottom: 0.5rem; color: #e94560; }
234
801
  .side-panel section { margin-bottom: 1.5rem; }
@@ -238,44 +805,109 @@ function generateReviewHtml(options) {
238
805
  #demo-list button:hover { background: #0f3460; }
239
806
  #demo-list button.active { background: #e94560; color: #fff; border-color: #e94560; }
240
807
  #summary-text { font-size: 0.9rem; line-height: 1.5; color: #ccc; }
241
- #annotations-list { list-style: none; }
242
- #annotations-list li { margin-bottom: 0.3rem; }
243
- #annotations-list button { width: 100%; text-align: left; padding: 0.3rem 0.5rem; background: transparent; color: #53a8b6; border: none; cursor: pointer; font-size: 0.85rem; }
244
- #annotations-list button:hover { color: #e94560; text-decoration: underline; }
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; }
245
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; }
246
830
  </style>
247
831
  </head>
248
832
  <body>
249
833
  <header>
250
834
  <h1>${escapeHtml(title)}</h1>
251
835
  </header>
252
- <main class="review-layout">
253
- <div class="video-panel">
254
- <video id="review-video" controls src="${escapeAttr(firstDemo.file)}"></video>
255
- </div>
256
- <div class="side-panel">
257
- <section>
258
- <h2>Demos</h2>
259
- <ul id="demo-list">
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">
260
862
  ${demoButtons}
261
- </ul>
262
- </section>
263
- <section>
264
- <h2>Summary</h2>
265
- <p id="summary-text"></p>
266
- </section>
267
- <section>
268
- <h2>Annotations</h2>
269
- <ul id="annotations-list"></ul>
270
- </section>
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>
271
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>` : ""}
272
892
  </main>
893
+ ${hasReview ? `<button id="feedback-selection-btn">Add to feedback</button>` : ""}
273
894
  <script>
274
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
+
275
907
  var metadata = ${metadataJson};
276
908
  var video = document.getElementById("review-video");
277
909
  var summaryText = document.getElementById("summary-text");
278
- var annotationsList = document.getElementById("annotations-list");
910
+ var stepsList = document.getElementById("steps-list");
279
911
  var demoButtons = document.querySelectorAll("#demo-list button");
280
912
 
281
913
  function esc(s) {
@@ -300,13 +932,13 @@ function generateReviewHtml(options) {
300
932
  btn.classList.toggle("active", i === index);
301
933
  });
302
934
 
303
- var html = "";
304
- demo.annotations.forEach(function(ann) {
305
- html += '<li><button data-time="' + ann.timestampSeconds + '">' +
306
- '<span class="timestamp">' + esc(formatTime(ann.timestampSeconds)) + '</span>' +
307
- esc(ann.text) + '</button></li>';
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>';
308
940
  });
309
- annotationsList.innerHTML = html;
941
+ stepsList.innerHTML = stepsHtml;
310
942
  }
311
943
 
312
944
  demoButtons.forEach(function(btn) {
@@ -315,7 +947,7 @@ function generateReviewHtml(options) {
315
947
  });
316
948
  });
317
949
 
318
- annotationsList.addEventListener("click", function(e) {
950
+ stepsList.addEventListener("click", function(e) {
319
951
  var btn = e.target.closest("button[data-time]");
320
952
  if (btn) {
321
953
  video.currentTime = parseFloat(btn.getAttribute("data-time"));
@@ -323,19 +955,235 @@ function generateReviewHtml(options) {
323
955
  }
324
956
  });
325
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
+
326
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
+ }
327
1134
  })();
328
1135
  </script>
329
1136
  </body>
330
1137
  </html>`;
331
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
+ }
332
1177
  export {
333
1178
  showCommentary,
334
- parseReviewMetadata,
1179
+ parseLlmResponse,
335
1180
  invokeClaude,
336
1181
  hideCommentary,
1182
+ getRepoContext,
337
1183
  generateReviewHtml,
338
- buildReviewPrompt
1184
+ extractJson,
1185
+ buildReviewPrompt,
1186
+ DemoRecorder
339
1187
  };
340
1188
 
341
- //# debugId=C244AE0F9893F9BF64756E2164756E21
1189
+ //# debugId=160BDBF1FB742F2264756E2164756E21