aipeek 0.2.4 → 0.2.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.
@@ -67,6 +67,47 @@ function check(raw) {
67
67
  function truncate(s, max) {
68
68
  return s.length > max ? `${s.slice(0, max)}\u2026` : s;
69
69
  }
70
+ function formatValue(v, seen = /* @__PURE__ */ new Set()) {
71
+ if (v === null || v === void 0)
72
+ return String(v);
73
+ const t = typeof v;
74
+ if (t === "string")
75
+ return v;
76
+ if (t === "number" || t === "boolean" || t === "bigint")
77
+ return String(v);
78
+ if (t === "symbol")
79
+ return v.toString();
80
+ if (t === "function")
81
+ return `[Function: ${v.name || "anonymous"}]`;
82
+ const obj = v;
83
+ if (seen.has(obj))
84
+ return "[Circular]";
85
+ if (v instanceof Error)
86
+ return v.stack || `${v.name}: ${v.message}`;
87
+ seen.add(obj);
88
+ if (v instanceof Map) {
89
+ const items = [...v.entries()].slice(0, 15).map(([k, val]) => `${formatValue(k, seen)} => ${formatValue(val, seen)}`);
90
+ return `Map(${v.size}) {${items.join(", ")}${v.size > 15 ? ", \u2026" : ""}}`;
91
+ }
92
+ if (v instanceof Set) {
93
+ const items = [...v.values()].slice(0, 15).map((val) => formatValue(val, seen));
94
+ return `Set(${v.size}) {${items.join(", ")}${v.size > 15 ? ", \u2026" : ""}}`;
95
+ }
96
+ if (Array.isArray(v)) {
97
+ const items = v.slice(0, 30).map((val) => formatValue(val, seen));
98
+ return `[${items.join(", ")}${v.length > 30 ? ", \u2026" : ""}]`;
99
+ }
100
+ try {
101
+ return JSON.stringify(v);
102
+ } catch {
103
+ const entries = Object.entries(v).slice(0, 15);
104
+ const parts = entries.map(([k, val]) => `${k}: ${formatValue(val, seen)}`);
105
+ return `{${parts.join(", ")}}`;
106
+ }
107
+ }
108
+ function appStackFrames(stack, max) {
109
+ return stack.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ") && !l.includes("node_modules") && !l.includes("<anonymous>")).slice(0, max);
110
+ }
70
111
  function compactUrl(url, search) {
71
112
  try {
72
113
  const u = new URL(url);
@@ -283,17 +324,12 @@ function compactErrors(errors) {
283
324
  for (const err of seen.values()) {
284
325
  lines.push(err.message);
285
326
  if (err.stack) {
286
- const frames = filterStack(err.stack);
287
- for (const frame of frames) {
288
- lines.push(` at ${frame}`);
289
- }
327
+ for (const frame of appStackFrames(err.stack, 5))
328
+ lines.push(` ${frame}`);
290
329
  }
291
330
  }
292
331
  return lines.join("\n");
293
332
  }
294
- function filterStack(stack) {
295
- return stack.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ")).map((l) => l.slice(3)).filter((l) => !l.includes("node_modules") && !l.includes("<anonymous>")).slice(0, 5);
296
- }
297
333
  function compactState(state) {
298
334
  if (!state || !Object.keys(state).length)
299
335
  return "";
@@ -302,7 +338,7 @@ function compactState(state) {
302
338
  lines.push(`${name}:`);
303
339
  if (typeof value === "object" && value !== null) {
304
340
  for (const [k, v] of Object.entries(value)) {
305
- lines.push(` ${k}: ${formatValue(v)}`);
341
+ lines.push(` ${k}: ${truncate(formatValue(v), 120)}`);
306
342
  }
307
343
  } else {
308
344
  lines.push(` ${String(value)}`);
@@ -310,19 +346,6 @@ function compactState(state) {
310
346
  }
311
347
  return lines.join("\n");
312
348
  }
313
- function formatValue(v) {
314
- if (v === null || v === void 0)
315
- return String(v);
316
- if (typeof v === "string")
317
- return v;
318
- if (typeof v === "number" || typeof v === "boolean")
319
- return String(v);
320
- if (typeof v === "object") {
321
- const s = JSON.stringify(v);
322
- return s.length > 120 ? `${s.slice(0, 120)}\u2026` : s;
323
- }
324
- return String(v);
325
- }
326
349
  function compact(raw) {
327
350
  return {
328
351
  url: raw.url,
@@ -459,12 +482,10 @@ function detailError(errors, index, full) {
459
482
  }
460
483
  const lines = [err.message];
461
484
  if (err.stack) {
462
- const appFrames = err.stack.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ") && !l.includes("node_modules")).slice(0, 3);
463
- if (appFrames.length)
464
- lines.push(...appFrames);
465
- const totalApp = err.stack.split("\n").filter((l) => l.trim().startsWith("at ") && !l.includes("node_modules")).length;
466
- if (totalApp > 3)
467
- lines.push(` ... ${totalApp - 3} more app frames`);
485
+ const all = appStackFrames(err.stack, Infinity);
486
+ lines.push(...all.slice(0, 3));
487
+ if (all.length > 3)
488
+ lines.push(` ... ${all.length - 3} more app frames`);
468
489
  }
469
490
  if (err.line != null)
470
491
  lines.push(`location: ${err.source || ""}:${err.line}:${err.column ?? 0}`);
@@ -482,9 +503,9 @@ function detailState(state, name, full) {
482
503
  const value = state[name];
483
504
  if (full) {
484
505
  try {
485
- return JSON.stringify(value, null, 2);
506
+ return JSON.stringify(value, null, 2) ?? formatValue(value);
486
507
  } catch {
487
- return String(value);
508
+ return formatValue(value);
488
509
  }
489
510
  }
490
511
  if (typeof value !== "object" || value === null)
@@ -502,20 +523,9 @@ function isArraySentinel(v) {
502
523
  return digits.length > 0 && [...digits].every((c) => c >= "0" && c <= "9");
503
524
  }
504
525
  function formatSummaryValue(v) {
505
- if (v === null || v === void 0)
506
- return String(v);
507
- if (typeof v === "string") {
508
- if (isArraySentinel(v))
509
- return v;
510
- return v.length > 80 ? `${v.slice(0, 80)}\u2026` : v;
511
- }
512
- if (typeof v === "number" || typeof v === "boolean")
513
- return String(v);
514
- if (typeof v === "object") {
515
- const s = JSON.stringify(v);
516
- return s.length > 80 ? `${s.slice(0, 80)}\u2026` : s;
517
- }
518
- return String(v);
526
+ if (typeof v === "string" && isArraySentinel(v))
527
+ return v;
528
+ return truncate(formatValue(v), 80);
519
529
  }
520
530
  function jsonSchema(sample) {
521
531
  try {
@@ -749,6 +759,10 @@ function readBody(req) {
749
759
  req.on("end", () => resolve2(s));
750
760
  });
751
761
  }
762
+ function send(res, status, body) {
763
+ res.writeHead(status, { "Content-Type": "text/plain; charset=utf-8" });
764
+ res.end(body);
765
+ }
752
766
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
753
767
  var clientDir = existsSync(resolve(__dirname2, "../client")) ? resolve(__dirname2, "../client") : resolve(__dirname2, "../src/client");
754
768
  var clientPath = resolve(clientDir, "client.ts");
@@ -826,61 +840,29 @@ JS in the page and returns the result \u2014 for anything the typed endpoints ca
826
840
  aipeek auto-detects errors after HMR and prints them to the terminal \u2014 watch for \`[aipeek]\` messages.
827
841
  `;
828
842
  }
829
- function norm(line) {
830
- const t = line.trim();
831
- const i = t.indexOf("localhost:");
832
- if (i === -1)
833
- return t;
834
- let j = i + "localhost:".length;
835
- while (j < t.length && t[j] >= "0" && t[j] <= "9") j++;
836
- return `${t.slice(0, i)}localhost:PORT${t.slice(j)}`;
837
- }
838
- function stripBlocks(content, snippet) {
839
- const known = new Set(snippet.split("\n").map(norm).filter((l) => l.length > 3));
840
- const lines = content.split("\n");
841
- const keep = [];
842
- let inside = false;
843
- let buf = [];
844
- let hits = 0;
845
- const flush = () => {
846
- if (buf.length && hits / buf.length <= 0.5)
847
- keep.push(...buf);
848
- buf = [];
849
- hits = 0;
850
- inside = false;
851
- };
852
- for (const line of lines) {
853
- const isKnown = known.has(norm(line));
854
- if (!inside) {
855
- if (isKnown) {
856
- inside = true;
857
- buf = [line];
858
- hits = 1;
859
- } else {
860
- keep.push(line);
861
- }
862
- continue;
863
- }
864
- buf.push(line);
865
- if (isKnown)
866
- hits++;
867
- else if (buf.slice(-3).every((l) => !known.has(norm(l))))
868
- flush();
869
- }
870
- flush();
871
- return keep.join("\n");
872
- }
843
+ var START_TAG = "<!-- AIPEEK:START -->";
844
+ var END_TAG = "<!-- AIPEEK:END -->";
873
845
  function injectClaudeMd(root, port) {
874
846
  const path = resolve(root, "CLAUDE.md");
875
- const snippet = aipeekSnippet(port);
847
+ const block = `${START_TAG}
848
+ ${aipeekSnippet(port).trim()}
849
+ ${END_TAG}
850
+ `;
876
851
  try {
877
852
  if (!existsSync(path)) {
878
- writeFileSync(path, snippet.trimStart());
853
+ writeFileSync(path, block);
854
+ return;
855
+ }
856
+ const content = readFileSync(path, "utf-8");
857
+ const si = content.indexOf(START_TAG);
858
+ const ei = content.indexOf(END_TAG);
859
+ if (si !== -1 && ei !== -1) {
860
+ writeFileSync(path, content.slice(0, si) + block.trimEnd() + content.slice(ei + END_TAG.length));
879
861
  return;
880
862
  }
881
- const stripped = stripBlocks(readFileSync(path, "utf-8"), snippet).trimEnd();
882
- writeFileSync(path, `${stripped}
883
- ${snippet}`);
863
+ const sep = content.endsWith("\n") ? "" : "\n";
864
+ writeFileSync(path, `${content}${sep}
865
+ ${block}`);
884
866
  } catch {
885
867
  }
886
868
  }
@@ -1043,13 +1025,11 @@ function aipeekPlugin() {
1043
1025
  if (!code && req.method === "POST")
1044
1026
  code = await readBody(req);
1045
1027
  if (!code) {
1046
- res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
1047
- res.end("eval needs ?code= or a POST body");
1028
+ send(res, 400, "eval needs ?code= or a POST body");
1048
1029
  return;
1049
1030
  }
1050
1031
  const r = await evalInClient(code);
1051
- res.writeHead(r.ok ? 200 : 422, { "Content-Type": "text/plain; charset=utf-8" });
1052
- res.end(r.ok ? r.value ?? "undefined" : `error: ${r.error}`);
1032
+ send(res, r.ok ? 200 : 422, r.ok ? r.value ?? "undefined" : `error: ${r.error}`);
1053
1033
  return;
1054
1034
  }
1055
1035
  if (parts[0] === "dom") {
@@ -1057,14 +1037,12 @@ function aipeekPlugin() {
1057
1037
  url.searchParams.get("scope") || void 0,
1058
1038
  url.searchParams.get("sel") || void 0
1059
1039
  );
1060
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1061
- res.end(dom || "(empty)");
1040
+ send(res, 200, dom || "(empty)");
1062
1041
  return;
1063
1042
  }
1064
1043
  if (parts[0] === "screen") {
1065
1044
  const screen = await collectScreenFromClient();
1066
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1067
- res.end(screen || "(empty)");
1045
+ send(res, 200, screen || "(empty)");
1068
1046
  return;
1069
1047
  }
1070
1048
  if (parts[0] === "chain") {
@@ -1075,8 +1053,7 @@ function aipeekPlugin() {
1075
1053
  if (!Array.isArray(steps))
1076
1054
  throw new Error("body must be a JSON array");
1077
1055
  } catch (e) {
1078
- res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
1079
- res.end(`invalid chain body: ${e instanceof Error ? e.message : String(e)}`);
1056
+ send(res, 400, `invalid chain body: ${e instanceof Error ? e.message : String(e)}`);
1080
1057
  return;
1081
1058
  }
1082
1059
  lastRaw = null;
@@ -1102,8 +1079,7 @@ function aipeekPlugin() {
1102
1079
  break;
1103
1080
  }
1104
1081
  }
1105
- res.writeHead(allOk ? 200 : 422, { "Content-Type": "text/plain; charset=utf-8" });
1106
- res.end(lastUi ? `${lines.join("\n")}
1082
+ send(res, allOk ? 200 : 422, lastUi ? `${lines.join("\n")}
1107
1083
 
1108
1084
  --- ui after ---
1109
1085
  ${lastUi}` : lines.join("\n"));
@@ -1121,8 +1097,7 @@ ${lastUi}` : lines.join("\n"));
1121
1097
  };
1122
1098
  const check2 = resolveAction(parts[0], args);
1123
1099
  if (!check2.valid) {
1124
- res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
1125
- res.end(check2.error);
1100
+ send(res, 400, check2.error ?? "invalid action");
1126
1101
  return;
1127
1102
  }
1128
1103
  const result = await sendAction(parts[0], args);
@@ -1133,15 +1108,13 @@ ${lastUi}` : lines.join("\n"));
1133
1108
  const name = q.get("out") || `shot-${result.dataUrl.length}.png`;
1134
1109
  const file = resolve(dir, name);
1135
1110
  writeFileSync(file, Buffer.from(result.dataUrl.split(",")[1], "base64"));
1136
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1137
- res.end(`saved: ${file}`);
1111
+ send(res, 200, `saved: ${file}`);
1138
1112
  return;
1139
1113
  }
1140
- res.writeHead(result.ok ? 200 : 422, { "Content-Type": "text/plain; charset=utf-8" });
1141
1114
  const head = result.ok ? result.detail || "ok" : `${result.error}${result.detail ? `
1142
1115
 
1143
1116
  clickable: ${result.detail}` : ""}`;
1144
- res.end(result.ui ? `${head}
1117
+ send(res, result.ok ? 200 : 422, result.ui ? `${head}
1145
1118
 
1146
1119
  --- ui after ---
1147
1120
  ${result.ui}` : head);
@@ -1152,8 +1125,7 @@ ${result.ui}` : head);
1152
1125
  lastRaw = raw2;
1153
1126
  const result = check(raw2);
1154
1127
  const output = emitCheck(result);
1155
- res.writeHead(result.pass ? 200 : 417, { "Content-Type": "text/plain; charset=utf-8" });
1156
- res.end(output);
1128
+ send(res, result.pass ? 200 : 417, output);
1157
1129
  return;
1158
1130
  }
1159
1131
  if (parts.length >= 1) {
@@ -1161,29 +1133,22 @@ ${result.ui}` : head);
1161
1133
  lastRaw = await collectFromClient();
1162
1134
  const result = detail(lastRaw, parts[0], parts[1], full);
1163
1135
  if (result !== null) {
1164
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1165
- res.end(result);
1136
+ send(res, 200, result);
1166
1137
  return;
1167
1138
  }
1168
- res.writeHead(404, { "Content-Type": "text/plain" });
1169
- res.end(`not found: ${parts.join("/")}`);
1139
+ send(res, 404, `not found: ${parts.join("/")}`);
1170
1140
  return;
1171
1141
  }
1172
1142
  const raw = await collectFromClient();
1173
1143
  lastRaw = raw;
1174
1144
  if (full) {
1175
1145
  const compacted = compact(raw);
1176
- const output = emit(compacted);
1177
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1178
- res.end(output);
1146
+ send(res, 200, emit(compacted));
1179
1147
  } else {
1180
- const output = emitSummary(raw);
1181
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1182
- res.end(output);
1148
+ send(res, 200, emitSummary(raw));
1183
1149
  }
1184
1150
  } catch (err) {
1185
- res.writeHead(504, { "Content-Type": "text/plain" });
1186
- res.end(err instanceof Error ? err.message : "unknown error");
1151
+ send(res, 504, err instanceof Error ? err.message : "unknown error");
1187
1152
  }
1188
1153
  });
1189
1154
  }
