@petukhovart/agent-view 0.12.0 → 0.13.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.
Files changed (46) hide show
  1. package/.claude-plugin/plugin.json +21 -21
  2. package/LICENSE +21 -21
  3. package/README.md +41 -1
  4. package/dist/cdp/console-stream.d.ts +35 -0
  5. package/dist/cdp/console-stream.d.ts.map +1 -0
  6. package/dist/cdp/console-stream.js +103 -0
  7. package/dist/cdp/console-stream.js.map +1 -0
  8. package/dist/cdp/transport.d.ts.map +1 -1
  9. package/dist/cdp/transport.js +273 -12
  10. package/dist/cdp/transport.js.map +1 -1
  11. package/dist/cdp/types.d.ts +93 -0
  12. package/dist/cdp/types.d.ts.map +1 -1
  13. package/dist/cdp/types.js +15 -0
  14. package/dist/cdp/types.js.map +1 -1
  15. package/dist/cli/commands/dialog.d.ts +11 -0
  16. package/dist/cli/commands/dialog.d.ts.map +1 -0
  17. package/dist/cli/commands/dialog.js +30 -0
  18. package/dist/cli/commands/dialog.js.map +1 -0
  19. package/dist/cli/commands/upload.d.ts +10 -0
  20. package/dist/cli/commands/upload.d.ts.map +1 -0
  21. package/dist/cli/commands/upload.js +35 -0
  22. package/dist/cli/commands/upload.js.map +1 -0
  23. package/dist/cli/index.js +71 -2
  24. package/dist/cli/index.js.map +1 -1
  25. package/dist/inspectors/dialog/index.d.ts +20 -0
  26. package/dist/inspectors/dialog/index.d.ts.map +1 -0
  27. package/dist/inspectors/dialog/index.js +58 -0
  28. package/dist/inspectors/dialog/index.js.map +1 -0
  29. package/dist/inspectors/dom.d.ts +17 -0
  30. package/dist/inspectors/dom.d.ts.map +1 -0
  31. package/dist/inspectors/dom.js +131 -0
  32. package/dist/inspectors/dom.js.map +1 -0
  33. package/dist/inspectors/scene/pixi.d.ts +3 -0
  34. package/dist/inspectors/scene/pixi.d.ts.map +1 -0
  35. package/dist/inspectors/scene/pixi.js +52 -0
  36. package/dist/inspectors/scene/pixi.js.map +1 -0
  37. package/dist/server/server.d.ts +36 -5
  38. package/dist/server/server.d.ts.map +1 -1
  39. package/dist/server/server.js +288 -62
  40. package/dist/server/server.js.map +1 -1
  41. package/dist/server/tauri-dialog-shim.d.ts +27 -0
  42. package/dist/server/tauri-dialog-shim.d.ts.map +1 -0
  43. package/dist/server/tauri-dialog-shim.js +55 -0
  44. package/dist/server/tauri-dialog-shim.js.map +1 -0
  45. package/package.json +1 -1
  46. package/skills/verify/SKILL.md +119 -2
@@ -1,21 +1,21 @@
1
- {
2
- "name": "agent-view",
3
- "description": "Visual verification CLI for desktop apps (Electron/Tauri/Browser) via Chrome DevTools Protocol. Ships the verify skill: run DOM, screenshot, eval, watch, console and log-feed checks against a live app.",
4
- "version": "0.12.0",
5
- "keywords": [
6
- "cdp",
7
- "electron",
8
- "tauri",
9
- "desktop",
10
- "testing",
11
- "accessibility",
12
- "screenshot",
13
- "automation"
14
- ],
15
- "author": {
16
- "name": "PetukhovArt"
17
- },
18
- "homepage": "https://github.com/PetukhovArt/agent-view",
19
- "repository": "https://github.com/PetukhovArt/agent-view",
20
- "license": "MIT"
21
- }
1
+ {
2
+ "name": "agent-view",
3
+ "description": "Visual verification CLI for desktop apps (Electron/Tauri/Browser) via Chrome DevTools Protocol. Ships the verify skill: run DOM, screenshot, eval, watch, console and log-feed checks against a live app.",
4
+ "version": "0.13.0",
5
+ "keywords": [
6
+ "cdp",
7
+ "electron",
8
+ "tauri",
9
+ "desktop",
10
+ "testing",
11
+ "accessibility",
12
+ "screenshot",
13
+ "automation"
14
+ ],
15
+ "author": {
16
+ "name": "PetukhovArt"
17
+ },
18
+ "homepage": "https://github.com/PetukhovArt/agent-view",
19
+ "repository": "https://github.com/PetukhovArt/agent-view",
20
+ "license": "MIT"
21
+ }
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Artem Petukhov
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Artem Petukhov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -154,6 +154,8 @@ The daemon is why `dom → click → dom` runs in ~17ms total: one persistent CD
154
154
  | `console` | `console.log` + `Log.entryAdded` per page **and per worker**, with `--follow --until <pattern>`. |
