Install
Inside DeepSeek Harness, with dsh-market
dsh plugin --profile web add dshmarket
Or from the command line
dsh plugin --profile web add dsh-builtin-browser
Installing runs third-party code with your own permissions — it can read your files, use your credentials and reach the network. Review the source first, and pin a commit (github:owner/repo#sha) when you can.
README
Documentation
| Goal | Entry |
|---|---|
| Why a shared real browser, and how it differs from headless approaches | Why a shared real browser |
| Installation, configuration, day-to-day use | User guide |
| All 34 tools: parameters, output, examples | Tool reference |
| How the seam / provider / tools layers and self-hosting work | Architecture |
| Documentation index and README split | Docs index |
What is this
dsh-builtin-browser adds browser capability to DeepSeek Harness:
- A real page, not a relay: the page is carried by a real browser — on the desktop it is the one in the official sidebar, on plain
dsh webit is a window the plugin launches, and either can be switched to an installed Chrome / Edge in settings. The human sees exactly what the agent is doing and can take over at any time; - One page for both parties (desktop): the page the agent works on is the same page the human sees — no longer a window you cannot see;
- Works out of the box: the carrier is chosen automatically (the desktop sidebar first, otherwise self-hosting), and plain
dsh webneeds no extra configuration at all; - One plugin, one toolset: after install the agent automatically gets 34
browser_*tools (open, a11y tree, wait, semantic/coordinate interaction, scroll, back/forward, batch and single-control form filling, keys, structured scraping, screenshot, download, auth management…).
In one sentence: installing the plugin gives you a real browser that is shared with the user and drivable by the agent.
Quick start
Install the plugin into the profile of the host you want to use. Installing it on both is fine — one codebase, one copy per profile.
Web (plain dsh web)
# install from npm
dsh plugin --profile web add dsh-builtin-browser
# or from a checkout (one plugin, one repository)
dsh plugin --profile web add <path-to-this-repo>
Desktop (DSH Desktop)
# 1. install into the desktop profile
dsh plugin --profile desktop add dsh-builtin-browser
# 2. desktop-only step: let the plugin drive the official sidebar's page
node <path-to-this-repo>/desktop-bridge/install.mjs
Step 2 is not optional, and it comes back every time: a desktop upgrade replaces
resources/app/, and the bridge goes with it; a plugin update needs the same re-run. With no bridge the plugin falls back to opening its own separate window — nothing breaks, you just lose "one page for both parties". The web side has no such step.
Updating (the two hosts differ — see Updating (the two hosts differ))
| Host | Steps |
|---|---|
| Web | update the profile dependency → restart dsh web |
| Desktop | update the dependency → re-run node desktop-bridge/install.mjs → restart DSH Desktop |
After install the agent can use the browser tools, e.g.:
| What you want | Tool | Notes |
|---|---|---|
| Open a page | browser_open |
Opens a URL and returns a numbered snapshot |
| Understand a page | browser_snapshot |
Numbered inventory of inputs/buttons/links to target |
| Operate a page | browser_execute |
Runs JS in the page (native setters, framework-friendly) |
| Fill a form | browser_fill |
Fills many fields in one call, optional submit |
| See the page | browser_screenshot |
PNG capture, optionally saved for a vision model |
See the full list in Tool reference.
Main features
Why this plugin
- Install-and-use, zero config: no desktop shell or extra startup step required; on plain
dsh webit self-hosts an Electron window and thebrowser_*tools just work. - 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.
- 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.
- Testable, replaceable architecture: the provider talks to Electron through the
ElectronBrowserViewHostseam, so a future headless relay provider can serve remote deployments without touching the tool layer.
Tool reference
| Tool | Purpose | Guard |
|---|---|---|
browser_open |
Open a URL (optionally in a new tab); returns a page snapshot | ✅ |
browser_wait |
Wait for page load (optional expected URL / CSS selector), returns readiness | – |
browser_snapshot |
Numbered inventory of interactive elements (inputs/buttons/links; pierces same-origin iframes and Shadow DOM) | – |
browser_a11y |
Accessibility tree: semantic role/name/value/states + coordinates per interactive node (pierces same-origin iframes and Shadow DOM) | – |
browser_execute |
Run JS in the page; args arrive as arguments[0..n] |
✅ |
browser_visited |
Read the persistent browsing history (visited pages, filterable by domain / capped); reopen with browser_open |
– |
browser_content |
Fetch the page as html / markdown / txt / json (selector, maxChars, timeoutMs) | – |
browser_click |
Click a semantic target (target: css/text/xpath, scrolls into view and clicks center) or viewport coordinates (vision-located) |
✅ |
browser_type |
Type text (optionally focusing a target element first; CDP Input.insertText) |
✅ |
browser_key |
Press a named key (Enter/Tab/arrows/Home/End…) | ✅ |
browser_scroll |
Scroll the page (pixel deltas / selector / top-bottom) | ✅ |
browser_back |
One step back in page history (no-op at the start) | ✅ |
browser_forward |
One step forward in page history (no-op at the end) | ✅ |
browser_refresh |
Reload the current page (like a browser refresh button) | ✅ |
browser_fill |
Batch form fill (selector/name/label matching, controlled inputs, selects, checkbox/radio, optional submit) | ✅ |
browser_set_value |
Set one control's value (target-located; native setter + input/change, React-controlled friendly) |
✅ |
browser_check |
Check/uncheck a checkbox or radio (target-located) |
✅ |
browser_select |
Select an option of a <select> by value/text/index (target-located) |
✅ |
browser_clear |
Clear an input/textarea/contenteditable, or uncheck (target-located) |
✅ |
browser_get_value |
Read an element's current value for verification (target-located) |
– |
browser_scrape |
Structured extraction: container selector + field map (selector@attr), static CSS only, CSP-safe |
– |
browser_screenshot |
Capture, optional fullPage, savePath, JPEG (format/quality) and scaling (maxWidth/maxHeight); savePath shares the download gate (confined to downloadDir, never overwrites) |
– |
browser_list_tabs |
List the session's tabs | – |
browser_switch_tab |
Switch to a tab by id (also switches the visible view when self-hosted) | ✅ |
browser_close_tab |
Close a tab by id; closing the active tab activates the next | – |
browser_reset |
Close all tabs of this task, back to one blank tab | ✅ |
browser_session |
Show this task's browser session and tabs | – |
browser_reset_session |
Close and rebuild this task's browser session | ✅ |
browser_history |
Operation log (newest last), with per-step success/error and result summary | – |
browser_replay |
Replay one step by sequence number (navigate/execute/click/type) | ✅ |
browser_download |
Download an HTTP(S) URL with session cookies to a local file (absolute savePath inside downloadDir, never overwrites, 256 MB cap) |
✅ |
browser_auth |
Export/restore cookies (login persistence, self-hosted) | ✅ |
browser_challenge |
Detect a human-verification challenge (CAPTCHA / Cloudflare / reCAPTCHA / hCaptcha / Turnstile) | – |
browser_restrict |
Restrict allowed browser actions (allow-list; empty list lifts it). Soft guardrail — the model can lift it itself; not a security boundary | – |
"Guard" column: ✅ actions are governed by the
browser_restrictallow-list; read-only tools (snapshot/content/screenshot/list_tabs/session/challenge/history) are never blocked.
Waiting for the page
browser_openand navigation already wait (bounded) for the new document to parse (readyStateplus 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, callbrowser_waitbeforebrowser_snapshot— passurl(what you opened) and an optionalselector, and wait forready: true— otherwise you snapshot the old document, a white screen, or an empty element list.- 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, sobrowser_clickworks directly. DOM selectors are frame-scoped — reach them viaiframe.contentDocumentinbrowser_execute.
Semantic targets and the a11y tree
browser_a11yis the best way to understand a page: every interactive node carries its semantic role (button/textbox/checkbox…), accessible name, current value, states (enabled/checked/expanded…) and coordinates — click/type them directly.browser_click/browser_typeaccept atarget:{by: css|text|xpath, value, index?}—textmatches 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.- Use the single-control tools for one field (
browser_set_value/browser_check/browser_select/browser_clear/browser_get_value),browser_fillfor batches, andbrowser_scrapefor structured list extraction.
Operating discipline (click/fill)
- Prefer DOM semantics over coordinates: submit forms with
form.requestSubmit(); click withelement.click(); coordinate clicks are the last resort. - Target the right element: pages often have hidden duplicates (e.g. mobile buttons); filter visible elements with
browser_execute(getBoundingClientRect()w/h > 0,getComputedStylenotdisplay:none), then take coordinates. - Click right after taking coordinates: do not insert other operations in between (filling/scrolling moves elements and invalidates old coordinates).
- Verify before clicking: use
document.elementFromPoint(x, y)to confirm the coordinate hits the intended element (button/link), then perform the real click. - DPR awareness: CDP input uses CSS pixels; on high-DPI screens calibrate with
elementFromPointinstead of guessing coordinates.
Configuration
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 resolves a row's specifier to a package root — a row named after a subpath is skipped, and then the settings panel would never appear) plus three functional rows (browser / browser-electron / tool-browser). Per-row config:
| Row | Key | Type | Default | Description |
|---|---|---|---|---|
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 |
browser-electron |
httpOnly |
boolean | true |
Allow HTTP(S) navigation only; other protocols (e.g. file:/data:) rejected (BROWSER_NAVIGATION_BLOCKED) |
browser-electron |
snapshotMaxElements |
number | 60 |
Max snapshot elements before truncation |
browser-electron |
contentMaxChars |
number | 100000 |
Default content character cap |
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 |
tool-browser |
timeoutMs |
number | 60000 |
Cooperative tool timeout (ms) |
tool-browser |
tabTools |
boolean | true |
Register tab-management tools (browser_list_tabs etc.) |
How it works
agent (browser_* tools)
→ ctx.browser (seam, dsh-builtin-browser/browser)
→ dsh-builtin-browser/browser-electron (provider)
→ ElectronBrowserViewHost ← the same seam, implemented once per carrier
① desktop sidebar via bridge → shell main process → webContents.debugger (CDP)
② installed Chrome/Edge via WebSocket → CDP
③ self-hosted Electron via loopback TCP JSON-RPC → child process → CDP
- Seam (
browserrow): provides thectx.browserservice — provider registration, session lifecycle, error codes — decoupled from any implementation. - Provider (
browser-electronrow): knows only the oneElectronBrowserViewHostseam (create/destroy/show,sendCommand), so switching carrier touches neither the tools, nor the history, nor the cursor, nor the teardown logic. - Tools (
tool-browserrow): the 34 model-facingbrowser_*tools, maintaining one browser session per calling task (DSH session).
Self-hosted mode: without a desktop shell, the plugin spawns its own Electron child process (host-main.js) and drives it over loopback TCP JSON-RPC. The RPC is authenticated with a random per-spawn token delivered over both stdin and an environment variable — on Windows the Electron GUI process never receives piped stdin, so the env fallback keeps the handshake reliable. The child auto-restarts after a crash; the plugin prefers its own bundled electron package — packaged app executables (e.g. DSH Desktop.exe) are never reused as the spawnable binary, which would launch the app itself and exit immediately; screenshots prefer Electron's native capturePage (CDP capture can hang with multiple views in the window); the Electron lookup order follows below (33.x has a compositor defect; ≥ 40 recommended; the electron 44+ package no longer downloads its binary at install time — if it is missing on first use, the tool errors and tells you to run npx install-electron first, needs network).
The self-hosted browser IS a real browser: every task (DSH session) gets its own browser window with a full toolbar — address bar, back/forward/reload buttons, and a tab strip (new/switch/close tabs). A human can use it exactly like Chrome: type a URL in the address bar (https:// is added automatically), click tabs, open new ones. Keyboard focus follows your clicks — click the address bar to type, click the page to interact (Windows focus routing; fixes the case where clicks did not move focus and the address bar could not receive typed URLs). Human and agent actions feed the same session model (same tabs, history, and navigation); the window title always shows the task label plus the page title/URL, and views follow the window size on resize. A window closes automatically with its session when the task ends.
Electron lookup order: ① ELECTRON_PATH (explicit override, wins first) → ② the electron package bundled with the plugin (filesystem-only probe, never triggers the 44+ lazy download; covers both node_modules and pnpm-store layouts) → ③ the newest among DSH install anchors and pnpm virtual stores → ④ reuse the host binary when the current process is a bare Electron (dev mode) → ⑤ walk the process ancestry for a bare Electron host (PowerShell CIM on Windows, last resort only). Packaged apps (e.g. DSH Desktop.exe) are never reused — they cannot be spawned with a script argument, and misusing them exits instantly (issue #6); when nothing is found a clear error tells you what to do (including the npx install-electron hint).
Division of labor with the desktop shell
The plugin picks a carrier automatically, and the setting can override it (four in total):
① Desktop: drive the official sidebar's page (one page for both parties)
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.
The result: the page the agent works on is the page the human looks at. The plugin no longer spawns its own Electron and no second window appears.
Install that bridge (it modifies an installed desktop app, so it is replayable):
node desktop-bridge/install.mjs # idempotent; backs up main.js.before-bridge on first run
node desktop-bridge/install.mjs --revert # roll back
Re-run
install.mjsafter every desktop upgrade — the upgrade replacesresources/app/and takes the bridge with it. With no bridge the plugin falls back to self-hosting: nothing breaks, you just get a separate window again.
⚠️ What the sandbox boundary change means
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:
- the agent can read the content of any page you open in the sidebar (that is precisely what "one page for both parties" means);
- the agent can read the cookies and login state in that partition, and
browser_authcan export them (controlled by a setting);- 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).
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 the agent stops reading cookies and login state, 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.
② Use the browser you already have (Chrome / Edge)
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).
Your data is not touched: the plugin launches it with a separate profile ($DSH_HOME/dsh-builtin-browser-host/<chrome|edge>-profile). Your everyday windows, bookmarks and logins are never opened, locked or modified, and closing the plugin never closes your browser.
What happens to login state:
cookies.persiston (default) → that fixed profile is kept, so you stay signed in across DSH restarts, andbrowser_authcan still export/restore its cookies.cookies.persistoff → a throwaway profile each time, deleted when the browser is released; no login trace is left behind.- 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.
③ A shell that provides electronViewHost (older desktop shells): that view is used directly.
④ No shell at all (plain dsh web): self-hosted — the plugin spawns the Electron it ships.
The visible view and column layout always belong to the host shell; the plugin owns the seam, the provider and the tools. Across all carriers the toolset, browsing history, settings panel, synthetic cursor and teardown rules are identical — only the carrier of the page differs.
Precedence: an explicitly chosen installed browser > the desktop sidebar > self-hosting. If the chosen browser is missing or fails to start, a warning is logged and the bundled browser is kept — it is never silently swapped for something else.
Requirements
- DeepSeek Harness (dsh) with the matching profile (
web/desktop, etc.) - 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-electronfirst, needs network):ELECTRON_PATHcan point at another binary explicitly (highest priority);- DSH Desktop: 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); the bundled electron is used directly, and dev-mode bare Electron hosts are still reusable; - plain
dsh webself-hosted: uses the bundled electron package directly
Verified versions
| Component | Version |
|---|---|
| DeepSeek Harness (dsh) | 0.2.0-rc.2 (peer range >=0.1.1-rc.2 <0.3.0) |
| Electron | 44.0.0 (≥ 40 recommended; 33.x has a compositor defect) |
| Node.js | 22.20.0 |
| Installed Chrome / Edge (optional carriers) | 154.0.8037.58 / 154.0.4258.37 |
| dsh-builtin-browser | 0.3.0 |
| OS | Windows 10 (10.0.26200) |
The plugin declares
electron >= 30; it has only been verified on Windows (macOS/Linux untested, not yet promised).
Updating (the two hosts differ)
The plugin has one installation per host, and the two are updated separately — updating one does not update the other.
Desktop (DSH Desktop)
- 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. - 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:
With no bridge the plugin falls back to self-hosting (one extra separate window) — nothing breaks, the tools stay available.node desktop-bridge/install.mjs # idempotent; refreshes the module if already installed node desktop-bridge/install.mjs --revert # roll back - 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.
- Upgrading the desktop app itself does not carry the plugin along; update it as described above.
Web (dsh web)
- The plugin is a dependency of the web profile (
$DSH_HOME/profiles/web); restartdsh webafter updating. - 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), runnpx install-electrononce to fetch it. - It can equally be pointed at an installed Chrome / Edge in settings — an option that behaves the same on both hosts.
- Update it the same way you installed it (npm package
dsh-builtin-browser, the GitHub repowqty123/dsh-browser, or a local directory).
What is the same on both
- 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). - 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>-profileused for an installed browser. - 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.
Known limitations
- JPEG screenshots are available only on the self-hosted native path (
capturePagetoJPEG); the desktop shell's CDP fallback stays PNG (CDP JPEG hangs on Electron 43). - Self-hosted captures prefer Electron's native
capturePage(CDPcaptureScreenshotcan hang with multiple views in the window); the target tab is raised before capturing. fullPagecapture is flaky under software compositing on some hosts.- CAPTCHA cannot be solved automatically: snapshots flag detected challenges; ask the human to complete it in the shared window instead of retrying.
- Private mode (
privateMode) is not implemented: it needs Electron session partitioning, which is host-layer territory; this plugin does not promise it. browser_downloadfetches in the page context (keeps logins) and is subject to same-origin/CORS constraints; HTTP(S) targets only;savePathmust be absolute and insidedownloadDir(default: the system Downloads folder, auto-detectingDownloads/下载/下載andXDG_DOWNLOAD_DIR; override withdownloadDir) and never replaces an existing file;browser_screenshot'ssavePathgoes 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).- 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.
browser_restrictis a soft guardrail against accidental actions, not a security boundary: the model can lift it itself.- 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. - The
browser_authcookie round-trip does not preservehostOnly/sameSite(host-only cookies come back as domain cookies); it is available on the self-hosted browser only. - 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_sessionneeded (it still works for an explicit reset). A new view preloadsabout:blank(bounded 3 s) before creation so it always has a live renderer, host-side commands are bounded at 20 s, and the child's stderr plus exit code/signal are written to$DSH_HOME/logs/dsh-builtin-browser-host.log(2 MB self-truncating) so a plaindsh webself-hosted setup can diagnose a crash loop itself. - 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-electronfirst; alternatively pre-install a binary and pointELECTRON_PATHat it. - This plugin contains no browser-column UI — that is host-shell territory; do not treat "browser column" as a plugin feature.
Development
# Type-check + build (lib/)
npm run build
Run tests:
npm test(=tsc -p tsconfig.json+node --test "tests/*.test.mjs"; fake-host tests, no Electron needed).
Code layout:
| Directory | Responsibility |
|---|---|
src/browser/ |
The ctx.browser seam and all request/result types |
src/browser-electron/ |
Electron CDP provider, self-hosted child (host-main.ts), RPC layer |
src/tool-browser/ |
Model-facing browser_* tools |
src/types/ |
Electron ambient types (shim; no hard electron type dependency) |
Update history
Round-by-round development and fixes (full detail in CHANGELOG.md). Published as of 0.1.16 (tag
v0.1.16).
| Round | Date | Content |
|---|---|---|
| 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 |
| 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 |
| 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 |
| 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 |
| 5 | 2026-08 | Electron 44 compatibility: available() is now side-effect free (no more triggering Electron 44 lazy download); flushAuth cookie-domain build fix |
| 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 across sessions (locateTab), browser_close_tab no longer fakes success, unknown ids error with the session's actual tab list |
| 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 |
| 0.1.16 | 2026-08-26 | Release: all seven rounds ship as 0.1.16 (build clean, 21/21 tests pass, v0.1.16) |
| 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 |
| 0.1.17 | 2026-08-27 | Release: round 8 ships as 0.1.17 (build clean, 21/21 tests pass) |
| 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 |
| 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) |
| 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) |
| 0.1.19 | 2026-08-27 | Release: round 10 (DSH-Store compatibility declaration) ships as 0.1.19 (build clean, 21/21 tests pass) |
| 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 |
| 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) |
| 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) |
| 0.1.20 | 2026-08-27 | Release: rounds 11/12 + hardening ship as 0.1.20 (build clean, 25/25 tests pass) |
| 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 |
| 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 |
| 0.1.21 | 2026-08-28 | Release: rounds 13/14 ship as 0.1.21 (build clean, 25/25 tests pass) |
| 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 |
| 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 |
| 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> |
| 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) |
| 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) |
| 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 |
| 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 |
| 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) |
| 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 get |
…
Comments
Comments live in GitHub Discussions. Sign in with GitHub to post or react.