@adhdev/daemon-core 0.7.5 → 0.7.7

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.
Files changed (43) hide show
  1. package/dist/index.d.mts +164 -38
  2. package/dist/index.d.ts +164 -38
  3. package/dist/index.js +4051 -2547
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +3696 -2192
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/{normalize-tKg8IiDk.d.mts → normalize-auJAPmKy.d.mts} +669 -629
  8. package/dist/{normalize-tKg8IiDk.d.ts → normalize-auJAPmKy.d.ts} +669 -629
  9. package/dist/status/normalize.d.mts +1 -1
  10. package/dist/status/normalize.d.ts +1 -1
  11. package/package.json +5 -1
  12. package/src/agent-stream/forward.ts +6 -0
  13. package/src/boot/daemon-lifecycle.ts +7 -4
  14. package/src/cli-adapter-types.ts +2 -0
  15. package/src/cli-adapters/provider-cli-adapter.ts +148 -11
  16. package/src/cli-adapters/pty-transport.ts +100 -0
  17. package/src/cli-adapters/session-host-transport.ts +392 -0
  18. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +126 -0
  19. package/src/cli-adapters/terminal-backends/types.ts +17 -0
  20. package/src/cli-adapters/terminal-backends/xterm-backend.ts +87 -0
  21. package/src/cli-adapters/terminal-screen.ts +40 -53
  22. package/src/commands/cli-manager.ts +184 -55
  23. package/src/config/config.d.ts +116 -0
  24. package/src/config/workspace-activity.d.ts +22 -0
  25. package/src/config/workspaces.d.ts +84 -0
  26. package/src/daemon/dev-auto-implement.ts +1087 -0
  27. package/src/daemon/dev-cdp-handlers.ts +1003 -0
  28. package/src/daemon/dev-cli-debug.ts +288 -0
  29. package/src/daemon/dev-server-types.ts +45 -0
  30. package/src/daemon/dev-server.ts +121 -1698
  31. package/src/index.ts +5 -1
  32. package/src/providers/cli-provider-instance.ts +13 -1
  33. package/src/providers/contracts.d.ts +408 -0
  34. package/src/providers/contracts.ts +9 -0
  35. package/src/providers/extension-provider-instance.ts +50 -10
  36. package/src/providers/provider-instance-manager.ts +48 -10
  37. package/src/providers/provider-instance.d.ts +142 -0
  38. package/src/providers/provider-instance.ts +23 -1
  39. package/src/shared-types.d.ts +157 -0
  40. package/src/shared-types.ts +14 -0
  41. package/src/status/builders.ts +6 -0
  42. package/src/status/normalize.d.ts +14 -0
  43. package/src/types.d.ts +127 -0