155
155
  | `logs` | Durable file feed of page + worker console, surviving reloads and worker restarts. `tail --grep/--since/--level`, `clear`, plus re-injecting `--probe` scripts. |
156
156
  | `network` | Request/response timeline, headers, timing, bodies, and WebSocket/SSE frames. Filters: `--url`, `--method`, `--status`, `--type`. Captures page-load traffic. |
157
+ | `upload` | Puts files into a file input — no picker opens. `--selector` reaches the hidden inputs that have no ref. |
158
+ | `dialog` | JS modals are answered automatically so they cannot freeze the run; sets the answer, and pre-answers native file pickers with `arm`. |
157
159
  | `wait` | Block until an element appears (default 10s). |
158
160
  | `scene` | WebGL scene graph (PixiJS today, engine-pluggable). `--compact` and `--diff` mirror `dom`. |
159
161
  | `snap` | DOM + scene + optional screenshot in one call. |
@@ -320,7 +322,7 @@ Full form with all optional fields:
320
322
  | `consoleTargets` | no | Target types `agent-view console` auto-attaches to on first call. Any subset of `["page", "iframe", "shared_worker", "service_worker", "worker"]`. Default `["page", "shared_worker", "service_worker"]` |
321
323
  | `captureBody` | no | `true` to capture response bodies and request payloads for `agent-view network`. Off by default; opt-in since bodies can carry tokens/PII. WebSocket frame payloads are visible regardless |
322
324
  | `networkBufferSize` | no | Per-target network ring capacity. Positive integer. Default `200` (smaller than console — entries are heavier) |
323
- | `logFile` | no | Feed file for `agent-view logs`. Relative paths resolve against the project root. Default `.agent-view/console.log` (gitignore it) |
325
+ | `logFile` | no | Feed file for `agent-view logs`. Relative paths resolve against the project root. Default `.agent-view/console.log` (gitignore it). Keep it per-checkout: two ports recording into one file is refused |
324
326
  | `logMaxBytes` | no | Feed size cap in bytes; on overflow it rotates once to `<file>.prev`. Default `8388608` (8 MB) |
325
327
 
326
328
  ---
@@ -396,6 +398,44 @@ agent-view drag --from 5 --to 9 --steps 20 --hold-ms 150
396
398
 
397
399
  `--steps` (default 10) controls intermediate `mouseMoved` events so libraries that throttle on movement deltas still see continuous motion. `--hold-ms` inserts a pause between press and the first move (some libs require >100ms for touch-style activation). `--button` accepts `left|right|middle`.
398
400
 
