nodebench-mcp 2.18.4 → 2.19.1

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.
@@ -0,0 +1,903 @@
1
+ /**
2
+ * UI/UX Full Dive Tools — Parallel subagent swarm for comprehensive UI traversal.
3
+ *
4
+ * Architecture:
5
+ * - Main agent starts a dive session and registers top-level page components.
6
+ * - Subagents claim individual components via start_component_flow (isolated context).
7
+ * - Each subagent logs interactions and tags bugs within its component scope.
8
+ * - The component tree builds up hierarchically (page → section → element).
9
+ * - get_dive_tree produces an XML-like overview of the entire app structure.
10
+ * - get_dive_report generates a final report with Mermaid diagram, bug summary,
11
+ * and per-component interaction traces.
12
+ *
13
+ * Designed for use with Claude Code subagents, Windsurf, or any MCP client that
14
+ * supports parallel tool invocation. Each subagent only needs the session_id and
15
+ * its assigned component_id to operate independently.
16
+ */
17
+ import { getDb, genId } from "../db.js";
18
+ // ── Browser Session Management (Built-in Playwright) ────────────────────
19
+ // Singleton browser/page per dive session. Auto-detects Playwright.
20
+ // Falls back gracefully to manual/logging-only mode if not installed.
21
+ let _browser = null;
22
+ let _page = null;
23
+ let _activeSessionId = null;
24
+ async function getPlaywright() {
25
+ try {
26
+ return await import("playwright");
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ async function ensureBrowser(url, sessionId, headless = true) {
33
+ if (_page && _activeSessionId === sessionId) {
34
+ return { page: _page, launched: false };
35
+ }
36
+ await closeBrowser();
37
+ const pw = await getPlaywright();
38
+ if (!pw) {
39
+ return {
40
+ page: null,
41
+ launched: false,
42
+ error: "Playwright not installed. Install for zero-friction browser automation:\n npm install playwright && npx playwright install chromium\nFalling back to manual logging mode.",
43
+ };
44
+ }
45
+ try {
46
+ _browser = await pw.chromium.launch({ headless });
47
+ _page = await _browser.newPage({ viewport: { width: 1280, height: 720 } });
48
+ await _page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
49
+ _activeSessionId = sessionId;
50
+ return { page: _page, launched: true };
51
+ }
52
+ catch (e) {
53
+ await closeBrowser();
54
+ return {
55
+ page: null,
56
+ launched: false,
57
+ error: `Browser launch failed: ${e.message}. Try: npx playwright install chromium`,
58
+ };
59
+ }
60
+ }
61
+ async function closeBrowser() {
62
+ try {
63
+ if (_browser)
64
+ await _browser.close();
65
+ }
66
+ catch {
67
+ /* ignore close errors */
68
+ }
69
+ _browser = null;
70
+ _page = null;
71
+ _activeSessionId = null;
72
+ }
73
+ async function executeAction(page, action, target, inputValue) {
74
+ const start = Date.now();
75
+ try {
76
+ switch (action) {
77
+ case "click":
78
+ if (!target)
79
+ return { success: false, observation: "No target selector for click", durationMs: Date.now() - start };
80
+ await page.click(target, { timeout: 10000 });
81
+ break;
82
+ case "type":
83
+ if (!target || !inputValue)
84
+ return { success: false, observation: "Need target and inputValue for type", durationMs: Date.now() - start };
85
+ await page.fill(target, inputValue, { timeout: 10000 });
86
+ break;
87
+ case "hover":
88
+ if (!target)
89
+ return { success: false, observation: "No target selector for hover", durationMs: Date.now() - start };
90
+ await page.hover(target, { timeout: 10000 });
91
+ break;
92
+ case "navigate":
93
+ if (!inputValue)
94
+ return { success: false, observation: "No URL for navigate (use inputValue)", durationMs: Date.now() - start };
95
+ await page.goto(inputValue, { waitUntil: "domcontentloaded", timeout: 30000 });
96
+ break;
97
+ case "scroll":
98
+ await page.evaluate(`window.scrollBy(0, ${parseInt(inputValue ?? "500", 10)})`);
99
+ break;
100
+ case "submit":
101
+ if (target)
102
+ await page.press(target, "Enter", { timeout: 10000 });
103
+ else
104
+ await page.keyboard.press("Enter");
105
+ break;
106
+ case "keypress":
107
+ await page.keyboard.press(inputValue ?? "Enter");
108
+ break;
109
+ case "focus":
110
+ if (target)
111
+ await page.focus(target, { timeout: 10000 });
112
+ break;
113
+ case "wait":
114
+ await page.waitForTimeout(parseInt(inputValue ?? "1000", 10));
115
+ break;
116
+ case "assert": {
117
+ if (!target)
118
+ return { success: true, observation: "No target for assert — skipped", durationMs: Date.now() - start };
119
+ const visible = await page.isVisible(target);
120
+ if (!visible)
121
+ return { success: false, observation: `Element not visible: ${target}`, durationMs: Date.now() - start };
122
+ break;
123
+ }
124
+ default:
125
+ return { success: true, observation: `Action '${action}' not auto-executable — logged only`, durationMs: Date.now() - start };
126
+ }
127
+ await page.waitForTimeout(300);
128
+ const title = await page.title();
129
+ const url = page.url();
130
+ return { success: true, observation: `Executed. Page: "${title}" (${url})`, durationMs: Date.now() - start };
131
+ }
132
+ catch (e) {
133
+ return { success: false, observation: `Action failed: ${e.message}`, durationMs: Date.now() - start };
134
+ }
135
+ }
136
+ async function autoDiscoverComponents(page) {
137
+ // Runs in the browser context via page.evaluate — passed as a string
138
+ // to avoid TypeScript DOM type errors in the Node compilation target.
139
+ const script = `(() => {
140
+ const found = [];
141
+ const landmarks = {
142
+ nav: "menu", header: "header", footer: "footer", main: "section",
143
+ aside: "sidebar", form: "form", dialog: "modal", table: "table",
144
+ };
145
+ for (const [tag, type] of Object.entries(landmarks)) {
146
+ document.querySelectorAll(tag).forEach((el, i) => {
147
+ const label = el.getAttribute("aria-label") || el.getAttribute("id") || tag + "_" + i;
148
+ const selector = el.id ? "#" + el.id : tag + ":nth-of-type(" + (i + 1) + ")";
149
+ found.push({
150
+ name: label, type, selector,
151
+ children: el.querySelectorAll("a, button, input, select, textarea, [role='button'], [role='link']").length,
152
+ });
153
+ });
154
+ }
155
+ const interSel = "button, [role='button'], a[href], input, select, textarea";
156
+ const insideLandmark = new Set();
157
+ for (const tag of Object.keys(landmarks)) {
158
+ document.querySelectorAll(tag).forEach(parent => {
159
+ parent.querySelectorAll(interSel).forEach(child => insideLandmark.add(child));
160
+ });
161
+ }
162
+ const orphans = [...document.querySelectorAll(interSel)].filter(el => !insideLandmark.has(el));
163
+ if (orphans.length > 0) {
164
+ found.push({ name: "Ungrouped Interactive Elements", type: "section", selector: "body", children: orphans.length });
165
+ }
166
+ return found;
167
+ })()`;
168
+ return page.evaluate(script);
169
+ }
170
+ function buildTreeXml(components, bugs, parentId = null, indent = 0) {
171
+ const children = components.filter(c => c.parent_id === parentId);
172
+ if (children.length === 0)
173
+ return "";
174
+ const pad = " ".repeat(indent);
175
+ let xml = "";
176
+ for (const c of children) {
177
+ const cBugs = bugs.filter(b => b.component_id === c.id);
178
+ const bugAttr = cBugs.length > 0 ? ` bugs="${cBugs.length}"` : "";
179
+ const statusAttr = ` status="${c.status}"`;
180
+ const agentAttr = c.agent_id ? ` agent="${c.agent_id}"` : "";
181
+ const interactionAttr = c.interaction_count > 0 ? ` interactions="${c.interaction_count}"` : "";
182
+ const childXml = buildTreeXml(components, bugs, c.id, indent + 1);
183
+ const bugXml = cBugs.map(b => `${pad} <bug severity="${b.severity}" category="${b.category}" status="${b.status}">${b.title}</bug>`).join("\n");
184
+ const hasChildren = childXml || bugXml;
185
+ if (hasChildren) {
186
+ xml += `${pad}<${c.component_type} name="${c.name}" id="${c.id}"${statusAttr}${agentAttr}${interactionAttr}${bugAttr}>\n`;
187
+ if (bugXml)
188
+ xml += bugXml + "\n";
189
+ if (childXml)
190
+ xml += childXml;
191
+ xml += `${pad}</${c.component_type}>\n`;
192
+ }
193
+ else {
194
+ xml += `${pad}<${c.component_type} name="${c.name}" id="${c.id}"${statusAttr}${agentAttr}${interactionAttr}${bugAttr} />\n`;
195
+ }
196
+ }
197
+ return xml;
198
+ }
199
+ function buildMermaidDiagram(components, bugs) {
200
+ if (components.length === 0)
201
+ return "graph TD\n empty[No components registered]";
202
+ let mermaid = "graph TD\n";
203
+ const sanitize = (s) => s.replace(/["\[\](){}]/g, "").replace(/\s+/g, "_");
204
+ for (const c of components) {
205
+ const cBugs = bugs.filter(b => b.component_id === c.id);
206
+ const label = cBugs.length > 0
207
+ ? `${c.name} [${c.status}] 🐛${cBugs.length}`
208
+ : `${c.name} [${c.status}]`;
209
+ const style = c.status === "completed"
210
+ ? ":::completed"
211
+ : cBugs.some(b => b.severity === "critical")
212
+ ? ":::critical"
213
+ : cBugs.length > 0
214
+ ? ":::hasBugs"
215
+ : "";
216
+ mermaid += ` ${sanitize(c.id)}["${label}"]${style}\n`;
217
+ if (c.parent_id) {
218
+ mermaid += ` ${sanitize(c.parent_id)} --> ${sanitize(c.id)}\n`;
219
+ }
220
+ }
221
+ mermaid += "\n classDef completed fill:#d4edda,stroke:#28a745\n";
222
+ mermaid += " classDef critical fill:#f8d7da,stroke:#dc3545\n";
223
+ mermaid += " classDef hasBugs fill:#fff3cd,stroke:#ffc107\n";
224
+ return mermaid;
225
+ }
226
+ // ── Tools ────────────────────────────────────────────────────────────────
227
+ export const uiUxDiveTools = [
228
+ // 1. Start a dive session
229
+ {
230
+ name: "start_ui_dive",
231
+ description: "Initialize a UI/UX Full Dive session. Auto-launches a headless Playwright browser if installed (zero setup). Navigates to the app URL and optionally auto-discovers page components from the DOM. If Playwright is not installed, falls back to manual logging mode where you drive the browser yourself (via playwright-mcp, mobile-mcp, or manual browsing) and just use the dive tools for structured logging. Returns the session_id needed by all subsequent tools.",
232
+ inputSchema: {
233
+ type: "object",
234
+ properties: {
235
+ appUrl: {
236
+ type: "string",
237
+ description: "URL of the application to dive into (e.g. http://localhost:3000, https://app.example.com)",
238
+ },
239
+ appName: {
240
+ type: "string",
241
+ description: "Human-readable name (e.g. 'NodeBench AI Dashboard')",
242
+ },
243
+ agentCount: {
244
+ type: "number",
245
+ description: "Number of parallel subagents planned (default: 1, set higher for parallel swarm)",
246
+ },
247
+ headless: {
248
+ type: "boolean",
249
+ description: "Run browser in headless mode (default: true). Set false to see the browser window.",
250
+ },
251
+ autoDiscover: {
252
+ type: "boolean",
253
+ description: "Auto-discover and register page components from the DOM (default: true). Scans for nav, header, footer, forms, modals, tables, sidebars, and interactive elements.",
254
+ },
255
+ metadata: {
256
+ type: "object",
257
+ description: "Optional JSON metadata (e.g. { viewport: '1280x720', auth: 'guest' })",
258
+ },
259
+ },
260
+ required: ["appUrl"],
261
+ },
262
+ handler: async (args) => {
263
+ const { appUrl, appName, agentCount, metadata, headless, autoDiscover } = args;
264
+ const db = getDb();
265
+ const id = genId("dive");
266
+ db.prepare("INSERT INTO ui_dive_sessions (id, app_url, app_name, agent_count, metadata) VALUES (?, ?, ?, ?, ?)").run(id, appUrl, appName ?? null, agentCount ?? 1, metadata ? JSON.stringify(metadata) : null);
267
+ // Try to launch browser
268
+ const browserResult = await ensureBrowser(appUrl, id, headless !== false);
269
+ // Auto-discover components if browser is active
270
+ let discoveredComponents = [];
271
+ const registeredIds = [];
272
+ if (browserResult.page && autoDiscover !== false) {
273
+ try {
274
+ discoveredComponents = await autoDiscoverComponents(browserResult.page);
275
+ // Auto-register discovered components
276
+ for (const comp of discoveredComponents) {
277
+ const compId = genId("comp");
278
+ db.prepare("INSERT INTO ui_dive_components (id, session_id, parent_id, name, component_type, selector, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)").run(compId, id, null, comp.name, comp.type, comp.selector, JSON.stringify({ autoDiscovered: true, interactiveChildren: comp.children }));
279
+ registeredIds.push(compId);
280
+ }
281
+ // Set first as root
282
+ if (registeredIds.length > 0) {
283
+ db.prepare("UPDATE ui_dive_sessions SET root_component_id = ? WHERE id = ?").run(registeredIds[0], id);
284
+ }
285
+ }
286
+ catch {
287
+ /* auto-discover is best-effort */
288
+ }
289
+ }
290
+ return {
291
+ sessionId: id,
292
+ appUrl,
293
+ appName: appName ?? null,
294
+ agentCount: agentCount ?? 1,
295
+ status: "active",
296
+ browser: {
297
+ active: !!browserResult.page,
298
+ launched: browserResult.launched,
299
+ mode: browserResult.page ? "auto" : "manual",
300
+ ...(browserResult.error ? { setupHint: browserResult.error } : {}),
301
+ },
302
+ autoDiscovery: {
303
+ componentsFound: discoveredComponents.length,
304
+ components: discoveredComponents.map((c, i) => ({
305
+ componentId: registeredIds[i],
306
+ ...c,
307
+ })),
308
+ },
309
+ _hint: browserResult.page
310
+ ? `Browser launched and ${discoveredComponents.length} components auto-discovered. Use log_interaction with CSS selectors in 'target' — actions will auto-execute in the browser. Call dive_snapshot for screenshots.`
311
+ : `Manual mode: drive the browser yourself and use log_interaction to record what you observe. Install Playwright for auto-execution: npm install playwright && npx playwright install chromium`,
312
+ _workflow: [
313
+ "1. Components auto-discovered (or register manually with register_component)",
314
+ "2. Assign subagents: start_component_flow({ componentId, agentId })",
315
+ "3. Each subagent: log_interaction (auto-executes clicks/types if browser active)",
316
+ "4. Tag bugs: tag_ui_bug for any issues found",
317
+ "5. dive_snapshot for visual evidence at any point",
318
+ "6. end_component_flow when each component is done",
319
+ "7. get_dive_report for final comprehensive report",
320
+ ],
321
+ };
322
+ },
323
+ },
324
+ // 2. Register a component in the tree
325
+ {
326
+ name: "register_component",
327
+ description: "Register a UI component in the dive tree. Components form a hierarchy: page → section → form/modal/list → button/input/link. Set parentId to nest under an existing component (null for top-level). The main agent registers the initial tree, then subagents can add child components they discover during traversal. Returns the component_id for use in start_component_flow and log_interaction.",
328
+ inputSchema: {
329
+ type: "object",
330
+ properties: {
331
+ sessionId: { type: "string", description: "Dive session ID from start_ui_dive" },
332
+ name: { type: "string", description: "Component name (e.g. 'Login Form', 'Navigation Bar', 'Settings Modal')" },
333
+ componentType: {
334
+ type: "string",
335
+ description: "Type: page, section, form, modal, menu, list, card, button, input, link, table, tab, drawer, dialog, toast, tooltip, dropdown, sidebar, header, footer, hero, chart, media",
336
+ },
337
+ parentId: { type: "string", description: "Parent component ID (null/omit for top-level page)" },
338
+ selector: { type: "string", description: "CSS selector or data-testid for identification (e.g. '[data-testid=login-form]', '#nav-bar')" },
339
+ metadata: { type: "object", description: "Optional JSON (e.g. { route: '/settings', requiresAuth: true })" },
340
+ },
341
+ required: ["sessionId", "name", "componentType"],
342
+ },
343
+ handler: async (args) => {
344
+ const { sessionId, name, componentType, parentId, selector, metadata } = args;
345
+ const db = getDb();
346
+ // Validate session exists
347
+ const session = db.prepare("SELECT id, status FROM ui_dive_sessions WHERE id = ?").get(sessionId);
348
+ if (!session)
349
+ return { error: true, message: `Session not found: ${sessionId}` };
350
+ if (session.status !== "active")
351
+ return { error: true, message: `Session is ${session.status}, not active` };
352
+ // Validate parent if provided
353
+ if (parentId) {
354
+ const parent = db.prepare("SELECT id FROM ui_dive_components WHERE id = ? AND session_id = ?").get(parentId, sessionId);
355
+ if (!parent)
356
+ return { error: true, message: `Parent component not found: ${parentId}` };
357
+ }
358
+ const id = genId("comp");
359
+ db.prepare("INSERT INTO ui_dive_components (id, session_id, parent_id, name, component_type, selector, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, sessionId, parentId ?? null, name, componentType, selector ?? null, metadata ? JSON.stringify(metadata) : null);
360
+ // Set as root if first top-level component
361
+ if (!parentId) {
362
+ const rootCheck = db.prepare("SELECT root_component_id FROM ui_dive_sessions WHERE id = ?").get(sessionId);
363
+ if (!rootCheck.root_component_id) {
364
+ db.prepare("UPDATE ui_dive_sessions SET root_component_id = ? WHERE id = ?").run(id, sessionId);
365
+ }
366
+ }
367
+ const siblingCount = db.prepare("SELECT COUNT(*) as c FROM ui_dive_components WHERE session_id = ? AND parent_id IS ?").get(sessionId, parentId ?? null);
368
+ return {
369
+ componentId: id,
370
+ name,
371
+ componentType,
372
+ parentId: parentId ?? null,
373
+ selector: selector ?? null,
374
+ siblingCount: siblingCount.c,
375
+ status: "pending",
376
+ _hint: `Component registered. Claim it for traversal: start_component_flow({ componentId: "${id}", agentId: "agent_1" }). Or register child components under it.`,
377
+ };
378
+ },
379
+ },
380
+ // 3. Start a component flow (claim for subagent)
381
+ {
382
+ name: "start_component_flow",
383
+ description: "Claim a component for traversal by a specific subagent. Marks it as 'in_progress' and assigns the agent_id. This provides context isolation — each subagent works on its own component independently. The subagent should then log_interaction for each step and tag_ui_bug for any issues found. Call end_component_flow when done.",
384
+ inputSchema: {
385
+ type: "object",
386
+ properties: {
387
+ componentId: { type: "string", description: "Component ID from register_component" },
388
+ agentId: { type: "string", description: "Unique agent identifier (e.g. 'agent_1', 'nav_agent', 'form_tester')" },
389
+ },
390
+ required: ["componentId"],
391
+ },
392
+ handler: async (args) => {
393
+ const { componentId, agentId } = args;
394
+ const db = getDb();
395
+ const comp = db.prepare("SELECT c.*, s.app_url, s.app_name FROM ui_dive_components c JOIN ui_dive_sessions s ON c.session_id = s.id WHERE c.id = ?").get(componentId);
396
+ if (!comp)
397
+ return { error: true, message: `Component not found: ${componentId}` };
398
+ if (comp.status === "in_progress") {
399
+ return { error: true, message: `Component already claimed by agent '${comp.agent_id}'. Use a different component or wait.` };
400
+ }
401
+ if (comp.status === "completed") {
402
+ return { error: true, message: `Component already completed. Register a new component for additional testing.` };
403
+ }
404
+ const aid = agentId ?? `agent_${Date.now()}`;
405
+ db.prepare("UPDATE ui_dive_components SET status = 'in_progress', agent_id = ? WHERE id = ?").run(aid, componentId);
406
+ // Get children for context
407
+ const children = db.prepare("SELECT id, name, component_type, status FROM ui_dive_components WHERE parent_id = ?").all(componentId);
408
+ return {
409
+ claimed: true,
410
+ componentId,
411
+ agentId: aid,
412
+ component: {
413
+ name: comp.name,
414
+ type: comp.component_type,
415
+ selector: comp.selector,
416
+ parentId: comp.parent_id,
417
+ },
418
+ app: { url: comp.app_url, name: comp.app_name },
419
+ children: children.map(ch => ({ id: ch.id, name: ch.name, type: ch.component_type, status: ch.status })),
420
+ _hint: `Component claimed. Now interact with it: log_interaction({ componentId: "${componentId}", action: "click", target: "...", result: "success", observation: "..." }). Tag bugs with tag_ui_bug. Call end_component_flow when done.`,
421
+ };
422
+ },
423
+ },
424
+ // 4. Log an interaction (auto-executes when browser is active)
425
+ {
426
+ name: "log_interaction",
427
+ description: "Log and optionally auto-execute an interaction step. If the built-in Playwright browser is active (launched by start_ui_dive), the action is automatically executed in the browser — just provide a CSS selector in 'target' and the result/observation are filled in for you. If no browser is active (manual mode), this logs your observation as-is. Actions: click, type, hover, scroll, navigate, submit, keypress, focus, wait, assert.",
428
+ inputSchema: {
429
+ type: "object",
430
+ properties: {
431
+ componentId: { type: "string", description: "Component ID being tested" },
432
+ action: {
433
+ type: "string",
434
+ description: "Interaction type: click, type, hover, scroll, navigate, submit, keypress, focus, wait, assert",
435
+ },
436
+ target: { type: "string", description: "CSS selector for auto-execution (e.g. '#submit-btn', '[data-testid=email]', 'button:has-text(\"Login\")'), or human-readable description in manual mode" },
437
+ inputValue: { type: "string", description: "Value to type/enter (for type, submit actions), URL (for navigate), pixels (for scroll), key name (for keypress)" },
438
+ result: {
439
+ type: "string",
440
+ description: "Outcome (auto-filled if browser active): success, error, unexpected, timeout, crash, no_response, partial",
441
+ },
442
+ observation: {
443
+ type: "string",
444
+ description: "What happened (auto-filled if browser active). Manual mode: describe what you observed.",
445
+ },
446
+ autoExecute: {
447
+ type: "boolean",
448
+ description: "Auto-execute the action in the browser (default: true). Set false to log-only even when browser is active.",
449
+ },
450
+ durationMs: { type: "number", description: "How long the interaction took in ms (auto-filled if browser active)" },
451
+ screenshotRef: { type: "string", description: "Reference to a screenshot capture (optional)" },
452
+ },
453
+ required: ["componentId", "action"],
454
+ },
455
+ handler: async (args) => {
456
+ const { componentId, action, target, inputValue, result, observation, autoExecute, durationMs, screenshotRef } = args;
457
+ const db = getDb();
458
+ const comp = db.prepare("SELECT id, session_id, status, interaction_count FROM ui_dive_components WHERE id = ?").get(componentId);
459
+ if (!comp)
460
+ return { error: true, message: `Component not found: ${componentId}` };
461
+ // Auto-execute if browser is active
462
+ let finalResult = result ?? "success";
463
+ let finalObservation = observation ?? "";
464
+ let finalDuration = durationMs ?? null;
465
+ let autoExecuted = false;
466
+ if (_page && _activeSessionId === comp.session_id && autoExecute !== false) {
467
+ const execResult = await executeAction(_page, action, target, inputValue);
468
+ finalResult = execResult.success ? "success" : "error";
469
+ finalObservation = execResult.observation + (observation ? ` | ${observation}` : "");
470
+ finalDuration = execResult.durationMs;
471
+ autoExecuted = true;
472
+ }
473
+ const seqNum = comp.interaction_count + 1;
474
+ const id = genId("int");
475
+ db.prepare(`INSERT INTO ui_dive_interactions (id, session_id, component_id, action, target, input_value, result, observation, screenshot_ref, duration_ms, sequence_num)
476
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, comp.session_id, componentId, action, target ?? null, inputValue ?? null, finalResult, finalObservation || null, screenshotRef ?? null, finalDuration, seqNum);
477
+ db.prepare("UPDATE ui_dive_components SET interaction_count = ? WHERE id = ?").run(seqNum, componentId);
478
+ return {
479
+ interactionId: id,
480
+ sequenceNum: seqNum,
481
+ action,
482
+ target: target ?? null,
483
+ result: finalResult,
484
+ observation: finalObservation || null,
485
+ autoExecuted,
486
+ durationMs: finalDuration,
487
+ _hint: finalResult !== "success"
488
+ ? `Non-success result (${finalResult}). Consider tagging a bug: tag_ui_bug({ componentId: "${componentId}", interactionId: "${id}", severity: "...", category: "functional", title: "..." })`
489
+ : `Interaction #${seqNum} logged. Continue testing or call end_component_flow when done.`,
490
+ };
491
+ },
492
+ },
493
+ // 5. End a component flow
494
+ {
495
+ name: "end_component_flow",
496
+ description: "Complete a component's traversal flow. Marks it as 'completed' with a summary. Call this after all interactions and bug tagging for a component are done. The summary should capture key findings, overall health, and any patterns observed.",
497
+ inputSchema: {
498
+ type: "object",
499
+ properties: {
500
+ componentId: { type: "string", description: "Component ID to complete" },
501
+ summary: {
502
+ type: "string",
503
+ description: "Summary of findings (e.g. 'Login form works correctly. Found 1 accessibility issue with missing aria-label on password field.')",
504
+ },
505
+ },
506
+ required: ["componentId", "summary"],
507
+ },
508
+ handler: async (args) => {
509
+ const { componentId, summary } = args;
510
+ const db = getDb();
511
+ const comp = db.prepare("SELECT id, session_id, name, interaction_count, bug_count FROM ui_dive_components WHERE id = ?").get(componentId);
512
+ if (!comp)
513
+ return { error: true, message: `Component not found: ${componentId}` };
514
+ db.prepare("UPDATE ui_dive_components SET status = 'completed', summary = ?, completed_at = datetime('now') WHERE id = ?").run(summary, componentId);
515
+ // Check session progress
516
+ const total = db.prepare("SELECT COUNT(*) as c FROM ui_dive_components WHERE session_id = ?").get(comp.session_id);
517
+ const completed = db.prepare("SELECT COUNT(*) as c FROM ui_dive_components WHERE session_id = ? AND status = 'completed'").get(comp.session_id);
518
+ const totalBugs = db.prepare("SELECT COUNT(*) as c FROM ui_dive_bugs WHERE session_id = ?").get(comp.session_id);
519
+ return {
520
+ completed: true,
521
+ componentId,
522
+ name: comp.name,
523
+ interactionCount: comp.interaction_count,
524
+ bugCount: comp.bug_count,
525
+ summary,
526
+ sessionProgress: {
527
+ completedComponents: completed.c,
528
+ totalComponents: total.c,
529
+ remainingComponents: total.c - completed.c,
530
+ totalBugsFound: totalBugs.c,
531
+ },
532
+ _hint: completed.c === total.c
533
+ ? `All components completed! Generate the final report: get_dive_report({ sessionId: "${comp.session_id}" })`
534
+ : `${total.c - completed.c} components remaining. Continue with the next component or get_dive_tree for current state.`,
535
+ };
536
+ },
537
+ },
538
+ // 6. Tag a bug
539
+ {
540
+ name: "tag_ui_bug",
541
+ description: "Tag a bug to a specific component (and optionally a specific interaction). Bugs are categorized by severity (critical/high/medium/low) and category (visual, functional, accessibility, performance, responsive, ux, content, security). Each bug is linked to its component in the tree for precise debugging.",
542
+ inputSchema: {
543
+ type: "object",
544
+ properties: {
545
+ componentId: { type: "string", description: "Component where the bug was found" },
546
+ interactionId: { type: "string", description: "Specific interaction that triggered the bug (optional)" },
547
+ severity: { type: "string", description: "Bug severity: critical, high, medium, low" },
548
+ category: {
549
+ type: "string",
550
+ description: "Bug category: visual (layout/styling), functional (broken behavior), accessibility (a11y), performance (slow/laggy), responsive (breakpoint issues), ux (confusing/poor UX), content (text/copy issues), security (auth/data exposure)",
551
+ },
552
+ title: { type: "string", description: "Short bug title (e.g. 'Submit button unresponsive on mobile')" },
553
+ description: { type: "string", description: "Detailed description of the bug" },
554
+ expected: { type: "string", description: "What should happen" },
555
+ actual: { type: "string", description: "What actually happens" },
556
+ screenshotRef: { type: "string", description: "Reference to a screenshot showing the bug (optional)" },
557
+ },
558
+ required: ["componentId", "severity", "category", "title"],
559
+ },
560
+ handler: async (args) => {
561
+ const { componentId, interactionId, severity, category, title, description, expected, actual, screenshotRef } = args;
562
+ const db = getDb();
563
+ const comp = db.prepare("SELECT id, session_id, bug_count FROM ui_dive_components WHERE id = ?").get(componentId);
564
+ if (!comp)
565
+ return { error: true, message: `Component not found: ${componentId}` };
566
+ const id = genId("bug");
567
+ db.prepare(`INSERT INTO ui_dive_bugs (id, session_id, component_id, interaction_id, severity, category, title, description, expected, actual, screenshot_ref)
568
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, comp.session_id, componentId, interactionId ?? null, severity, category, title, description ?? null, expected ?? null, actual ?? null, screenshotRef ?? null);
569
+ const newBugCount = comp.bug_count + 1;
570
+ db.prepare("UPDATE ui_dive_components SET bug_count = ? WHERE id = ?").run(newBugCount, componentId);
571
+ const sessionBugs = db.prepare("SELECT severity, COUNT(*) as c FROM ui_dive_bugs WHERE session_id = ? GROUP BY severity").all(comp.session_id);
572
+ return {
573
+ bugId: id,
574
+ severity,
575
+ category,
576
+ title,
577
+ componentBugCount: newBugCount,
578
+ sessionBugSummary: Object.fromEntries(sessionBugs.map(b => [b.severity, b.c])),
579
+ _hint: `Bug tagged. Continue testing this component or call end_component_flow when done.`,
580
+ };
581
+ },
582
+ },
583
+ // 7. Get the component tree
584
+ {
585
+ name: "get_dive_tree",
586
+ description: "Get the full XML-like component tree for a dive session. Shows all registered components in their hierarchy with status, agent assignments, interaction counts, and bug counts. Also returns a Mermaid diagram for visual rendering. Use this for a quick overview of app structure and dive progress.",
587
+ inputSchema: {
588
+ type: "object",
589
+ properties: {
590
+ sessionId: { type: "string", description: "Dive session ID" },
591
+ format: {
592
+ type: "string",
593
+ description: "Output format: 'xml' (default — XML-like tree), 'mermaid' (Mermaid flowchart), 'both' (both formats)",
594
+ },
595
+ },
596
+ required: ["sessionId"],
597
+ },
598
+ handler: async (args) => {
599
+ const { sessionId, format } = args;
600
+ const db = getDb();
601
+ const session = db.prepare("SELECT * FROM ui_dive_sessions WHERE id = ?").get(sessionId);
602
+ if (!session)
603
+ return { error: true, message: `Session not found: ${sessionId}` };
604
+ const components = db.prepare("SELECT * FROM ui_dive_components WHERE session_id = ? ORDER BY created_at").all(sessionId);
605
+ const bugs = db.prepare("SELECT * FROM ui_dive_bugs WHERE session_id = ? ORDER BY created_at").all(sessionId);
606
+ const fmt = format ?? "both";
607
+ const completed = components.filter(c => c.status === "completed").length;
608
+ const inProgress = components.filter(c => c.status === "in_progress").length;
609
+ const pending = components.filter(c => c.status === "pending").length;
610
+ const result = {
611
+ session: {
612
+ id: session.id,
613
+ appUrl: session.app_url,
614
+ appName: session.app_name,
615
+ status: session.status,
616
+ },
617
+ stats: {
618
+ totalComponents: components.length,
619
+ completed,
620
+ inProgress,
621
+ pending,
622
+ totalBugs: bugs.length,
623
+ bugsBySeverity: Object.fromEntries(["critical", "high", "medium", "low"]
624
+ .map(s => [s, bugs.filter(b => b.severity === s).length])
625
+ .filter(([, c]) => c > 0)),
626
+ },
627
+ };
628
+ if (fmt === "xml" || fmt === "both") {
629
+ const xml = `<app name="${session.app_name ?? session.app_url}" url="${session.app_url}">\n${buildTreeXml(components, bugs, null, 1)}</app>`;
630
+ result.xmlTree = xml;
631
+ }
632
+ if (fmt === "mermaid" || fmt === "both") {
633
+ result.mermaidDiagram = buildMermaidDiagram(components, bugs);
634
+ }
635
+ return result;
636
+ },
637
+ },
638
+ // 8. Generate final report
639
+ {
640
+ name: "get_dive_report",
641
+ description: "Generate a comprehensive UI/UX Full Dive report for a session. Includes: executive summary, component tree, all bugs grouped by severity, per-component interaction traces, Mermaid diagram, and actionable recommendations. Optionally marks the session as completed. This is the final deliverable after all subagents have finished their traversals.",
642
+ inputSchema: {
643
+ type: "object",
644
+ properties: {
645
+ sessionId: { type: "string", description: "Dive session ID" },
646
+ completeSession: { type: "boolean", description: "Mark the session as completed (default: true)" },
647
+ },
648
+ required: ["sessionId"],
649
+ },
650
+ handler: async (args) => {
651
+ const { sessionId, completeSession } = args;
652
+ const db = getDb();
653
+ const session = db.prepare("SELECT * FROM ui_dive_sessions WHERE id = ?").get(sessionId);
654
+ if (!session)
655
+ return { error: true, message: `Session not found: ${sessionId}` };
656
+ const components = db.prepare("SELECT * FROM ui_dive_components WHERE session_id = ? ORDER BY created_at").all(sessionId);
657
+ const bugs = db.prepare("SELECT b.*, c.name as component_name, c.component_type FROM ui_dive_bugs b JOIN ui_dive_components c ON b.component_id = c.id WHERE b.session_id = ? ORDER BY CASE b.severity WHEN 'critical' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 WHEN 'low' THEN 4 END, b.created_at").all(sessionId);
658
+ const interactions = db.prepare("SELECT i.*, c.name as component_name FROM ui_dive_interactions i JOIN ui_dive_components c ON i.component_id = c.id WHERE i.session_id = ? ORDER BY c.id, i.sequence_num").all(sessionId);
659
+ // Mark session complete and close browser
660
+ if (completeSession !== false) {
661
+ db.prepare("UPDATE ui_dive_sessions SET status = 'completed', completed_at = datetime('now') WHERE id = ?").run(sessionId);
662
+ if (_activeSessionId === sessionId) {
663
+ await closeBrowser();
664
+ }
665
+ }
666
+ // Build per-component summaries
667
+ const componentDetails = components.map(c => {
668
+ const cBugs = bugs.filter((b) => b.component_id === c.id);
669
+ const cInteractions = interactions.filter((i) => i.component_id === c.id);
670
+ return {
671
+ id: c.id,
672
+ name: c.name,
673
+ type: c.component_type,
674
+ status: c.status,
675
+ agent: c.agent_id,
676
+ summary: c.summary,
677
+ interactionCount: cInteractions.length,
678
+ bugs: cBugs.map((b) => ({
679
+ id: b.id,
680
+ severity: b.severity,
681
+ category: b.category,
682
+ title: b.title,
683
+ description: b.description,
684
+ expected: b.expected,
685
+ actual: b.actual,
686
+ })),
687
+ interactions: cInteractions.map((i) => ({
688
+ seq: i.sequence_num,
689
+ action: i.action,
690
+ target: i.target,
691
+ result: i.result,
692
+ observation: i.observation,
693
+ })),
694
+ };
695
+ });
696
+ const completed = components.filter(c => c.status === "completed").length;
697
+ const criticalBugs = bugs.filter((b) => b.severity === "critical").length;
698
+ const highBugs = bugs.filter((b) => b.severity === "high").length;
699
+ const healthScore = components.length > 0
700
+ ? Math.max(0, Math.round(100 - (criticalBugs * 25) - (highBugs * 10) - (bugs.length * 2)))
701
+ : 0;
702
+ const xmlTree = `<app name="${session.app_name ?? session.app_url}" url="${session.app_url}">\n${buildTreeXml(components, bugs, null, 1)}</app>`;
703
+ const mermaid = buildMermaidDiagram(components, bugs);
704
+ return {
705
+ report: {
706
+ title: `UI/UX Full Dive Report: ${session.app_name ?? session.app_url}`,
707
+ appUrl: session.app_url,
708
+ sessionId,
709
+ createdAt: session.created_at,
710
+ completedAt: new Date().toISOString(),
711
+ healthScore,
712
+ healthGrade: healthScore >= 90 ? "A" : healthScore >= 75 ? "B" : healthScore >= 60 ? "C" : healthScore >= 40 ? "D" : "F",
713
+ },
714
+ summary: {
715
+ totalComponents: components.length,
716
+ completedComponents: completed,
717
+ totalInteractions: interactions.length,
718
+ totalBugs: bugs.length,
719
+ bugsBySeverity: {
720
+ critical: criticalBugs,
721
+ high: highBugs,
722
+ medium: bugs.filter((b) => b.severity === "medium").length,
723
+ low: bugs.filter((b) => b.severity === "low").length,
724
+ },
725
+ bugsByCategory: Object.fromEntries([...new Set(bugs.map((b) => b.category))].map(cat => [
726
+ cat,
727
+ bugs.filter((b) => b.category === cat).length,
728
+ ])),
729
+ agentsUsed: [...new Set(components.filter(c => c.agent_id).map(c => c.agent_id))],
730
+ },
731
+ xmlTree,
732
+ mermaidDiagram: mermaid,
733
+ components: componentDetails,
734
+ recommendations: [
735
+ ...(criticalBugs > 0 ? [`FIX IMMEDIATELY: ${criticalBugs} critical bug(s) found — these block usability.`] : []),
736
+ ...(highBugs > 0 ? [`HIGH PRIORITY: ${highBugs} high-severity bug(s) should be fixed before next release.`] : []),
737
+ ...(completed < components.length ? [`${components.length - completed} component(s) not fully tested — consider re-running the dive.`] : []),
738
+ ...(interactions.length === 0 ? ["No interactions logged — ensure subagents are calling log_interaction during traversal."] : []),
739
+ ...(bugs.some((b) => b.category === "accessibility") ? ["Accessibility issues found — run an a11y audit with axe-core or lighthouse."] : []),
740
+ ...(bugs.some((b) => b.category === "responsive") ? ["Responsive issues found — test with capture_responsive_suite across breakpoints."] : []),
741
+ ],
742
+ };
743
+ },
744
+ },
745
+ // 9. Take a screenshot / accessibility snapshot
746
+ {
747
+ name: "dive_snapshot",
748
+ description: "Capture a screenshot or accessibility snapshot of the current page during a dive session. Requires the built-in Playwright browser to be active (launched by start_ui_dive). Returns the screenshot as an inline image for multimodal agents, or an accessibility tree as text. Use this to capture visual evidence of bugs or document the current state of a component.",
749
+ rawContent: true,
750
+ inputSchema: {
751
+ type: "object",
752
+ properties: {
753
+ sessionId: { type: "string", description: "Dive session ID (verifies browser belongs to this session)" },
754
+ mode: {
755
+ type: "string",
756
+ description: "Capture mode: 'screenshot' (default — full page PNG), 'viewport' (visible area only), 'accessibility' (a11y tree as text)",
757
+ },
758
+ selector: { type: "string", description: "CSS selector to screenshot a specific element (optional — screenshots the element only)" },
759
+ label: { type: "string", description: "Label for this snapshot (e.g. 'after-login', 'error-state', 'mobile-nav-open')" },
760
+ },
761
+ required: ["sessionId"],
762
+ },
763
+ handler: async (args) => {
764
+ const { sessionId, mode, selector, label } = args;
765
+ if (!_page || _activeSessionId !== sessionId) {
766
+ return [{
767
+ type: "text",
768
+ text: JSON.stringify({
769
+ error: true,
770
+ message: "No active browser for this session. Start a dive with start_ui_dive first, or install Playwright: npm install playwright && npx playwright install chromium",
771
+ }),
772
+ }];
773
+ }
774
+ const captureMode = mode ?? "screenshot";
775
+ if (captureMode === "accessibility") {
776
+ try {
777
+ const snapshot = await _page.accessibility.snapshot();
778
+ return [{
779
+ type: "text",
780
+ text: JSON.stringify({
781
+ label: label ?? "accessibility-snapshot",
782
+ pageTitle: await _page.title(),
783
+ pageUrl: _page.url(),
784
+ accessibilityTree: snapshot,
785
+ }, null, 2),
786
+ }];
787
+ }
788
+ catch (e) {
789
+ return [{ type: "text", text: JSON.stringify({ error: true, message: `Accessibility snapshot failed: ${e.message}` }) }];
790
+ }
791
+ }
792
+ // Screenshot mode
793
+ try {
794
+ const screenshotOpts = {
795
+ type: "png",
796
+ fullPage: captureMode !== "viewport",
797
+ };
798
+ let screenshotBuf;
799
+ if (selector) {
800
+ const el = await _page.$(selector);
801
+ if (!el) {
802
+ return [{ type: "text", text: JSON.stringify({ error: true, message: `Element not found: ${selector}` }) }];
803
+ }
804
+ screenshotBuf = await el.screenshot({ type: "png" });
805
+ }
806
+ else {
807
+ screenshotBuf = await _page.screenshot(screenshotOpts);
808
+ }
809
+ const base64 = screenshotBuf.toString("base64");
810
+ const title = await _page.title();
811
+ const url = _page.url();
812
+ return [
813
+ {
814
+ type: "text",
815
+ text: JSON.stringify({
816
+ label: label ?? `dive-snapshot-${Date.now()}`,
817
+ pageTitle: title,
818
+ pageUrl: url,
819
+ captureMode,
820
+ selector: selector ?? "full page",
821
+ sizeBytes: screenshotBuf.length,
822
+ }),
823
+ },
824
+ {
825
+ type: "image",
826
+ data: base64,
827
+ mimeType: "image/png",
828
+ },
829
+ ];
830
+ }
831
+ catch (e) {
832
+ return [{ type: "text", text: JSON.stringify({ error: true, message: `Screenshot failed: ${e.message}` }) }];
833
+ }
834
+ },
835
+ },
836
+ // 10. Auto-discover components from the current page DOM
837
+ {
838
+ name: "dive_auto_discover",
839
+ description: "Scan the current page DOM and auto-register components in the dive tree. Discovers semantic landmarks (nav, header, footer, forms, modals, tables, sidebars) and counts interactive elements within each. Useful after navigating to a new page during a dive session. Requires the built-in Playwright browser to be active.",
840
+ inputSchema: {
841
+ type: "object",
842
+ properties: {
843
+ sessionId: { type: "string", description: "Dive session ID" },
844
+ parentId: { type: "string", description: "Parent component ID to nest discovered components under (optional — null for top-level)" },
845
+ navigateUrl: { type: "string", description: "Navigate to this URL before discovering (optional — discovers current page if omitted)" },
846
+ },
847
+ required: ["sessionId"],
848
+ },
849
+ handler: async (args) => {
850
+ const { sessionId, parentId, navigateUrl } = args;
851
+ if (!_page || _activeSessionId !== sessionId) {
852
+ return {
853
+ error: true,
854
+ message: "No active browser for this session. Start a dive with start_ui_dive first.",
855
+ setupHint: "npm install playwright && npx playwright install chromium",
856
+ };
857
+ }
858
+ const db = getDb();
859
+ const session = db.prepare("SELECT id, status FROM ui_dive_sessions WHERE id = ?").get(sessionId);
860
+ if (!session)
861
+ return { error: true, message: `Session not found: ${sessionId}` };
862
+ if (session.status !== "active")
863
+ return { error: true, message: `Session is ${session.status}, not active` };
864
+ // Navigate if URL provided
865
+ if (navigateUrl) {
866
+ try {
867
+ await _page.goto(navigateUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
868
+ }
869
+ catch (e) {
870
+ return { error: true, message: `Navigation failed: ${e.message}` };
871
+ }
872
+ }
873
+ const pageTitle = await _page.title();
874
+ const pageUrl = _page.url();
875
+ try {
876
+ const discovered = await autoDiscoverComponents(_page);
877
+ const registeredIds = [];
878
+ for (const comp of discovered) {
879
+ const compId = genId("comp");
880
+ db.prepare("INSERT INTO ui_dive_components (id, session_id, parent_id, name, component_type, selector, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)").run(compId, sessionId, parentId ?? null, comp.name, comp.type, comp.selector, JSON.stringify({ autoDiscovered: true, interactiveChildren: comp.children, pageUrl, pageTitle }));
881
+ registeredIds.push(compId);
882
+ }
883
+ return {
884
+ pageTitle,
885
+ pageUrl,
886
+ componentsDiscovered: discovered.length,
887
+ components: discovered.map((c, i) => ({
888
+ componentId: registeredIds[i],
889
+ ...c,
890
+ })),
891
+ parentId: parentId ?? null,
892
+ _hint: discovered.length > 0
893
+ ? `${discovered.length} components auto-registered. Claim them for testing with start_component_flow.`
894
+ : "No semantic landmarks found. Register components manually with register_component.",
895
+ };
896
+ }
897
+ catch (e) {
898
+ return { error: true, message: `Auto-discover failed: ${e.message}` };
899
+ }
900
+ },
901
+ },
902
+ ];
903
+ //# sourceMappingURL=uiUxDiveTools.js.map