dsh-builtin-browser 0.1.20 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -238,7 +238,7 @@ The browser's **visible view**, the **browser column layout**, and the **column-
238
238
  | DeepSeek Harness (dsh) | `0.1.1-rc.2` (peer range `^0.1.1-rc.2`) |
239
239
  | Electron | `44.0.0` (≥ 40 recommended; 33.x has a compositor defect) |
240
240
  | Node.js | `22.20.0` |
241
- | dsh-builtin-browser | `0.1.20` |
241
+ | dsh-builtin-browser | `0.1.21` |
242
242
  | OS | Windows 10 (10.0.26200) |
243
243
 
244
244
  > The plugin declares `electron >= 30`; it has **only been verified on Windows** (macOS/Linux untested, not yet promised).
@@ -253,7 +253,7 @@ The browser's **visible view**, the **browser column layout**, and the **column-
253
253
  - `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 (default confined to `~/Downloads`, override with `downloadDir`); 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).
254
254
  - 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.
255
255
  - `browser_restrict` is a **soft guardrail** against accidental actions, not a security boundary: the model can lift it itself.
256
- - Popups (`window.open` / `target=_blank`) are re-routed into the current tab instead of opening untracked native windows, so the tab/session model stays intact.
256
+ - Popups (`window.open` / `target=_blank`) open as a **new tab** in the same session window: the original page and its opener context survive and the jump is recorded in the session history. Non-HTTP(S) popups (OAuth handoff pages, `mailto:`, custom schemes) are still handed to the system, and untracked native windows are never created.
257
257
  - The `browser_auth` cookie round-trip does not preserve `hostOnly`/`sameSite` (host-only cookies come back as domain cookies); it is available on the self-hosted browser only.
258
258
  - 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).
259
259
  - Electron 44+ downloads its binary (~100 MB) on the first window open and needs network; it is never needed again afterwards. If detection runs without network, pre-install a binary and point `ELECTRON_PATH` at it.
@@ -301,6 +301,9 @@ Code layout:
301
301
  | 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) |
302
302
  | 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) |
303
303
  | **0.1.20** | 2026-08-27 | **Release**: rounds 11/12 + hardening ship as **0.1.20** (build clean, 25/25 tests pass) |
304
+ | 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 |
305
+ | 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 |
306
+ | **0.1.21** | 2026-08-28 | **Release**: rounds 13/14 ship as **0.1.21** (build clean, 25/25 tests pass) |
304
307
 
305
308
  ## Acknowledgements
306
309
 
package/README.md CHANGED
@@ -238,7 +238,7 @@ agent (browser_* 工具)
238
238
  | DeepSeek Harness(dsh) | `0.1.1-rc.2`(peer 声明 `^0.1.1-rc.2`) |
239
239
  | Electron | `44.0.0`(推荐 ≥ 40;33.x 存在合成器缺陷) |
240
240
  | Node.js | `22.20.0` |
241
- | dsh-builtin-browser | `0.1.20` |
241
+ | dsh-builtin-browser | `0.1.21` |
242
242
  | 操作系统 | Windows 10 (10.0.26200) |
243
243
 
244
244
  > 插件声明 `electron >= 30`;**当前仅在 Windows 环境实测**(macOS/Linux 未验证,暂不承诺)。
@@ -253,7 +253,7 @@ agent (browser_* 工具)
253
253
  - `browser_download` 在页面上下文内 `fetch`(带登录态),受同源/CORS 约束;仅允许 HTTP(S) 目标;`savePath` 必须为绝对路径(默认限定在 `~/Downloads`,可用 `downloadDir` 覆盖);单文件上限 256MB(流式限流,按 Content-Length 提前拒绝),文件由浏览器子进程直接落盘(临时文件 + 原子改名)。
254
254
  - 自托管浏览器的 cookie 在磁盘上以明文存储(Electron 默认行为);需要加密落盘的部署应在宿主层接入系统钥匙串 / DPAPI。
255
255
  - `browser_restrict` 是防误操作的**软护栏**,不是安全边界:模型可以自行解除白名单。
256
- - 页面弹窗(`window.open` / `target=_blank`)会被重定向到当前标签页内打开,不创建独立窗口,以免破坏标签/会话模型。
256
+ - 页面弹窗(`window.open` / `target=_blank`)会在同一会话窗口中**新开一个标签页**打开,保留原页面与 opener 上下文,跳转也计入会话历史;非 HTTP(S) 弹窗(OAuth 承接页、`mailto:`、自定义协议)仍交给系统处理,不创建未跟踪的独立窗口。
257
257
  - `browser_auth` 的 cookie 往返不保留 `hostOnly`/`sameSite` 字段(host-only cookie 恢复后变成 domain cookie);仅自托管浏览器可用。
258
258
  - 自托管浏览器子进程崩溃(或宿主 DSH 重启)后会自动重启;崩溃前已打开的会话在**下一次调用时自动重建**——仅页面状态丢失,无需手动 `browser_reset_session`。`browser_reset_session` 仍可用于主动重置。
259
259
  - electron 随插件安装;Electron 44+ 首次打开浏览器窗口时自动下载二进制(约 100MB,需网络),之后不再需要。若探测时网络不可用,可预装 `ELECTRON_PATH` 指定的二进制。
@@ -301,6 +301,9 @@ npm run build
301
301
  | 第十二轮 | 2026-08-27 | **resolveElectronPath 排除打包应用**(issue #6):新增 `isBareElectron`(旁有 `app.asar` 即打包应用,一律不复用,全平台含 macOS bundle 布局);bundled electron 纯文件系统探测置最优先;`ELECTRON_PATH` 显式覆盖最优先;dist 缺失时明确报错(`npx install-electron` 指引) |
302
302
  | 全面复审加固 | 2026-08-27 | **三轮审查加固**:并发恢复双重建竞态(child `createView` 幂等化)、macOS bundle 路径判定、`dispose()` vs `start()` 僵尸 child 竞态三道闸、pendingSocket 泄漏、选路顺序全量单测(24→25 项测试) |
303
303
  | **0.1.20** | 2026-08-27 | **发布**:第十一/十二轮 + 全面复审加固随 **0.1.20** 发布(构建零错误、25 项测试全绿) |
304
+ | 第十三轮 | 2026-08-28 | **macOS/Linux 输入框无法键入修复**(issue #7):0.1.16 引入的 Windows 焦点路由(mousedown 强制 focus + 窗口 refocus 恢复)未做平台判断,与 macOS 原生 click-to-focus 冲突导致登录框收不到键入 → 两处焦点逻辑加 `win32` 平台门,非 Windows 恢复原生行为 |
305
+ | 第十四轮 | 2026-08-28 | **window.open/target=_blank 新开标签**(issue #8):HTTP(S) 弹窗不再 loadURL 覆盖当前视图,转交父进程在**同一会话窗口新开标签**——原页面与 opener 上下文保留(门户「工作台」类跳转不再 403),跳转计入会话历史;未分组视图保留回退;非 HTTP 弹窗仍放行系统 |
306
+ | **0.1.21** | 2026-08-28 | **发布**:第十三/十四轮随 **0.1.21** 发布(构建零错误、25 项测试全绿) |
304
307
 
305
308
  ## 特别感谢
306
309
 
@@ -292,8 +292,12 @@ function windowFor(windowId) {
292
292
  // regaining focus (alt-tab, click) does not automatically refocus any view
293
293
  // (electron#28163) — the last addChildView keeps the focus. Restore the view
294
294
  // the human was last using (click steering via wireFocusRouting), falling
295
- // back to the visible page view.
295
+ // back to the visible page view. Windows-only: on macOS/Linux the OS routes
296
+ // keyboard input to the clicked view natively, and forcing focus() on window
297
+ // focus fights the platform — it can leave page inputs untypeable (issue #7).
296
298
  w.on('focus', () => {
299
+ if (process.platform !== 'win32')
300
+ return;
297
301
  const last = win.lastFocusedViewId;
298
302
  const wc = last === TOOLBAR_FOCUS ? win.toolbarView?.webContents
299
303
  : last !== undefined ? win.views.get(last)?.webContentsView.webContents
@@ -334,8 +338,15 @@ function layoutWindow(win) {
334
338
  * regaining focus does not refocus any view, electron#28163). Focusing the
335
339
  * clicked view on mousedown makes the toolbar and pages behave like a real
336
340
  * browser: click the URL bar and type, click the page and type/scroll.
341
+ *
342
+ * Windows-only by design (issue #7): macOS/Linux already route keyboard input
343
+ * to the clicked view natively; calling webContents.focus() from inside the
344
+ * mousedown input-event there interferes with native click-to-focus and can
345
+ * leave login/page inputs unable to receive typed characters.
337
346
  */
338
347
  function wireFocusRouting(win, view, focusKey) {
348
+ if (process.platform !== 'win32')
349
+ return;
339
350
  view.webContents.on('input-event', (_event, input) => {
340
351
  if (input.type !== 'mouseDown')
341
352
  return;
@@ -508,14 +519,26 @@ async function handle(op, msg) {
508
519
  const assignment = assignments.get(viewId);
509
520
  const win = windowFor(assignment?.windowId);
510
521
  const view = new WebContentsView();
511
- // Route popups (window.open / target=_blank) back into THIS view
512
- // instead of letting Electron open untracked native windows that would
513
- // diverge from the session/tab model.
522
+ // Route popups (window.open / target=_blank) into the session/tab
523
+ // model instead of letting Electron open untracked native windows.
514
524
  view.webContents.setWindowOpenHandler(({ url }) => {
515
- // HTTP(S) popups (target=_blank, ads): redirect into this view
516
- // instead of opening an untracked native window.
525
+ // HTTP(S) popups open as a NEW TAB of this view's session: the
526
+ // parent (provider) owns the session model and its newTab action
527
+ // creates the tab in the same window, navigates it, and records it
528
+ // in the session history. Overriding THIS view with loadURL()
529
+ // destroyed the opener page — pages that need their opener context
530
+ // (portal dashboards carrying token/referer to the new window)
531
+ // lost it and the target page then 403'd (issue #8); the tab strip
532
+ // now keeps the original page alive like a real browser.
517
533
  if (/^https?:/i.test(url)) {
518
- void view.webContents.loadURL(url).catch(() => { });
534
+ if (win.windowId !== SHARED_WINDOW_ID) {
535
+ sendUserAction({ type: 'newTab', windowId: win.windowId, url });
536
+ }
537
+ else {
538
+ // Ungrouped view (no live session to attach a tab to,
539
+ // defensive only): fall back to navigating this view.
540
+ void view.webContents.loadURL(url).catch(() => { });
541
+ }
519
542
  return { action: 'deny' };
520
543
  }
521
544
  // Non-HTTP popups (OAuth redirects, mailto:, custom schemes):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-builtin-browser",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "Shared real browser plugin for DeepSeek Harness: install-and-use — a visible native browser the human can take over, driven by the agent over CDP. DOM-level interaction (React/Vue safe), per-task session isolation, cookie persistence (browser_auth), CAPTCHA detection, batch form fill (browser_fill), authenticated download, history/replay, action restriction",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -327,8 +327,11 @@ function windowFor(windowId: string | undefined): HostWindow {
327
327
  // regaining focus (alt-tab, click) does not automatically refocus any view
328
328
  // (electron#28163) — the last addChildView keeps the focus. Restore the view
329
329
  // the human was last using (click steering via wireFocusRouting), falling
330
- // back to the visible page view.
330
+ // back to the visible page view. Windows-only: on macOS/Linux the OS routes
331
+ // keyboard input to the clicked view natively, and forcing focus() on window
332
+ // focus fights the platform — it can leave page inputs untypeable (issue #7).
331
333
  w.on('focus', () => {
334
+ if (process.platform !== 'win32') return
332
335
  const last = win.lastFocusedViewId
333
336
  const wc = last === TOOLBAR_FOCUS ? win.toolbarView?.webContents
334
337
  : last !== undefined ? win.views.get(last)?.webContentsView.webContents
@@ -361,8 +364,14 @@ function layoutWindow(win: HostWindow): void {
361
364
  * regaining focus does not refocus any view, electron#28163). Focusing the
362
365
  * clicked view on mousedown makes the toolbar and pages behave like a real
363
366
  * browser: click the URL bar and type, click the page and type/scroll.
367
+ *
368
+ * Windows-only by design (issue #7): macOS/Linux already route keyboard input
369
+ * to the clicked view natively; calling webContents.focus() from inside the
370
+ * mousedown input-event there interferes with native click-to-focus and can
371
+ * leave login/page inputs unable to receive typed characters.
364
372
  */
365
373
  function wireFocusRouting(win: HostWindow, view: WebContentsView, focusKey: string): void {
374
+ if (process.platform !== 'win32') return
366
375
  view.webContents.on('input-event', (_event, input) => {
367
376
  if (input.type !== 'mouseDown') return
368
377
  win.lastFocusedViewId = focusKey
@@ -509,14 +518,25 @@ async function handle(op: string, msg: { id: number; viewId?: string; windowId?:
509
518
  const assignment = assignments.get(viewId)
510
519
  const win = windowFor(assignment?.windowId)
511
520
  const view = new WebContentsView()
512
- // Route popups (window.open / target=_blank) back into THIS view
513
- // instead of letting Electron open untracked native windows that would
514
- // diverge from the session/tab model.
521
+ // Route popups (window.open / target=_blank) into the session/tab
522
+ // model instead of letting Electron open untracked native windows.
515
523
  view.webContents.setWindowOpenHandler(({ url }) => {
516
- // HTTP(S) popups (target=_blank, ads): redirect into this view
517
- // instead of opening an untracked native window.
524
+ // HTTP(S) popups open as a NEW TAB of this view's session: the
525
+ // parent (provider) owns the session model and its newTab action
526
+ // creates the tab in the same window, navigates it, and records it
527
+ // in the session history. Overriding THIS view with loadURL()
528
+ // destroyed the opener page — pages that need their opener context
529
+ // (portal dashboards carrying token/referer to the new window)
530
+ // lost it and the target page then 403'd (issue #8); the tab strip
531
+ // now keeps the original page alive like a real browser.
518
532
  if (/^https?:/i.test(url)) {
519
- void view.webContents.loadURL(url).catch(() => { /* navigation failures surface on the next snapshot */ })
533
+ if (win.windowId !== SHARED_WINDOW_ID) {
534
+ sendUserAction({ type: 'newTab', windowId: win.windowId, url })
535
+ } else {
536
+ // Ungrouped view (no live session to attach a tab to,
537
+ // defensive only): fall back to navigating this view.
538
+ void view.webContents.loadURL(url).catch(() => { /* navigation failures surface on the next snapshot */ })
539
+ }
520
540
  return { action: 'deny' }
521
541
  }
522
542
  // Non-HTTP popups (OAuth redirects, mailto:, custom schemes):