401
+ ### `upload`
402
+
403
+ Puts files into a file input through CDP `DOM.setFileInputFiles`. No picker opens, so there is nothing to arm and nothing to close.
404
+
405
+ ```bash
406
+ agent-view upload --selector "#file-input" --file ./fixtures/a.png
407
+ agent-view upload --selector "#images" --file ./a.png --file ./b.png # multi-select input
408
+ agent-view upload --ref 12 --file ./a.png
409
+ ```
410
+
411
+ `--selector` is the flag you will usually want: upload inputs are almost always hidden (`display:none`, `class="d-none"`, `v-show="false"`) and clicked from code, which keeps them out of the accessibility tree — so `dom` never prints a `[ref=N]` for them. There is deliberately no `--filter`: an accessible name resolves to the label or the button carrying it, not to the `<input type=file>` behind it. Paths resolve against the current directory and must be real files; CDP accepts a missing path or a directory without complaint and the app then reads an empty file.
412
+
413
+ ### `dialog`
414
+
415
+ Everything modal. JS modals are answered automatically — this command is how you change that answer, inspect what happened, and pre-answer native file pickers.
416
+
417
+ ```bash
418
+ agent-view dialog # standing answer + log of modals and pickers
419
+ agent-view dialog policy accept # confirm() → true from now on
420
+ agent-view dialog policy accept --text "hi" # prompt() → "hi"
421
+ agent-view dialog policy dismiss # back to the default
422
+ agent-view dialog accept | dismiss # answer the one open right now
423
+ agent-view dialog arm --file ./a.png # pre-answer the next native file picker
424
+ agent-view dialog arm --cancel # …as if the user pressed Cancel
425
+ agent-view dialog disarm # let real pickers open again
426
+ ```
427
+
428
+ **JS modals.** `connectToPage` enables the CDP `Page` domain, and Chromium then stops showing `alert`/`confirm`/`prompt`/`beforeunload` natively — it blocks the renderer until the client answers. Every page session therefore answers on its own; the default is dismiss. `beforeunload` is always dismissed regardless of the policy: accepting it would navigate away mid-run and lose the state under inspection. Each one is written to the console feed as `[agent-view] confirm auto-dismissed: <message>`, which is how you explain a `confirm()` that returned `false` for no visible reason. `dialog accept` / `dialog dismiss` handle the one case the policy cannot: a modal that was already open before agent-view attached, which produced no event.
429
+
430
+ **Native file pickers.** `dialog arm` holds an answer ready so the picker never opens, and arms two engines at once — CDP file-chooser interception for webview pickers, and a `window.__TAURI_INTERNALS__.invoke` shim for `@tauri-apps/plugin-dialog`, whose dialog runs in Rust past the webview where CDP cannot reach it. Use it for the input that is created and removed inside the click handler; a selector cannot address that one. Otherwise prefer `upload`.
431
+
432
+ Two rules that bite:
433
+
434
+ - **Arm before the click.** A picker cannot be caught once open. The arm is one-shot: the first picker spends it and interception turns itself off, so a later click opens a real OS dialog.
435
+ - **Click with `agent-view click`, not `eval "el.click()"`.** Chromium refuses to open a file picker without user activation, and an eval-driven click has none — the picker is silently dropped and never intercepted.
436
+
437
+ Out of reach: `showOpenFilePicker()` (File System Access API) exposes no input to fill and can only be cancelled; Electron does not implement `window.prompt`; a native dialog opened from an Electron **main** process is invisible to CDP.
438
+
399
439
  ### `screenshot`
400
440
 
401
441
  Captures a screenshot, saves to temp dir, prints the file path. PNG by default; WebP (q=80) when `--scale` is set (JPEG fallback for older Chrome/Electron).