@@ -0,0 +1,1003 @@
1
+ /**
2
+ * DevServer — CDP & DOM Handlers
3
+ *
4
+ * Extracted from dev-server.ts for maintainability.
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import type * as http from 'http';
10
+ import type { DevServerContext } from './dev-server-types.js';
11
+ import { LOG } from '../logging/logger.js';
12
+
13
+ export async function handleCdpEvaluate(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
14
+ const body = await ctx.readBody(req);
15
+ const { expression, timeout, ideType } = body;
16
+ if (!expression) {
17
+ ctx.json(res, 400, { error: 'expression required' });
18
+ return;
19
+ }
20
+
21
+ const cdp = ctx.getCdp(ideType);
22
+ if (!cdp && !ideType) {
23
+ LOG.warn('DevServer', 'CDP evaluate without ideType — picked first connected manager');
24
+ }
25
+ if (!cdp?.isConnected) {
26
+ ctx.json(res, 503, { error: 'No CDP connection available' });
27
+ return;
28
+ }
29
+
30
+ try {
31
+ const raw = await cdp.evaluate(expression, timeout || 30000);
32
+ let result = raw;
33
+ if (typeof raw === 'string') {
34
+ try { result = JSON.parse(raw); } catch { /* keep */ }
35
+ }
36
+ ctx.json(res, 200, { result });
37
+ } catch (e: any) {
38
+ ctx.json(res, 500, { error: e.message });
39
+ }
40
+ }
41
+
42
+ export async function handleCdpClick(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
43
+ const body = await ctx.readBody(req);
44
+ const { ideType, x, y } = body;
45
+ if (x == null || y == null) {
46
+ ctx.json(res, 400, { error: 'x and y coordinates required' });
47
+ return;
48
+ }
49
+
50
+ const cdp = ctx.getCdp(ideType);
51
+ if (!cdp?.isConnected) {
52
+ ctx.json(res, 503, { error: 'No CDP connection available' });
53
+ return;
54
+ }
55
+
56
+ try {
57
+ await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
58
+ await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
59
+ ctx.json(res, 200, { success: true, clicked: true, x, y });
60
+ } catch (e: any) {
61
+ ctx.json(res, 500, { error: e.message });
62
+ }
63
+ }
64
+
65
+ export async function handleCdpDomQuery(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
66
+ const body = await ctx.readBody(req);
67
+ const { selector, limit = 10, ideType } = body;
68
+ if (!selector) {
69
+ ctx.json(res, 400, { error: 'selector required' });
70
+ return;
71
+ }
72
+
73
+ const cdp = ctx.getCdp(ideType as string);
74
+ if (!cdp) {
75
+ ctx.json(res, 503, { error: 'No CDP connection available' });
76
+ return;
77
+ }
78
+
79
+ const expr = `(() => {
80
+ try {
81
+ const els = document.querySelectorAll('${selector.replace(/'/g, "\\'")}');
82
+ const results = [];
83
+ for (let i = 0; i < Math.min(els.length, ${limit}); i++) {
84
+ const el = els[i];
85
+ results.push({
86
+ index: i,
87
+ tag: el.tagName?.toLowerCase(),
88
+ id: el.id || null,
89
+ class: el.className && typeof el.className === 'string' ? el.className.trim().slice(0, 200) : null,
90
+ role: el.getAttribute?.('role') || null,
91
+ text: (el.textContent || '').trim().slice(0, 100),
92
+ visible: el.offsetParent !== null || el.offsetWidth > 0,
93
+ rect: (() => { try { const r = el.getBoundingClientRect(); return { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) }; } catch { return null; } })()
94
+ });
95
+ }
96
+ return JSON.stringify({ total: els.length, results });
97
+ } catch (e) { return JSON.stringify({ error: e.message }); }
98
+ })()`;
99
+
100
+ try {
101
+ const raw = await cdp.evaluate(expr, 10000);
102
+ const result = typeof raw === 'string' ? JSON.parse(raw) : raw;
103
+ ctx.json(res, 200, result);
104
+ } catch (e: any) {
105
+ ctx.json(res, 500, { error: e.message });
106
+ }
107
+ }
108
+
109
+ export async function handleScreenshot(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
110
+ const url = new URL(req.url || '/', 'http://localhost');
111
+ const ideType = url.searchParams.get('ideType') || undefined;
112
+ const cdp = ctx.getCdp(ideType);
113
+ if (!cdp) {
114
+ ctx.json(res, 503, { error: 'No CDP connection available' });
115
+ return;
116
+ }
117
+
118
+ try {
119
+ // Get viewport metrics before capturing
120
+ let vpW = 0, vpH = 0;
121
+ try {
122
+ const metrics = await cdp.send('Page.getLayoutMetrics', {}, 3000);
123
+ const vp = metrics?.cssVisualViewport || metrics?.visualViewport;
124
+ if (vp) {
125
+ vpW = Math.round(vp.clientWidth || vp.width || 0);
126
+ vpH = Math.round(vp.clientHeight || vp.height || 0);
127
+ }
128
+ } catch { /* ignore */ }
129
+
130
+ const buf = await cdp.captureScreenshot();
131
+ if (buf) {
132
+ res.writeHead(200, {
133
+ 'Content-Type': 'image/webp',
134
+ 'X-Viewport-Width': String(vpW),
135
+ 'X-Viewport-Height': String(vpH),
136
+ });
137
+ res.end(buf);
138
+ } else {
139
+ ctx.json(res, 500, { error: 'Screenshot failed' });
140
+ }
141
+ } catch (e: any) {
142
+ ctx.json(res, 500, { error: e.message });
143
+ }
144
+ }
145
+
146
+ export async function handleScriptsRun(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
147
+ const body = await ctx.readBody(req);
148
+ const { type, script: scriptName, params } = body;
149
+ if (!type || !scriptName) {
150
+ ctx.json(res, 400, { error: 'type and script required' });
151
+ return;
152
+ }
153
+ // Delegate to handleRunScript
154
+ await this.handleRunScript(type, req, res, body);
155
+ }
156
+
157
+ export async function handleTypeAndSend(ctx: DevServerContext, type: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
158
+ const body = await ctx.readBody(req);
159
+ const { selector, text } = body;
160
+ if (!selector || typeof selector !== 'string' || !text || typeof text !== 'string') {
161
+ ctx.json(res, 400, { error: 'selector and text strings required' }); return;
162
+ }
163
+ const cdp = ctx.getCdp(type);
164
+ if (!cdp) {
165
+ ctx.json(res, 503, { error: `CDP not connected for '${type}'` }); return;
166
+ }
167
+ try {
168
+ const sent = await cdp.typeAndSend(selector, text);
169
+ ctx.json(res, 200, { sent });
170
+ } catch (e: any) {
171
+ ctx.json(res, 500, { error: e.message });
172
+ }
173
+ }
174
+
175
+ export async function handleTypeAndSendAt(ctx: DevServerContext, type: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
176
+ const body = await ctx.readBody(req);
177
+ const { x, y, text } = body;
178
+ if (typeof x !== 'number' || typeof y !== 'number' || !text || typeof text !== 'string') {
179
+ ctx.json(res, 400, { error: 'x, y numbers and text string required' }); return;
180
+ }
181
+ const cdp = ctx.getCdp(type);
182
+ if (!cdp) {
183
+ ctx.json(res, 503, { error: `CDP not connected for '${type}'` }); return;
184
+ }
185
+ try {
186
+ const sent = await cdp.typeAndSendAt(x, y, text);
187
+ ctx.json(res, 200, { sent });
188
+ } catch (e: any) {
189
+ ctx.json(res, 500, { error: e.message });
190
+ }
191
+ }
192
+
193
+ export async function handleScriptHints(ctx: DevServerContext, type: string, _req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
194
+ const dir = this.findProviderDir(type);
195
+ if (!dir) { ctx.json(res, 404, { error: `Provider not found: ${type}` }); return; }
196
+
197
+ // Find scripts.js in the provider dir (may be versioned)
198
+ let scriptsPath = '';
199
+ const directScripts = path.join(dir, 'scripts.js');
200
+ if (fs.existsSync(directScripts)) {
201
+ scriptsPath = directScripts;
202
+ } else {
203
+ // Check versioned scripts dirs
204
+ const scriptsDir = path.join(dir, 'scripts');
205
+ if (fs.existsSync(scriptsDir)) {
206
+ const versions = fs.readdirSync(scriptsDir).filter(d => {
207
+ return fs.statSync(path.join(scriptsDir, d)).isDirectory();
208
+ }).sort().reverse();
209
+ for (const ver of versions) {
210
+ const p = path.join(scriptsDir, ver, 'scripts.js');
211
+ if (fs.existsSync(p)) { scriptsPath = p; break; }
212
+ }
213
+ }
214
+ }
215
+
216
+ if (!scriptsPath) {
217
+ ctx.json(res, 200, { hints: {} });
218
+ return;
219
+ }
220
+
221
+ try {
222
+ const source = fs.readFileSync(scriptsPath, 'utf-8');
223
+ const hints: Record<string, { template: Record<string, any>; description: string }> = {};
224
+
225
+ // Parse exported functions and extract param usage
226
+ const funcRegex = /module\.exports\.(\w+)\s*=\s*function\s+\w+\s*\(params\)/g;
227
+ let match;
228
+ while ((match = funcRegex.exec(source)) !== null) {
229
+ const name = match[1];
230
+ // Find the function body (rough: from match to next module.exports or end)
231
+ const startIdx = match.index;
232
+ const nextFunc = source.indexOf('module.exports.', startIdx + 1);
233
+ const funcBody = source.substring(startIdx, nextFunc > 0 ? nextFunc : source.length);
234
+
235
+ const paramFields: Record<string, any> = {};
236
+
237
+ // Pattern 1: params?.xxx or params.xxx
238
+ const dotRegex = /params\?\.([a-zA-Z_]+)|params\.([a-zA-Z_]+)/g;
239
+ let dm;
240
+ while ((dm = dotRegex.exec(funcBody)) !== null) {
241
+ const field = dm[1] || dm[2];
242
+ if (field === 'length') continue;
243
+ if (!(field in paramFields)) {
244
+ // Infer type from context
245
+ if (/index|count|port|timeout/i.test(field)) paramFields[field] = 0;
246
+ else if (/action|text|title|message|model|mode|button|name|filter/i.test(field)) paramFields[field] = '';
247
+ else paramFields[field] = '';
248
+ }
249
+ }
250
+
251
+ // Pattern 2: typeof params === 'string' ? params : params?.xxx
252
+ const typeofRegex = /typeof params === 'string' \? params : params\?\.([a-zA-Z_]+)/g;
253
+ let tm;
254
+ while ((tm = typeofRegex.exec(funcBody)) !== null) {
255
+ const field = tm[1];
256
+ if (!(field in paramFields)) paramFields[field] = '';
257
+ }
258
+
259
+ // Pattern 3: typeof params === 'number' ? params : params?.xxx
260
+ const numRegex = /typeof params === 'number' \? params : params\?\.([a-zA-Z_]+)/g;
261
+ let nm;
262
+ while ((nm = numRegex.exec(funcBody)) !== null) {
263
+ const field = nm[1];
264
+ if (!(field in paramFields)) paramFields[field] = 0;
265
+ }
266
+
267
+ // Determine description from function name
268
+ const descriptions: Record<string, string> = {
269
+ readChat: 'No params required',
270
+ sendMessage: 'Text to send to the chat',
271
+ listSessions: 'No params required',
272
+ switchSession: 'Switch by index or title',
273
+ newSession: 'No params required',
274
+ focusEditor: 'No params required',
275
+ openPanel: 'No params required',
276
+ resolveAction: 'Approve/reject action buttons',
277
+ listNotifications: 'Optional message filter',
278
+ dismissNotification: 'Dismiss by index, message, or button',
279
+ listModels: 'No params required',
280
+ setModel: 'Model name to select',
281
+ listModes: 'No params required',
282
+ setMode: 'Mode name to select',
283
+ };
284
+
285
+ hints[name] = {
286
+ template: Object.keys(paramFields).length > 0 ? paramFields : {},
287
+ description: descriptions[name] || (Object.keys(paramFields).length > 0 ? 'Params: ' + Object.keys(paramFields).join(', ') : 'No params'),
288
+ };
289
+ }
290
+
291
+ ctx.json(res, 200, { hints });
292
+ } catch (e: any) {
293
+ ctx.json(res, 500, { error: e.message });
294
+ }
295
+ }
296
+
297
+ export async function handleCdpTargets(ctx: DevServerContext, _req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
298
+ const targets: { ide: string; connected: boolean; port: number }[] = [];
299
+ for (const [ide, cdp] of ctx.cdpManagers.entries()) {
300
+ targets.push({ ide, connected: cdp.isConnected, port: cdp.getPort() });
301
+ }
302
+ ctx.json(res, 200, { targets });
303
+ }
304
+
305
+ export async function handleDomInspect(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
306
+ const body = await ctx.readBody(req);
307
+ const { x, y, selector, ideType } = body;
308
+ const cdp = ctx.getCdp(ideType);
309
+ if (!cdp) { ctx.json(res, 503, { error: 'No CDP connection' }); return; }
310
+
311
+ const selectorArg = selector ? JSON.stringify(selector) : 'null';
312
+ const inspectScript = `(() => {
313
+ function gs(el) {
314
+ if (!el || el === document.body) return 'body';
315
+ if (el.id) return '#' + CSS.escape(el.id);
316
+ let s = el.tagName.toLowerCase();
317
+ if (el.className && typeof el.className === 'string') {
318
+ const cls = el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).slice(0, 3);
319
+ if (cls.length) s += '.' + cls.map(c => CSS.escape(c)).join('.');
320
+ }
321
+ const p = el.parentElement;
322
+ if (p) {
323
+ const sibs = [...p.children].filter(c => c.tagName === el.tagName);
324
+ if (sibs.length > 1) s += ':nth-child(' + ([...p.children].indexOf(el) + 1) + ')';
325
+ }
326
+ return s;
327
+ }
328
+ function gp(el) {
329
+ const parts = [];
330
+ let c = el;
331
+ while (c && c !== document.documentElement) { parts.unshift(gs(c)); c = c.parentElement; }
332
+ return parts;
333
+ }
334
+ function ni(el) {
335
+ if (!el) return null;
336
+ const tag = el.tagName?.toLowerCase() || '#text';
337
+ const attrs = {};
338
+ if (el.attributes) for (const a of el.attributes) if (a.name !== 'class' && a.name !== 'style') attrs[a.name] = a.value?.substring(0, 200);
339
+ const cls = (el.className && typeof el.className === 'string') ? el.className.trim().split(/\\s+/).filter(Boolean).slice(0, 10) : [];
340
+ const text = el.textContent?.trim().substring(0, 150) || '';
341
+ const dt = [...(el.childNodes||[])].filter(n=>n.nodeType===3).map(n=>n.textContent.trim()).filter(Boolean).join(' ').substring(0,100);
342
+ const cc = el.children?.length || 0;
343
+ const r = el.getBoundingClientRect?.();
344
+ return { tag, cls, attrs, text, directText: dt, childCount: cc, selector: gs(el), fullSelector: gp(el).join(' > '), rect: r ? {x:Math.round(r.x),y:Math.round(r.y),w:Math.round(r.width),h:Math.round(r.height)} : null };
345
+ }
346
+ const sel = ${selectorArg};
347
+ let el = sel ? document.querySelector(sel) : document.elementFromPoint(${x || 0}, ${y || 0});
348
+ if (!el) return JSON.stringify({ error: 'No element found' });
349
+ const info = ni(el);
350
+ const ancestors = [];
351
+ let pp = el.parentElement;
352
+ while (pp && pp !== document.documentElement) {
353
+ ancestors.push({ tag: pp.tagName.toLowerCase(), selector: gs(pp), cls: (pp.className && typeof pp.className === 'string') ? pp.className.trim().split(/\\s+/).slice(0,3) : [] });
354
+ pp = pp.parentElement;
355
+ }
356
+ const children = [...(el.children||[])].slice(0,50).map(c => ni(c));
357
+ return JSON.stringify({ element: info, ancestors: ancestors.reverse(), children });
358
+ })()`;
359
+
360
+ try {
361
+ const raw = await cdp.evaluate(inspectScript, 10000);
362
+ let result = raw;
363
+ if (typeof raw === 'string') { try { result = JSON.parse(raw as string); } catch { } }
364
+ ctx.json(res, 200, result as Record<string, unknown>);
365
+ } catch (e: any) {
366
+ ctx.json(res, 500, { error: e.message });
367
+ }
368
+ }
369
+
370
+ export async function handleDomChildren(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
371
+ const body = await ctx.readBody(req);
372
+ const { selector, ideType } = body;
373
+ const cdp = ctx.getCdp(ideType);
374
+ if (!cdp) { ctx.json(res, 503, { error: 'No CDP connection' }); return; }
375
+ if (!selector) { ctx.json(res, 400, { error: 'selector required' }); return; }
376
+
377
+ const script = `(() => {
378
+ function gs(el) {
379
+ if (!el || el === document.body) return 'body';
380
+ if (el.id) return '#' + CSS.escape(el.id);
381
+ let s = el.tagName.toLowerCase();
382
+ if (el.className && typeof el.className === 'string') {
383
+ const cls = el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).slice(0, 3);
384
+ if (cls.length) s += '.' + cls.map(c => CSS.escape(c)).join('.');
385
+ }
386
+ const p = el.parentElement;
387
+ if (p) {
388
+ const sibs = [...p.children].filter(c => c.tagName === el.tagName);
389
+ if (sibs.length > 1) s += ':nth-child(' + ([...p.children].indexOf(el) + 1) + ')';
390
+ }
391
+ return s;
392
+ }
393
+ const el = document.querySelector(${JSON.stringify(selector)});
394
+ if (!el) return JSON.stringify({ error: 'Element not found' });
395
+ const children = [...(el.children||[])].slice(0,100).map(c => {
396
+ const tag = c.tagName?.toLowerCase();
397
+ const cls = (c.className && typeof c.className === 'string') ? c.className.trim().split(/\\s+/).filter(Boolean).slice(0,10) : [];
398
+ const attrs = {};
399
+ for (const a of c.attributes) if (a.name!=='class'&&a.name!=='style') attrs[a.name] = a.value?.substring(0,200);
400
+ const text = c.textContent?.trim().substring(0,150)||'';
401
+ const dt = [...c.childNodes].filter(n=>n.nodeType===3).map(n=>n.textContent.trim()).filter(Boolean).join(' ').substring(0,100);
402
+ return { tag, cls, attrs, text, directText: dt, childCount: c.children?.length||0, selector: gs(c) };
403
+ });
404
+ return JSON.stringify({ selector: ${JSON.stringify(selector)}, childCount: el.children?.length||0, children });
405
+ })()`;
406
+
407
+ try {
408
+ const raw = await cdp.evaluate(script, 10000);
409
+ let result = raw;
410
+ if (typeof raw === 'string') { try { result = JSON.parse(raw as string); } catch { } }
411
+ ctx.json(res, 200, result as Record<string, unknown>);
412
+ } catch (e: any) {
413
+ ctx.json(res, 500, { error: e.message });
414
+ }
415
+ }
416
+
417
+ export async function handleDomAnalyze(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
418
+ const body = await ctx.readBody(req);
419
+ const { ideType, selector, x, y } = body;
420
+ const cdp = ctx.getCdp(ideType);
421
+ if (!cdp) { ctx.json(res, 503, { error: 'No CDP connection' }); return; }
422
+
423
+ const selectorArg = selector ? JSON.stringify(selector) : 'null';
424
+ const analyzeScript = `(() => {
425
+ function gs(el) {
426
+ if (!el || el === document.body) return 'body';
427
+ if (el.id) return '#' + CSS.escape(el.id);
428
+ let s = el.tagName.toLowerCase();
429
+ if (el.className && typeof el.className === 'string') {
430
+ const cls = el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).slice(0, 3);
431
+ if (cls.length) s += '.' + cls.map(c => CSS.escape(c)).join('.');
432
+ }
433
+ return s;
434
+ }
435
+ function fp(el) {
436
+ const parts = [];
437
+ let c = el;
438
+ while (c && c !== document.documentElement) { parts.unshift(gs(c)); c = c.parentElement; }
439
+ return parts.join(' > ');
440
+ }
441
+ function sigOf(el) {
442
+ return el.tagName + '|' + ((el.className && typeof el.className === 'string') ? el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).sort().join('.') : '');
443
+ }
444
+
445
+ // Find target element
446
+ const sel = ${selectorArg};
447
+ let target = sel ? document.querySelector(sel) : document.elementFromPoint(${x || 0}, ${y || 0});
448
+ if (!target) return JSON.stringify({ error: 'Element not found' });
449
+
450
+ const result = {
451
+ target: { tag: target.tagName.toLowerCase(), selector: fp(target), text: (target.textContent||'').trim().substring(0, 200) },
452
+ siblingPattern: null,
453
+ ancestorAnalysis: [],
454
+ subtreeTexts: [],
455
+ };
456
+
457
+ // 1. Walk UP parents — at each level, find sibling patterns
458
+ let el = target;
459
+ let depth = 0;
460
+ while (el && el !== document.body && depth < 15) {
461
+ const parent = el.parentElement;
462
+ if (!parent) break;
463
+
464
+ const mySig = sigOf(el);
465
+ const siblings = [...parent.children].filter(c => sigOf(c) === mySig);
466
+ const totalChildren = parent.children.length;
467
+ const childSel = gs(el).replace(/:nth-child\\(\\d+\\)/, '');
468
+ const parentSel = fp(parent);
469
+
470
+ result.ancestorAnalysis.push({
471
+ depth,
472
+ parentTag: parent.tagName.toLowerCase(),
473
+ parentSelector: parentSel,
474
+ totalChildren,
475
+ matchingSiblings: siblings.length,
476
+ childSelector: childSel,
477
+ fullSelector: parentSel + ' > ' + childSel,
478
+ });
479
+
480
+ // Best sibling pattern: 3+ matching siblings with text
481
+ if (!result.siblingPattern && siblings.length >= 3) {
482
+ const siblingData = siblings.map((s, i) => {
483
+ const directText = [...s.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent.trim()).filter(Boolean).join(' ').substring(0, 120);
484
+ const allText = (s.textContent || '').trim().substring(0, 200);
485
+ const childCount = s.children?.length || 0;
486
+ const cls = (s.className && typeof s.className === 'string') ? s.className.trim().split(/\\s+/).filter(Boolean) : [];
487
+ const attrs = {};
488
+ if (s.attributes) for (const a of s.attributes) {
489
+ if (a.name !== 'class' && a.name !== 'style' && a.value) attrs[a.name] = a.value.substring(0, 100);
490
+ }
491
+ return { index: i, directText, allText, childCount, cls, attrs, tag: s.tagName.toLowerCase() };
492
+ });
493
+
494
+ // Find common attributes across siblings
495
+ const allAttrs = siblingData.map(s => Object.keys(s.attrs));
496
+ const commonAttrs = allAttrs[0]?.filter(attr => allAttrs.every(a => a.includes(attr))) || [];
497
+ // Find varying attributes (data-*, role, etc)
498
+ const varyingAttrs = {};
499
+ for (const attr of commonAttrs) {
500
+ const values = siblingData.map(s => s.attrs[attr]);
501
+ const unique = [...new Set(values)];
502
+ if (unique.length > 1) varyingAttrs[attr] = unique.slice(0, 5);
503
+ }
504
+
505
+ result.siblingPattern = {
506
+ count: siblings.length,
507
+ selector: parentSel + ' > ' + childSel,
508
+ parentSelector: parentSel,
509
+ depthFromTarget: depth,
510
+ siblings: siblingData.slice(0, 30),
511
+ commonAttrs,
512
+ varyingAttrs,
513
+ };
514
+ }
515
+
516
+ el = parent;
517
+ depth++;
518
+ }
519
+
520
+ // 2. Collect subtree text nodes from target
521
+ const walker = document.createTreeWalker(target, NodeFilter.SHOW_TEXT, null);
522
+ let node;
523
+ while ((node = walker.nextNode()) && result.subtreeTexts.length < 30) {
524
+ const text = node.textContent.trim();
525
+ if (text.length > 2) {
526
+ const parentTag = node.parentElement?.tagName?.toLowerCase() || '';
527
+ const parentCls = (node.parentElement?.className && typeof node.parentElement.className === 'string')
528
+ ? node.parentElement.className.trim().split(/\\s+/).filter(Boolean).slice(0,3).join('.') : '';
529
+ result.subtreeTexts.push({
530
+ text: text.substring(0, 150),
531
+ parentTag,
532
+ parentCls,
533
+ parentSelector: gs(node.parentElement),
534
+ });
535
+ }
536
+ }
537
+
538
+ return JSON.stringify(result);
539
+ })()`;
540
+
541
+ try {
542
+ const raw = await cdp.evaluate(analyzeScript, 15000);
543
+ let result = raw;
544
+ if (typeof raw === 'string') { try { result = JSON.parse(raw as string); } catch { } }
545
+ ctx.json(res, 200, result as Record<string, unknown>);
546
+ } catch (e: any) {
547
+ ctx.json(res, 500, { error: e.message });
548
+ }
549
+ }
550
+
551
+ export async function handleFindCommon(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
552
+ const body = await ctx.readBody(req);
553
+ const { include, exclude, ideType } = body;
554
+ if (!Array.isArray(include) || include.length === 0) { ctx.json(res, 400, { error: 'include[] is required' }); return; }
555
+ const cdp = ctx.getCdp(ideType);
556
+ if (!cdp) { ctx.json(res, 503, { error: 'No CDP connection' }); return; }
557
+
558
+ const script = `(() => {
559
+ const includes = ${JSON.stringify(include)};
560
+ const excludes = ${JSON.stringify(exclude || [])};
561
+
562
+ function gs(el) {
563
+ if (!el || el === document.body) return 'body';
564
+ if (el.id) return '#' + CSS.escape(el.id);
565
+ let s = el.tagName.toLowerCase();
566
+ if (el.className && typeof el.className === 'string') {
567
+ const cls = el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).slice(0, 3);
568
+ if (cls.length) s += '.' + cls.map(c => CSS.escape(c)).join('.');
569
+ }
570
+ return s;
571
+ }
572
+ function fp(el) {
573
+ const parts = [];
574
+ let c = el;
575
+ while (c && c !== document.documentElement) { parts.unshift(gs(c)); c = c.parentElement; }
576
+ return parts.join(' > ');
577
+ }
578
+ function sig(el) {
579
+ return el.tagName + '|' + ((el.className && typeof el.className === 'string') ? el.className.trim() : '');
580
+ }
581
+
582
+ // Step 1: For each include, find all matching leaf elements
583
+ const includeMatches = includes.map(text => {
584
+ const lower = text.toLowerCase();
585
+ const found = [];
586
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
587
+ acceptNode: n => n.textContent.toLowerCase().includes(lower) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT
588
+ });
589
+ let node;
590
+ while ((node = walker.nextNode()) && found.length < 5) {
591
+ if (node.parentElement) found.push(node.parentElement);
592
+ }
593
+ return found;
594
+ });
595
+
596
+ if (includeMatches.some(m => m.length === 0)) {
597
+ const missing = includes.filter((_, i) => includeMatches[i].length === 0);
598
+ return JSON.stringify({ results: [], message: 'Text not found: ' + missing.join(', ') });
599
+ }
600
+
601
+ // Step 2: Find LCA for each combination of include elements
602
+ // For each pair of include[0] element and include[1] element, find their LCA
603
+ // Then within the LCA, find the direct-child subtree branch for each
604
+ const containers = [];
605
+ const seen = new Set();
606
+
607
+ function findLCA(el1, el2) {
608
+ const ancestors1 = new Set();
609
+ let c = el1;
610
+ while (c) { ancestors1.add(c); c = c.parentElement; }
611
+ c = el2;
612
+ while (c) { if (ancestors1.has(c)) return c; c = c.parentElement; }
613
+ return document.body;
614
+ }
615
+
616
+ function findDirectChildContaining(parent, descendant) {
617
+ let c = descendant;
618
+ while (c && c.parentElement !== parent) c = c.parentElement;
619
+ return c;
620
+ }
621
+
622
+ // Try all combinations (first 3 matches per include)
623
+ for (const el1 of includeMatches[0].slice(0, 3)) {
624
+ for (let ii = 1; ii < includeMatches.length; ii++) {
625
+ for (const el2 of includeMatches[ii].slice(0, 3)) {
626
+ if (el1 === el2) continue;
627
+ const lca = findLCA(el1, el2);
628
+ if (!lca || lca === document.body || lca === document.documentElement) continue;
629
+
630
+ // Find which direct child of LCA contains each include element
631
+ const child1 = findDirectChildContaining(lca, el1);
632
+ const child2 = findDirectChildContaining(lca, el2);
633
+ if (!child1 || !child2 || child1 === child2) continue;
634
+
635
+ const lcaSel = fp(lca);
636
+ if (seen.has(lcaSel)) continue;
637
+ seen.add(lcaSel);
638
+
639
+ // Check exclude
640
+ if (excludes.length > 0) {
641
+ const lcaText = (lca.textContent || '').toLowerCase();
642
+ if (excludes.some(ex => lcaText.includes(ex.toLowerCase()))) continue;
643
+ }
644
+
645
+ // Are child1 and child2 same tag? (relaxed — ignore classes)
646
+ const tag1 = child1.tagName;
647
+ const tag2 = child2.tagName;
648
+
649
+ // Bubble up: walk up from LCA, find the best list container
650
+ // (the one with most repeating same-tag children)
651
+ let container = lca;
652
+ let bestContainer = lca;
653
+ let bestListCount = 0;
654
+ for (let up = 0; up < 10; up++) {
655
+ const p = container.parentElement;
656
+ if (!p || p === document.body || p === document.documentElement) break;
657
+ // Check how many same-tag siblings 'container' has in parent
658
+ const myTag = container.tagName;
659
+ const sibCount = [...p.children].filter(c => c.tagName === myTag).length;
660
+ if (sibCount > bestListCount) {
661
+ bestListCount = sibCount;
662
+ bestContainer = p;
663
+ }
664
+ container = p;
665
+ }
666
+ container = bestListCount >= 3 ? bestContainer : lca;
667
+
668
+ const allChildren = [...container.children];
669
+ const childTag = tag1 === tag2 ? tag1 : (allChildren.length > 0 ? allChildren[0].tagName : '');
670
+ const sameTagCount = allChildren.filter(c => c.tagName === childTag).length;
671
+ const isList = sameTagCount >= 3 && sameTagCount >= allChildren.length * 0.4;
672
+
673
+ // Gather all same-tag children as list items
674
+ const listItems = isList
675
+ ? allChildren.filter(c => c.tagName === childTag)
676
+ : allChildren;
677
+
678
+ // Filter rendered items (skip virtual scroll placeholders)
679
+ const rendered = listItems.filter(c => (c.innerText || '').trim().length > 0);
680
+ const placeholderCount = listItems.length - rendered.length;
681
+
682
+ const containerSel = fp(container);
683
+ if (seen.has(containerSel)) continue;
684
+ seen.add(containerSel);
685
+
686
+ const r = container.getBoundingClientRect();
687
+ containers.push({
688
+ selector: containerSel,
689
+ tag: container.tagName.toLowerCase(),
690
+ childCount: allChildren.length,
691
+ listItemCount: listItems.length,
692
+ renderedCount: rendered.length,
693
+ placeholderCount,
694
+ isList,
695
+ rect: { w: Math.round(r.width), h: Math.round(r.height) },
696
+ depth: containerSel.split(' > ').length,
697
+ items: rendered.slice(0, 30).map((el, i) => {
698
+ const fullText = (el.innerText || el.textContent || '').trim();
699
+ // Find snippet around first matched include text
700
+ let text = fullText.substring(0, 200);
701
+ const matched = [];
702
+ for (const inc of includes) {
703
+ const idx = fullText.toLowerCase().indexOf(inc.toLowerCase());
704
+ if (idx >= 0) {
705
+ matched.push(inc);
706
+ if (matched.length === 1) {
707
+ // Show snippet around first match
708
+ const start = Math.max(0, idx - 30);
709
+ const end = Math.min(fullText.length, idx + inc.length + 80);
710
+ text = (start > 0 ? '...' : '') + fullText.substring(start, end) + (end < fullText.length ? '...' : '');
711
+ }
712
+ }
713
+ }
714
+ return {
715
+ index: i,
716
+ tag: el.tagName.toLowerCase(),
717
+ cls: (el.className && typeof el.className === 'string') ? el.className.trim().split(/\\s+/).slice(0, 2).join(' ') : '',
718
+ text,
719
+ matchedIncludes: matched,
720
+ childCount: el.children.length,
721
+ h: Math.round(el.getBoundingClientRect().height),
722
+ };
723
+ }),
724
+ });
725
+ }
726
+ }
727
+ }
728
+
729
+ // Sort: list containers first (more items = better), then by depth
730
+ containers.sort((a, b) => {
731
+ if (a.isList !== b.isList) return a.isList ? -1 : 1;
732
+ return b.listItemCount - a.listItemCount || b.depth - a.depth;
733
+ });
734
+
735
+ return JSON.stringify({
736
+ results: containers.slice(0, 10),
737
+ includeCount: includes.length,
738
+ excludeCount: excludes.length,
739
+ });
740
+ })()`;
741
+
742
+ try {
743
+ const raw = await cdp.evaluate(script, 10000);
744
+ let result = raw;
745
+ if (typeof raw === 'string') { try { result = JSON.parse(raw as string); } catch { } }
746
+ ctx.json(res, 200, result as Record<string, unknown>);
747
+ } catch (e: any) {
748
+ ctx.json(res, 500, { error: e.message });
749
+ }
750
+ }
751
+
752
+ export async function handleFindByText(ctx: DevServerContext, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
753
+ const body = await ctx.readBody(req);
754
+ const { text, ideType, containerSelector } = body;
755
+ if (!text || typeof text !== 'string') { ctx.json(res, 400, { error: 'text is required' }); return; }
756
+ const cdp = ctx.getCdp(ideType);
757
+ if (!cdp) { ctx.json(res, 503, { error: 'No CDP connection' }); return; }
758
+
759
+ const containerArg = containerSelector ? JSON.stringify(containerSelector) : 'null';
760
+ const script = `(() => {
761
+ function gs(el) {
762
+ if (!el || el === document.body) return 'body';
763
+ if (el.id) return '#' + CSS.escape(el.id);
764
+ let s = el.tagName.toLowerCase();
765
+ if (el.className && typeof el.className === 'string') {
766
+ const cls = el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).slice(0, 3);
767
+ if (cls.length) s += '.' + cls.map(c => CSS.escape(c)).join('.');
768
+ }
769
+ return s;
770
+ }
771
+ function fp(el) {
772
+ const parts = [];
773
+ let c = el;
774
+ while (c && c !== document.documentElement) { parts.unshift(gs(c)); c = c.parentElement; }
775
+ return parts.join(' > ');
776
+ }
777
+ function parentSig(el) {
778
+ // Signature: tag+class chain up 3 levels
779
+ const parts = [];
780
+ let c = el;
781
+ for (let i = 0; i < 3 && c; i++) { parts.push(gs(c)); c = c.parentElement; }
782
+ return parts.join(' < ');
783
+ }
784
+
785
+ const searchText = ${JSON.stringify(text)}.toLowerCase();
786
+ const container = ${containerArg} ? document.querySelector(${containerArg}) : document.body;
787
+ if (!container) return JSON.stringify({ error: 'Container not found' });
788
+
789
+ const matches = [];
790
+ const seen = new Set();
791
+
792
+ // Find all text nodes containing the search text
793
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
794
+ acceptNode: n => n.textContent.toLowerCase().includes(searchText) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT
795
+ });
796
+ let node;
797
+ while ((node = walker.nextNode()) && matches.length < 50) {
798
+ // Walk up to find the most specific visible element
799
+ let el = node.parentElement;
800
+ if (!el) continue;
801
+
802
+ // Skip hidden elements
803
+ const r = el.getBoundingClientRect();
804
+ if (r.width === 0 && r.height === 0) continue;
805
+
806
+ const selector = fp(el);
807
+ if (seen.has(selector)) continue;
808
+ seen.add(selector);
809
+
810
+ // Walk up parent chain — record each level's selector + sibling count
811
+ const ancestors = [];
812
+ let cur = el;
813
+ let pLvl = cur.parentElement;
814
+ for (let lvl = 0; lvl < 10 && pLvl && pLvl !== document.body; lvl++) {
815
+ const mySig = cur.tagName + '|' + ((cur.className && typeof cur.className === 'string') ? cur.className.trim().split(/\\s+/).sort().join('.') : '');
816
+ const sibs = [...pLvl.children].filter(c => {
817
+ const sig = c.tagName + '|' + ((c.className && typeof c.className === 'string') ? c.className.trim().split(/\\s+/).sort().join('.') : '');
818
+ return sig === mySig;
819
+ });
820
+ const childSel = gs(cur).replace(/:nth-child\\(\\d+\\)/, '');
821
+ ancestors.push({
822
+ parentSelector: fp(pLvl),
823
+ childSelector: childSel,
824
+ fullSelector: fp(pLvl) + ' > ' + childSel,
825
+ siblingCount: sibs.length,
826
+ parentTag: pLvl.tagName.toLowerCase(),
827
+ });
828
+ cur = pLvl;
829
+ pLvl = pLvl.parentElement;
830
+ }
831
+
832
+ const directText = (node.textContent || '').trim().substring(0, 200);
833
+ const allText = (node.parentElement.textContent || '').trim().substring(0, 300);
834
+ const tag = node.parentElement.tagName.toLowerCase();
835
+ const cls = (node.parentElement.className && typeof node.parentElement.className === 'string')
836
+ ? node.parentElement.className.trim().split(/\\s+/).filter(Boolean) : [];
837
+
838
+ matches.push({
839
+ selector,
840
+ tag,
841
+ cls,
842
+ directText,
843
+ allText,
844
+ ancestors,
845
+ rect: { w: Math.round(r.width), h: Math.round(r.height) },
846
+ depth: selector.split(' > ').length,
847
+ });
848
+ }
849
+
850
+ // Sort: prefer elements with more siblings in ancestry, then fewer depth
851
+ matches.sort((a, b) => {
852
+ const aMax = Math.max(1, ...a.ancestors.map(x => x.siblingCount));
853
+ const bMax = Math.max(1, ...b.ancestors.map(x => x.siblingCount));
854
+ return (bMax - aMax) || (a.depth - b.depth);
855
+ });
856
+
857
+ return JSON.stringify({ query: ${JSON.stringify(text)}, matches, total: matches.length });
858
+ })()`;
859
+
860
+ try {
861
+ const raw = await cdp.evaluate(script, 10000);
862
+ let result = raw;
863
+ if (typeof raw === 'string') { try { result = JSON.parse(raw as string); } catch { } }
864
+ ctx.json(res, 200, result as Record<string, unknown>);
865
+ } catch (e: any) {
866
+ ctx.json(res, 500, { error: e.message });
867
+ }
868
+ }
869
+
870
+ export async function handleDomContext(ctx: DevServerContext, type: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
871
+ const body = await ctx.readBody(req);
872
+ const { ideType } = body;
873
+ const provider = ctx.providerLoader.resolve(type);
874
+ if (!provider) { ctx.json(res, 404, { error: `Provider not found: ${type}` }); return; }
875
+
876
+ const cdp = ctx.getCdp(ideType || type);
877
+ if (!cdp) { ctx.json(res, 503, { error: 'No CDP connection available. Target IDE must be running with CDP enabled.' }); return; }
878
+
879
+ try {
880
+ // 1. Capture screenshot
881
+ let screenshot: string | null = null;
882
+ try {
883
+ const buf = await cdp.captureScreenshot();
884
+ if (buf) screenshot = buf.toString('base64');
885
+ } catch { /* screenshot optional */ }
886
+
887
+ // 2. Collect DOM snapshot
888
+ const domScript = `(() => {
889
+ function gs(el) {
890
+ if (!el || el === document.body) return 'body';
891
+ if (el.id) return '#' + CSS.escape(el.id);
892
+ let s = el.tagName.toLowerCase();
893
+ if (el.className && typeof el.className === 'string') {
894
+ const cls = el.className.trim().split(/\\s+/).filter(c => c && !c.startsWith('_')).slice(0, 3);
895
+ if (cls.length) s += '.' + cls.map(c => CSS.escape(c)).join('.');
896
+ }
897
+ return s;
898
+ }
899
+ function fp(el) {
900
+ const parts = [];
901
+ let c = el;
902
+ while (c && c !== document.documentElement) { parts.unshift(gs(c)); c = c.parentElement; }
903
+ return parts.join(' > ');
904
+ }
905
+ function rect(el) {
906
+ try { const r = el.getBoundingClientRect(); return { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) }; }
907
+ catch { return null; }
908
+ }
909
+
910
+ const result = { contentEditables: [], chatContainers: [], buttons: [], sidebars: [], dropdowns: [], inputs: [] };
911
+
912
+ // Content editables + textareas + inputs
913
+ document.querySelectorAll('[contenteditable], textarea, input[type="text"], input:not([type])').forEach(el => {
914
+ if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
915
+ result.contentEditables.push({
916
+ selector: fp(el),
917
+ tag: el.tagName.toLowerCase(),
918
+ contenteditable: el.getAttribute('contenteditable'),
919
+ role: el.getAttribute('role'),
920
+ ariaLabel: el.getAttribute('aria-label'),
921
+ placeholder: el.getAttribute('placeholder'),
922
+ rect: rect(el),
923
+ visible: el.offsetParent !== null || el.offsetWidth > 0,
924
+ });
925
+ });
926
+
927
+ // Chat containers — large divs with scroll
928
+ document.querySelectorAll('div, section, main').forEach(el => {
929
+ const style = getComputedStyle(el);
930
+ const isScrollable = style.overflowY === 'auto' || style.overflowY === 'scroll';
931
+ const r = el.getBoundingClientRect();
932
+ if (!isScrollable || r.height < 200 || r.width < 200) return;
933
+ const childCount = el.children.length;
934
+ if (childCount < 2) return;
935
+ result.chatContainers.push({
936
+ selector: fp(el),
937
+ childCount,
938
+ rect: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) },
939
+ hasScrollable: true,
940
+ scrollTop: Math.round(el.scrollTop),
941
+ scrollHeight: Math.round(el.scrollHeight),
942
+ });
943
+ });
944
+
945
+ // Buttons
946
+ document.querySelectorAll('button, [role="button"]').forEach(el => {
947
+ if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
948
+ const text = (el.textContent || '').trim().substring(0, 80);
949
+ if (!text && !el.getAttribute('aria-label')) return;
950
+ result.buttons.push({
951
+ text,
952
+ ariaLabel: el.getAttribute('aria-label'),
953
+ selector: fp(el),
954
+ rect: rect(el),
955
+ disabled: el.disabled || el.getAttribute('aria-disabled') === 'true',
956
+ });
957
+ });
958
+
959
+ // Sidebars — panels on left/right edges
960
+ document.querySelectorAll('[class*="sidebar"], [class*="side-bar"], [class*="panel"], [role="complementary"], [role="navigation"], aside').forEach(el => {
961
+ if (el.offsetWidth === 0 && el.offsetHeight === 0) return;
962
+ const r = el.getBoundingClientRect();
963
+ if (r.width < 50 || r.height < 200) return;
964
+ result.sidebars.push({
965
+ selector: fp(el),
966
+ position: r.x < window.innerWidth / 3 ? 'left' : r.x > window.innerWidth * 2 / 3 ? 'right' : 'center',
967
+ rect: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) },
968
+ childCount: el.children.length,
969
+ });
970
+ });
971
+
972
+ // Dropdowns — select, popover, menu patterns
973
+ document.querySelectorAll('select, [role="listbox"], [role="menu"], [role="combobox"], [class*="dropdown"], [class*="popover"]').forEach(el => {
974
+ result.dropdowns.push({
975
+ selector: fp(el),
976
+ tag: el.tagName.toLowerCase(),
977
+ role: el.getAttribute('role'),
978
+ visible: el.offsetParent !== null || el.offsetWidth > 0,
979
+ rect: rect(el),
980
+ });
981
+ });
982
+
983
+ return JSON.stringify(result);
984
+ })()`;
985
+
986
+ const raw = await cdp.evaluate(domScript, 15000);
987
+ let domSnapshot: any = {};
988
+ if (typeof raw === 'string') { try { domSnapshot = JSON.parse(raw); } catch { domSnapshot = { raw }; } }
989
+ else domSnapshot = raw;
990
+
991
+ ctx.json(res, 200, {
992
+ screenshot: screenshot ? `base64:${screenshot}` : null,
993
+ domSnapshot,
994
+ pageTitle: await cdp.evaluate('document.title', 3000).catch(() => ''),
995
+ pageUrl: await cdp.evaluate('window.location.href', 3000).catch(() => ''),
996
+ providerType: type,
997
+ timestamp: new Date().toISOString(),
998
+ });
999
+ } catch (e: any) {
1000
+ ctx.json(res, 500, { error: `DOM context collection failed: ${e.message}` });
1001
+ }
1002
+ }
1003
+