@petukhovart/agent-view 0.1.0 → 0.5.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.
Files changed (131) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/.claude-plugin/plugin.json +13 -2
  3. package/README.md +211 -61
  4. package/dist/adapters/browser.d.ts.map +1 -1
  5. package/dist/adapters/browser.js +11 -11
  6. package/dist/adapters/browser.js.map +1 -1
  7. package/dist/adapters/electron.d.ts.map +1 -1
  8. package/dist/adapters/electron.js +11 -11
  9. package/dist/adapters/electron.js.map +1 -1
  10. package/dist/adapters/tauri.d.ts.map +1 -1
  11. package/dist/adapters/tauri.js +11 -11
  12. package/dist/adapters/tauri.js.map +1 -1
  13. package/dist/adapters/types.d.ts +5 -2
  14. package/dist/adapters/types.d.ts.map +1 -1
  15. package/dist/cdp/_tests/console-stream.d.ts +35 -0
  16. package/dist/cdp/_tests/console-stream.d.ts.map +1 -0
  17. package/dist/cdp/_tests/console-stream.js +103 -0
  18. package/dist/cdp/_tests/console-stream.js.map +1 -0
  19. package/dist/cdp/ax-cache.d.ts +16 -0
  20. package/dist/cdp/ax-cache.d.ts.map +1 -0
  21. package/dist/cdp/ax-cache.js +31 -0
  22. package/dist/cdp/ax-cache.js.map +1 -0
  23. package/dist/cdp/console-stream.d.ts +35 -0
  24. package/dist/cdp/console-stream.d.ts.map +1 -0
  25. package/dist/cdp/console-stream.js +103 -0
  26. package/dist/cdp/console-stream.js.map +1 -0
  27. package/dist/cdp/transport.d.ts +5 -2
  28. package/dist/cdp/transport.d.ts.map +1 -1
  29. package/dist/cdp/transport.js +309 -26
  30. package/dist/cdp/transport.js.map +1 -1
  31. package/dist/cdp/types.d.ts +109 -8
  32. package/dist/cdp/types.d.ts.map +1 -1
  33. package/dist/cdp/types.js +35 -1
  34. package/dist/cdp/types.js.map +1 -1
  35. package/dist/cli/commands/console.d.ts +12 -0
  36. package/dist/cli/commands/console.d.ts.map +1 -0
  37. package/dist/cli/commands/console.js +32 -0
  38. package/dist/cli/commands/console.js.map +1 -0
  39. package/dist/cli/commands/dom.d.ts +5 -0
  40. package/dist/cli/commands/dom.d.ts.map +1 -1
  41. package/dist/cli/commands/dom.js +5 -0
  42. package/dist/cli/commands/dom.js.map +1 -1
  43. package/dist/cli/commands/drag.d.ts +13 -0
  44. package/dist/cli/commands/drag.d.ts.map +1 -0
  45. package/dist/cli/commands/drag.js +72 -0
  46. package/dist/cli/commands/drag.js.map +1 -0
  47. package/dist/cli/commands/eval.d.ts +9 -0
  48. package/dist/cli/commands/eval.d.ts.map +1 -0
  49. package/dist/cli/commands/eval.js +30 -0
  50. package/dist/cli/commands/eval.js.map +1 -0
  51. package/dist/cli/commands/scene.d.ts +1 -0
  52. package/dist/cli/commands/scene.d.ts.map +1 -1
  53. package/dist/cli/commands/scene.js +2 -0
  54. package/dist/cli/commands/scene.js.map +1 -1
  55. package/dist/cli/commands/screenshot.d.ts +2 -0
  56. package/dist/cli/commands/screenshot.d.ts.map +1 -1
  57. package/dist/cli/commands/screenshot.js +6 -0
  58. package/dist/cli/commands/screenshot.js.map +1 -1
  59. package/dist/cli/commands/snap.d.ts +1 -0
  60. package/dist/cli/commands/snap.d.ts.map +1 -1
  61. package/dist/cli/commands/snap.js +2 -0
  62. package/dist/cli/commands/snap.js.map +1 -1
  63. package/dist/cli/commands/targets.d.ts +7 -0
  64. package/dist/cli/commands/targets.d.ts.map +1 -0
  65. package/dist/cli/commands/targets.js +30 -0
  66. package/dist/cli/commands/targets.js.map +1 -0
  67. package/dist/cli/commands/watch.d.ts +12 -0
  68. package/dist/cli/commands/watch.d.ts.map +1 -0
  69. package/dist/cli/commands/watch.js +233 -0
  70. package/dist/cli/commands/watch.js.map +1 -0
  71. package/dist/cli/index.js +86 -1
  72. package/dist/cli/index.js.map +1 -1
  73. package/dist/config/manager.d.ts.map +1 -1
  74. package/dist/config/manager.js +11 -1
  75. package/dist/config/manager.js.map +1 -1
  76. package/dist/config/types.d.ts +11 -3
  77. package/dist/config/types.d.ts.map +1 -1
  78. package/dist/inspectors/dom/index.d.ts +30 -0
  79. package/dist/inspectors/dom/index.d.ts.map +1 -0
  80. package/dist/inspectors/dom/index.js +283 -0
  81. package/dist/inspectors/dom/index.js.map +1 -0
  82. package/dist/inspectors/scene/adapters/pixi/adapter.d.ts +3 -0
  83. package/dist/inspectors/scene/adapters/pixi/adapter.d.ts.map +1 -0
  84. package/dist/inspectors/scene/adapters/pixi/adapter.js +51 -0
  85. package/dist/inspectors/scene/adapters/pixi/adapter.js.map +1 -0
  86. package/dist/inspectors/scene/adapters/pixi/index.d.ts +2 -0
  87. package/dist/inspectors/scene/adapters/pixi/index.d.ts.map +1 -0
  88. package/dist/inspectors/scene/adapters/pixi/index.js +2 -0
  89. package/dist/inspectors/scene/adapters/pixi/index.js.map +1 -0
  90. package/dist/inspectors/scene/adapters/pixi/injection.d.ts +39 -0
  91. package/dist/inspectors/scene/adapters/pixi/injection.d.ts.map +1 -0
  92. package/dist/inspectors/scene/adapters/pixi/injection.js +41 -0
  93. package/dist/inspectors/scene/adapters/pixi/injection.js.map +1 -0
  94. package/dist/inspectors/scene/formatter.d.ts +0 -1
  95. package/dist/inspectors/scene/formatter.d.ts.map +1 -1
  96. package/dist/inspectors/scene/formatter.js +100 -43
  97. package/dist/inspectors/scene/formatter.js.map +1 -1
  98. package/dist/inspectors/scene/index.d.ts +4 -4
  99. package/dist/inspectors/scene/index.d.ts.map +1 -1
  100. package/dist/inspectors/scene/index.js +8 -5
  101. package/dist/inspectors/scene/index.js.map +1 -1
  102. package/dist/inspectors/scene/pixi.js.map +1 -1
  103. package/dist/inspectors/scene/registry.d.ts +2 -2
  104. package/dist/inspectors/scene/registry.d.ts.map +1 -1
  105. package/dist/inspectors/scene/registry.js +8 -8
  106. package/dist/inspectors/scene/registry.js.map +1 -1
  107. package/dist/inspectors/scene/types.d.ts +6 -10
  108. package/dist/inspectors/scene/types.d.ts.map +1 -1
  109. package/dist/inspectors/watch/index.d.ts +18 -0
  110. package/dist/inspectors/watch/index.d.ts.map +1 -0
  111. package/dist/inspectors/watch/index.js +39 -0
  112. package/dist/inspectors/watch/index.js.map +1 -0
  113. package/dist/inspectors/watch/types.d.ts +70 -0
  114. package/dist/inspectors/watch/types.d.ts.map +1 -0
  115. package/dist/inspectors/watch/types.js +21 -0
  116. package/dist/inspectors/watch/types.js.map +1 -0
  117. package/dist/server/server.d.ts +31 -1
  118. package/dist/server/server.d.ts.map +1 -1
  119. package/dist/server/server.js +620 -67
  120. package/dist/server/server.js.map +1 -1
  121. package/dist/server/watch-session.d.ts +32 -0
  122. package/dist/server/watch-session.d.ts.map +1 -0
  123. package/dist/server/watch-session.js +165 -0
  124. package/dist/server/watch-session.js.map +1 -0
  125. package/dist/types.d.ts +4 -1
  126. package/dist/types.d.ts.map +1 -1
  127. package/dist/types.js +1 -0
  128. package/dist/types.js.map +1 -1
  129. package/package.json +61 -59
  130. package/skills/verify/SKILL.md +146 -11
  131. package/skills/verify-recipe/SKILL.md +185 -0
