@petukhovart/agent-view 0.14.0 → 0.16.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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +61 -12
- package/dist/cdp/png.d.ts +15 -0
- package/dist/cdp/png.d.ts.map +1 -0
- package/dist/cdp/png.js +147 -0
- package/dist/cdp/png.js.map +1 -0
- package/dist/cdp/transport.d.ts.map +1 -1
- package/dist/cdp/transport.js +82 -43
- package/dist/cdp/transport.js.map +1 -1
- package/dist/cdp/types.d.ts +33 -5
- package/dist/cdp/types.d.ts.map +1 -1
- package/dist/cdp/types.js.map +1 -1
- package/dist/cli/commands/drag.d.ts +4 -0
- package/dist/cli/commands/drag.d.ts.map +1 -1
- package/dist/cli/commands/drag.js +18 -0
- package/dist/cli/commands/drag.js.map +1 -1
- package/dist/cli/commands/heap.d.ts +14 -0
- package/dist/cli/commands/heap.d.ts.map +1 -0
- package/dist/cli/commands/heap.js +25 -0
- package/dist/cli/commands/heap.js.map +1 -0
- package/dist/cli/index.js +67 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/inspectors/coverage/index.d.ts.map +1 -1
- package/dist/inspectors/coverage/index.js +2 -5
- package/dist/inspectors/coverage/index.js.map +1 -1
- package/dist/inspectors/format.d.ts +6 -0
- package/dist/inspectors/format.d.ts.map +1 -0
- package/dist/inspectors/format.js +17 -0
- package/dist/inspectors/format.js.map +1 -0
- package/dist/inspectors/heap/index.d.ts +65 -0
- package/dist/inspectors/heap/index.d.ts.map +1 -0
- package/dist/inspectors/heap/index.js +269 -0
- package/dist/inspectors/heap/index.js.map +1 -0
- package/dist/server/server.d.ts +12 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +93 -14
- package/dist/server/server.js.map +1 -1
- package/package.json +2 -1
- package/skills/verify/SKILL.md +2 -0
- package/skills/verify/references/commands.md +35 -2
- package/skills/verify/references/memory-leaks.md +94 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "agent-view",
|
|
10
10
|
"source": "./",
|
|
11
|
-
"description": "Visual verification CLI for
|
|
11
|
+
"description": "Visual verification CLI for Electron/Tauri/Chromium apps via Chrome DevTools Protocol. Ships with verify skill for easy and cheap checks against a live app, including heap-snapshot memory-leak debugging."
|
|
12
12
|
}
|
|
13
13
|
]
|
|
14
14
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-view",
|
|
3
|
-
"description": "Visual verification CLI for
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "Visual verification CLI for Electron/Tauri/Chromium apps via Chrome DevTools Protocol. Ships with verify skill for easy and cheap checks against a live app, including heap-snapshot memory-leak debugging.",
|
|
4
|
+
"version": "0.16.1",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cdp",
|
|
7
7
|
"electron",
|
package/README.md
CHANGED
|
@@ -31,10 +31,10 @@ Works with any agent that can run shell commands. There's a Claude Code plugin i
|
|
|
31
31
|
|
|
32
32
|
- Reads state inside `SharedWorker`, `ServiceWorker`, and dedicated workers. Half of a modern app's state lives there, and most browser-automation tools don't follow it.
|
|
33
33
|
- Every command takes `--window <id>`. Settings, tray, and detached windows in Electron and Tauri apps work the same as the main window.
|
|
34
|
-
- Electron, Tauri (WebView2
|
|
34
|
+
- Electron, Tauri on Windows (WebView2), and plain Chromium. One CLI, same commands. Tauri on macOS/Linux embeds WebKit, which has no Chrome DevTools Protocol, so it is out of reach.
|
|
35
35
|
- `click`, `fill`, and `drag` fire real CDP input events. Vue `v-model`, React controlled inputs, and native fields actually accept the value; synthetic DOM events fail silently there.
|
|
36
36
|
- `watch` emits RFC-6902 JSON-patches of any JS expression between two events. Answers "what mutated after the click?" without parsing screenshots.
|
|
37
|
-
- `dom` returns the accessibility tree with `[ref=N]` handles. `--compact` cuts deep trees by 40–60%; `--diff`, `--count`, and `--max-lines` keep output bounded. `screenshot --crop` and
|
|
37
|
+
- `dom` returns the accessibility tree with `[ref=N]` handles. `--compact` cuts deep trees by 40–60%; `--diff`, `--count`, and `--max-lines` keep output bounded. `screenshot --crop` and `--scale` do the same for vision tokens.
|
|
38
38
|
- Lazy CDP daemon, one persistent socket, 300 ms AX-tree cache. `dom → click → dom` in about 17 ms.
|
|
39
39
|
|
|
40
40
|
---
|
|
@@ -86,7 +86,7 @@ In `package.json`, wrap the dev script with [`cross-env`](https://www.npmjs.com/
|
|
|
86
86
|
}
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
Then `npm run dev` as usual. Devtools must be enabled in `tauri.conf.json` (default in `tauri dev`; for release builds, enable the `devtools` Cargo feature). macOS/Linux WebKit
|
|
89
|
+
Then `npm run dev` as usual. Devtools must be enabled in `tauri.conf.json` (default in `tauri dev`; for release builds, enable the `devtools` Cargo feature). Windows only: on macOS/Linux Tauri embeds WebKit, which speaks the WebKit Remote Inspector protocol, not CDP, so agent-view cannot attach there.
|
|
90
90
|
</details>
|
|
91
91
|
|
|
92
92
|
<details>
|
|
@@ -147,8 +147,8 @@ The daemon is why `dom → click → dom` runs in ~17ms total: one persistent CD
|
|
|
147
147
|
| Command | What it gives the agent |
|
|
148
148
|
|---------------|-----------------------------------------------------------------------------------------|
|
|
149
149
|
| `dom` | Accessibility tree with `[ref=N]` handles. Flags: `--filter`, `--compact`, `--count`, `--max-lines`, `--diff`. |
|
|
150
|
-
| `screenshot` | PNG,
|
|
151
|
-
| `click` / `fill` / `drag` | Real CDP input events. Works with Vue/React/native and
|
|
150
|
+
| `screenshot` | PNG, scaled with `--scale`, or `--crop <element>` for one element only. Cuts vision tokens. |
|
|
151
|
+
| `click` / `fill` / `drag` | Real CDP input events. Works with Vue/React/native; `drag` does HTML5 DnD through `Input.dragIntercepted` and pointer DnD through mouse events. |
|
|
152
152
|
| `eval` | Run JS in the page's main world. Read store/state directly instead of scraping DOM. |
|
|
153
153
|
| `watch` | Stream JSON-patch diffs of any expression. Answers "what changed between click and final state?". |
|
|
154
154
|
| `console` | `console.log` + `Log.entryAdded` per page **and per worker**, with `--follow --until <pattern>`. |
|
|
@@ -156,6 +156,7 @@ The daemon is why `dom → click → dom` runs in ~17ms total: one persistent CD
|
|
|
156
156
|
| `network` | Request/response timeline, headers, timing, bodies, and WebSocket/SSE frames. Filters: `--url`, `--method`, `--status`, `--type`. Captures page-load traffic. |
|
|
157
157
|
| `coverage` | Which functions ran since the last `--clear`. Turns "does any action reach this code?" into a positive answer. Filters: `--filter`, `--file`, `--count`. |
|
|
158
158
|
| `listeners` | Event handlers bound to a node, each with the `file:line` it was declared at. |
|
|
159
|
+
| `heap` | Named heap snapshots diffed by class: what grew after repeating an action, how many are detached DOM nodes, and what retains them. |
|
|
159
160
|
| `upload` | Puts files into a file input — no picker opens. `--selector` reaches the hidden inputs that have no ref. |
|
|
160
161
|
| `dialog` | JS modals are answered automatically so they cannot freeze the run; sets the answer, and pre-answers native file pickers with `arm`. |
|
|
161
162
|
| `wait` | Block until an element appears (default 10s). |
|
|
@@ -266,7 +267,7 @@ npx electron-vite dev -- --remote-debugging-port=9876
|
|
|
266
267
|
|
|
267
268
|
| Runtime | Setup |
|
|
268
269
|
|----------------------|--------------------------------------------|
|
|
269
|
-
| **Tauri**
|
|
270
|
+
| **Tauri (Windows)** | `WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS`, see above. WebKit builds (macOS/Linux) are not supported |
|
|
270
271
|
| **Any Chromium app** | `--remote-debugging-port=9876` launch flag |
|
|
271
272
|
|
|
272
273
|
### Verify CDP is working
|
|
@@ -390,16 +391,37 @@ agent-view fill 3 "hello@example.com"
|
|
|
390
391
|
|
|
391
392
|
### `drag`
|
|
392
393
|
|
|
393
|
-
|
|
394
|
+
Drag-and-drop over CDP. Two paths, picked automatically:
|
|
395
|
+
|
|
396
|
+
- **HTML5** (`draggable=true`, `dragstart`/`dragover`/`drop`). Plain mouse events never start one in Chromium: `dragstart` does not fire, `dataTransfer` stays empty, `drop` never arrives. So `drag` enables `Input.setInterceptDrags`, presses and moves, and when Chromium reports `Input.dragIntercepted` it finishes the gesture with `Input.dispatchDragEvent` (`dragEnter` → N × `dragOver` → `drop`), carrying the app's real `dataTransfer` payload. The same mechanism Puppeteer uses.
|
|
397
|
+
- **Pointer** (`mousedown`/`mousemove`/`mouseup`, pointer events). Used when no `dragIntercepted` arrives within 1 s: `vue-draggable-resizable`, `react-grid-layout`, gridstack, canvas, resize handles.
|
|
394
398
|
|
|
395
399
|
```bash
|
|
396
400
|
agent-view drag --from 42 --to 88 # ref → ref
|
|
397
401
|
agent-view drag --from-pos 86,792 --to-pos 640,200 # coord → coord (canvas, custom DnD)
|
|
398
402
|
agent-view drag --from 42 --to-pos 640,200 # mixed
|
|
399
403
|
agent-view drag --from 5 --to 9 --steps 20 --hold-ms 150
|
|
404
|
+
agent-view drag --from-pos 385,303 --to-pos 1250,589 --cancel # HTML5: dragCancel instead of drop (Esc mid-drag)
|
|
405
|
+
agent-view drag --from-pos 385,303 --to-pos 1250,589 --html5 # fail instead of falling back to pointer
|
|
406
|
+
agent-view drag --from-pos 385,303 --to-pos 1250,589 --pointer # never intercept
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
The output names the path and, for HTML5, the intercepted MIME types and data — check that the payload is the app's (`application/json`, a custom type), not a text selection:
|
|
410
|
+
|
|
411
|
+
```
|
|
412
|
+
Dragged (385, 303) → (1250, 589) via html5 drop (dragOperationsMask=3)
|
|
413
|
+
text/plain: "1:hr:4100@UInt16"
|
|
414
|
+
application/json: "[{\"id\":\"1:hr:4100@UInt16\",\"name\":\"Float HiLo\",...}]"
|
|
400
415
|
```
|
|
401
416
|
|
|
402
|
-
|
|
417
|
+
```
|
|
418
|
+
Dragged (485, 140) → (1250, 589) via pointer
|
|
419
|
+
warning: no HTML5 drag started (Input.dragIntercepted not fired within 1 s): the element at --from is not draggable, or a control (input/select/button) under the cursor swallowed dragstart
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Pitfalls of the HTML5 path: start on a cell without a control — an `<input>`, `<select>` or `<button>` under the cursor swallows `dragstart` even inside a `draggable` row; Chromium needs several `mouseMoved` past its drag threshold, so `--steps 0` will not start one. `--mask <n>` overrides `dragOperationsMask` on the dispatched events — diagnostics for `effectAllowed` / `dropEffect` mismatches, which the browser enforces only on a real drag.
|
|
423
|
+
|
|
424
|
+
`--steps` (default 10) controls intermediate `mouseMoved` / `dragOver` 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`.
|
|
403
425
|
|
|
404
426
|
### `upload`
|
|
405
427
|
|
|
@@ -441,18 +463,18 @@ Out of reach: `showOpenFilePicker()` (File System Access API) exposes no input t
|
|
|
441
463
|
|
|
442
464
|
### `screenshot`
|
|
443
465
|
|
|
444
|
-
Captures a screenshot, saves to temp dir, prints the file path. PNG
|
|
466
|
+
Captures a screenshot, saves to temp dir, prints the file path. Always PNG.
|
|
445
467
|
|
|
446
468
|
```bash
|
|
447
469
|
agent-view screenshot
|
|
448
|
-
agent-view screenshot --scale 0.5 # Half-res
|
|
449
|
-
agent-view screenshot --scale 0.25 # Quarter-res
|
|
470
|
+
agent-view screenshot --scale 0.5 # Half-res (~3× fewer vision tokens)
|
|
471
|
+
agent-view screenshot --scale 0.25 # Quarter-res (~12× fewer, 1 tile)
|
|
450
472
|
agent-view screenshot --crop "Sidebar" # Crop to element bounding box (~12× fewer in best case)
|
|
451
473
|
agent-view screenshot --crop "Chart" --scale 0.5 # Crop + scale (stacks)
|
|
452
474
|
agent-view screenshot --crop "Active bookings" --crop-up 1 # Crop the card, not its heading
|
|
453
475
|
```
|
|
454
476
|
|
|
455
|
-
`--scale` accepts a factor in `(0, 1]
|
|
477
|
+
`--scale` accepts a factor in `(0, 1]`; recommended for agent loops where vision tokens dominate cost. Crop and scale run inside the server on a plain full-viewport capture: a CDP `clip` resizes the app's render view until the capture completes, and on a hidden or minimised window it never completes, leaving the app drawn in a corner.
|
|
456
478
|
|
|
457
479
|
`--crop <filter>` resolves a DOM element by the same filter syntax as `dom --filter`, then crops the screenshot to its bounding box before encoding. One tile (~1.6k vision tokens) instead of twelve (~19k) in the best case. If the filter matches nothing a warning is emitted to stderr and the full window is captured instead. Combines naturally with `--scale`.
|
|
458
480
|
|
|
@@ -637,6 +659,33 @@ A node with no handlers prints `(no listeners on this node)` and exits 0. A `--f
|
|
|
637
659
|
|
|
638
660
|
`--selector` is the escape hatch for a node the AX tree never exposes — a hidden or `aria-hidden` element has no `[ref=N]` at all, so no other flag can address it. A selector matching nothing is an error: `No element matches selector "<css>"`.
|
|
639
661
|
|
|
662
|
+
### `heap`
|
|
663
|
+
|
|
664
|
+
Takes named V8 heap snapshots and compares them by class. Answers "does repeating this action leak memory, and what holds the leaked objects?" without the multi-hundred-MB `.heapsnapshot` ever reaching the agent: the server parses it and keeps only the class table and the graph.
|
|
665
|
+
|
|
666
|
+
```bash
|
|
667
|
+
agent-view heap take --name baseline # full GC, then snapshot
|
|
668
|
+
# … repeat the suspected action ~10 times with click / fill / eval …
|
|
669
|
+
agent-view heap take --name target
|
|
670
|
+
agent-view heap diff baseline target # per-class growth, largest first
|
|
671
|
+
agent-view heap diff --detached # detached DOM nodes only
|
|
672
|
+
agent-view heap retainers "Detached <div class=\"row\">" # who holds them, one hop up
|
|
673
|
+
agent-view heap summary --filter "OrderRow" # one snapshot's classes by size
|
|
674
|
+
agent-view heap list # snapshots the server holds
|
|
675
|
+
agent-view heap clear
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
```
|
|
679
|
+
baseline → target (page:Orders): +3,201 nodes, +2.4 MB, detached +240 (+960 KB)
|
|
680
|
+
class Δcount Δsize count
|
|
681
|
+
Detached <div class="row"> +240 +960 KB 252
|
|
682
|
+
OrderRowVM +10 +12 KB 10
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
A `Δcount` that is a multiple of the repetition count is the leak; `+3` is noise. `retainers` names the object and property holding the instances (`Array []`, `RowRegistry .el`), counting distinct instances and skipping weak edges. One hop only: retained sizes, dominators and full retaining paths are not computed, and for those the app's own DevTools Memory panel shows the same class names on the same CDP port.
|
|
686
|
+
|
|
687
|
+
Snapshots live in the lazy server and vanish when it idles out (5 min) or on `clear`. `--target <worker>` snapshots a worker's own heap. The full method, with a table of what each diff shape usually means, is in [`skills/verify/references/memory-leaks.md`](skills/verify/references/memory-leaks.md).
|
|
688
|
+
|
|
640
689
|
### `watch`
|
|
641
690
|
|
|
642
691
|
Polls a JS expression and streams JSON-patch (RFC 6902) diffs as it changes. Closes the "what changed between click and final state?" gap that screenshots and DOM dumps can't cover. **Requires `"allowEval": true`** (same gate as `eval`).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type PixelRect = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Crops an 8-bit RGB/RGBA PNG to `rect` (image pixels, clamped to the image)
|
|
9
|
+
* and downscales it by `scale` with an area average. Chromium can do both
|
|
10
|
+
* through `Page.captureScreenshot({ clip })`, but a clip resizes the render
|
|
11
|
+
* view for the capture and restores it only if the capture completes — a
|
|
12
|
+
* hidden window never completes it, so the app stays shrunk.
|
|
13
|
+
*/
|
|
14
|
+
export declare function cropScalePng(png: Buffer, rect: PixelRect | undefined, scale: number): Buffer;
|
|
15
|
+
//# sourceMappingURL=png.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"png.d.ts","sourceRoot":"","sources":["../../src/cdp/png.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,SAAS,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAM/E;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CA6B5F"}
|
package/dist/cdp/png.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { deflateSync, inflateSync } from 'node:zlib';
|
|
2
|
+
const SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
3
|
+
/**
|
|
4
|
+
* Crops an 8-bit RGB/RGBA PNG to `rect` (image pixels, clamped to the image)
|
|
5
|
+
* and downscales it by `scale` with an area average. Chromium can do both
|
|
6
|
+
* through `Page.captureScreenshot({ clip })`, but a clip resizes the render
|
|
7
|
+
* view for the capture and restores it only if the capture completes — a
|
|
8
|
+
* hidden window never completes it, so the app stays shrunk.
|
|
9
|
+
*/
|
|
10
|
+
export function cropScalePng(png, rect, scale) {
|
|
11
|
+
const src = decode(png);
|
|
12
|
+
const x0 = clamp(Math.round(rect?.x ?? 0), 0, src.width - 1);
|
|
13
|
+
const y0 = clamp(Math.round(rect?.y ?? 0), 0, src.height - 1);
|
|
14
|
+
const x1 = clamp(Math.round((rect?.x ?? 0) + (rect?.width ?? src.width)), x0 + 1, src.width);
|
|
15
|
+
const y1 = clamp(Math.round((rect?.y ?? 0) + (rect?.height ?? src.height)), y0 + 1, src.height);
|
|
16
|
+
const width = Math.max(1, Math.round((x1 - x0) * scale));
|
|
17
|
+
const height = Math.max(1, Math.round((y1 - y0) * scale));
|
|
18
|
+
const rgba = Buffer.alloc(width * height * 4);
|
|
19
|
+
for (let y = 0; y < height; y++) {
|
|
20
|
+
const sy0 = y0 + Math.floor((y * (y1 - y0)) / height);
|
|
21
|
+
const sy1 = Math.max(sy0 + 1, y0 + Math.floor(((y + 1) * (y1 - y0)) / height));
|
|
22
|
+
for (let x = 0; x < width; x++) {
|
|
23
|
+
const sx0 = x0 + Math.floor((x * (x1 - x0)) / width);
|
|
24
|
+
const sx1 = Math.max(sx0 + 1, x0 + Math.floor(((x + 1) * (x1 - x0)) / width));
|
|
25
|
+
const sum = [0, 0, 0, 0];
|
|
26
|
+
for (let sy = sy0; sy < sy1; sy++) {
|
|
27
|
+
for (let sx = sx0; sx < sx1; sx++) {
|
|
28
|
+
const i = (sy * src.width + sx) * 4;
|
|
29
|
+
for (let c = 0; c < 4; c++)
|
|
30
|
+
sum[c] += src.rgba[i + c];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const n = (sy1 - sy0) * (sx1 - sx0);
|
|
34
|
+
const o = (y * width + x) * 4;
|
|
35
|
+
for (let c = 0; c < 4; c++)
|
|
36
|
+
rgba[o + c] = Math.round(sum[c] / n);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return encode({ width, height, rgba });
|
|
40
|
+
}
|
|
41
|
+
function clamp(v, min, max) {
|
|
42
|
+
return Math.min(Math.max(v, min), max);
|
|
43
|
+
}
|
|
44
|
+
function decode(png) {
|
|
45
|
+
if (!png.subarray(0, 8).equals(SIGNATURE))
|
|
46
|
+
throw new Error('Not a PNG');
|
|
47
|
+
let width = 0;
|
|
48
|
+
let height = 0;
|
|
49
|
+
let channels = 0;
|
|
50
|
+
const idat = [];
|
|
51
|
+
for (let off = 8; off < png.length;) {
|
|
52
|
+
const len = png.readUInt32BE(off);
|
|
53
|
+
const type = png.toString('latin1', off + 4, off + 8);
|
|
54
|
+
const data = png.subarray(off + 8, off + 8 + len);
|
|
55
|
+
if (type === 'IHDR') {
|
|
56
|
+
width = data.readUInt32BE(0);
|
|
57
|
+
height = data.readUInt32BE(4);
|
|
58
|
+
const colorType = data[9];
|
|
59
|
+
if (data[8] !== 8 || (colorType !== 2 && colorType !== 6) || data[12] !== 0) {
|
|
60
|
+
throw new Error(`Unsupported PNG: bit depth ${data[8]}, color type ${colorType}, interlace ${data[12]}`);
|
|
61
|
+
}
|
|
62
|
+
channels = colorType === 6 ? 4 : 3;
|
|
63
|
+
}
|
|
64
|
+
else if (type === 'IDAT') {
|
|
65
|
+
idat.push(data);
|
|
66
|
+
}
|
|
67
|
+
else if (type === 'IEND') {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
off += 12 + len;
|
|
71
|
+
}
|
|
72
|
+
const raw = inflateSync(Buffer.concat(idat));
|
|
73
|
+
const stride = width * channels;
|
|
74
|
+
const pixels = Buffer.alloc(stride * height);
|
|
75
|
+
for (let y = 0; y < height; y++) {
|
|
76
|
+
const filter = raw[y * (stride + 1)];
|
|
77
|
+
const line = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
|
|
78
|
+
const row = y * stride;
|
|
79
|
+
for (let i = 0; i < stride; i++) {
|
|
80
|
+
const a = i >= channels ? pixels[row + i - channels] : 0;
|
|
81
|
+
const b = y > 0 ? pixels[row - stride + i] : 0;
|
|
82
|
+
const c = i >= channels && y > 0 ? pixels[row - stride + i - channels] : 0;
|
|
83
|
+
pixels[row + i] = (line[i] + unfilter(filter, a, b, c)) & 0xff;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (channels === 4)
|
|
87
|
+
return { width, height, rgba: pixels };
|
|
88
|
+
const rgba = Buffer.alloc(width * height * 4, 0xff);
|
|
89
|
+
for (let p = 0; p < width * height; p++)
|
|
90
|
+
pixels.copy(rgba, p * 4, p * 3, p * 3 + 3);
|
|
91
|
+
return { width, height, rgba };
|
|
92
|
+
}
|
|
93
|
+
function unfilter(filter, a, b, c) {
|
|
94
|
+
switch (filter) {
|
|
95
|
+
case 0: return 0;
|
|
96
|
+
case 1: return a;
|
|
97
|
+
case 2: return b;
|
|
98
|
+
case 3: return (a + b) >> 1;
|
|
99
|
+
case 4: {
|
|
100
|
+
const p = a + b - c;
|
|
101
|
+
const pa = Math.abs(p - a);
|
|
102
|
+
const pb = Math.abs(p - b);
|
|
103
|
+
const pc = Math.abs(p - c);
|
|
104
|
+
return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
|
|
105
|
+
}
|
|
106
|
+
default: throw new Error(`Unknown PNG filter ${filter}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function encode({ width, height, rgba }) {
|
|
110
|
+
const stride = width * 4;
|
|
111
|
+
const raw = Buffer.alloc((stride + 1) * height);
|
|
112
|
+
for (let y = 0; y < height; y++) {
|
|
113
|
+
// Filter 1 (Sub) — flat UI areas become runs of zeros and deflate well.
|
|
114
|
+
raw[y * (stride + 1)] = 1;
|
|
115
|
+
for (let i = 0; i < stride; i++) {
|
|
116
|
+
const left = i >= 4 ? rgba[y * stride + i - 4] : 0;
|
|
117
|
+
raw[y * (stride + 1) + 1 + i] = (rgba[y * stride + i] - left) & 0xff;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const ihdr = Buffer.alloc(13);
|
|
121
|
+
ihdr.writeUInt32BE(width, 0);
|
|
122
|
+
ihdr.writeUInt32BE(height, 4);
|
|
123
|
+
ihdr[8] = 8;
|
|
124
|
+
ihdr[9] = 6;
|
|
125
|
+
return Buffer.concat([SIGNATURE, chunk('IHDR', ihdr), chunk('IDAT', deflateSync(raw)), chunk('IEND', Buffer.alloc(0))]);
|
|
126
|
+
}
|
|
127
|
+
function chunk(type, data) {
|
|
128
|
+
const head = Buffer.alloc(8);
|
|
129
|
+
head.writeUInt32BE(data.length, 0);
|
|
130
|
+
head.write(type, 4, 'latin1');
|
|
131
|
+
const crc = Buffer.alloc(4);
|
|
132
|
+
crc.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data])), 0);
|
|
133
|
+
return Buffer.concat([head, data, crc]);
|
|
134
|
+
}
|
|
135
|
+
const CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
|
|
136
|
+
let c = n;
|
|
137
|
+
for (let k = 0; k < 8; k++)
|
|
138
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
139
|
+
return c >>> 0;
|
|
140
|
+
});
|
|
141
|
+
function crc32(buf) {
|
|
142
|
+
let c = 0xffffffff;
|
|
143
|
+
for (const byte of buf)
|
|
144
|
+
c = CRC_TABLE[(c ^ byte) & 0xff] ^ (c >>> 8);
|
|
145
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=png.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"png.js","sourceRoot":"","sources":["../../src/cdp/png.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,EAaL,KAAK,SAAS,
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/cdp/transport.ts"],"names":[],"mappings":"AAEA,OAAO,EAaL,KAAK,SAAS,EAKd,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAgBjB,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AA6BhD,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;AAgzBD,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC,CA+BhG;AAMD,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,WAAW,CAAC,CAoWtB"}
|
package/dist/cdp/transport.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-expect-error no types available for chrome-remote-interface
|
|
2
2
|
import CDP from 'chrome-remote-interface';
|
|
3
3
|
import { TargetType, ConsoleLevel, ConsoleSource, EvaluationError, MouseButton, NetworkEventKind, NetworkResourceType, JsDialogType, FileChooserMode, } from './types.js';
|
|
4
|
+
import { cropScalePng } from './png.js';
|
|
4
5
|
// CDP hosts to try: IPv4 first, then IPv6 (WebView2/Tauri often listens on ::1)
|
|
5
6
|
const CDP_HOSTS = ['127.0.0.1', '::1'];
|
|
6
7
|
// Maps targetId → host for connection routing
|
|
@@ -665,6 +666,19 @@ function attachCoverage(client) {
|
|
|
665
666
|
},
|
|
666
667
|
};
|
|
667
668
|
}
|
|
669
|
+
/** Chunks arrive before the command resolves, so subscribing first and joining after is the whole protocol. */
|
|
670
|
+
async function takeHeapSnapshotImpl(client) {
|
|
671
|
+
const HeapProfiler = client.HeapProfiler;
|
|
672
|
+
const chunks = [];
|
|
673
|
+
const unsubscribe = HeapProfiler.addHeapSnapshotChunk(({ chunk }) => { chunks.push(chunk); });
|
|
674
|
+
try {
|
|
675
|
+
await HeapProfiler.takeHeapSnapshot({ reportProgress: false, captureNumericValue: false });
|
|
676
|
+
}
|
|
677
|
+
finally {
|
|
678
|
+
unsubscribe();
|
|
679
|
+
}
|
|
680
|
+
return chunks.join('');
|
|
681
|
+
}
|
|
668
682
|
/** Bounds the wait for the `scriptParsed` replay when an id is still missing. */
|
|
669
683
|
const SCRIPT_PARSED_DEADLINE_MS = 1_000;
|
|
670
684
|
const SCRIPT_PARSED_POLL_MS = 20;
|
|
@@ -733,11 +747,14 @@ export async function connectToRuntime(port, target) {
|
|
|
733
747
|
getResponseBody: (requestId) => networkSub.getResponseBody(requestId),
|
|
734
748
|
startCoverage: () => coverage.start(),
|
|
735
749
|
takeCoverage: () => coverage.take(),
|
|
750
|
+
takeHeapSnapshot: () => takeHeapSnapshotImpl(client),
|
|
736
751
|
async close() {
|
|
737
752
|
await client.close();
|
|
738
753
|
},
|
|
739
754
|
};
|
|
740
755
|
}
|
|
756
|
+
const DRAG_INTERCEPT_TIMEOUT_MS = 1000;
|
|
757
|
+
const DRAG_NOT_INTERCEPTED = 'no HTML5 drag started (Input.dragIntercepted not fired within 1 s): the element at --from is not draggable, or a control (input/select/button) under the cursor swallowed dragstart';
|
|
741
758
|
export async function connectToPage(port, target, cache) {
|
|
742
759
|
if (target.type !== TargetType.Page && target.type !== TargetType.Iframe) {
|
|
743
760
|
throw new Error(`connectToPage requires a page/iframe target, got: ${target.type}`);
|
|
@@ -777,8 +794,6 @@ export async function connectToPage(port, target, cache) {
|
|
|
777
794
|
});
|
|
778
795
|
// null = not yet tested; true = available; false = unavailable (API not supported)
|
|
779
796
|
let queryAXTreeAvailable = null;
|
|
780
|
-
// null = not yet tested; true = webp supported; false = not supported (old Chrome/Electron)
|
|
781
|
-
let webpSupported = null;
|
|
782
797
|
Page.frameNavigated(async () => {
|
|
783
798
|
cache.invalidate(cacheKey);
|
|
784
799
|
fileChooserSub.forget();
|
|
@@ -802,21 +817,64 @@ export async function connectToPage(port, target, cache) {
|
|
|
802
817
|
await Input.dispatchMouseEvent({ type: 'mouseReleased', x, y, button, clickCount: i });
|
|
803
818
|
}
|
|
804
819
|
}
|
|
820
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
805
821
|
async function dispatchDrag(from, to, opts) {
|
|
806
822
|
const steps = Math.max(0, opts?.steps ?? 10);
|
|
807
823
|
const button = opts?.button ?? MouseButton.Left;
|
|
808
824
|
const holdMs = Math.max(0, opts?.holdMs ?? 0);
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
await new Promise(r => setTimeout(r, holdMs));
|
|
812
|
-
for (let i = 1; i <= steps; i++) {
|
|
825
|
+
const mode = opts?.mode ?? 'auto';
|
|
826
|
+
const at = (i) => {
|
|
813
827
|
const t = i / (steps + 1);
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
828
|
+
return { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t };
|
|
829
|
+
};
|
|
830
|
+
// With interception on, Chromium reports a starting HTML5 drag as `Input.dragIntercepted`
|
|
831
|
+
// instead of showing the native drag image; after that only `dispatchDragEvent` reaches the page.
|
|
832
|
+
let intercepted;
|
|
833
|
+
const unsub = mode === 'pointer' ? undefined : Input.dragIntercepted(({ data }) => { intercepted = data; });
|
|
834
|
+
if (unsub)
|
|
835
|
+
await Input.setInterceptDrags({ enabled: true });
|
|
836
|
+
try {
|
|
837
|
+
await Input.dispatchMouseEvent({ type: 'mouseMoved', x: from.x, y: from.y });
|
|
838
|
+
await Input.dispatchMouseEvent({ type: 'mousePressed', x: from.x, y: from.y, button, clickCount: 1 });
|
|
839
|
+
if (holdMs > 0)
|
|
840
|
+
await sleep(holdMs);
|
|
841
|
+
// Chromium starts an HTML5 drag only after several mouseMoved past its threshold; the
|
|
842
|
+
// pause between them is what lets the renderer process each one.
|
|
843
|
+
let i = 1;
|
|
844
|
+
for (; i <= steps + 1 && !intercepted; i++) {
|
|
845
|
+
const p = at(i);
|
|
846
|
+
await Input.dispatchMouseEvent({ type: 'mouseMoved', x: p.x, y: p.y, button });
|
|
847
|
+
if (unsub)
|
|
848
|
+
await sleep(20);
|
|
849
|
+
}
|
|
850
|
+
if (unsub && !intercepted) {
|
|
851
|
+
const deadline = Date.now() + DRAG_INTERCEPT_TIMEOUT_MS;
|
|
852
|
+
while (!intercepted && Date.now() < deadline)
|
|
853
|
+
await sleep(50);
|
|
854
|
+
}
|
|
855
|
+
if (intercepted) {
|
|
856
|
+
const data = opts?.mask === undefined ? intercepted : { ...intercepted, dragOperationsMask: opts.mask };
|
|
857
|
+
const ended = opts?.cancel ? 'dragCancel' : 'drop';
|
|
858
|
+
await Input.dispatchDragEvent({ type: 'dragEnter', x: at(i - 1).x, y: at(i - 1).y, data });
|
|
859
|
+
for (; i <= steps + 1; i++) {
|
|
860
|
+
const p = at(i);
|
|
861
|
+
await Input.dispatchDragEvent({ type: 'dragOver', x: p.x, y: p.y, data });
|
|
862
|
+
await sleep(20);
|
|
863
|
+
}
|
|
864
|
+
await Input.dispatchDragEvent({ type: ended, x: to.x, y: to.y, data });
|
|
865
|
+
await Input.dispatchMouseEvent({ type: 'mouseReleased', x: to.x, y: to.y, button, clickCount: 1 });
|
|
866
|
+
return { path: 'html5', ended, data };
|
|
867
|
+
}
|
|
868
|
+
await Input.dispatchMouseEvent({ type: 'mouseReleased', x: to.x, y: to.y, button, clickCount: 1 });
|
|
869
|
+
if (mode === 'html5')
|
|
870
|
+
throw new Error(DRAG_NOT_INTERCEPTED);
|
|
871
|
+
return { path: 'pointer', warning: unsub ? DRAG_NOT_INTERCEPTED : undefined };
|
|
872
|
+
}
|
|
873
|
+
finally {
|
|
874
|
+
unsub?.();
|
|
875
|
+
if (unsub)
|
|
876
|
+
await Input.setInterceptDrags({ enabled: false });
|
|
817
877
|
}
|
|
818
|
-
await Input.dispatchMouseEvent({ type: 'mouseMoved', x: to.x, y: to.y, button });
|
|
819
|
-
await Input.dispatchMouseEvent({ type: 'mouseReleased', x: to.x, y: to.y, button, clickCount: 1 });
|
|
820
878
|
}
|
|
821
879
|
async function scrollNodeIntoView(backendNodeId) {
|
|
822
880
|
const { object } = await DOM.resolveNode({ backendNodeId });
|
|
@@ -892,6 +950,7 @@ export async function connectToPage(port, target, cache) {
|
|
|
892
950
|
getResponseBody: (requestId) => networkSub.getResponseBody(requestId),
|
|
893
951
|
startCoverage: () => coverage.start(),
|
|
894
952
|
takeCoverage: () => coverage.take(),
|
|
953
|
+
takeHeapSnapshot: () => takeHeapSnapshotImpl(client),
|
|
895
954
|
async getEventListeners(backendDOMNodeId, depth = 0) {
|
|
896
955
|
const { object } = await DOM.resolveNode({ backendNodeId: backendDOMNodeId });
|
|
897
956
|
return listenersOn(object.objectId, depth);
|
|
@@ -953,38 +1012,18 @@ export async function connectToPage(port, target, cache) {
|
|
|
953
1012
|
}
|
|
954
1013
|
},
|
|
955
1014
|
async captureScreenshot(opts) {
|
|
1015
|
+
// Never pass `clip` to Chromium: see cropScalePng for why.
|
|
1016
|
+
const { data } = await Page.captureScreenshot({ format: 'png' });
|
|
1017
|
+
const png = Buffer.from(data, 'base64');
|
|
956
1018
|
const scale = opts?.scale ?? 1;
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
}
|
|
966
|
-
if (scale >= 1) {
|
|
967
|
-
const { data } = await Page.captureScreenshot({ format: 'png' });
|
|
968
|
-
return { buffer: Buffer.from(data, 'base64'), format: 'png' };
|
|
969
|
-
}
|
|
970
|
-
const { cssLayoutViewport } = await Page.getLayoutMetrics();
|
|
971
|
-
const clip = { x: 0, y: 0, width: cssLayoutViewport.clientWidth, height: cssLayoutViewport.clientHeight, scale };
|
|
972
|
-
if (webpSupported !== false) {
|
|
973
|
-
try {
|
|
974
|
-
const { data } = await Page.captureScreenshot({ format: 'webp', quality: 80, clip });
|
|
975
|
-
webpSupported = true;
|
|
976
|
-
return { buffer: Buffer.from(data, 'base64'), format: 'webp' };
|
|
977
|
-
}
|
|
978
|
-
catch {
|
|
979
|
-
if (webpSupported === null) {
|
|
980
|
-
// eslint-disable-next-line no-console
|
|
981
|
-
console.error('[agent-view] WebP not supported by this Chrome/Electron version, falling back to JPEG');
|
|
982
|
-
webpSupported = false;
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
const { data } = await Page.captureScreenshot({ format: 'jpeg', quality: 80, clip });
|
|
987
|
-
return { buffer: Buffer.from(data, 'base64'), format: 'jpeg' };
|
|
1019
|
+
if (opts?.clip === undefined && scale >= 1)
|
|
1020
|
+
return { buffer: png, format: 'png' };
|
|
1021
|
+
// The capture is in device pixels, clip rects are in CSS pixels.
|
|
1022
|
+
const { layoutViewport, cssLayoutViewport } = await Page.getLayoutMetrics();
|
|
1023
|
+
const dpr = layoutViewport.clientWidth / cssLayoutViewport.clientWidth;
|
|
1024
|
+
const clip = opts?.clip;
|
|
1025
|
+
const rect = clip && { x: clip.x * dpr, y: clip.y * dpr, width: clip.width * dpr, height: clip.height * dpr };
|
|
1026
|
+
return { buffer: cropScalePng(png, rect, Math.min(scale, 1)), format: 'png' };
|
|
988
1027
|
},
|
|
989
1028
|
async clickByNodeId(backendNodeId, opts) {
|
|
990
1029
|
const { x, y } = await resolveBoxCenter(backendNodeId, true);
|
|
@@ -1002,7 +1041,7 @@ export async function connectToPage(port, target, cache) {
|
|
|
1002
1041
|
return resolveBoxRect(nodeId, opts?.scrollIntoView ?? true);
|
|
1003
1042
|
},
|
|
1004
1043
|
async dragBetweenPositions(from, to, opts) {
|
|
1005
|
-
|
|
1044
|
+
return dispatchDrag(from, to, opts);
|
|
1006
1045
|
},
|
|
1007
1046
|
async uploadByNodeId(backendNodeId, files) {
|
|
1008
1047
|
await DOM.setFileInputFiles({ backendNodeId, files });
|