@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.
- package/.claude-plugin/plugin.json +21 -21
- package/LICENSE +21 -21
- package/README.md +41 -1
- package/dist/cdp/console-stream.d.ts +35 -0
- package/dist/cdp/console-stream.d.ts.map +1 -0
- package/dist/cdp/console-stream.js +103 -0
- package/dist/cdp/console-stream.js.map +1 -0
- package/dist/cdp/transport.d.ts.map +1 -1
- package/dist/cdp/transport.js +273 -12
- package/dist/cdp/transport.js.map +1 -1
- package/dist/cdp/types.d.ts +93 -0
- package/dist/cdp/types.d.ts.map +1 -1
- package/dist/cdp/types.js +15 -0
- package/dist/cdp/types.js.map +1 -1
- package/dist/cli/commands/dialog.d.ts +11 -0
- package/dist/cli/commands/dialog.d.ts.map +1 -0
- package/dist/cli/commands/dialog.js +30 -0
- package/dist/cli/commands/dialog.js.map +1 -0
- package/dist/cli/commands/upload.d.ts +10 -0
- package/dist/cli/commands/upload.d.ts.map +1 -0
- package/dist/cli/commands/upload.js +35 -0
- package/dist/cli/commands/upload.js.map +1 -0
- package/dist/cli/index.js +71 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/inspectors/dialog/index.d.ts +20 -0
- package/dist/inspectors/dialog/index.d.ts.map +1 -0
- package/dist/inspectors/dialog/index.js +58 -0
- package/dist/inspectors/dialog/index.js.map +1 -0
- package/dist/inspectors/dom.d.ts +17 -0
- package/dist/inspectors/dom.d.ts.map +1 -0
- package/dist/inspectors/dom.js +131 -0
- package/dist/inspectors/dom.js.map +1 -0
- package/dist/inspectors/scene/pixi.d.ts +3 -0
- package/dist/inspectors/scene/pixi.d.ts.map +1 -0
- package/dist/inspectors/scene/pixi.js +52 -0
- package/dist/inspectors/scene/pixi.js.map +1 -0
- package/dist/server/server.d.ts +36 -5
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +288 -62
- package/dist/server/server.js.map +1 -1
- package/dist/server/tauri-dialog-shim.d.ts +27 -0
- package/dist/server/tauri-dialog-shim.d.ts.map +1 -0
- package/dist/server/tauri-dialog-shim.js +55 -0
- package/dist/server/tauri-dialog-shim.js.map +1 -0
- package/package.json +1 -1
- 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.
|
|
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,
|
|
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"}
|
package/dist/cdp/transport.js
CHANGED
|
@@ -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
|
});
|