@@ -5,14 +5,19 @@ import { homedir, tmpdir } from 'node:os';
5
5
  import { join, resolve } from 'node:path';
6
6
  import { mkdirSync } from 'node:fs';
7
7
  import { getAdapter } from '../adapters/registry.js';
8
- import { formatAccessibilityTree } from '../inspectors/dom.js';
8
+ import { formatAccessibilityTree, countAccessibilityNodes, diffDomText } from '../inspectors/dom/index.js';
9
9
  import { getSceneGraph, getRawScene, diffScenes } from '../inspectors/scene/index.js';
10
10
  import { RefStore } from './ref-store.js';
11
11
  import { launch, isRunning } from './launcher.js';
12
12
  import { readConfig } from '../config/manager.js';
13
13
  import { RuntimeType, WebGLEngine } from '../types.js';
14
+ import { TargetType, ConsoleLevel, EvaluationError, MouseButton, } from '../cdp/types.js';
15
+ import { listSupportedTargets, connectToRuntime } from '../cdp/transport.js';
16
+ import { ConsoleStream } from '../cdp/_tests/console-stream.js';
17
+ import { AxTreeCache } from '../cdp/ax-cache.js';
18
+ import { WatchSession } from './watch-session.js';
19
+ import { StopReason, WATCH_MIN_INTERVAL_MS } from '../inspectors/watch/index.js';
14
20
  const SERVER_PORT = 47922;
15
- const VALID_COMMANDS = new Set(['discover', 'launch', 'dom', 'click', 'fill', 'screenshot', 'scene', 'snap', 'wait', 'stop']);
16
21
  const VALID_RUNTIMES = new Set(Object.values(RuntimeType));
17
22
  const VALID_ENGINES = new Set(Object.values(WebGLEngine));
18
23
  const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
@@ -20,6 +25,13 @@ const DELIMITER = '\n';
20
25
  const MAX_BUFFER_SIZE = 1_048_576; // 1 MB
21
26
  const TOKEN_DIR = join(homedir(), '.agent-view');
22
27
  const TOKEN_PATH = join(TOKEN_DIR, 'token');