@@ -1196,5 +1161,8 @@ export {
1196
1161
  emitSummary,
1197
1162
  emitCheck,
1198
1163
  emitDiff,
1164
+ START_TAG,
1165
+ END_TAG,
1166
+ injectClaudeMd,
1199
1167
  aipeekPlugin
1200
1168
  };
package/dist/index.cjs CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
 
7
7
 
8
- var _chunk3NVB3GGEcjs = require('./chunk-3NVB3GGE.cjs');
8
+ var _chunk5ZZYOETFcjs = require('./chunk-5ZZYOETF.cjs');
9
9
  require('./chunk-Z2Y65YOY.cjs');
10
10
 
11
11
 
@@ -14,4 +14,4 @@ require('./chunk-Z2Y65YOY.cjs');
14
14
 
15
15
 
16
16
 
17
- exports.aipeekPlugin = _chunk3NVB3GGEcjs.aipeekPlugin; exports.check = _chunk3NVB3GGEcjs.check; exports.diffState = _chunk3NVB3GGEcjs.diffState; exports.emitCheck = _chunk3NVB3GGEcjs.emitCheck; exports.emitDiff = _chunk3NVB3GGEcjs.emitDiff; exports.emitSummary = _chunk3NVB3GGEcjs.emitSummary;
17
+ exports.aipeekPlugin = _chunk5ZZYOETFcjs.aipeekPlugin; exports.check = _chunk5ZZYOETFcjs.check; exports.diffState = _chunk5ZZYOETFcjs.diffState; exports.emitCheck = _chunk5ZZYOETFcjs.emitCheck; exports.emitDiff = _chunk5ZZYOETFcjs.emitDiff; exports.emitSummary = _chunk5ZZYOETFcjs.emitSummary;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  emitCheck,
6
6
  emitDiff,
