refactor(browser): isolate Electron code and defer native coverage
This commit is contained in:
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 保活仍遵循现有覆盖率要求.
|
||||
+5
-5
@@ -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 {
|
||||
+2
-2
@@ -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
-1
@@ -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 => ({
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user