@@ -0,0 +1,35 @@
1
+ import type { RuntimeSession, ConsoleMessage, ConsoleLevel, TargetType } from './types.js';
2
+ export type ConsoleStreamOptions = {
3
+ /** Per-target ring size. Default 500. */
4
+ capacity?: number;
5
+ };
6
+ export type ConsoleFilter = {
7
+ since?: number;
8
+ level?: ReadonlySet<ConsoleLevel>;
9
+ targetId?: string;
10
+ };
11
+ export type StampedConsoleMessage = ConsoleMessage & {
12
+ targetId: string;
13
+ targetType: TargetType;
14
+ };
15
+ /**
16
+ * Multi-target console subscription with per-target ring buffer.
17
+ * Server owns one instance for its lifetime; commands attach/drain.
18
+ */
19
+ export declare class ConsoleStream {
20
+ private readonly capacity;
21
+ private readonly targets;
22
+ private readonly extraSubscribers;
23
+ constructor(opts?: ConsoleStreamOptions);
24
+ get attachedCount(): number;
25
+ /** Idempotent per target id — second attach for same id is a no-op. */
26
+ attach(session: RuntimeSession): void;
27
+ detach(targetId?: string): void;
28
+ clear(targetId?: string): void;
29
+ /** Snapshot of buffered messages, sorted by timestamp (oldest first). */
30
+ drain(filter?: ConsoleFilter): StampedConsoleMessage[];
31
+ /** Subscribe to live messages. Returns disposer. Used by --follow streaming. */
32
+ subscribe(handler: (msg: StampedConsoleMessage) => void): () => void;
33
+ private snapshot;
34
+ }
35
+ //# sourceMappingURL=console-stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-stream.d.ts","sourceRoot":"","sources":["../../src/cdp/console-stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAI1F,MAAM,MAAM,oBAAoB,GAAG;IACjC,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,CAAA;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG,cAAc,GAAG;IACnD,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,UAAU,CAAA;CACvB,CAAA;AAUD;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoC;IAC5D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAkD;gBAEvE,IAAI,GAAE,oBAAyB;IAI3C,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED,uEAAuE;IACvE,MAAM,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IA6BrC,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IAY/B,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IAc9B,yEAAyE;IACzE,KAAK,CAAC,MAAM,GAAE,aAAkB,GAAG,qBAAqB,EAAE;IAe1D,gFAAgF;IAChF,SAAS,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,qBAAqB,KAAK,IAAI,GAAG,MAAM,IAAI;IAKpE,OAAO,CAAC,QAAQ;CAIjB"}
@@ -0,0 +1,103 @@
1
+ const DEFAULT_CAPACITY = 500;
2
+ /**
3
+ * Multi-target console subscription with per-target ring buffer.
4
+ * Server owns one instance for its lifetime; commands attach/drain.
5
+ */
6
+ export class ConsoleStream {
7
+ capacity;
8
+ targets = new Map();
9
+ extraSubscribers = new Set();
10
+ constructor(opts = {}) {
11
+ this.capacity = opts.capacity ?? DEFAULT_CAPACITY;
12
+ }
13
+ get attachedCount() {
14
+ return this.targets.size;
15
+ }
16
+ /** Idempotent per target id — second attach for same id is a no-op. */
17
+ attach(session) {
18
+ const id = session.target.id;
19
+ if (this.targets.has(id))
20
+ return;
21
+ const entry = {
22
+ session,
23
+ ring: new Array(this.capacity),
24
+ cursor: 0,
25
+ full: false,
26
+ dispose: () => { },
27
+ };
28
+ entry.dispose = session.onConsole((msg) => {
29
+ const stamped = {
30
+ ...msg,
31
+ targetId: id,
32
+ targetType: session.target.type,
33
+ };
34
+ entry.ring[entry.cursor] = stamped;
35
+ entry.cursor = (entry.cursor + 1) % this.capacity;
36
+ if (entry.cursor === 0)
37
+ entry.full = true;
38
+ for (const sub of this.extraSubscribers) {
39
+ try {
40
+ sub(stamped);
41
+ }
42
+ catch { /* ignore */ }
43
+ }
44
+ });
45
+ this.targets.set(id, entry);
46
+ }
47
+ detach(targetId) {
48
+ if (targetId === undefined) {
49
+ for (const t of this.targets.values())
50
+ t.dispose();
51
+ this.targets.clear();
52
+ return;
53
+ }
54
+ const t = this.targets.get(targetId);
55
+ if (!t)
56
+ return;
57
+ t.dispose();
58
+ this.targets.delete(targetId);
59
+ }
60
+ clear(targetId) {
61
+ if (targetId === undefined) {
62
+ for (const t of this.targets.values()) {
63
+ t.cursor = 0;
64
+ t.full = false;
65
+ }
66
+ return;
67
+ }
68
+ const t = this.targets.get(targetId);
69
+ if (!t)
70
+ return;
71
+ t.cursor = 0;
72
+ t.full = false;
73
+ }
74
+ /** Snapshot of buffered messages, sorted by timestamp (oldest first). */
75
+ drain(filter = {}) {
76
+ const result = [];
77
+ for (const [id, entry] of this.targets) {
78
+ if (filter.targetId && filter.targetId !== id)
79
+ continue;
80
+ const msgs = this.snapshot(entry);
81
+ for (const m of msgs) {
82
+ if (filter.since !== undefined && m.ts < filter.since)
83
+ continue;
84
+ if (filter.level && !filter.level.has(m.level))
85
+ continue;
86
+ result.push(m);
87
+ }
88
+ }
89
+ result.sort((a, b) => a.ts - b.ts);
90
+ return result;
91
+ }
92
+ /** Subscribe to live messages. Returns disposer. Used by --follow streaming. */
93
+ subscribe(handler) {
94
+ this.extraSubscribers.add(handler);
95
+ return () => this.extraSubscribers.delete(handler);
96
+ }
97
+ snapshot(entry) {
98
+ if (!entry.full)
99
+ return entry.ring.slice(0, entry.cursor);
100
+ return [...entry.ring.slice(entry.cursor), ...entry.ring.slice(0, entry.cursor)];
101
+ }
102
+ }
103
+ //# sourceMappingURL=console-stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-stream.js","sourceRoot":"","sources":["../../src/cdp/console-stream.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/cdp/transport.ts"],"names":[],"mappings":"AAEA,OAAO,EAWL,KAAK,SAAS,EAId,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAOjB,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AA4BhD,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAmCD,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAkBpE;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAQ9E;AA4YD,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC,CA2BhG;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,WAAW,CAAC,CA6PtB"}
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/cdp/transport.ts"],"names":[],"mappings":"AAEA,OAAO,EAaL,KAAK,SAAS,EAId,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAWjB,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AA4BhD,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAmCD,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAkBpE;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAQ9E;AAoqBD,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC,CA2BhG;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,WAAW,CAAC,CA2StB"}
@@ -1,6 +1,6 @@
1
1
  // @ts-expect-error no types available for chrome-remote-interface