7
7
  emitSummary
8
- } from "./chunk-72ZKZ42D.js";
8
+ } from "./chunk-XA2LT6I4.js";
9
9
  export {
10
10
  aipeekPlugin,
11
11
  check,
package/dist/plugin.cjs CHANGED
@@ -1,7 +1,13 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
2
 
3
- var _chunk3NVB3GGEcjs = require('./chunk-3NVB3GGE.cjs');
3
+
4
+
5
+
6
+ var _chunk5ZZYOETFcjs = require('./chunk-5ZZYOETF.cjs');
4
7
  require('./chunk-Z2Y65YOY.cjs');
5
8
 
6
9
 
7
- exports.aipeekPlugin = _chunk3NVB3GGEcjs.aipeekPlugin;
10
+
11
+
12
+
13
+ exports.END_TAG = _chunk5ZZYOETFcjs.END_TAG; exports.START_TAG = _chunk5ZZYOETFcjs.START_TAG; exports.aipeekPlugin = _chunk5ZZYOETFcjs.aipeekPlugin; exports.injectClaudeMd = _chunk5ZZYOETFcjs.injectClaudeMd;
package/dist/plugin.js CHANGED
@@ -1,6 +1,12 @@
1
1
  import {
2
- aipeekPlugin
3
- } from "./chunk-72ZKZ42D.js";
2
+ END_TAG,
3
+ START_TAG,
4
+ aipeekPlugin,
5
+ injectClaudeMd
6
+ } from "./chunk-XA2LT6I4.js";
4
7
  export {
5
- aipeekPlugin
8
+ END_TAG,
9
+ START_TAG,
10
+ aipeekPlugin,
11
+ injectClaudeMd
6
12
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aipeek",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Gives AI a peek into your running browser app — UI tree, console, network, errors, state",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,6 +42,7 @@
42
42
  },
