refactor(browser): isolate Electron code and defer native coverage

This commit is contained in:
imccyu committed 2026-09-21 03:25:30 +08:00
1 parent 94bc5b25b8
commit d8438ecf06
13 files changed
+50 -40

No files matched your search

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-20-desktop-browser-webview.md
2026-09-20-desktop-browser-webview.md: 586cfb378b05d4d1083b810c321f153e5cb93646
2026-09-20-desktop-browser-webview.zh.md: 8522df6301707ed80c2f75ccda22c63f4e8f2491
2026-09-20-desktop-browser-webview.md: 63e1d94213aeac5a0fb1cc14192dbb92428d04cf
2026-09-20-desktop-browser-webview.zh.md: 5b2e1ee033825152a6dca53bd173687bbe26b7f5
@@ -18,7 +18,7 @@ The [stable Sidebar mounting decision](../architecture/2026-09-20-sidebar-retain
- `BrowserController` owns address commands and recoverable presentation state. Native history stays in the guest; actual URL and title observations update the persisted address.
- Controller registries are keyed by DSH Session, independently of presentation bindings. Rebinding replaces the store writer without recreating the page.
- `pages.ts` composes a navigation provider and a presentation object. `ElectronWebViewImpl` owns guest leases and native navigation; `ElectronWebviewPresentation` creates the tag and attaches it inside the Sidebar-owned content container.
- `electron/pages.ts` composes the Electron navigation provider and presentation object. `ElectronWebViewImpl` owns guest leases and native navigation; `ElectronWebviewPresentation` creates the tag and attaches it inside the Sidebar-owned content container.
- The Desktop Browser type declares `keepMounted`. Sidebar retains its DOM ancestors across hiding and docking; CSS owns layout and clipping. Docking gestures disable guest pointer input, and body-local drop hints use normal stacking. A physical unmount cancels pending attachment and releases the guest; a later mount recreates it from the known address.
- Tab occurrence cancellation, plugin unload and window destruction release guests. Guest crashes leave a retryable failure; Reload creates a new guest. Application restart offers the saved title and URL for explicit restoration; it creates no guest until the user restores or submits an address. Page memory and native history are not restored.
@@ -55,3 +55,5 @@ The Desktop toolbar has no sandbox-disable switch. The implementation adds no re
Sidebar-owned stable ancestors preserve the page without Browser-owned geometry or occlusion handling. Hidden guests retain page memory and may continue network activity; there is no idle eviction policy. Persistent storage, selectable isolation scopes and permission-grant UI remain separate work. Temporary Workspace partitions are the current policy, not a promise that Workspace isolation always implies temporary storage.
Focused tests cover native navigation error recovery, preload listener scoping and migrated iframe behavior. They do not establish real Electron attachment timing, overlap, focus, platform styling or storage isolation; those remain runtime verification gaps. No GUI recording accompanies the change. This implementation is not evidence that the complete browser security policy is ready for release.
The package's Electron provider, presentation, page factory and Workspace storage resolver live in `src/client/electron/`. That directory is temporarily excluded from per-file coverage until a native Electron harness covers guest behavior; its unit tests still run. Shared Browser controllers, restoration UI, iframe code and Sidebar retention remain subject to the existing coverage requirements.
@@ -18,7 +18,7 @@ Desktop 通过 `ElectronWebViewImpl` 使用 `<webview>`;Web 保留显式启用
- `BrowserController` 负责地址命令与可恢复的展示状态。原生 history 留在 guest 内;真实 URL 与标题观察更新持久化地址。
- 控制器注册表按 DSH Session 索引,不依赖呈现绑定的存活期。重新绑定只替换存储写入方,不重建页面。
- `pages.ts` 组装导航提供方和呈现对象。`ElectronWebViewImpl` 负责 guest 租约与原生导航;`ElectronWebviewPresentation` 创建标签并挂载到 Sidebar 持有的内容容器内。
- `electron/pages.ts` 组装 Electron 导航提供方和呈现对象. `ElectronWebViewImpl` 负责 guest 租约与原生导航; `ElectronWebviewPresentation` 创建标签并挂载到 Sidebar 持有的内容容器内.
- Desktop Browser 类型声明 `keepMounted`。Sidebar 在隐藏和停靠切换时保留其 DOM 祖先,CSS 负责布局与裁剪。停靠手势期间禁用 guest 指针输入,Body 内的放置提示使用普通层叠。物理卸载会取消未完成的挂载并释放 guest,之后重新挂载时从已知地址重建。
- tab occurrence 取消、插件卸载与窗口销毁会释放 guest。guest 崩溃留下可重试的失败状态;刷新创建新 guest。应用重启展示保存的标题与 URL,等待显式恢复;用户恢复或提交地址之前不创建 guest。不恢复页面内存或原生 history。
@@ -55,3 +55,5 @@ Desktop toolbar 没有关闭 sandbox 的开关。实现不增加远程调试端
Sidebar 持有的稳定祖先保留页面,无需 Browser 自行处理几何或遮挡。隐藏 guest 保留页面内存,也可能继续联网;当前没有空闲回收策略。持久化存储、可选择的隔离级别与权限授权 UI 留作独立工作。临时 Workspace partition 是当前策略,并不意味着 Workspace 隔离必然要求临时存储。
定向测试覆盖原生导航错误恢复、preload 监听范围和已迁移的 iframe 行为。这些测试不能确认真实 Electron 的挂载时序、遮挡、焦点、平台样式或存储隔离;这些仍是运行时验证缺口。本次变更不附带 GUI 录像。此实现不构成完整浏览器安全策略已经可以发布的证据。
包内的 Electron 提供方、呈现适配器、页面工厂与 Workspace 存储解析集中在 `src/client/electron/`. 在原生 Electron 测试环境覆盖 guest 行为之前, 该目录暂时不参与逐文件覆盖率门禁; 其单元测试仍然运行. 共享 Browser 控制器、恢复 UI、iframe 代码和 Sidebar 保活仍遵循现有覆盖率要求.
@@ -1,11 +1,11 @@
/** Electron navigation and guest lifetime, independent from DOM placement. */
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
import type { DesktopBrowserBridge, DesktopBrowserLeaseId } from '../../types.ts'
import type { ElectronWebviewPresentation, WebviewElement } from '../view/ElectronWebviewPresentation.ts'
import { emptyBrowserFrame, type BrowserFrame, type BrowserFrameState, type BrowserLoadError } from './BrowserFrame.ts'
import type { BrowserPageOptions } from './BrowserPage.ts'
import { browserAddressCheckpoint, currentBrowserTarget } from './BrowserPersistence.ts'
import { parseBrowserAddress, type BrowserTarget } from './url.ts'
import type { ElectronWebviewPresentation, WebviewElement } from './ElectronWebviewPresentation.ts'
import { emptyBrowserFrame, type BrowserFrame, type BrowserFrameState, type BrowserLoadError } from '../browser/BrowserFrame.ts'
import type { BrowserPageOptions } from '../browser/BrowserPage.ts'
import { browserAddressCheckpoint, currentBrowserTarget } from '../browser/BrowserPersistence.ts'
import { parseBrowserAddress, type BrowserTarget } from '../browser/url.ts'
interface NavigationEvent extends Event { readonly isMainFrame: boolean }
interface LoadFailureEvent extends NavigationEvent {
@@ -1,7 +1,7 @@
/** Electron tag DOM inside a Sidebar-owned, stable content container. */
import type { DesktopBrowserReservation } from '../../types.ts'
import type { BrowserPresentation } from './BrowserPresentation.ts'
import css from './Browser.module.css'
import type { BrowserPresentation } from '../view/BrowserPresentation.ts'
import css from '../view/Browser.module.css'
/** The Electron tag API used by its navigation provider. */
export interface WebviewElement extends HTMLElement {
@@ -0,0 +1,22 @@
/** Assemble Electron navigation and presentation behind the shared BrowserPage interface. */
import type { DesktopBrowserBridge } from '../../types.ts'
import type { BrowserPage, BrowserPageOptions } from '../browser/BrowserPage.ts'
import { ElectronWebViewImpl } from './ElectronWebViewImpl.ts'
import { ElectronWebviewPresentation } from './ElectronWebviewPresentation.ts'
/**
* Assemble an idle Electron provider; guest creation waits for mounting and navigation.
* @param options - checkpoint and source-tab callbacks.
* @param bridge - desktop-only transport.
* @param workspace - storage account resolver.
* @returns separate navigation and presentation faces.
*/
export function createElectronPage(options: BrowserPageOptions, bridge: DesktopBrowserBridge,
workspace: (signal: AbortSignal) => Promise<string>): BrowserPage {
const presentation = new ElectronWebviewPresentation({
mounted: () => { frame.attach() },
unmounted: () => { frame.detach() },
})
const frame = new ElectronWebViewImpl(options, bridge, workspace, presentation)
return { frame, presentation }
}
@@ -1,4 +1,4 @@
/** Resolve CWD-keyed browser storage after the authoritative Workspace list has arrived. */
/** Resolve CWD-keyed Electron storage after the authoritative Workspace list has arrived. */
import type { WorkspaceSource } from '@deepseek-ai/dsh-api-workspace-controller/client'
/**
@@ -9,9 +9,10 @@ import { BrowserBody, type BrowserBodyProps } from './view/BrowserBody.tsx'
import { BrowserTitle } from './view/BrowserTitle.tsx'
import { createBrowserControllers } from './browser/BrowserController.ts'
import type { BrowserInjected } from './browser/BrowserController.ts'
import { createElectronPage, createIframePage } from './pages.ts'
import { createIframePage } from './pages.ts'
import { createElectronPage } from './electron/pages.ts'
import type { DesktopBrowserBridge } from '../types.ts'
import { browserWorkspace } from './browser/workspace.ts'
import { browserWorkspace } from './electron/workspace.ts'
import type { BrowserPageFactory } from './browser/BrowserPage.ts'
import { BROWSER_ID, browserDefinition } from './definition.tsx'
import { en, zh } from './locales.ts'
@@ -1,10 +1,7 @@
/** Assemble navigation providers and presentation adapters without platform branches in consumers. */
import type { DesktopBrowserBridge } from '../types.ts'
/** Assemble iframe navigation and presentation without platform branches in consumers. */
import type { BrowserPage, BrowserPageOptions } from './browser/BrowserPage.ts'
import { IframeImpl } from './browser/IframeImpl.ts'
import { ElectronWebViewImpl } from './browser/ElectronWebViewImpl.ts'
import { IframePresentation } from './view/IframePresentation.ts'
import { ElectronWebviewPresentation } from './view/ElectronWebviewPresentation.ts'
/**
* Assemble an idle iframe provider and its DOM presentation.
@@ -20,20 +17,3 @@ export function createIframePage(options: BrowserPageOptions): BrowserPage {
const frame = new IframeImpl(options, presentation)
return { frame, presentation }
}
/**
* Assemble an idle Electron provider; guest creation waits for mounting and navigation.
* @param options - checkpoint and source-tab callbacks.
* @param bridge - desktop-only transport.
* @param workspace - storage account resolver.
* @returns separate navigation and presentation faces.
*/
export function createElectronPage(options: BrowserPageOptions, bridge: DesktopBrowserBridge,
workspace: (signal: AbortSignal) => Promise<string>): BrowserPage {
const presentation = new ElectronWebviewPresentation({
mounted: () => { frame.attach() },
unmounted: () => { frame.detach() },
})
const frame = new ElectronWebViewImpl(options, bridge, workspace, presentation)
return { frame, presentation }
}
@@ -1,7 +1,7 @@
// @vitest-environment jsdom
import { expect, it, vi } from 'vitest'
import { ElectronWebViewImpl } from '../src/client/browser/ElectronWebViewImpl.ts'
import { ElectronWebviewPresentation, type WebviewElement } from '../src/client/view/ElectronWebviewPresentation.ts'
import { ElectronWebViewImpl } from '../src/client/electron/ElectronWebViewImpl.ts'
import { ElectronWebviewPresentation, type WebviewElement } from '../src/client/electron/ElectronWebviewPresentation.ts'
import type { DesktopBrowserBridge, DesktopBrowserLeaseId } from '../src/types.ts'
it('clears a failed load when the main page retries without a toolbar command', async () => {
@@ -2,8 +2,8 @@
import { vi } from 'vitest'
import type { DesktopBrowserBridge, DesktopBrowserLeaseId, DesktopBrowserReservation } from '../src/types.ts'
import type { BrowserTabState } from '../src/client/browser/BrowserPersistence.ts'
import { createElectronPage } from '../src/client/pages.ts'
import { ElectronWebviewPresentation } from '../src/client/view/ElectronWebviewPresentation.ts'
import { createElectronPage } from '../src/client/electron/pages.ts'
import { ElectronWebviewPresentation } from '../src/client/electron/ElectronWebviewPresentation.ts'
let sequence = 0
@@ -2,7 +2,7 @@ import { afterEach, expect, it, vi } from 'vitest'
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
import type { WorkspaceId, WorkspaceSnapshot, WorkspaceView } from '@deepseek-ai/dsh-api-workspace-controller/client'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import { browserWorkspace } from '../src/client/browser/workspace.ts'
import { browserWorkspace } from '../src/client/electron/workspace.ts'
const SESSION = 'session' as SessionId
const workspace = (id: string, path: string, sessionIds: readonly SessionId[]): WorkspaceView => ({
+3
View File
@@ -224,6 +224,9 @@ export default defineConfig({
// harness the jsdom lane doesn't cover yet. TODO(gui): cover and
// remove as the client test lane matures.
'packages/client/ui-trajectory/src/*',
// Electron guest integration retains its unit specs; per-file coverage
// is deferred until a native Electron harness covers guest behavior.
'packages/client/ui-sidebar-browser/src/client/electron/**',
// Trajectory's compact Markdown projection retains deferred branch coverage.
'packages/client/ui-primitives/src/markdown/plain-text.ts',
'packages/client/ui-user-questions/src/client/QuestionComposer.tsx',