DSH / Atlas
2026-09-20implementedfeature

Retained Desktop browser webviews

保持页面实例的 Desktop 浏览器 webview

An iframe cannot expose cross-origin navigation or render sites that refuse embedding. An Electron guest loses forms, scroll position and native history if its tab body unmounts on selection or container changes. Page lifetime must therefore remain independent of visibility and layout changes. Independent per-tab storage also prevents related pages in one Workspace from sharing site login state. Page lifetime and sto

English

Problem

An iframe cannot expose cross-origin navigation or render sites that refuse embedding. An Electron guest loses forms, scroll position and native history if its tab body unmounts on selection or container changes. Page lifetime must therefore remain independent of visibility and layout changes.

Independent per-tab storage also prevents related pages in one Workspace from sharing site login state. Page lifetime and storage ownership need different owners.

Decision

Desktop uses <webview> through ElectronWebViewImpl; Web retains its opt-in iframe carrier. The Sidebar Browser decision owns shared tab behavior and iframe limitations; this note owns the Desktop carrier and Workspace-keyed partitions.

The stable Sidebar mounting decision owns retained Session and tab containers in real CSS layout. Workspace storage ownership and baseline guest security remain with this note.

  • 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.
  • 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.

The standard ./types subpath exports the shared lease, reservation, open-request and bridge declarations through a declaration-only types condition; Desktop consumers use import type. There is no runtime default, extra JavaScript artifact or early Host bundling for these declarations. Preload exposes scoped operations and callbacks, not raw IPC or Electron objects.

Browser has separate Host and Client compiler programs. Desktop and the Host aggregate reference only tsconfig.host.json, which compiles the package root and shared bridge declarations; the Client aggregate references tsconfig.client.json for the browser implementation. The package root tsconfig is solution-only. This keeps Client consumers of generated /remote declarations out of the Host compilation that precedes Typert generation.

Storage ownership

DesktopBrowserGuests assigns a random, non-persistent Electron partition to each canonical CWD account. The Client uses the Host-normalized WorkspaceView.path, not the Workspace record UUID, so recreating a Workspace at the same directory does not change its storage account key. Browser tabs whose DSH Sessions resolve to that CWD share the account; Sessions without a resolved Workspace remain separately isolated. Workspace membership is resolved after the Client receives its authoritative baseline and is fixed for a guest occurrence. The CWD key controls sharing, not disk persistence.

Closing a tab releases its guest, not its account's cookies or storage. Cookies, localStorage, IndexedDB, Service Workers and cache remain partition-owned and subject to normal origin rules. DOM, native history and sessionStorage remain page-owned. Account partitions survive window recreation within the Electron process but do not persist across application exit.

Initial guest policy

Only the primary application window enables webviewTag. The main process accepts guest reservations only from that window's application top frame and validates a one-use lease, partition and inert initial about:blank document before allowing the guest. The main process replaces renderer-supplied preferences: no Node integration, guest preload, nested webviews, plugins, insecure content, dialogs or drag navigation; sandbox, context isolation and Web security stay enabled.

Guest Sessions do not register the application protocol or inherit the application's authenticated request forwarding. Permission requests and checks, device access, display capture, downloads, native popup windows and HTTP authentication prompts are denied. Approved direct HTTP(S) popup requests without a POST body are routed through their live lease to a new Sidebar tab; scripted blank-window and POST popup flows remain unsupported. Navigation accepts credential-free HTTP(S); request filtering rejects local-file and privileged schemes and the known DSH Host endpoint, including common loopback aliases. This is not a general private-network or DNS-rebinding firewall.

The Desktop toolbar has no sandbox-disable switch. The implementation adds no remote-debugging endpoint or browser-use integration. A future automation provider requires an authenticated, target-scoped broker rather than access to every application target. The separation of a target/lifetime handle from navigation commands follows the same distinction as Playwright's Android WebView and Page objects; device-level input is a separate responsibility.

Alternatives considered

Render the guest in a visibility-mounted tab body. Unmounting disconnects the guest. Direct mounting requires the Sidebar's retained body and stable ancestor mechanism.

Move the guest between visible and hidden DOM containers. Electron 44's WebViewElement.disconnectedCallback detaches the guest and resets its internal instance. Retaining the element reference or React key does not preserve that instance; the parent stays fixed instead.

Use one partition per tab. This isolates site accounts between related pages. Workspace ownership provides the requested sharing without sharing the application Session; a tab still owns its own guest.

Change the partition of an active webview. Electron fixes the partition before first navigation. Selecting another isolation scope requires a replacement guest and an explicit policy for existing site data, not a live attribute toggle.

Consequences

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.

中文

Problem

iframe 无法提供跨域导航状态, 也无法显示拒绝嵌入的站点. 如果 Tab Body 因选中项或容器变化而卸载, Electron guest 就会丢失表单、滚动位置与原生 history. 页面生命周期因此必须独立于可见性和布局变化.

独立的 per-tab 存储也会阻止同一 Workspace 内相关页面共享站点登录态。页面生命周期与存储需要不同的所有者。

Decision

Desktop 通过 ElectronWebViewImpl 使用 <webview>; Web 保留显式启用的 iframe 载体. Sidebar Browser 决策负责共享 Tab 行为与 iframe 限制; 本文负责 Desktop 载体和按 Workspace 共享的分区.