43
43
  "devDependencies": {
44
44
  "jsdom": "^29.1.1",
45
+ "teact": "file:../teact",
45
46
  "tsup": "^8.4.0",
46
47
  "typescript": "~5.7.2",
47
48
  "vitest": "^3.1.2"
@@ -39,7 +39,7 @@ for (const level of ['log', 'info', 'warn', 'error', 'debug'] as const) {
39
39
  console[level] = (...args: unknown[]) => {
40
40
  pushBounded(consoleLogs, {
41
41
  level,
42
- text: args.map(a => typeof a === 'string' ? a : tryStringify(a)).join(' '),
42
+ text: args.map(a => typeof a === 'string' ? a : formatValue(a)).join(' '),
43
43
  timestamp: Date.now(),
44
44
  }, MAX_CONSOLE)
45
45
  orig.apply(console, args)
@@ -302,11 +302,43 @@ function sampleOf(v: unknown, d: number): unknown {
302
302
  return null
303
303
  }
304
304
 
305
- function tryStringify(v: unknown): string {
305
+ // client-patch 是自包含内联脚本(esbuild transformSync,无模块解析)——不能 import core/util。
306
+ // 这是 core/util.ts formatValue 的镜像;两处契约由各自单测守,client 侧靠运行时验证。
307
+ function formatValue(v: unknown, seen: Set<object> = new Set()): string {
308
+ if (v === null || v === undefined)
309
+ return String(v)
310
+ const t = typeof v
311
+ if (t === 'string')
312
+ return v as string
313
+ if (t === 'number' || t === 'boolean' || t === 'bigint')
314
+ return String(v)
315
+ if (t === 'symbol')
316
+ return (v as symbol).toString()
317
+ if (t === 'function')
318
+ return `[Function: ${(v as { name?: string }).name || 'anonymous'}]`
319
+ const obj = v as object
320
+ if (seen.has(obj))
321
+ return '[Circular]'
322
+ if (v instanceof Error)
323
+ return v.stack || `${v.name}: ${v.message}`
324
+ seen.add(obj)
325
+ if (v instanceof Map) {
326
+ const items = [...v.entries()].slice(0, 15).map(([k, val]) => `${formatValue(k, seen)} => ${formatValue(val, seen)}`)
327
+ return `Map(${v.size}) {${items.join(', ')}${v.size > 15 ? ', …' : ''}}`
328
+ }
329
+ if (v instanceof Set) {
330
+ const items = [...v.values()].slice(0, 15).map(val => formatValue(val, seen))
331
+ return `Set(${v.size}) {${items.join(', ')}${v.size > 15 ? ', …' : ''}}`
332
+ }
333
+ if (Array.isArray(v)) {
334
+ const items = v.slice(0, 30).map(val => formatValue(val, seen))
335
+ return `[${items.join(', ')}${v.length > 30 ? ', …' : ''}]`
336
+ }
306
337
  try {
307
338
  return JSON.stringify(v)
308
339
  }
309
340
  catch {
310
- return String(v)
341
+ const entries = Object.entries(v as Record<string, unknown>).slice(0, 15)
342
+ return `{${entries.map(([k, val]) => `${k}: ${formatValue(val, seen)}`).join(', ')}}`
311
343
  }
312
344
  }
@@ -5,7 +5,7 @@
5
5
 
6
6
  import type { ActionArgs, ActionType } from '../core/action'
7
7
  import type { ErrorEntry, LogEntry, NetworkRequest } from '../core/types'
8
- import { INTERACTIVE, performAction } from '../core/action'
8
+ import { INTERACTIVE, performAction, runEval, withDialogGuard } from '../core/action'
9
9
 
10
10
  declare global {
11
11
  interface Window { __AIPEEK_STORES__?: Record<string, unknown> }
@@ -643,7 +643,10 @@ if (import.meta.hot) {
643
643
  import.meta.hot.on('aipeek:action', async (msg: { id: number, type: ActionType, args: ActionArgs, requireVisible?: boolean }) => {
644
644
  if (skip(msg))
645
645
  return
646
- const result = await performAction(msg.type, msg.args)
646
+ // Guard against native alert/confirm/prompt freezing the probe (see withDialogGuard).
647
+ const { result, dialogs } = await withDialogGuard(() => performAction(msg.type, msg.args))
648
+ if (dialogs.length)
649
+ result.detail = `${result.detail ?? ''} [auto-dismissed ${dialogs.join('; ')}]`.trim()
647
650
  // For mutating actions, settle the DOM then ship both the full UI tree and
648
651
  // the compact screen projection — the caller skips a round-trip to /ui, and
649
652
  // /chain uses the per-step screen so an interaction's every transition shows.
@@ -654,24 +657,11 @@ if (import.meta.hot) {
654
657
  import.meta.hot!.send('aipeek:result', { id: msg.id, ...result })
655
658
  })
656
659
 
657
- // eval: run server-supplied code in the page. Wrapped in an async IIFE so the
658
- // code can `await` and use `return`; non-string results are JSON-stringified.
660
+ // eval: run server-supplied code in the page with auto-return (see runEval).
659
661
  import.meta.hot.on('aipeek:eval', async (msg: { id: number, code: string, requireVisible?: boolean }) => {
660
662
  if (skip(msg))
661
663
  return
662
- let ok = true
663
- let value: string | undefined
664
- let error: string | undefined
665
- try {
666
- // eslint-disable-next-line no-new-func
667
- const fn = new Function(`return (async () => { ${msg.code} })()`)
668
- const result = await fn()
669
- value = typeof result === 'string' ? result : JSON.stringify(result, null, 2)
670
- }
671
- catch (e) {
672
- ok = false
673
- error = e instanceof Error ? `${e.message}\n${e.stack ?? ''}` : String(e)
674
- }
664
+ const { ok, value, error } = await runEval(msg.code)
675
665
  import.meta.hot!.send('aipeek:eval-result', { id: msg.id, ok, value, error })
676
666
  })
677
667
 
@@ -57,7 +57,7 @@ export function resolveAction(type: string, args: ActionArgs): { valid: boolean,
57
57
 
58
58
  // --- Browser-side execution (client.ts only) ---
59
59
 
60
- export const INTERACTIVE = 'a, button, input, textarea, select, [role], [onclick], [tabindex], [contenteditable]'
60
+ export const INTERACTIVE = 'a, button, input, textarea, select, [role], [onclick], [tabindex], [contenteditable], [aria-label]'
61
61
 
62
62
  function visibleText(el: Element): string {
63
63
  const aria = el.getAttribute('aria-label')
@@ -114,6 +114,59 @@ export async function performAction(type: ActionType, args: ActionArgs): Promise
114
114
  }
115
115
  }
116
116
 
117
+ export interface EvalResult { ok: boolean, value?: string, error?: string }
118
+
119
+ // Run server-supplied JS in the page with auto-return (Chrome-console / Node-REPL
120
+ // ergonomics): try the whole code as a single expression first so `qsa(...).length`
121
+ // or `1+1` yield a value without an explicit `return`. If that fails to *compile*
122
+ // (multi-statement, contains `return`, etc.) fall back to a plain statement block.
123
+ // A compile error only swaps the wrapper; a runtime throw surfaces as the error.
124
+ // undefined results are dropped (no value); objects are JSON-stringified.
125
+ export async function runEval(code: string): Promise<EvalResult> {
126
+ try {
127
+ let fn: () => Promise<unknown>
128
+ try {
129
+ // eslint-disable-next-line no-new-func
130
+ fn = new Function(`return (async () => (${code}))()`) as () => Promise<unknown>
131
+ }
132
+ catch {
133
+ // eslint-disable-next-line no-new-func
134
+ fn = new Function(`return (async () => { ${code} })()`) as () => Promise<unknown>
135
+ }
136
+ const result = await fn()
137
+ const value = result === undefined ? undefined : typeof result === 'string' ? result : JSON.stringify(result, null, 2)
138
+ return { ok: true, value }
139
+ }
140
+ catch (e) {
141
+ return { ok: false, error: e instanceof Error ? `${e.message}\n${e.stack ?? ''}` : String(e) }
142
+ }
143
+ }
144
+
145
+ // Native alert/confirm/prompt are *synchronous* and freeze the whole JS thread until
146
+ // a human dismisses them — which deadlocks the probe (it runs on that same thread, so
147
+ // the HMR channel can never answer and every curl times out). A click that hits a
148
+ // `copy-to-clipboard` fallback or a `confirm("delete?")` would hang aipeek forever.
149
+ // So we stub them for the duration of `fn`: auto-answer (confirm→true, prompt→default,
150
+ // alert→noop) and return what was suppressed so the caller can report it. Always
151
+ // restored in finally — the page's own dialogs work again after the action settles.
152
+ export async function withDialogGuard<T>(fn: () => Promise<T>): Promise<{ result: T, dialogs: string[] }> {
153
+ const realAlert = window.alert
154
+ const realConfirm = window.confirm
155
+ const realPrompt = window.prompt
156
+ const dialogs: string[] = []
157
+ window.alert = (m?: unknown) => { dialogs.push(`alert: ${String(m ?? '')}`.slice(0, 80)) }
158
+ window.confirm = (m?: unknown) => { dialogs.push(`confirm→true: ${String(m ?? '')}`.slice(0, 80)); return true }
159
+ window.prompt = (m?: unknown, d?: string) => { dialogs.push(`prompt→default: ${String(m ?? '')}`.slice(0, 80)); return d ?? '' }
160
+ try {
161
+ return { result: await fn(), dialogs }
162
+ }
163
+ finally {
164
+ window.alert = realAlert
165
+ window.confirm = realConfirm
166
+ window.prompt = realPrompt
167
+ }
168
+ }
169
+
117
170
  // Click like a human, not like el.click(). A real click is a *position* the browser
118
171
  // hit-tests, then a full event sequence at that point — hover, pointerdown/mousedown,
119
172
  // browser-decided focus, pointerup/mouseup, click. Two things matter that el.click()