aihand 0.0.1 → 0.1.0
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 +136 -2
- package/dist/chunk-2NTK7H4W.js +10 -0
- package/dist/chunk-3X4FTHLC.cjs +369 -0
- package/dist/chunk-BXVNR4E2.js +399 -0
- package/dist/chunk-C7DGE6MY.cjs +1456 -0
- package/dist/chunk-DUUCVLC3.cjs +254 -0
- package/dist/chunk-FAHI53KO.cjs +125 -0
- package/dist/chunk-G7KVJ7NF.js +369 -0
- package/dist/chunk-GNEUSRGP.js +52 -0
- package/dist/chunk-IGNEAOLT.cjs +130 -0
- package/dist/chunk-IS5XFUDB.js +125 -0
- package/dist/chunk-JLYC76XL.js +2448 -0
- package/dist/chunk-KQOABC2O.cjs +52 -0
- package/dist/chunk-OVMK33AC.cjs +104 -0
- package/dist/chunk-OWYK2IGV.js +250 -0
- package/dist/chunk-PQSQN4CN.js +126 -0
- package/dist/chunk-QF6AG3M5.cjs +410 -0
- package/dist/chunk-QSAMLXML.js +1456 -0
- package/dist/chunk-VEKYRKPF.cjs +399 -0
- package/dist/chunk-Y6H7W7PI.cjs +2451 -0
- package/dist/chunk-YKSYW77R.js +410 -0
- package/dist/chunk-Z2Y65YOY.cjs +7 -0
- package/dist/chunk-ZJQRNIK7.js +104 -0
- package/dist/cli-FDS2C2CZ.cjs +651 -0
- package/dist/cli-HHRGYPSM.js +649 -0
- package/dist/cli-JQEIE7RQ.js +120 -0
- package/dist/cli-K3OS2QQH.cjs +122 -0
- package/dist/cli-OSYG6LJD.cjs +89 -0
- package/dist/cli-TXRW5PG6.js +89 -0
- package/dist/cli.cjs +81 -0
- package/dist/cli.js +81 -0
- package/dist/config-5KEQLN6L.cjs +13 -0
- package/dist/config-PJPYKDLQ.js +13 -0
- package/dist/graph-IH56SCPK.js +8 -0
- package/dist/graph-ZUXXCJ5A.cjs +8 -0
- package/dist/index.cjs +481 -0
- package/dist/index.d.cts +461 -0
- package/dist/index.d.ts +461 -0
- package/dist/index.js +479 -0
- package/dist/locate-5XFSXJ5J.cjs +15 -0
- package/dist/locate-NKSUGL3A.js +15 -0
- package/dist/refactor-5FWSZIBN.cjs +19 -0
- package/dist/refactor-BOB3SZSA.js +19 -0
- package/dist/scan-4R7GQG2W.cjs +9 -0
- package/dist/scan-VF54GAAX.js +9 -0
- package/dist/ui/probe/server.cjs +505 -0
- package/dist/ui/probe/server.js +507 -0
- package/dist/vite.cjs +12 -0
- package/dist/vite.d.cts +12 -0
- package/dist/vite.d.ts +12 -0
- package/dist/vite.js +12 -0
- package/package.json +82 -9
- package/src/cli.ts +107 -0
- package/src/index.ts +54 -0
- package/src/read/cli.ts +650 -0
- package/src/read/compact.ts +286 -0
- package/src/read/config.ts +62 -0
- package/src/read/graph.ts +182 -0
- package/src/read/index.ts +12 -0
- package/src/read/inject.ts +121 -0
- package/src/read/locate.ts +104 -0
- package/src/read/panel.ts +335 -0
- package/src/read/pipeline.ts +78 -0
- package/src/read/refactor.ts +576 -0
- package/src/read/render.ts +1118 -0
- package/src/read/scan.ts +61 -0
- package/src/read/seam.ts +0 -0
- package/src/read/security.ts +171 -0
- package/src/read/signals.ts +333 -0
- package/src/read/state.ts +71 -0
- package/src/read/stategraph.ts +205 -0
- package/src/read/types.ts +162 -0
- package/src/read/vite.ts +77 -0
- package/src/ui/babel/line-profiler.ts +197 -0
- package/src/ui/babel/source-loc.ts +68 -0
- package/src/ui/bridge/cdp-bridge.ts +138 -0
- package/src/ui/bridge/compile-probe.ts +80 -0
- package/src/ui/bridge/transport.ts +26 -0
- package/src/ui/bridge/vite-bridge.ts +116 -0
- package/src/ui/client/client-patch.ts +899 -0
- package/src/ui/client/client.ts +2562 -0
- package/src/ui/core/action.ts +747 -0
- package/src/ui/core/candidates.ts +348 -0
- package/src/ui/core/canvas.ts +305 -0
- package/src/ui/core/check.ts +34 -0
- package/src/ui/core/compact.ts +314 -0
- package/src/ui/core/detail.ts +244 -0
- package/src/ui/core/diff.ts +253 -0
- package/src/ui/core/emit.ts +198 -0
- package/src/ui/core/knob-exec.ts +137 -0
- package/src/ui/core/perf.ts +254 -0
- package/src/ui/core/types.ts +164 -0
- package/src/ui/core/util.ts +221 -0
- package/src/ui/index.ts +5 -0
- package/src/ui/probe/cli.ts +139 -0
- package/src/ui/probe/server.ts +468 -0
- package/src/ui/self/act.ts +47 -0
- package/src/ui/self/discover.ts +101 -0
- package/src/ui/self/grow.ts +121 -0
- package/src/ui/self/install.ts +100 -0
- package/src/ui/self/probe.ts +105 -0
- package/src/ui/self/screen-hook.ts +44 -0
- package/src/ui/self/self.ts +48 -0
- package/src/ui/self/store-refs.ts +123 -0
- package/src/ui/self/store-schema.ts +65 -0
- package/src/ui/self/synth.ts +37 -0
- package/src/ui/server/cli.ts +102 -0
- package/src/ui/server/dispatch.ts +276 -0
- package/src/ui/server/help-text.ts +237 -0
- package/src/ui/server/knob-schema.ts +87 -0
- package/src/ui/server/plugin.ts +1151 -0
- package/src/vite.ts +39 -0
- package/index.js +0 -2
|
@@ -0,0 +1,2448 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildPanel,
|
|
3
|
+
buildStateGraph,
|
|
4
|
+
classifyKnob,
|
|
5
|
+
fmtTransitions,
|
|
6
|
+
openProject
|
|
7
|
+
} from "./chunk-G7KVJ7NF.js";
|
|
8
|
+
import {
|
|
9
|
+
TYPES,
|
|
10
|
+
appStackFrames,
|
|
11
|
+
appendAction,
|
|
12
|
+
argsFromQuery,
|
|
13
|
+
compactUrl,
|
|
14
|
+
diagnose,
|
|
15
|
+
dispatchAction,
|
|
16
|
+
dispatchScreen,
|
|
17
|
+
formatActions,
|
|
18
|
+
formatTabs,
|
|
19
|
+
formatValue,
|
|
20
|
+
isDispatchAction,
|
|
21
|
+
isLive,
|
|
22
|
+
isSecretKey,
|
|
23
|
+
misroutedAction,
|
|
24
|
+
redactSecretValue,
|
|
25
|
+
resolveAction,
|
|
26
|
+
runChain,
|
|
27
|
+
truncate
|
|
28
|
+
} from "./chunk-YKSYW77R.js";
|
|
29
|
+
import {
|
|
30
|
+
loadConfig
|
|
31
|
+
} from "./chunk-PQSQN4CN.js";
|
|
32
|
+
import {
|
|
33
|
+
scan
|
|
34
|
+
} from "./chunk-GNEUSRGP.js";
|
|
35
|
+
|
|
36
|
+
// src/vite.ts
|
|
37
|
+
import { resolve as resolve4 } from "path";
|
|
38
|
+
|
|
39
|
+
// src/read/vite.ts
|
|
40
|
+
import { spawn } from "child_process";
|
|
41
|
+
import { existsSync } from "fs";
|
|
42
|
+
import { dirname, resolve } from "path";
|
|
43
|
+
import { fileURLToPath } from "url";
|
|
44
|
+
function resolveCliCmd() {
|
|
45
|
+
try {
|
|
46
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
47
|
+
const src = resolve(here, "..", "cli.ts");
|
|
48
|
+
if (existsSync(src)) {
|
|
49
|
+
return { cmd: process.execPath, args: ["--import", "tsx", src, "read"] };
|
|
50
|
+
}
|
|
51
|
+
for (const rel of ["../cli.js", "../../dist/cli.js"]) {
|
|
52
|
+
const p = resolve(here, rel);
|
|
53
|
+
if (existsSync(p))
|
|
54
|
+
return { cmd: process.execPath, args: [p, "read"] };
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
return { cmd: "npx", args: ["aihand", "read"] };
|
|
59
|
+
}
|
|
60
|
+
function repodexWatchPlugin(opts = {}) {
|
|
61
|
+
const { args = [], stdio = "inherit" } = opts;
|
|
62
|
+
return {
|
|
63
|
+
name: "aihand-read-watch",
|
|
64
|
+
apply: "serve",
|
|
65
|
+
configureServer(server) {
|
|
66
|
+
const { cmd, args: prefix } = resolveCliCmd();
|
|
67
|
+
const child = spawn(cmd, [...prefix, "--watch", ...args], {
|
|
68
|
+
stdio,
|
|
69
|
+
env: { ...process.env, FORCE_COLOR: process.env.FORCE_COLOR ?? "1" }
|
|
70
|
+
});
|
|
71
|
+
let done = false;
|
|
72
|
+
const cleanup = () => {
|
|
73
|
+
if (done)
|
|
74
|
+
return;
|
|
75
|
+
done = true;
|
|
76
|
+
if (!child.killed)
|
|
77
|
+
child.kill();
|
|
78
|
+
};
|
|
79
|
+
child.once("exit", () => {
|
|
80
|
+
done = true;
|
|
81
|
+
});
|
|
82
|
+
server.httpServer?.once("close", cleanup);
|
|
83
|
+
process.once("exit", cleanup);
|
|
84
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
85
|
+
process.once(sig, () => {
|
|
86
|
+
cleanup();
|
|
87
|
+
process.kill(process.pid, sig);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/ui/self/store-refs.ts
|
|
95
|
+
import { relative } from "path";
|
|
96
|
+
|
|
97
|
+
// src/ui/self/discover.ts
|
|
98
|
+
import { existsSync as existsSync2, statSync } from "fs";
|
|
99
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
100
|
+
function projectRootFor(start) {
|
|
101
|
+
let dir = statSync(start).isDirectory() ? start : dirname2(start);
|
|
102
|
+
for (; ; ) {
|
|
103
|
+
if (["tsconfig.app.json", "tsconfig.json"].some((f) => existsSync2(resolve2(dir, f))))
|
|
104
|
+
return dir;
|
|
105
|
+
const up = dirname2(dir);
|
|
106
|
+
if (up === dir)
|
|
107
|
+
return start;
|
|
108
|
+
dir = up;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function scopePrefix(root) {
|
|
112
|
+
const abs = resolve2(root);
|
|
113
|
+
try {
|
|
114
|
+
return statSync(abs).isDirectory() ? `${abs}/` : abs;
|
|
115
|
+
} catch {
|
|
116
|
+
return abs;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function discover(roots, marker = "_loading") {
|
|
120
|
+
return (await discoverWithPaths(roots, marker)).map(({ abs: _abs, ...n }) => {
|
|
121
|
+
const i = n.anchor.lastIndexOf(":");
|
|
122
|
+
const abs = n.anchor.slice(0, i);
|
|
123
|
+
return { ...n, anchor: `${abs.split("/").slice(-2).join("/")}${n.anchor.slice(i)}` };
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async function discoverWithPaths(roots, marker = "_loading") {
|
|
127
|
+
const rawRoots = Array.isArray(roots) ? roots : [roots];
|
|
128
|
+
const list = rawRoots.map(scopePrefix);
|
|
129
|
+
const project = await openProject(projectRootFor(resolve2(rawRoots[0])));
|
|
130
|
+
const { nodes } = buildStateGraph(project, (abs) => abs, marker);
|
|
131
|
+
return nodes.filter((n) => {
|
|
132
|
+
const fp = n.anchor.slice(0, n.anchor.lastIndexOf(":"));
|
|
133
|
+
return list.some((p) => p.endsWith("/") ? fp.startsWith(p) : fp === p);
|
|
134
|
+
}).map((n) => ({ ...n, abs: n.anchor.slice(0, n.anchor.lastIndexOf(":")) }));
|
|
135
|
+
}
|
|
136
|
+
async function locateExports(roots, names) {
|
|
137
|
+
const rawRoots = Array.isArray(roots) ? roots : [roots];
|
|
138
|
+
const project = await openProject(projectRootFor(resolve2(rawRoots[0])));
|
|
139
|
+
const out = /* @__PURE__ */ new Map();
|
|
140
|
+
for (const sf of project.getSourceFiles()) {
|
|
141
|
+
const fp = sf.getFilePath();
|
|
142
|
+
if (fp.endsWith(".d.ts") || fp.includes("/node_modules/"))
|
|
143
|
+
continue;
|
|
144
|
+
for (const v of sf.getVariableDeclarations()) {
|
|
145
|
+
const name = v.getName();
|
|
146
|
+
if (!names.has(name) || out.has(name))
|
|
147
|
+
continue;
|
|
148
|
+
const stmt = v.getVariableStatement();
|
|
149
|
+
if (stmt && stmt.isExported())
|
|
150
|
+
out.set(name, fp);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/ui/self/store-refs.ts
|
|
157
|
+
var VIRTUAL = "virtual:store-refs";
|
|
158
|
+
var RESOLVED = `\0${VIRTUAL}`;
|
|
159
|
+
function toAlias(abs, srcRoot) {
|
|
160
|
+
let rel = relative(srcRoot, abs);
|
|
161
|
+
for (const ext of [".tsx", ".ts", ".jsx", ".js"]) {
|
|
162
|
+
if (rel.endsWith(ext)) {
|
|
163
|
+
rel = rel.slice(0, -ext.length);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return `@/${rel}`;
|
|
168
|
+
}
|
|
169
|
+
function pickFacades(knobNames, discoveredNames) {
|
|
170
|
+
return [...knobNames].filter((n) => !discoveredNames.has(n));
|
|
171
|
+
}
|
|
172
|
+
function emitModule(refs) {
|
|
173
|
+
const byAlias = /* @__PURE__ */ new Map();
|
|
174
|
+
for (const r of refs) {
|
|
175
|
+
const names = byAlias.get(r.alias) ?? [];
|
|
176
|
+
names.push(r.name);
|
|
177
|
+
byAlias.set(r.alias, names);
|
|
178
|
+
}
|
|
179
|
+
const imports = [];
|
|
180
|
+
for (const [alias, names] of byAlias)
|
|
181
|
+
imports.push(`import { ${[...new Set(names)].sort().join(", ")} } from '${alias}'`);
|
|
182
|
+
const all = [...new Set(refs.map((r) => r.name))].sort();
|
|
183
|
+
return `${imports.join("\n")}
|
|
184
|
+
export const stores = { ${all.join(", ")} }
|
|
185
|
+
`;
|
|
186
|
+
}
|
|
187
|
+
function storeRefs(options) {
|
|
188
|
+
const roots = Array.isArray(options.roots) ? options.roots : [options.roots];
|
|
189
|
+
let cache = null;
|
|
190
|
+
const build = async () => {
|
|
191
|
+
if (cache)
|
|
192
|
+
return cache;
|
|
193
|
+
const discovered = await discoverWithPaths(roots, options.storeMarker);
|
|
194
|
+
const byName = new Map(discovered.map((d) => [d.store, d.abs]));
|
|
195
|
+
const knobs = await buildPanel(options.knobFiles);
|
|
196
|
+
const knobNames = new Set(knobs.map((k) => k.store).filter(Boolean));
|
|
197
|
+
const facadeNames = new Set(pickFacades(knobNames, new Set(byName.keys())));
|
|
198
|
+
const facadePaths = facadeNames.size ? await locateExports(roots, facadeNames) : /* @__PURE__ */ new Map();
|
|
199
|
+
const refs = [];
|
|
200
|
+
const missing = [];
|
|
201
|
+
for (const name of knobNames) {
|
|
202
|
+
const abs = byName.get(name) ?? facadePaths.get(name);
|
|
203
|
+
if (!abs) {
|
|
204
|
+
missing.push(name);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
refs.push({ name, alias: toAlias(abs, options.srcRoot) });
|
|
208
|
+
}
|
|
209
|
+
if (missing.length)
|
|
210
|
+
console.warn(`[aihand:store-refs] \u65CB\u94AE\u5F15\u7528\u4F46\u5B9A\u4F4D\u4E0D\u5230\u58F0\u660E\u7684 store(\u5DF2\u8DF3\u8FC7): ${missing.join(", ")}`);
|
|
211
|
+
cache = emitModule(refs);
|
|
212
|
+
return cache;
|
|
213
|
+
};
|
|
214
|
+
return {
|
|
215
|
+
name: "aihand:store-refs",
|
|
216
|
+
resolveId(id) {
|
|
217
|
+
if (id === VIRTUAL)
|
|
218
|
+
return RESOLVED;
|
|
219
|
+
},
|
|
220
|
+
async load(id) {
|
|
221
|
+
if (id !== RESOLVED)
|
|
222
|
+
return;
|
|
223
|
+
return build();
|
|
224
|
+
},
|
|
225
|
+
// store 文件或组件改动 → 失效缓存,下次 cold load 重 codegen(加旋钮引新 store → 自动并入)。
|
|
226
|
+
handleHotUpdate(ctx) {
|
|
227
|
+
if (roots.some((r) => ctx.file.startsWith(r)) || ctx.file.endsWith(".tsx") || ctx.file.endsWith(".jsx"))
|
|
228
|
+
cache = null;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/ui/self/store-schema.ts
|
|
234
|
+
var VIRTUAL2 = "virtual:store-schema";
|
|
235
|
+
var RESOLVED2 = "\0" + VIRTUAL2;
|
|
236
|
+
function storeSchema(options) {
|
|
237
|
+
let cache = null;
|
|
238
|
+
const roots = Array.isArray(options.roots) ? options.roots : [options.roots];
|
|
239
|
+
const build = async () => {
|
|
240
|
+
if (!cache)
|
|
241
|
+
cache = await discover(roots, options.storeMarker);
|
|
242
|
+
return cache;
|
|
243
|
+
};
|
|
244
|
+
return {
|
|
245
|
+
name: "aihand:store-schema",
|
|
246
|
+
resolveId(id) {
|
|
247
|
+
if (id === VIRTUAL2)
|
|
248
|
+
return RESOLVED2;
|
|
249
|
+
},
|
|
250
|
+
async load(id) {
|
|
251
|
+
if (id !== RESOLVED2)
|
|
252
|
+
return;
|
|
253
|
+
const grown = await build();
|
|
254
|
+
const byStore = {};
|
|
255
|
+
for (const g of grown)
|
|
256
|
+
byStore[g.store] = g;
|
|
257
|
+
return `export const schema = ${JSON.stringify(byStore)}
|
|
258
|
+
`;
|
|
259
|
+
},
|
|
260
|
+
// store 文件改动 → 失效缓存。下次 cold load 重跑引擎(~2s),值域改了 schema 跟着变。
|
|
261
|
+
// 不在 handleHotUpdate 里同步重投影:引擎离线纪律,绝不让 ts-morph 进同步 HMR 热路径 ——
|
|
262
|
+
// 值域是类型注解(改动罕见),下次 load 拿到即可,不值得为它在热路径养一个常驻 Project。
|
|
263
|
+
handleHotUpdate(ctx) {
|
|
264
|
+
if (roots.some((r) => ctx.file.startsWith(r)))
|
|
265
|
+
cache = null;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/ui/server/knob-schema.ts
|
|
271
|
+
import { globSync } from "glob";
|
|
272
|
+
var VIRTUAL3 = "virtual:aihand-knobs";
|
|
273
|
+
var RESOLVED3 = "\0" + VIRTUAL3;
|
|
274
|
+
function keyOf(label, tag, file) {
|
|
275
|
+
return `${label ?? `<${tag}>`} ${file}`;
|
|
276
|
+
}
|
|
277
|
+
function knobSchema(options = {}) {
|
|
278
|
+
const include = options.include ?? ["src/**/*.tsx", "src/**/*.jsx"];
|
|
279
|
+
let cache = null;
|
|
280
|
+
const build = async () => {
|
|
281
|
+
if (cache)
|
|
282
|
+
return cache;
|
|
283
|
+
const files = globSync(include, { nodir: true, ignore: ["**/node_modules/**"] });
|
|
284
|
+
const knobs = await buildPanel(files);
|
|
285
|
+
const byKey = {};
|
|
286
|
+
for (const k of knobs) {
|
|
287
|
+
const file = k.filePath.split("/").slice(-2).join("/");
|
|
288
|
+
const { ops, arity, executable } = classifyKnob(k.transitions);
|
|
289
|
+
byKey[keyOf(k.label, k.tag, file)] = {
|
|
290
|
+
transitions: fmtTransitions(k.transitions),
|
|
291
|
+
store: k.store,
|
|
292
|
+
line: k.line,
|
|
293
|
+
ops,
|
|
294
|
+
arity,
|
|
295
|
+
executable
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
cache = byKey;
|
|
299
|
+
return byKey;
|
|
300
|
+
};
|
|
301
|
+
return {
|
|
302
|
+
name: "aihand:knob-schema",
|
|
303
|
+
resolveId(id) {
|
|
304
|
+
if (id === VIRTUAL3)
|
|
305
|
+
return RESOLVED3;
|
|
306
|
+
},
|
|
307
|
+
async load(id) {
|
|
308
|
+
if (id !== RESOLVED3)
|
|
309
|
+
return;
|
|
310
|
+
const knobs = await build();
|
|
311
|
+
return `export const knobs = ${JSON.stringify(knobs)}
|
|
312
|
+
`;
|
|
313
|
+
},
|
|
314
|
+
// 组件改动 → 失效缓存,HMR 重投影(onClick 改了 → 态射跟着变)。
|
|
315
|
+
handleHotUpdate(ctx) {
|
|
316
|
+
if (ctx.file.endsWith(".tsx") || ctx.file.endsWith(".jsx"))
|
|
317
|
+
cache = null;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/ui/server/plugin.ts
|
|
323
|
+
import { Buffer } from "buffer";
|
|
324
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
325
|
+
import { dirname as dirname3, isAbsolute, relative as relative2, resolve as resolve3 } from "path";
|
|
326
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
327
|
+
import { transformSync } from "esbuild";
|
|
328
|
+
|
|
329
|
+
// src/ui/babel/source-loc.ts
|
|
330
|
+
import path from "path";
|
|
331
|
+
function sourceLocPlugin(t) {
|
|
332
|
+
return {
|
|
333
|
+
name: "aihand-source-loc",
|
|
334
|
+
visitor: {
|
|
335
|
+
JSXOpeningElement(nodePath, state) {
|
|
336
|
+
const filename = state.file?.opts?.filename ?? "";
|
|
337
|
+
if (!filename || filename.includes("node_modules"))
|
|
338
|
+
return;
|
|
339
|
+
if (filename.includes("packages/aidev/"))
|
|
340
|
+
return;
|
|
341
|
+
const nameNode = nodePath.node.name;
|
|
342
|
+
if (!t.isJSXIdentifier(nameNode))
|
|
343
|
+
return;
|
|
344
|
+
const tag = nameNode.name;
|
|
345
|
+
if (tag[0] === tag[0].toUpperCase())
|
|
346
|
+
return;
|
|
347
|
+
if (hasInspAttr(t, nodePath.node))
|
|
348
|
+
return;
|
|
349
|
+
const loc = nodePath.node.loc;
|
|
350
|
+
if (!loc)
|
|
351
|
+
return;
|
|
352
|
+
const root = state.file?.opts?.root ?? "";
|
|
353
|
+
const rel = root ? path.relative(root, filename) : filename;
|
|
354
|
+
const value = `${rel}:${loc.start.line}:${loc.start.column + 1}:${path.basename(rel, path.extname(rel))}`;
|
|
355
|
+
nodePath.node.attributes.push(
|
|
356
|
+
t.jsxAttribute(t.jsxIdentifier("data-insp-path"), t.stringLiteral(value))
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function hasInspAttr(t, node) {
|
|
363
|
+
return node.attributes.some((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === "data-insp-path");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/ui/bridge/vite-bridge.ts
|
|
367
|
+
var VISIBLE_MS = 400;
|
|
368
|
+
function viteBridge(server, deps) {
|
|
369
|
+
const progressAt = /* @__PURE__ */ new Map();
|
|
370
|
+
server.hot.on("aihand:action-progress", (data) => {
|
|
371
|
+
if (typeof data.id === "number")
|
|
372
|
+
progressAt.set(data.id, Date.now());
|
|
373
|
+
});
|
|
374
|
+
const request = (reqEvent, replyEvent, payload, opts) => {
|
|
375
|
+
const tab = opts?.tab;
|
|
376
|
+
const fullMs = opts?.timeoutMs ?? 3e3;
|
|
377
|
+
const id = payload.id;
|
|
378
|
+
const lastProgress = opts?.lastProgress ?? (id !== void 0 ? () => progressAt.get(id) : void 0);
|
|
379
|
+
return new Promise((resolve5, reject) => {
|
|
380
|
+
let settled = false;
|
|
381
|
+
const onReply = (data) => {
|
|
382
|
+
if (settled)
|
|
383
|
+
return;
|
|
384
|
+
if (id !== void 0 && data.id !== id)
|
|
385
|
+
return;
|
|
386
|
+
settled = true;
|
|
387
|
+
cleanup();
|
|
388
|
+
resolve5(data);
|
|
389
|
+
};
|
|
390
|
+
server.hot.on(replyEvent, onReply);
|
|
391
|
+
const cleanup = () => {
|
|
392
|
+
server.hot.off(replyEvent, onReply);
|
|
393
|
+
if (id !== void 0)
|
|
394
|
+
progressAt.delete(id);
|
|
395
|
+
};
|
|
396
|
+
if (tab) {
|
|
397
|
+
const RETRY_MS = 500;
|
|
398
|
+
const ABSENT_CEILING_MS = 1e4;
|
|
399
|
+
const startedAt = Date.now();
|
|
400
|
+
const deliver = () => server.hot.send(reqEvent, { ...payload, tab });
|
|
401
|
+
deliver();
|
|
402
|
+
const iv = setInterval(() => {
|
|
403
|
+
if (settled) {
|
|
404
|
+
clearInterval(iv);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const t = deps.tabs.get(tab);
|
|
408
|
+
const live = !!t && isLive(t, Date.now());
|
|
409
|
+
const progress = lastProgress?.();
|
|
410
|
+
const sinceLife = Date.now() - Math.max(startedAt, progress ?? 0);
|
|
411
|
+
const elapsed = Date.now() - startedAt;
|
|
412
|
+
if (live) {
|
|
413
|
+
if (sinceLife > fullMs) {
|
|
414
|
+
clearInterval(iv);
|
|
415
|
+
cleanup();
|
|
416
|
+
reject(new Error(deps.diagnose(tab)));
|
|
417
|
+
}
|
|
418
|
+
} else if (elapsed > ABSENT_CEILING_MS) {
|
|
419
|
+
clearInterval(iv);
|
|
420
|
+
cleanup();
|
|
421
|
+
reject(new Error(deps.diagnose(tab)));
|
|
422
|
+
} else {
|
|
423
|
+
deliver();
|
|
424
|
+
}
|
|
425
|
+
}, RETRY_MS);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
server.hot.send(reqEvent, { ...payload, requireVisible: true });
|
|
429
|
+
setTimeout(() => {
|
|
430
|
+
if (settled)
|
|
431
|
+
return;
|
|
432
|
+
server.hot.send(reqEvent, { ...payload, requireVisible: false });
|
|
433
|
+
setTimeout(() => {
|
|
434
|
+
if (settled)
|
|
435
|
+
return;
|
|
436
|
+
cleanup();
|
|
437
|
+
reject(new Error(deps.diagnose(tab)));
|
|
438
|
+
}, fullMs);
|
|
439
|
+
}, VISIBLE_MS);
|
|
440
|
+
});
|
|
441
|
+
};
|
|
442
|
+
const emit2 = (event, payload) => server.hot.send(event, payload);
|
|
443
|
+
return { request, emit: emit2 };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/ui/core/check.ts
|
|
447
|
+
function check(raw) {
|
|
448
|
+
const consoleErrors = raw.console.filter((l) => l.level === "error");
|
|
449
|
+
const failedRequests = raw.network.filter((r) => r.status >= 400 || r.failed);
|
|
450
|
+
const assertions = [
|
|
451
|
+
{
|
|
452
|
+
name: "no-console-errors",
|
|
453
|
+
pass: consoleErrors.length === 0,
|
|
454
|
+
...consoleErrors.length && { detail: consoleErrors[0].text }
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
name: "no-uncaught-errors",
|
|
458
|
+
pass: raw.errors.length === 0,
|
|
459
|
+
...raw.errors.length && { detail: raw.errors[0].message }
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
name: "no-failed-requests",
|
|
463
|
+
pass: failedRequests.length === 0,
|
|
464
|
+
...failedRequests.length && { detail: `${failedRequests[0].method} ${failedRequests[0].url} ${failedRequests[0].status}` }
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
name: "ui-not-empty",
|
|
468
|
+
pass: raw.ui.trim().length > 0,
|
|
469
|
+
...!raw.ui.trim().length && { detail: "UI tree is empty" }
|
|
470
|
+
}
|
|
471
|
+
];
|
|
472
|
+
return {
|
|
473
|
+
pass: assertions.every((a) => a.pass),
|
|
474
|
+
assertions
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/ui/core/perf.ts
|
|
479
|
+
var FRAME_NOISE_PCT = 3;
|
|
480
|
+
var SELF_NOISE_REL = 0.1;
|
|
481
|
+
var SELF_NOISE_MS = 5;
|
|
482
|
+
var avgOf = (samples) => samples.length > 0 ? samples.reduce((s, v) => s + v, 0) / samples.length : 0;
|
|
483
|
+
function summarizeStat(stat) {
|
|
484
|
+
const avg = stat.n > 0 ? stat.total / stat.n : 0;
|
|
485
|
+
return {
|
|
486
|
+
total: stat.total,
|
|
487
|
+
n: stat.n,
|
|
488
|
+
avg,
|
|
489
|
+
max: stat.max
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function compactPerformance(p) {
|
|
493
|
+
const all = p.buckets.find((b) => b.name === "__all__");
|
|
494
|
+
if (!all) return "perf: (no data)";
|
|
495
|
+
const { total, long, samples } = all.frames;
|
|
496
|
+
const fps = total > 0 ? Math.round(1e3 / avgOf(samples) * 10) / 10 : 0;
|
|
497
|
+
const droppedPct = total > 0 ? Math.round(long / total * 100) : 0;
|
|
498
|
+
const parts = [`${fps}fps`, `${droppedPct}% dropped`];
|
|
499
|
+
if (p.longtasks.count > 0) {
|
|
500
|
+
parts.push(`${p.longtasks.count} longtasks(max ${Math.round(p.longtasks.max)}ms)`);
|
|
501
|
+
}
|
|
502
|
+
const topComps = Object.entries(all.components).map(([name, stat]) => ({ name, total: stat.total })).sort((a, b) => b.total - a.total).slice(0, 2);
|
|
503
|
+
if (topComps.length > 0 && p.mobxPatched) {
|
|
504
|
+
const hot = topComps.map((c) => `${c.name} ${Math.round(c.total)}ms`).join(", ");
|
|
505
|
+
parts.push(`hot: ${hot}`);
|
|
506
|
+
}
|
|
507
|
+
return `perf: ${parts.join(" \xB7 ")}`;
|
|
508
|
+
}
|
|
509
|
+
function detailPerformance(p) {
|
|
510
|
+
const lines = [];
|
|
511
|
+
const windowSec = (p.windowMs / 1e3).toFixed(1);
|
|
512
|
+
lines.push(`=== Performance (${windowSec}s window) ===`);
|
|
513
|
+
if (p.hiddenFrames > 0) {
|
|
514
|
+
lines.push(`\u26A0 Tab was backgrounded or unfocused \u2014 ${p.hiddenFrames} throttled frames skipped (rAF throttling lies about timing)`);
|
|
515
|
+
lines.push("For accurate profiling: keep tab FOREGROUND + FOCUSED + run /profile/reset before reproducing");
|
|
516
|
+
}
|
|
517
|
+
lines.push(`MobX attribution: ${p.mobxPatched ? "ON" : "OFF"}`);
|
|
518
|
+
if (!p.mobxPatched) {
|
|
519
|
+
lines.push("(Component attribution disabled \u2014 set window.__AIPEEK_MOBX__={Reaction} in src/lib/mobx.ts to enable)");
|
|
520
|
+
}
|
|
521
|
+
lines.push("");
|
|
522
|
+
if (p.longtasks.count > 0) {
|
|
523
|
+
lines.push(`Longtasks (>50ms): ${p.longtasks.count}, max ${Math.round(p.longtasks.max)}ms`);
|
|
524
|
+
lines.push("");
|
|
525
|
+
}
|
|
526
|
+
const showPerView = p.buckets.length > 1;
|
|
527
|
+
for (const bucket of p.buckets) {
|
|
528
|
+
if (bucket.name !== "__all__" && !showPerView) continue;
|
|
529
|
+
lines.push(`--- ${bucket.name} ---`);
|
|
530
|
+
const { total, long, max, samples } = bucket.frames;
|
|
531
|
+
if (total === 0) {
|
|
532
|
+
lines.push("Frames: none sampled (tab unfocused/hidden \u2014 no frame data this window)");
|
|
533
|
+
} else {
|
|
534
|
+
const droppedPct = (long / total * 100).toFixed(1);
|
|
535
|
+
const avgFrame = avgOf(samples).toFixed(1);
|
|
536
|
+
lines.push(`Frames: ${total} total, ${long} dropped (${droppedPct}%), max ${Math.round(max)}ms, avg ${avgFrame}ms`);
|
|
537
|
+
}
|
|
538
|
+
const comps = Object.entries(bucket.components).map(([name, stat]) => ({ name, ...summarizeStat(stat) })).sort((a, b) => b.total - a.total).slice(0, 20);
|
|
539
|
+
if (comps.length > 0 && p.mobxPatched) {
|
|
540
|
+
lines.push("Components (by total self-time):");
|
|
541
|
+
for (const c of comps) {
|
|
542
|
+
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`);
|
|
543
|
+
}
|
|
544
|
+
} else if (!p.mobxPatched) {
|
|
545
|
+
lines.push("(no component data \u2014 attribution OFF)");
|
|
546
|
+
}
|
|
547
|
+
const hotLines = Object.entries(bucket.lines ?? {}).map(([label, stat]) => ({ label, ...summarizeStat(stat) })).sort((a, b) => b.total - a.total).slice(0, 15);
|
|
548
|
+
if (hotLines.length > 0) {
|
|
549
|
+
lines.push("Hot lines (by total inclusive time \u2014 contains nested calls):");
|
|
550
|
+
for (const l of hotLines) {
|
|
551
|
+
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`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
lines.push("");
|
|
555
|
+
}
|
|
556
|
+
return lines.join("\n");
|
|
557
|
+
}
|
|
558
|
+
function diffPerformance(before, after) {
|
|
559
|
+
const out = [];
|
|
560
|
+
const beforeAll = before.buckets.find((b) => b.name === "__all__");
|
|
561
|
+
const afterAll = after.buckets.find((b) => b.name === "__all__");
|
|
562
|
+
const droppedPct = (b) => b && b.frames.total > 0 ? b.frames.long / b.frames.total * 100 : 0;
|
|
563
|
+
const beforeDrop = droppedPct(beforeAll);
|
|
564
|
+
const afterDrop = droppedPct(afterAll);
|
|
565
|
+
const dropDelta = afterDrop - beforeDrop;
|
|
566
|
+
const sumTotal = (b) => b ? Object.values(b.components).reduce((s, c) => s + c.total, 0) : 0;
|
|
567
|
+
const beforeSelf = sumTotal(beforeAll);
|
|
568
|
+
const afterSelf = sumTotal(afterAll);
|
|
569
|
+
const selfDelta = afterSelf - beforeSelf;
|
|
570
|
+
const bothSampledFrames = (beforeAll?.frames.total ?? 0) > 0 && (afterAll?.frames.total ?? 0) > 0;
|
|
571
|
+
const sawDrops = (beforeAll?.frames.long ?? 0) > 0 || (afterAll?.frames.long ?? 0) > 0;
|
|
572
|
+
const framesExercised = bothSampledFrames && sawDrops;
|
|
573
|
+
const afterBlind = afterSelf === 0 && (beforeSelf > 0 || (afterAll?.frames.total ?? 0) === 0);
|
|
574
|
+
let verdict;
|
|
575
|
+
if (afterBlind) {
|
|
576
|
+
verdict = "NO DATA";
|
|
577
|
+
} else if (framesExercised) {
|
|
578
|
+
if (dropDelta < -FRAME_NOISE_PCT) verdict = "IMPROVED";
|
|
579
|
+
else if (dropDelta > FRAME_NOISE_PCT) verdict = "REGRESSED";
|
|
580
|
+
else verdict = "UNCHANGED";
|
|
581
|
+
} else {
|
|
582
|
+
const floor = Math.max(SELF_NOISE_MS, beforeSelf * SELF_NOISE_REL);
|
|
583
|
+
if (selfDelta < -floor) verdict = "IMPROVED";
|
|
584
|
+
else if (selfDelta > floor) verdict = "REGRESSED";
|
|
585
|
+
else verdict = "UNCHANGED";
|
|
586
|
+
}
|
|
587
|
+
out.push(`=== Perf diff: ${verdict} ===`);
|
|
588
|
+
if (afterBlind) {
|
|
589
|
+
const why = (afterAll?.frames.total ?? 0) === 0 ? "recorded no frames and no re-renders" : `recorded frames but zero re-renders (was ${Math.round(beforeSelf)}ms self-time)`;
|
|
590
|
+
out.push(`after-window ${why} \u2014 tab backgrounded or workload not reproduced. Re-run with the tab focused and the interaction repeated.`);
|
|
591
|
+
out.push("");
|
|
592
|
+
return out.join("\n").trimEnd();
|
|
593
|
+
}
|
|
594
|
+
out.push(`Dropped frames: ${beforeDrop.toFixed(1)}% \u2192 ${afterDrop.toFixed(1)}% (${dropDelta >= 0 ? "+" : ""}${dropDelta.toFixed(1)}%)`);
|
|
595
|
+
if (!framesExercised && Math.round(selfDelta) !== 0) {
|
|
596
|
+
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`);
|
|
597
|
+
}
|
|
598
|
+
if (before.longtasks.count !== after.longtasks.count) {
|
|
599
|
+
out.push(`Longtasks: ${before.longtasks.count} \u2192 ${after.longtasks.count}`);
|
|
600
|
+
}
|
|
601
|
+
out.push("");
|
|
602
|
+
const compDelta = keyedTotalDelta(beforeAll?.components ?? {}, afterAll?.components ?? {});
|
|
603
|
+
if (compDelta.length > 0) {
|
|
604
|
+
out.push("Components (\u0394 total self-time):");
|
|
605
|
+
for (const d of compDelta) out.push(" " + formatDelta(d));
|
|
606
|
+
out.push("");
|
|
607
|
+
}
|
|
608
|
+
const lineDelta = keyedTotalDelta(beforeAll?.lines ?? {}, afterAll?.lines ?? {});
|
|
609
|
+
if (lineDelta.length > 0) {
|
|
610
|
+
out.push("Hot lines (\u0394 total inclusive time):");
|
|
611
|
+
for (const d of lineDelta) out.push(" " + formatDelta(d));
|
|
612
|
+
}
|
|
613
|
+
return out.join("\n").trimEnd();
|
|
614
|
+
}
|
|
615
|
+
function keyedTotalDelta(a, b) {
|
|
616
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
617
|
+
const rows = [];
|
|
618
|
+
for (const key of keys) {
|
|
619
|
+
const beforeT = a[key]?.total ?? 0;
|
|
620
|
+
const afterT = b[key]?.total ?? 0;
|
|
621
|
+
const delta = afterT - beforeT;
|
|
622
|
+
if (Math.round(delta) === 0) continue;
|
|
623
|
+
rows.push({ key, before: beforeT, after: afterT, delta, isNew: !a[key], gone: !b[key] });
|
|
624
|
+
}
|
|
625
|
+
return rows.sort((x, y) => Math.abs(y.delta) - Math.abs(x.delta)).slice(0, 20);
|
|
626
|
+
}
|
|
627
|
+
function formatDelta(d) {
|
|
628
|
+
const sign = d.delta >= 0 ? "+" : "";
|
|
629
|
+
const tag = d.isNew ? " (new)" : d.gone ? " (gone)" : "";
|
|
630
|
+
return `${d.key.padEnd(36)} ${Math.round(d.before)}ms \u2192 ${Math.round(d.after)}ms (${sign}${Math.round(d.delta)}ms)${tag}`;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/ui/core/compact.ts
|
|
634
|
+
var SLOW_THRESHOLD = 1e3;
|
|
635
|
+
var MAX_UI_DEPTH = 6;
|
|
636
|
+
var UI_PRIMITIVES = /* @__PURE__ */ new Set([
|
|
637
|
+
"Button",
|
|
638
|
+
"Input",
|
|
639
|
+
"Label",
|
|
640
|
+
"Badge",
|
|
641
|
+
"Checkbox",
|
|
642
|
+
"Skeleton",
|
|
643
|
+
"Spinner",
|
|
644
|
+
"Switch",
|
|
645
|
+
"Tabs",
|
|
646
|
+
"Tooltip",
|
|
647
|
+
"Popover",
|
|
648
|
+
"Dialog",
|
|
649
|
+
"Select",
|
|
650
|
+
"Card",
|
|
651
|
+
"Table",
|
|
652
|
+
"Slider",
|
|
653
|
+
"Progress",
|
|
654
|
+
"RadioGroup",
|
|
655
|
+
"HoverCard",
|
|
656
|
+
"DropdownMenu",
|
|
657
|
+
"ContextMenu",
|
|
658
|
+
"Command",
|
|
659
|
+
"Form",
|
|
660
|
+
"Alert",
|
|
661
|
+
"Pagination",
|
|
662
|
+
"Textarea",
|
|
663
|
+
"TooltipProvider",
|
|
664
|
+
"DialogPortal",
|
|
665
|
+
"Router",
|
|
666
|
+
"RenderErrorBoundary",
|
|
667
|
+
"RouterProvider",
|
|
668
|
+
"RouterProvider2",
|
|
669
|
+
"PanelGroup",
|
|
670
|
+
"Panel"
|
|
671
|
+
]);
|
|
672
|
+
function nameOf(line) {
|
|
673
|
+
let end = 0;
|
|
674
|
+
while (end < line.length) {
|
|
675
|
+
const c = line[end];
|
|
676
|
+
if (c === " " || c === " " || c === "[" || c === "\u2014")
|
|
677
|
+
break;
|
|
678
|
+
end++;
|
|
679
|
+
}
|
|
680
|
+
return line.slice(0, end);
|
|
681
|
+
}
|
|
682
|
+
function compactUI(tree) {
|
|
683
|
+
if (!tree)
|
|
684
|
+
return "";
|
|
685
|
+
const lines = tree.split("\n");
|
|
686
|
+
const result = [];
|
|
687
|
+
const repeatTracker = /* @__PURE__ */ new Map();
|
|
688
|
+
for (let i = 0; i < lines.length; i++) {
|
|
689
|
+
const line = lines[i];
|
|
690
|
+
const trimmed = line.trimStart();
|
|
691
|
+
if (!trimmed)
|
|
692
|
+
continue;
|
|
693
|
+
const indent = line.length - trimmed.length;
|
|
694
|
+
const depth = Math.floor(indent / 2);
|
|
695
|
+
if (depth > MAX_UI_DEPTH)
|
|
696
|
+
continue;
|
|
697
|
+
const componentName = nameOf(trimmed);
|
|
698
|
+
if (UI_PRIMITIVES.has(componentName))
|
|
699
|
+
continue;
|
|
700
|
+
const key = `${depth}:${componentName}`;
|
|
701
|
+
const tracker = repeatTracker.get(key);
|
|
702
|
+
if (tracker && i - tracker.lastIndex <= 2) {
|
|
703
|
+
tracker.count++;
|
|
704
|
+
tracker.lastIndex = i;
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
for (const [k, t] of repeatTracker) {
|
|
708
|
+
if (t.count > 1) {
|
|
709
|
+
const d = Number.parseInt(k.split(":")[0]);
|
|
710
|
+
const name = k.split(":").slice(1).join(":");
|
|
711
|
+
result.push(`${" ".repeat(d)}${name} \xD7${t.count}`);
|
|
712
|
+
}
|
|
713
|
+
if (t.count > 1 || i - t.lastIndex > 2) {
|
|
714
|
+
repeatTracker.delete(k);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
repeatTracker.set(key, { count: 1, lastIndex: i });
|
|
718
|
+
result.push(line);
|
|
719
|
+
}
|
|
720
|
+
for (const [k, t] of repeatTracker) {
|
|
721
|
+
if (t.count > 1) {
|
|
722
|
+
const d = Number.parseInt(k.split(":")[0]);
|
|
723
|
+
const name = k.split(":").slice(1).join(":");
|
|
724
|
+
result.push(`${" ".repeat(d)}${name} \xD7${t.count}`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return result.join("\n");
|
|
728
|
+
}
|
|
729
|
+
var NOISE_SUBSTRINGS = [
|
|
730
|
+
"[hmr]",
|
|
731
|
+
"[vite]",
|
|
732
|
+
"hot module",
|
|
733
|
+
"react-devtools",
|
|
734
|
+
"download the react devtools",
|
|
735
|
+
"warning: react does not recognize",
|
|
736
|
+
"source map",
|
|
737
|
+
"favicon.ico",
|
|
738
|
+
"webpack"
|
|
739
|
+
];
|
|
740
|
+
function compactConsole(logs) {
|
|
741
|
+
if (!logs.length)
|
|
742
|
+
return "";
|
|
743
|
+
const filtered = logs.filter((l) => {
|
|
744
|
+
const lower = l.text.toLowerCase();
|
|
745
|
+
return !NOISE_SUBSTRINGS.some((s) => lower.includes(s));
|
|
746
|
+
});
|
|
747
|
+
if (!filtered.length)
|
|
748
|
+
return "";
|
|
749
|
+
const deduped = [];
|
|
750
|
+
for (const log of filtered) {
|
|
751
|
+
const last = deduped[deduped.length - 1];
|
|
752
|
+
if (last && last.entry.text === log.text && last.entry.level === log.level) {
|
|
753
|
+
last.count++;
|
|
754
|
+
} else {
|
|
755
|
+
deduped.push({ entry: log, count: 1 });
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const errors = deduped.filter((d) => d.entry.level === "error");
|
|
759
|
+
const warns = deduped.filter((d) => d.entry.level === "warn");
|
|
760
|
+
const rest = deduped.filter((d) => d.entry.level !== "error" && d.entry.level !== "warn");
|
|
761
|
+
const recentRest = rest.slice(-10);
|
|
762
|
+
const lines = [];
|
|
763
|
+
for (const group of [...errors, ...warns, ...recentRest]) {
|
|
764
|
+
const prefix = `[${group.entry.level}]`;
|
|
765
|
+
const count = group.count > 1 ? ` \xD7${group.count}` : "";
|
|
766
|
+
const source = group.entry.source ? ` (${group.entry.source})` : "";
|
|
767
|
+
const text = truncate(group.entry.text, 200);
|
|
768
|
+
lines.push(`${prefix}${count} ${text}${source}`);
|
|
769
|
+
}
|
|
770
|
+
return lines.join("\n");
|
|
771
|
+
}
|
|
772
|
+
function compactNetwork(requests) {
|
|
773
|
+
if (!requests.length)
|
|
774
|
+
return "";
|
|
775
|
+
const relevant = requests.filter(
|
|
776
|
+
(r) => r.resourceType === "fetch" || r.resourceType === "xhr" || r.resourceType === "websocket" || r.resourceType === "eventsource" || isApiUrl(r.url)
|
|
777
|
+
);
|
|
778
|
+
if (!relevant.length)
|
|
779
|
+
return "";
|
|
780
|
+
const lines = [];
|
|
781
|
+
for (const req of relevant) {
|
|
782
|
+
const duration = req.duration > 0 ? ` ${formatDuration(req.duration)}` : "";
|
|
783
|
+
const slow = req.duration >= SLOW_THRESHOLD ? " [SLOW]" : "";
|
|
784
|
+
const url = compactUrl(req.url, 50);
|
|
785
|
+
const headers = diagnosticHeaders(req);
|
|
786
|
+
if (req.failed || req.status >= 400) {
|
|
787
|
+
const body = req.responseBody ? ` "${truncate(req.responseBody, 100)}"` : "";
|
|
788
|
+
const failure = req.failureText ? ` (${req.failureText})` : "";
|
|
789
|
+
lines.push(`${req.method} ${url} ${req.status}${failure}${body}${headers}${duration}${slow}`);
|
|
790
|
+
} else {
|
|
791
|
+
lines.push(`${req.method} ${url} ${req.status}${headers}${duration}${slow}`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return lines.join("\n");
|
|
795
|
+
}
|
|
796
|
+
function isApiUrl(url) {
|
|
797
|
+
try {
|
|
798
|
+
const u = new URL(url);
|
|
799
|
+
return u.pathname.startsWith("/api") || u.pathname.includes("/graphql");
|
|
800
|
+
} catch {
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
var DIAGNOSTIC_HEADERS = ["content-type", "x-error", "www-authenticate", "access-control-allow-origin"];
|
|
805
|
+
function diagnosticHeaders(req) {
|
|
806
|
+
const h = req.responseHeaders;
|
|
807
|
+
if (!h)
|
|
808
|
+
return "";
|
|
809
|
+
const parts = [];
|
|
810
|
+
for (const key of DIAGNOSTIC_HEADERS) {
|
|
811
|
+
const val = h[key];
|
|
812
|
+
if (!val)
|
|
813
|
+
continue;
|
|
814
|
+
if (key === "content-type" && req.status < 400 && val.includes("application/json"))
|
|
815
|
+
continue;
|
|
816
|
+
parts.push(`${key}: ${truncate(val, 60)}`);
|
|
817
|
+
}
|
|
818
|
+
if (!parts.length)
|
|
819
|
+
return "";
|
|
820
|
+
return ` [${parts.join(", ")}]`;
|
|
821
|
+
}
|
|
822
|
+
function formatDuration(ms) {
|
|
823
|
+
return ms >= 1e3 ? `${(ms / 1e3).toFixed(1)}s` : `${Math.round(ms)}ms`;
|
|
824
|
+
}
|
|
825
|
+
function compactErrors(errors) {
|
|
826
|
+
if (!errors.length)
|
|
827
|
+
return "";
|
|
828
|
+
const seen = /* @__PURE__ */ new Map();
|
|
829
|
+
for (const err of errors) {
|
|
830
|
+
if (!seen.has(err.message)) {
|
|
831
|
+
seen.set(err.message, err);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
const lines = [];
|
|
835
|
+
for (const err of seen.values()) {
|
|
836
|
+
lines.push(err.message);
|
|
837
|
+
if (err.stack) {
|
|
838
|
+
for (const frame of appStackFrames(err.stack, 5))
|
|
839
|
+
lines.push(` ${frame}`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return lines.join("\n");
|
|
843
|
+
}
|
|
844
|
+
function compactState(state) {
|
|
845
|
+
if (!state || !Object.keys(state).length)
|
|
846
|
+
return "";
|
|
847
|
+
const lines = [];
|
|
848
|
+
for (const [name, value] of Object.entries(state)) {
|
|
849
|
+
lines.push(`${name}:`);
|
|
850
|
+
if (typeof value === "object" && value !== null) {
|
|
851
|
+
for (const [k, v] of Object.entries(value)) {
|
|
852
|
+
lines.push(` ${k}: ${truncate(formatValue(v), 120)}`);
|
|
853
|
+
}
|
|
854
|
+
} else {
|
|
855
|
+
lines.push(` ${String(value)}`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return lines.join("\n");
|
|
859
|
+
}
|
|
860
|
+
function compact(raw) {
|
|
861
|
+
return {
|
|
862
|
+
url: raw.url,
|
|
863
|
+
ui: compactUI(raw.ui),
|
|
864
|
+
console: compactConsole(raw.console),
|
|
865
|
+
network: compactNetwork(raw.network),
|
|
866
|
+
errors: compactErrors(raw.errors),
|
|
867
|
+
state: compactState(raw.state),
|
|
868
|
+
performance: raw.performance ? detailPerformance(raw.performance) : void 0,
|
|
869
|
+
timestamp: raw.timestamp,
|
|
870
|
+
counts: {
|
|
871
|
+
console: raw.console.length,
|
|
872
|
+
network: raw.network.length,
|
|
873
|
+
errors: raw.errors.length,
|
|
874
|
+
state: Object.keys(raw.state).length
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// src/ui/core/detail.ts
|
|
880
|
+
var headerLine = (k, v) => ` ${k}: ${isSecretKey(k) ? redactSecretValue(v) : v}`;
|
|
881
|
+
function detail(raw, section, index, full) {
|
|
882
|
+
switch (section) {
|
|
883
|
+
case "ui":
|
|
884
|
+
return full ? raw.ui || null : compactUI(raw.ui) || null;
|
|
885
|
+
case "console":
|
|
886
|
+
return detailConsole(raw.console, index, full);
|
|
887
|
+
case "network":
|
|
888
|
+
return detailNetwork(raw.network, index, full);
|
|
889
|
+
case "errors":
|
|
890
|
+
return detailError(raw.errors, index, full);
|
|
891
|
+
case "state":
|
|
892
|
+
return detailState(raw.state, index, full);
|
|
893
|
+
case "profile":
|
|
894
|
+
return raw.performance ? detailPerformance(raw.performance) : "(no perf data \u2014 is the tab foreground?)";
|
|
895
|
+
default:
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function detailConsole(logs, index, full) {
|
|
900
|
+
if (index === void 0) {
|
|
901
|
+
if (!logs.length)
|
|
902
|
+
return "(empty)";
|
|
903
|
+
return logs.map((log2, i2) => `[${i2}] [${log2.level}] ${truncate(log2.text, 120)}`).join("\n");
|
|
904
|
+
}
|
|
905
|
+
const i = Number.parseInt(index);
|
|
906
|
+
if (Number.isNaN(i) || i < 0 || i >= logs.length)
|
|
907
|
+
return null;
|
|
908
|
+
const log = logs[i];
|
|
909
|
+
if (full) {
|
|
910
|
+
const parts = [`[${log.level}] ${log.text}`];
|
|
911
|
+
if (log.timestamp)
|
|
912
|
+
parts.push(`timestamp: ${new Date(log.timestamp).toISOString()}`);
|
|
913
|
+
if (log.source)
|
|
914
|
+
parts.push(`source: ${log.source}`);
|
|
915
|
+
return parts.join("\n");
|
|
916
|
+
}
|
|
917
|
+
return `[${log.level}] ${truncate(log.text, 200)}`;
|
|
918
|
+
}
|
|
919
|
+
function detailNetwork(requests, index, full) {
|
|
920
|
+
if (index === void 0) {
|
|
921
|
+
if (!requests.length)
|
|
922
|
+
return "(empty)";
|
|
923
|
+
return requests.map((req2, i2) => `[${i2}] ${req2.method} ${req2.status} ${truncate(req2.url, 80)} ${req2.duration}ms${req2.failed ? " FAILED" : ""}`).join("\n");
|
|
924
|
+
}
|
|
925
|
+
const i = Number.parseInt(index);
|
|
926
|
+
if (Number.isNaN(i) || i < 0 || i >= requests.length)
|
|
927
|
+
return null;
|
|
928
|
+
const req = requests[i];
|
|
929
|
+
const lines = [
|
|
930
|
+
`${req.method} ${req.url}`,
|
|
931
|
+
`status: ${req.status}`,
|
|
932
|
+
`duration: ${req.duration}ms`,
|
|
933
|
+
`type: ${req.resourceType}`
|
|
934
|
+
];
|
|
935
|
+
if (req.failed)
|
|
936
|
+
lines.push(`failed: ${req.failureText || "true"}`);
|
|
937
|
+
if (full) {
|
|
938
|
+
if (req.requestHeaders && Object.keys(req.requestHeaders).length) {
|
|
939
|
+
lines.push("request-headers:");
|
|
940
|
+
for (const [k, v] of Object.entries(req.requestHeaders)) lines.push(headerLine(k, v));
|
|
941
|
+
}
|
|
942
|
+
if (req.requestBody)
|
|
943
|
+
lines.push(`request-body:
|
|
944
|
+
${req.requestBody}`);
|
|
945
|
+
if (req.responseHeaders && Object.keys(req.responseHeaders).length) {
|
|
946
|
+
lines.push("response-headers:");
|
|
947
|
+
for (const [k, v] of Object.entries(req.responseHeaders)) lines.push(headerLine(k, v));
|
|
948
|
+
}
|
|
949
|
+
if (req.responseBody)
|
|
950
|
+
lines.push(`response-body:
|
|
951
|
+
${req.responseBody}`);
|
|
952
|
+
} else {
|
|
953
|
+
if (req.requestBody) {
|
|
954
|
+
lines.push(`request-body: ${byteSize(req.requestBody)}`);
|
|
955
|
+
if (req.requestSample) {
|
|
956
|
+
const schema = jsonSchema(req.requestSample);
|
|
957
|
+
if (schema)
|
|
958
|
+
lines.push(schema);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (req.responseBody) {
|
|
962
|
+
if (req.status >= 400) {
|
|
963
|
+
lines.push(`response-body: ${byteSize(req.responseBody)} "${truncate(req.responseBody, 100)}"`);
|
|
964
|
+
} else {
|
|
965
|
+
if (req.responseSample) {
|
|
966
|
+
lines.push(`response-body: ${byteSize(req.responseBody)}`);
|
|
967
|
+
const schema = jsonSchema(req.responseSample);
|
|
968
|
+
if (schema)
|
|
969
|
+
lines.push(schema);
|
|
970
|
+
} else {
|
|
971
|
+
lines.push(`response-body: ${byteSize(req.responseBody)} ${truncate(req.responseBody, 100)}`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return lines.join("\n");
|
|
977
|
+
}
|
|
978
|
+
function detailError(errors, index, full) {
|
|
979
|
+
if (index === void 0) {
|
|
980
|
+
if (!errors.length)
|
|
981
|
+
return "(empty)";
|
|
982
|
+
return errors.map((err2, i2) => `[${i2}] ${truncate(err2.message, 120)}`).join("\n");
|
|
983
|
+
}
|
|
984
|
+
const i = Number.parseInt(index);
|
|
985
|
+
if (Number.isNaN(i) || i < 0 || i >= errors.length)
|
|
986
|
+
return null;
|
|
987
|
+
const err = errors[i];
|
|
988
|
+
if (full) {
|
|
989
|
+
const lines2 = [err.message];
|
|
990
|
+
if (err.stack)
|
|
991
|
+
lines2.push(err.stack);
|
|
992
|
+
if (err.source)
|
|
993
|
+
lines2.push(`source: ${err.source}`);
|
|
994
|
+
if (err.line != null)
|
|
995
|
+
lines2.push(`location: ${err.source || ""}:${err.line}:${err.column ?? 0}`);
|
|
996
|
+
return lines2.join("\n");
|
|
997
|
+
}
|
|
998
|
+
const lines = [err.message];
|
|
999
|
+
if (err.stack) {
|
|
1000
|
+
const all = appStackFrames(err.stack, Infinity);
|
|
1001
|
+
lines.push(...all.slice(0, 3));
|
|
1002
|
+
if (all.length > 3)
|
|
1003
|
+
lines.push(` ... ${all.length - 3} more app frames`);
|
|
1004
|
+
}
|
|
1005
|
+
if (err.line != null)
|
|
1006
|
+
lines.push(`location: ${err.source || ""}:${err.line}:${err.column ?? 0}`);
|
|
1007
|
+
return lines.join("\n");
|
|
1008
|
+
}
|
|
1009
|
+
function detailState(state, name, full) {
|
|
1010
|
+
if (!name) {
|
|
1011
|
+
const keys = Object.keys(state);
|
|
1012
|
+
if (!keys.length)
|
|
1013
|
+
return "(empty)";
|
|
1014
|
+
return keys.map((k) => `${k}: ${formatSummaryValue(state[k])}`).join("\n");
|
|
1015
|
+
}
|
|
1016
|
+
if (!(name in state))
|
|
1017
|
+
return null;
|
|
1018
|
+
const value = state[name];
|
|
1019
|
+
if (full) {
|
|
1020
|
+
try {
|
|
1021
|
+
return JSON.stringify(value, null, 2) ?? formatValue(value);
|
|
1022
|
+
} catch {
|
|
1023
|
+
return formatValue(value);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
if (typeof value !== "object" || value === null)
|
|
1027
|
+
return `${name}: ${typeof value}`;
|
|
1028
|
+
const lines = [];
|
|
1029
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1030
|
+
lines.push(`${k}: ${formatSummaryValue(v)}`);
|
|
1031
|
+
}
|
|
1032
|
+
return lines.join("\n");
|
|
1033
|
+
}
|
|
1034
|
+
function isArraySentinel(v) {
|
|
1035
|
+
if (!v.startsWith("Array(") || !v.endsWith(")"))
|
|
1036
|
+
return false;
|
|
1037
|
+
const digits = v.slice(6, -1);
|
|
1038
|
+
return digits.length > 0 && [...digits].every((c) => c >= "0" && c <= "9");
|
|
1039
|
+
}
|
|
1040
|
+
function formatSummaryValue(v) {
|
|
1041
|
+
if (typeof v === "string" && isArraySentinel(v))
|
|
1042
|
+
return v;
|
|
1043
|
+
return truncate(formatValue(v), 80);
|
|
1044
|
+
}
|
|
1045
|
+
function jsonSchema(sample) {
|
|
1046
|
+
try {
|
|
1047
|
+
return schemaOf(JSON.parse(sample), 0);
|
|
1048
|
+
} catch {
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
function schemaOf(v, d) {
|
|
1053
|
+
if (v === null)
|
|
1054
|
+
return "null";
|
|
1055
|
+
if (typeof v === "string")
|
|
1056
|
+
return "string";
|
|
1057
|
+
if (typeof v === "number")
|
|
1058
|
+
return "number";
|
|
1059
|
+
if (typeof v === "boolean")
|
|
1060
|
+
return "boolean";
|
|
1061
|
+
if (Array.isArray(v)) {
|
|
1062
|
+
if (!v.length)
|
|
1063
|
+
return "[]";
|
|
1064
|
+
if (d >= 3)
|
|
1065
|
+
return "[\u2026]";
|
|
1066
|
+
return `${schemaOf(v[0], d + 1)}[]`;
|
|
1067
|
+
}
|
|
1068
|
+
if (typeof v === "object") {
|
|
1069
|
+
if (d >= 3)
|
|
1070
|
+
return "{\u2026}";
|
|
1071
|
+
const entries = Object.entries(v);
|
|
1072
|
+
if (!entries.length)
|
|
1073
|
+
return "{}";
|
|
1074
|
+
const max = d === 0 ? 12 : 6;
|
|
1075
|
+
const fields = entries.slice(0, max).map(([k, val]) => `${k}: ${schemaOf(val, d + 1)}`);
|
|
1076
|
+
if (entries.length > max)
|
|
1077
|
+
fields.push(`\u2026 ${entries.length - max} more`);
|
|
1078
|
+
return `{ ${fields.join(", ")} }`;
|
|
1079
|
+
}
|
|
1080
|
+
return typeof v;
|
|
1081
|
+
}
|
|
1082
|
+
function byteSize(s) {
|
|
1083
|
+
const bytes = new TextEncoder().encode(s).length;
|
|
1084
|
+
if (bytes < 1024)
|
|
1085
|
+
return `${bytes}B`;
|
|
1086
|
+
if (bytes < 1024 * 1024)
|
|
1087
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1088
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// src/ui/core/diff.ts
|
|
1092
|
+
var GONE_FRAC = 0.15;
|
|
1093
|
+
function regionsChanged(before, after) {
|
|
1094
|
+
if (!before.length || !after.length)
|
|
1095
|
+
return false;
|
|
1096
|
+
const afterKeys = new Set(after.map((r) => r.key));
|
|
1097
|
+
const beforeKeys = new Set(before.map((r) => r.key));
|
|
1098
|
+
const gone = before.filter((r) => !afterKeys.has(r.key)).length;
|
|
1099
|
+
const appeared = after.filter((r) => !beforeKeys.has(r.key)).length;
|
|
1100
|
+
return gone >= before.length * GONE_FRAC && appeared > 0;
|
|
1101
|
+
}
|
|
1102
|
+
var POPOVER_MIN_CHILDREN = 2;
|
|
1103
|
+
function popoverChange(before, after) {
|
|
1104
|
+
const beforeKeys = new Set(before.map((r) => r.key));
|
|
1105
|
+
const afterKeys = new Set(after.map((r) => r.key));
|
|
1106
|
+
const appeared = after.filter((r) => !beforeKeys.has(r.key));
|
|
1107
|
+
const gone = before.filter((r) => !afterKeys.has(r.key));
|
|
1108
|
+
const top = (rs) => rs.reduce((m, r) => !m || childCount(r) > childCount(m) ? r : m, void 0);
|
|
1109
|
+
const add = top(appeared);
|
|
1110
|
+
if (appeared.length > gone.length && add && childCount(add) >= POPOVER_MIN_CHILDREN)
|
|
1111
|
+
return `panel: appeared (${add.label})`;
|
|
1112
|
+
const rm = top(gone);
|
|
1113
|
+
if (gone.length > appeared.length && rm && childCount(rm) >= POPOVER_MIN_CHILDREN)
|
|
1114
|
+
return `panel: dismissed (${rm.label})`;
|
|
1115
|
+
return void 0;
|
|
1116
|
+
}
|
|
1117
|
+
function childCount(r) {
|
|
1118
|
+
const i = r.key.lastIndexOf("#");
|
|
1119
|
+
return i < 0 ? 0 : Number(r.key.slice(i + 1)) || 0;
|
|
1120
|
+
}
|
|
1121
|
+
function diffScreen(before, after, newErrors, newExceptions, newFailedRequests) {
|
|
1122
|
+
const lines = [];
|
|
1123
|
+
let bodyOrModalNamed = false;
|
|
1124
|
+
if (after.view !== before.view) {
|
|
1125
|
+
lines.push(`view: ${before.view} \u2192 ${after.view}`);
|
|
1126
|
+
bodyOrModalNamed = true;
|
|
1127
|
+
} else if (before.view === "(unknown)" && after.modal === "none" && regionsChanged(before.regions ?? [], after.regions ?? [])) {
|
|
1128
|
+
lines.push("view: changed (DOM \u533A\u57DF\u6307\u7EB9)");
|
|
1129
|
+
bodyOrModalNamed = true;
|
|
1130
|
+
}
|
|
1131
|
+
if (after.modal !== before.modal) {
|
|
1132
|
+
if (after.modal === "none")
|
|
1133
|
+
lines.push(`modal: closed (${before.modal})`);
|
|
1134
|
+
else if (before.modal === "none")
|
|
1135
|
+
lines.push(`modal: opened ${after.modal}`);
|
|
1136
|
+
else
|
|
1137
|
+
lines.push(`modal: ${before.modal} \u2192 ${after.modal}`);
|
|
1138
|
+
bodyOrModalNamed = true;
|
|
1139
|
+
}
|
|
1140
|
+
if (!bodyOrModalNamed) {
|
|
1141
|
+
const pop = popoverChange(before.regions ?? [], after.regions ?? []);
|
|
1142
|
+
if (pop)
|
|
1143
|
+
lines.push(pop);
|
|
1144
|
+
}
|
|
1145
|
+
if (after.focus !== before.focus)
|
|
1146
|
+
lines.push(`focus: ${after.focus}`);
|
|
1147
|
+
const beforeDomain = before.domain ?? {};
|
|
1148
|
+
const afterDomain = after.domain ?? {};
|
|
1149
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(beforeDomain), ...Object.keys(afterDomain)])) {
|
|
1150
|
+
const b = redactDomain(key, beforeDomain[key]);
|
|
1151
|
+
const a = redactDomain(key, afterDomain[key]);
|
|
1152
|
+
if (b !== a)
|
|
1153
|
+
lines.push(`${key}: ${b} \u2192 ${a}`);
|
|
1154
|
+
}
|
|
1155
|
+
for (const e of newErrors)
|
|
1156
|
+
lines.push(`+error: ${e.text}`);
|
|
1157
|
+
for (const e of newExceptions)
|
|
1158
|
+
lines.push(`+exception: ${e.message}`);
|
|
1159
|
+
for (const r of newFailedRequests)
|
|
1160
|
+
lines.push(`+failed: ${r.method} ${r.url} \u2192 ${r.status || "failed"}`);
|
|
1161
|
+
return lines;
|
|
1162
|
+
}
|
|
1163
|
+
function stringifyDomain(v) {
|
|
1164
|
+
if (v === null || v === void 0)
|
|
1165
|
+
return String(v);
|
|
1166
|
+
if (typeof v === "object")
|
|
1167
|
+
return JSON.stringify(v).slice(0, 80);
|
|
1168
|
+
return String(v).slice(0, 80);
|
|
1169
|
+
}
|
|
1170
|
+
function redactDomain(key, v) {
|
|
1171
|
+
const s = stringifyDomain(v);
|
|
1172
|
+
if (!isSecretKey(key))
|
|
1173
|
+
return s;
|
|
1174
|
+
const raw = v === null || v === void 0 ? "" : String(v);
|
|
1175
|
+
return raw ? redactSecretValue(raw) : s;
|
|
1176
|
+
}
|
|
1177
|
+
function diffState(prev, curr) {
|
|
1178
|
+
if (!prev) {
|
|
1179
|
+
return {
|
|
1180
|
+
newErrors: curr.console.filter((l) => l.level === "error"),
|
|
1181
|
+
newExceptions: [...curr.errors],
|
|
1182
|
+
newFailedRequests: curr.network.filter((r) => r.status >= 400 || r.failed),
|
|
1183
|
+
uiGone: false,
|
|
1184
|
+
clean: curr.console.filter((l) => l.level === "error").length === 0 && curr.errors.length === 0 && curr.network.filter((r) => r.status >= 400 || r.failed).length === 0
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
const prevErrorTexts = new Set(prev.console.filter((l) => l.level === "error").map((l) => l.text));
|
|
1188
|
+
const newErrors = curr.console.filter((l) => l.level === "error" && !prevErrorTexts.has(l.text));
|
|
1189
|
+
const prevExceptionMsgs = new Set(prev.errors.map((e) => e.message));
|
|
1190
|
+
const newExceptions = curr.errors.filter((e) => !prevExceptionMsgs.has(e.message));
|
|
1191
|
+
const prevFailedKeys = new Set(
|
|
1192
|
+
prev.network.filter((r) => r.status >= 400 || r.failed).map((r) => `${r.method}|${r.url}|${r.status}`)
|
|
1193
|
+
);
|
|
1194
|
+
const newFailedRequests = curr.network.filter((r) => r.status >= 400 || r.failed).filter((r) => !prevFailedKeys.has(`${r.method}|${r.url}|${r.status}`));
|
|
1195
|
+
const uiGone = prev.ui.trim().length > 0 && curr.ui.trim().length === 0;
|
|
1196
|
+
return {
|
|
1197
|
+
newErrors,
|
|
1198
|
+
newExceptions,
|
|
1199
|
+
newFailedRequests,
|
|
1200
|
+
uiGone,
|
|
1201
|
+
clean: newErrors.length === 0 && newExceptions.length === 0 && newFailedRequests.length === 0 && !uiGone
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/ui/core/emit.ts
|
|
1206
|
+
import pc from "picocolors";
|
|
1207
|
+
var SECTIONS = ["ui", "console", "network", "errors", "state", "performance"];
|
|
1208
|
+
var COUNTED_SECTIONS = {
|
|
1209
|
+
console: "console",
|
|
1210
|
+
network: "network",
|
|
1211
|
+
errors: "errors",
|
|
1212
|
+
state: "state"
|
|
1213
|
+
};
|
|
1214
|
+
function emit(state) {
|
|
1215
|
+
const sections = [];
|
|
1216
|
+
for (const key of SECTIONS) {
|
|
1217
|
+
if (state[key]) {
|
|
1218
|
+
const countKey = COUNTED_SECTIONS[key];
|
|
1219
|
+
const count = countKey ? state.counts?.[countKey] ?? 0 : 0;
|
|
1220
|
+
const attr = count ? ` count="${count}"` : "";
|
|
1221
|
+
sections.push(`<${key}${attr}>
|
|
1222
|
+
${state[key]}
|
|
1223
|
+
</${key}>`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
if (!sections.length) {
|
|
1227
|
+
sections.push("<empty/>");
|
|
1228
|
+
}
|
|
1229
|
+
return `<aihand url="${state.url}">
|
|
1230
|
+
|
|
1231
|
+
${sections.join("\n\n")}
|
|
1232
|
+
|
|
1233
|
+
</aihand>
|
|
1234
|
+
|
|
1235
|
+
detail: GET /__aihand/{section}/{index}?full`;
|
|
1236
|
+
}
|
|
1237
|
+
function emitSummary(raw) {
|
|
1238
|
+
const consoleErrors = raw.console.filter((l) => l.level === "error");
|
|
1239
|
+
const consoleWarns = raw.console.filter((l) => l.level === "warn");
|
|
1240
|
+
const failedReqs = raw.network.filter((r) => r.status >= 400 || r.failed);
|
|
1241
|
+
const hasIssues = consoleErrors.length > 0 || raw.errors.length > 0 || failedReqs.length > 0;
|
|
1242
|
+
const lines = [];
|
|
1243
|
+
if (raw.ui.trim()) {
|
|
1244
|
+
lines.push(`ui: ${summarizeUI(raw.ui)}`);
|
|
1245
|
+
}
|
|
1246
|
+
if (raw.console.length) {
|
|
1247
|
+
if (consoleErrors.length || consoleWarns.length) {
|
|
1248
|
+
const parts = [];
|
|
1249
|
+
for (const e of consoleErrors) parts.push(` [error] ${truncate(e.text, 150)}`);
|
|
1250
|
+
for (const w of consoleWarns) parts.push(` [warn] ${truncate(w.text, 150)}`);
|
|
1251
|
+
const rest = raw.console.length - consoleErrors.length - consoleWarns.length;
|
|
1252
|
+
if (rest > 0)
|
|
1253
|
+
parts.push(` \u2026 ${rest} more`);
|
|
1254
|
+
lines.push(`console (${raw.console.length}):`);
|
|
1255
|
+
lines.push(...parts);
|
|
1256
|
+
} else {
|
|
1257
|
+
lines.push(`console: ${raw.console.length} logs`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
if (raw.network.length) {
|
|
1261
|
+
if (failedReqs.length) {
|
|
1262
|
+
lines.push(`network (${raw.network.length}):`);
|
|
1263
|
+
for (const r of failedReqs) {
|
|
1264
|
+
const body = r.responseBody ? ` "${truncate(r.responseBody, 80)}"` : "";
|
|
1265
|
+
lines.push(` ${r.method} ${compactUrl(r.url)} ${r.status}${body}`);
|
|
1266
|
+
}
|
|
1267
|
+
const ok = raw.network.length - failedReqs.length;
|
|
1268
|
+
if (ok > 0)
|
|
1269
|
+
lines.push(` \u2026 ${ok} ok`);
|
|
1270
|
+
} else {
|
|
1271
|
+
lines.push(`network: ${raw.network.length} ok`);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
if (raw.errors.length) {
|
|
1275
|
+
lines.push(`errors (${raw.errors.length}):`);
|
|
1276
|
+
for (const e of raw.errors) lines.push(` ${truncate(e.message, 150)}`);
|
|
1277
|
+
}
|
|
1278
|
+
if (raw.performance) {
|
|
1279
|
+
const all = raw.performance.buckets.find((b) => b.name === "__all__");
|
|
1280
|
+
if (all) {
|
|
1281
|
+
const droppedPct = all.frames.total > 0 ? all.frames.long / all.frames.total * 100 : 0;
|
|
1282
|
+
if (droppedPct > 10) {
|
|
1283
|
+
lines.push(compactPerformance(raw.performance));
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
const storeNames = Object.keys(raw.state);
|
|
1288
|
+
if (storeNames.length) {
|
|
1289
|
+
const parts = storeNames.map((n) => {
|
|
1290
|
+
const v = raw.state[n];
|
|
1291
|
+
const keys = typeof v === "object" && v !== null ? Object.keys(v).length : 0;
|
|
1292
|
+
return keys > 0 ? `${n}(${keys})` : n;
|
|
1293
|
+
});
|
|
1294
|
+
lines.push(`state: ${parts.join(", ")}`);
|
|
1295
|
+
}
|
|
1296
|
+
if (!lines.length)
|
|
1297
|
+
return "<aihand>empty</aihand>";
|
|
1298
|
+
const status = hasIssues ? `${consoleErrors.length + raw.errors.length + failedReqs.length} issues` : "ok";
|
|
1299
|
+
return `<aihand url="${raw.url}" status="${status}">
|
|
1300
|
+
${lines.join("\n")}
|
|
1301
|
+
</aihand>${hasIssues ? "\n\ndetail: GET /__aihand/{section}/{index}" : ""}`;
|
|
1302
|
+
}
|
|
1303
|
+
function summarizeUI(tree) {
|
|
1304
|
+
const components = [];
|
|
1305
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1306
|
+
for (const line of tree.split("\n")) {
|
|
1307
|
+
const trimmed = line.trimStart();
|
|
1308
|
+
if (!trimmed)
|
|
1309
|
+
continue;
|
|
1310
|
+
const indent = line.length - trimmed.length;
|
|
1311
|
+
if (indent > 2)
|
|
1312
|
+
continue;
|
|
1313
|
+
const name = nameOf(trimmed);
|
|
1314
|
+
if (!name)
|
|
1315
|
+
continue;
|
|
1316
|
+
const focused = trimmed.includes("[focused]");
|
|
1317
|
+
const generating = trimmed.includes("[generating]");
|
|
1318
|
+
const loading = trimmed.includes("[loading]");
|
|
1319
|
+
const existing = counts.get(name) || 0;
|
|
1320
|
+
if (existing > 0) {
|
|
1321
|
+
counts.set(name, existing + 1);
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
counts.set(name, 1);
|
|
1325
|
+
let label = name;
|
|
1326
|
+
if (focused)
|
|
1327
|
+
label += "[focused]";
|
|
1328
|
+
if (generating)
|
|
1329
|
+
label += "[generating]";
|
|
1330
|
+
if (loading)
|
|
1331
|
+
label += "[loading]";
|
|
1332
|
+
components.push(label);
|
|
1333
|
+
}
|
|
1334
|
+
const result = components.map((c) => {
|
|
1335
|
+
const name = c.split("[")[0];
|
|
1336
|
+
const count = counts.get(name) || 1;
|
|
1337
|
+
return count > 1 ? `${c}(\xD7${count})` : c;
|
|
1338
|
+
});
|
|
1339
|
+
return result.join(", ") || "empty";
|
|
1340
|
+
}
|
|
1341
|
+
function emitCheck(result) {
|
|
1342
|
+
const lines = result.assertions.map(
|
|
1343
|
+
(a) => a.pass ? `\u2713 ${a.name}` : `\u2717 ${a.name}${a.detail ? `: ${a.detail}` : ""}`
|
|
1344
|
+
);
|
|
1345
|
+
return `<aihand-check pass="${result.pass}">
|
|
1346
|
+
${lines.join("\n")}
|
|
1347
|
+
</aihand-check>`;
|
|
1348
|
+
}
|
|
1349
|
+
function emitDiff(diff) {
|
|
1350
|
+
const issues = [];
|
|
1351
|
+
for (const e of diff.newErrors) issues.push(pc.red(` [error] ${e.text}`));
|
|
1352
|
+
for (const e of diff.newExceptions) issues.push(pc.red(` [exception] ${e.message}`));
|
|
1353
|
+
for (const r of diff.newFailedRequests) issues.push(pc.yellow(` [network] ${r.method} ${r.url} ${r.status}`));
|
|
1354
|
+
if (diff.uiGone)
|
|
1355
|
+
issues.push(pc.magenta(" [ui] component tree disappeared"));
|
|
1356
|
+
if (!issues.length)
|
|
1357
|
+
return "";
|
|
1358
|
+
const count = issues.length;
|
|
1359
|
+
return `${pc.bold("[aihand]")} ${pc.red(`\u2717 ${count} issue${count > 1 ? "s" : ""} after HMR`)}
|
|
1360
|
+
${issues.join("\n")}`;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// src/ui/server/help-text.ts
|
|
1364
|
+
function helpText(port) {
|
|
1365
|
+
const base = `http://localhost:${port}/__aihand`;
|
|
1366
|
+
return `
|
|
1367
|
+
# aihand \u2014 Runtime Browser Inspector
|
|
1368
|
+
|
|
1369
|
+
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.
|
|
1370
|
+
|
|
1371
|
+
## Read state \u2014 cheapest first
|
|
1372
|
+
|
|
1373
|
+
\`\`\`bash
|
|
1374
|
+
curl ${base}/screen # semantic layout tree (indent=nesting, \u5DE6/\u4E2D/\u53F3) + {view, modal, focus, knobs} \u2014 START HERE (returns token: tN)
|
|
1375
|
+
curl '${base}/screen?form=visual' # same screen as an ASCII art sketch instead of the layout tree
|
|
1376
|
+
curl '${base}/screen?form=knobs' # only the control surface: clickable knobs + their store morphisms, grouped by column
|
|
1377
|
+
curl '${base}/screen?since=tN' # only what moved since that token (view/modal/focus + new errors), not a full snapshot
|
|
1378
|
+
curl ${base}/ui # React component tree \u2014 deep-dive when /screen isn't enough
|
|
1379
|
+
curl '${base}/dom?scope=ChatInput' # semantic DOM scoped to a component \u2014 UI as text, src locations
|
|
1380
|
+
curl ${base} # high-density summary (ok sections \u2192 1 line, issues \u2192 expanded)
|
|
1381
|
+
curl ${base}?full # full dump: UI tree + console + network + errors + state
|
|
1382
|
+
curl ${base}/check # pass/fail health check \u2014 use after code changes
|
|
1383
|
+
curl ${base}/console # console logs (errors, warnings, info)
|
|
1384
|
+
curl ${base}/network # fetch/XHR requests with status and timing
|
|
1385
|
+
curl ${base}/errors # uncaught errors and unhandled rejections
|
|
1386
|
+
curl ${base}/state # registered store snapshots (overview)
|
|
1387
|
+
curl ${base}/state/imStore # drill into one store (domain)
|
|
1388
|
+
curl '${base}/state?path=imStore.conversations.0' # expand a nested value the overview collapsed to Array(N) (point)
|
|
1389
|
+
curl ${base}/tabs # list live tabs (id, visible/background, title) for ?tab= addressing
|
|
1390
|
+
curl ${base}/timeline # interleaved action stream across all tabs (who clicked what, in order)
|
|
1391
|
+
curl '${base}/query?sel=[role=menuitem]' # this selector right now: count + each element's text/visible/attrs
|
|
1392
|
+
curl ${base}/profile # performance profiler: which component/function is burning frames (+ source lines with AIPEEK_LINES=1)
|
|
1393
|
+
curl ${base}/profile/reset # clear the profiler window, then reproduce the interaction
|
|
1394
|
+
curl ${base}/profile/diff # closed loop: 1st call marks baseline, fix+reproduce, 2nd call \u2192 IMPROVED/REGRESSED verdict
|
|
1395
|
+
\`\`\`
|
|
1396
|
+
|
|
1397
|
+
\`/profile\` is a verdict, not a flame graph \u2014 read it, fix, re-read, don't eyeball frames.
|
|
1398
|
+
Three tiers, escalate only as needed:
|
|
1399
|
+
|
|
1400
|
+
\`\`\`bash
|
|
1401
|
+
# 1. Diagnose \u2014 one read names the component/function burning frames (self-time \xD7 render count)
|
|
1402
|
+
curl ${base}/profile
|
|
1403
|
+
|
|
1404
|
+
# 2. Closed-loop A/B \u2014 proves a fix worked instead of guessing
|
|
1405
|
+
curl ${base}/profile/reset # clear the window
|
|
1406
|
+
# \u2026reproduce the slow interaction (scroll, stream, type)\u2026
|
|
1407
|
+
curl ${base}/profile # 1st call = baseline snapshot
|
|
1408
|
+
# \u2026edit code + reproduce the SAME interaction\u2026
|
|
1409
|
+
curl ${base}/profile/diff # 2nd call \u2192 IMPROVED / REGRESSED / NO DATA verdict
|
|
1410
|
+
|
|
1411
|
+
# 3. Line-level \u2014 break a hot component down to the exact source line
|
|
1412
|
+
AIPEEK_LINES=1 <your dev command> # opt-in: line instrumentation taxes every render, off by default
|
|
1413
|
+
curl ${base}/profile
|
|
1414
|
+
\`\`\`
|
|
1415
|
+
|
|
1416
|
+
Profiling is the ONE read that needs the tab FOREGROUND + FOCUSED \u2014 the OS throttles rAF
|
|
1417
|
+
for backgrounded/unfocused windows, so timing there is a lie. The profiler refuses to launder
|
|
1418
|
+
that lie: a throttled window reads \`Frames: none sampled\` and a window that didn't reproduce
|
|
1419
|
+
the workload reads \`NO DATA\`, never a fabricated \`0.0% dropped\` / \`IMPROVED\`. A real verdict
|
|
1420
|
+
requires: tab in front, \`/profile/reset\`, then repeat the exact interaction. (\`/screen\` and
|
|
1421
|
+
\`/dom\` work fine backgrounded \u2014 only profiling needs focus.)
|
|
1422
|
+
|
|
1423
|
+
\`/query\` is the read-side twin of click/fill's \`sel=\` \u2014 assert on a specific element
|
|
1424
|
+
(how many match, its text, \`data-state\`, \`aria-*\`/\`data-*\`, value, disabled) without \`/eval\`.
|
|
1425
|
+
Secret fields (password inputs, API-key/token fields) show \`\u2039redacted N chars\u203A\` instead of
|
|
1426
|
+
their value across \`/dom\`, \`/query\` and \`/screen\` \u2014 length stays visible, the secret doesn't.
|
|
1427
|
+
|
|
1428
|
+
\`/screen\` projects the whole UI to a few state variables \u2014 start there, not \`/ui\`. Append
|
|
1429
|
+
\`?full\` for untruncated output. Each read prints a \`token: tN\` line; pass it back as
|
|
1430
|
+
\`/screen?since=tN\` to get only the transition since that read (view/modal/focus + new
|
|
1431
|
+
errors/failed requests), \`(no state change)\` if nothing moved \u2014 the cheap "what changed
|
|
1432
|
+
after I acted" read, without re-paying for the unchanged 99%.
|
|
1433
|
+
|
|
1434
|
+
To inspect or edit a component, work top-down \u2014 the full DOM is huge, a scoped view is
|
|
1435
|
+
accurate: \`/screen\` or \`/ui\` to find the component, then \`/dom?scope=<Name>\` (matches the
|
|
1436
|
+
source path) or \`/dom?sel=<css>\` for just that subtree. Each line carries its source
|
|
1437
|
+
location (\`@File.tsx:line\`), so the DOM view tells you exactly where to edit.
|
|
1438
|
+
|
|
1439
|
+
## UI \u2192 code reason (reverse morphism)
|
|
1440
|
+
|
|
1441
|
+
\`\`\`bash
|
|
1442
|
+
curl '${base}/source?path=src/features/Sidebar/index.tsx:78' # which symbol owns this line + its callers/callees
|
|
1443
|
+
curl -G ${base}/source --data-urlencode 'path=<data-insp-path>' # feed /dom's data-insp-path raw (rel:line:col:Name)
|
|
1444
|
+
\`\`\`
|
|
1445
|
+
|
|
1446
|
+
\`/dom\` tells you *where* a UI element lives (\`@File.tsx:line\`); \`/source\` tells you *what code
|
|
1447
|
+
backs it* \u2014 the enclosing symbol, who calls it, what it calls. Paste a \`data-insp-path\`
|
|
1448
|
+
(\`rel:line:col:Name\`) straight from \`/dom\`. This is the static twin of \`/open\` (which sends the
|
|
1449
|
+
same coordinate to your editor): point at a rendered element \u2192 get its code reason, without grepping.
|
|
1450
|
+
|
|
1451
|
+
## Drive the page (acts on the currently-open tab \u2014 no separate browser)
|
|
1452
|
+
|
|
1453
|
+
Pass \`text=\`/\`sel=\`/\`value=\` via \`-G --data-urlencode\` \u2014 **always**, not just for CJK. A raw
|
|
1454
|
+
\`?text=\u7FA4\u804A\` is an illegal HTTP request-target, rejected with an empty 400 *before aihand runs*
|
|
1455
|
+
(a wasted round-trip); \`--data-urlencode\` escapes it for you, so the same form works for \`New\`
|
|
1456
|
+
and \`\u7FA4\u804A\` alike \u2014 write it right the first time.
|
|
1457
|
+
|
|
1458
|
+
\`\`\`bash
|
|
1459
|
+
curl -G ${base}/click --data-urlencode 'text=New' # click by visible text (or 'sel=<css>')
|
|
1460
|
+
curl -G ${base}/dblclick --data-urlencode 'text=todo' # double-click (inline edit / rename / select); fires dblclick handlers click can't
|
|
1461
|
+
curl -G ${base}/hover --data-urlencode 'text=More' # hover only (reveal a menu/tooltip; no click)
|
|
1462
|
+
curl -G ${base}/fill --data-urlencode 'sel=textarea' --data-urlencode 'value=hi' # set input/textarea; <select> matches by option text
|
|
1463
|
+
curl -G ${base}/press --data-urlencode 'key=Enter' # key on focused element (e.g. Control+a)
|
|
1464
|
+
curl -G ${base}/wait --data-urlencode 'text=Done' -d 'timeout=8000' # poll until text/sel appears (gone=1 until it disappears; enabled=1 until not [disabled])
|
|
1465
|
+
curl '${base}/screenshot?out=shot.png' # DOM\u2192PNG into .aidev/ (html-to-image; lossy)
|
|
1466
|
+
\`\`\`
|
|
1467
|
+
|
|
1468
|
+
ASCII-only values may use the bare \`?text=New\` form, but \`-G --data-urlencode\` is never wrong \u2014
|
|
1469
|
+
prefer it so a value that turns out to contain CJK/space/\`&\` doesn't silently 400.
|
|
1470
|
+
|
|
1471
|
+
\`click\`/\`fill\`/\`press\` settle the DOM and append \`--- changed ---\`: only the state-machine
|
|
1472
|
+
transition this action caused (\`view: a \u2192 b\`, \`modal: opened X\`, \`focus: \u2026\`) plus any new
|
|
1473
|
+
errors/failed requests \u2014 not a fresh snapshot. \`(no state change)\` means nothing moved. Read
|
|
1474
|
+
the delta, then drill into /ui or /dom for detail if you need it. On a miss, the response lists
|
|
1475
|
+
the reachable clickable elements so you can re-target.
|
|
1476
|
+
|
|
1477
|
+
Each \`click\`/\`fill\`/\`press\` response also carries a \`--- recent actions ---\` timeline:
|
|
1478
|
+
the semantic page actions (yours and the user's) in order, \`T\`=trusted human / \`S\`=synthetic
|
|
1479
|
+
aihand, each with its resulting UI change (\`\u2192 \u5F39\u7A97\u6253\u5F00\u300C\u2026\u300D\`/\`\u2192 \u5F39\u7A97\u5173\u95ED\`). Your own action is
|
|
1480
|
+
bracketed by \`\u4F60\u5F53\u524D\u7684\u884C\u4E3A\` dividers. So if the user closed a dialog you just opened, you see
|
|
1481
|
+
their \`T key:Escape \u2192 \u5F39\u7A97\u5173\u95ED\` right after your \`S\` action \u2014 no need to query for it.
|
|
1482
|
+
|
|
1483
|
+
**Beyond click/fill/press** \u2014 four more interactions for what those can't reach:
|
|
1484
|
+
|
|
1485
|
+
\`\`\`bash
|
|
1486
|
+
curl '${base}/scrollIntoView?text=Row 99' # scroll a target into view (off-screen list rows)
|
|
1487
|
+
curl '${base}/drag?sel=.item&to=.slot' # synthetic pointer drag, source \u2192 destination
|
|
1488
|
+
curl '${base}/drop?sel=.dropzone&files=a.png,b.pdf' # fire a file-drop (DataTransfer) on a target
|
|
1489
|
+
curl '${base}/clipboard?mode=write&value=hi' # seed the clipboard (mode=read reports it back)
|
|
1490
|
+
\`\`\`
|
|
1491
|
+
|
|
1492
|
+
\`drag\` fires a real pointer sequence (down \u2192 stepped moves past dnd-kit's activation
|
|
1493
|
+
distance \u2192 up); if a dnd-kit reorder doesn't take, retry the same gesture via \`realclick\`
|
|
1494
|
+
(trusted events). \`drop\` delivers the drop event with the named files (synthetic Files have
|
|
1495
|
+
no byte content \u2014 fine for triggering handlers, not for real uploads). \`clipboard\` needs the
|
|
1496
|
+
tab focused (browser security) and says so plainly when it isn't, rather than hanging.
|
|
1497
|
+
|
|
1498
|
+
A control tagged \`{needs-trusted?}\` in \`/screen\` or \`/dom\` opens a popup (\`aria-haspopup\`)
|
|
1499
|
+
that a synthetic click may not trigger \u2014 reach for \`realclick\` on it from the start instead
|
|
1500
|
+
of discovering it via a dead click. (Right-click-only menus carry no DOM marker, so they
|
|
1501
|
+
still surface only on a miss \u2014 use \`realclick\` with \`button=right\` there.)
|
|
1502
|
+
|
|
1503
|
+
**Green channel \u2014 replay a knob's morphism, no DOM.** \`/screen?form=knobs\` lists every
|
|
1504
|
+
control with its store morphism (\`\u300C\u7FA4\u804A\u300D\u2192 appUIStore.{ mode=im }\`). \`/action?knob=<label>\`
|
|
1505
|
+
replays that morphism by writing the store directly \u2014 bypassing selector resolution, disabled
|
|
1506
|
+
gating, and the silent-false-success a covered \`/click\` can return. It's smoother than
|
|
1507
|
+
\`/click\`+\`/fill\` for any control whose effect is a store write:
|
|
1508
|
+
|
|
1509
|
+
\`\`\`bash
|
|
1510
|
+
curl -G ${base}/action --data-urlencode 'knob=\u7FA4\u804A' # literal: appUIStore.mode='im'
|
|
1511
|
+
curl -G ${base}/action --data-urlencode 'knob=\u6DF1\u5EA6\u7814\u7A76' # toggle: flips the bool
|
|
1512
|
+
curl -G ${base}/action --data-urlencode 'knob=\u6700\u5927Token' --data-urlencode 'value=8' # param: needs ?value=
|
|
1513
|
+
curl ${base}/action?knob=@k3 # @kN: addressable ref from /screen, no CJK escaping
|
|
1514
|
+
\`\`\`
|
|
1515
|
+
|
|
1516
|
+
Every \`/screen\` projection tags each knob with a stable \`@kN\` ref (the number in \`?form=knobs\`,
|
|
1517
|
+
shared across the ASCII art / tree / legend). \`/action?knob=@k3\` resolves it back to the label \u2014
|
|
1518
|
+
saving the CJK \`--data-urlencode\` round-trip and any \`&file=\` disambiguation. A stale/unknown
|
|
1519
|
+
\`@kN\` is refused (422 listing the current numbers) \u2014 re-read \`/screen\` for fresh ones; it never
|
|
1520
|
+
silently hits the wrong knob.
|
|
1521
|
+
|
|
1522
|
+
Returns the same \`--- changed ---\` trajectory a \`/click\` does (waitForStable + diffScreen),
|
|
1523
|
+
not a full state dump. A knob whose value depends on runtime context (\`e.target.value\`, a
|
|
1524
|
+
mapped array, a method with args) is **refused** (422 + reason) \u2014 fall back to \`/click\`/\`/fill\`
|
|
1525
|
+
there; it never fake-replays. A label two components share returns 422 + candidates; add
|
|
1526
|
+
\`&file=<path-fragment>\` to disambiguate.
|
|
1527
|
+
|
|
1528
|
+
**Multiple tabs.** Every read/drive command takes \`?tab=<id>\` to address one specific tab \u2014
|
|
1529
|
+
including a **background** one (you can drive the Chat tab while the user is looking at a
|
|
1530
|
+
different tab). Run \`${base}/tabs\` to see the live ids. With one tab open, omit \`?tab=\` and
|
|
1531
|
+
it just works. With several tabs open and no \`?tab=\`, the command returns \`409\` + the tab
|
|
1532
|
+
list (rather than randomly hitting one) \u2014 pick an id from it and retry with \`?tab=\`.
|
|
1533
|
+
|
|
1534
|
+
**Multiple servers (federation).** When several dev servers run at once \u2014 a micro-frontend,
|
|
1535
|
+
separate front/back servers, or a teammate's machine \u2014 every command also takes
|
|
1536
|
+
\`?host=<host:port>\` to reach a *sibling* aihand. The plugin you curl reverse-proxies the
|
|
1537
|
+
request to that peer (server-side, no browser): \`${base}/screen?host=localhost:5174\` reads
|
|
1538
|
+
the app on :5174; combine with \`?tab=\` to point at one tab over there
|
|
1539
|
+
(\`?host=192.168.1.9:5173&tab=t3\`). Omit \`?host=\` and it's the local server as always. There's
|
|
1540
|
+
no registry \u2014 you name the peer, so list its tabs with \`/tabs?host=<host:port>\` first.
|
|
1541
|
+
|
|
1542
|
+
**Cross-tab timeline.** \`${base}/timeline\` interleaves the semantic actions of *every* tab
|
|
1543
|
+
in time order \u2014 each line \`<tab> [T|S] <action> \u2192 <ui change>\` (\`T\`=trusted human,
|
|
1544
|
+
\`S\`=synthetic aihand). The per-action \`--- recent actions ---\` tail only shows the acting
|
|
1545
|
+
tab; \`/timeline\` is the group view, so an A/B comparison across two tabs (drive A, watch B
|
|
1546
|
+
react) is one read. \`?tab=<id>\` filters to one tab's history.
|
|
1547
|
+
|
|
1548
|
+
**Chain \u2014 a whole interaction in one round-trip.** POST a JSON array; runs in sequence,
|
|
1549
|
+
each step settles before the next, stops on first failure. A failed step carries the same
|
|
1550
|
+
\`clickable:\` fallback list a single-shot \`/click\` gives, and a trailing
|
|
1551
|
+
\`[n..m] skipped (\u2026 not run)\` line names every step the fail-fast stop left unrun \u2014 the page
|
|
1552
|
+
is parked in a half-done state, not rolled back (page actions aren't transactional):
|
|
1553
|
+
|
|
1554
|
+
\`\`\`bash
|
|
1555
|
+
curl -X POST ${base}/chain -d '[
|
|
1556
|
+
{"type":"knob","knob":"\u7FA4\u804A"},
|
|
1557
|
+
{"type":"assert","screen":"appUIStore.mode","equals":"im"},
|
|
1558
|
+
{"type":"click","sel":"button[title=\\"\u77E5\u8BC6\u5E93\\"]"},
|
|
1559
|
+
{"type":"wait","text":"Done"},
|
|
1560
|
+
{"type":"fill","sel":"textarea","value":"hi"},
|
|
1561
|
+
{"type":"assert","screen":"<a domain key from /screen>","equals":"false"},
|
|
1562
|
+
{"type":"press","key":"Enter"}
|
|
1563
|
+
]'
|
|
1564
|
+
\`\`\`
|
|
1565
|
+
|
|
1566
|
+
The \`screen\` key in an \`assert\` must be a real field from \`/screen\`'s \`domain:\` block (e.g.
|
|
1567
|
+
\`streaming\`, \`appUIStore.mode\`) \u2014 copy it verbatim, the names are app-specific. A wrong key
|
|
1568
|
+
fails with \`no domain key "X" \u2014 available: \u2026\` listing every real one.
|
|
1569
|
+
|
|
1570
|
+
A \`knob\` step (\`{type:"knob",knob:"<panel label>"[,value,file]}\`) replays a panel morphism
|
|
1571
|
+
via the green channel \u2014 the store-direct optimal path \`/action?knob=\` takes, now inside a
|
|
1572
|
+
batch. Prefer it over \`click\` when a knob exists: no selector resolution, no overlay gating.
|
|
1573
|
+
|
|
1574
|
+
\`assert\` is the chain's mid-step judge: \`{type,screen,equals}\` checks a domain variable
|
|
1575
|
+
(from the app's \`window.__AIPEEK_SCREEN__\`), or \`{type,sel,equals}\` an element's text. On
|
|
1576
|
+
mismatch the chain stops and reports \`asserted X=="Y", actual "Z"\` \u2014 a test, not a guess.
|
|
1577
|
+
Domain variables also show up in \`/screen\`'s \`domain:\` block and in every \`--- changed ---\`
|
|
1578
|
+
diff (e.g. \`\u6D41\u5F0F\u4E2D: false \u2192 true\`) \u2014 the app's own state machine, which a DOM-only inspector
|
|
1579
|
+
can't see. The app opts in by setting \`window.__AIPEEK_SCREEN__ = () => ({...})\`.
|
|
1580
|
+
|
|
1581
|
+
**Escape hatch.** \`curl '${base}/eval?code=...'\` (or POST the code as the body) runs arbitrary
|
|
1582
|
+
JS in the page and returns the result \u2014 for what the typed endpoints can't do (install listeners,
|
|
1583
|
+
read closures, probe event flow). For count/text/state/attr assertions reach for \`/query\` first.
|
|
1584
|
+
When the code is a hand-rolled \`/state\` (or the selector is illegal), the response tail carries a \`hint:\` line pointing at the typed twin.
|
|
1585
|
+
|
|
1586
|
+
aihand auto-detects errors after HMR and prints them to the terminal \u2014 watch for \`[aihand]\` messages.
|
|
1587
|
+
|
|
1588
|
+
**Iterating on aihand itself.** Editing this server's own code (\`plugin.ts\` and anything in the Vite
|
|
1589
|
+
config dependency graph) makes Vite AUTO-restart the dev server in-process \u2014 terminal prints
|
|
1590
|
+
\`\u2026changed, restarting server\u2026\` then \`server restarted.\`, PID unchanged. You do NOT restart it by
|
|
1591
|
+
hand and you do NOT ask the user to; just edit and wait ~2-4s, then poll \`${base}/tabs\` for the page
|
|
1592
|
+
to re-register (it self-heals via an unthrottled heartbeat). The client probe (\`client.ts\`) instead
|
|
1593
|
+
hot-swaps in place keeping app state. Server stdout goes to the user's terminal (you can't read it) \u2014
|
|
1594
|
+
to make server-side state curl-observable, add a temporary debug route, let Vite auto-restart, then
|
|
1595
|
+
remove it.
|
|
1596
|
+
`;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// src/ui/server/plugin.ts
|
|
1600
|
+
function readBody(req) {
|
|
1601
|
+
return new Promise((resolve5) => {
|
|
1602
|
+
let s = "";
|
|
1603
|
+
req.on("data", (c) => s += c);
|
|
1604
|
+
req.on("end", () => resolve5(s));
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
function send(res, status, body) {
|
|
1608
|
+
res.writeHead(status, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1609
|
+
res.end(body);
|
|
1610
|
+
}
|
|
1611
|
+
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
1612
|
+
var clientDir = [
|
|
1613
|
+
resolve3(__dirname2, "../client"),
|
|
1614
|
+
resolve3(__dirname2, "../src/ui/client")
|
|
1615
|
+
].find(existsSync3) ?? resolve3(__dirname2, "../src/ui/client");
|
|
1616
|
+
var clientPath = resolve3(clientDir, "client.ts");
|
|
1617
|
+
var patchPath = resolve3(clientDir, "client-patch.ts");
|
|
1618
|
+
function compilePatch() {
|
|
1619
|
+
const source = readFileSync(patchPath, "utf-8");
|
|
1620
|
+
const result = transformSync(source, {
|
|
1621
|
+
loader: "ts",
|
|
1622
|
+
target: "es2020",
|
|
1623
|
+
format: "iife",
|
|
1624
|
+
minify: false
|
|
1625
|
+
});
|
|
1626
|
+
return result.code;
|
|
1627
|
+
}
|
|
1628
|
+
function sourceInjectPlugin() {
|
|
1629
|
+
let babel = null;
|
|
1630
|
+
let plugin = null;
|
|
1631
|
+
let root = "";
|
|
1632
|
+
return {
|
|
1633
|
+
name: "aihand:source-loc",
|
|
1634
|
+
apply: "serve",
|
|
1635
|
+
enforce: "pre",
|
|
1636
|
+
async configResolved(config) {
|
|
1637
|
+
root = config.root;
|
|
1638
|
+
},
|
|
1639
|
+
async transform(code, id) {
|
|
1640
|
+
const file = id.split("?")[0];
|
|
1641
|
+
if (!file.endsWith(".tsx") && !file.endsWith(".jsx"))
|
|
1642
|
+
return;
|
|
1643
|
+
if (file.includes("node_modules") || file.includes("packages/aidev/"))
|
|
1644
|
+
return;
|
|
1645
|
+
if (!babel) {
|
|
1646
|
+
babel = await import("@babel/core");
|
|
1647
|
+
plugin = sourceLocPlugin(babel.types);
|
|
1648
|
+
}
|
|
1649
|
+
const result = await babel.transformAsync(code, {
|
|
1650
|
+
filename: file,
|
|
1651
|
+
root,
|
|
1652
|
+
plugins: [plugin],
|
|
1653
|
+
parserOpts: { plugins: ["jsx", "typescript"] },
|
|
1654
|
+
sourceMaps: true,
|
|
1655
|
+
configFile: false,
|
|
1656
|
+
babelrc: false
|
|
1657
|
+
});
|
|
1658
|
+
if (!result?.code)
|
|
1659
|
+
return;
|
|
1660
|
+
return { code: result.code, map: result.map };
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
function aihandPlugin() {
|
|
1665
|
+
return [aihandMainPlugin(), sourceInjectPlugin()];
|
|
1666
|
+
}
|
|
1667
|
+
function aihandMainPlugin() {
|
|
1668
|
+
let pendingResolve = null;
|
|
1669
|
+
let server;
|
|
1670
|
+
let lastRaw = null;
|
|
1671
|
+
let perfBaseline = null;
|
|
1672
|
+
let pushTimer;
|
|
1673
|
+
const pendingActions = /* @__PURE__ */ new Map();
|
|
1674
|
+
const actionProgressAt = /* @__PURE__ */ new Map();
|
|
1675
|
+
let actionId = 0;
|
|
1676
|
+
let graphCache = null;
|
|
1677
|
+
let graphBuilding = null;
|
|
1678
|
+
async function cachedGraph() {
|
|
1679
|
+
if (graphCache)
|
|
1680
|
+
return graphCache;
|
|
1681
|
+
if (!graphBuilding) {
|
|
1682
|
+
graphBuilding = (async () => {
|
|
1683
|
+
const { loadConfig: loadConfig2, isTsFile } = await import("./config-PJPYKDLQ.js");
|
|
1684
|
+
const { scan: scan2 } = await import("./scan-VF54GAAX.js");
|
|
1685
|
+
const { buildCallGraph } = await import("./graph-IH56SCPK.js");
|
|
1686
|
+
const config = await loadConfig2(server.config.root);
|
|
1687
|
+
const g = await buildCallGraph(scan2(config).filter(isTsFile));
|
|
1688
|
+
graphCache = g;
|
|
1689
|
+
graphBuilding = null;
|
|
1690
|
+
return g;
|
|
1691
|
+
})();
|
|
1692
|
+
}
|
|
1693
|
+
return graphBuilding;
|
|
1694
|
+
}
|
|
1695
|
+
const tabs = /* @__PURE__ */ new Map();
|
|
1696
|
+
function seen(data) {
|
|
1697
|
+
if (!data.tab)
|
|
1698
|
+
return;
|
|
1699
|
+
const prev = tabs.get(data.tab);
|
|
1700
|
+
tabs.set(data.tab, {
|
|
1701
|
+
id: data.tab,
|
|
1702
|
+
url: data.url ?? prev?.url ?? "",
|
|
1703
|
+
title: data.title ?? prev?.title ?? "",
|
|
1704
|
+
visible: data.visible ?? prev?.visible ?? false,
|
|
1705
|
+
lastSeen: Date.now()
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
const liveTabs = () => [...tabs.values()].filter((t) => isLive(t, Date.now()));
|
|
1709
|
+
const diagnose2 = (tab) => diagnose(tab, [...tabs.values()], Date.now(), server.ws.clients.size, server.config.server.port || 5173);
|
|
1710
|
+
const actionLog = [];
|
|
1711
|
+
const BOOT_ID = Date.now().toString(36);
|
|
1712
|
+
const droppedByTab = /* @__PURE__ */ new Map();
|
|
1713
|
+
const mergeActions = (tab, actions, dropped) => {
|
|
1714
|
+
if (!tab)
|
|
1715
|
+
return;
|
|
1716
|
+
if (typeof dropped === "number")
|
|
1717
|
+
droppedByTab.set(tab, dropped);
|
|
1718
|
+
if (!actions?.length)
|
|
1719
|
+
return;
|
|
1720
|
+
for (const entry of actions) {
|
|
1721
|
+
if (!actionLog.some((e) => e.tab === tab && e.ts === entry.ts))
|
|
1722
|
+
appendAction(actionLog, tab, entry, 200);
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
const cdpQueue = [];
|
|
1726
|
+
let cdpWaiter = null;
|
|
1727
|
+
const cdpResults = /* @__PURE__ */ new Map();
|
|
1728
|
+
let cdpId = 0;
|
|
1729
|
+
function runCdpClick(x, y, button) {
|
|
1730
|
+
const id = ++cdpId;
|
|
1731
|
+
const cmd = { id, x, y, button };
|
|
1732
|
+
return new Promise((resolve5, reject) => {
|
|
1733
|
+
cdpResults.set(id, resolve5);
|
|
1734
|
+
if (cdpWaiter) {
|
|
1735
|
+
cdpWaiter(cmd);
|
|
1736
|
+
cdpWaiter = null;
|
|
1737
|
+
} else {
|
|
1738
|
+
cdpQueue.push(cmd);
|
|
1739
|
+
}
|
|
1740
|
+
setTimeout(() => {
|
|
1741
|
+
if (cdpResults.delete(id))
|
|
1742
|
+
reject(new Error("cdp timeout: no extension result within 10s (is the aihand extension loaded and the debugger attached?)"));
|
|
1743
|
+
}, 1e4);
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
let pendingDom = null;
|
|
1747
|
+
let pendingStatePath = null;
|
|
1748
|
+
let pendingScreen = null;
|
|
1749
|
+
const pendingEvals = /* @__PURE__ */ new Map();
|
|
1750
|
+
let evalId = 0;
|
|
1751
|
+
const pendingSemantic = /* @__PURE__ */ new Map();
|
|
1752
|
+
let semanticId = 0;
|
|
1753
|
+
const pendingKnob = /* @__PURE__ */ new Map();
|
|
1754
|
+
let knobId = 0;
|
|
1755
|
+
const knobProgressAt = /* @__PURE__ */ new Map();
|
|
1756
|
+
const screenStash = /* @__PURE__ */ new Map();
|
|
1757
|
+
let screenToken = 0;
|
|
1758
|
+
function stashScreen(r) {
|
|
1759
|
+
const token = `t${++screenToken}`;
|
|
1760
|
+
screenStash.set(token, { snap: r.snap, console: r.console, network: r.network, errors: r.errors });
|
|
1761
|
+
if (screenStash.size > 32)
|
|
1762
|
+
screenStash.delete(screenStash.keys().next().value);
|
|
1763
|
+
return token;
|
|
1764
|
+
}
|
|
1765
|
+
const VISIBLE_MS2 = 400;
|
|
1766
|
+
function twoPhase(event, payload, arm, fullMs = 3e3, tab, lastProgress) {
|
|
1767
|
+
return new Promise((resolve5, reject) => {
|
|
1768
|
+
let settled = false;
|
|
1769
|
+
const clear = arm((v) => {
|
|
1770
|
+
settled = true;
|
|
1771
|
+
resolve5(v);
|
|
1772
|
+
});
|
|
1773
|
+
if (tab) {
|
|
1774
|
+
const RETRY_MS = 500;
|
|
1775
|
+
const ABSENT_CEILING_MS = 1e4;
|
|
1776
|
+
const startedAt = Date.now();
|
|
1777
|
+
const deliver = () => server.hot.send(event, { ...payload, tab });
|
|
1778
|
+
deliver();
|
|
1779
|
+
const iv = setInterval(() => {
|
|
1780
|
+
if (settled) {
|
|
1781
|
+
clearInterval(iv);
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
const t = tabs.get(tab);
|
|
1785
|
+
const live = !!t && isLive(t, Date.now());
|
|
1786
|
+
const progressAt = lastProgress?.();
|
|
1787
|
+
const sinceLife = Date.now() - Math.max(startedAt, progressAt ?? 0);
|
|
1788
|
+
const elapsed = Date.now() - startedAt;
|
|
1789
|
+
if (live) {
|
|
1790
|
+
if (sinceLife > fullMs) {
|
|
1791
|
+
clearInterval(iv);
|
|
1792
|
+
clear();
|
|
1793
|
+
reject(new Error(diagnose2(tab)));
|
|
1794
|
+
}
|
|
1795
|
+
} else if (elapsed > ABSENT_CEILING_MS) {
|
|
1796
|
+
clearInterval(iv);
|
|
1797
|
+
clear();
|
|
1798
|
+
reject(new Error(diagnose2(tab)));
|
|
1799
|
+
} else {
|
|
1800
|
+
deliver();
|
|
1801
|
+
}
|
|
1802
|
+
}, RETRY_MS);
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
server.hot.send(event, { ...payload, requireVisible: true });
|
|
1806
|
+
setTimeout(() => {
|
|
1807
|
+
if (settled)
|
|
1808
|
+
return;
|
|
1809
|
+
server.hot.send(event, { ...payload, requireVisible: false });
|
|
1810
|
+
setTimeout(() => {
|
|
1811
|
+
if (settled)
|
|
1812
|
+
return;
|
|
1813
|
+
clear();
|
|
1814
|
+
reject(new Error(diagnose2(tab)));
|
|
1815
|
+
}, fullMs);
|
|
1816
|
+
}, VISIBLE_MS2);
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
function collectFromClient(tab) {
|
|
1820
|
+
return twoPhase("aihand:collect", {}, (resolve5) => {
|
|
1821
|
+
pendingResolve = resolve5;
|
|
1822
|
+
return () => {
|
|
1823
|
+
pendingResolve = null;
|
|
1824
|
+
};
|
|
1825
|
+
}, 3e3, tab);
|
|
1826
|
+
}
|
|
1827
|
+
function collectDomFromClient(scope, sel, tab) {
|
|
1828
|
+
return twoPhase("aihand:collect-dom", { scope, sel }, (resolve5) => {
|
|
1829
|
+
pendingDom = resolve5;
|
|
1830
|
+
return () => {
|
|
1831
|
+
pendingDom = null;
|
|
1832
|
+
};
|
|
1833
|
+
}, 3e3, tab);
|
|
1834
|
+
}
|
|
1835
|
+
function collectStatePathFromClient(path2, tab) {
|
|
1836
|
+
return twoPhase("aihand:collect-state-path", { path: path2 }, (resolve5) => {
|
|
1837
|
+
pendingStatePath = resolve5;
|
|
1838
|
+
return () => {
|
|
1839
|
+
pendingStatePath = null;
|
|
1840
|
+
};
|
|
1841
|
+
}, 3e3, tab);
|
|
1842
|
+
}
|
|
1843
|
+
function collectScreenFromClient(tab, form) {
|
|
1844
|
+
return twoPhase("aihand:collect-screen", { form }, (resolve5) => {
|
|
1845
|
+
pendingScreen = resolve5;
|
|
1846
|
+
return () => {
|
|
1847
|
+
pendingScreen = null;
|
|
1848
|
+
};
|
|
1849
|
+
}, 3e3, tab);
|
|
1850
|
+
}
|
|
1851
|
+
function sendAction(type, args, tab) {
|
|
1852
|
+
const id = ++actionId;
|
|
1853
|
+
const fullMs = Math.max(args.timeout ?? 0, 3e3) + 2e3;
|
|
1854
|
+
return twoPhase("aihand:action", { id, type, args }, (resolve5) => {
|
|
1855
|
+
pendingActions.set(id, resolve5);
|
|
1856
|
+
return () => {
|
|
1857
|
+
pendingActions.delete(id);
|
|
1858
|
+
actionProgressAt.delete(id);
|
|
1859
|
+
};
|
|
1860
|
+
}, fullMs, tab, () => actionProgressAt.get(id));
|
|
1861
|
+
}
|
|
1862
|
+
async function runAction(type, args, tab) {
|
|
1863
|
+
const result = await sendAction(type, args, tab);
|
|
1864
|
+
lastRaw = null;
|
|
1865
|
+
if (type === "realclick" && result.ok && !result.fired) {
|
|
1866
|
+
const cdp = await runCdpClick(result.x, result.y, args.button ?? "left");
|
|
1867
|
+
if (!cdp.ok)
|
|
1868
|
+
return { ok: false, error: `cdp click failed: ${cdp.error ?? "unknown"}` };
|
|
1869
|
+
result.detail = `${result.detail} \u2192 clicked via extension`;
|
|
1870
|
+
result.screen = (await collectScreenFromClient(tab)).screen;
|
|
1871
|
+
}
|
|
1872
|
+
return result;
|
|
1873
|
+
}
|
|
1874
|
+
function evalInClient(code, tab) {
|
|
1875
|
+
const id = ++evalId;
|
|
1876
|
+
return twoPhase("aihand:eval", { id, code }, (resolve5) => {
|
|
1877
|
+
pendingEvals.set(id, resolve5);
|
|
1878
|
+
return () => {
|
|
1879
|
+
pendingEvals.delete(id);
|
|
1880
|
+
};
|
|
1881
|
+
}, 8e3, tab);
|
|
1882
|
+
}
|
|
1883
|
+
function semanticInClient(name, args, tab) {
|
|
1884
|
+
const id = ++semanticId;
|
|
1885
|
+
return twoPhase("aihand:semantic-action", { id, name, args }, (resolve5) => {
|
|
1886
|
+
pendingSemantic.set(id, resolve5);
|
|
1887
|
+
return () => {
|
|
1888
|
+
pendingSemantic.delete(id);
|
|
1889
|
+
};
|
|
1890
|
+
}, 15e3, tab);
|
|
1891
|
+
}
|
|
1892
|
+
function knobInClient(knob, file, value, tab) {
|
|
1893
|
+
const id = ++knobId;
|
|
1894
|
+
return twoPhase("aihand:knob-action", { id, knob, file, value }, (resolve5) => {
|
|
1895
|
+
pendingKnob.set(id, resolve5);
|
|
1896
|
+
return () => {
|
|
1897
|
+
pendingKnob.delete(id);
|
|
1898
|
+
knobProgressAt.delete(id);
|
|
1899
|
+
};
|
|
1900
|
+
}, 15e3, tab, () => knobProgressAt.get(id));
|
|
1901
|
+
}
|
|
1902
|
+
return {
|
|
1903
|
+
name: "aihand",
|
|
1904
|
+
apply: "serve",
|
|
1905
|
+
transformIndexHtml() {
|
|
1906
|
+
const patchCode = compilePatch();
|
|
1907
|
+
return [
|
|
1908
|
+
// Synchronous inline script — patches console/fetch/XHR/errors
|
|
1909
|
+
// BEFORE any ES modules execute
|
|
1910
|
+
{
|
|
1911
|
+
tag: "script",
|
|
1912
|
+
children: patchCode,
|
|
1913
|
+
injectTo: "head-prepend"
|
|
1914
|
+
},
|
|
1915
|
+
// Module script — collectors + HMR channel (can be deferred)
|
|
1916
|
+
{
|
|
1917
|
+
tag: "script",
|
|
1918
|
+
attrs: { type: "module" },
|
|
1919
|
+
children: `import '/@fs/${clientPath}'`,
|
|
1920
|
+
injectTo: "body"
|
|
1921
|
+
}
|
|
1922
|
+
];
|
|
1923
|
+
},
|
|
1924
|
+
configureServer(_server) {
|
|
1925
|
+
server = _server;
|
|
1926
|
+
const bridge = viteBridge(server, { tabs, diagnose: diagnose2 });
|
|
1927
|
+
const isTs = (f) => f.endsWith(".ts") || f.endsWith(".tsx") || f.endsWith(".js") || f.endsWith(".jsx");
|
|
1928
|
+
const invalidateGraph = (f) => {
|
|
1929
|
+
if (isTs(f)) graphCache = null;
|
|
1930
|
+
};
|
|
1931
|
+
server.watcher.on("change", invalidateGraph);
|
|
1932
|
+
server.watcher.on("add", invalidateGraph);
|
|
1933
|
+
server.watcher.on("unlink", invalidateGraph);
|
|
1934
|
+
server.hot.on("aihand:state", (data) => {
|
|
1935
|
+
seen(data);
|
|
1936
|
+
mergeActions(data.tab, data.actions, data.actionsDropped);
|
|
1937
|
+
if (pendingResolve) {
|
|
1938
|
+
pendingResolve(data);
|
|
1939
|
+
pendingResolve = null;
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
server.hot.on("aihand:hello", (data) => {
|
|
1943
|
+
seen(data);
|
|
1944
|
+
if (data.tab)
|
|
1945
|
+
server.hot.send("aihand:hello-ack", { tab: data.tab });
|
|
1946
|
+
});
|
|
1947
|
+
server.hot.on("aihand:result", (data) => {
|
|
1948
|
+
seen(data);
|
|
1949
|
+
const resolve5 = pendingActions.get(data.id);
|
|
1950
|
+
if (resolve5) {
|
|
1951
|
+
pendingActions.delete(data.id);
|
|
1952
|
+
resolve5(data);
|
|
1953
|
+
}
|
|
1954
|
+
});
|
|
1955
|
+
server.hot.on("aihand:action-progress", (data) => {
|
|
1956
|
+
seen(data);
|
|
1957
|
+
if (data.kind === "knob") {
|
|
1958
|
+
if (pendingKnob.has(data.id))
|
|
1959
|
+
knobProgressAt.set(data.id, Date.now());
|
|
1960
|
+
} else if (pendingActions.has(data.id)) {
|
|
1961
|
+
actionProgressAt.set(data.id, Date.now());
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
1964
|
+
server.hot.on("aihand:eval-result", (data) => {
|
|
1965
|
+
seen(data);
|
|
1966
|
+
const resolve5 = pendingEvals.get(data.id);
|
|
1967
|
+
if (resolve5) {
|
|
1968
|
+
pendingEvals.delete(data.id);
|
|
1969
|
+
resolve5(data);
|
|
1970
|
+
}
|
|
1971
|
+
});
|
|
1972
|
+
server.hot.on("aihand:semantic-result", (data) => {
|
|
1973
|
+
seen(data);
|
|
1974
|
+
const resolve5 = pendingSemantic.get(data.id);
|
|
1975
|
+
if (resolve5) {
|
|
1976
|
+
pendingSemantic.delete(data.id);
|
|
1977
|
+
resolve5(data);
|
|
1978
|
+
}
|
|
1979
|
+
});
|
|
1980
|
+
server.hot.on("aihand:knob-result", (data) => {
|
|
1981
|
+
seen(data);
|
|
1982
|
+
const resolve5 = pendingKnob.get(data.id);
|
|
1983
|
+
if (resolve5) {
|
|
1984
|
+
pendingKnob.delete(data.id);
|
|
1985
|
+
resolve5(data);
|
|
1986
|
+
}
|
|
1987
|
+
});
|
|
1988
|
+
server.hot.on("aihand:dom", (data) => {
|
|
1989
|
+
seen(data);
|
|
1990
|
+
if (pendingDom) {
|
|
1991
|
+
pendingDom(data.dom);
|
|
1992
|
+
pendingDom = null;
|
|
1993
|
+
}
|
|
1994
|
+
});
|
|
1995
|
+
server.hot.on("aihand:state-path", (data) => {
|
|
1996
|
+
seen(data);
|
|
1997
|
+
if (pendingStatePath) {
|
|
1998
|
+
pendingStatePath(data);
|
|
1999
|
+
pendingStatePath = null;
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
server.hot.on("aihand:screen", (data) => {
|
|
2003
|
+
seen(data);
|
|
2004
|
+
if (pendingScreen) {
|
|
2005
|
+
pendingScreen(data);
|
|
2006
|
+
pendingScreen = null;
|
|
2007
|
+
}
|
|
2008
|
+
});
|
|
2009
|
+
server.hot.on("vite:afterUpdate", () => {
|
|
2010
|
+
clearTimeout(pushTimer);
|
|
2011
|
+
pushTimer = setTimeout(async () => {
|
|
2012
|
+
try {
|
|
2013
|
+
const raw = await collectFromClient();
|
|
2014
|
+
const diff = diffState(lastRaw, raw);
|
|
2015
|
+
lastRaw = raw;
|
|
2016
|
+
if (!diff.clean) {
|
|
2017
|
+
const msg = emitDiff(diff);
|
|
2018
|
+
if (msg)
|
|
2019
|
+
server.config.logger.warn(msg);
|
|
2020
|
+
}
|
|
2021
|
+
} catch {
|
|
2022
|
+
}
|
|
2023
|
+
}, 500);
|
|
2024
|
+
});
|
|
2025
|
+
server.middlewares.use("/__aihand", async (req, res) => {
|
|
2026
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
2027
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
2028
|
+
const full = url.searchParams.has("full");
|
|
2029
|
+
const explicitTab = url.searchParams.get("tab") || void 0;
|
|
2030
|
+
const live = liveTabs();
|
|
2031
|
+
const tab = explicitTab ?? (live.length === 1 ? live[0].id : void 0);
|
|
2032
|
+
const host = url.searchParams.get("host") || void 0;
|
|
2033
|
+
const selfPort = server.config.server.port || 5173;
|
|
2034
|
+
const selfHosts = /* @__PURE__ */ new Set([`localhost:${selfPort}`, `127.0.0.1:${selfPort}`, `:${selfPort}`, `${selfPort}`]);
|
|
2035
|
+
if (host && !selfHosts.has(host)) {
|
|
2036
|
+
const fwd = new URL(url);
|
|
2037
|
+
fwd.searchParams.delete("host");
|
|
2038
|
+
const target = `http://${host}/__aihand/${parts.join("/")}${fwd.search}`;
|
|
2039
|
+
try {
|
|
2040
|
+
const body = req.method === "POST" ? await readBody(req) : void 0;
|
|
2041
|
+
const r = await fetch(target, { method: req.method, body });
|
|
2042
|
+
send(res, r.status, await r.text());
|
|
2043
|
+
} catch (e) {
|
|
2044
|
+
const code = e.cause?.code;
|
|
2045
|
+
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"}).`;
|
|
2046
|
+
send(res, 502, `cannot reach aihand peer at ${host}: ${why}`);
|
|
2047
|
+
}
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
if (parts[0] === "ping") {
|
|
2051
|
+
send(res, 200, BOOT_ID);
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
if (parts[0] === "help") {
|
|
2055
|
+
send(res, 200, helpText(selfPort).trim());
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
if (parts[0] === "open") {
|
|
2059
|
+
const file = url.searchParams.get("file") || "";
|
|
2060
|
+
const line = Number(url.searchParams.get("line")) || 1;
|
|
2061
|
+
const column = Number(url.searchParams.get("column")) || 1;
|
|
2062
|
+
const root = server.config.root;
|
|
2063
|
+
const abs = isAbsolute(file) ? file : resolve3(root, file);
|
|
2064
|
+
const rel = relative2(root, abs);
|
|
2065
|
+
if (!file || rel.startsWith("..") || isAbsolute(rel)) {
|
|
2066
|
+
send(res, 400, `refusing to open '${file}' \u2014 not under project root`);
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
try {
|
|
2070
|
+
const { launchIDE } = await import("launch-ide");
|
|
2071
|
+
launchIDE({ file: abs, line, column });
|
|
2072
|
+
send(res, 200, `opening ${rel}:${line}:${column}`);
|
|
2073
|
+
} catch (err) {
|
|
2074
|
+
send(res, 500, `launch-ide failed: ${err instanceof Error ? err.message : "unknown"}`);
|
|
2075
|
+
}
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
if (parts[0] === "source") {
|
|
2079
|
+
const path2 = url.searchParams.get("path") || "";
|
|
2080
|
+
if (!path2) {
|
|
2081
|
+
send(res, 400, "usage: /__aihand/source?path=<file:line> (path \u53EF\u76F4\u63A5\u662F /dom \u7684 data-insp-path)");
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
const { parseInspPath, locate, renderLocate } = await import("./locate-NKSUGL3A.js");
|
|
2085
|
+
const { file, line } = parseInspPath(path2);
|
|
2086
|
+
if (!file || !Number.isFinite(line)) {
|
|
2087
|
+
send(res, 400, `bad path '${path2}' \u2014 expected file:line (or data-insp-path)`);
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
try {
|
|
2091
|
+
const graph = await cachedGraph();
|
|
2092
|
+
const result = locate(graph, file, line);
|
|
2093
|
+
if (!result) {
|
|
2094
|
+
send(res, 404, `no symbol covers ${file}:${line} \u2014 file not in scan scope, or line outside any symbol`);
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
send(res, 200, renderLocate(result, url.searchParams.has("json")));
|
|
2098
|
+
} catch (err) {
|
|
2099
|
+
send(res, 500, `source lookup failed: ${err instanceof Error ? err.message : "unknown"}`);
|
|
2100
|
+
}
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
const ambiguous = () => !tab && !["tabs", "timeline", "cdp"].includes(parts[0]) && liveTabs().length > 1;
|
|
2104
|
+
const refuse = () => send(res, 409, `multiple live tabs \u2014 add ?tab=<id>:
|
|
2105
|
+
|
|
2106
|
+
${formatTabs(liveTabs(), Date.now())}`);
|
|
2107
|
+
try {
|
|
2108
|
+
if (parts[0] === "tabs") {
|
|
2109
|
+
const roster = formatTabs(liveTabs(), Date.now());
|
|
2110
|
+
send(res, 200, roster === "(no live tabs)" ? `(${diagnose2()})` : roster);
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
if (parts[0] === "timeline") {
|
|
2114
|
+
const targets = tab ? [tab] : liveTabs().map((t) => t.id);
|
|
2115
|
+
for (const id of targets)
|
|
2116
|
+
await collectFromClient(id).catch(() => {
|
|
2117
|
+
});
|
|
2118
|
+
const entries = tab ? actionLog.filter((e) => e.tab === tab) : actionLog;
|
|
2119
|
+
const dropped = targets.reduce((n, id) => n + (droppedByTab.get(id) ?? 0), 0);
|
|
2120
|
+
send(res, 200, formatActions(entries, void 0, dropped));
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
if (ambiguous()) {
|
|
2124
|
+
refuse();
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
if (parts[0] === "eval") {
|
|
2128
|
+
let code = url.searchParams.get("code") || "";
|
|
2129
|
+
if (!code && req.method === "POST")
|
|
2130
|
+
code = await readBody(req);
|
|
2131
|
+
if (!code) {
|
|
2132
|
+
send(res, 400, "eval needs ?code= or a POST body");
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
const r = await evalInClient(code, tab);
|
|
2136
|
+
const body = r.ok ? r.value ?? "undefined" : `error: ${r.error}`;
|
|
2137
|
+
send(res, r.ok ? 200 : 422, r.hint ? `${body}
|
|
2138
|
+
|
|
2139
|
+
hint: ${r.hint}` : body);
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
if (parts[0] === "action") {
|
|
2143
|
+
const knob = url.searchParams.get("knob") || "";
|
|
2144
|
+
if (knob) {
|
|
2145
|
+
lastRaw = null;
|
|
2146
|
+
const r2 = await knobInClient(
|
|
2147
|
+
knob,
|
|
2148
|
+
url.searchParams.get("file") || void 0,
|
|
2149
|
+
url.searchParams.get("value") ?? void 0,
|
|
2150
|
+
tab
|
|
2151
|
+
);
|
|
2152
|
+
send(res, r2.ok ? 200 : 422, r2.ok ? r2.value ?? "undefined" : `error: ${r2.error}`);
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
const name = url.searchParams.get("name") || "";
|
|
2156
|
+
if (!name) {
|
|
2157
|
+
const hint = misroutedAction([...url.searchParams.keys()]);
|
|
2158
|
+
const base = "action needs ?knob= (a panel label) or ?name= (a __AIHAND_ACTIONS__ key)";
|
|
2159
|
+
send(res, 400, hint ? `${base}
|
|
2160
|
+
|
|
2161
|
+
hint: ${hint}` : base);
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
let args = [];
|
|
2165
|
+
const rawArgs = url.searchParams.get("args") || (req.method === "POST" ? await readBody(req) : "");
|
|
2166
|
+
if (rawArgs) {
|
|
2167
|
+
try {
|
|
2168
|
+
const parsed = JSON.parse(rawArgs);
|
|
2169
|
+
args = Array.isArray(parsed) ? parsed : [parsed];
|
|
2170
|
+
} catch {
|
|
2171
|
+
send(res, 400, `action args= must be a JSON array, got: ${rawArgs.slice(0, 80)}`);
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
lastRaw = null;
|
|
2176
|
+
const r = await semanticInClient(name, args, tab);
|
|
2177
|
+
send(res, r.ok ? 200 : 422, r.ok ? r.value ?? "undefined" : `error: ${r.error}`);
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
if (parts[0] === "dom") {
|
|
2181
|
+
const dom = await collectDomFromClient(
|
|
2182
|
+
url.searchParams.get("scope") || void 0,
|
|
2183
|
+
url.searchParams.get("sel") || void 0,
|
|
2184
|
+
tab
|
|
2185
|
+
);
|
|
2186
|
+
send(res, 200, dom || "(empty)");
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
if (parts[0] === "state" && url.searchParams.has("path")) {
|
|
2190
|
+
const path2 = url.searchParams.get("path") || "";
|
|
2191
|
+
const r = await collectStatePathFromClient(path2, tab);
|
|
2192
|
+
if (!r.ok) {
|
|
2193
|
+
send(res, 404, `path not found: ${r.error}`);
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
let body;
|
|
2197
|
+
try {
|
|
2198
|
+
body = JSON.stringify(r.value, null, 2) ?? String(r.value);
|
|
2199
|
+
} catch {
|
|
2200
|
+
body = String(r.value);
|
|
2201
|
+
}
|
|
2202
|
+
send(res, 200, body);
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
if (parts[0] === "screen") {
|
|
2206
|
+
const { reply, result } = await dispatchScreen(bridge, { tab, form: url.searchParams.get("form") || void 0 });
|
|
2207
|
+
const since = url.searchParams.get("since");
|
|
2208
|
+
const token = stashScreen(reply);
|
|
2209
|
+
if (since) {
|
|
2210
|
+
const prev = screenStash.get(since);
|
|
2211
|
+
if (!prev) {
|
|
2212
|
+
send(res, 422, `unknown since token "${since}" (expired or never issued) \u2014 read /screen first for a fresh token`);
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
const d = diffState(
|
|
2216
|
+
{ ui: "", console: prev.console, network: prev.network, errors: prev.errors, state: {}, url: "", timestamp: 0 },
|
|
2217
|
+
{ ui: "", console: reply.console, network: reply.network, errors: reply.errors, state: {}, url: "", timestamp: 0 }
|
|
2218
|
+
);
|
|
2219
|
+
const changed = diffScreen(prev.snap, reply.snap, d.newErrors, d.newExceptions, d.newFailedRequests);
|
|
2220
|
+
send(res, 200, `token: ${token}
|
|
2221
|
+
${changed.length ? changed.join("\n") : "(no state change)"}`);
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
send(res, 200, result.body === "(empty)" ? "(empty)" : `token: ${token}
|
|
2225
|
+
${result.body}`);
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
2228
|
+
if (parts[0] === "cdp" && parts[1] === "poll") {
|
|
2229
|
+
const queued = cdpQueue.shift();
|
|
2230
|
+
if (queued) {
|
|
2231
|
+
send(res, 200, JSON.stringify(queued));
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
const cmd = await new Promise((resolve5) => {
|
|
2235
|
+
cdpWaiter = resolve5;
|
|
2236
|
+
setTimeout(() => {
|
|
2237
|
+
if (cdpWaiter === resolve5) {
|
|
2238
|
+
cdpWaiter = null;
|
|
2239
|
+
resolve5(null);
|
|
2240
|
+
}
|
|
2241
|
+
}, 25e3);
|
|
2242
|
+
});
|
|
2243
|
+
if (cmd)
|
|
2244
|
+
send(res, 200, JSON.stringify(cmd));
|
|
2245
|
+
else
|
|
2246
|
+
send(res, 204, "");
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
if (parts[0] === "cdp" && parts[1] === "result") {
|
|
2250
|
+
const body = await readBody(req);
|
|
2251
|
+
let data;
|
|
2252
|
+
try {
|
|
2253
|
+
data = JSON.parse(body);
|
|
2254
|
+
} catch {
|
|
2255
|
+
send(res, 400, "cdp/result needs a JSON body {id, ok, error?}");
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
const resolveCdp = cdpResults.get(data.id);
|
|
2259
|
+
if (resolveCdp) {
|
|
2260
|
+
cdpResults.delete(data.id);
|
|
2261
|
+
resolveCdp({ ok: data.ok, error: data.error });
|
|
2262
|
+
}
|
|
2263
|
+
send(res, 200, "ok");
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
if (parts[0] === "chain") {
|
|
2267
|
+
const body = await readBody(req);
|
|
2268
|
+
let steps;
|
|
2269
|
+
try {
|
|
2270
|
+
steps = JSON.parse(body);
|
|
2271
|
+
if (!Array.isArray(steps))
|
|
2272
|
+
throw new Error("body must be a JSON array");
|
|
2273
|
+
} catch (e) {
|
|
2274
|
+
send(res, 400, `invalid chain body: ${e instanceof Error ? e.message : String(e)}`);
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
lastRaw = null;
|
|
2278
|
+
const dr = await runChain(steps, async (type, args) => {
|
|
2279
|
+
if (type === "knob") {
|
|
2280
|
+
const k = args;
|
|
2281
|
+
if (!k.knob)
|
|
2282
|
+
return { ok: false, label: "knob", error: "needs knob=<panel label>" };
|
|
2283
|
+
const kr = await knobInClient(k.knob, k.file, k.value, tab);
|
|
2284
|
+
return { ok: kr.ok, label: "knob", detail: kr.ok ? `replayed ${k.knob}` : void 0, error: kr.ok ? void 0 : kr.error, screen: kr.ok ? kr.value : void 0 };
|
|
2285
|
+
}
|
|
2286
|
+
const check2 = resolveAction(type, args);
|
|
2287
|
+
if (!check2.valid)
|
|
2288
|
+
return { ok: false, label: type, error: check2.error };
|
|
2289
|
+
const r = await runAction(type, args, tab);
|
|
2290
|
+
return { ok: r.ok, label: type, detail: r.detail, error: r.error, screen: r.screen, actions: r.actions };
|
|
2291
|
+
});
|
|
2292
|
+
send(res, dr.status, dr.body);
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
if (TYPES.includes(parts[0])) {
|
|
2296
|
+
const args = argsFromQuery(url.searchParams);
|
|
2297
|
+
if (isDispatchAction(parts[0])) {
|
|
2298
|
+
const dr = await dispatchAction(bridge, parts[0], args, { nextId: () => ++actionId, tab });
|
|
2299
|
+
lastRaw = null;
|
|
2300
|
+
send(res, dr.status, dr.body);
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
const check2 = resolveAction(parts[0], args);
|
|
2304
|
+
if (!check2.valid) {
|
|
2305
|
+
send(res, 400, check2.error ?? "invalid action");
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
const result = await runAction(parts[0], args, tab);
|
|
2309
|
+
if (parts[0] === "screenshot" && result.dataUrl) {
|
|
2310
|
+
const dir = resolve3(server.config.root, ".aidev");
|
|
2311
|
+
mkdirSync(dir, { recursive: true });
|
|
2312
|
+
const name = url.searchParams.get("out") || `shot-${result.dataUrl.length}.png`;
|
|
2313
|
+
const file = resolve3(dir, name);
|
|
2314
|
+
writeFileSync(file, Buffer.from(result.dataUrl.split(",")[1], "base64"));
|
|
2315
|
+
send(res, 200, `saved: ${file}`);
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
const head = result.ok ? result.detail || "ok" : `${result.error}${result.detail ? `
|
|
2319
|
+
|
|
2320
|
+
clickable: ${result.detail}` : ""}`;
|
|
2321
|
+
const actionsTail = result.actions ? `
|
|
2322
|
+
|
|
2323
|
+
--- recent actions ---
|
|
2324
|
+
${result.actions}` : "";
|
|
2325
|
+
const changedTail = result.screen ? `
|
|
2326
|
+
|
|
2327
|
+
--- changed ---
|
|
2328
|
+
${result.screen}` : "";
|
|
2329
|
+
const flowTail = result.flow ? `
|
|
2330
|
+
|
|
2331
|
+
--- flow ---
|
|
2332
|
+
${result.flow}` : "";
|
|
2333
|
+
send(res, result.ok ? 200 : 422, `${head}${actionsTail}${changedTail}${flowTail}`);
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
if (parts[0] === "check") {
|
|
2337
|
+
const raw2 = await collectFromClient(tab);
|
|
2338
|
+
lastRaw = raw2;
|
|
2339
|
+
const result = check(raw2);
|
|
2340
|
+
const output = emitCheck(result);
|
|
2341
|
+
send(res, result.pass ? 200 : 417, output);
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
if (parts[0] === "profile") {
|
|
2345
|
+
const noPerfMsg = (t) => {
|
|
2346
|
+
const info = t ? tabs.get(t) : liveTabs()[0];
|
|
2347
|
+
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)})`;
|
|
2348
|
+
};
|
|
2349
|
+
const resetPerfWindow = () => new Promise((resolve5, reject) => {
|
|
2350
|
+
const timeout = setTimeout(() => reject(new Error("timeout waiting for perf-reset-ack")), 3e3);
|
|
2351
|
+
const handler = (data) => {
|
|
2352
|
+
if (tab && data.tab !== tab) return;
|
|
2353
|
+
clearTimeout(timeout);
|
|
2354
|
+
server.hot.off("aihand:perf-reset-ack", handler);
|
|
2355
|
+
resolve5();
|
|
2356
|
+
};
|
|
2357
|
+
server.hot.on("aihand:perf-reset-ack", handler);
|
|
2358
|
+
server.hot.send("aihand:perf-reset", { tab, requireVisible: false });
|
|
2359
|
+
});
|
|
2360
|
+
if (parts[1] === "reset") {
|
|
2361
|
+
await resetPerfWindow();
|
|
2362
|
+
send(res, 200, "perf window cleared \u2014 reproduce the interaction, then GET /profile");
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2365
|
+
if (parts[1] === "diff") {
|
|
2366
|
+
const raw3 = await collectFromClient(tab);
|
|
2367
|
+
lastRaw = raw3;
|
|
2368
|
+
if (!raw3.performance) {
|
|
2369
|
+
send(res, 200, noPerfMsg(tab));
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
if (!perfBaseline) {
|
|
2373
|
+
perfBaseline = raw3.performance;
|
|
2374
|
+
await resetPerfWindow();
|
|
2375
|
+
send(res, 200, "baseline captured + window cleared \u2014 make your fix, reproduce the interaction, then GET /profile/diff again for the verdict");
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
const report = diffPerformance(perfBaseline, raw3.performance);
|
|
2379
|
+
perfBaseline = null;
|
|
2380
|
+
send(res, 200, report);
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
const raw2 = await collectFromClient(tab);
|
|
2384
|
+
lastRaw = raw2;
|
|
2385
|
+
if (!raw2.performance) {
|
|
2386
|
+
send(res, 200, noPerfMsg(tab));
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
const hiddenNote = raw2.performance.hiddenFrames > 10 ? `
|
|
2390
|
+
|
|
2391
|
+
\u26A0 ${raw2.performance.hiddenFrames} frames skipped while tab was hidden \u2014 bring it to foreground and /profile/reset for accurate data.` : "";
|
|
2392
|
+
send(res, 200, detail(raw2, "profile", void 0, false) + hiddenNote);
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
if (parts.length >= 1) {
|
|
2396
|
+
if (!lastRaw)
|
|
2397
|
+
lastRaw = await collectFromClient(tab);
|
|
2398
|
+
const result = detail(lastRaw, parts[0], parts[1], full);
|
|
2399
|
+
if (result !== null) {
|
|
2400
|
+
send(res, 200, result);
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
const SECTIONS2 = ["ui", "console", "network", "errors", "state", "profile"];
|
|
2404
|
+
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.)`);
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
const raw = await collectFromClient(tab);
|
|
2408
|
+
lastRaw = raw;
|
|
2409
|
+
if (full) {
|
|
2410
|
+
const compacted = compact(raw);
|
|
2411
|
+
send(res, 200, emit(compacted));
|
|
2412
|
+
} else {
|
|
2413
|
+
send(res, 200, emitSummary(raw));
|
|
2414
|
+
}
|
|
2415
|
+
} catch (err) {
|
|
2416
|
+
send(res, 504, err instanceof Error ? err.message : "unknown error");
|
|
2417
|
+
}
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
// src/vite.ts
|
|
2424
|
+
async function aihand(opts = {}) {
|
|
2425
|
+
const config = await loadConfig();
|
|
2426
|
+
const plugins = [];
|
|
2427
|
+
if (config.runtime.enabled) {
|
|
2428
|
+
plugins.push(...aihandPlugin(), knobSchema());
|
|
2429
|
+
const files = scan(config).map((f) => resolve4(f));
|
|
2430
|
+
plugins.push(storeSchema({ roots: files, storeMarker: config.runtime.storeMarker }));
|
|
2431
|
+
const knobFiles = files.filter((f) => f.endsWith(".tsx") || f.endsWith(".jsx"));
|
|
2432
|
+
plugins.push(storeRefs({ roots: files, knobFiles, srcRoot: resolve4("src"), storeMarker: config.runtime.storeMarker }));
|
|
2433
|
+
}
|
|
2434
|
+
if (config.read.enabled)
|
|
2435
|
+
plugins.push(repodexWatchPlugin(opts));
|
|
2436
|
+
return plugins;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
export {
|
|
2440
|
+
discover,
|
|
2441
|
+
storeSchema,
|
|
2442
|
+
check,
|
|
2443
|
+
diffState,
|
|
2444
|
+
emitSummary,
|
|
2445
|
+
emitCheck,
|
|
2446
|
+
emitDiff,
|
|
2447
|
+
aihand
|
|
2448
|
+
};
|