diff --git a/.agents/notes/implemented/feature/2026-09-20-desktop-browser-webview.i18n.yaml b/.agents/notes/implemented/feature/2026-09-20-desktop-browser-webview.i18n.yaml index 7f4c2e3dfc..432965b93c 100644 --- a/.agents/notes/implemented/feature/2026-09-20-desktop-browser-webview.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-20-desktop-browser-webview.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/feature/2026-09-20-desktop-browser-webview.md b/.agents/notes/implemented/feature/2026-09-20-desktop-browser-webview.md index 586cfb378b..63e1d94213 100644 --- a/.agents/notes/implemented/feature/2026-09-20-desktop-browser-webview.md +++ b/.agents/notes/implemented/feature/2026-09-20-desktop-browser-webview.md @@ -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. diff --git a/.agents/notes/implemented/feature/2026-09-20-desktop-browser-webview.zh.md b/.agents/notes/implemented/feature/2026-09-20-desktop-browser-webview.zh.md index 8522df6301..5b2e1ee033 100644 --- a/.agents/notes/implemented/feature/2026-09-20-desktop-browser-webview.zh.md +++ b/.agents/notes/implemented/feature/2026-09-20-desktop-browser-webview.zh.md @@ -18,7 +18,7 @@ Desktop 通过 `ElectronWebViewImpl` 使用 ``;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 保活仍遵循现有覆盖率要求. diff --git a/packages/client/ui-sidebar-browser/src/client/browser/ElectronWebViewImpl.ts b/packages/client/ui-sidebar-browser/src/client/electron/ElectronWebViewImpl.ts similarity index 97% rename from packages/client/ui-sidebar-browser/src/client/browser/ElectronWebViewImpl.ts rename to packages/client/ui-sidebar-browser/src/client/electron/ElectronWebViewImpl.ts index 79c075aaaf..b6987bbc8f 100644 --- a/packages/client/ui-sidebar-browser/src/client/browser/ElectronWebViewImpl.ts +++ b/packages/client/ui-sidebar-browser/src/client/electron/ElectronWebViewImpl.ts @@ -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 { diff --git a/packages/client/ui-sidebar-browser/src/client/view/ElectronWebviewPresentation.ts b/packages/client/ui-sidebar-browser/src/client/electron/ElectronWebviewPresentation.ts similarity index 96% rename from packages/client/ui-sidebar-browser/src/client/view/ElectronWebviewPresentation.ts rename to packages/client/ui-sidebar-browser/src/client/electron/ElectronWebviewPresentation.ts index 3fe0c68223..4fc6798e50 100644 --- a/packages/client/ui-sidebar-browser/src/client/view/ElectronWebviewPresentation.ts +++ b/packages/client/ui-sidebar-browser/src/client/electron/ElectronWebviewPresentation.ts @@ -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 { diff --git a/packages/client/ui-sidebar-browser/src/client/electron/pages.ts b/packages/client/ui-sidebar-browser/src/client/electron/pages.ts new file mode 100644 index 0000000000..d2430ad019 --- /dev/null +++ b/packages/client/ui-sidebar-browser/src/client/electron/pages.ts @@ -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): BrowserPage { + const presentation = new ElectronWebviewPresentation({ + mounted: () => { frame.attach() }, + unmounted: () => { frame.detach() }, + }) + const frame = new ElectronWebViewImpl(options, bridge, workspace, presentation) + return { frame, presentation } +} diff --git a/packages/client/ui-sidebar-browser/src/client/browser/workspace.ts b/packages/client/ui-sidebar-browser/src/client/electron/workspace.ts similarity index 93% rename from packages/client/ui-sidebar-browser/src/client/browser/workspace.ts rename to packages/client/ui-sidebar-browser/src/client/electron/workspace.ts index 9298529a33..85cd73de20 100644 --- a/packages/client/ui-sidebar-browser/src/client/browser/workspace.ts +++ b/packages/client/ui-sidebar-browser/src/client/electron/workspace.ts @@ -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' /** diff --git a/packages/client/ui-sidebar-browser/src/client/index.ts b/packages/client/ui-sidebar-browser/src/client/index.ts index af5eab39cc..780f7f2d85 100644 --- a/packages/client/ui-sidebar-browser/src/client/index.ts +++ b/packages/client/ui-sidebar-browser/src/client/index.ts @@ -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' diff --git a/packages/client/ui-sidebar-browser/src/client/pages.ts b/packages/client/ui-sidebar-browser/src/client/pages.ts index b3ec7ef9c5..220616b4bd 100644 --- a/packages/client/ui-sidebar-browser/src/client/pages.ts +++ b/packages/client/ui-sidebar-browser/src/client/pages.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): BrowserPage { - const presentation = new ElectronWebviewPresentation({ - mounted: () => { frame.attach() }, - unmounted: () => { frame.detach() }, - }) - const frame = new ElectronWebViewImpl(options, bridge, workspace, presentation) - return { frame, presentation } -} diff --git a/packages/client/ui-sidebar-browser/tests/electron-frame.client.spec.ts b/packages/client/ui-sidebar-browser/tests/electron-frame.client.spec.ts index 53a54ea551..20480c615b 100644 --- a/packages/client/ui-sidebar-browser/tests/electron-frame.client.spec.ts +++ b/packages/client/ui-sidebar-browser/tests/electron-frame.client.spec.ts @@ -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 () => { diff --git a/packages/client/ui-sidebar-browser/tests/electron-harness.client.ts b/packages/client/ui-sidebar-browser/tests/electron-harness.client.ts index 31a8f521d5..bebfb96c53 100644 --- a/packages/client/ui-sidebar-browser/tests/electron-harness.client.ts +++ b/packages/client/ui-sidebar-browser/tests/electron-harness.client.ts @@ -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 diff --git a/packages/client/ui-sidebar-browser/tests/workspace.client.spec.ts b/packages/client/ui-sidebar-browser/tests/workspace.client.spec.ts index bd98e06d45..236e306ea9 100644 --- a/packages/client/ui-sidebar-browser/tests/workspace.client.spec.ts +++ b/packages/client/ui-sidebar-browser/tests/workspace.client.spec.ts @@ -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 => ({ diff --git a/vitest.config.ts b/vitest.config.ts index baf4f35fc3..22c5e530e3 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -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',