Sidebar 稳定挂载决策负责真实 CSS 布局中的保活会话与 Tab 容器。Workspace 存储所有权与 guest 基础安全配置仍由本文负责。

  • BrowserController 负责地址命令与可恢复的展示状态。原生 history 留在 guest 内;真实 URL 与标题观察更新持久化地址。
  • 控制器注册表按 DSH Session 索引,不依赖呈现绑定的存活期。重新绑定只替换存储写入方,不重建页面。
  • 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。

标准 ./types 子路径通过仅指向声明文件的 types 条件导出共享的租约、申请结果、打开请求与桥接声明,Desktop 消费方使用 import type。这些声明没有运行时 default、额外 JavaScript 产物或提前进行的 Host 打包。preload 只暴露限定范围的操作与回调,不暴露原始 IPC 或 Electron 对象。

Browser 分别拥有 Host 与 Client 编译程序。Desktop 和 Host 聚合配置只引用 tsconfig.host.json,编译包根与共享桥接声明;Client 聚合配置通过 tsconfig.client.json 编译浏览器实现。包根 tsconfig 只作为 solution。这样,依赖生成 /remote 声明的 Client 消费方不会进入 Typert 生成之前的 Host 编译阶段。

Storage ownership

DesktopBrowserGuests 为每个规范化 CWD 存储账号分配随机、非持久化的 Electron partition。Client 使用 Host 规范化后的 WorkspaceView.path,不使用 Workspace 记录 UUID,因此在同一目录重建 Workspace 不改变存储账号 key。DSH Session 解析到同一 CWD 的 Browser Tab 共享账号;没有可解析 Workspace 的 Session 仍单独隔离。Workspace 归属在 Client 收到权威基线后解析,并在一次 guest occurrence 内保持不变。CWD key 控制共享关系,不决定是否落盘。

关闭 tab 只释放它的 guest,不清空账号的 Cookie 或存储。Cookie、localStorage、IndexedDB、Service Worker 与缓存由 partition 持有,并继续遵循普通 origin 规则。DOM、原生 history 与 sessionStorage 仍由页面持有。账号 partition 在同一 Electron 进程内重建窗口后仍保留,但不跨应用退出持久化。

Initial guest policy

只有主应用窗口启用 webviewTag。主进程只接受该窗口应用顶层 frame 发起的 guest 申请,并在放行前校验一次性租约、partition 与无活动内容的初始 about:blank 文档。主进程替换 renderer 提供的偏好:不启用 Node integration、guest preload、嵌套 webview、plugin、不安全内容、模态对话框或拖放导航;sandbox、context isolation 与 Web security 保持开启。

guest Session 不注册应用协议,也不继承应用的认证请求转发。权限请求与检查、设备访问、屏幕捕获、下载、原生弹窗与 HTTP 认证提示全部拒绝。通过检查且不带 POST body 的直接 HTTP(S) 弹窗请求,通过活动租约路由为新 Sidebar Tab;脚本操作空白窗口和 POST 弹窗流程仍不支持。导航接受不带内嵌凭据的 HTTP(S);请求过滤拒绝本地文件、特权协议与已知 DSH Host 地址,包括常见 loopback 别名。这不是通用的私有网络或 DNS rebinding 防火墙。

Desktop toolbar 没有关闭 sandbox 的开关。实现不增加远程调试端口或 browser-use 集成。未来的自动化提供方需要经过认证、限定 target 的 broker,而不是访问全部应用 target。把 target/生命周期句柄与导航命令分开的方式,与 Playwright 的 Android WebView 和 Page 对象区分相同;设备级输入属于另一项职责。

Alternatives considered

在按可见性挂载的 tab body 内渲染 guest。 卸载会断开 guest;直接挂载需要 Sidebar 提供 Body 保活与稳定祖先机制。

在可见与隐藏 DOM 容器之间搬移 guest。 Electron 44 的 WebViewElement.disconnectedCallback 会 detach guest 并重置内部实例。保留元素引用或 React key 不能保留该实例,因此父节点保持固定。

每个 tab 使用独立 partition。 这会隔离相关页面的站点账号。Workspace 所有权提供所需的共享,同时不共享应用 Session;每个 tab 仍拥有独立 guest。

修改活动 webview 的 partition。 Electron 在首次导航前确定 partition。选择其他隔离粒度需要替换 guest,并明确已有站点数据的处置策略,而不是实时切换属性。

Consequences

Sidebar 持有的稳定祖先保留页面,无需 Browser 自行处理几何或遮挡。隐藏 guest 保留页面内存,也可能继续联网;当前没有空闲回收策略。持久化存储、可选择的隔离级别与权限授权 UI 留作独立工作。临时 Workspace partition 是当前策略,并不意味着 Workspace 隔离必然要求临时存储。

定向测试覆盖原生导航错误恢复、preload 监听范围和已迁移的 iframe 行为。这些测试不能确认真实 Electron 的挂载时序、遮挡、焦点、平台样式或存储隔离;这些仍是运行时验证缺口。本次变更不附带 GUI 录像。此实现不构成完整浏览器安全策略已经可以发布的证据。

包内的 Electron 提供方、呈现适配器、页面工厂与 Workspace 存储解析集中在 src/client/electron/. 在原生 Electron 测试环境覆盖 guest 行为之前, 该目录暂时不参与逐文件覆盖率门禁; 其单元测试仍然运行. 共享 Browser 控制器、恢复 UI、iframe 代码和 Sidebar 保活仍遵循现有覆盖率要求.