28
+ const EVAL_OUTPUT_CAP = 64 * 1024;
29
+ const DEFAULT_CONSOLE_TARGETS = [TargetType.Page, TargetType.SharedWorker, TargetType.ServiceWorker];
30
+ const RUNTIME_ONLY_TARGETS = new Set([
31
+ TargetType.SharedWorker,
32
+ TargetType.ServiceWorker,
33
+ TargetType.Worker,
34
+ ]);
23
35
  function argStr(args, key) {
24
36
  const v = args[key];
25
37
  return typeof v === 'string' ? v : undefined;
@@ -32,13 +44,95 @@ function argBool(args, key) {
32
44
  const v = args[key];
33
45
  return typeof v === 'boolean' ? v : undefined;
34
46
  }
47
+ function argStrArray(args, key) {
48
+ const v = args[key];
49
+ if (!Array.isArray(v))
50
+ return undefined;
51
+ return v.filter((x) => typeof x === 'string');
52
+ }
53
+ const ARIA_ROLES = new Set([
54
+ 'button', 'link', 'menuitem', 'tab', 'checkbox', 'radio',
55
+ 'textbox', 'searchbox', 'combobox', 'spinbutton', 'textarea',
56
+ 'listitem', 'option', 'treeitem', 'cell', 'row', 'heading',
57
+ ]);
58
+ export function resolveDepth(filter, explicit) {
59
+ if (explicit !== undefined)
60
+ return explicit;
61
+ if (filter !== undefined)
62
+ return undefined; // unlimited depth when filtering
63
+ return 4; // default snapshot depth
64
+ }
65
+ export async function textContentFallback(conn, filter) {
66
+ const safeFilter = JSON.stringify(filter);
67
+ const js = `(() => {
68
+ const q = ${safeFilter};
69
+ const results = [];
70
+ for (const el of document.querySelectorAll('body *')) {
71
+ if (results.length >= 5) break;
72
+ const directText = Array.from(el.childNodes)
73
+ .filter(n => n.nodeType === Node.TEXT_NODE)
74
+ .map(n => n.textContent.trim())
75
+ .join(' ');
76
+ if (directText.toLowerCase().includes(q.toLowerCase()) && el.offsetParent !== null) {
77
+ const id = el.id ? '#' + el.id : '';
78
+ const cls = el.className && typeof el.className === 'string' && el.className.trim()
79
+ ? '.' + el.className.trim().split(/\\s+/)[0] : '';
80
+ results.push(el.tagName.toLowerCase() + id + cls);
81
+ }
82
+ }
83
+ return results.length ? results.join(', ') : null;
84
+ })()`;
85
+ const result = await conn.evaluate(js);
86
+ if (!result || typeof result !== 'string') {
87
+ return `(no text-match for "${filter}")`;
88
+ }
89
+ return result.split(', ')
90
+ .map(loc => `[text-match] "${filter}" found in ${loc} (no accessible role in AX tree)`)
91
+ .join('\n');
92
+ }
93
+ export function parseFilter(filter) {
94
+ const colonIdx = filter.indexOf(':');
95
+ if (colonIdx > 0) {
96
+ const role = filter.slice(0, colonIdx).trim().toLowerCase();
97
+ const name = filter.slice(colonIdx + 1).trim();
98
+ if (name && ARIA_ROLES.has(role)) {
99
+ return { kind: 'simple', name, role };
100
+ }
101
+ }
102
+ if (filter.startsWith('~') || /[.*+?^${}()|[\]\\]/.test(filter)) {
103
+ return { kind: 'heuristic', raw: filter };
104
+ }
105
+ return { kind: 'simple', name: filter };
106
+ }
35
107
  export class AgentViewServer {
36
108
  server = null;
37
109
  connections = new Map();
38
110
  refStore = new RefStore();
39
111
  idleTimer = null;
40
112
  sceneCache = new Map();
113
+ domTextCache = new Map();
114
+ axTreeCache = new AxTreeCache();
115
+ consoleStream = new ConsoleStream();
41
116
  token = '';
117
+ activeWatches = new Set();
118
+ handlers = {
119
+ discover: (req) => this.handleDiscover(req),
120
+ launch: (req) => this.handleLaunch(req),
121
+ dom: (req) => this.handleDom(req),
122
+ click: (req) => this.handleClick(req),
123
+ drag: (req) => this.handleDrag(req),
124
+ fill: (req) => this.handleFill(req),
125
+ wait: (req) => this.handleWait(req),
126
+ screenshot: (req) => this.handleScreenshot(req),
127
+ scene: (req) => this.handleScene(req),
128
+ snap: (req) => this.handleSnap(req),
129
+ targets: (req) => this.handleTargets(req),
130
+ eval: (req) => this.handleEval(req),
131
+ console: (req) => this.handleConsole(req),
132
+ stop: () => this.handleStop(),
133
+ };
134
+ streamingCommands = new Set(['watch']);
135
+ validCommands = new Set([...Object.keys(this.handlers), ...this.streamingCommands]);
42
136
  async start() {
43
137
  mkdirSync(TOKEN_DIR, { recursive: true });
44
138
  this.token = randomBytes(32).toString('hex');
@@ -55,6 +149,11 @@ export class AgentViewServer {
55
149
  resetIdleTimer() {
56
150
  if (this.idleTimer)
57
151
  clearTimeout(this.idleTimer);
152
+ if (this.activeWatches.size > 0) {
153
+ // Pause idle shutdown while streaming handlers are alive.
154
+ this.idleTimer = null;
155
+ return;
156
+ }
58
157
  this.idleTimer = setTimeout(() => this.shutdown(), IDLE_TIMEOUT_MS);
59
158
  }
60
159
  handleSocket(socket) {
@@ -81,7 +180,7 @@ export class AgentViewServer {
81
180
  socket.end(JSON.stringify({ ok: false, error: 'Unauthorized' }) + DELIMITER);
82
181
  return;
83
182
  }
84
- if (typeof request.command !== 'string' || !VALID_COMMANDS.has(request.command) ||
183
+ if (typeof request.command !== 'string' || !this.validCommands.has(request.command) ||
85
184
  (request.command !== 'stop' && (!VALID_RUNTIMES.has(request.runtime) ||
86
185
  typeof request.port !== 'number' || request.port < 1 || request.port > 65535))) {
87
186
  socket.end(JSON.stringify({ ok: false, error: 'Invalid request' }) + DELIMITER);
@@ -95,6 +194,10 @@ export class AgentViewServer {
95
194
  socket.end(JSON.stringify({ ok: false, error: 'Invalid args' }) + DELIMITER);
96
195
  return;
97
196
  }
197
+ if (this.streamingCommands.has(request.command)) {
198
+ await this.handleWatchStreaming(request, socket);
199
+ return;
200
+ }
98
201
  const response = await this.handleCommand(request);
99
202
  socket.end(JSON.stringify(response) + DELIMITER);
100
203
  }
@@ -107,30 +210,10 @@ export class AgentViewServer {
107
210
  }
108
211
  }
109
212
  async handleCommand(req) {
110
- switch (req.command) {
111
- case 'discover':
112
- return this.handleDiscover(req);
113
- case 'launch':
114
- return this.handleLaunch(req);
115
- case 'dom':
116
- return this.handleDom(req);
117
- case 'click':
118
- return this.handleClick(req);
119
- case 'fill':
120
- return this.handleFill(req);
121
- case 'screenshot':
122
- return this.handleScreenshot(req);
123
- case 'scene':
124
- return this.handleScene(req);
125
- case 'snap':
126
- return this.handleSnap(req);
127
- case 'wait':
128
- return this.handleWait(req);
129
- case 'stop':
130
- return this.handleStop();
131
- default:
132
- return { ok: false, error: `Unknown command: ${req.command}` };
133
- }
213
+ const handler = this.handlers[req.command];
214
+ if (!handler)
215
+ return { ok: false, error: `Unknown command: ${req.command}` };
216
+ return handler(req);
134
217
  }
135
218
  async resolveWindow(req) {
136
219
  const adapter = getAdapter(req.runtime);
@@ -154,38 +237,50 @@ export class AgentViewServer {
154
237
  }
155
238
  return { targetId, windows };
156
239
  }
157
- async getConnection(req, targetId) {
240
+ async getPageSession(req, targetId) {
158
241
  const connKey = `${req.port}:${targetId}`;
159
- let conn = this.connections.get(connKey);
160
- if (!conn) {
161
- const adapter = getAdapter(req.runtime);
162
- conn = await adapter.connect(req.port, targetId);
163
- this.connections.set(connKey, conn);
242
+ const cached = this.connections.get(connKey);
243
+ if (cached) {
244
+ if (cached.kind === 'page')
245
+ return cached.session;
246
+ throw new Error(`Cached session for ${targetId} is runtime-only — cannot use page operations.`);
164
247
  }
165
- return conn;
248
+ const adapter = getAdapter(req.runtime);
249
+ const session = await adapter.connect(req.port, targetId, this.axTreeCache);
250
+ this.connections.set(connKey, { kind: 'page', session });
251
+ return session;
252
+ }
253
+ async getRuntimeSession(req, target) {
254
+ const connKey = `${req.port}:${target.id}`;
255
+ const cached = this.connections.get(connKey);
256
+ if (cached)
257
+ return cached.session;
258
+ if (target.type === TargetType.Page || target.type === TargetType.Iframe) {
259
+ // Page targets can serve runtime requests via their PageSession (it extends RuntimeSession).
260
+ return this.getPageSession(req, target.id);
261
+ }
262
+ if (!RUNTIME_ONLY_TARGETS.has(target.type)) {
263
+ throw new Error(`Target type "${target.type}" does not support eval/console.`);
264
+ }
265
+ const session = await connectToRuntime(req.port, target);
266
+ this.connections.set(connKey, { kind: 'runtime', session });
267
+ return session;
166
268
  }
167
269
  async handleDiscover(req) {
168
270
  const adapter = getAdapter(req.runtime);
169
271
  const windows = await adapter.discover(req.port);
170
272
  return {
171
273
  ok: true,
172
- data: {
173
- runtime: req.runtime,
174
- port: req.port,
175
- windows,
176
- },
274
+ data: { runtime: req.runtime, port: req.port, windows },
177
275
  };
178
276
  }
179
277
  async handleLaunch(req) {
180
278
  const launchCmd = argStr(req.args, 'launch');
181
279
  const cwd = argStr(req.args, 'cwd');
182
- if (!launchCmd) {
280
+ if (!launchCmd)
183
281
  return { ok: false, error: 'No launch command provided' };
184
- }
185
- if (!cwd) {
282
+ if (!cwd)
186
283
  return { ok: false, error: 'launch requires cwd to validate config' };
187
- }
188
- // Validate launch command against on-disk config to prevent injection
189
284
  const config = readConfig(resolve(cwd));
190
285
  if (!config || config.launch !== launchCmd) {
191
286
  return { ok: false, error: 'Launch command does not match project config' };
@@ -198,17 +293,76 @@ export class AgentViewServer {
198
293
  }
199
294
  async handleDom(req) {
200
295
  const { targetId } = await this.resolveWindow(req);
201
- const conn = await this.getConnection(req, targetId);
202
- const nodes = await conn.getAccessibilityTree();
296
+ const conn = await this.getPageSession(req, targetId);
297
+ const filter = argStr(req.args, 'filter');
298
+ const useText = argBool(req.args, 'text') ?? false;
299
+ const compact = argBool(req.args, 'compact') ?? false;
300
+ const useCount = argBool(req.args, 'count') ?? false;
301
+ const isDiff = argBool(req.args, 'diff') ?? false;
302
+ const cacheKey = `${req.port}:${targetId}`;
303
+ const { nodes, fromCache } = await conn.getAccessibilityTreeMeta();
304
+ if (useCount) {
305
+ const { count } = countAccessibilityNodes(nodes, {
306
+ filter,
307
+ depth: resolveDepth(filter, argNum(req.args, 'depth')),
308
+ });
309
+ return { ok: true, data: String(count) };
310
+ }
203
311
  const { text, refs, nextRef } = formatAccessibilityTree(nodes, {
204
- filter: argStr(req.args, 'filter'),
205
- depth: argNum(req.args, 'depth'),
312
+ filter,
313
+ depth: resolveDepth(filter, argNum(req.args, 'depth')),
206
314
  startRef: this.refStore.getNextRef(),
315
+ compact,
316
+ maxLines: argNum(req.args, 'maxLines'),
207
317
  });
208
318
  this.refStore.store(refs, req.port, targetId, nextRef);
209
- return { ok: true, data: text };
319
+ if (isDiff) {
320
+ const prev = this.domTextCache.get(cacheKey);
321
+ this.domTextCache.set(cacheKey, text);
322
+ if (prev === undefined) {
323
+ // First call — no snapshot yet, return full tree
324
+ return { ok: true, data: text };
325
+ }
326
+ return { ok: true, data: diffDomText(prev, text) };
327
+ }
328
+ if (useText && filter && text.startsWith('(no matching')) {
329
+ return { ok: true, data: await textContentFallback(conn, filter) };
330
+ }
331
+ const data = fromCache ? `[cache]\n${text}` : text;
332
+ return { ok: true, data };
210
333
  }
211
334
  async findByFilter(conn, filter, req, targetId, preferRoles) {
335
+ const parsed = parseFilter(filter);
336
+ if (parsed.kind === 'simple') {
337
+ const queryNodes = await conn.queryAXTree({ accessibleName: parsed.name, role: parsed.role });
338
+ if (queryNodes !== null) {
339
+ if (queryNodes.length === 0)
340
+ return null;
341
+ const startRef = this.refStore.getNextRef();
342
+ let refNum = startRef;
343
+ const refs = [];
344
+ for (const node of queryNodes) {
345
+ if (node.backendDOMNodeId) {
346
+ refs.push({ ref: refNum++, backendDOMNodeId: node.backendDOMNodeId });
347
+ }
348
+ }
349
+ this.refStore.store(refs, req.port, targetId, refNum);
350
+ if (refs.length === 0)
351
+ return null;
352
+ if (preferRoles) {
353
+ for (let i = 0; i < queryNodes.length; i++) {
354
+ const node = queryNodes[i];
355
+ if (node.backendDOMNodeId && preferRoles.has(node.role?.value ?? '')) {
356
+ return { backendDOMNodeId: node.backendDOMNodeId, name: node.name?.value ?? parsed.name };
357
+ }
358
+ }
359
+ }
360
+ const first = queryNodes.find(n => n.backendDOMNodeId);
361
+ if (!first?.backendDOMNodeId)
362
+ return null;
363
+ return { backendDOMNodeId: first.backendDOMNodeId, name: first.name?.value ?? parsed.name };
364
+ }
365
+ }
212
366
  const nodes = await conn.getAccessibilityTree();
213
367
  const { refs, nextRef } = formatAccessibilityTree(nodes, {
214
368
  filter,
@@ -217,7 +371,6 @@ export class AgentViewServer {
217
371
  this.refStore.store(refs, req.port, targetId, nextRef);
218
372
  if (refs.length === 0)
219
373
  return null;
220
- // Build map with fallback name resolution (same logic as dom.ts)
221
374
  const nodeById = new Map();
222
375
  for (const node of nodes)
223
376
  nodeById.set(node.nodeId, node);
@@ -250,7 +403,6 @@ export class AgentViewServer {
250
403
  }
251
404
  }
252
405
  const lowerFilter = filter.toLowerCase();
253
- // If preferred roles specified, try those first
254
406
  if (preferRoles) {
255
407
  for (const entry of refs) {
256
408
  const info = nodeByDOMId.get(entry.backendDOMNodeId);
@@ -259,26 +411,26 @@ export class AgentViewServer {
259
411
  }
260
412
  }
261
413
  }
262
- // Pick the deepest match whose name contains the filter text (leaf-first)
263
414
  for (let i = refs.length - 1; i >= 0; i--) {
264
415
  const info = nodeByDOMId.get(refs[i].backendDOMNodeId);
265
416
  if (info && info.name.toLowerCase().includes(lowerFilter)) {
266
417
  return { backendDOMNodeId: refs[i].backendDOMNodeId, name: info.name };
267
418
  }
268
419
  }
269
- // Fallback: last ref (deepest element in filtered tree)
270
420
  const last = refs[refs.length - 1];
271
421
  const lastInfo = nodeByDOMId.get(last.backendDOMNodeId);
272
422
  return { backendDOMNodeId: last.backendDOMNodeId, name: lastInfo?.name ?? filter };
273
423
  }
274
424
  async handleClick(req) {
275
425
  const { targetId } = await this.resolveWindow(req);
276
- const conn = await this.getConnection(req, targetId);
426
+ const conn = await this.getPageSession(req, targetId);
427
+ const cacheKey = `${req.port}:${targetId}`;
277
428
  if (req.args.pos && typeof req.args.pos === 'object') {
278
429
  const pos = req.args.pos;
279
430
  const x = typeof pos.x === 'number' ? pos.x : 0;
280
431
  const y = typeof pos.y === 'number' ? pos.y : 0;
281
432
  await conn.clickAtPosition(x, y);
433
+ this.axTreeCache.invalidate(cacheKey);
282
434
  return { ok: true, data: `Clicked at (${x}, ${y})` };
283
435
  }
284
436
  const clickFilter = argStr(req.args, 'filter');
@@ -290,6 +442,7 @@ export class AgentViewServer {
290
442
  return { ok: false, error: `No element found matching "${filter}"` };
291
443
  }
292
444
  await conn.clickByNodeId(found.backendDOMNodeId);
445
+ this.axTreeCache.invalidate(cacheKey);
293
446
  return { ok: true, data: `Clicked "${found.name}"` };
294
447
  }
295
448
  const ref = argNum(req.args, 'ref');
@@ -301,11 +454,52 @@ export class AgentViewServer {
301
454
  return { ok: false, error: `Invalid ref: ${ref}. Run \`agent-view dom\` to get fresh refs.` };
302
455
  }
303
456
  await conn.clickByNodeId(entry.backendDOMNodeId);
457
+ this.axTreeCache.invalidate(cacheKey);
304
458
  return { ok: true, data: `Clicked ref ${ref}` };
305
459
  }
460
+ async handleDrag(req) {
461
+ const { targetId } = await this.resolveWindow(req);
462
+ const conn = await this.getPageSession(req, targetId);
463
+ const cacheKey = `${req.port}:${targetId}`;
464
+ const from = await this.resolveDragPoint(req, conn, 'from', { scrollIntoView: true });
465
+ if ('error' in from)
466
+ return { ok: false, error: from.error };
467
+ const to = await this.resolveDragPoint(req, conn, 'to', { scrollIntoView: false });
468
+ if ('error' in to)
469
+ return { ok: false, error: to.error };
470
+ const opts = {
471
+ steps: argNum(req.args, 'steps'),
472
+ button: parseMouseButton(argStr(req.args, 'button')),
473
+ holdMs: argNum(req.args, 'holdMs'),
474
+ };
475
+ await conn.dragBetweenPositions(from.point, to.point, opts);
476
+ this.axTreeCache.invalidate(cacheKey);
477
+ return {
478
+ ok: true,
479
+ data: `Dragged (${from.point.x.toFixed(0)}, ${from.point.y.toFixed(0)}) → (${to.point.x.toFixed(0)}, ${to.point.y.toFixed(0)})`,
480
+ };
481
+ }
482
+ async resolveDragPoint(req, conn, side, opts) {
483
+ const ref = argNum(req.args, `${side}Ref`);
484
+ const x = argNum(req.args, `${side}X`);
485
+ const y = argNum(req.args, `${side}Y`);
486
+ if (ref !== undefined) {
487
+ const entry = this.refStore.get(ref);
488
+ if (!entry) {
489
+ return { error: `Invalid --${side} ref: ${ref}. Run \`agent-view dom\` to get fresh refs.` };
490
+ }
491
+ const point = await conn.getBoxCenter(entry.backendDOMNodeId, opts);
492
+ return { point };
493
+ }
494
+ if (x !== undefined && y !== undefined) {
495
+ return { point: { x, y } };
496
+ }
497
+ return { error: `drag requires --${side} <ref> or --${side}-pos <x,y>` };
498
+ }
306
499
  async handleFill(req) {
307
500
  const { targetId } = await this.resolveWindow(req);
308
- const conn = await this.getConnection(req, targetId);
501
+ const conn = await this.getPageSession(req, targetId);
502
+ const cacheKey = `${req.port}:${targetId}`;
309
503
  const value = argStr(req.args, 'value');
310
504
  if (value === undefined) {
311
505
  return { ok: false, error: 'fill requires --value' };
@@ -319,6 +513,7 @@ export class AgentViewServer {
319
513
  return { ok: false, error: `No element found matching "${filter}"` };
320
514
  }
321
515
  await conn.fillByNodeId(found.backendDOMNodeId, value);
516
+ this.axTreeCache.invalidate(cacheKey);
322
517
  return { ok: true, data: `Filled "${found.name}" with "${value}"` };
323
518
  }
324
519
  const fillRef = argNum(req.args, 'ref');
@@ -330,6 +525,7 @@ export class AgentViewServer {
330
525
  return { ok: false, error: `Invalid ref: ${fillRef}. Run \`agent-view dom\` to get fresh refs.` };
331
526
  }
332
527
  await conn.fillByNodeId(entry.backendDOMNodeId, value);
528
+ this.axTreeCache.invalidate(cacheKey);
333
529
  return { ok: true, data: `Filled ref ${fillRef} with "${value}"` };
334
530
  }
335
531
  async handleWait(req) {
@@ -341,7 +537,7 @@ export class AgentViewServer {
341
537
  const interval = 500;
342
538
  const maxAttempts = Math.ceil((timeout * 1000) / interval);
343
539
  const { targetId } = await this.resolveWindow(req);
344
- const conn = await this.getConnection(req, targetId);
540
+ const conn = await this.getPageSession(req, targetId);
345
541
  for (let i = 0; i < maxAttempts; i++) {
346
542
  const nodes = await conn.getAccessibilityTree();
347
543
  const { refs, text, nextRef } = formatAccessibilityTree(nodes, {
@@ -356,23 +552,42 @@ export class AgentViewServer {
356
552
  }
357
553
  return { ok: false, error: `Timeout: "${filter}" not found after ${timeout}s` };
358
554
  }
359
- async handleScreenshot(req) {
360
- const { targetId } = await this.resolveWindow(req);
361
- const conn = await this.getConnection(req, targetId);
362
- const buffer = await conn.captureScreenshot();
363
- const filename = `agent-view-screenshot-${Date.now()}.png`;
555
+ async captureScreenshotToFile(conn, opts = {}) {
556
+ const { buffer, format } = await conn.captureScreenshot(opts);
557
+ const ext = format === 'jpeg' ? 'jpg' : format;
558
+ const filename = `agent-view-screenshot-${Date.now()}.${ext}`;
364
559
  const filepath = join(tmpdir(), filename);
365
560
  await writeFile(filepath, buffer);
366
- return { ok: true, data: filepath };
561
+ return filepath;
562
+ }
563
+ async handleScreenshot(req) {
564
+ const { targetId } = await this.resolveWindow(req);
565
+ const conn = await this.getPageSession(req, targetId);
566
+ const scale = argNum(req.args, 'scale');
567
+ const cropFilter = argStr(req.args, 'crop');
568
+ let warning;
569
+ let clip;
570
+ if (cropFilter !== undefined) {
571
+ const found = await this.findByFilter(conn, cropFilter, req, targetId);
572
+ if (!found) {
573
+ warning = `crop filter '${cropFilter}' matched nothing — capturing full window`;
574
+ }
575
+ else {
576
+ clip = await conn.getBoxRect(found.backendDOMNodeId, { scrollIntoView: true });
577
+ }
578
+ }
579
+ const filepath = await this.captureScreenshotToFile(conn, { scale, clip });
580
+ return { ok: true, data: filepath, warning };
367
581
  }
368
582
  async handleScene(req) {
369
583
  const { targetId } = await this.resolveWindow(req);
370
- const conn = await this.getConnection(req, targetId);
584
+ const conn = await this.getPageSession(req, targetId);
371
585
  const isDiff = argBool(req.args, 'diff') ?? false;
372
586
  const cacheKey = `${req.port}:${targetId}`;
373
587
  const sceneFilter = argStr(req.args, 'filter');
374
588
  const sceneDepth = argNum(req.args, 'depth');
375
589
  const sceneVerbose = argBool(req.args, 'verbose');
590
+ const sceneCompact = argBool(req.args, 'compact');
376
591
  if (isDiff) {
377
592
  const curr = await getRawScene(conn, req.engine);
378
593
  if (!curr) {
@@ -385,6 +600,7 @@ export class AgentViewServer {
385
600
  filter: sceneFilter,
386
601
  depth: sceneDepth,
387
602
  verbose: sceneVerbose,
603
+ compact: sceneCompact,
388
604
  });
389
605
  return { ok: true, data: text };
390
606
  }
@@ -394,15 +610,16 @@ export class AgentViewServer {
394
610
  filter: sceneFilter,
395
611
  depth: sceneDepth,
396
612
  verbose: sceneVerbose,
613
+ compact: sceneCompact,
397
614
  });
398
615
  return { ok: true, data: text };
399
616
  }
400
617
  async handleSnap(req) {
401
618
  const { targetId } = await this.resolveWindow(req);
402
- const conn = await this.getConnection(req, targetId);
619
+ const conn = await this.getPageSession(req, targetId);
403
620
  const snapFilter = argStr(req.args, 'filter');
404
621
  const snapDepth = argNum(req.args, 'depth');
405
- // DOM section
622
+ const snapScale = argNum(req.args, 'scale');
406
623
  const nodes = await conn.getAccessibilityTree();
407
624
  const { text: domText, refs, nextRef } = formatAccessibilityTree(nodes, {
408
625
  filter: snapFilter,
@@ -420,8 +637,278 @@ export class AgentViewServer {
420
637
  sections.push(`=== Scene ===\n${sceneText}`);
421
638
  }
422
639
  }
640
+ if (snapScale !== undefined) {
641
+ const filepath = await this.captureScreenshotToFile(conn, { scale: snapScale });
642
+ sections.push(`=== Screenshot ===\n${filepath}`);
643
+ }
423
644
  return { ok: true, data: sections.join('\n\n') };
424
645
  }
646
+ // ── New v0.3.0 commands ────────────────────────────────────────────────────
647
+ async handleTargets(req) {
648
+ const targets = await listSupportedTargets(req.port);
649
+ const typeFilter = argStrArray(req.args, 'types');
650
+ const filtered = typeFilter && typeFilter.length > 0
651
+ ? targets.filter(t => typeFilter.includes(t.type))
652
+ : targets;
653
+ return { ok: true, data: { runtime: req.runtime, port: req.port, targets: filtered } };
654
+ }
655
+ async resolveTarget(req) {
656
+ const explicitId = argStr(req.args, 'target');
657
+ const windowArg = argStr(req.args, 'window');
658
+ const allTargets = await listSupportedTargets(req.port);
659
+ if (explicitId) {
660
+ const found = findTargetByIdOrSubstring(allTargets, explicitId);
661
+ if (found)
662
+ return found;
663
+ throw new Error(`Target not found: "${explicitId}". Run \`agent-view targets\` for the full list.`);
664
+ }
665
+ if (windowArg) {
666
+ const pages = allTargets.filter(t => t.type === TargetType.Page);
667
+ const byId = pages.find(t => t.id === windowArg);
668
+ const byTitle = pages.find(t => t.title.toLowerCase().includes(windowArg.toLowerCase()));
669
+ const found = byId ?? byTitle;
670
+ if (!found) {
671
+ throw new Error(`Window not found: "${windowArg}".`);
672
+ }
673
+ return found;
674
+ }
675
+ const firstPage = allTargets.find(t => t.type === TargetType.Page);
676
+ if (!firstPage)
677
+ throw new Error('No page targets found.');
678
+ return firstPage;
679
+ }
680
+ async handleEval(req) {
681
+ const cwd = argStr(req.args, 'cwd');
682
+ if (!cwd) {
683
+ return { ok: false, error: 'eval requires cwd to validate allowEval policy' };
684
+ }
685
+ const config = readConfig(resolve(cwd));
686
+ if (!config?.allowEval) {
687
+ return {
688
+ ok: false,
689
+ error: 'eval is disabled. Set "allowEval": true in agent-view.config.json to enable.',
690
+ };
691
+ }
692
+ const expression = argStr(req.args, 'expression');
693
+ if (!expression)
694
+ return { ok: false, error: 'eval requires --expression' };
695
+ const target = await this.resolveTarget(req);
696
+ if (!RUNTIME_ONLY_TARGETS.has(target.type) && target.type !== TargetType.Page && target.type !== TargetType.Iframe) {
697
+ return { ok: false, error: `Target type "${target.type}" does not support eval.` };
698
+ }
699
+ const session = await this.getRuntimeSession(req, target);
700
+ const awaitPromise = argBool(req.args, 'await') ?? false;
701
+ const asJson = argBool(req.args, 'json') ?? false;
702
+ try {
703
+ const value = await session.evaluate(expression, { awaitPromise });
704
+ const formatted = asJson ? safeJSONStringify(value) : formatEvalValue(value);
705
+ const capped = formatted.length > EVAL_OUTPUT_CAP
706
+ ? `${formatted.slice(0, EVAL_OUTPUT_CAP)}\n... <${formatted.length - EVAL_OUTPUT_CAP} more bytes truncated>`
707
+ : formatted;
708
+ return { ok: true, data: { target: { id: target.id, type: target.type }, result: capped } };
709
+ }
710
+ catch (err) {
711
+ if (err instanceof EvaluationError) {
712
+ return { ok: false, error: err.message + (err.stack ? `\n${err.stack}` : '') };
713
+ }
714
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
715
+ }
716
+ }
717
+ async handleWatchStreaming(req, socket) {
718
+ const writeFrame = (frame) => {
719
+ if (socket.writableEnded || socket.destroyed)
720
+ return false;
721
+ return socket.write(JSON.stringify(frame) + DELIMITER);
722
+ };
723
+ const writeError = (msg) => {
724
+ if (!socket.writableEnded && !socket.destroyed) {
725
+ socket.end(JSON.stringify({ ok: false, error: msg }) + DELIMITER);
726
+ }
727
+ };
728
+ const cwd = argStr(req.args, 'cwd');
729
+ if (!cwd)
730
+ return writeError('watch requires cwd to validate allowEval policy');
731
+ const config = readConfig(resolve(cwd));
732
+ if (!config?.allowEval) {
733
+ return writeError('watch is disabled. Set "allowEval": true in agent-view.config.json to enable.');
734
+ }
735
+ const expression = argStr(req.args, 'expression');
736
+ if (!expression)
737
+ return writeError('watch requires --expression');
738
+ const intervalRaw = argNum(req.args, 'intervalMs') ?? 250;
739
+ const intervalMs = Math.max(WATCH_MIN_INTERVAL_MS, intervalRaw);
740
+ const durationS = argNum(req.args, 'durationS') ?? 30;
741
+ const maxChanges = argNum(req.args, 'maxChanges') ?? 10;
742
+ const until = argStr(req.args, 'until');
743
+ if (maxChanges <= 0)
744
+ return writeError('--max-changes must be > 0');
745
+ if (durationS <= 0)
746
+ return writeError('--duration must be > 0');
747
+ let target;
748
+ try {
749
+ target = await this.resolveTarget(req);
750
+ }
751
+ catch (err) {
752
+ return writeError(err instanceof Error ? err.message : String(err));
753
+ }
754
+ if (!RUNTIME_ONLY_TARGETS.has(target.type) && target.type !== TargetType.Page && target.type !== TargetType.Iframe) {
755
+ return writeError(`Target type "${target.type}" does not support watch.`);
756
+ }
757
+ let session;
758
+ try {
759
+ session = await this.getRuntimeSession(req, target);
760
+ }
761
+ catch (err) {
762
+ return writeError(err instanceof Error ? err.message : String(err));
763
+ }
764
+ if (this.idleTimer) {
765
+ clearTimeout(this.idleTimer);
766
+ this.idleTimer = null;
767
+ }
768
+ const watch = new WatchSession(session, {
769
+ expression,
770
+ intervalMs,
771
+ durationS,
772
+ maxChanges,
773
+ until,
774
+ emit: writeFrame,
775
+ });
776
+ this.activeWatches.add(watch);
777
+ const cleanup = () => {
778
+ if (!this.activeWatches.has(watch))
779
+ return;
780
+ this.activeWatches.delete(watch);
781
+ if (!socket.writableEnded && !socket.destroyed)
782
+ socket.end();
783
+ if (this.activeWatches.size === 0)
784
+ this.resetIdleTimer();
785
+ };
786
+ watch.onStop(cleanup);
787
+ socket.on('close', () => {
788
+ // Client closed (e.g. SIGINT). Stop watch with sigint reason if not already stopped.
789
+ watch.stop(StopReason.Sigint, true);
790
+ });
791
+ socket.on('error', () => {
792
+ watch.stop(StopReason.Sigint, true);
793
+ });
794
+ try {
795
+ await watch.start();
796
+ }
797
+ catch (err) {
798
+ writeFrame({ type: 'error', ts: new Date().toISOString(), message: err instanceof Error ? err.message : String(err) });
799
+ watch.stop(StopReason.EvalFailed, false);
800
+ }
801
+ }
802
+ async handleConsole(req) {
803
+ const cwd = argStr(req.args, 'cwd');
804
+ const config = cwd ? readConfig(resolve(cwd)) : null;
805
+ const bufferSize = config?.consoleBufferSize ?? 500;
806
+ if (this.consoleStream.attachedCount === 0) {
807
+ // Recreate with config-tuned capacity on first attach
808
+ this.consoleStream = new ConsoleStream({ capacity: bufferSize });
809
+ }
810
+ const targetQuery = argStr(req.args, 'target');
811
+ // Fuzzy-resolve the target query once (id exact → title substring → url substring)
812
+ const all = await listSupportedTargets(req.port);
813
+ let resolvedTargetId;
814
+ if (targetQuery) {
815
+ const resolved = findTargetByIdOrSubstring(all, targetQuery);
816
+ if (!resolved) {
817
+ return { ok: false, error: `Target not found: "${targetQuery}". Run \`agent-view targets\` for the full list.` };
818
+ }
819
+ resolvedTargetId = resolved.id;
820
+ }
821
+ if (argBool(req.args, 'clear')) {
822
+ this.consoleStream.clear(resolvedTargetId);
823
+ return { ok: true, data: 'Console buffer cleared' };
824
+ }
825
+ const requestedTypes = argStrArray(req.args, 'consoleTargets')
826
+ ?? config?.consoleTargets
827
+ ?? DEFAULT_CONSOLE_TARGETS;
828
+ const allowedTypes = new Set(requestedTypes
829
+ .filter((t) => typeof t === 'string')
830
+ .filter((t) => Object.values(TargetType).includes(t)));
831
+ // Lazy attach: ensure every matching target has a session
832
+ if (process.env.AV_DEBUG_CONSOLE) {
833
+ // eslint-disable-next-line no-console
834
+ console.error(`[av-debug] handleConsole: targets=${all.length} explicit=${resolvedTargetId ?? 'none'} types=${[...allowedTypes].join(',')}`);
835
+ }
836
+ for (const t of all) {
837
+ if (resolvedTargetId && t.id !== resolvedTargetId)
838
+ continue;
839
+ if (!resolvedTargetId && !allowedTypes.has(t.type))
840
+ continue;
841
+ if (!RUNTIME_ONLY_TARGETS.has(t.type) && t.type !== TargetType.Page && t.type !== TargetType.Iframe)
842
+ continue;
843
+ try {
844
+ const session = await this.getRuntimeSession(req, t);
845
+ this.consoleStream.attach(session);
846
+ if (process.env.AV_DEBUG_CONSOLE) {
847
+ // eslint-disable-next-line no-console
848
+ console.error(`[av-debug] handleConsole: attached ${t.type}:${t.id.slice(0, 8)} (stream now has ${this.consoleStream.attachedCount})`);
849
+ }
850
+ }
851
+ catch (err) {
852
+ if (process.env.AV_DEBUG_CONSOLE) {
853
+ // eslint-disable-next-line no-console
854
+ console.error(`[av-debug] handleConsole: SKIP ${t.type}:${t.id.slice(0, 8)} — ${err.message}`);
855
+ }
856
+ }
857
+ }
858
+ const levelFilter = parseLevelFilter(argStrArray(req.args, 'levels'));
859
+ const since = argNum(req.args, 'since');
860
+ const follow = argBool(req.args, 'follow') ?? false;
861
+ const untilPattern = argStr(req.args, 'until');
862
+ if (untilPattern && !follow) {
863
+ return { ok: false, error: '--until requires --follow' };
864
+ }
865
+ if (follow) {
866
+ const timeoutSec = argNum(req.args, 'timeout') ?? 10;
867
+ const collected = this.consoleStream.drain({
868
+ since,
869
+ level: levelFilter,
870
+ targetId: resolvedTargetId,
871
+ });
872
+ const seenAt = collected.length > 0 ? collected[collected.length - 1].ts : (since ?? Date.now());
873
+ const matcher = untilPattern ? buildMatcher(untilPattern) : null;
874
+ // Check if pattern already matched in buffered messages
875
+ const earlyMatch = matcher ? collected.findIndex(m => matcher(m.text)) : -1;
876
+ if (earlyMatch !== -1) {
877
+ return { ok: true, data: formatConsoleMessages(collected.slice(0, earlyMatch + 1)) };
878
+ }
879
+ const timedOut = await new Promise((resolveFollow) => {
880
+ const dispose = this.consoleStream.subscribe((msg) => {
881
+ if (resolvedTargetId && msg.targetId !== resolvedTargetId)
882
+ return;
883
+ if (levelFilter && !levelFilter.has(msg.level))
884
+ return;
885
+ if (msg.ts <= seenAt)
886
+ return;
887
+ collected.push(msg);
888
+ if (matcher && matcher(msg.text)) {
889
+ clearTimeout(timer);
890
+ dispose();
891
+ resolveFollow(false);
892
+ }
893
+ });
894
+ const timer = setTimeout(() => {
895
+ dispose();
896
+ resolveFollow(true);
897
+ }, timeoutSec * 1000);
898
+ timer.unref?.();
899
+ });
900
+ if (timedOut && matcher) {
901
+ return { ok: false, error: `Timeout: pattern not seen in ${timeoutSec}s` };
902
+ }
903
+ return { ok: true, data: formatConsoleMessages(collected) };
904
+ }
905
+ const messages = this.consoleStream.drain({
906
+ since,
907
+ level: levelFilter,
908
+ targetId: resolvedTargetId,
909
+ });
910
+ return { ok: true, data: formatConsoleMessages(messages) };
911
+ }
425
912
  async handleStop() {
426
913
  setTimeout(() => this.shutdown(), 100);
427
914
  return { ok: true, data: 'Server stopping' };
@@ -429,10 +916,14 @@ export class AgentViewServer {
429
916
  async shutdown() {
430
917
  if (this.idleTimer)
431
918
  clearTimeout(this.idleTimer);
919
+ for (const watch of [...this.activeWatches]) {
920
+ watch.stop(StopReason.ServerShutdown, false);
921
+ }
432
922
  await unlink(TOKEN_PATH).catch(() => { });
433
- for (const conn of this.connections.values()) {
923
+ this.consoleStream.detach();
924
+ for (const cached of this.connections.values()) {
434
925
  try {
435
- await conn.close();
926
+ await cached.session.close();
436
927
  }
437
928
  catch { /* ignore */ }
438
929
  }
@@ -441,4 +932,66 @@ export class AgentViewServer {
441
932
  process.exit(0);
442
933
  }
443
934
  }
935
+ // ── helpers ──────────────────────────────────────────────────────────────────
936
+ export function findTargetByIdOrSubstring(targets, query) {
937
+ const byId = targets.find(t => t.id === query);
938
+ if (byId)
939
+ return byId;
940
+ const q = query.toLowerCase();
941
+ return targets.find(t => t.title.toLowerCase().includes(q) || t.url.toLowerCase().includes(q)) ?? null;
942
+ }
943
+ function parseMouseButton(value) {
944
+ if (!value)
945
+ return undefined;
946
+ const valid = Object.values(MouseButton);
947
+ return valid.includes(value) ? value : undefined;
948
+ }
949
+ function parseLevelFilter(levels) {
950
+ if (!levels || levels.length === 0)
951
+ return undefined;
952
+ const valid = Object.values(ConsoleLevel);
953
+ const set = new Set();
954
+ for (const l of levels) {
955
+ if (valid.includes(l))
956
+ set.add(l);
957
+ }
958
+ return set.size > 0 ? set : undefined;
959
+ }
960
+ function buildMatcher(pattern) {
961
+ const regexMatch = /^\/(.+)\/([gimsuy]*)$/.exec(pattern);
962
+ if (regexMatch) {
963
+ const re = new RegExp(regexMatch[1], regexMatch[2]);
964
+ return (text) => re.test(text);
965
+ }
966
+ return (text) => text.includes(pattern);
967
+ }
968
+ function formatConsoleMessages(msgs) {
969
+ if (msgs.length === 0)
970
+ return '(no console messages)';
971
+ return msgs.map(formatOneConsoleMessage).join('\n');
972
+ }
973
+ function formatOneConsoleMessage(msg) {
974
+ const time = new Date(msg.ts).toISOString().slice(11, 23);
975
+ const head = `[${time}] [${msg.level}] [${msg.targetType}:${msg.targetId.slice(0, 8)}] ${msg.text}`;
976
+ return msg.stack ? `${head}\n${msg.stack}` : head;
977
+ }
978
+ function safeJSONStringify(value) {
979
+ try {
980
+ return JSON.stringify(value, null, 2) ?? String(value);
981
+ }
982
+ catch {
983
+ return String(value);
984
+ }
985
+ }
986
+ function formatEvalValue(value) {
987
+ if (value === undefined)
988
+ return 'undefined';
989
+ if (value === null)
990
+ return 'null';
991
+ if (typeof value === 'string')
992
+ return JSON.stringify(value);
993
+ if (typeof value === 'number' || typeof value === 'boolean')
994
+ return String(value);
995
+ return safeJSONStringify(value);
996
+ }
444
997
  //# sourceMappingURL=server.js.map