Shared real browser for DSH: a native Electron window the human can watch and take over, driven by the agent over CDP with 20 browser_* tools (open/snapshot/execute/fill/screenshot/download/auth), per-task session isolation, cookie persistence, CAPTCHA detection; self-hosts on plain dsh web without a desktop shell.
Install
# from npm (prebuilt)
dsh plugin --profile web add dsh-builtin-browser
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:wqty123/dsh-browser
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
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 33 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 view, not a relay: the browser is a native
WebContentsView; the human sees every step the agent takes and can grab control at any time; - Install-and-use: with a desktop shell the shell's embedded view is used; on plain
dsh webthe plugin self-hosts — it spawns its own Electron window with zero extra configuration; - One plugin, one toolset: after install the agent automatically gets 33
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
# Option 1: install from npm (published)
dsh plugin --profile web add dsh-builtin-browser
# Option 2: install from a checkout (one plugin, one repository)
dsh plugin --profile web add <path-to-this-repo>
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_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 (three rows); per-row config:
| Row | Key | Type | Default | Description |
|---|---|---|---|---|
browser-electron |
viewHost |
object | required | ElectronBrowserViewHost supplied by the host shell (typically !!js ctx.get('electronViewHost')) |
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 (supplied by the host shell)
→ WebContentsView + webContents.debugger (CDP)
- Seam (
browserrow): provides thectx.browserservice — provider registration, session lifecycle, error codes — decoupled from any implementation. - Provider (
browser-electronrow): operates views through theElectronBrowserViewHostseam (create/destroy/show,sendCommand), implemented with real Electron objects by the shell. - Tools (
tool-browserrow): the 33 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 browser's visible view, the browser column layout, and the column-to-view alignment belong to the host shell (e.g. dsh's apps/desktop), not this plugin. This plugin consumes the shell-provided electronViewHost and owns the seam, provider, and tools. Without a shell the plugin self-hosts and everything still works.
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.1.1-rc.2 (peer range ^0.1.1-rc.2) |
| Electron | 44.0.0 (≥ 40 recommended; 33.x has a compositor defect) |
| Node.js | 22.20.0 |
| dsh-builtin-browser | 0.1.21 |
| OS | Windows 10 (10.0.26200) |
The plugin declares
electron >= 30; it has only been verified on Windows (macOS/Linux untested, not yet promised).
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 |
The npm badge at the top is the authority on the registry's latest version:
0.1.22is committed and tagged; if the badge still reads0.1.21, that version is not published yet.
Acknowledgements
Special thanks to the DeepSeek Harness repository and the DeepSeek AI team: the seam, the tool runtime, and the plugin system this plugin builds on all come from that project.
Thanks as well to Cordis for the plugin foundation, and to everyone in the community who discussed, tested, gave feedback, and built plugins.
A Note from the Author
Have feedback about this plugin, or an idea for another plugin you'd like built? Feel free to reach out on WeChat:
wx: hui13866591135 (please mention this project when sending the friend request)
License
MIT License, see LICENSE.
This project is a community plugin for DeepSeek Harness, not an official DeepSeek product.
Links
More in this category
Tencent/BrowserSkill#dsh-plugin-browserskill★ 7907
BrowserSkill bridge for controlling visible Chrome and Edge Agent Windows from DeepSeek Harness, with native browser tools, accessibility and VOM observations, screenshots, owned multi-session control, and a live Web UI overlay.
omdsh-dev/dsh-browser#packages/browser/bridge-browser★ 746
Chrome sidebar extension that lets DSH operate your browser directly, no vision capabilities required.
liustack/modsearch★ 572
Web search bridge for text-only agents: ask the web or X, get structured JSON evidence (search, fetch, citations).
DDDMUC/dsh-free-search★ 281
Free, keyless web search for DSH: 7 engines (DuckDuckGo/Bing/SearXNG free + Exa/Perplexity/DeepSeek paid), auto-failover, settings-page UI with API key inputs and official links, web_fetch, and an engine test tool.
Tabbit-Browser/dsh-tabbit★ 101
Gives DeepSeek Harness control of the Tabbit Browser: auto-loads the tabbit-browser skill on install, detects official Tabbit and Tabbit Browser releases (>= 1.9.0), checks the tabbit-cli persistent runtime, diagnoses the per-platform DSH sandbox mode needed to call the CLI, and downloads the region-matched official installer via a background job when no qualifying version is present.
anweat/dsh-web-search-pro★ 72
Persistent enhanced web search: multi-engine routing (DeepSeek/Exa/DDG/Bing/Jina + GitHub/Bilibili/YouTube/V2EX/Xiaohongshu/Twitter/Reddit/RSS), SQLite+LRU cache, userscript-style extraction, Playwright rendering.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.