@restregular/dsh-visual-browser 0.1.0 → 0.1.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/README.md +129 -124
- package/lib/browser.js +94 -12
- package/lib/cdp.js +208 -192
- package/package.json +4 -3
- package/test/format.test.mjs +62 -0
- package/test/package.test.mjs +56 -0
- package/test/routes.test.mjs +207 -0
- package/test/scripts.test.mjs +214 -0
package/README.md
CHANGED
|
@@ -1,124 +1,129 @@
|
|
|
1
|
-
# dsh-visual-browser
|
|
2
|
-
|
|
3
|
-
A DeepSeek Harness plugin that gives the Agent a **real Chrome** and gives you a **window into it**.
|
|
4
|
-
|
|
5
|
-
- The Agent never reads the DOM. It reads a numbered list of *visible interactive
|
|
6
|
-
elements* plus a short text excerpt — a few hundred tokens per page instead of a
|
|
7
|
-
full snapshot.
|
|
8
|
-
- Every action the Agent takes (click, type, key, scroll, navigation) is mirrored
|
|
9
|
-
into a **面板 (panel)** with the page frame, an operation timeline you can replay,
|
|
10
|
-
and a picture you can click to take over the browser yourself.
|
|
11
|
-
- Cookies persist across restarts (one dedicated Chrome profile), so a login the
|
|
12
|
-
Agent performs once keeps working.
|
|
13
|
-
|
|
14
|
-
## Install
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
# The plugin is a normal DSH bundle; install it from the registry
|
|
18
|
-
dsh plugin add @restregular/dsh-visual-browser
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Or from the Harness UI: **Plugins → Add → `@restregular/dsh-visual-browser`**.
|
|
22
|
-
|
|
23
|
-
Requirements: Google Chrome, Microsoft Edge, or Chromium on the machine
|
|
24
|
-
(auto-detected), and Node.js 22 or newer (the plugin ships no runtime
|
|
25
|
-
dependencies; it talks CDP over the built-in WebSocket).
|
|
26
|
-
|
|
27
|
-
## Use
|
|
28
|
-
|
|
29
|
-
1. Click the **浏览器 / Browser** icon in the sidebar to open the panel.
|
|
30
|
-
2. Ask the Agent something that needs a website, for example
|
|
31
|
-
*"打开 example.com 看看首页写了什么"*. The Agent calls `browser_open`.
|
|
32
|
-
3. Watch the frame update. Click the picture to click the page; wheel over it to
|
|
33
|
-
scroll; use the toolbar for back / forward / reload / live view / stop.
|
|
34
|
-
|
|
35
|
-
## Tools the Agent gets
|
|
36
|
-
|
|
37
|
-
| Tool | Purpose |
|
|
38
|
-
| --- | --- |
|
|
39
|
-
| `browser_open` | Open a URL (launches Chrome on first use) and return the numbered view. |
|
|
40
|
-
| `browser_see` | Fresh numbered view of the current page. |
|
|
41
|
-
| `browser_act` | `click` / `type` / `key` / `hover` / `scroll` / `select` / `back` / `forward` / `reload` / `wait` / `close`, then return the new view. |
|
|
42
|
-
| `browser_text` | Read text of the page or of one element (a link also returns its `href`). |
|
|
43
|
-
| `browser_screenshot` | Save a PNG/JPEG to disk and return the path (feed it to `read_image`). |
|
|
44
|
-
|
|
45
|
-
### What the Agent sees
|
|
46
|
-
|
|
47
|
-
```
|
|
48
|
-
URL: https://example.com/login
|
|
49
|
-
TITLE: 登录 - Example
|
|
50
|
-
VIEW: 1280x800 scroll 0/2400 · 4/6 interactive
|
|
51
|
-
[1] textbox "手机号" value=13800000000
|
|
52
|
-
[2] textbox "密码" value=••••••
|
|
53
|
-
[3] button "登录"
|
|
54
|
-
[4] link "忘记密码" → /forgot
|
|
55
|
-
HEADINGS: 用户登录 | 没有账号?
|
|
56
|
-
TEXT: 用户登录 手机号 密码 登录 忘记密码
|
|
57
|
-
ACTIVE: [1] textbox "手机号"
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Numbers are reassigned on every view, so an action always uses the newest
|
|
61
|
-
numbers; a stale one fails with a clear "take a fresh snapshot" message instead
|
|
62
|
-
of clicking the wrong element.
|
|
63
|
-
|
|
64
|
-
## Configuration
|
|
65
|
-
|
|
66
|
-
Row config (in the profile's `cordis.patch.yml`, override by row id
|
|
67
|
-
`dsh-visual-browser`):
|
|
68
|
-
|
|
69
|
-
```yaml
|
|
70
|
-
- id: dsh-visual-browser
|
|
71
|
-
config:
|
|
72
|
-
headless: true # false = visible Chrome window
|
|
73
|
-
executablePath: '' # explicit chrome.exe path
|
|
74
|
-
userDataDir: '' # default: ~/.dsh/visual-browser/chrome-profile
|
|
75
|
-
viewport:
|
|
76
|
-
width: 1280
|
|
77
|
-
height: 800
|
|
78
|
-
maxElements: 100 # cap on the numbered list
|
|
79
|
-
maxTextChars: 320 # cap on the text excerpt
|
|
80
|
-
frameQuality: 62 # JPEG quality of panel frames
|
|
81
|
-
timelineFrames: 24 # replayable frames kept in memory
|
|
82
|
-
allowUserControl: true # allow clicks from the panel
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
1
|
+
# dsh-visual-browser
|
|
2
|
+
|
|
3
|
+
A DeepSeek Harness plugin that gives the Agent a **real Chrome** and gives you a **window into it**.
|
|
4
|
+
|
|
5
|
+
- The Agent never reads the DOM. It reads a numbered list of *visible interactive
|
|
6
|
+
elements* plus a short text excerpt — a few hundred tokens per page instead of a
|
|
7
|
+
full snapshot.
|
|
8
|
+
- Every action the Agent takes (click, type, key, scroll, navigation) is mirrored
|
|
9
|
+
into a **面板 (panel)** with the page frame, an operation timeline you can replay,
|
|
10
|
+
and a picture you can click to take over the browser yourself.
|
|
11
|
+
- Cookies persist across restarts (one dedicated Chrome profile), so a login the
|
|
12
|
+
Agent performs once keeps working.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# The plugin is a normal DSH bundle; install it from the registry
|
|
18
|
+
dsh plugin add @restregular/dsh-visual-browser
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or from the Harness UI: **Plugins → Add → `@restregular/dsh-visual-browser`**.
|
|
22
|
+
|
|
23
|
+
Requirements: Google Chrome, Microsoft Edge, or Chromium on the machine
|
|
24
|
+
(auto-detected), and Node.js 22 or newer (the plugin ships no runtime
|
|
25
|
+
dependencies; it talks CDP over the built-in WebSocket).
|
|
26
|
+
|
|
27
|
+
## Use
|
|
28
|
+
|
|
29
|
+
1. Click the **浏览器 / Browser** icon in the sidebar to open the panel.
|
|
30
|
+
2. Ask the Agent something that needs a website, for example
|
|
31
|
+
*"打开 example.com 看看首页写了什么"*. The Agent calls `browser_open`.
|
|
32
|
+
3. Watch the frame update. Click the picture to click the page; wheel over it to
|
|
33
|
+
scroll; use the toolbar for back / forward / reload / live view / stop.
|
|
34
|
+
|
|
35
|
+
## Tools the Agent gets
|
|
36
|
+
|
|
37
|
+
| Tool | Purpose |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `browser_open` | Open a URL (launches Chrome on first use) and return the numbered view. |
|
|
40
|
+
| `browser_see` | Fresh numbered view of the current page. |
|
|
41
|
+
| `browser_act` | `click` / `type` / `key` / `hover` / `scroll` / `select` / `back` / `forward` / `reload` / `wait` / `close`, then return the new view. |
|
|
42
|
+
| `browser_text` | Read text of the page or of one element (a link also returns its `href`). |
|
|
43
|
+
| `browser_screenshot` | Save a PNG/JPEG to disk and return the path (feed it to `read_image`). |
|
|
44
|
+
|
|
45
|
+
### What the Agent sees
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
URL: https://example.com/login
|
|
49
|
+
TITLE: 登录 - Example
|
|
50
|
+
VIEW: 1280x800 scroll 0/2400 · 4/6 interactive
|
|
51
|
+
[1] textbox "手机号" value=13800000000
|
|
52
|
+
[2] textbox "密码" value=••••••
|
|
53
|
+
[3] button "登录"
|
|
54
|
+
[4] link "忘记密码" → /forgot
|
|
55
|
+
HEADINGS: 用户登录 | 没有账号?
|
|
56
|
+
TEXT: 用户登录 手机号 密码 登录 忘记密码
|
|
57
|
+
ACTIVE: [1] textbox "手机号"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Numbers are reassigned on every view, so an action always uses the newest
|
|
61
|
+
numbers; a stale one fails with a clear "take a fresh snapshot" message instead
|
|
62
|
+
of clicking the wrong element.
|
|
63
|
+
|
|
64
|
+
## Configuration
|
|
65
|
+
|
|
66
|
+
Row config (in the profile's `cordis.patch.yml`, override by row id
|
|
67
|
+
`dsh-visual-browser`):
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
- id: dsh-visual-browser
|
|
71
|
+
config:
|
|
72
|
+
headless: true # false = visible Chrome window
|
|
73
|
+
executablePath: '' # explicit chrome.exe path
|
|
74
|
+
userDataDir: '' # default: ~/.dsh/visual-browser/chrome-profile
|
|
75
|
+
viewport:
|
|
76
|
+
width: 1280
|
|
77
|
+
height: 800
|
|
78
|
+
maxElements: 100 # cap on the numbered list
|
|
79
|
+
maxTextChars: 320 # cap on the text excerpt
|
|
80
|
+
frameQuality: 62 # JPEG quality of panel frames
|
|
81
|
+
timelineFrames: 24 # replayable frames kept in memory
|
|
82
|
+
allowUserControl: true # allow clicks from the panel
|
|
83
|
+
reuseExisting: true # adopt a Chrome that already owns the profile
|
|
84
|
+
actionTimeoutMs: 12000
|
|
85
|
+
navigationTimeoutMs: 30000
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## How it works
|
|
89
|
+
|
|
90
|
+
- **Host** (`index.js`, `lib/`): launches Chrome with
|
|
91
|
+
`--remote-debugging-port=0 --user-data-dir=<profile>`, parses the DevTools
|
|
92
|
+
endpoint from stderr, attaches one flattened CDP session, and exposes:
|
|
93
|
+
- five tools through the `tools` service (registration is effect-scoped),
|
|
94
|
+
- a same-origin prefix route `/visual-browser/api` (`/state`, `/frame`,
|
|
95
|
+
`/live`, `/start`, `/stop`, `/capture`, `/click`, `/wheel`, `/key`, `/nav`)
|
|
96
|
+
that the panel polls and posts to.
|
|
97
|
+
- **Snapshot** (`lib/page-scripts.js`): one serialized function runs in the page,
|
|
98
|
+
filters interactive elements to the viewport, assigns `data-dsh-vb-ref`
|
|
99
|
+
numbers, and returns structured JSON. Nothing else about the DOM leaves the
|
|
100
|
+
page.
|
|
101
|
+
- **Frames** stay on the host: the panel displays the newest JPEG by sequence
|
|
102
|
+
number, so a live picture costs no session tokens. The timeline holds the last
|
|
103
|
+
N frames; clicking a row replays that moment.
|
|
104
|
+
- **One code path**: panel clicks, wheel events and keys run the same
|
|
105
|
+
`BrowserManager` methods the tools use.
|
|
106
|
+
- **Survives a host restart**: Chrome writes `DevToolsActivePort` into its
|
|
107
|
+
profile, so a restarted host (or one whose previous host crashed with Chrome
|
|
108
|
+
still alive) attaches to that browser instead of failing on the profile lock.
|
|
109
|
+
An adopted browser is only detached — its process is not killed.
|
|
110
|
+
|
|
111
|
+
Chrome is killed with its process tree when the plugin unloads.
|
|
112
|
+
|
|
113
|
+
## Notes and limits
|
|
114
|
+
|
|
115
|
+
- Cross-origin iframes are counted but not inspected; open the direct URL when a
|
|
116
|
+
flow lives inside a frame.
|
|
117
|
+
- Blocking JS dialogs are auto-accepted and logged in the timeline.
|
|
118
|
+
- The panel route is served by the Harness web server on its existing origin;
|
|
119
|
+
it performs no extra authentication, matching the other local panel routes.
|
|
120
|
+
- Headless mode is the default. Some sites behave differently headless; set
|
|
121
|
+
`headless: false` when a site insists on a window.
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm test # syntax checks + snapshot/format/route/package smoke tests
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
MIT © RestRegular
|
package/lib/browser.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
10
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
11
|
-
import { writeFile } from 'node:fs/promises';
|
|
11
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
12
12
|
import { homedir, tmpdir } from 'node:os';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
|
|
@@ -45,6 +45,7 @@ export const DEFAULT_CONFIG = {
|
|
|
45
45
|
frameQuality: 62,
|
|
46
46
|
timelineFrames: 24,
|
|
47
47
|
allowUserControl: true,
|
|
48
|
+
reuseExisting: true,
|
|
48
49
|
actionTimeoutMs: 12_000,
|
|
49
50
|
navigationTimeoutMs: 30_000,
|
|
50
51
|
startupTimeoutMs: 30_000,
|
|
@@ -73,6 +74,7 @@ export function normalizeConfig(input = {}) {
|
|
|
73
74
|
frameQuality: clamp(source.frameQuality, 20, 95, DEFAULT_CONFIG.frameQuality),
|
|
74
75
|
timelineFrames: clamp(source.timelineFrames, 4, 120, DEFAULT_CONFIG.timelineFrames),
|
|
75
76
|
allowUserControl: source.allowUserControl === undefined ? DEFAULT_CONFIG.allowUserControl : source.allowUserControl !== false,
|
|
77
|
+
reuseExisting: source.reuseExisting === undefined ? DEFAULT_CONFIG.reuseExisting : source.reuseExisting !== false,
|
|
76
78
|
actionTimeoutMs: clamp(source.actionTimeoutMs, 500, 60_000, DEFAULT_CONFIG.actionTimeoutMs),
|
|
77
79
|
navigationTimeoutMs: clamp(source.navigationTimeoutMs, 1000, 120_000, DEFAULT_CONFIG.navigationTimeoutMs),
|
|
78
80
|
startupTimeoutMs: clamp(source.startupTimeoutMs, 2000, 120_000, DEFAULT_CONFIG.startupTimeoutMs),
|
|
@@ -213,6 +215,7 @@ export class BrowserManager {
|
|
|
213
215
|
#targetId = null;
|
|
214
216
|
#starting = null;
|
|
215
217
|
#running = false;
|
|
218
|
+
#owned = false;
|
|
216
219
|
#executable = '';
|
|
217
220
|
#url = 'about:blank';
|
|
218
221
|
#title = '';
|
|
@@ -281,6 +284,22 @@ export class BrowserManager {
|
|
|
281
284
|
} catch (error) {
|
|
282
285
|
throw new BrowserError('PROFILE_DIR_FAILED', `Could not create the browser profile directory ${userDataDir}: ${error.message}`);
|
|
283
286
|
}
|
|
287
|
+
if (this.#config.reuseExisting) {
|
|
288
|
+
const reused = await this.#reuseProfile(userDataDir).catch((error) => {
|
|
289
|
+
this.#logger?.info?.(`[visual-browser] reuse probe failed: ${error?.message ?? error}`);
|
|
290
|
+
return false;
|
|
291
|
+
});
|
|
292
|
+
if (reused) {
|
|
293
|
+
this.#owned = false;
|
|
294
|
+
await this.#attach();
|
|
295
|
+
this.#running = true;
|
|
296
|
+
this.#lastError = '';
|
|
297
|
+
await this.#capture({ force: true }).catch(() => null);
|
|
298
|
+
this.#record('start', userDataDir, 'attached to a running Chrome');
|
|
299
|
+
this.#logger?.info?.('[visual-browser] attached to the Chrome that already owns this profile');
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
284
303
|
const args = [
|
|
285
304
|
'--remote-debugging-port=0',
|
|
286
305
|
`--user-data-dir=${userDataDir}`,
|
|
@@ -326,7 +345,11 @@ export class BrowserManager {
|
|
|
326
345
|
if (match) finish(null, match[1]);
|
|
327
346
|
};
|
|
328
347
|
const onExit = (code) => {
|
|
329
|
-
|
|
348
|
+
const hint =
|
|
349
|
+
stderr.trim() === '' && code === 0
|
|
350
|
+
? 'Chrome handed the launch to a browser that already owns this profile, but no DevTools endpoint answered. Close the leftover browser (the panel Stop button, or Task Manager) or set a different `userDataDir`.'
|
|
351
|
+
: stderr.slice(-600);
|
|
352
|
+
finish(new BrowserError('BROWSER_EXITED', `Chrome exited with code ${code} before it was ready. ${hint}`));
|
|
330
353
|
};
|
|
331
354
|
const onError = (error) => {
|
|
332
355
|
finish(new BrowserError('SPAWN_FAILED', `Could not start ${executable}: ${error.message}`));
|
|
@@ -345,6 +368,7 @@ export class BrowserManager {
|
|
|
345
368
|
});
|
|
346
369
|
|
|
347
370
|
const cdp = await CdpConnection.open(endpoint);
|
|
371
|
+
this.#owned = true;
|
|
348
372
|
this.#cdp = cdp;
|
|
349
373
|
child.once('exit', () => {
|
|
350
374
|
if (this.#proc === child) this.#proc = null;
|
|
@@ -356,8 +380,49 @@ export class BrowserManager {
|
|
|
356
380
|
await this.#attach();
|
|
357
381
|
this.#running = true;
|
|
358
382
|
this.#lastError = '';
|
|
359
|
-
this.#record('start', '', `${this.#config.headless ? 'headless' : 'windowed'} Chrome started`);
|
|
360
383
|
await this.#capture({ force: true }).catch(() => null);
|
|
384
|
+
this.#record('start', '', `${this.#config.headless ? 'headless' : 'windowed'} Chrome started`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Chrome writes `<userDataDir>/DevToolsActivePort` when it starts with remote
|
|
389
|
+
* debugging. A host restart, or a crashed host whose Chrome survived, can
|
|
390
|
+
* therefore adopt the running browser instead of failing on the profile lock.
|
|
391
|
+
*/
|
|
392
|
+
async #reuseProfile(userDataDir) {
|
|
393
|
+
const portFile = path.join(userDataDir, 'DevToolsActivePort');
|
|
394
|
+
let text = '';
|
|
395
|
+
try {
|
|
396
|
+
text = await readFile(portFile, 'utf8');
|
|
397
|
+
} catch {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
const lines = text.split(/\r?\n/).map((line) => line.trim());
|
|
401
|
+
const port = Number(lines[0]);
|
|
402
|
+
const wsPath = lines[1] ?? '';
|
|
403
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) return false;
|
|
404
|
+
if (!wsPath.startsWith('/devtools/browser/')) return false;
|
|
405
|
+
let cdp = null;
|
|
406
|
+
try {
|
|
407
|
+
cdp = await CdpConnection.open(`ws://127.0.0.1:${port}${wsPath}`, { timeoutMs: 3000 });
|
|
408
|
+
await cdp.send('Browser.getVersion');
|
|
409
|
+
} catch {
|
|
410
|
+
try {
|
|
411
|
+
cdp?.close();
|
|
412
|
+
} catch {
|
|
413
|
+
/* already closed */
|
|
414
|
+
}
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
this.#cdp = cdp;
|
|
418
|
+
cdp.onClose(() => {
|
|
419
|
+
if (this.#cdp === cdp) {
|
|
420
|
+
this.#running = false;
|
|
421
|
+
this.#sessionId = null;
|
|
422
|
+
this.#targetId = null;
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
return true;
|
|
361
426
|
}
|
|
362
427
|
|
|
363
428
|
async #attach() {
|
|
@@ -398,17 +463,32 @@ export class BrowserManager {
|
|
|
398
463
|
});
|
|
399
464
|
}
|
|
400
465
|
|
|
401
|
-
/** Shut Chrome down
|
|
466
|
+
/** Shut Chrome down (or just detach when this host did not spawn it). */
|
|
402
467
|
async close() {
|
|
403
468
|
const proc = this.#proc;
|
|
469
|
+
const owned = this.#owned;
|
|
470
|
+
const cdp = this.#cdp;
|
|
471
|
+
const targetId = this.#targetId;
|
|
404
472
|
this.#proc = null;
|
|
473
|
+
this.#owned = false;
|
|
405
474
|
this.#running = false;
|
|
406
475
|
this.#sessionId = null;
|
|
407
476
|
this.#targetId = null;
|
|
408
|
-
const cdp = this.#cdp;
|
|
409
477
|
this.#cdp = null;
|
|
410
|
-
|
|
411
|
-
|
|
478
|
+
if (owned) {
|
|
479
|
+
cdp?.close();
|
|
480
|
+
} else if (cdp !== null && targetId !== null) {
|
|
481
|
+
// Only our own tab is ours to close: another host may still use the browser.
|
|
482
|
+
try {
|
|
483
|
+
await Promise.race([cdp.send('Target.closeTarget', { targetId }), sleep(1000)]);
|
|
484
|
+
} catch {
|
|
485
|
+
/* the browser may already be gone */
|
|
486
|
+
}
|
|
487
|
+
cdp.close();
|
|
488
|
+
} else {
|
|
489
|
+
cdp?.close();
|
|
490
|
+
}
|
|
491
|
+
if (owned && proc !== null) {
|
|
412
492
|
try {
|
|
413
493
|
if (process.platform === 'win32') {
|
|
414
494
|
spawn('taskkill', ['/pid', String(proc.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
|
|
@@ -505,8 +585,8 @@ export class BrowserManager {
|
|
|
505
585
|
return this.#serial(async () => {
|
|
506
586
|
await this.#navigate(target);
|
|
507
587
|
const snapshot = await this.snapshot();
|
|
508
|
-
this.#record('open', target, snapshot.title || '');
|
|
509
588
|
await this.#capture().catch(() => null);
|
|
589
|
+
this.#record('open', target, snapshot.title || '');
|
|
510
590
|
return snapshot;
|
|
511
591
|
});
|
|
512
592
|
}
|
|
@@ -791,8 +871,8 @@ export class BrowserManager {
|
|
|
791
871
|
}
|
|
792
872
|
await this.#settleAfterAction();
|
|
793
873
|
const snapshot = await this.snapshot();
|
|
794
|
-
this.#record(action, target.ref !== undefined ? `[${target.ref}]` : (target.selector ?? ''), summary.slice(0, 160));
|
|
795
874
|
await this.#capture().catch(() => null);
|
|
875
|
+
this.#record(action, target.ref !== undefined ? `[${target.ref}]` : (target.selector ?? ''), summary.slice(0, 160));
|
|
796
876
|
return { summary, snapshot };
|
|
797
877
|
});
|
|
798
878
|
}
|
|
@@ -810,8 +890,8 @@ export class BrowserManager {
|
|
|
810
890
|
await this.#clickResolved({ x: px, y: py, name: '' });
|
|
811
891
|
await this.#settleAfterAction();
|
|
812
892
|
const snapshot = await this.snapshot();
|
|
813
|
-
this.#record('user-click', `${Math.round(x * 100)}%,${Math.round(y * 100)}%`, `you clicked ${Math.round(x * 100)}%, ${Math.round(y * 100)}%`);
|
|
814
893
|
await this.#capture().catch(() => null);
|
|
894
|
+
this.#record('user-click', `${Math.round(x * 100)}%,${Math.round(y * 100)}%`, `you clicked ${Math.round(x * 100)}%, ${Math.round(y * 100)}%`);
|
|
815
895
|
return { summary: 'clicked in the panel', snapshot };
|
|
816
896
|
});
|
|
817
897
|
}
|
|
@@ -830,8 +910,8 @@ export class BrowserManager {
|
|
|
830
910
|
pointerType: 'mouse',
|
|
831
911
|
});
|
|
832
912
|
await sleep(180);
|
|
833
|
-
this.#record('user-scroll', `${deltaY || deltaX}px`, `you scrolled ${Math.abs(deltaY || deltaX)}px`);
|
|
834
913
|
await this.#capture().catch(() => null);
|
|
914
|
+
this.#record('user-scroll', `${deltaY || deltaX}px`, `you scrolled ${Math.abs(deltaY || deltaX)}px`);
|
|
835
915
|
return { summary: 'scrolled in the panel' };
|
|
836
916
|
});
|
|
837
917
|
}
|
|
@@ -843,8 +923,8 @@ export class BrowserManager {
|
|
|
843
923
|
return this.#serial(async () => {
|
|
844
924
|
const spec = await this.#press(key);
|
|
845
925
|
await this.#settleAfterAction();
|
|
846
|
-
this.#record('user-key', spec.key, `you pressed ${spec.key}`);
|
|
847
926
|
await this.#capture().catch(() => null);
|
|
927
|
+
this.#record('user-key', spec.key, `you pressed ${spec.key}`);
|
|
848
928
|
return { summary: `pressed ${spec.key}` };
|
|
849
929
|
});
|
|
850
930
|
}
|
|
@@ -871,6 +951,7 @@ export class BrowserManager {
|
|
|
871
951
|
const filePath = requested !== '' ? path.resolve(requested) : path.join(tmpdir(), 'dsh-visual-browser', `page-${timestamp}.${format}`);
|
|
872
952
|
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
873
953
|
await writeFile(filePath, bytes);
|
|
954
|
+
await this.#capture().catch(() => null);
|
|
874
955
|
this.#record('screenshot', path.basename(filePath), `saved ${format} screenshot`);
|
|
875
956
|
return { path: filePath, bytes: bytes.length, format };
|
|
876
957
|
});
|
|
@@ -937,6 +1018,7 @@ export class BrowserManager {
|
|
|
937
1018
|
starting: this.#starting !== null,
|
|
938
1019
|
executable: this.#executable,
|
|
939
1020
|
headless: this.#config.headless,
|
|
1021
|
+
managed: this.#owned,
|
|
940
1022
|
url: this.#url,
|
|
941
1023
|
title: this.#title,
|
|
942
1024
|
viewport: { width: this.#config.viewportWidth, height: this.#config.viewportHeight },
|