aipeek 0.2.6 → 0.2.8
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/README.md +94 -19
- package/dist/{chunk-XA2LT6I4.js → chunk-4BPXH2SW.js} +715 -59
- package/dist/{chunk-5ZZYOETF.cjs → chunk-SDUTK75Y.cjs} +717 -61
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +1 -1
- package/dist/plugin.cjs +4 -2
- package/dist/plugin.js +5 -3
- package/package.json +3 -1
- package/src/babel/line-profiler.ts +190 -0
- package/src/client/client-patch.ts +326 -2
- package/src/client/client.ts +255 -42
- package/src/core/action.ts +274 -18
- package/src/core/compact.ts +2 -0
- package/src/core/detail.ts +3 -1
- package/src/core/diff.ts +55 -1
- package/src/core/emit.ts +14 -2
- package/src/core/perf.ts +239 -0
- package/src/core/types.ts +73 -0
- package/src/core/util.ts +115 -0
- package/src/server/plugin.ts +577 -65
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "url";
|
|
|
6
6
|
import { transformSync } from "esbuild";
|
|
7
7
|
|
|
8
8
|
// src/core/action.ts
|
|
9
|
-
var TYPES = ["click", "fill", "press", "wait", "screenshot"];
|
|
9
|
+
var TYPES = ["click", "fill", "press", "wait", "screenshot", "realclick", "query", "assert", "drag", "scrollIntoView", "drop", "clipboard"];
|
|
10
10
|
function resolveAction(type, args) {
|
|
11
11
|
if (!TYPES.includes(type))
|
|
12
12
|
return { valid: false, error: `unknown action: ${type}` };
|
|
@@ -14,6 +14,8 @@ function resolveAction(type, args) {
|
|
|
14
14
|
switch (type) {
|
|
15
15
|
case "click":
|
|
16
16
|
return hasTarget ? { valid: true } : { valid: false, error: "click needs sel= or text=" };
|
|
17
|
+
case "realclick":
|
|
18
|
+
return hasTarget || args.x !== void 0 && args.y !== void 0 ? { valid: true } : { valid: false, error: "realclick needs sel=, text=, or x= & y=" };
|
|
17
19
|
case "fill":
|
|
18
20
|
if (!hasTarget)
|
|
19
21
|
return { valid: false, error: "fill needs sel= or text=" };
|
|
@@ -26,6 +28,32 @@ function resolveAction(type, args) {
|
|
|
26
28
|
return hasTarget ? { valid: true } : { valid: false, error: "wait needs sel= or text=" };
|
|
27
29
|
case "screenshot":
|
|
28
30
|
return { valid: true };
|
|
31
|
+
case "query":
|
|
32
|
+
return args.sel ? { valid: true } : { valid: false, error: "query needs sel=" };
|
|
33
|
+
case "assert":
|
|
34
|
+
if (!args.screen && !args.sel)
|
|
35
|
+
return { valid: false, error: "assert needs screen= (a __AIPEEK_SCREEN__ key) or sel=" };
|
|
36
|
+
if (args.equals === void 0)
|
|
37
|
+
return { valid: false, error: "assert needs equals=" };
|
|
38
|
+
return { valid: true };
|
|
39
|
+
case "drag":
|
|
40
|
+
if (!hasTarget)
|
|
41
|
+
return { valid: false, error: "drag needs sel= or text= (the source)" };
|
|
42
|
+
if (!args.to)
|
|
43
|
+
return { valid: false, error: "drag needs to= (destination selector)" };
|
|
44
|
+
return { valid: true };
|
|
45
|
+
case "scrollIntoView":
|
|
46
|
+
return hasTarget ? { valid: true } : { valid: false, error: "scrollIntoView needs sel= or text=" };
|
|
47
|
+
case "drop":
|
|
48
|
+
if (!hasTarget)
|
|
49
|
+
return { valid: false, error: "drop needs sel= or text= (the drop target)" };
|
|
50
|
+
if (!args.files || !args.files.length)
|
|
51
|
+
return { valid: false, error: "drop needs files= (array of file names)" };
|
|
52
|
+
return { valid: true };
|
|
53
|
+
case "clipboard":
|
|
54
|
+
if (args.mode === "write" && args.value === void 0)
|
|
55
|
+
return { valid: false, error: "clipboard write needs value=" };
|
|
56
|
+
return { valid: true };
|
|
29
57
|
default:
|
|
30
58
|
return { valid: false, error: `unknown action: ${type}` };
|
|
31
59
|
}
|
|
@@ -63,6 +91,155 @@ function check(raw) {
|
|
|
63
91
|
};
|
|
64
92
|
}
|
|
65
93
|
|
|
94
|
+
// src/core/perf.ts
|
|
95
|
+
var FRAME_NOISE_PCT = 3;
|
|
96
|
+
var SELF_NOISE_REL = 0.1;
|
|
97
|
+
var SELF_NOISE_MS = 5;
|
|
98
|
+
var avgOf = (samples) => samples.length > 0 ? samples.reduce((s, v) => s + v, 0) / samples.length : 0;
|
|
99
|
+
function p95(samples) {
|
|
100
|
+
if (samples.length === 0) return 0;
|
|
101
|
+
const sorted = [...samples].sort((a, b) => a - b);
|
|
102
|
+
const idx = Math.floor(0.95 * sorted.length);
|
|
103
|
+
return sorted[idx] ?? 0;
|
|
104
|
+
}
|
|
105
|
+
function summarizeStat(stat) {
|
|
106
|
+
const avg = stat.n > 0 ? stat.total / stat.n : 0;
|
|
107
|
+
return {
|
|
108
|
+
total: stat.total,
|
|
109
|
+
n: stat.n,
|
|
110
|
+
avg,
|
|
111
|
+
max: stat.max,
|
|
112
|
+
p95: p95(stat.samples)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function compactPerformance(p) {
|
|
116
|
+
const all = p.buckets.find((b) => b.name === "__all__");
|
|
117
|
+
if (!all) return "perf: (no data)";
|
|
118
|
+
const { total, long, samples } = all.frames;
|
|
119
|
+
const fps = total > 0 ? Math.round(1e3 / avgOf(samples) * 10) / 10 : 0;
|
|
120
|
+
const droppedPct = total > 0 ? Math.round(long / total * 100) : 0;
|
|
121
|
+
const parts = [`${fps}fps`, `${droppedPct}% dropped`];
|
|
122
|
+
if (p.longtasks.count > 0) {
|
|
123
|
+
parts.push(`${p.longtasks.count} longtasks(max ${Math.round(p.longtasks.max)}ms)`);
|
|
124
|
+
}
|
|
125
|
+
const topComps = Object.entries(all.components).map(([name, stat]) => ({ name, total: stat.total })).sort((a, b) => b.total - a.total).slice(0, 2);
|
|
126
|
+
if (topComps.length > 0 && p.mobxPatched) {
|
|
127
|
+
const hot = topComps.map((c) => `${c.name} ${Math.round(c.total)}ms`).join(", ");
|
|
128
|
+
parts.push(`hot: ${hot}`);
|
|
129
|
+
}
|
|
130
|
+
return `perf: ${parts.join(" \xB7 ")}`;
|
|
131
|
+
}
|
|
132
|
+
function detailPerformance(p) {
|
|
133
|
+
const lines = [];
|
|
134
|
+
const windowSec = (p.windowMs / 1e3).toFixed(1);
|
|
135
|
+
lines.push(`=== Performance (${windowSec}s window) ===`);
|
|
136
|
+
if (p.hiddenFrames > 0) {
|
|
137
|
+
lines.push(`\u26A0 Tab was backgrounded \u2014 ${p.hiddenFrames} hidden frames skipped (rAF throttling lies about timing)`);
|
|
138
|
+
lines.push("For accurate profiling: keep tab FOREGROUND + run /profile/reset before reproducing");
|
|
139
|
+
}
|
|
140
|
+
lines.push(`MobX attribution: ${p.mobxPatched ? "ON" : "OFF"}`);
|
|
141
|
+
if (!p.mobxPatched) {
|
|
142
|
+
lines.push("(Component attribution disabled \u2014 set window.__AIPEEK_MOBX__={Reaction} in src/lib/mobx.ts to enable)");
|
|
143
|
+
}
|
|
144
|
+
lines.push("");
|
|
145
|
+
if (p.longtasks.count > 0) {
|
|
146
|
+
lines.push(`Longtasks (>50ms): ${p.longtasks.count}, max ${Math.round(p.longtasks.max)}ms`);
|
|
147
|
+
lines.push("");
|
|
148
|
+
}
|
|
149
|
+
const showPerView = p.buckets.length > 1;
|
|
150
|
+
for (const bucket of p.buckets) {
|
|
151
|
+
if (bucket.name !== "__all__" && !showPerView) continue;
|
|
152
|
+
lines.push(`--- ${bucket.name} ---`);
|
|
153
|
+
const { total, long, max, samples } = bucket.frames;
|
|
154
|
+
const droppedPct = total > 0 ? (long / total * 100).toFixed(1) : "0.0";
|
|
155
|
+
const avgFrame = avgOf(samples).toFixed(1);
|
|
156
|
+
lines.push(`Frames: ${total} total, ${long} dropped (${droppedPct}%), max ${Math.round(max)}ms, avg ${avgFrame}ms`);
|
|
157
|
+
const comps = Object.entries(bucket.components).map(([name, stat]) => ({ name, ...summarizeStat(stat) })).sort((a, b) => b.total - a.total).slice(0, 20);
|
|
158
|
+
if (comps.length > 0 && p.mobxPatched) {
|
|
159
|
+
lines.push("Components (by total self-time):");
|
|
160
|
+
for (const c of comps) {
|
|
161
|
+
lines.push(` ${c.name.padEnd(30)} ${Math.round(c.total)}ms / ${c.n}\xD7 = ${c.avg.toFixed(1)}ms avg, max ${Math.round(c.max)}ms, p95 ${Math.round(c.p95)}ms`);
|
|
162
|
+
}
|
|
163
|
+
} else if (!p.mobxPatched) {
|
|
164
|
+
lines.push("(no component data \u2014 attribution OFF)");
|
|
165
|
+
}
|
|
166
|
+
const hotLines = Object.entries(bucket.lines ?? {}).map(([label, stat]) => ({ label, ...summarizeStat(stat) })).sort((a, b) => b.total - a.total).slice(0, 15);
|
|
167
|
+
if (hotLines.length > 0) {
|
|
168
|
+
lines.push("Hot lines (by total self-time):");
|
|
169
|
+
for (const l of hotLines) {
|
|
170
|
+
lines.push(` ${l.label.padEnd(36)} ${Math.round(l.total)}ms / ${l.n}\xD7 = ${l.avg.toFixed(1)}ms avg, max ${Math.round(l.max)}ms`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
lines.push("");
|
|
174
|
+
}
|
|
175
|
+
return lines.join("\n");
|
|
176
|
+
}
|
|
177
|
+
function diffPerformance(before, after) {
|
|
178
|
+
const out = [];
|
|
179
|
+
const beforeAll = before.buckets.find((b) => b.name === "__all__");
|
|
180
|
+
const afterAll = after.buckets.find((b) => b.name === "__all__");
|
|
181
|
+
const droppedPct = (b) => b && b.frames.total > 0 ? b.frames.long / b.frames.total * 100 : 0;
|
|
182
|
+
const beforeDrop = droppedPct(beforeAll);
|
|
183
|
+
const afterDrop = droppedPct(afterAll);
|
|
184
|
+
const dropDelta = afterDrop - beforeDrop;
|
|
185
|
+
const sumTotal = (b) => b ? Object.values(b.components).reduce((s, c) => s + c.total, 0) : 0;
|
|
186
|
+
const beforeSelf = sumTotal(beforeAll);
|
|
187
|
+
const afterSelf = sumTotal(afterAll);
|
|
188
|
+
const selfDelta = afterSelf - beforeSelf;
|
|
189
|
+
const bothSampledFrames = (beforeAll?.frames.total ?? 0) > 0 && (afterAll?.frames.total ?? 0) > 0;
|
|
190
|
+
const sawDrops = (beforeAll?.frames.long ?? 0) > 0 || (afterAll?.frames.long ?? 0) > 0;
|
|
191
|
+
const framesExercised = bothSampledFrames && sawDrops;
|
|
192
|
+
let verdict;
|
|
193
|
+
if (framesExercised) {
|
|
194
|
+
if (dropDelta < -FRAME_NOISE_PCT) verdict = "IMPROVED";
|
|
195
|
+
else if (dropDelta > FRAME_NOISE_PCT) verdict = "REGRESSED";
|
|
196
|
+
else verdict = "UNCHANGED";
|
|
197
|
+
} else {
|
|
198
|
+
const floor = Math.max(SELF_NOISE_MS, beforeSelf * SELF_NOISE_REL);
|
|
199
|
+
if (selfDelta < -floor) verdict = "IMPROVED";
|
|
200
|
+
else if (selfDelta > floor) verdict = "REGRESSED";
|
|
201
|
+
else verdict = "UNCHANGED";
|
|
202
|
+
}
|
|
203
|
+
out.push(`=== Perf diff: ${verdict} ===`);
|
|
204
|
+
out.push(`Dropped frames: ${beforeDrop.toFixed(1)}% \u2192 ${afterDrop.toFixed(1)}% (${dropDelta >= 0 ? "+" : ""}${dropDelta.toFixed(1)}%)`);
|
|
205
|
+
if (!framesExercised && Math.round(selfDelta) !== 0) {
|
|
206
|
+
out.push(`Total self-time: ${Math.round(beforeSelf)}ms \u2192 ${Math.round(afterSelf)}ms (${selfDelta >= 0 ? "+" : ""}${Math.round(selfDelta)}ms) \u2014 frames flat, verdict by self-time`);
|
|
207
|
+
}
|
|
208
|
+
if (before.longtasks.count !== after.longtasks.count) {
|
|
209
|
+
out.push(`Longtasks: ${before.longtasks.count} \u2192 ${after.longtasks.count}`);
|
|
210
|
+
}
|
|
211
|
+
out.push("");
|
|
212
|
+
const compDelta = keyedTotalDelta(beforeAll?.components ?? {}, afterAll?.components ?? {});
|
|
213
|
+
if (compDelta.length > 0) {
|
|
214
|
+
out.push("Components (\u0394 total self-time):");
|
|
215
|
+
for (const d of compDelta) out.push(" " + formatDelta(d));
|
|
216
|
+
out.push("");
|
|
217
|
+
}
|
|
218
|
+
const lineDelta = keyedTotalDelta(beforeAll?.lines ?? {}, afterAll?.lines ?? {});
|
|
219
|
+
if (lineDelta.length > 0) {
|
|
220
|
+
out.push("Hot lines (\u0394 total self-time):");
|
|
221
|
+
for (const d of lineDelta) out.push(" " + formatDelta(d));
|
|
222
|
+
}
|
|
223
|
+
return out.join("\n").trimEnd();
|
|
224
|
+
}
|
|
225
|
+
function keyedTotalDelta(a, b) {
|
|
226
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
227
|
+
const rows = [];
|
|
228
|
+
for (const key of keys) {
|
|
229
|
+
const beforeT = a[key]?.total ?? 0;
|
|
230
|
+
const afterT = b[key]?.total ?? 0;
|
|
231
|
+
const delta = afterT - beforeT;
|
|
232
|
+
if (Math.round(delta) === 0) continue;
|
|
233
|
+
rows.push({ key, before: beforeT, after: afterT, delta, isNew: !a[key], gone: !b[key] });
|
|
234
|
+
}
|
|
235
|
+
return rows.sort((x, y) => Math.abs(y.delta) - Math.abs(x.delta)).slice(0, 20);
|
|
236
|
+
}
|
|
237
|
+
function formatDelta(d) {
|
|
238
|
+
const sign = d.delta >= 0 ? "+" : "";
|
|
239
|
+
const tag = d.isNew ? " (new)" : d.gone ? " (gone)" : "";
|
|
240
|
+
return `${d.key.padEnd(36)} ${Math.round(d.before)}ms \u2192 ${Math.round(d.after)}ms (${sign}${Math.round(d.delta)}ms)${tag}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
66
243
|
// src/core/util.ts
|
|
67
244
|
function truncate(s, max) {
|
|
68
245
|
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
@@ -118,6 +295,78 @@ function compactUrl(url, search) {
|
|
|
118
295
|
return truncate(url, 80);
|
|
119
296
|
}
|
|
120
297
|
}
|
|
298
|
+
var STALE_MS = 3e4;
|
|
299
|
+
var isLive = (t, now) => now - t.lastSeen < STALE_MS;
|
|
300
|
+
function formatTabs(tabs, now) {
|
|
301
|
+
const live = tabs.filter((t) => isLive(t, now));
|
|
302
|
+
if (!live.length)
|
|
303
|
+
return "(no live tabs)";
|
|
304
|
+
return live.sort((a, b) => Number(b.visible) - Number(a.visible) || a.id.localeCompare(b.id)).map((t) => {
|
|
305
|
+
const age = Math.round((now - t.lastSeen) / 1e3);
|
|
306
|
+
const mark = t.visible ? "\u25CF visible" : "\u25CB background";
|
|
307
|
+
return `${t.id} ${mark} ${truncate(t.title || "(untitled)", 40)} ${compactUrl(t.url)} ${age}s ago`;
|
|
308
|
+
}).join("\n");
|
|
309
|
+
}
|
|
310
|
+
function diagnose(tab, tabs, now, socketCount, port) {
|
|
311
|
+
if (tab) {
|
|
312
|
+
const t = tabs.find((x) => x.id === tab);
|
|
313
|
+
return t && isLive(t, now) ? `tab '${tab}' is connected but didn't answer in time \u2014 the handler likely threw or hung (bad selector, infinite loop, or a read that needs the tab foreground). Check /__aipeek/console.` : `tab '${tab}' is not connected \u2014 it closed, or the server restarted and it hasn't re-handshaked yet. Check /__aipeek/tabs for live ids; if the page is mid self-heal, retry in ~2s.`;
|
|
314
|
+
}
|
|
315
|
+
const live = tabs.filter((t) => isLive(t, now));
|
|
316
|
+
if (live.length)
|
|
317
|
+
return `${live.length} tab(s) connected but none answered \u2014 a handler threw, or they just closed. Check /__aipeek/tabs (still live?) and /__aipeek/console (errors?).`;
|
|
318
|
+
return socketCount > 0 ? `no aipeek tab registered, but ${socketCount} page(s) are connected to this dev server via HMR \u2014 the page is open yet the aipeek client didn't load. Is it served by THIS vite server on :${port}? A separate vite config / vite preview / production build won't inject it.` : `no browser tab connected to this dev server on :${port} \u2014 open the app, or check you're hitting the right port.`;
|
|
319
|
+
}
|
|
320
|
+
function formatActions(entries, anchorTs) {
|
|
321
|
+
if (!entries.length)
|
|
322
|
+
return "(no actions)";
|
|
323
|
+
const sorted = [...entries].sort((a, b) => a.ts - b.ts);
|
|
324
|
+
const multiTab = new Set(sorted.map((e) => e.tab).filter(Boolean)).size > 1;
|
|
325
|
+
const lines = [];
|
|
326
|
+
const prevByTab = /* @__PURE__ */ new Map();
|
|
327
|
+
let anchored = false;
|
|
328
|
+
for (const e of sorted) {
|
|
329
|
+
const isAnchor = anchorTs !== void 0 && !anchored && e.ts === anchorTs;
|
|
330
|
+
if (isAnchor) {
|
|
331
|
+
lines.push("\u2014\u2014\u2014\u2014\u2014\u2014 \u4F60\u5F53\u524D\u7684\u884C\u4E3A \u2014\u2014\u2014\u2014\u2014\u2014");
|
|
332
|
+
anchored = true;
|
|
333
|
+
}
|
|
334
|
+
const who = e.trusted ? "T" : "S";
|
|
335
|
+
const tag = multiTab ? `${e.tab ?? "?"} ` : "";
|
|
336
|
+
const val = e.value ? ` value="${e.value}"` : "";
|
|
337
|
+
const change = uiChange(prevByTab.get(e.tab), e);
|
|
338
|
+
lines.push(`${tag}[${who}] ${e.type} ${e.target}${val}${change ? ` \u2192 ${change}` : ""}`);
|
|
339
|
+
if (isAnchor)
|
|
340
|
+
lines.push("\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014");
|
|
341
|
+
prevByTab.set(e.tab, e);
|
|
342
|
+
}
|
|
343
|
+
return lines.join("\n");
|
|
344
|
+
}
|
|
345
|
+
function appendAction(log, tab, entry, max) {
|
|
346
|
+
const last = log[log.length - 1];
|
|
347
|
+
if (entry.type === "input" && last?.type === "input" && last.tab === tab && last.target === entry.target) {
|
|
348
|
+
log[log.length - 1] = { ...entry, tab };
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
log.push({ ...entry, tab });
|
|
352
|
+
if (log.length > max)
|
|
353
|
+
log.shift();
|
|
354
|
+
}
|
|
355
|
+
function uiChange(prev, cur) {
|
|
356
|
+
if (cur.modal === void 0)
|
|
357
|
+
return "";
|
|
358
|
+
const prevModal = prev?.modal ?? "none";
|
|
359
|
+
if (cur.modal !== prevModal) {
|
|
360
|
+
if (cur.modal === "none")
|
|
361
|
+
return "\u5F39\u7A97\u5173\u95ED";
|
|
362
|
+
if (prevModal === "none")
|
|
363
|
+
return `\u5F39\u7A97\u6253\u5F00\u300C${cur.modal}\u300D`;
|
|
364
|
+
return `\u5F39\u7A97\u5207\u6362\u300C${cur.modal}\u300D`;
|
|
365
|
+
}
|
|
366
|
+
if (prev && cur.view !== void 0 && cur.view !== prev.view)
|
|
367
|
+
return `\u89C6\u56FE ${prev.view}\u2192${cur.view}`;
|
|
368
|
+
return "";
|
|
369
|
+
}
|
|
121
370
|
|
|
122
371
|
// src/core/compact.ts
|
|
123
372
|
var SLOW_THRESHOLD = 1e3;
|
|
@@ -354,6 +603,7 @@ function compact(raw) {
|
|
|
354
603
|
network: compactNetwork(raw.network),
|
|
355
604
|
errors: compactErrors(raw.errors),
|
|
356
605
|
state: compactState(raw.state),
|
|
606
|
+
performance: raw.performance ? detailPerformance(raw.performance) : void 0,
|
|
357
607
|
timestamp: raw.timestamp,
|
|
358
608
|
counts: {
|
|
359
609
|
console: raw.console.length,
|
|
@@ -377,6 +627,8 @@ function detail(raw, section, index, full) {
|
|
|
377
627
|
return detailError(raw.errors, index, full);
|
|
378
628
|
case "state":
|
|
379
629
|
return detailState(raw.state, index, full);
|
|
630
|
+
case "profile":
|
|
631
|
+
return raw.performance ? detailPerformance(raw.performance) : "(no perf data \u2014 is the tab foreground?)";
|
|
380
632
|
default:
|
|
381
633
|
return null;
|
|
382
634
|
}
|
|
@@ -574,6 +826,43 @@ function byteSize(s) {
|
|
|
574
826
|
}
|
|
575
827
|
|
|
576
828
|
// src/core/diff.ts
|
|
829
|
+
function diffScreen(before, after, newErrors, newExceptions, newFailedRequests) {
|
|
830
|
+
const lines = [];
|
|
831
|
+
if (after.view !== before.view)
|
|
832
|
+
lines.push(`view: ${before.view} \u2192 ${after.view}`);
|
|
833
|
+
if (after.modal !== before.modal) {
|
|
834
|
+
if (after.modal === "none")
|
|
835
|
+
lines.push(`modal: closed (${before.modal})`);
|
|
836
|
+
else if (before.modal === "none")
|
|
837
|
+
lines.push(`modal: opened ${after.modal}`);
|
|
838
|
+
else
|
|
839
|
+
lines.push(`modal: ${before.modal} \u2192 ${after.modal}`);
|
|
840
|
+
}
|
|
841
|
+
if (after.focus !== before.focus)
|
|
842
|
+
lines.push(`focus: ${after.focus}`);
|
|
843
|
+
const beforeDomain = before.domain ?? {};
|
|
844
|
+
const afterDomain = after.domain ?? {};
|
|
845
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(beforeDomain), ...Object.keys(afterDomain)])) {
|
|
846
|
+
const b = stringifyDomain(beforeDomain[key]);
|
|
847
|
+
const a = stringifyDomain(afterDomain[key]);
|
|
848
|
+
if (b !== a)
|
|
849
|
+
lines.push(`${key}: ${b} \u2192 ${a}`);
|
|
850
|
+
}
|
|
851
|
+
for (const e of newErrors)
|
|
852
|
+
lines.push(`+error: ${e.text}`);
|
|
853
|
+
for (const e of newExceptions)
|
|
854
|
+
lines.push(`+exception: ${e.message}`);
|
|
855
|
+
for (const r of newFailedRequests)
|
|
856
|
+
lines.push(`+failed: ${r.method} ${r.url} \u2192 ${r.status || "failed"}`);
|
|
857
|
+
return lines;
|
|
858
|
+
}
|
|
859
|
+
function stringifyDomain(v) {
|
|
860
|
+
if (v === null || v === void 0)
|
|
861
|
+
return String(v);
|
|
862
|
+
if (typeof v === "object")
|
|
863
|
+
return JSON.stringify(v).slice(0, 80);
|
|
864
|
+
return String(v).slice(0, 80);
|
|
865
|
+
}
|
|
577
866
|
function diffState(prev, curr) {
|
|
578
867
|
if (!prev) {
|
|
579
868
|
return {
|
|
@@ -604,7 +893,7 @@ function diffState(prev, curr) {
|
|
|
604
893
|
|
|
605
894
|
// src/core/emit.ts
|
|
606
895
|
import pc from "picocolors";
|
|
607
|
-
var SECTIONS = ["ui", "console", "network", "errors", "state"];
|
|
896
|
+
var SECTIONS = ["ui", "console", "network", "errors", "state", "performance"];
|
|
608
897
|
var COUNTED_SECTIONS = {
|
|
609
898
|
console: "console",
|
|
610
899
|
network: "network",
|
|
@@ -675,6 +964,15 @@ function emitSummary(raw) {
|
|
|
675
964
|
lines.push(`errors (${raw.errors.length}):`);
|
|
676
965
|
for (const e of raw.errors) lines.push(` ${truncate(e.message, 150)}`);
|
|
677
966
|
}
|
|
967
|
+
if (raw.performance) {
|
|
968
|
+
const all = raw.performance.buckets.find((b) => b.name === "__all__");
|
|
969
|
+
if (all) {
|
|
970
|
+
const droppedPct = all.frames.total > 0 ? all.frames.long / all.frames.total * 100 : 0;
|
|
971
|
+
if (droppedPct > 10) {
|
|
972
|
+
lines.push(compactPerformance(raw.performance));
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
678
976
|
const storeNames = Object.keys(raw.state);
|
|
679
977
|
if (storeNames.length) {
|
|
680
978
|
const parts = storeNames.map((n) => {
|
|
@@ -782,12 +1080,24 @@ function aipeekSnippet(port) {
|
|
|
782
1080
|
return `
|
|
783
1081
|
# aipeek \u2014 Runtime Browser Inspector
|
|
784
1082
|
|
|
1083
|
+
IMPORTANT: Before debugging any UI issue, visual bug, or runtime error, ALWAYS \`curl ${base}/screen\` to see the live app first. Do NOT guess, and do NOT ask the user to open or describe the page \u2014 you read it directly, including background tabs.
|
|
1084
|
+
|
|
1085
|
+
- \`curl ${base}/screen\` \u2014 state-machine view of the UI (start here). \`/ui\`, \`/dom?scope=X\` for detail; \`/console\`, \`/network\`, \`/errors\`, \`/check\` for health.
|
|
1086
|
+
- \`curl ${base}/click?text=\u2026\` (also \`/fill\`, \`/press\`, \`/wait\`) \u2014 drive the page. POST \`/chain\` for a whole scripted interaction.
|
|
1087
|
+
- \`curl ${base}/help\` \u2014 full command reference (every endpoint, flags, federation, examples). Read it the first time you reach for a command you don't see above.`;
|
|
1088
|
+
}
|
|
1089
|
+
function helpText(port) {
|
|
1090
|
+
const base = `http://localhost:${port}/__aipeek`;
|
|
1091
|
+
return `
|
|
1092
|
+
# aipeek \u2014 Runtime Browser Inspector
|
|
1093
|
+
|
|
785
1094
|
IMPORTANT: Before debugging any UI issue, visual bug, or runtime error, ALWAYS fetch the live app state first. Do NOT guess \u2014 look at the actual browser state.
|
|
786
1095
|
|
|
787
1096
|
## Read state \u2014 cheapest first
|
|
788
1097
|
|
|
789
1098
|
\`\`\`bash
|
|
790
|
-
curl ${base}/screen # state-machine projection {view, modal, focus, knobs} \u2014 START HERE
|
|
1099
|
+
curl ${base}/screen # state-machine projection {view, modal, focus, knobs} \u2014 START HERE (returns token: tN)
|
|
1100
|
+
curl '${base}/screen?since=tN' # only what moved since that token (view/modal/focus + new errors), not a full snapshot
|
|
791
1101
|
curl ${base}/ui # React component tree \u2014 deep-dive when /screen isn't enough
|
|
792
1102
|
curl '${base}/dom?scope=ChatInput' # semantic DOM scoped to a component \u2014 UI as text, src locations
|
|
793
1103
|
curl ${base} # high-density summary (ok sections \u2192 1 line, issues \u2192 expanded)
|
|
@@ -797,10 +1107,24 @@ curl ${base}/console # console logs (errors, warnings, info)
|
|
|
797
1107
|
curl ${base}/network # fetch/XHR requests with status and timing
|
|
798
1108
|
curl ${base}/errors # uncaught errors and unhandled rejections
|
|
799
1109
|
curl ${base}/state # registered store snapshots
|
|
1110
|
+
curl ${base}/tabs # list live tabs (id, visible/background, title) for ?tab= addressing
|
|
1111
|
+
curl ${base}/timeline # interleaved action stream across all tabs (who clicked what, in order)
|
|
1112
|
+
curl '${base}/query?sel=[role=menuitem]' # this selector right now: count + each element's text/visible/attrs
|
|
1113
|
+
curl ${base}/profile # performance profiler: which component/function is burning frames (+ source lines with AIPEEK_LINES=1)
|
|
1114
|
+
curl ${base}/profile/reset # clear the profiler window, then reproduce the interaction
|
|
1115
|
+
curl ${base}/profile/diff # closed loop: 1st call marks baseline, fix+reproduce, 2nd call \u2192 IMPROVED/REGRESSED verdict
|
|
800
1116
|
\`\`\`
|
|
801
1117
|
|
|
1118
|
+
\`/query\` is the read-side twin of click/fill's \`sel=\` \u2014 assert on a specific element
|
|
1119
|
+
(how many match, its text, \`data-state\`, \`aria-*\`/\`data-*\`, value, disabled) without \`/eval\`.
|
|
1120
|
+
Secret fields (password inputs, API-key/token fields) show \`\u2039redacted N chars\u203A\` instead of
|
|
1121
|
+
their value across \`/dom\`, \`/query\` and \`/screen\` \u2014 length stays visible, the secret doesn't.
|
|
1122
|
+
|
|
802
1123
|
\`/screen\` projects the whole UI to a few state variables \u2014 start there, not \`/ui\`. Append
|
|
803
|
-
\`?full\` for untruncated output.
|
|
1124
|
+
\`?full\` for untruncated output. Each read prints a \`token: tN\` line; pass it back as
|
|
1125
|
+
\`/screen?since=tN\` to get only the transition since that read (view/modal/focus + new
|
|
1126
|
+
errors/failed requests), \`(no state change)\` if nothing moved \u2014 the cheap "what changed
|
|
1127
|
+
after I acted" read, without re-paying for the unchanged 99%.
|
|
804
1128
|
|
|
805
1129
|
To inspect or edit a component, work top-down \u2014 the full DOM is huge, a scoped view is
|
|
806
1130
|
accurate: \`/screen\` or \`/ui\` to find the component, then \`/dom?scope=<Name>\` (matches the
|
|
@@ -817,11 +1141,59 @@ curl '${base}/wait?text=Done&timeout=8000' # poll until text/sel appears (add go
|
|
|
817
1141
|
curl '${base}/screenshot?out=shot.png' # DOM\u2192PNG into .aipeek/ (html-to-image; lossy)
|
|
818
1142
|
\`\`\`
|
|
819
1143
|
|
|
820
|
-
\`click\`/\`fill\`/\`press\` settle the DOM and append
|
|
821
|
-
|
|
822
|
-
|
|
1144
|
+
\`click\`/\`fill\`/\`press\` settle the DOM and append \`--- changed ---\`: only the state-machine
|
|
1145
|
+
transition this action caused (\`view: a \u2192 b\`, \`modal: opened X\`, \`focus: \u2026\`) plus any new
|
|
1146
|
+
errors/failed requests \u2014 not a fresh snapshot. \`(no state change)\` means nothing moved. Read
|
|
1147
|
+
the delta, then drill into /ui or /dom for detail if you need it. On a miss, the response lists
|
|
1148
|
+
the reachable clickable elements so you can re-target. URL-encode any \`sel=\` with non-ASCII or quotes:
|
|
823
1149
|
\`curl -G ${base}/click --data-urlencode 'sel=button[title="\u77E5\u8BC6\u5E93"]'\`.
|
|
824
1150
|
|
|
1151
|
+
Each \`click\`/\`fill\`/\`press\` response also carries a \`--- recent actions ---\` timeline:
|
|
1152
|
+
the semantic page actions (yours and the user's) in order, \`T\`=trusted human / \`S\`=synthetic
|
|
1153
|
+
aipeek, each with its resulting UI change (\`\u2192 \u5F39\u7A97\u6253\u5F00\u300C\u2026\u300D\`/\`\u2192 \u5F39\u7A97\u5173\u95ED\`). Your own action is
|
|
1154
|
+
bracketed by \`\u4F60\u5F53\u524D\u7684\u884C\u4E3A\` dividers. So if the user closed a dialog you just opened, you see
|
|
1155
|
+
their \`T key:Escape \u2192 \u5F39\u7A97\u5173\u95ED\` right after your \`S\` action \u2014 no need to query for it.
|
|
1156
|
+
|
|
1157
|
+
**Beyond click/fill/press** \u2014 four more interactions for what those can't reach:
|
|
1158
|
+
|
|
1159
|
+
\`\`\`bash
|
|
1160
|
+
curl '${base}/scrollIntoView?text=Row 99' # scroll a target into view (off-screen list rows)
|
|
1161
|
+
curl '${base}/drag?sel=.item&to=.slot' # synthetic pointer drag, source \u2192 destination
|
|
1162
|
+
curl '${base}/drop?sel=.dropzone&files=a.png,b.pdf' # fire a file-drop (DataTransfer) on a target
|
|
1163
|
+
curl '${base}/clipboard?mode=write&value=hi' # seed the clipboard (mode=read reports it back)
|
|
1164
|
+
\`\`\`
|
|
1165
|
+
|
|
1166
|
+
\`drag\` fires a real pointer sequence (down \u2192 stepped moves past dnd-kit's activation
|
|
1167
|
+
distance \u2192 up); if a dnd-kit reorder doesn't take, retry the same gesture via \`realclick\`
|
|
1168
|
+
(trusted events). \`drop\` delivers the drop event with the named files (synthetic Files have
|
|
1169
|
+
no byte content \u2014 fine for triggering handlers, not for real uploads). \`clipboard\` needs the
|
|
1170
|
+
tab focused (browser security) and says so plainly when it isn't, rather than hanging.
|
|
1171
|
+
|
|
1172
|
+
A control tagged \`{needs-trusted?}\` in \`/screen\` or \`/dom\` opens a popup (\`aria-haspopup\`)
|
|
1173
|
+
that a synthetic click may not trigger \u2014 reach for \`realclick\` on it from the start instead
|
|
1174
|
+
of discovering it via a dead click. (Right-click-only menus carry no DOM marker, so they
|
|
1175
|
+
still surface only on a miss \u2014 use \`realclick\` with \`button=right\` there.)
|
|
1176
|
+
|
|
1177
|
+
**Multiple tabs.** Every read/drive command takes \`?tab=<id>\` to address one specific tab \u2014
|
|
1178
|
+
including a **background** one (you can drive the Chat tab while the user is looking at a
|
|
1179
|
+
different tab). Run \`${base}/tabs\` to see the live ids. With one tab open, omit \`?tab=\` and
|
|
1180
|
+
it just works. With several tabs open and no \`?tab=\`, the command returns \`409\` + the tab
|
|
1181
|
+
list (rather than randomly hitting one) \u2014 pick an id from it and retry with \`?tab=\`.
|
|
1182
|
+
|
|
1183
|
+
**Multiple servers (federation).** When several dev servers run at once \u2014 a micro-frontend,
|
|
1184
|
+
separate front/back servers, or a teammate's machine \u2014 every command also takes
|
|
1185
|
+
\`?host=<host:port>\` to reach a *sibling* aipeek. The plugin you curl reverse-proxies the
|
|
1186
|
+
request to that peer (server-side, no browser): \`${base}/screen?host=localhost:5174\` reads
|
|
1187
|
+
the app on :5174; combine with \`?tab=\` to point at one tab over there
|
|
1188
|
+
(\`?host=192.168.1.9:5173&tab=t3\`). Omit \`?host=\` and it's the local server as always. There's
|
|
1189
|
+
no registry \u2014 you name the peer, so list its tabs with \`/tabs?host=<host:port>\` first.
|
|
1190
|
+
|
|
1191
|
+
**Cross-tab timeline.** \`${base}/timeline\` interleaves the semantic actions of *every* tab
|
|
1192
|
+
in time order \u2014 each line \`<tab> [T|S] <action> \u2192 <ui change>\` (\`T\`=trusted human,
|
|
1193
|
+
\`S\`=synthetic aipeek). The per-action \`--- recent actions ---\` tail only shows the acting
|
|
1194
|
+
tab; \`/timeline\` is the group view, so an A/B comparison across two tabs (drive A, watch B
|
|
1195
|
+
react) is one read. \`?tab=<id>\` filters to one tab's history.
|
|
1196
|
+
|
|
825
1197
|
**Chain \u2014 a whole interaction in one round-trip.** POST a JSON array; runs in sequence,
|
|
826
1198
|
each step settles before the next, stops on first failure:
|
|
827
1199
|
|
|
@@ -830,39 +1202,46 @@ curl -X POST ${base}/chain -d '[
|
|
|
830
1202
|
{"type":"click","sel":"button[title=\\"\u77E5\u8BC6\u5E93\\"]"},
|
|
831
1203
|
{"type":"wait","text":"Done"},
|
|
832
1204
|
{"type":"fill","sel":"textarea","value":"hi"},
|
|
1205
|
+
{"type":"assert","screen":"\u6D41\u5F0F\u4E2D","equals":"false"},
|
|
833
1206
|
{"type":"press","key":"Enter"}
|
|
834
1207
|
]'
|
|
835
1208
|
\`\`\`
|
|
836
1209
|
|
|
1210
|
+
\`assert\` is the chain's mid-step judge: \`{type,screen,equals}\` checks a domain variable
|
|
1211
|
+
(from the app's \`window.__AIPEEK_SCREEN__\`), or \`{type,sel,equals}\` an element's text. On
|
|
1212
|
+
mismatch the chain stops and reports \`asserted X=="Y", actual "Z"\` \u2014 a test, not a guess.
|
|
1213
|
+
Domain variables also show up in \`/screen\`'s \`domain:\` block and in every \`--- changed ---\`
|
|
1214
|
+
diff (e.g. \`\u6D41\u5F0F\u4E2D: false \u2192 true\`) \u2014 the app's own state machine, which a DOM-only inspector
|
|
1215
|
+
can't see. The app opts in by setting \`window.__AIPEEK_SCREEN__ = () => ({...})\`.
|
|
1216
|
+
|
|
837
1217
|
**Escape hatch.** \`curl '${base}/eval?code=...'\` (or POST the code as the body) runs arbitrary
|
|
838
|
-
JS in the page and returns the result \u2014 for
|
|
1218
|
+
JS in the page and returns the result \u2014 for what the typed endpoints can't do (install listeners,
|
|
1219
|
+
read closures, probe event flow). For count/text/state/attr assertions reach for \`/query\` first.
|
|
839
1220
|
|
|
840
1221
|
aipeek auto-detects errors after HMR and prints them to the terminal \u2014 watch for \`[aipeek]\` messages.
|
|
841
1222
|
`;
|
|
842
1223
|
}
|
|
843
1224
|
var START_TAG = "<!-- AIPEEK:START -->";
|
|
844
1225
|
var END_TAG = "<!-- AIPEEK:END -->";
|
|
845
|
-
function
|
|
846
|
-
const path = resolve(root, "CLAUDE.md");
|
|
1226
|
+
function renderClaudeMd(existing, port) {
|
|
847
1227
|
const block = `${START_TAG}
|
|
848
1228
|
${aipeekSnippet(port).trim()}
|
|
849
1229
|
${END_TAG}
|
|
850
1230
|
`;
|
|
1231
|
+
if (existing === null)
|
|
1232
|
+
return block;
|
|
1233
|
+
const si = existing.indexOf(START_TAG);
|
|
1234
|
+
const ei = existing.indexOf(END_TAG);
|
|
1235
|
+
if (si !== -1 && ei !== -1)
|
|
1236
|
+
return existing.slice(0, si) + block.trimEnd() + existing.slice(ei + END_TAG.length);
|
|
1237
|
+
const sep = existing.endsWith("\n") ? "" : "\n";
|
|
1238
|
+
return `${existing}${sep}
|
|
1239
|
+
${block}`;
|
|
1240
|
+
}
|
|
1241
|
+
function injectClaudeMd(root, port) {
|
|
1242
|
+
const path = resolve(root, "CLAUDE.md");
|
|
851
1243
|
try {
|
|
852
|
-
|
|
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));
|
|
861
|
-
return;
|
|
862
|
-
}
|
|
863
|
-
const sep = content.endsWith("\n") ? "" : "\n";
|
|
864
|
-
writeFileSync(path, `${content}${sep}
|
|
865
|
-
${block}`);
|
|
1244
|
+
writeFileSync(path, renderClaudeMd(existsSync(path) ? readFileSync(path, "utf-8") : null, port));
|
|
866
1245
|
} catch {
|
|
867
1246
|
}
|
|
868
1247
|
}
|
|
@@ -870,21 +1249,107 @@ function aipeekPlugin() {
|
|
|
870
1249
|
let pendingResolve = null;
|
|
871
1250
|
let server;
|
|
872
1251
|
let lastRaw = null;
|
|
1252
|
+
let perfBaseline = null;
|
|
873
1253
|
let pushTimer;
|
|
874
1254
|
const pendingActions = /* @__PURE__ */ new Map();
|
|
875
1255
|
let actionId = 0;
|
|
1256
|
+
const tabs = /* @__PURE__ */ new Map();
|
|
1257
|
+
function seen(data) {
|
|
1258
|
+
if (!data.tab)
|
|
1259
|
+
return;
|
|
1260
|
+
const prev = tabs.get(data.tab);
|
|
1261
|
+
tabs.set(data.tab, {
|
|
1262
|
+
id: data.tab,
|
|
1263
|
+
url: data.url ?? prev?.url ?? "",
|
|
1264
|
+
title: data.title ?? prev?.title ?? "",
|
|
1265
|
+
visible: data.visible ?? prev?.visible ?? false,
|
|
1266
|
+
lastSeen: Date.now()
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
const liveTabs = () => [...tabs.values()].filter((t) => isLive(t, Date.now()));
|
|
1270
|
+
const diagnose2 = (tab) => diagnose(tab, [...tabs.values()], Date.now(), server.ws.clients.size, server.config.server.port || 5173);
|
|
1271
|
+
const actionLog = [];
|
|
1272
|
+
const BOOT_ID = Date.now().toString(36);
|
|
1273
|
+
const mergeActions = (tab, actions) => {
|
|
1274
|
+
if (!tab || !actions?.length)
|
|
1275
|
+
return;
|
|
1276
|
+
for (const entry of actions) {
|
|
1277
|
+
if (!actionLog.some((e) => e.tab === tab && e.ts === entry.ts))
|
|
1278
|
+
appendAction(actionLog, tab, entry, 200);
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
const cdpQueue = [];
|
|
1282
|
+
let cdpWaiter = null;
|
|
1283
|
+
const cdpResults = /* @__PURE__ */ new Map();
|
|
1284
|
+
let cdpId = 0;
|
|
1285
|
+
function runCdpClick(x, y, button) {
|
|
1286
|
+
const id = ++cdpId;
|
|
1287
|
+
const cmd = { id, x, y, button };
|
|
1288
|
+
return new Promise((resolve2, reject) => {
|
|
1289
|
+
cdpResults.set(id, resolve2);
|
|
1290
|
+
if (cdpWaiter) {
|
|
1291
|
+
cdpWaiter(cmd);
|
|
1292
|
+
cdpWaiter = null;
|
|
1293
|
+
} else {
|
|
1294
|
+
cdpQueue.push(cmd);
|
|
1295
|
+
}
|
|
1296
|
+
setTimeout(() => {
|
|
1297
|
+
if (cdpResults.delete(id))
|
|
1298
|
+
reject(new Error("cdp timeout: no extension result within 10s (is the aipeek extension loaded and the debugger attached?)"));
|
|
1299
|
+
}, 1e4);
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
876
1302
|
let pendingDom = null;
|
|
877
1303
|
let pendingScreen = null;
|
|
878
1304
|
const pendingEvals = /* @__PURE__ */ new Map();
|
|
879
1305
|
let evalId = 0;
|
|
1306
|
+
const screenStash = /* @__PURE__ */ new Map();
|
|
1307
|
+
let screenToken = 0;
|
|
1308
|
+
function stashScreen(r) {
|
|
1309
|
+
const token = `t${++screenToken}`;
|
|
1310
|
+
screenStash.set(token, { snap: r.snap, console: r.console, network: r.network, errors: r.errors });
|
|
1311
|
+
if (screenStash.size > 32)
|
|
1312
|
+
screenStash.delete(screenStash.keys().next().value);
|
|
1313
|
+
return token;
|
|
1314
|
+
}
|
|
880
1315
|
const VISIBLE_MS = 400;
|
|
881
|
-
function twoPhase(event, payload, arm, fullMs = 3e3) {
|
|
1316
|
+
function twoPhase(event, payload, arm, fullMs = 3e3, tab) {
|
|
882
1317
|
return new Promise((resolve2, reject) => {
|
|
883
1318
|
let settled = false;
|
|
884
1319
|
const clear = arm((v) => {
|
|
885
1320
|
settled = true;
|
|
886
1321
|
resolve2(v);
|
|
887
1322
|
});
|
|
1323
|
+
if (tab) {
|
|
1324
|
+
const RETRY_MS = 500;
|
|
1325
|
+
const ABSENT_CEILING_MS = 1e4;
|
|
1326
|
+
const startedAt = Date.now();
|
|
1327
|
+
const deliver = () => server.hot.send(event, { ...payload, tab });
|
|
1328
|
+
deliver();
|
|
1329
|
+
const iv = setInterval(() => {
|
|
1330
|
+
if (settled) {
|
|
1331
|
+
clearInterval(iv);
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
const t = tabs.get(tab);
|
|
1335
|
+
const live = !!t && isLive(t, Date.now());
|
|
1336
|
+
const elapsed = Date.now() - startedAt;
|
|
1337
|
+
if (live) {
|
|
1338
|
+
if (elapsed > fullMs) {
|
|
1339
|
+
clearInterval(iv);
|
|
1340
|
+
clear();
|
|
1341
|
+
reject(new Error(diagnose2(tab)));
|
|
1342
|
+
}
|
|
1343
|
+
} else if (elapsed > ABSENT_CEILING_MS) {
|
|
1344
|
+
clearInterval(iv);
|
|
1345
|
+
clear();
|
|
1346
|
+
reject(new Error(diagnose2(tab)));
|
|
1347
|
+
} else {
|
|
1348
|
+
deliver();
|
|
1349
|
+
}
|
|
1350
|
+
}, RETRY_MS);
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
888
1353
|
server.hot.send(event, { ...payload, requireVisible: true });
|
|
889
1354
|
setTimeout(() => {
|
|
890
1355
|
if (settled)
|
|
@@ -894,36 +1359,36 @@ function aipeekPlugin() {
|
|
|
894
1359
|
if (settled)
|
|
895
1360
|
return;
|
|
896
1361
|
clear();
|
|
897
|
-
reject(new Error(
|
|
1362
|
+
reject(new Error(diagnose2(tab)));
|
|
898
1363
|
}, fullMs);
|
|
899
1364
|
}, VISIBLE_MS);
|
|
900
1365
|
});
|
|
901
1366
|
}
|
|
902
|
-
function collectFromClient() {
|
|
1367
|
+
function collectFromClient(tab) {
|
|
903
1368
|
return twoPhase("aipeek:collect", {}, (resolve2) => {
|
|
904
1369
|
pendingResolve = resolve2;
|
|
905
1370
|
return () => {
|
|
906
1371
|
pendingResolve = null;
|
|
907
1372
|
};
|
|
908
|
-
});
|
|
1373
|
+
}, 3e3, tab);
|
|
909
1374
|
}
|
|
910
|
-
function collectDomFromClient(scope, sel) {
|
|
1375
|
+
function collectDomFromClient(scope, sel, tab) {
|
|
911
1376
|
return twoPhase("aipeek:collect-dom", { scope, sel }, (resolve2) => {
|
|
912
1377
|
pendingDom = resolve2;
|
|
913
1378
|
return () => {
|
|
914
1379
|
pendingDom = null;
|
|
915
1380
|
};
|
|
916
|
-
});
|
|
1381
|
+
}, 3e3, tab);
|
|
917
1382
|
}
|
|
918
|
-
function collectScreenFromClient() {
|
|
1383
|
+
function collectScreenFromClient(tab) {
|
|
919
1384
|
return twoPhase("aipeek:collect-screen", {}, (resolve2) => {
|
|
920
1385
|
pendingScreen = resolve2;
|
|
921
1386
|
return () => {
|
|
922
1387
|
pendingScreen = null;
|
|
923
1388
|
};
|
|
924
|
-
});
|
|
1389
|
+
}, 3e3, tab);
|
|
925
1390
|
}
|
|
926
|
-
function sendAction(type, args) {
|
|
1391
|
+
function sendAction(type, args, tab) {
|
|
927
1392
|
const id = ++actionId;
|
|
928
1393
|
const fullMs = Math.max(args.timeout ?? 0, 3e3) + 2e3;
|
|
929
1394
|
return twoPhase("aipeek:action", { id, type, args }, (resolve2) => {
|
|
@@ -931,16 +1396,28 @@ function aipeekPlugin() {
|
|
|
931
1396
|
return () => {
|
|
932
1397
|
pendingActions.delete(id);
|
|
933
1398
|
};
|
|
934
|
-
}, fullMs);
|
|
1399
|
+
}, fullMs, tab);
|
|
1400
|
+
}
|
|
1401
|
+
async function runAction(type, args, tab) {
|
|
1402
|
+
const result = await sendAction(type, args, tab);
|
|
1403
|
+
lastRaw = null;
|
|
1404
|
+
if (type === "realclick" && result.ok && !result.fired) {
|
|
1405
|
+
const cdp = await runCdpClick(result.x, result.y, args.button ?? "left");
|
|
1406
|
+
if (!cdp.ok)
|
|
1407
|
+
return { ok: false, error: `cdp click failed: ${cdp.error ?? "unknown"}` };
|
|
1408
|
+
result.detail = `${result.detail} \u2192 clicked via extension`;
|
|
1409
|
+
result.screen = (await collectScreenFromClient(tab)).screen;
|
|
1410
|
+
}
|
|
1411
|
+
return result;
|
|
935
1412
|
}
|
|
936
|
-
function evalInClient(code) {
|
|
1413
|
+
function evalInClient(code, tab) {
|
|
937
1414
|
const id = ++evalId;
|
|
938
1415
|
return twoPhase("aipeek:eval", { id, code }, (resolve2) => {
|
|
939
1416
|
pendingEvals.set(id, resolve2);
|
|
940
1417
|
return () => {
|
|
941
1418
|
pendingEvals.delete(id);
|
|
942
1419
|
};
|
|
943
|
-
}, 8e3);
|
|
1420
|
+
}, 8e3, tab);
|
|
944
1421
|
}
|
|
945
1422
|
return {
|
|
946
1423
|
name: "aipeek",
|
|
@@ -968,12 +1445,16 @@ function aipeekPlugin() {
|
|
|
968
1445
|
server = _server;
|
|
969
1446
|
injectClaudeMd(server.config.root, server.config.server.port || 5173);
|
|
970
1447
|
server.hot.on("aipeek:state", (data) => {
|
|
1448
|
+
seen(data);
|
|
1449
|
+
mergeActions(data.tab, data.actions);
|
|
971
1450
|
if (pendingResolve) {
|
|
972
1451
|
pendingResolve(data);
|
|
973
1452
|
pendingResolve = null;
|
|
974
1453
|
}
|
|
975
1454
|
});
|
|
1455
|
+
server.hot.on("aipeek:hello", (data) => seen(data));
|
|
976
1456
|
server.hot.on("aipeek:result", (data) => {
|
|
1457
|
+
seen(data);
|
|
977
1458
|
const resolve2 = pendingActions.get(data.id);
|
|
978
1459
|
if (resolve2) {
|
|
979
1460
|
pendingActions.delete(data.id);
|
|
@@ -981,6 +1462,7 @@ function aipeekPlugin() {
|
|
|
981
1462
|
}
|
|
982
1463
|
});
|
|
983
1464
|
server.hot.on("aipeek:eval-result", (data) => {
|
|
1465
|
+
seen(data);
|
|
984
1466
|
const resolve2 = pendingEvals.get(data.id);
|
|
985
1467
|
if (resolve2) {
|
|
986
1468
|
pendingEvals.delete(data.id);
|
|
@@ -988,14 +1470,16 @@ function aipeekPlugin() {
|
|
|
988
1470
|
}
|
|
989
1471
|
});
|
|
990
1472
|
server.hot.on("aipeek:dom", (data) => {
|
|
1473
|
+
seen(data);
|
|
991
1474
|
if (pendingDom) {
|
|
992
1475
|
pendingDom(data.dom);
|
|
993
1476
|
pendingDom = null;
|
|
994
1477
|
}
|
|
995
1478
|
});
|
|
996
1479
|
server.hot.on("aipeek:screen", (data) => {
|
|
1480
|
+
seen(data);
|
|
997
1481
|
if (pendingScreen) {
|
|
998
|
-
pendingScreen(data
|
|
1482
|
+
pendingScreen(data);
|
|
999
1483
|
pendingScreen = null;
|
|
1000
1484
|
}
|
|
1001
1485
|
});
|
|
@@ -1019,7 +1503,56 @@ function aipeekPlugin() {
|
|
|
1019
1503
|
const url = new URL(req.url || "/", "http://localhost");
|
|
1020
1504
|
const parts = url.pathname.split("/").filter(Boolean);
|
|
1021
1505
|
const full = url.searchParams.has("full");
|
|
1506
|
+
const tab = url.searchParams.get("tab") || void 0;
|
|
1507
|
+
const host = url.searchParams.get("host") || void 0;
|
|
1508
|
+
const selfPort = server.config.server.port || 5173;
|
|
1509
|
+
const selfHosts = /* @__PURE__ */ new Set([`localhost:${selfPort}`, `127.0.0.1:${selfPort}`, `:${selfPort}`, `${selfPort}`]);
|
|
1510
|
+
if (host && !selfHosts.has(host)) {
|
|
1511
|
+
const fwd = new URL(url);
|
|
1512
|
+
fwd.searchParams.delete("host");
|
|
1513
|
+
const target = `http://${host}/__aipeek/${parts.join("/")}${fwd.search}`;
|
|
1514
|
+
try {
|
|
1515
|
+
const body = req.method === "POST" ? await readBody(req) : void 0;
|
|
1516
|
+
const r = await fetch(target, { method: req.method, body });
|
|
1517
|
+
send(res, r.status, await r.text());
|
|
1518
|
+
} catch (e) {
|
|
1519
|
+
const code = e.cause?.code;
|
|
1520
|
+
const why = code === "ECONNREFUSED" ? `nothing is listening on ${host} \u2014 its dev server isn't running (start it), or the port is wrong.` : code === "ENOTFOUND" ? `host '${host}' doesn't resolve \u2014 check the hostname.` : code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT" ? `connection to ${host} timed out \u2014 it's unreachable (firewall, or wrong host).` : `${e.message} (code ${code ?? "unknown"}).`;
|
|
1521
|
+
send(res, 502, `cannot reach aipeek peer at ${host}: ${why}`);
|
|
1522
|
+
}
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
if (parts[0] === "ping") {
|
|
1526
|
+
send(res, 200, BOOT_ID);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
if (parts[0] === "help") {
|
|
1530
|
+
send(res, 200, helpText(selfPort).trim());
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
const ambiguous = () => !tab && !["tabs", "timeline", "cdp"].includes(parts[0]) && liveTabs().length > 1;
|
|
1534
|
+
const refuse = () => send(res, 409, `multiple live tabs \u2014 add ?tab=<id>:
|
|
1535
|
+
|
|
1536
|
+
${formatTabs(liveTabs(), Date.now())}`);
|
|
1022
1537
|
try {
|
|
1538
|
+
if (parts[0] === "tabs") {
|
|
1539
|
+
const roster = formatTabs(liveTabs(), Date.now());
|
|
1540
|
+
send(res, 200, roster === "(no live tabs)" ? `(${diagnose2()})` : roster);
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
if (parts[0] === "timeline") {
|
|
1544
|
+
const targets = tab ? [tab] : liveTabs().map((t) => t.id);
|
|
1545
|
+
for (const id of targets)
|
|
1546
|
+
await collectFromClient(id).catch(() => {
|
|
1547
|
+
});
|
|
1548
|
+
const entries = tab ? actionLog.filter((e) => e.tab === tab) : actionLog;
|
|
1549
|
+
send(res, 200, formatActions(entries));
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
if (ambiguous()) {
|
|
1553
|
+
refuse();
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1023
1556
|
if (parts[0] === "eval") {
|
|
1024
1557
|
let code = url.searchParams.get("code") || "";
|
|
1025
1558
|
if (!code && req.method === "POST")
|
|
@@ -1028,21 +1561,78 @@ function aipeekPlugin() {
|
|
|
1028
1561
|
send(res, 400, "eval needs ?code= or a POST body");
|
|
1029
1562
|
return;
|
|
1030
1563
|
}
|
|
1031
|
-
const r = await evalInClient(code);
|
|
1564
|
+
const r = await evalInClient(code, tab);
|
|
1032
1565
|
send(res, r.ok ? 200 : 422, r.ok ? r.value ?? "undefined" : `error: ${r.error}`);
|
|
1033
1566
|
return;
|
|
1034
1567
|
}
|
|
1035
1568
|
if (parts[0] === "dom") {
|
|
1036
1569
|
const dom = await collectDomFromClient(
|
|
1037
1570
|
url.searchParams.get("scope") || void 0,
|
|
1038
|
-
url.searchParams.get("sel") || void 0
|
|
1571
|
+
url.searchParams.get("sel") || void 0,
|
|
1572
|
+
tab
|
|
1039
1573
|
);
|
|
1040
1574
|
send(res, 200, dom || "(empty)");
|
|
1041
1575
|
return;
|
|
1042
1576
|
}
|
|
1043
1577
|
if (parts[0] === "screen") {
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1578
|
+
const reply = await collectScreenFromClient(tab);
|
|
1579
|
+
const since = url.searchParams.get("since");
|
|
1580
|
+
const token = stashScreen(reply);
|
|
1581
|
+
if (since) {
|
|
1582
|
+
const prev = screenStash.get(since);
|
|
1583
|
+
if (!prev) {
|
|
1584
|
+
send(res, 422, `unknown since token "${since}" (expired or never issued) \u2014 read /screen first for a fresh token`);
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
const d = diffState(
|
|
1588
|
+
{ ui: "", console: prev.console, network: prev.network, errors: prev.errors, state: {}, url: "", timestamp: 0 },
|
|
1589
|
+
{ ui: "", console: reply.console, network: reply.network, errors: reply.errors, state: {}, url: "", timestamp: 0 }
|
|
1590
|
+
);
|
|
1591
|
+
const changed = diffScreen(prev.snap, reply.snap, d.newErrors, d.newExceptions, d.newFailedRequests);
|
|
1592
|
+
send(res, 200, `token: ${token}
|
|
1593
|
+
${changed.length ? changed.join("\n") : "(no state change)"}`);
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
send(res, 200, reply.screen ? `token: ${token}
|
|
1597
|
+
${reply.screen}` : "(empty)");
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
if (parts[0] === "cdp" && parts[1] === "poll") {
|
|
1601
|
+
const queued = cdpQueue.shift();
|
|
1602
|
+
if (queued) {
|
|
1603
|
+
send(res, 200, JSON.stringify(queued));
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
const cmd = await new Promise((resolve2) => {
|
|
1607
|
+
cdpWaiter = resolve2;
|
|
1608
|
+
setTimeout(() => {
|
|
1609
|
+
if (cdpWaiter === resolve2) {
|
|
1610
|
+
cdpWaiter = null;
|
|
1611
|
+
resolve2(null);
|
|
1612
|
+
}
|
|
1613
|
+
}, 25e3);
|
|
1614
|
+
});
|
|
1615
|
+
if (cmd)
|
|
1616
|
+
send(res, 200, JSON.stringify(cmd));
|
|
1617
|
+
else
|
|
1618
|
+
send(res, 204, "");
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
if (parts[0] === "cdp" && parts[1] === "result") {
|
|
1622
|
+
const body = await readBody(req);
|
|
1623
|
+
let data;
|
|
1624
|
+
try {
|
|
1625
|
+
data = JSON.parse(body);
|
|
1626
|
+
} catch {
|
|
1627
|
+
send(res, 400, "cdp/result needs a JSON body {id, ok, error?}");
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
const resolveCdp = cdpResults.get(data.id);
|
|
1631
|
+
if (resolveCdp) {
|
|
1632
|
+
cdpResults.delete(data.id);
|
|
1633
|
+
resolveCdp({ ok: data.ok, error: data.error });
|
|
1634
|
+
}
|
|
1635
|
+
send(res, 200, "ok");
|
|
1046
1636
|
return;
|
|
1047
1637
|
}
|
|
1048
1638
|
if (parts[0] === "chain") {
|
|
@@ -1058,7 +1648,7 @@ function aipeekPlugin() {
|
|
|
1058
1648
|
}
|
|
1059
1649
|
lastRaw = null;
|
|
1060
1650
|
const lines = [];
|
|
1061
|
-
let
|
|
1651
|
+
let lastActions = "";
|
|
1062
1652
|
let allOk = true;
|
|
1063
1653
|
for (let i = 0; i < steps.length; i++) {
|
|
1064
1654
|
const { type, ...args } = steps[i];
|
|
@@ -1068,24 +1658,25 @@ function aipeekPlugin() {
|
|
|
1068
1658
|
allOk = false;
|
|
1069
1659
|
break;
|
|
1070
1660
|
}
|
|
1071
|
-
const r = await
|
|
1661
|
+
const r = await runAction(type, args, tab);
|
|
1072
1662
|
lines.push(`[${i}] ${r.ok ? "\u2713" : "\u2717"} ${type}: ${r.ok ? r.detail || "ok" : r.error}`);
|
|
1073
1663
|
if (r.screen)
|
|
1074
1664
|
lines.push(r.screen.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
1075
|
-
if (r.
|
|
1076
|
-
|
|
1665
|
+
if (r.actions)
|
|
1666
|
+
lastActions = r.actions;
|
|
1077
1667
|
if (!r.ok) {
|
|
1078
1668
|
allOk = false;
|
|
1079
1669
|
break;
|
|
1080
1670
|
}
|
|
1081
1671
|
}
|
|
1082
|
-
|
|
1672
|
+
const chainActions = lastActions ? `
|
|
1083
1673
|
|
|
1084
|
-
---
|
|
1085
|
-
${
|
|
1674
|
+
--- recent actions ---
|
|
1675
|
+
${lastActions}` : "";
|
|
1676
|
+
send(res, allOk ? 200 : 422, `${lines.join("\n")}${chainActions}`);
|
|
1086
1677
|
return;
|
|
1087
1678
|
}
|
|
1088
|
-
if (["click", "fill", "press", "wait", "screenshot"].includes(parts[0])) {
|
|
1679
|
+
if (["click", "fill", "press", "wait", "screenshot", "realclick", "query", "assert", "drag", "scrollIntoView", "drop", "clipboard"].includes(parts[0])) {
|
|
1089
1680
|
const q = url.searchParams;
|
|
1090
1681
|
const args = {
|
|
1091
1682
|
sel: q.get("sel") || void 0,
|
|
@@ -1093,15 +1684,22 @@ ${lastUi}` : lines.join("\n"));
|
|
|
1093
1684
|
value: q.has("value") ? q.get("value") : void 0,
|
|
1094
1685
|
key: q.get("key") || void 0,
|
|
1095
1686
|
timeout: q.has("timeout") ? Number(q.get("timeout")) : void 0,
|
|
1096
|
-
gone: q.has("gone") ? q.get("gone") !== "false" : void 0
|
|
1687
|
+
gone: q.has("gone") ? q.get("gone") !== "false" : void 0,
|
|
1688
|
+
button: q.get("button") === "right" ? "right" : q.get("button") === "left" ? "left" : void 0,
|
|
1689
|
+
x: q.has("x") ? Number(q.get("x")) : void 0,
|
|
1690
|
+
y: q.has("y") ? Number(q.get("y")) : void 0,
|
|
1691
|
+
screen: q.get("screen") || void 0,
|
|
1692
|
+
equals: q.has("equals") ? q.get("equals") : void 0,
|
|
1693
|
+
to: q.get("to") || void 0,
|
|
1694
|
+
files: q.has("files") ? q.get("files").split(",").map((s) => s.trim()).filter(Boolean) : void 0,
|
|
1695
|
+
mode: q.get("mode") === "write" ? "write" : q.get("mode") === "read" ? "read" : void 0
|
|
1097
1696
|
};
|
|
1098
1697
|
const check2 = resolveAction(parts[0], args);
|
|
1099
1698
|
if (!check2.valid) {
|
|
1100
1699
|
send(res, 400, check2.error ?? "invalid action");
|
|
1101
1700
|
return;
|
|
1102
1701
|
}
|
|
1103
|
-
const result = await
|
|
1104
|
-
lastRaw = null;
|
|
1702
|
+
const result = await runAction(parts[0], args, tab);
|
|
1105
1703
|
if (parts[0] === "screenshot" && result.dataUrl) {
|
|
1106
1704
|
const dir = resolve(server.config.root, ".aipeek");
|
|
1107
1705
|
mkdirSync(dir, { recursive: true });
|
|
@@ -1114,32 +1712,89 @@ ${lastUi}` : lines.join("\n"));
|
|
|
1114
1712
|
const head = result.ok ? result.detail || "ok" : `${result.error}${result.detail ? `
|
|
1115
1713
|
|
|
1116
1714
|
clickable: ${result.detail}` : ""}`;
|
|
1117
|
-
|
|
1715
|
+
const actionsTail = result.actions ? `
|
|
1118
1716
|
|
|
1119
|
-
---
|
|
1120
|
-
${result.
|
|
1717
|
+
--- recent actions ---
|
|
1718
|
+
${result.actions}` : "";
|
|
1719
|
+
const changedTail = result.screen ? `
|
|
1720
|
+
|
|
1721
|
+
--- changed ---
|
|
1722
|
+
${result.screen}` : "";
|
|
1723
|
+
send(res, result.ok ? 200 : 422, `${head}${actionsTail}${changedTail}`);
|
|
1121
1724
|
return;
|
|
1122
1725
|
}
|
|
1123
1726
|
if (parts[0] === "check") {
|
|
1124
|
-
const raw2 = await collectFromClient();
|
|
1727
|
+
const raw2 = await collectFromClient(tab);
|
|
1125
1728
|
lastRaw = raw2;
|
|
1126
1729
|
const result = check(raw2);
|
|
1127
1730
|
const output = emitCheck(result);
|
|
1128
1731
|
send(res, result.pass ? 200 : 417, output);
|
|
1129
1732
|
return;
|
|
1130
1733
|
}
|
|
1734
|
+
if (parts[0] === "profile") {
|
|
1735
|
+
const noPerfMsg = (t) => {
|
|
1736
|
+
const info = t ? tabs.get(t) : liveTabs()[0];
|
|
1737
|
+
return info ? `tab '${info.id}' is connected but BACKGROUNDED \u2014 the browser throttles rAF to ~1fps for hidden tabs, so there are no real frames to profile. You don't need to open anything: the page is already running. Ask the user to click that browser tab to the foreground and keep it there ~2s, then re-run /profile. (Profiling is the only read needing foreground \u2014 /screen and /dom work backgrounded.)` : `(${diagnose2(t)})`;
|
|
1738
|
+
};
|
|
1739
|
+
const resetPerfWindow = () => new Promise((resolve2, reject) => {
|
|
1740
|
+
const timeout = setTimeout(() => reject(new Error("timeout waiting for perf-reset-ack")), 3e3);
|
|
1741
|
+
const handler = (data) => {
|
|
1742
|
+
if (tab && data.tab !== tab) return;
|
|
1743
|
+
clearTimeout(timeout);
|
|
1744
|
+
server.hot.off("aipeek:perf-reset-ack", handler);
|
|
1745
|
+
resolve2();
|
|
1746
|
+
};
|
|
1747
|
+
server.hot.on("aipeek:perf-reset-ack", handler);
|
|
1748
|
+
server.hot.send("aipeek:perf-reset", { tab, requireVisible: false });
|
|
1749
|
+
});
|
|
1750
|
+
if (parts[1] === "reset") {
|
|
1751
|
+
await resetPerfWindow();
|
|
1752
|
+
send(res, 200, "perf window cleared \u2014 reproduce the interaction, then GET /profile");
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
if (parts[1] === "diff") {
|
|
1756
|
+
const raw3 = await collectFromClient(tab);
|
|
1757
|
+
lastRaw = raw3;
|
|
1758
|
+
if (!raw3.performance) {
|
|
1759
|
+
send(res, 200, noPerfMsg(tab));
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
if (!perfBaseline) {
|
|
1763
|
+
perfBaseline = raw3.performance;
|
|
1764
|
+
await resetPerfWindow();
|
|
1765
|
+
send(res, 200, "baseline captured + window cleared \u2014 make your fix, reproduce the interaction, then GET /profile/diff again for the verdict");
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
const report = diffPerformance(perfBaseline, raw3.performance);
|
|
1769
|
+
perfBaseline = null;
|
|
1770
|
+
send(res, 200, report);
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
const raw2 = await collectFromClient(tab);
|
|
1774
|
+
lastRaw = raw2;
|
|
1775
|
+
if (!raw2.performance) {
|
|
1776
|
+
send(res, 200, noPerfMsg(tab));
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
const hiddenNote = raw2.performance.hiddenFrames > 10 ? `
|
|
1780
|
+
|
|
1781
|
+
\u26A0 ${raw2.performance.hiddenFrames} frames skipped while tab was hidden \u2014 bring it to foreground and /profile/reset for accurate data.` : "";
|
|
1782
|
+
send(res, 200, detail(raw2, "profile", void 0, false) + hiddenNote);
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1131
1785
|
if (parts.length >= 1) {
|
|
1132
1786
|
if (!lastRaw)
|
|
1133
|
-
lastRaw = await collectFromClient();
|
|
1787
|
+
lastRaw = await collectFromClient(tab);
|
|
1134
1788
|
const result = detail(lastRaw, parts[0], parts[1], full);
|
|
1135
1789
|
if (result !== null) {
|
|
1136
1790
|
send(res, 200, result);
|
|
1137
1791
|
return;
|
|
1138
1792
|
}
|
|
1139
|
-
|
|
1793
|
+
const SECTIONS2 = ["ui", "console", "network", "errors", "state", "profile"];
|
|
1794
|
+
send(res, 404, SECTIONS2.includes(parts[0]) ? `'${parts[0]}' is empty right now \u2014 nothing captured this window (not an error).` : `unknown section '${parts[0]}'. Valid: ${SECTIONS2.join(", ")}. (Or /screen, /dom, /tabs, /timeline, /check.)`);
|
|
1140
1795
|
return;
|
|
1141
1796
|
}
|
|
1142
|
-
const raw = await collectFromClient();
|
|
1797
|
+
const raw = await collectFromClient(tab);
|
|
1143
1798
|
lastRaw = raw;
|
|
1144
1799
|
if (full) {
|
|
1145
1800
|
const compacted = compact(raw);
|
|
@@ -1163,6 +1818,7 @@ export {
|
|
|
1163
1818
|
emitDiff,
|
|
1164
1819
|
START_TAG,
|
|
1165
1820
|
END_TAG,
|
|
1821
|
+
renderClaudeMd,
|
|
1166
1822
|
injectClaudeMd,
|
|
1167
1823
|
aipeekPlugin
|
|
1168
1824
|
};
|