dsh-builtin-browser 0.4.7 → 0.4.9
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/CHANGELOG.md +74 -0
- package/README.en.md +62 -491
- package/README.md +44 -470
- package/client.js +10 -10
- package/cordis.patch.yml +4 -4
- package/desktop-bridge/plugin-browser-bridge.js +1 -1
- package/docs/README.md +8 -4
- package/docs/architecture.md +82 -11
- package/docs/development.md +38 -0
- package/docs/limitations.md +49 -0
- package/docs/tool-reference.md +42 -42
- package/docs/user-guide.md +93 -37
- package/docs/why-browser.md +3 -3
- package/lib/browser/types.d.ts +2 -2
- package/lib/browser-electron/entry.d.ts +2 -2
- package/lib/browser-electron/entry.js +1 -1
- package/lib/browser-electron/history-store.d.ts +1 -1
- package/lib/browser-electron/history-store.js +1 -1
- package/lib/browser-electron/host-main.js +3 -3
- package/lib/browser-electron/provider.d.ts +9 -9
- package/lib/browser-electron/provider.js +14 -14
- package/lib/browser-electron/remote-host.js +1 -1
- package/lib/browser-electron/settings-store.d.ts +4 -4
- package/lib/browser-electron/system-browser.d.ts +60 -0
- package/lib/browser-electron/system-browser.js +257 -23
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/tool-browser/element-fields.d.ts +4 -4
- package/lib/tool-browser/element-fields.js +6 -6
- package/lib/tool-browser/index.d.ts +6 -6
- package/lib/tool-browser/index.js +161 -139
- package/package.json +2 -2
- package/scripts/verify-host-compat.mjs +6 -6
- package/src/browser/types.ts +2 -2
- package/src/browser-electron/entry.ts +2 -2
- package/src/browser-electron/history-store.ts +1 -1
- package/src/browser-electron/host-main.ts +3 -3
- package/src/browser-electron/provider.ts +15 -15
- package/src/browser-electron/remote-host.ts +1 -1
- package/src/browser-electron/settings-store.ts +4 -4
- package/src/browser-electron/system-browser.ts +244 -16
- package/src/index.ts +1 -1
- package/src/tool-browser/element-fields.ts +6 -6
- package/src/tool-browser/index.ts +164 -140
package/README.en.md
CHANGED
|
@@ -6,545 +6,116 @@
|
|
|
6
6
|
<img src="https://img.shields.io/badge/Platform-Windows-4493F8?style=flat-square" alt="Platform: Windows (verified)">
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
|
-
<p align="center"><a href="README.md">中文</a> · English</p>
|
|
9
|
+
<p align="center"><sub><a href="README.md">中文</a> · English</sub></p>
|
|
10
10
|
|
|
11
|
-
<h3 align="center">A <b>shared real browser</b>
|
|
11
|
+
<h3 align="center">A <b>shared real browser</b> for the DeepSeek Harness ecosystem — install and it works, human and agent on the same page</h3>
|
|
12
12
|
|
|
13
|
-
<h4 align="center">The agent drives a real, visible browser
|
|
14
|
-
|
|
15
|
-
## Documentation
|
|
16
|
-
|
|
17
|
-
| Goal | Entry |
|
|
18
|
-
| --- | --- |
|
|
19
|
-
| Why a shared real browser, and how it differs from headless approaches | [Why a shared real browser](docs/why-browser.md) |
|
|
20
|
-
| Installation, configuration, day-to-day use | [User guide](docs/user-guide.md) |
|
|
21
|
-
| All 34 tools: parameters, output, examples | [Tool reference](docs/tool-reference.md) |
|
|
22
|
-
| How the seam / provider / tools layers and self-hosting work | [Architecture](docs/architecture.md) |
|
|
23
|
-
| Documentation index and README split | [Docs index](docs/README.md) |
|
|
13
|
+
<h4 align="center">The agent drives a real, visible browser you can take over at any moment — and you are both looking at the <b>same page</b>.</h4>
|
|
24
14
|
|
|
25
15
|
## What is this
|
|
26
16
|
|
|
27
|
-
`dsh-builtin-browser`
|
|
17
|
+
`dsh-builtin-browser` gives [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) browser capability:
|
|
28
18
|
|
|
29
|
-
- **A real page, not a relay**:
|
|
30
|
-
- **
|
|
31
|
-
- **Works
|
|
32
|
-
- **One plugin, one toolset**:
|
|
19
|
+
- **A real page, not a relay**: pages are hosted by a **real browser** — on the desktop that is the official side panel, on `dsh web` it is a window this plugin launches, and you can point it at **your own installed Chrome / Edge** instead. The user watches what the agent is doing and can take over at any time;
|
|
20
|
+
- **Human and agent on the same page** (desktop): the page the agent drives is the *same* page the human sees — no more acting inside a window you cannot watch;
|
|
21
|
+
- **Works on install**: the carrier is chosen automatically (desktop side panel first, otherwise self-hosted), and plain `dsh web` needs no extra configuration;
|
|
22
|
+
- **One plugin, one toolset**: installing it gives the agent 34 `dsh_browser_*` tools (open, read, accessibility tree, wait, semantic/coordinate actions, scroll, history navigation, batch and single-control form filling, keys, structured extraction, screenshots, downloads, login-state management).
|
|
33
23
|
|
|
34
|
-
In one
|
|
24
|
+
In one line: **install the plugin and you have a real browser shared with, and drivable by, the user's agent.**
|
|
35
25
|
|
|
36
26
|
## Quick start
|
|
37
27
|
|
|
38
|
-
|
|
28
|
+
**First, which side are you on** — these are two separate installs; updating one does not update the other, and they host the browser differently.
|
|
39
29
|
|
|
40
|
-
|
|
30
|
+
### DSH Desktop
|
|
41
31
|
|
|
42
32
|
```sh
|
|
43
|
-
# install from npm
|
|
44
|
-
dsh plugin --profile web add dsh-builtin-browser
|
|
45
|
-
# or from a checkout (one plugin, one repository)
|
|
46
|
-
dsh plugin --profile web add <path-to-this-repo>
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
**Desktop (DSH Desktop)**
|
|
50
|
-
|
|
51
|
-
```sh
|
|
52
|
-
# 1. install into the desktop profile
|
|
53
33
|
dsh plugin --profile desktop add dsh-builtin-browser
|
|
54
|
-
|
|
55
|
-
# 2. desktop-only step: let the plugin drive the official sidebar's page
|
|
56
|
-
node <path-to-this-repo>/desktop-bridge/install.mjs
|
|
57
34
|
```
|
|
58
35
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
**Updating** (the two hosts differ — see [Updating (the two hosts differ)](#updating-the-two-hosts-differ))
|
|
62
|
-
|
|
63
|
-
| Host | Steps |
|
|
64
|
-
| --- | --- |
|
|
65
|
-
| Web | update the profile dependency → restart `dsh web` |
|
|
66
|
-
| Desktop | update the dependency → **re-run `node desktop-bridge/install.mjs`** → restart DSH Desktop |
|
|
67
|
-
|
|
68
|
-
After install the agent can use the browser tools, e.g.:
|
|
69
|
-
|
|
70
|
-
| What you want | Tool | Notes |
|
|
71
|
-
| --- | --- | --- |
|
|
72
|
-
| Open a page | `browser_open` | Opens a URL and returns a numbered snapshot |
|
|
73
|
-
| Understand a page | `browser_snapshot` | Numbered inventory of inputs/buttons/links to target |
|
|
74
|
-
| Operate a page | `browser_execute` | Runs JS in the page (native setters, framework-friendly) |
|
|
75
|
-
| Fill a form | `browser_fill` | Fills many fields in one call, optional submit |
|
|
76
|
-
| See the page | `browser_screenshot` | PNG capture, optionally saved for a vision model |
|
|
77
|
-
|
|
78
|
-
See the full list in [Tool reference](#tool-reference).
|
|
79
|
-
|
|
80
|
-
## Main features
|
|
81
|
-
|
|
82
|
-
<table>
|
|
83
|
-
<tr>
|
|
84
|
-
<td width="50%" valign="top">
|
|
85
|
-
<h3>Shared real browser</h3>
|
|
86
|
-
<p>A native view, not a headless screenshot. Human and agent operate the same page: the user sees every step and can take over; the agent drives the very window in front of the user.</p>
|
|
87
|
-
</td>
|
|
88
|
-
<td width="50%" valign="top">
|
|
89
|
-
<h3>DOM-level driving, framework-friendly</h3>
|
|
90
|
-
<p><code>browser_snapshot</code> returns numbered interactive elements; <code>browser_execute</code> runs JS in the page (native setters + input/change events for controlled inputs), so React/Vue pages work reliably. <b>Semantic targeting comes first</b> — the whole page stays operable without any image input; under the <b>non-visual</b> strategy a coordinate click that depends on a screenshot is refused, with an instruction to use a semantic target instead.</p>
|
|
91
|
-
</td>
|
|
92
|
-
</tr>
|
|
93
|
-
<tr>
|
|
94
|
-
<td width="50%" valign="top">
|
|
95
|
-
<h3>Multi-tab sessions</h3>
|
|
96
|
-
<p>Open URLs in parallel tabs; list/switch/close/reset tabs while each session keeps its own state. On the desktop <b>each session gets its own tab in the sidebar, and can open further tabs within it</b>, so ending one session does not disturb another.</p>
|
|
97
|
-
</td>
|
|
98
|
-
<td width="50%" valign="top">
|
|
99
|
-
<h3>Multi-format content</h3>
|
|
100
|
-
<p>Fetch pages as html / markdown / txt / json, scoped by CSS selector, capped by character limit and timeout.</p>
|
|
101
|
-
</td>
|
|
102
|
-
</tr>
|
|
103
|
-
<tr>
|
|
104
|
-
<td width="50%" valign="top">
|
|
105
|
-
<h3>Per-task session isolation</h3>
|
|
106
|
-
<p>Each DSH task (session) drives <b>its own sidebar panel and its own page</b>: which conversation the shell happens to be displaying makes no difference, so a page requested in one conversation can never turn up in another's. Concurrent tasks never fight over the page or pollute each other (cookies, history and logins are shared across the session by design).</p>
|
|
107
|
-
</td>
|
|
108
|
-
<td width="50%" valign="top">
|
|
109
|
-
<h3>Login persistence</h3>
|
|
110
|
-
<p><code>browser_auth</code> exports/restores cookies so logins survive host restarts; self-hosted cookies are also persisted to disk.</p>
|
|
111
|
-
</td>
|
|
112
|
-
</tr>
|
|
113
|
-
<tr>
|
|
114
|
-
<td width="50%" valign="top">
|
|
115
|
-
<h3>CAPTCHA / bot-detection awareness</h3>
|
|
116
|
-
<p>Detects Cloudflare, reCAPTCHA, hCaptcha, Turnstile and generic challenges (<code>browser_challenge</code>, also flagged in snapshots); instead of retrying blindly, the agent asks the human to complete it in the shared window.</p>
|
|
117
|
-
</td>
|
|
118
|
-
<td width="50%" valign="top">
|
|
119
|
-
<h3>Batch form filling</h3>
|
|
120
|
-
<p><code>browser_fill</code> fills many fields at once — matched by selector/name/label, handling controlled inputs, selects, checkboxes and radio groups, with optional submit; one failing field never aborts the rest.</p>
|
|
121
|
-
</td>
|
|
122
|
-
</tr>
|
|
123
|
-
<tr>
|
|
124
|
-
<td width="50%" valign="top">
|
|
125
|
-
<h3>Operation history & replay</h3>
|
|
126
|
-
<p><code>browser_history</code> logs operations (open/execute/click/type/fill/download/auth); <code>browser_replay</code> re-runs one step by sequence number.</p>
|
|
127
|
-
</td>
|
|
128
|
-
<td width="50%" valign="top">
|
|
129
|
-
<h3>Authenticated downloads</h3>
|
|
130
|
-
<p><code>browser_download</code> fetches a file in the page context with the session's cookies and writes it to disk — content behind login works too.</p>
|
|
131
|
-
</td>
|
|
132
|
-
</tr>
|
|
133
|
-
<tr>
|
|
134
|
-
<td width="50%" valign="top">
|
|
135
|
-
<h3>Safety restriction</h3>
|
|
136
|
-
<p><code>browser_restrict</code> limits which browser actions are allowed (allow-list) to prevent stray clicks/navigation; read-only tools (snapshot / a11y / content / scrape / screenshot / get_value / wait / challenge / list_tabs / session / history / visited) plus the "lift the restriction / reset the session" tools are always exempt.</p>
|
|
137
|
-
</td>
|
|
138
|
-
<td width="50%" valign="top">
|
|
139
|
-
<h3>Screenshot, save and read</h3>
|
|
140
|
-
<p><code>browser_screenshot</code> supports <code>savePath</code> to write the PNG to disk, ready for vision models (modlens etc.) to locate elements visually.</p>
|
|
141
|
-
</td>
|
|
142
|
-
</tr>
|
|
143
|
-
<tr>
|
|
144
|
-
<td width="50%" valign="top">
|
|
145
|
-
<h3>Searchable browsing history</h3>
|
|
146
|
-
<p><code>browser_visited</code> reads the <b>persistent</b> record of visited pages (stored beside the browser profile, so it survives closing the browser and restarting DSH), filterable by domain and reopenable. It is a different thing from <code>browser_history</code>, which is the session's operation log.</p>
|
|
147
|
-
</td>
|
|
148
|
-
<td width="50%" valign="top">
|
|
149
|
-
<h3>Synthetic cursor</h3>
|
|
150
|
-
<p>While the agent works, a virtual pointer and click ripple are drawn inside the page, with a <b>bubble labelling the current action</b> beside the pointer — <b>its appearance means the agent has taken over that tab</b>. DOM-level actions (set value, check, select) also show a landing point. It never moves the real system pointer, and can be switched off in settings.</p>
|
|
151
|
-
</td>
|
|
152
|
-
</tr>
|
|
153
|
-
<tr>
|
|
154
|
-
<td width="50%" valign="top">
|
|
155
|
-
<h3>A "Browser" section in Settings</h3>
|
|
156
|
-
<p><b>Which browser</b> (sidebar browser / bundled Electron / installed Chrome / installed Edge / automatic), keep history, auto-expand the side panel, close the browser when a session ends, show the cursor, vision strategy, allow credential reads — every one of these is <b>read fresh on each use</b>, so a change takes effect immediately with no restart. <b>Two exceptions</b>: <code>browser.channel</code> (which browser) and <code>cookies.persist</code> (keep cookies) are read once when the plugin is applied, so changing either needs a <b>plugin reload</b> (restart dsh / refresh the page). The section also carries a group of <b>"What the agent may do"</b> switches — <b>run scripts in the page</b>, <b>download files to disk</b>, <b>write login state</b>. They are not the same thing as <code>browser_restrict</code>: that one is the model's own soft guardrail and the model can lift it whenever it likes, while these live in the settings document, which <b>no tool can write</b> — the model can only be refused.</p>
|
|
157
|
-
</td>
|
|
158
|
-
<td width="50%" valign="top">
|
|
159
|
-
<h3>Unambiguous teardown</h3>
|
|
160
|
-
<p><b>Closing the page that carries it ends that session</b> (the next open is a clean one) — on the desktop that means closing the sidebar's browser tab, on the web closing the window — while browsing history and login state survive. <b>Collapsing the interface is only a collapse</b>: the browser keeps running. Whether the browser is released automatically when a session ends is a setting.</p>
|
|
161
|
-
</td>
|
|
162
|
-
</tr>
|
|
163
|
-
</table>
|
|
164
|
-
|
|
165
|
-
## Why this plugin
|
|
166
|
-
|
|
167
|
-
- **Install-and-use, zero config**: no desktop shell or extra startup step required; on plain `dsh web` it self-hosts an Electron window and the `browser_*` tools just work.
|
|
168
|
-
- **Human-in-the-loop, non-interfering**: the user sees and can take over every agent action; per-task isolation gives each parallel task its own tabs and history.
|
|
169
|
-
- **Built for the real world**: CAPTCHA detection, login persistence, batch form filling, authenticated downloads, operation replay, action restriction — real-browser automation that is actually reliable.
|
|
170
|
-
- **Testable, replaceable architecture**: the provider talks to Electron through the `ElectronBrowserViewHost` seam, so a future headless relay provider can serve remote deployments without touching the tool layer.
|
|
171
|
-
|
|
172
|
-
## Tool reference
|
|
173
|
-
|
|
174
|
-
| Tool | Purpose | Guard |
|
|
175
|
-
| --- | --- | --- |
|
|
176
|
-
| `browser_open` | Open a URL; returns a page snapshot. **A further call in the same session opens a new tab** (the first call opens the side panel automatically) | ✅ |
|
|
177
|
-
| `browser_wait` | Wait for page load (optional expected URL / CSS selector), returns readiness | – |
|
|
178
|
-
| `browser_snapshot` | Numbered inventory of interactive elements (inputs/buttons/links), each line carrying a `{#id}` / `{[name=x]}` selector when one is derivable (pierces same-origin iframes and Shadow DOM) | – |
|
|
179
|
-
| `browser_a11y` | Accessibility tree: semantic role/name/value/states per interactive node, plus a `{#id}` / `{[name=x]}` selector when derivable; coordinates only on request (`coords: true`, off by default, pierces same-origin iframes and Shadow DOM) | – |
|
|
180
|
-
| `browser_execute` | Run JS in the page; args arrive as `arguments[0..n]`. Governed by the "Run scripts in the page" switch — off refuses it outright (`BROWSER_EXECUTE_DISABLED`) | ✅ |
|
|
181
|
-
| `browser_visited` | Read the persistent browsing history (visited pages, filterable by domain / capped); reopen with `browser_open` | – |
|
|
182
|
-
| `browser_content` | Fetch the page as html / markdown / txt / json (selector, maxChars, timeoutMs) | – |
|
|
183
|
-
| `browser_click` | Click a semantic target (`target`: css/text/xpath, scrolls into view and clicks center) or viewport coordinates (vision-located) | ✅ |
|
|
184
|
-
| `browser_type` | Type text (optionally focusing a `target` element first; CDP `Input.insertText`) | ✅ |
|
|
185
|
-
| `browser_key` | Press a named key (Enter/Tab/arrows/Home/End…) | ✅ |
|
|
186
|
-
| `browser_scroll` | Scroll the page (pixel deltas / selector / top-bottom) | ✅ |
|
|
187
|
-
| `browser_back` | One step back in page history (no-op at the start) | ✅ |
|
|
188
|
-
| `browser_forward` | One step forward in page history (no-op at the end) | ✅ |
|
|
189
|
-
| `browser_refresh` | Reload the current page (like a browser refresh button) | ✅ |
|
|
190
|
-
| `browser_fill` | Batch form fill (selector/name/label matching, controlled inputs, selects, checkbox/radio, optional submit) | ✅ |
|
|
191
|
-
| `browser_set_value` | Set one control's value (`target`-located; native setter + input/change, React-controlled friendly) | ✅ |
|
|
192
|
-
| `browser_check` | Check/uncheck a checkbox or radio (`target`-located) | ✅ |
|
|
193
|
-
| `browser_select` | Select an option of a `<select>` by value/text/index (`target`-located) | ✅ |
|
|
194
|
-
| `browser_clear` | Clear an input/textarea/contenteditable, or uncheck (`target`-located) | ✅ |
|
|
195
|
-
| `browser_get_value` | Read an element's current value for verification (`target`-located) | – exempt |
|
|
196
|
-
| `browser_scrape` | Structured extraction: container selector + field map (`selector@attr`), static CSS only, CSP-safe | – exempt |
|
|
197
|
-
| `browser_screenshot` | Capture, optional `fullPage`, `savePath`, JPEG (`format`/`quality`) and scaling (`maxWidth`/`maxHeight`); `savePath` shares the download gate (confined to `downloadDir`, never overwrites) | – |
|
|
198
|
-
| `browser_list_tabs` | List the session's tabs | – |
|
|
199
|
-
| `browser_switch_tab` | Switch to a tab by id (also switches the visible view when self-hosted) | ✅ |
|
|
200
|
-
| `browser_close_tab` | Close a tab by id; closing the active tab activates the next | ✅ |
|
|
201
|
-
| `browser_reset` | Close all tabs of this task, back to one blank tab | – exempt |
|
|
202
|
-
| `browser_session` | Show this task's browser session and tabs | – |
|
|
203
|
-
| `browser_reset_session` | Close and rebuild this task's browser session | – exempt |
|
|
204
|
-
| `browser_history` | Operation log (newest last), with per-step success/error and result summary | – |
|
|
205
|
-
| `browser_replay` | Replay one step by sequence number (navigate/click/type/scroll/key/execute) | ✅ |
|
|
206
|
-
| `browser_download` | Download an HTTP(S) URL with session cookies to a local file (absolute `savePath` inside `downloadDir`, never overwrites, 256 MB cap). Governed by the "Download files to disk" switch (`BROWSER_DOWNLOAD_DISABLED`) | ✅ |
|
|
207
|
-
| `browser_auth` | Export/restore cookies (login persistence; **works on all three carriers** — self-hosted reads its own session, the sidebar and an installed browser go through CDP). Export is governed by the Credentials switch (`BROWSER_AUTH_DISABLED`) and writing by "Write login state" (`BROWSER_AUTH_WRITE_DISABLED`); the two are independent | ✅ |
|
|
208
|
-
| `browser_challenge` | Detect a human-verification challenge (CAPTCHA / Cloudflare / reCAPTCHA / hCaptcha / Turnstile) | – |
|
|
209
|
-
| `browser_restrict` | Restrict allowed browser actions (allow-list; empty list lifts it). **Soft guardrail** — the model can lift it itself; not a security boundary | – |
|
|
210
|
-
|
|
211
|
-
> "Guard" column: ✅ actions are governed by the `browser_restrict` allow-list; **rows marked "– exempt" are never blocked, whatever the allow-list says**, and **a plain "–" means the tool only observes — it performs no action, so there is nothing to restrict**. The exempt set is `READ_ONLY_TOOLS` (`src/tool-browser/index.ts`): `snapshot` / `a11y` / `content` / `scrape` / `screenshot` / `get_value` / `wait` / `challenge` / `list_tabs` / `session` / `history` / `visited`, plus `restrict` (the restriction itself must stay liftable, or a task that restricted everything could never get out), `reset_session` and `reset`. `browser_close_tab` is **not** exempt: like `open`/`click`/`switch_tab` it can be restricted; neither is `browser_auth`, whose `restore` writes cookies to arbitrary domains — an action, not an observation.
|
|
212
|
-
|
|
213
|
-
### Waiting for the page
|
|
214
|
-
|
|
215
|
-
- **`browser_open` and navigation already wait (bounded) for the new document to parse** (`readyState` plus a document fingerprint, so a same-URL reload or an A→B→A redirect is not mistaken for the old document), but they do **not** wait for async content: on slow sites or XHR-rendered pages, call `browser_wait` before `browser_snapshot` — pass `url` (what you opened) and an optional `selector`, and wait for `ready: true` — otherwise you snapshot the old document, a white screen, or an empty element list.
|
|
216
|
-
- **Content you cannot see may live in an iframe / Shadow DOM**: snapshots and the a11y tree pierce same-origin iframes and shadow roots and mark them `(iframe)`; coordinates are always top-document, so `browser_click` works directly. DOM selectors are frame-scoped — reach them via `iframe.contentDocument` in `browser_execute`.
|
|
217
|
-
|
|
218
|
-
### Semantic targets and the a11y tree
|
|
219
|
-
|
|
220
|
-
- **`browser_a11y` is the best way to understand a page**: every interactive node carries its semantic role (button/textbox/checkbox…), accessible name, current value and states (checked/expanded/required…) — it explains *what this is and what it does* better than a numbered snapshot. **150 nodes by default** (`maxNodes` 10-5000; ask for more explicitly), and **coordinates are no longer included by default** (pass `coords: true` when you actually need a pixel to click). Only the literal `enabled` is omitted from `states` (`unchecked`, `collapsed` and others are still printed) — no `states=` means the node is enabled. A node's `name` is exactly what `browser_click`/`browser_type` match when you target by `text`.
|
|
221
|
-
- **`browser_click`/`browser_type` accept a `target`**: `{by: css|text|xpath, value, index?}` — `text` matches an element's own visible text (exact first, then contains, deepest preferred); clicks scroll the element to the viewport center first; typing focuses it first.
|
|
222
|
-
- **Use the single-control tools for one field** (`browser_set_value`/`browser_check`/`browser_select`/`browser_clear`/`browser_get_value`), `browser_fill` for batches, and `browser_scrape` for structured list extraction.
|
|
223
|
-
|
|
224
|
-
### Operating discipline (locating/clicking/filling)
|
|
225
|
-
|
|
226
|
-
- **Semantics first, coordinates last**: `browser_a11y` gives roles and accessible names, `browser_snapshot` gives refs and coordinates, and **both carry a selector** (see the next section) — **use it when it exists**, because scrolling and layout changes do not invalidate it.
|
|
227
|
-
- **`browser_click`'s `target` (css/text) IS semantic location**; use it instead of computing coordinates yourself. Scrolling into view, clicking the centre, iframe offsets and device pixel ratio are all handled for you.
|
|
228
|
-
- **Prefer DOM semantics over coordinates**: submit forms with `form.requestSubmit()`; click with `element.click()`; coordinate clicks are the last resort.
|
|
229
|
-
- **Target the right element**: pages often have hidden duplicates (e.g. mobile buttons); filter visible elements with `browser_execute` (`getBoundingClientRect()` w/h > 0, `getComputedStyle` not `display:none`), then take coordinates.
|
|
230
|
-
- **Click right after taking coordinates**: do not insert other operations in between (filling/scrolling moves elements and invalidates old coordinates).
|
|
231
|
-
- **Verify before clicking**: use `document.elementFromPoint(x, y)` to confirm the coordinate hits the intended element (button/link), then perform the real click.
|
|
232
|
-
- **DPR awareness**: CDP input uses CSS pixels; on high-DPI screens calibrate with `elementFromPoint` instead of guessing coordinates.
|
|
233
|
-
- **Two failures of the same action means change the approach**: a different selector, a semantic target, or a fresh snapshot — not the same coordinate a third time.
|
|
234
|
-
|
|
235
|
-
### Vision strategy: know which one you are in first
|
|
236
|
-
|
|
237
|
-
The **`vision.strategy`** setting decides whether coordinate clicks are usable, and **that is the only thing it affects**:
|
|
238
|
-
|
|
239
|
-
| Strategy | Coordinate clicks | Suited to |
|
|
240
|
-
| --- | --- | --- |
|
|
241
|
-
| `auto` (default) | allowed | **A model that can read images** — screenshots are a valid way to check layout |
|
|
242
|
-
| `nonVisual` | **refused** | **A model with no image input** |
|
|
243
|
-
|
|
244
|
-
**Under `nonVisual` a coordinate click is refused outright**, reported as `BROWSER_NON_VISUAL_COORDINATES`, and the error names the fix: pass a semantic target, e.g. `target { by: "text", value: "Sign in" }`, which locates the element from the DOM and clicks its centre. **That is a configuration, not a failure — do not retry the same coordinate.**
|
|
245
|
-
|
|
246
|
-
**DOM queries, the accessibility tree, text extraction and structured scraping are identical under both strategies** — that is, **the page can be operated completely without image input**. So the "when not to screenshot" advice below is the default for **every** model, not a concession to the ones that cannot see.
|
|
247
|
-
|
|
248
|
-
### When NOT to take a screenshot
|
|
249
|
-
|
|
250
|
-
Tool output is spent out of the caller's context, and **a screenshot is the most expensive kind**:
|
|
251
|
-
|
|
252
|
-
- **If text will do, use text.** `browser_snapshot` / `browser_a11y` / `browser_content` carry the same page as text; **a model without image input gains nothing from a screenshot** and only pays for it. Keep screenshots for what genuinely needs pixels: layout, charts, design checks.
|
|
253
|
-
- **A downscaled capture returns `width`/`height`** (the real pixel size). When you use `maxWidth`/`maxHeight` to control cost, read those to confirm it took effect rather than decoding the data URL yourself.
|
|
254
|
-
|
|
255
|
-
### The operator's three switches, which no tool can change
|
|
256
|
-
|
|
257
|
-
**Running page scripts**, **downloading to disk** and **writing login state** are controlled by the operator in the settings page, and **no `browser_*` tool can change them**. When one is off the error code is unambiguous — **do not retry and do not route around it**; say that the human needs to enable it:
|
|
258
|
-
|
|
259
|
-
`BROWSER_EXECUTE_DISABLED` (scripts), `BROWSER_DOWNLOAD_DISABLED` (downloads), `BROWSER_AUTH_WRITE_DISABLED` (writing login state), `BROWSER_AUTH_DISABLED` (reading cookies).
|
|
260
|
-
|
|
261
|
-
Do not confuse those with `BROWSER_DOWNLOAD_BLOCKED` / `BROWSER_SCREENSHOT_BLOCKED`, which mean **the save path was refused by the sandbox** (change the path, not a setting), or `BROWSER_DOWNLOAD_UNSUPPORTED`, which means **this carrier has no such capability**.
|
|
262
|
-
|
|
263
|
-
**`browser_restrict` is different** — it is a temporary allow-list the model sets and lifts itself, and **it is not a way around the three switches above**.
|
|
264
|
-
|
|
265
|
-
### Output size and truncation
|
|
36
|
+
On the desktop the plugin drives the browser panel inside the **official side panel** — no extra window, no Electron dependency, and the agent operates the very page you see in the interface.
|
|
266
37
|
|
|
267
|
-
|
|
38
|
+
Driving that panel also needs a bridge, installed into **the desktop app's own install directory** rather than the profile:
|
|
268
39
|
|
|
269
|
-
|
|
270
|
-
-
|
|
271
|
-
|
|
272
|
-
- **`browser_execute` caps its return value at 50 000 characters**, and states the cut in the result ("truncated at 50 000 of N") so a shortened value is never silently short.
|
|
273
|
-
- **`browser_scrape` returns compact JSON** (no indentation) with the item count in front — indentation carries nothing a model can use and measured over a tenth of the output.
|
|
274
|
-
- **`browser_history` shows the last 20 operations and omits each operation's parameters** (they are what you just sent), leading with the total so a tail is not mistaken for the whole; pass `verbose: true` when you actually want the parameters.
|
|
275
|
-
- **`browser_visited` timestamps are minute-granular**, and the session tag prints only when it changes, shortened to 8 characters — not a full session id per row.
|
|
276
|
-
|
|
277
|
-
## Configuration
|
|
278
|
-
|
|
279
|
-
The plugin mounts through `cordis.patch.yml` (**four rows**): one **inert root row** (it exists only to name the package, because the host's client-module scan reads a row's `dsh.client` only when that row resolves to an **exact package name** — a row named after a subpath is skipped entirely, and then the settings panel would never appear) plus three functional rows (`browser` / `browser-electron` / `tool-browser`). Per-row config:
|
|
280
|
-
|
|
281
|
-
| Row | Key | Type | Default | Description |
|
|
282
|
-
| --- | --- | --- | --- | --- |
|
|
283
|
-
| `browser-electron` | `viewHost` | object | optional | `ElectronBrowserViewHost` instance supplied by the host (typically `!!js ctx.get('electronViewHost')`). **When it is absent the plugin picks the carrier itself** — the desktop sidebar if that is where it runs, otherwise self-hosting; a browser chosen in settings outranks both |
|
|
284
|
-
| `browser-electron` | `httpOnly` | boolean | `true` | Allow HTTP(S) navigation only; other protocols (e.g. `file:`/`data:`) rejected (`BROWSER_NAVIGATION_BLOCKED`) |
|
|
285
|
-
| `browser-electron` | `snapshotMaxElements` | number | `60` | Max snapshot elements before truncation |
|
|
286
|
-
| `browser-electron` | `contentMaxChars` | number | unset (per-format defaults) | The default content character cap. **Unset**, the caps are per format (html 50 000, json 50 000, txt 20 000, markdown 20 000); **set**, it is the operator's value and overrides them. A call's own `maxChars` outranks both |
|
|
287
|
-
| `browser-electron` | `downloadDir` | string | system Downloads folder (`Downloads`/`下载`/`下載`, or `XDG_DOWNLOAD_DIR`, auto-detected) | Confine `browser_download` AND `browser_screenshot` save paths to this directory, never overwriting an existing file (stops a prompt-injected agent writing or replacing arbitrary paths); override for a sandbox dir |
|
|
288
|
-
| `tool-browser` | `timeoutMs` | number | `60000` | Cooperative tool timeout (ms) |
|
|
289
|
-
| `tool-browser` | `tabTools` | boolean | `true` | Register tab-management tools (`browser_list_tabs` etc.) |
|
|
290
|
-
| `tool-browser` | `allowedActions` | string[] | unrestricted | **Deployment-level** action allow-list, applied to every task in the deployment (a task can still narrow it further with `browser_restrict`); `READ_ONLY_TOOLS` and `browser_restrict` are never blocked, whatever the list says. `cordis.patch.yml` ships an empty `config: {}` (i.e. unrestricted); its comments show how to tighten it |
|
|
291
|
-
|
|
292
|
-
## How it works
|
|
293
|
-
|
|
294
|
-
```
|
|
295
|
-
agent (browser_* tools)
|
|
296
|
-
→ ctx.browser (seam, dsh-builtin-browser/browser)
|
|
297
|
-
→ dsh-builtin-browser/browser-electron (provider)
|
|
298
|
-
→ ElectronBrowserViewHost ← the same seam, implemented once per carrier
|
|
299
|
-
① desktop sidebar via bridge → shell main process → webContents.debugger (CDP)
|
|
300
|
-
② installed Chrome/Edge via WebSocket → CDP
|
|
301
|
-
③ self-hosted Electron via loopback TCP JSON-RPC → child process → CDP
|
|
40
|
+
```sh
|
|
41
|
+
node desktop-bridge/install.mjs # idempotent; backs up main.js.before-bridge first
|
|
42
|
+
node desktop-bridge/install.mjs --revert # roll back
|
|
302
43
|
```
|
|
303
44
|
|
|
304
|
-
|
|
305
|
-
- **Provider** (`browser-electron` row): knows only the one `ElectronBrowserViewHost` seam (create/destroy/show, `sendCommand`), so **switching carrier touches neither the tools, nor the history, nor the cursor, nor the teardown logic**.
|
|
306
|
-
- **Tools** (`tool-browser` row): the 34 model-facing `browser_*` tools, maintaining one browser session per calling task (DSH session).
|
|
45
|
+
> **Optional**: without the bridge the plugin falls back to self-hosting — identical capability, just one extra standalone window. **Re-run it after every desktop upgrade**, which replaces `resources/app/` and takes the bridge with it.
|
|
307
46
|
|
|
308
|
-
|
|
47
|
+
### Web (`dsh web`)
|
|
309
48
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
## Division of labor with the desktop shell
|
|
315
|
-
|
|
316
|
-
The plugin picks a carrier automatically, and the setting can override it (four in total):
|
|
317
|
-
|
|
318
|
-
**① Desktop: drive the official sidebar's page (one page for both parties)**
|
|
319
|
-
|
|
320
|
-
DSH Desktop is two layers: an Electron shell plus an `--expose-internals` **Node-mode host** (the plugin runs there, with **no Electron API**). 0.2 removed `electronViewHost`, and the host/shell event set carries nothing view-related either — so the plugin borrows a **small bridge**: a loopback + token service inside the shell's main process hands the plugin CDP access to the sidebar browser's guest, i.e. the very page you see on screen.
|
|
49
|
+
```sh
|
|
50
|
+
dsh plugin --profile web add dsh-builtin-browser
|
|
51
|
+
```
|
|
321
52
|
|
|
322
|
-
|
|
53
|
+
There is **no side panel and no bridge** on the web side: the shared browser is **self-hosted** in a standalone window (titled `dsh-browser`) with a full toolbar (address bar, back/forward/reload, tab strip) you can use like any browser.
|
|
323
54
|
|
|
324
|
-
|
|
55
|
+
> If the Electron binary is missing on first install (44+ no longer downloads it), run `npx install-electron` once as the error suggests — it needs network.
|
|
325
56
|
|
|
326
|
-
|
|
57
|
+
### Both sides
|
|
327
58
|
|
|
328
|
-
|
|
59
|
+
No further configuration; just ask the agent:
|
|
329
60
|
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
node desktop-bridge/install.mjs --revert # roll back
|
|
61
|
+
```
|
|
62
|
+
Open https://example.com and summarise the main content for me.
|
|
333
63
|
```
|
|
334
64
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
> **⚠️ What the sandbox boundary change means**
|
|
338
|
-
>
|
|
339
|
-
> The official sidebar browser is built on the premise that its pages are **not readable from outside** — it uses a separate partition and the shell refuses cross-site content access. Letting the agent drive that guest **deliberately breaks that premise**:
|
|
340
|
-
>
|
|
341
|
-
> - the agent can read the content of **any page you open in the sidebar** (that is precisely what "one page for both parties" means);
|
|
342
|
-
> - the agent can read the **cookies and login state** in that partition, and `browser_auth` can export them (controlled by a setting);
|
|
343
|
-
> - your actions in the sidebar and the agent's actions act on **the same page** and can affect each other (the agent will not overwrite what you are typing, but navigation changes what you both see).
|
|
344
|
-
>
|
|
345
|
-
> That is the inherent cost of one shared page. We think it is worth it — it turns "the agent is doing something in a window you cannot see" into "you can watch it work and take over" — but you are entitled to know it exists, so there are switches: with **credential access** off, `browser_auth` **refuses the export** (`BROWSER_AUTH_DISABLED`) — it governs reading only, while writing login state (restore) answers to its own switch and reports `BROWSER_AUTH_WRITE_DISABLED`; and with the **vision strategy** set to non-visual any coordinate click that depends on a screenshot is refused. If you would rather not accept the boundary change at all, removing the plugin from the desktop profile returns you to the old separate-window shape.
|
|
346
|
-
|
|
347
|
-
**② Use the browser you already have (Chrome / Edge)**
|
|
348
|
-
|
|
349
|
-
The settings panel can point the plugin at an **installed Chrome or Edge** (`browser.channel`: `bundled` / `auto` / `chrome` / `edge`). The approach is the same one Codex Browser Use takes: launch it with `--remote-debugging-port=0`, read the port it writes into `DevToolsActivePort`, and drive it entirely over CDP (through Node 22's built-in `WebSocket` — **no new dependency**).
|
|
65
|
+
The browser window appears with the first call, and you can operate in it yourself at any point. **Either side can be pointed at your own installed Chrome / Edge** in settings — that option behaves identically on both.
|
|
350
66
|
|
|
351
|
-
|
|
67
|
+
## Documentation
|
|
352
68
|
|
|
353
|
-
|
|
69
|
+
| Goal | Entry point |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| Why this plugin exists, and how it differs from headless | [Why a shared real browser](docs/why-browser.md) |
|
|
72
|
+
| Install, configure, daily use, troubleshooting | [User guide](docs/user-guide.md) |
|
|
73
|
+
| Every one of the 34 tools: parameters, output, examples | [Tool reference](docs/tool-reference.md) |
|
|
74
|
+
| The seam / provider / tool layers and the self-hosted host | [Architecture](docs/architecture.md) |
|
|
75
|
+
| What it deliberately **cannot** do, and why | [Known limitations](docs/limitations.md) |
|
|
76
|
+
| Build, test, re-verify host compatibility | [Development](docs/development.md) |
|
|
77
|
+
| All documents and how they divide the work | [Documentation index](docs/README.md) |
|
|
354
78
|
|
|
355
|
-
**
|
|
79
|
+
> Requirements, the four carriers and their tradeoffs, configuration, operating discipline, troubleshooting and updating all live in those documents — **this README is an entry point and does not repeat them.**
|
|
356
80
|
|
|
357
|
-
|
|
358
|
-
- `cookies.persist` **off** → a **throwaway profile** each time, deleted when the browser is released; no login trace is left behind. A process killed outright never reaches that release path, so the next start **sweeps** the throwaway profiles an earlier run left behind — only those whose owner is gone AND which have not been written to for an hour, so the profile an orphaned browser is still working in is left alone.
|
|
359
|
-
- The trade-off, stated plainly: a separate profile **does not see** the sites you are signed into in your everyday browser. Sign in once in the window the plugin opens and the session stays in its own profile.
|
|
81
|
+
## Tool namespace
|
|
360
82
|
|
|
361
|
-
|
|
83
|
+
This plugin claims the global **`dsh_browser_*`** namespace (34 tools).
|
|
362
84
|
|
|
363
|
-
|
|
85
|
+
**Before 0.4.8 it was `browser_*`.** That is an unprefixed, generic word, and it collided directly with Tencent BrowserSkill's DSH plugin (`browser_session` in `@wxg-prc-cpg/browser-skill-dsh-plugin` @0.3.2) — and **DSH's tool registry is global and refuses duplicates**, so whichever registers second throws. All six of that plugin's tools failed to register, which reads as "installed but does nothing".
|
|
364
86
|
|
|
365
|
-
|
|
366
|
-
>
|
|
367
|
-
> **Precedence**: an explicitly chosen installed browser > the desktop sidebar > self-hosting. A missing carrier is handled two different ways: `automatic` means "any browser will do", so it **logs a warning and keeps the bundled one**; but when you **named a browser explicitly** and it is absent, the plugin no longer settles for a log line — it adopts a carrier whose only job is to explain, so **every command reports to the caller which browser was missing, the names and locations that were searched, and three ways out**. You are not left staring at an Electron error wondering what actually went wrong.
|
|
87
|
+
**Why we changed rather than waiting:** a bare name like `browser_*` belongs to nobody, and the cost of one collision is **not "each plugin keeps one" — it is that the second one loses its entire toolset.** Giving up the generic name is a better answer than asking every user to patch their own `node_modules`. Tencent's upstream has renamed its six tools to `bsk_*` as well.
|
|
368
88
|
|
|
369
|
-
|
|
89
|
+
**If your log shows `could not register dsh_browser_xxx`**: another plugin holds that name. From 0.4.8 every tool registers **independently**, so a collision costs that one name and nothing else — the check exists so the next collision is visible at a glance rather than starting from "the plugin is broken".
|
|
370
90
|
|
|
371
|
-
|
|
372
|
-
- **Electron runtime** (required dependency, installed automatically with the plugin, ≥ 40 recommended; the 44+ binary is not downloaded at install time — if missing, follow the error and run `npx install-electron` first, needs network) — **only the self-hosted carrier needs it**:
|
|
373
|
-
- `ELECTRON_PATH` can point at another binary explicitly (highest priority);
|
|
374
|
-
- **DSH Desktop**: the official sidebar by default, so **no Electron is needed**; the bundled electron is used only when it falls back to self-hosting. The packaged host exe (`DSH Desktop.exe`) is **never reused** — packaged apps cannot be spawned with a script argument and misuse exits instantly (issue #6); dev-mode **bare** Electron hosts are still reusable;
|
|
375
|
-
- **an installed Chrome / Edge needs no Electron either**;
|
|
376
|
-
- **plain `dsh web` self-hosted**: uses the bundled electron package directly
|
|
91
|
+
**Who should update to 0.4.8:** only names changed, not behaviour. **If this is your only browser plugin, you do not need to update.** If you also run another browser plugin (Tencent BrowserSkill especially), updating is recommended. The upgrade note is at the end of the [user guide](docs/user-guide.md) (Chinese).
|
|
377
92
|
|
|
378
|
-
|
|
93
|
+
## Release history
|
|
379
94
|
|
|
380
|
-
|
|
381
|
-
| --- | --- |
|
|
382
|
-
| DeepSeek Harness (dsh) | `0.2.1-alpha.1` (peer range `>=0.1.1-rc.2 <0.3.0`; `0.2.0-rc.2` and `0.1.x` were verified earlier) |
|
|
383
|
-
| Electron | `44.0.0` (≥ 40 recommended; 33.x has a compositor defect) |
|
|
384
|
-
| Node.js | `22.20.0` |
|
|
385
|
-
| Installed Chrome / Edge (optional carriers) | `154.0.8037.58` / `154.0.4258.37` |
|
|
386
|
-
| dsh-builtin-browser | `0.4.4` (unreleased, on the development branch) |
|
|
387
|
-
| OS | Windows 10 (10.0.26200) |
|
|
388
|
-
|
|
389
|
-
> The plugin declares `electron >= 30`. The **core path is fully verified on Windows**; system-browser detection is now adapted for Linux and macOS (PATH first, then each platform's conventional install locations, all overridable with `DSH_BROWSER_CHROME_PATH` / `DSH_BROWSER_EDGE_PATH`), but the end-to-end path on those platforms has not been measured, so no promise is made yet.
|
|
390
|
-
|
|
391
|
-
> **Re-verify host compatibility at any time**: `node scripts/verify-host-compat.mjs` assembles a host out
|
|
392
|
-
> of the `@deepseek-ai/*` packages **actually installed** in a profile, loads this plugin's compiled
|
|
393
|
-
> output, registers its tools through the host's own `defineTool`, and calls two of them over a fake
|
|
394
|
-
> provider (`browser_session` / `browser_a11y`). It answers "did a DSH upgrade break us?" without starting
|
|
395
|
-
> a browser. It reads the profiles under `$DSH_HOME/profiles`; `DSH_PROFILE` / `DSH_HOME` override that.
|
|
396
|
-
>
|
|
397
|
-
> **On semver and prereleases**: the range `>=0.1.1-rc.2 <0.3.0` matches no prerelease-tagged version
|
|
398
|
-
> (such as `0.2.1-alpha.1`) under **default semver semantics** — that is by specification, where a
|
|
399
|
-
> prerelease is only admitted by a comparator carrying the same major.minor.patch tuple. DSH evaluates
|
|
400
|
-
> plugin compatibility with `includePrerelease: true`
|
|
401
|
-
> (`packages/boot/app-boot/src/plugin-compatibility.ts`), so those hosts are in range in practice; if
|
|
402
|
-
> some external tool reads "incompatible" under default semantics, that is its rule differing from DSH's,
|
|
403
|
-
> not a wrong claim here.
|
|
404
|
-
|
|
405
|
-
## Updating (the two hosts differ)
|
|
406
|
-
|
|
407
|
-
The plugin has one installation per host, and the two are updated separately — **updating one does not update the other**.
|
|
408
|
-
|
|
409
|
-
**Desktop (DSH Desktop)**
|
|
410
|
-
- The plugin is a dependency of the desktop profile (usually `$DSH_HOME/profiles/desktop`). Updating means moving that dependency to the new version and then **restarting DSH Desktop**, which is when the settings panel and the tools pick up the new code.
|
|
411
|
-
- **The desktop has one extra step, which the web does not**: the bridge that lets the plugin drive the sidebar lives in the **desktop app's own install directory** (`resources/app/`), and a plugin update does **not** carry it along. A desktop upgrade replaces that directory and takes the bridge with it, so re-run it once:
|
|
412
|
-
```bash
|
|
413
|
-
node desktop-bridge/install.mjs # idempotent; refreshes the module if already installed
|
|
414
|
-
node desktop-bridge/install.mjs --revert # roll back
|
|
415
|
-
```
|
|
416
|
-
With no bridge the plugin falls back to self-hosting (one extra separate window) — nothing breaks, the tools stay available.
|
|
417
|
-
- The browser engine comes from the desktop app's own Electron by default, and the plugin never downloads a second copy; you can also point it at an installed Chrome / Edge in settings.
|
|
418
|
-
- **What happens when the browser you picked is not installed**: `bundled` uses its own Electron; `automatic` picks an installed one and **quietly falls back to bundled if there is none**; but an **explicit `chrome` / `edge` choice that is missing fails with a message saying so** — it lists the names and locations that were checked and offers three ways out (install it / point `DSH_BROWSER_CHROME_PATH` or `DSH_BROWSER_EDGE_PATH` at it / set the carrier back to `bundled` or `automatic`). You are never left guessing from an error about Electron.
|
|
419
|
-
- Upgrading the desktop app itself does not carry the plugin along; update it as described above.
|
|
420
|
-
|
|
421
|
-
**Web (`dsh web`)**
|
|
422
|
-
- The plugin is a dependency of the web profile (`$DSH_HOME/profiles/web`); **restart `dsh web`** after updating.
|
|
423
|
-
- There is no desktop shell, so the shared browser is self-hosted by the plugin: the first install may need an Electron binary. If the package manager's build allow-list blocked it (pnpm v10+ blocks `electron`'s postinstall), run `npx install-electron` once to fetch it.
|
|
424
|
-
- It can equally be pointed at an installed Chrome / Edge in settings — an option that behaves the **same on both hosts**.
|
|
425
|
-
- Update it the same way you installed it (npm package `dsh-builtin-browser`, the GitHub repo `wqty123/dsh-browser`, or a local directory).
|
|
426
|
-
|
|
427
|
-
**What is the same on both**
|
|
428
|
-
- The toolset (34 `browser_*` tools), the "Browser" section in Settings, and how browsing history and cookies persist are identical; only the carrier of the page differs (the official sidebar on the desktop, the plugin's own self-hosted window on the web, or an installed browser you picked in settings).
|
|
429
|
-
- Upgrading loses no data: history and settings live in `$DSH_HOME/dsh-builtin-browser-host/` (`history.jsonl`, `settings.json`), and login state sits in the same profile directory — including the `<chrome|edge>-profile` used for an installed browser.
|
|
430
|
-
- If history behaves unexpectedly after an upgrade, check **Settings → Browser**: history defaults to **on**, one-time auto-expand defaults to **on**, closing the browser when a session ends defaults to **off**, and the carrier defaults to **bundled**.
|
|
431
|
-
|
|
432
|
-
## Known limitations
|
|
433
|
-
|
|
434
|
-
- JPEG screenshots work on the **self-hosted** carrier and with an **installed Chrome / Edge**. The **desktop sidebar** goes through the shell's `webContents.debugger`, whose Electron CDP JPEG encoder hangs, so a JPEG request on that carrier returns PNG. Downscaling (`maxWidth`/`maxHeight`) **works on all three** — through a CDP `clip.scale`, or the native `capturePage` when self-hosted.
|
|
435
|
-
- Self-hosted captures prefer Electron's native `capturePage` (CDP `captureScreenshot` can hang with multiple views in the window); the target tab is raised before capturing.
|
|
436
|
-
- `fullPage` capture is flaky under software compositing on some hosts — with `fullPage` the native `capturePage` path is **skipped entirely** (`capturePage` cannot reach content beyond the viewport), so **all three carriers go through CDP `captureBeyondViewport`** and the flakiness is not carrier-specific: any carrier can hit it. Only viewport captures prefer the native `capturePage`.
|
|
437
|
-
- CAPTCHA cannot be solved automatically: snapshots flag detected challenges; ask the human to complete it in the shared window instead of retrying.
|
|
438
|
-
- Private mode (`privateMode`) is not implemented: it needs Electron session partitioning, which is host-layer territory; this plugin does not promise it.
|
|
439
|
-
- A settings document that **exists but cannot be read** (corrupt JSON, a half-written file, unreadable permissions) resolves the four **gate** switches (`credentials.allowRead` plus the three action switches) to **off**, with every other setting at its default — "the operator's intent cannot be read" is not "the operator permits everything". A file that is simply **absent** (first run) still takes the defaults. A gate field of the wrong type (`"false"` as a string, `null`, a number) reads as **off** as well, and so does a **gate-holding section** written as a non-object (`actions: false`, `null`, a stray string): such a section is read as unreadable rather than as absent, so every gate inside it is **off** instead of taking its permissive default.
|
|
440
|
-
- `browser_download` fetches in the page context (keeps logins) and is subject to same-origin/CORS constraints; HTTP(S) targets only; `savePath` must be absolute and inside `downloadDir` (default: the system Downloads folder, auto-detecting `Downloads`/`下载`/`下載` and `XDG_DOWNLOAD_DIR`; override with `downloadDir`) and never replaces an existing file; `browser_screenshot`'s `savePath` goes through the same gate; single files are capped at 256 MB (streamed with a Content-Length early reject) and are written by the browser child itself (temp file + atomic rename). Admission also resolves where a path really lands: a symlink inside `downloadDir` cannot carry the write out of it, and a dangling link is refused too (it reads as a name already taken, not as a free path).
|
|
441
|
-
- The self-hosted browser's cookies are stored in plaintext on disk (Electron default); deployments that need encrypted-at-rest should integrate a system keychain / DPAPI at the host layer.
|
|
442
|
-
- `browser_restrict` is a **soft guardrail** against accidental actions, not a security boundary: the model can lift it itself. For a limit the model cannot remove with a tool, use the three action switches in Settings ("What the agent may do") — they live in the settings document, which no `browser_*` tool writes — or the deployment-level `tool-browser` config `allowedActions` (see the comments in `cordis.patch.yml`). **Be precise about that boundary**: settings travel over a local HTTP endpoint whose writes require a same-origin `Origin` (so a bare HTTP client cannot write them), but a **same-origin page still can** — that is inherent to exposing an endpoint at all, and it is **not a credential system**. The panel's own text says exactly this, and it is worth reading as what it is rather than as a lock.
|
|
443
|
-
- Popups (`window.open` / `target=_blank`) no longer overwrite the current view: HTTP(S) popups open as a **new tab** in the same session window, recorded in the session history, keeping the original page and its opener context alive. Non-HTTP(S) popups (empty-URL popup handoffs, `mailto:`, custom schemes) are still **allowed as native windows** and handed to the system — such windows are simply not part of the session model.
|
|
444
|
-
- The `browser_auth` cookie round-trip does not preserve `hostOnly`/`sameSite` (host-only cookies come back as domain cookies). It **works on all three carriers**: self-hosted reads its own session, while the sidebar and an installed Chrome/Edge go through CDP's `Network.getCookies` / `Storage.setCookies`. **`flush` exports only the cookies of the site the current page is on**, not the machine-wide shared jar — the export is scoped to the URL of the page this session is sitting on, and when that URL is unknown the tool **refuses rather than widening** (the self-hosted sidebar path reports that the cookie scope is unknown; the CDP path raises `BROWSER_AUTH_SCOPE_UNKNOWN`). `browser_auth` **is subject to the `browser_restrict` allow-list** too, because `restore` is an action that writes cookies to arbitrary domains.
|
|
445
|
-
- After a self-hosted child crash (or a DSH restart that kills it) the browser host restarts automatically, and sessions opened before the crash **rebuild on their next use** — only page state is lost, no manual `browser_reset_session` needed (it still works for an explicit reset). A new view preloads `about:blank` (bounded 3 s) before creation so it always has a live renderer, and host-side commands are bounded at 20 s. The child's stderr plus exit code/signal go to `$DSH_HOME/logs/dsh-builtin-browser-host.log`; before appending, a file already over 2 MiB is **truncated: the old contents are discarded and replaced by a single timestamped rotation line** (`<ISO timestamp> log rotated: previous content exceeded 2097152 bytes and was discarded`). Nothing is kept from before the rotation — but that line records that it happened, so a plain `dsh web` self-hosted setup can still diagnose a crash loop itself.
|
|
446
|
-
- The electron package ships with the plugin, but Electron 44+ no longer downloads its binary at install time (~100 MB, needs network) — the probe is filesystem-only and never triggers its lazy download, so a missing binary surfaces as a clear error on first use telling you to run `npx install-electron` first; alternatively pre-install a binary and point `ELECTRON_PATH` at it.
|
|
447
|
-
- This plugin draws **no browser interface of its own** (no address bar, no tab strip, no side panel): on the desktop that interface is the **shell's own official sidebar**, and the plugin merely drives the page inside it. On the self-hosted carrier the window is drawn by the Electron child the plugin spawns — that is the carrier, not plugin UI. Do not treat "the sidebar" or "browser column" as a plugin feature.
|
|
448
|
-
- **The sidebar carrier does not report user-action events**: a human clicking in that page is the shell's own event, and the bridge has no operation that reports it. Features relying on that event therefore do not fire on the desktop sidebar; they do on the self-hosted carrier.
|
|
449
|
-
- **An installed Chrome / Edge runs with a plugin-owned profile**: it is launched with its own user-data directory (`$DSH_HOME/dsh-builtin-browser-host/<chrome|edge>-profile`), so your everyday bookmarks, extensions and logins are **not carried over**. That is deliberate — plugin activity stays out of your personal session — and whether login state survives a restart is decided by `cookies.persist`.
|
|
450
|
-
|
|
451
|
-
## Development
|
|
95
|
+
The full record — per round, with the cause and the evidence for each fix — is in [CHANGELOG.md](CHANGELOG.md) (Chinese). Published with a tag since **0.1.16**.
|
|
452
96
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
97
|
+
| Version | Date | Theme |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| **0.4.8** | 2026-10-14 | **Tool namespace**: 34 tools move to `dsh_browser_*`; one taken name can no longer cost the whole toolset |
|
|
100
|
+
| **0.4.7** | 2026-10-12 | Three defects of one shape: a hardcoded carrier, a dead host never replaced, a setting read once; takes the four suggestions from #26 |
|
|
101
|
+
| **0.4.6** | 2026-10-08 | Reading another conversation's sidebar, blank pages spawned while waiting, sign-outs on upgrade |
|
|
102
|
+
| **0.4.5** | 2026-10-06 | The real cause of cross-conversation bleed: panel placement keyed by conversation id, not by what the shell displays |
|
|
103
|
+
| **0.4.0**–**0.4.4** | 2026-10-02 | Splitting "limitations" into real ones and implementation ones: `dsh_browser_auth` works on all three carriers, JPEG works on an installed Chrome/Edge, downscaling everywhere; plus fixes for defects that crashed the host, silently did the wrong thing, or hung forever |
|
|
457
104
|
|
|
458
|
-
|
|
105
|
+
## Credits
|
|
459
106
|
|
|
460
|
-
|
|
107
|
+
Thanks to the [DeepSeek Harness repository](https://github.com/deepseek-ai/deepseek-harness) and the DeepSeek AI team: this plugin's seam, tool runtime and plugin system are all built on that project.
|
|
461
108
|
|
|
462
|
-
|
|
463
|
-
| --- | --- |
|
|
464
|
-
| `src/browser/` | The `ctx.browser` seam and all request/result types |
|
|
465
|
-
| `src/browser-electron/` | The provider and its three carriers — the desktop sidebar bridge (`desktop-bridge-host.ts`), an installed browser (`system-browser.ts`), the self-hosted child (`host-main.ts`); plus the transport (`bridge-connection.ts`) and the RPC operation vocabulary (`rpc-ops.ts`), settings, history and the synthetic cursor |
|
|
466
|
-
| `src/tool-browser/` | Model-facing `browser_*` tools; the element/node field set (`element-fields.ts`), where the item schema and the projection come from one declaration |
|
|
467
|
-
| `src/types/` | Electron ambient types (shim; no hard electron type dependency) |
|
|
468
|
-
| `desktop-bridge/` | The bridge installed into the desktop app, with its idempotent installer (`install.mjs`) |
|
|
469
|
-
| `tools/` | Operational scripts (e.g. `install-web-plugin.mjs`: pin → install → repair the profile → verify) |
|
|
109
|
+
Thanks as well to [Cordis](https://github.com/cordiverse/cordis) for the plugin foundation, and to everyone who discussed, tested, reported and built plugins around this one — including the reporters who arrived with a full reproduction and a local patch, which is why these fixes did not have to be guessed.
|
|
470
110
|
|
|
471
|
-
##
|
|
111
|
+
## Author
|
|
472
112
|
|
|
473
|
-
|
|
113
|
+
If you have an opinion about the plugin, or an idea for another one, add me on WeChat:
|
|
474
114
|
|
|
475
|
-
|
|
476
|
-
| --- | --- | --- |
|
|
477
|
-
| 1 | 2026-08-18 | **Security & robustness**: random-token RPC auth + single connection; download admission (HTTP(S) only, absolute path, `downloadDir`-confined) with streamed caps (Content-Length early reject, 256 MB max); CDP timeout interrupts and click/type timeout key-release recovery; per-task sessions/allow-lists with agent-lifecycle auto-close; history redaction (typed text, replay/execute args not leaked); popup re-routing into the tab |
|
|
478
|
-
| 2 | 2026-08 | **Feature completion + tests + CI**: window title shows the task; flicker-free showView; snapshots/a11y pierce same-origin iframes & Shadow DOM; new `browser_wait`/`scroll`/`back`/`forward`/`key` tools; real `available()` probe; child-side downloads (temp file + atomic rename); constrained Electron lookup; JPEG/scaled screenshots; snapshot perf; test suite + CI |
|
|
479
|
-
| 3 | 2026-08 | **browser-bridge parity + review fixes**: `browser_a11y` a11y tree; 6 form-control tools (`browser_set_value`/`check`/`select`/`clear`/`get_value`/`refresh`); semantic `target` (css/text/xpath); `browser_scrape` structured extraction; independent BrowserWindow + real toolbar (address bar, back/forward/reload, tab strip) routed back into the session model; tool count **20 → 33**; CI switched to npm (no lockfile → pnpm cache broken), README corrections |
|
|
480
|
-
| 4 | 2026-08 | **DSH 0.1.1-rc.2 alignment + review fixes**: peer floor `^0.1.1-rc.2`; fixed `browser_type` dropping text with a target, `browser_key` Space missing CDP `text`, keyUp failure sticking a key, `browser_wait` same-origin URL mis-match, `.part` rename residue, `snapshotMaxElements`/`contentMaxChars` config wiring, missing type exports; 3 regression tests |
|
|
481
|
-
| 5 | 2026-08 | **Electron 44 compatibility**: `available()` is now side-effect free (no more triggering Electron 44 lazy download); `flushAuth` cookie-domain build fix |
|
|
482
|
-
| 6 | 2026-08 | **Windows handshake & tab lookup**: the Electron GUI process never receives piped stdin → RPC token now flows over **stdin + env var**; `browser_switch_tab`/`browser_close_tab` locate tabs (`locateTab`), `browser_close_tab` no longer fakes success, unknown ids error with the session's actual tab list. That round fell back to searching **every** session; it was tightened later — lookup is now **confined to the calling session** (a stale id must not close or switch another task's tab, or the human's), keeping only the convenience of accepting a bare uuid as well as `tab:<uuid>` |
|
|
483
|
-
| 7 | 2026-08 | **Toolbar interaction (Windows focus routing)**: keyboard input only reaches the focused view and the page view grabbed it, so the address bar could not receive input → added `wireFocusRouting` (clicking a view focuses it) + window refocus restores the last-clicked view; verified with real OS input probes |
|
|
484
|
-
| **0.1.16** | 2026-08-26 | **Release**: all seven rounds ship as **0.1.16** (build clean, 21/21 tests pass, `v0.1.16`) |
|
|
485
|
-
| 8 | 2026-08-27 | **DSH Desktop host-Electron reuse**: running inside an Electron process reuses the host binary directly; when the host runs the plugin in a child Node process, walk the process ancestry to find the host's Electron (PowerShell CIM on Windows, last resort only) — **DSH Desktop works with zero install**; error now hints per active profile; electron shim completed to fix the CI typecheck; docs updated |
|
|
486
|
-
| **0.1.17** | 2026-08-27 | **Release**: round 8 ships as **0.1.17** (build clean, 21/21 tests pass) |
|
|
487
|
-
| 9 | 2026-08-27 | **electron becomes a required dependency**: moved from optional peer into `dependencies`, so installing the plugin brings the electron package automatically (44+ downloads its binary lazily on first use); DSH Desktop still reuses the host binary; docs and error message updated |
|
|
488
|
-
| **0.1.18** | 2026-08-27 | **Release**: round 9 (electron as a required dependency) ships as **0.1.18** (build clean, 21/21 tests pass) |
|
|
489
|
-
| 10 | 2026-08-27 | **DSH-Store compatibility declaration**: added `dsh.compatibility.dshReleases` (rc.2/rc.1=compatible, rc.8=unknown) plus profiles/dsh range, clearing the store's auto-unlisting (HOLD) |
|
|
490
|
-
| **0.1.19** | 2026-08-27 | **Release**: round 10 (DSH-Store compatibility declaration) ships as **0.1.19** (build clean, 21/21 tests pass) |
|
|
491
|
-
| 11 | 2026-08-27 | **Self-hosted session self-healing after host death** (issue #5): after the host dies (DSH restart / checkpoint restore / crash), already-open sessions rebuild the host and retry on their **next call** — no more "browser host is not running", no more half-dead state; host-gone logging + a fake-child regression test added |
|
|
492
|
-
| 12 | 2026-08-27 | **resolveElectronPath excludes packaged apps** (issue #6): added `isBareElectron` (a sibling `app.asar` means packaged — never reused, all platforms incl. macOS bundle layout); bundled-electron filesystem probe goes first; `ELECTRON_PATH` override wins first; missing dist errors clearly (`npx install-electron` hint) |
|
|
493
|
-
| Hardening | 2026-08-27 | **Three review passes hardened**: concurrent-recovery double-rebuild race (child `createView` made idempotent), macOS bundle-path detection, `dispose()` vs `start()` zombie-child race triple-guard, pendingSocket leak, fully unit-tested lookup order (24→25 tests) |
|
|
494
|
-
| **0.1.20** | 2026-08-28 | **Release**: rounds 11/12 + hardening ship as **0.1.20** (build clean, 25/25 tests pass) |
|
|
495
|
-
| 13 | 2026-08-28 | **macOS/Linux untypeable inputs fix** (issue #7): the Windows focus routing from 0.1.16 (mousedown force-focus + window-refocus restore) shipped without a platform guard and fought macOS native click-to-focus, leaving login inputs untypeable → both handlers are now gated to `win32`; non-Windows restores native behavior |
|
|
496
|
-
| 14 | 2026-08-28 | **window.open/target=_blank opens a new tab** (issue #8): HTTP(S) popups no longer loadURL over the current view; they are handed to the parent and open as a **new tab in the same session window** — the opener page and its context survive (portal "workspace" jumps no longer 403) and the jump lands in session history; ungrouped views keep the fallback; non-HTTP popups still go to the system |
|
|
497
|
-
| **0.1.21** | 2026-09-02 | **Release**: rounds 13/14 ship as **0.1.21** (build clean, 25/25 tests pass) |
|
|
498
|
-
| macOS binary probe | 2026-09-09 | **Electron.app layout detection** (issues #9 / #14): `electronDistExe()` only probed `dist/electron(.exe)`, so macOS's `dist/Electron.app/Contents/MacOS/Electron` was never found → darwin candidate added to the shared platform probe (bundled + profile/anchor layers both benefit); regression test added |
|
|
499
|
-
| 15 | 2026-09-16 | **Toolbar parse-time SyntaxError** (issue #11): the inline script's `const bridge = window.bridge` collided with the non-configurable global installed by `contextBridge.exposeInMainWorld` (`HasRestrictedGlobalProperty`) → a **parse-time** early error, so not one line ran and the address bar, the four nav buttons, the tab strip and the error bar were all dead → the whole script is wrapped in an IIFE and the handle renamed `tb`, making the class of collision structurally impossible; 3 toolbar regression tests parse the snippets out of the **published artifact** and execute them in a `vm` under contextBridge semantics |
|
|
500
|
-
| 16 | 2026-09-16 | **Self-hosted trio fixes** (issue #10): (1) `browser_open` waits (bounded 5 s) for the new document to settle via a `performance.timeOrigin` fingerprint + `readyState`, so it no longer returns a titled-but-empty snapshot; (2) a new waiting `presentView` barrier (materialize the view, then showView, then a ping barrier; child dispatch is strictly serial) is required before `click`/`type`/`key` dispatch `Input.*`, which now fail loudly with `BROWSER_VIEW_NOT_PRESENTED` instead of faking success; (3) `createView` preloads `about:blank` (bounded 3 s) so a fresh view always has a live renderer (the step that wedged after a host restart); (4) `did-navigate` forces re-presentation, host commands are bounded at 20 s, child stderr plus exit code/signal go to `$DSH_HOME/logs/dsh-builtin-browser-host.log` (2 MB self-truncating), and `locateTab` accepts a bare uuid as well as `tab:<uuid>` |
|
|
501
|
-
| 17 | 2026-09-16 | **Screenshot savePath confined + localized download dir** (issue #13): `browser_screenshot` wrote straight to `writeFileSync` — anywhere the process could reach, silently replacing existing files (bypassing the read-only sandbox's write protection) → one shared `admitSavePath` gate for downloads AND screenshots (absolute, inside `downloadDir`, never overwriting an existing file), plus parent-directory creation for screenshots; the default download directory is no longer hardcoded to `~/Downloads` but probed in order: `downloadDir` → `XDG_DOWNLOAD_DIR` → `~/Downloads` / `~/下载` / `~/下載` → fallback (a Chinese desktop needs no configuration) |
|
|
502
|
-
| **0.1.22** | 2026-09-16 | **Release**: the macOS binary-probe fix (issues #9 / #14) and rounds 15–17 (issue #11 toolbar SyntaxError / #10 self-hosted trio / #13 screenshot savePath + download dir) ship as **0.1.22** (build clean, **35/35 tests pass**, tag `v0.1.22`) |
|
|
503
|
-
| Round 18 | 2026-09-20 | **Windows on-device trio + probe self-healing + locate verdicts** (measured on a real self-hosted `dsh web` host; defects ①–④ were all the "CDP answered success, the page received nothing" kind): ① Chromium's `CalculateNativeWinOcclusion` marks the plugin window HIDDEN while another window covers it — the page stops producing frames and **every** synthesized mouse/key event is dropped by the renderer (`CanReceiveInput()` false) while CDP replies `{}` → the child appends `disable-features=CalculateNativeWinOcclusion` before `app.whenReady()` (win32 only); ② `click()` had no leading `mouseMoved`, so the first click on a fresh view was routed away and lost → now move→press→release; ③ a fresh view holds no web focus, so the FIRST `browser_key` of a session vanished → new host `focus` op (optional `focus?()` on the view handle), `key()` focuses best-effort before dispatch and waits 80ms only when focus had to move (focus lands asynchronously; a key dispatched in the same turn is still dropped); ④ `available()` cached a FAILED Electron probe for the host's lifetime while provider selection runs once per process, so an Electron that arrived after DSH started was never adopted → successes stay cached, failures re-probe after a cooldown (`DSH_BROWSER_PROBE_RETRY_MS`, default 30s), and `resolveProvider()` now distinguishes "no provider registered" from "registered but reports itself unavailable" with the matching remedy; ⑤ **a failed locate was masked by the outer timeout** — the in-page locate script polls for its whole budget and answers only afterwards, while the outer wait used the SAME budget, so `browser: click timed out after 10000ms` won the race and the in-page verdict never got out; a css/xpath **parse error** also reported as "not found yet", polling a selector that can never become valid. Now a parse error is terminal and names itself (`invalid CSS selector "…" / invalid XPath …`), the outer wait gives the in-page answer 2 s of transport grace, and a miss reports the strategy the provider assumed (`by` defaults to `"by":"css"`) plus the time spent; `scrape`'s item selector fails the same way at once. Same call: before `click timed out after 10000ms`, after `element not found: {"value":"Learn more","by":"css"} (looked for 10000ms)`. 7 new regression tests (**47/47 pass**), 17/17 end-to-end steps against the real host, plus 4/4 locate-verdict steps |
|
|
504
|
-
| 19 | 2026-10-01 | **DSH 0.2 compatibility**: DSH moved to the 0.2 line (`@deepseek-ai/dsh@0.2.0-rc.2`, with `dsh-llm`/`dsh-tools`/`dsh-system-prompt` following to `0.2.0-rc.2`), while our declaration `>=0.1.1-rc.1 <0.2.0` shut 0.2 out → verified the plugin's (narrow) runtime dependency surface against a **real 0.2.0-rc.2 host** (`cordis` Context/Service, `dsh-tools` defineTool, `dsh-llm` HarnessError, `schemastery`) and found no breaking change: session, navigation, snapshot and the screenshot trio (outside-path refused / legal write / overwrite refused) all pass → peer ranges for the three dsh packages widened to `>=0.1.1-rc.2 <0.3.0`, `dsh.compatibility.dsh` widened to `>=0.1.1-rc.1 <0.3.0`, and `dshReleases` gained `0.2.0-rc.1`/`0.2.0-rc.2` = compatible |
|
|
505
|
-
| **0.1.23** | 2026-10-01 | **Release**: round 18 (PR #15: the Windows synthesized-input trio + Electron probe self-healing + locate verdicts) and round 19 (DSH 0.2 compatibility) ship as **0.1.23** (build clean, **47/47 tests pass**, tag `v0.1.23`) |
|
|
506
|
-
| Round 20 | 2026-10-01 | **Browsing history / settings panel / synthetic cursor / teardown**: (1) **persistent browsing history** (`history-store`: append-only JSONL beside the browser profile; capped at 5000 entries or 90 days, whichever comes first; a damaged line loses only itself) plus the new **`browser_visited`** tool (tool count **33 → 34**), reopening via `browser_open`; (2) a "Browser" section in Settings (hand-written client bundle registered into `settings.section` with `order: 60`, below the host's own rows) served by `GET/PUT /dsh-builtin-browser/settings` (same-origin guard, 64 KiB cap) over a `settings-store` document (per-field validation, unknown keys dropped, malformed file falls back to defaults) — **switches take effect immediately** because the provider reads the document on every use; (3) an in-page **synthetic cursor** (inline styles + Web Animations, so a page's `style-src` CSP cannot drop it; `buildTargetScript` now attaches the element centre as `__point`, which gives click / type / setValue / check / select / clear **a landing point**) — **its appearance means the agent has taken over that tab**; (4) **closing a window ends its session**: on `closed` the host releases every view's `webContents` (a BrowserWindow does not destroy child views, so each window would otherwise leak a renderer) and reports `viewClosed`; the provider ends that session and the seam gains `exists()` so the tool layer re-checks a cached session — the next call gets a **clean session** while browsing history and login state survive. 21 new tests (**68/68 pass**) |
|
|
507
|
-
| Round 20 addendum | 2026-10-01 | **Root cause of the invisible settings panel, plus crash diagnostics**: (1) On a real host the panel did not appear — the cause is that all three `cordis.patch.yml` rows were registered under **subpath** names (`dsh-builtin-browser/browser`), while the host's client-module scan resolves a row's specifier to a **package root** (`exactPackageSpecifier` returns `undefined` as soon as it sees a `/`), leaving the package with no row to read its `dsh.client` from. Fixed by adding an inert root row under the bare package name and giving the root entry `export const name` plus an empty `apply()`. Measured: boot entries 67 → **68**, the panel appears below "规则设定", and switches persist immediately. (2) Three host-log defects fixed: ISO timestamps, both paths plus their existence recorded before spawning, and a 2 MiB rotation that leaves a dated marker instead of wiping the file (which is how weeks of history vanished); the `exit` line gained `pid=` and `entryExists=` so "present at spawn, absent at exit" names an installation replaced underneath a running host. (3) Test pollution of the real log fixed: three spawn-based tests restored `DSH_HOME` in a `finally` while `dispose()` kills children asynchronously, so their synthetic exit lines landed in the operator's real log — now isolated at module scope. **70/70 tests** |
|
|
508
|
-
| **0.2.0** | 2026-10-01 | **Release**: round 20 (persistent browsing history / the "Browser" settings section / synthetic cursor / teardown semantics) and its addendum ship as **0.2.0** — tool count **33 → 34** (new `browser_visited`), plus a new "Browser" section in Settings. Build clean, **70/70 tests pass**, tag `v0.2.0` |
|
|
509
|
-
| Round 21 | 2026-10-01 | **The desktop is now carried by the official sidebar (one page for both parties)**: DSH Desktop is two layers — an Electron shell plus a **Node-mode host** where the plugin runs (no Electron API) — and 0.2 removed `electronViewHost`, leaving no view-carrying channel between host and shell → the plugin borrows a **loopback + token bridge** (installed into the shell's main process by `install.mjs`, idempotent, revertible with `--revert`) that hands it CDP access to the sidebar browser's guest. The result: **the page the agent works on is the page the human sees**, with no self-hosted Electron spawned and no second window. Measurement corrected three things we had assumed: the sidebar guest is created **lazily** (an un-navigated sidebar has no webContents at all); the address-bar route is unreliable (React's controlled input ignores synthetic keyboard events, and focus is taken back by re-renders), so it drives the sidebar's own **"restore last page"** control to force the guest out and then navigates purely over CDP; and the endpoint file must be **rewritten periodically**, or a reader gets the address of an exited shell |
|
|
510
|
-
| Round 22 | 2026-10-01 | **issue #16: an error-reporting path must not be fatal**: `notifyUserActionError` took the host method out and called it **unbound**, so the host's own first statement `void this.ready()` threw a TypeError, and that throw turned into an unhandled rejection inside an async catch → **the whole DSH host exited with 1**; and even with correct binding, `ready()` **throwing synchronously** once disposed escaped every `.catch`. Fixed by calling on the owner and containing every failure along the way, and by making `ready()` return a rejected promise (marked as handled). Four regression tests, including the failing toolbar action driven by the reporter's own `this`-reading stub |
|
|
511
|
-
| Round 23 | 2026-10-01 | **CVE-2026-84961 (undici)**: the CVE is real, but the suggested auto-fix **does nothing in this repository** — `pnpm.overrides` was written into `package.json`, and pnpm 10 no longer reads that field (measured: it prints a warning, ignores it, and the lock still resolved 7.29.0). Pinned `undici: 7.29.1` in **`pnpm-workspace.yaml`** (the new home for it) instead, staying on the same major rather than taking a jump that buys nothing. Impact clarified: the plugin never imports undici, and the published package ships no `node_modules` |
|
|
512
|
-
| Round 24 | 2026-10-01 | **Every last item of the requirements doc**: (1) **the vision strategy finally does something** — under `nonVisual` a coordinate click is **refused with an actionable alternative**, and tool descriptions lead with semantic targeting (the setting was stored, and read by nobody); (2) **non-visual output improved** — snapshots indent by `depth`, coordinates are opt-in (`coords: true`), empty states dropped, and `content(txt)` uses the browser's rendered text; (3) **the sandbox-boundary change stated** (both READMEs plus the settings panel); (4) `closeWithSession` / `autoExpandOnce` **actually work** (the bridge gained `closeSidebarBrowser` and `collapseSidebar`); (5) **each session owns its own sidebar tab**, and a release closes only its own; (6) history gained **keyword and originating-session** filters; (7) the cursor gained an **action bubble**. Six measured bugs fixed along the way (letter-by-letter animated text split into a column of letters, the first call after a restart always failing, three settings that were dead, a release closing somebody else's tab, and every setting silently dropped when the settings file carried a BOM) |
|
|
513
|
-
| Round 25 | 2026-10-01 | **Speed and structure**: measurement put the structural overhead of each command at **49.1 ms** (a fresh TCP connection with the token exchange at 24.8 ms, plus a liveness check before every command at 24 ms) against **0.2 ms** on a reused connection → now **one long-lived connection with serialised requests**, and liveness is settled by "the command failed, so rebuild" instead of being probed up front. The transport moved into its own module, `bridge-connection.ts` (`desktop-bridge-host.ts` 426 → 306 lines). Cursor: an unchanged position is no longer repainted, easing became 190 ms, and `forgetCursor` was added (the cache has to be dropped when the document is replaced, otherwise the pointer never comes back after a navigation) |
|
|
514
|
-
| Round 26 | 2026-10-01 | **Optional system Chrome / Edge**: settings now offer `bundled` / `auto` / `chrome` / `edge`, taking the same approach as Codex Browser Use — launch with `--remote-debugging-port=0`, read the port the browser writes into its own `DevToolsActivePort` file, and drive everything over CDP (through Node 22's built-in `WebSocket`, **zero new dependencies**). **The user's everyday data is never touched** (a separate profile); login state is kept or discarded according to `cookies.persist`. Precedence: explicit choice > desktop sidebar > self-hosting. (At the time the chosen browser was missing, a warning was logged and the bundled one was kept — later refined: `automatic` still falls back, but an explicit choice now reports the missing browser instead of silently substituting one.) |
|
|
515
|
-
| **0.3.0** | 2026-10-01 | **Release**: rounds 21–26 ship together (the desktop sidebar carrier, the optional installed browser, the whole of requirements doc v2). **99/99 tests pass**, tag `v0.3.0` |
|
|
516
|
-
| Round 27 | 2026-10-01 | **The system browser starts only when it is needed** (reported bug) plus teardown hardening: the entry used to `await launch()` while registering, so **installing the plugin popped a browser up** and even starting DSH dragged one along → construction is now **inert and the browser launches on first need** (concurrent first calls are merged into one startup). Self-review added two more edges: a released host **refuses to start** (otherwise it spawns a process nobody owns), and a release during startup **stops the poll and refuses to publish the client** (otherwise it leaves a dangling connection). Also added `tools/install-web-plugin.mjs`, which turns "change the pin → install → **repair the profile immediately** → verify" into a single command, and the machine username was removed from the CHANGELOG |
|
|
517
|
-
| **0.3.1** | 2026-10-01 | **Release**: round 27 (lazy startup + refusing to start after release) ships as **0.3.1**. **100/100 tests pass**, tag `v0.3.1` |
|
|
518
|
-
| Round 28 | 2026-10-01 | **Two "limits" were implementation limits**: reviewing the documented limitations showed they were never about the carriers. ① `browser_auth` called a native method only the self-hosted handle implements and refused otherwise — but **cookies are part of CDP** (`Storage.getCookies` / `Storage.setCookies`) and **all three carriers speak CDP**, so export and restore now go through it when no native method exists, **working on all three** (domain+path vs URL, the leading-dot spelling and seconds vs milliseconds are handled, and cookies that cannot form a usable URL are dropped). ② JPEG was denied to every CDP path because Electron's CDP JPEG encoder hangs — true for Electron, irrelevant to an installed Chrome/Edge — so a view now advertises `supportsCdpJpeg` and only then is the format passed through; the sidebar keeps PNG. ③ The tool description promised downscaling that existed only on the native path, so the CDP path now scales through `clip.scale` (reading `Page.getLayoutMetrics` first, and capturing unscaled rather than failing when the size is unavailable). ④ Two carrier differences that were never documented are now written down: **the sidebar does not report user-action events**, and **an installed Chrome/Edge uses a plugin-owned profile**. 11 new tests over the pure parts, **124/124** |
|
|
519
|
-
| Round 29 | 2026-10-01 | **Independent code review**: a separate reviewer read the whole tree and reproduced the findings. ① **`spawn` had no `error` listener** — a launch failure arrives as an asynchronous event, and without a listener Node takes **the entire DSH host process down**; it now fails the command instead. ② **`dispose()` during `start()`'s 250 ms poll** left the launched browser alive with nobody to kill it; the poll now kills it before stopping. ③ Three different failures all reported "did not expose CDP within 30s", in half a second; each now says what happened. ④ **An explicit `chrome`/`edge` choice was still overridden by the desktop sidebar**, because discovery never checked the channel — which also meant the "your browser is not installed" explanation could never appear, cancelling the previous round's fix. ⑤ `CdpClient.whenReady()` awaited an unbounded promise, so a dropped connection could hang a tool call forever. ⑥ `focus()` used `kill('SIGCONT')`, a no-op on Windows, while its comment promised to raise the window. ⑦ Removed an unreachable `brave` branch. **125/125** |
|
|
520
|
-
| Round 30 | 2026-10-02 | **The second independent review's 4 HIGH + 2 MEDIUM**: ① **read-only tools were in fact blocked by the allow-list** — `assertAllowed` exempted only `browser_restrict` itself, so a narrow restriction also locked away `browser_history` / `browser_reset_session` / `browser_auth` with no way back → an explicit read-only set (plus the tools that lift a restriction) now exists. ② **A cancelled navigation completed as success** — `navigate()` checked the signal once at the top while `settleDocument` returned quietly on abort, so the call finished with no exception, was recorded as success, and wrote the page into the persistent browsing history. ③ **The settings switch nothing read** — `credentials.allowRead` existed only in the settings store and the panel, so turning it off still let `browser_auth` export every cookie; both auth paths are genuinely gated now. ④ **A stale tab id could close someone else's tab** — `locateTab` searched every other session when the id was not local and reported success, so isolation was broken; it is confined to the calling session now (a bare uuid and `tab:<uuid>` are still both accepted). ⑤ With an empty `target.value`, `browser_type` fell back to the focused element (possibly a password field) and `browser_click` dropped the semantic target for coordinates, both reporting success → an empty value is an error. ⑥ `scrape`'s outer budget equalled the page-side deadline, so a page-side "no elements matched" verdict could never travel back → the outer call now leaves room for the transfer. **130/130** |
|
|
521
|
-
| **0.4.0** | 2026-10-02 | **Release**: rounds 27–31 ship together — the theme is **separating real limits from implementation limits**: `browser_auth` works on all three carriers, an installed Chrome/Edge can encode JPEG, and downscaling works everywhere; alongside fixes for defects that crashed the host, silently did the wrong thing, or hung forever. **130/130 tests pass**, tag `v0.4.0` |
|
|
522
|
-
| Round 31 | 2026-10-02 | **Third review round + atomic settings writes**: ① **settings writes go through a temporary file and a same-directory rename** (the rename is the commit point, so a crash or a full disk cannot leave half a JSON document that the reader then silently replaces with defaults), and a failed write cleans up only the temporary and **no longer publishes the new value as cached**; ② fixed an `el` never rebound after a filter (a selector matching several elements wrote the value onto the first one while reporting success); ③ `handle.focus()` got a 5 s budget (the only unbounded await in the file, and it runs before typing); ④ `browser_wait`'s poll leaked an abort listener every 200 ms (~150 for a 30 s wait) and now reuses `delay()`; ⑤ with `autoExpandOnce` off, `showActive` treated "show the view" and "fold the carrier" as exclusive branches and returned after the second — on carriers with no collapse capability the view was **never shown at all**, so the agent drove a view the human could not see and every screenshot failed with "view not painted" (an error that blamed the page for a setting); ⑥ assorted `bridge-connection` / `scrape` timeout clean-up. **130/130** |
|
|
523
|
-
| **0.4.1** | 2026-10-02 | **Release**: issue #21 — **the system browser carrier (chrome/edge) neither reconnected nor restarted after its process died**, so every `browser_*` call hung for 30 seconds and only restarting DSH helped. Launch-time failures were handled in the previous round, but death **after a successful launch** was never observed: there was no `exit` listener, `this.client` was never cleared once assigned, and `ensureClient()` kept returning that dead connection forever. Fixed along the two failure shapes reported: process death → the child is observed throughout, its exit clears the cache and the next call relaunches; socket death with the process alive → the cached client's liveness is checked before use; process and socket both alive but the command never answered → **a failed command drops the client**. Commands sent on an already-closed socket now **fail immediately and say why**, instead of waiting out the budget and reporting a "timeout" that points nowhere near the real cause. **132/132 tests pass**, tag `v0.4.1` |
|
|
524
|
-
| Round 32 | 2026-10-03 | **Output-cost audit + independent verification** (shipped in **0.4.2**): ① **`browser_a11y`**: the default node count went **500 → 150**, indentation two spaces → one, `states=[…]` prints omitting the literal `enabled` (unchecked, collapsed and others are still printed) (**no `states=` means enabled**) and the trailing hint is short; ② **snapshots and the a11y tree now hand you a citable selector**: `{#id}` / `{[name=x]}` — the page-side script had always computed it and the tool layer threw it away, while the targeting tools accept only css/text/xpath, leaving the model to guess at text or fall back to a screenshot — and the field is **declared in the output schema**, without which `additionalProperties: false` rejects the whole result, which is exactly how the gap was found on the first real call; ③ **`browser_content` caps are per format** (html/json 50 000, the rest 20 000, overridable with `maxChars`), **`json` no longer returns `{}`** (a DOM node serialises to nothing) but `{"html":…,"tag":…}`, and the truncation notice says how to narrow; ④ `browser_scrape` returns compact JSON; ⑤ `browser_execute`'s return value is capped at 50 000 characters with the cut stated; ⑥ `browser_history` shows the last 20 entries without parameters and leads with the total — `verbose: true` brings the parameters (the parameter had been declared and never read); ⑦ `browser_visited` timestamps are minute-granular and the session tag prints once per change, shortened to 8 characters; ⑧ **`browser_auth`'s `flush` exports only the cookies of the site the current page is on** (it used to be the machine-wide shared jar) and **refuses rather than widening** when the scope is unknown, while `restore`'s count now reflects what was really submitted (`Storage.setCookies` returns nothing, so it was always 0 and read as failure) and **`browser_auth` left the read-only set** — its `restore` writes cookies to arbitrary domains, an action rather than an observation; ⑨ **atomic settings writes**, a settings cache that returns before reading the file, a browsing history that no longer rewrites the whole file on every navigation (648 KB per navigation once past the cap, measured at 17.5–20.6 ms), the `browser_content` page-script syntax error that broke all four formats, `browser_visited` printing blank rows, bridge replies correlated by request id, and `tools/install-web-plugin.mjs` refusing to damage a profile it cannot repair. Two unbounded host awaits (self-hosted capture/download, and cookie export/restore) got timeouts. **132 → 134** |
|
|
525
|
-
| Round 33 | 2026-10-03 | **The system browser carrier: actually usable after it dies** (shipped in **0.4.2**): ① **one failed command no longer tears down the connection** — a CDP protocol error means the browser answered and refused that one command, so the connection is healthy; ② **a restart now drops the stale session mappings**, not just the cached connection and child, because every command the replacement received carried a session id issued by the dead process — that is the reported "it restarts, and every call fails forever"; ③ **a slow page is not a dead browser** — deciding liveness by error text turned the 5 s/15 s outer timeouts into "the browser is dead" and killed the window you were using, tabs and half-filled forms with it; the state is now three-way — socket closed → discard, answered and refused → keep, **no answer → mark suspect** and probe once with `Browser.getVersion` on the next call; ④ **after the kill it waits up to 3 seconds for a real exit** (on Windows the kill is asynchronous, and not waiting handed the new process's command line to the old instance, whose resulting exit was misreported as "check that the path is a runnable browser"); ⑤ **only the launch's own `DevToolsActivePort` counts** (a killed browser never cleans it up, and the stale file made the host connect to another Chromium or a `node --inspect`); ⑥ **the port-discovery regression test now uses a fixture that really speaks CDP** (the old `.cmd` stub was refused by Node's spawn on Windows, and all three assertions were satisfied by that single synchronous failure — CI was green while the exit handling never ran once). **134/134** |
|
|
526
|
-
| Round 34 | 2026-10-03 | **Operator-level action switches (the real entry point)** (shipped in **0.4.2**): the settings panel gains "What the agent may do" — **run scripts in the page** / **download files to disk** / **write login state**. Switching one off refuses that capability outright (`BROWSER_EXECUTE_DISABLED` / `BROWSER_DOWNLOAD_DISABLED` / `BROWSER_AUTH_WRITE_DISABLED`). What separates them from `browser_restrict` is **who can change them**: that one is set by the model and can be lifted by the model, while these live in the settings document, which no tool can write. Also fixes a read/write mix-up: restoring login state (`browser_auth restore`) answered to the READ switch and reported "reading cookies is switched off", so an operator who allowed reading had no way to refuse writes — it now answers to the write switch. **148 → 157** |
|
|
527
|
-
| Round 35 | 2026-10-03 | **DSH 0.2.1 adaption** (shipped in **0.4.2**): the host moved to `0.2.1-alpha.1` (266 commits past rc.2), so every host-facing dependency was checked item by item — cordis `Context`/`Service` (vendor 4.0.5-alpha.1), `dsh-tools` `defineTool` and every `DefineToolOptions` field, `dsh-system-prompt` `section()`, `dsh-llm` `HarnessError`, the schemastery default export, and on the client side `slots.inject('settings.section')` with `locale.register`/`bind`. **None of the shapes changed** (the client calls match DSH's own plugin word for word). Added `scripts/verify-host-compat.mjs`: it assembles a host from the `@deepseek-ai/*` packages **actually installed** in a profile, loads this build, registers 34 tools through the host's own `defineTool`, and calls `browser_session` plus `browser_a11y` over a fake provider — **all green**. `dsh.compatibility.dshReleases` now records `0.2.1-alpha.1 = compatible`. One semantic trap recorded too: that range matches no prerelease under **default semver semantics**, while DSH evaluates with `includePrerelease: true`, so these hosts are in range in practice |
|
|
528
|
-
| Round 36 | 2026-10-04 | **An external audit, worked through** (shipped in **0.4.2**): eight findings with line numbers and evidence, each verified against the code and fixed in three batches — (1) **storage and settings**: a settings document that EXISTS but cannot be parsed no longer resolves to "everything on" (a torn write used to reopen every capability the operator had switched off), a gate field of the wrong type now reads as OFF, `history.jsonl` uses the settings store's atomic write for its whole-file rewrite, and `contentMaxChars` — assigned but never read since the caps became per-format — works again; (2) **lifecycle**: the browser child gets the socket close first and time to `app.quit()` its profile out, and only then is killed (the old comment claiming `app.exit(0)` "flushes the profile" was false, and on Windows `kill()` is TerminateProcess, which lands before any handler runs), and the child no longer calls `app.exit(0)` either; (3) **usability**: click/type/fill resolved targets against the top document only while the snapshot and `waitFor` walked shadow roots and iframes — **visible, waitable, unclickable** — so the resolve scripts now collect every root; on Windows the per-user `%LOCALAPPDATA%` install locations are searched, and the "exited immediately (exit code 0)" message no longer points at a broken path when it means the browser handed the request to an instance already holding its profile. Two places that silently wreck someone else's work are fixed as well: `install-web-plugin.mjs` refuses to overwrite a `link:` pin with a registry version, and a failed adopt disposes its half-built host. **Two findings I do not agree with**: a zero-hit grep for `window-all-closed`/`before-quit` says nothing about behaviour (Electron's default is already right), and a `requestSingleInstanceLock` would break normal restarts rather than fix orphans. |
|
|
529
|
-
| **0.4.2** | 2026-10-04 | **Release**: rounds 32-36 in one release — the theme is **making invisible errors visible**. (1) **Output cost**: what every call pays dropped to a fraction (a11y defaults to **500 → 150** nodes, indentation and `states` carry only information, `content` caps are per format, `history` shows 20 entries by default). (2) **Action switches**: three capability gates held by the **operator** that no `browser_*` tool can change (run page scripts / download to disk / write login state), kept clearly separate from `browser_restrict`, which the model sets and lifts itself. (3) **Recovery semantics completed**: a dead system-browser process now takes its sessions down with it (previously the browser could restart while the view still held the dead one's session id — "fails fast and keeps failing"), a slow page is no longer mistaken for a dead browser, and `kill` waits for the process to actually exit. (4) **An external audit, worked through**: a settings document that EXISTS but cannot be parsed is now fail-closed (a torn write used to reopen **every** capability the operator had switched off, and the next save wrote those defaults to disk), `history.jsonl` is written atomically, the client panel — read at request time — no longer fails to open because the host lacks a field, click/type/fill reach shadow-DOM and iframe elements the snapshot already shows, and Windows user-level installs (`%LOCALAPPDATA%`) are searched. **171/171 passing** (172 total, 1 skipped by platform), tag `v0.4.2` |
|
|
530
|
-
| Round 37 | 2026-10-04 | **Architecture refactor + an external review worked through** (no version bump; these changes ship with the next release): the theme is **one fact, no longer hand-copied in four places** — both published bugs (`browser_a11y`'s `selector`, `browser_screenshot`'s `width`/`height`) came out of the same mechanism, "the provider computed it, the schema declared it, and the tool layer's hand-written allow-list dropped it", and two full test runs stayed green because **no test ever ran a tool**. (1) **One field set, declared once**: new `src/tool-browser/element-fields.ts` (the element/node field tables, plus the item schema and the projections derived from them); `browser_snapshot`'s schema joined `browser_open`'s, which the earlier pass had done only half of; and new `tests/element-fields.test.mjs` (7 cases: schema ⟷ tables equal in both directions, the projection carries every declared field, the projection drops undeclared fields, and all 34 registered tools are walked to assert that what the schema requires the tool really produces). (2) **Page-script parsing goes from 4/16 to full coverage**: new `tests/page-scripts-parse.test.mjs` scans the source directly (the provider, `host-main`'s toolbar and download, the virtual cursor, the sidebar bridge) instead of relying on driving a behaviour to capture a script — the cursor, download and bridge scripts had never been parsed at all. (3) **Two real self-hosted bugs**: `materializeOnce`'s "reset on failure" was dead code (it stored a derived promise and compared the original, so the branch could never be taken), letting a child that is alive but not answering **poison that handle permanently** while the retry its comment promised was unreachable; and `fail()` cleared `kill()`'s 500 ms backstop — a dead socket is not a dead child — so it now clears only on an observed exit or the release path. (4) **The seam stops lying**: the handle's `download`/`flushAuth`/`restoreAuth`/`capture` and the host's `notifyUserActionError` were promoted from structural casts to interface declarations (so a new host written against the interface no longer gets a silent no-op), `capture`'s contract gained the size the `browser_screenshot` schema had always declared, and 8 empty methods that existed only to satisfy the interface plus 2 host-level `focus()` methods with no caller anywhere were deleted. (5) **Screenshot size is no longer CDP-carrier-only**: the earlier fix did only the CDP half — the native `capturePage` path never passed its size back, so `width`/`height` were **always missing on the default self-hosted carrier** — and the child now measures and reports them after any downscale, wired through `capture()`'s type, `screenshot()`'s return type and the native branch (reported when the capture was measured; `width?`/`height?` were optional all along). (6) **Settings and storage**: a **gate-holding section** that is present but not an object (`actions: false`, `null`, a string) is no longer treated as absent (which took each gate's permissive default) — every gate inside it now reads as **off**; and `history.maxEntries`/`maxAgeDays` stopped being dead knobs (parsed and persisted, but never handed to the `HistoryStore`), the reader now passing the live values to `updateLimits()`. (7) **System-browser carrier**: the `'exited'` error no longer reads every exit code as "handed the request to an instance holding the profile" — that story holds only for code 0, while a crash code sent people looking for a leftover process that was not there; the launch poll **checks `signalCode` as well** (a browser killed by a signal never sets `exitCode`, so checking only that let an instant death run out the full 30 seconds and report "did not expose CDP"); and the port probe's `fetch` gained an `AbortSignal`. (8) **RPC vocabulary**: new `src/browser-electron/rpc-ops.ts` gives the child's switch and the parent's senders one list of op names, with `tests/rpc-ops.test.mjs` reading both sources and asserting they agree (it caught two ops named from memory that the child does not handle). **183/183 passing** (**184** total, 1 skipped: creating a file symlink on Windows needs elevation) |
|
|
531
|
-
|
|
532
|
-
> The npm badge at the top is the authority on the registry's latest version (currently `0.4.2`). After a desktop upgrade, re-run `node desktop-bridge/install.mjs` once; the web side has no such step — see [Updating (the two hosts differ)](#updating-the-two-hosts-differ).
|
|
533
|
-
|
|
534
|
-
## Acknowledgements
|
|
535
|
-
|
|
536
|
-
Special thanks to the [DeepSeek Harness repository](https://github.com/deepseek-ai/deepseek-harness) and the DeepSeek AI team: the seam, the tool runtime, and the plugin system this plugin builds on all come from that project.
|
|
537
|
-
|
|
538
|
-
Thanks as well to [Cordis](https://github.com/cordiverse/cordis) for the plugin foundation, and to everyone in the community who discussed, tested, gave feedback, and built plugins.
|
|
539
|
-
|
|
540
|
-
## A Note from the Author
|
|
541
|
-
|
|
542
|
-
Have feedback about this plugin, or an idea for another plugin you'd like built? Feel free to reach out on WeChat:
|
|
543
|
-
|
|
544
|
-
**wx: `hui13866591135`** (please mention this project when sending the friend request)
|
|
115
|
+
**wx: `hui13866591135`** (please say what it is about when you add me)
|
|
545
116
|
|
|
546
117
|
## License
|
|
547
118
|
|
|
548
|
-
MIT License
|
|
119
|
+
[MIT License](LICENSE).
|
|
549
120
|
|
|
550
|
-
>
|
|
121
|
+
> A community plugin for DeepSeek Harness, not an official DeepSeek product.
|