2
2
  import CDP from 'chrome-remote-interface';
3
- import { TargetType, ConsoleLevel, ConsoleSource, EvaluationError, MouseButton, NetworkEventKind, NetworkResourceType, } from './types.js';
3
+ import { TargetType, ConsoleLevel, ConsoleSource, EvaluationError, MouseButton, NetworkEventKind, NetworkResourceType, JsDialogType, FileChooserMode, } from './types.js';
4
4
  // CDP hosts to try: IPv4 first, then IPv6 (WebView2/Tauri often listens on ::1)
5
5
  const CDP_HOSTS = ['127.0.0.1', '::1'];
6
6
  // Maps targetId → host for connection routing
@@ -185,6 +185,228 @@ function attachConsoleSubscription(client) {
185
185
  },
186
186
  };
187
187
  }
188
+ /** Dialogs are rare; a short tail is enough to explain a surprising `confirm()` result. */
189
+ const MAX_TRACKED_DIALOGS = 20;
190
+ const DIALOG_TYPES = new Set(Object.values(JsDialogType));
191
+ function dialogTypeFrom(raw) {
192
+ return raw && DIALOG_TYPES.has(raw) ? raw : JsDialogType.Alert;
193
+ }
194
+ function dialogNotice(dialog, accept) {
195
+ const verb = accept ? 'auto-accepted' : 'auto-dismissed';
196
+ const body = dialog.message ? `: ${dialog.message}` : '';
197
+ return {
198
+ ts: Date.now(),
199
+ level: ConsoleLevel.Warn,
200
+ source: ConsoleSource.Log,
201
+ text: `[agent-view] ${dialog.type} ${verb}${body}`,
202
+ };
203
+ }
204
+ /**
205
+ * Answers JS dialogs so the renderer never stalls.
206
+ *
207
+ * While the Page domain is enabled Chromium stops showing the native dialog and
208
+ * waits for `Page.handleJavaScriptDialog` instead — with no client answer the
209
+ * page hangs forever. Since `connectToPage` always enables Page, every page
210
+ * session must own this. The standing policy dismisses; `setPolicy` changes it.
211
+ */
212
+ function attachJsDialogSubscription(Page, notice) {
213
+ const handlers = new Set();
214
+ const seen = [];
215
+ let policy = { accept: false };
216
+ let pending = null;
217
+ async function send(accept, promptText, automatic) {
218
+ const params = { accept };
219
+ if (promptText !== undefined)
220
+ params.promptText = promptText;
221
+ const answered = pending;
222
+ pending = null;
223
+ try {
224
+ await Page.handleJavaScriptDialog(params);
225
+ }
226
+ catch (err) {
227
+ // Only the dialog this call was answering may be restored: a later one
228
+ // may already have taken the slot, and overwriting it would make the
229
+ // session answer the wrong dialog next time.
230
+ if (pending === null)
231
+ pending = answered;
232
+ throw err;
233
+ }
234
+ if (answered) {
235
+ answered.answer = { accept, promptText, automatic };
236
+ return;
237
+ }
238
+ // A blind answer — the dialog opened before agent-view attached, so no
239
+ // opening event created a record. Without this the one scenario the manual
240
+ // command exists for leaves no trace in `dialog status`.
241
+ record({
242
+ ts: Date.now(),
243
+ type: JsDialogType.Alert,
244
+ message: '(opened before agent-view attached)',
245
+ url: '',
246
+ answer: { accept, promptText, automatic },
247
+ });
248
+ }
249
+ function record(dialog) {
250
+ seen.push(dialog);
251
+ if (seen.length > MAX_TRACKED_DIALOGS)
252
+ seen.shift();
253
+ return dialog;
254
+ }
255
+ Page.javascriptDialogOpening((params) => {
256
+ const dialog = {
257
+ ts: Date.now(),
258
+ type: dialogTypeFrom(params.type),
259
+ message: params.message ?? '',
260
+ defaultPrompt: params.defaultPrompt,
261
+ url: params.url ?? '',
262
+ };
263
+ pending = dialog;
264
+ record(dialog);
265
+ for (const handler of handlers) {
266
+ try {
267
+ handler(dialog);
268
+ }
269
+ catch { /* one bad handler shouldn't break others */ }
270
+ }
271
+ // `beforeunload` is not a question about the app's own state, it is a
272
+ // question about leaving the page. Accepting it would navigate away in the
273
+ // middle of a verification run and lose everything under inspection, so the
274
+ // standing answer — which exists to make `confirm()` return true — does not
275
+ // reach it. Staying put is always the recoverable outcome.
276
+ const accept = dialog.type === JsDialogType.BeforeUnload ? false : policy.accept;
277
+ const promptText = accept ? policy.promptText : undefined;
278
+ void send(accept, promptText, true)
279
+ .then(() => notice(dialogNotice(dialog, accept)))
280
+ .catch(() => { });
281
+ });
282
+ return {
283
+ add(handler) {
284
+ handlers.add(handler);
285
+ return () => handlers.delete(handler);
286
+ },
287
+ /**
288
+ * Sent even with nothing pending: a dialog opened before agent-view attached
289
+ * never produced an opening event, and answering blind is the only way out.
290
+ */
291
+ answer: (accept, promptText) => send(accept, promptText, false),
292
+ setPolicy: (next) => { policy = next; },
293
+ getPolicy: () => policy,
294
+ recent: () => [...seen],
295
+ };
296
+ }
297
+ /**
298
+ * Intercepts the native file chooser and answers it from the armed value.
299
+ *
300
+ * Interception stays off until something is armed: turning it on unconditionally
301
+ * would suppress a chooser the user actually wanted to see, and the app would
302
+ * look broken. Arming is one-shot for the same reason — a standing intercept
303
+ * silently changes behaviour long after the command that set it.
304
+ */
305
+ function attachFileChooserSubscription(Page, setFiles, dispatchCancel, notice) {
306
+ const seen = [];
307
+ let armed = null;
308
+ const record = (event) => {
309
+ seen.push(event);
310
+ if (seen.length > MAX_TRACKED_DIALOGS)
311
+ seen.shift();
312
+ return event;
313
+ };
314
+ Page.fileChooserOpened((params) => {
315
+ const pending = armed;
316
+ armed = null;
317
+ const event = record({
318
+ ts: Date.now(),
319
+ mode: params.mode === FileChooserMode.Multiple ? FileChooserMode.Multiple : FileChooserMode.Single,
320
+ matchedInput: params.backendNodeId !== undefined,
321
+ answer: 'unanswered',
322
+ });
323
+ /**
324
+ * Answering is async, so a fresh `arm` can land while this one is still in
325
+ * flight. Turning interception off then would kill the new arm and let the
326
+ * next chooser open as a real OS window, while `fileChooserArm()` still
327
+ * reports it armed — so only the arm that is still current disarms.
328
+ */
329
+ const disarm = () => {
330
+ if (armed !== null)
331
+ return Promise.resolve();
332
+ return Page.setInterceptFileChooserDialog({ enabled: false }).catch(() => { });
333
+ };
334
+ const cancelOnPage = (backendNodeId, outcome) => {
335
+ // Suppressing the chooser is not enough: the page is waiting on
336
+ // `input.oncancel`, and Chromium's own cancel path does not deliver it
337
+ // here. Without this event the app waits for a user who will never come.
338
+ void dispatchCancel(backendNodeId)
339
+ .then(() => notice(chooserNotice(outcome)))
340
+ .catch((err) => notice(chooserNotice(`cancel could not be delivered: ${errText(err)}`)))
341
+ .finally(() => disarm());
342
+ };
343
+ if (!pending || pending.kind === 'cancel') {
344
+ event.answer = 'cancel';
345
+ if (params.backendNodeId !== undefined) {
346
+ cancelOnPage(params.backendNodeId, 'cancelled');
347
+ return;
348
+ }
349
+ notice(chooserNotice('cancelled'));
350
+ void disarm();
351
+ return;
352
+ }
353
+ if (params.backendNodeId === undefined) {
354
+ // Nothing to attach files to. The chooser is already suppressed, so the
355
+ // call resolves as a cancel — say so instead of reporting a fake success.
356
+ event.answer = 'cancel';
357
+ notice(chooserNotice('cancelled — chooser had no file input (File System Access API)'));
358
+ void disarm();
359
+ return;
360
+ }
361
+ const { backendNodeId } = params;
362
+ void setFiles(backendNodeId, pending.files)
363
+ .then(() => {
364
+ event.answer = 'files';
365
+ event.files = pending.files;
366
+ notice(chooserNotice(`answered with ${pending.files.length} file(s)`));
367
+ void disarm();
368
+ })
369
+ .catch((err) => {
370
+ // The chooser is suppressed either way, so failing to attach the files
371
+ // must still produce an answer — otherwise the page waits forever,
372
+ // which is the exact hang this feature exists to prevent.
373
+ event.answer = 'cancel';
374
+ cancelOnPage(backendNodeId, `files could not be attached (${errText(err)}), cancelled instead`);
375
+ });
376
+ });
377
+ return {
378
+ async arm(next) {
379
+ armed = next;
380
+ // Interception only suppresses the chooser; what the page sees afterwards
381
+ // is decided here, not by Chromium's own `cancel` flag.
382
+ await Page.setInterceptFileChooserDialog(next ? { enabled: true, cancel: false } : { enabled: false });
383
+ },
384
+ current: () => armed,
385
+ recent: () => [...seen],
386
+ /**
387
+ * A new document means the click the arm was meant for will never happen.
388
+ * Leaving it live would answer some later, unrelated chooser — the standing
389
+ * intercept that one-shot arming exists to avoid.
390
+ */
391
+ forget() {
392
+ if (armed === null)
393
+ return;
394
+ armed = null;
395
+ void Page.setInterceptFileChooserDialog({ enabled: false }).catch(() => { });
396
+ },
397
+ };
398
+ }
399
+ function errText(err) {
400
+ return err instanceof Error ? err.message : String(err);
401
+ }
402
+ function chooserNotice(outcome) {
403
+ return {
404
+ ts: Date.now(),
405
+ level: ConsoleLevel.Warn,
406
+ source: ConsoleSource.Log,
407
+ text: `[agent-view] file chooser intercepted, ${outcome}`,
408
+ };
409
+ }
188
410
  const CDP_RESOURCE_TYPE_MAP = {
189
411
  Document: NetworkResourceType.Document,
190
412
  Stylesheet: NetworkResourceType.Stylesheet,
@@ -446,6 +668,14 @@ export async function connectToPage(port, target, cache) {
446
668
  // Subscribe BEFORE enable so we catch buffered console/log entries emitted at enable-time.
447
669
  const consoleSub = attachConsoleSubscription(client);
448
670
  const networkSub = attachNetworkSubscription(client.Network);
671
+ const jsDialogSub = attachJsDialogSubscription(Page, consoleSub.emit);
672
+ const fileChooserSub = attachFileChooserSubscription(Page, (backendNodeId, files) => DOM.setFileInputFiles({ backendNodeId, files }).then(() => undefined), async (backendNodeId) => {
673
+ const { object } = await DOM.resolveNode({ backendNodeId });
674
+ await Runtime.callFunctionOn({
675
+ objectId: object.objectId,
676
+ functionDeclaration: `function() { this.dispatchEvent(new Event('cancel')) }`,
677
+ });
678
+ }, consoleSub.emit);
449
679
  const onDisconnect = attachDisconnectSubscription(client);
450
680
  let documentBackendNodeId = 0;
451
681
  await bringUpSession(client, target, async () => {
@@ -464,6 +694,7 @@ export async function connectToPage(port, target, cache) {
464
694
  let webpSupported = null;
465
695
  Page.frameNavigated(async () => {
466
696
  cache.invalidate(cacheKey);
697
+ fileChooserSub.forget();
467
698
  try {
468
699
  const { root: newRoot } = await DOM.getDocument({ depth: 0 });
469
700
  documentBackendNodeId = newRoot.backendNodeId;
@@ -522,10 +753,10 @@ export async function connectToPage(port, target, cache) {
522
753
  const { object } = await DOM.resolveNode({ backendNodeId });
523
754
  const climbed = await Runtime.callFunctionOn({
524
755
  objectId: object.objectId,
525
- functionDeclaration: `function(levels) {
526
- let el = this.nodeType === Node.TEXT_NODE ? this.parentElement : this;
527
- for (let i = 0; i < levels && el.parentElement; i++) el = el.parentElement;
528
- return el;
756
+ functionDeclaration: `function(levels) {
757
+ let el = this.nodeType === Node.TEXT_NODE ? this.parentElement : this;
758
+ for (let i = 0; i < levels && el.parentElement; i++) el = el.parentElement;
759
+ return el;
529
760
  }`,
530
761
  arguments: [{ value: levels }],
531
762
  });
@@ -555,6 +786,14 @@ export async function connectToPage(port, target, cache) {
555
786
  onNetwork: (handler) => networkSub.add(handler),
556
787
  enableNetwork: () => networkSub.enable(),
557
788
  getResponseBody: (requestId) => networkSub.getResponseBody(requestId),
789
+ onJsDialog: (handler) => jsDialogSub.add(handler),
790
+ answerJsDialog: (accept, promptText) => jsDialogSub.answer(accept, promptText),
791
+ setJsDialogPolicy: (policy) => jsDialogSub.setPolicy(policy),
792
+ getJsDialogPolicy: () => jsDialogSub.getPolicy(),
793
+ recentJsDialogs: () => jsDialogSub.recent(),
794
+ armFileChooser: (arm) => fileChooserSub.arm(arm),
795
+ fileChooserArm: () => fileChooserSub.current(),
796
+ recentFileChoosers: () => fileChooserSub.recent(),
558
797
  async getAccessibilityTree() {
559
798
  const cached = cache.get(cacheKey);
560
799
  if (cached)
@@ -641,18 +880,40 @@ export async function connectToPage(port, target, cache) {
641
880
  async dragBetweenPositions(from, to, opts) {
642
881
  await dispatchDrag(from, to, opts);
643
882
  },
883
+ async uploadByNodeId(backendNodeId, files) {
884
+ await DOM.setFileInputFiles({ backendNodeId, files });
885
+ },
886
+ async uploadBySelector(selector, files) {
887
+ // Addressed by objectId rather than nodeId: `DOM.querySelector` needs a
888
+ // document nodeId, and the document node is re-numbered on every
889
+ // navigation. A RemoteObject from the page skips that bookkeeping.
890
+ const { result, exceptionDetails } = await Runtime.evaluate({
891
+ expression: `document.querySelector(${JSON.stringify(selector)})`,
892
+ returnByValue: false,
893
+ });
894
+ // A malformed selector throws, and the thrown DOMException comes back as
895
+ // a perfectly good RemoteObject — without this check it would be passed
896
+ // to setFileInputFiles as if it were the matched node.
897
+ if (exceptionDetails) {
898
+ throw new EvaluationError(`Invalid selector ${JSON.stringify(selector)}`);
899
+ }
900
+ if (!result.objectId || result.subtype === 'null')
901
+ return false;
902
+ await DOM.setFileInputFiles({ objectId: result.objectId, files });
903
+ return true;
904
+ },
644
905
  async fillByNodeId(backendNodeId, value) {
645
906
  const { object } = await DOM.resolveNode({ backendNodeId });
646
907
  await DOM.focus({ backendNodeId });
647
908
  await Runtime.callFunctionOn({
648
909
  objectId: object.objectId,
649
- functionDeclaration: `function(val) {
650
- const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set
651
- || Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
652
- if (nativeSetter) nativeSetter.call(this, val);
653
- else this.value = val;
654
- this.dispatchEvent(new Event('input', { bubbles: true }));
655
- this.dispatchEvent(new Event('change', { bubbles: true }));
910
+ functionDeclaration: `function(val) {
911
+ const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set
912
+ || Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
913
+ if (nativeSetter) nativeSetter.call(this, val);
914
+ else this.value = val;
915
+ this.dispatchEvent(new Event('input', { bubbles: true }));
916
+ this.dispatchEvent(new Event('change', { bubbles: true }));
656
917
  }`,
657
918
  arguments: [{ value }],
658
919
  });