From dfb07a038ca3d14bc0059982f34e247db311d123 Mon Sep 17 00:00:00 2001
From: 07akioni <07akioni2@gmail.com>
Date: Tue, 15 Sep 2026 16:48:27 +0800
Subject: [PATCH] feat(desktop): replace failure pages with native recovery
dialog
---
...mmediate-window-and-direct-start.i18n.yaml | 4 +-
...sktop-immediate-window-and-direct-start.md | 2 +-
...op-immediate-window-and-direct-start.zh.md | 2 +-
...6-09-09-desktop-in-place-profile.i18n.yaml | 4 +-
.../2026-09-09-desktop-in-place-profile.md | 2 +-
.../2026-09-09-desktop-in-place-profile.zh.md | 2 +-
.../2026-09-10-desktop-web-wrapper.i18n.yaml | 4 +-
.../2026-09-10-desktop-web-wrapper.md | 2 +-
.../2026-09-10-desktop-web-wrapper.zh.md | 2 +-
...15-desktop-native-fatal-recovery.i18n.yaml | 6 +
...026-09-15-desktop-native-fatal-recovery.md | 27 +++
...-09-15-desktop-native-fatal-recovery.zh.md | 27 +++
apps/desktop/README.i18n.yaml | 4 +-
apps/desktop/README.md | 12 +-
apps/desktop/README.zh.md | 12 +-
apps/desktop/renderer/startup.css | 16 --
apps/desktop/renderer/startup.html | 26 --
apps/desktop/renderer/startup.js | 45 ----
apps/desktop/src/backend-controller.ts | 2 +-
apps/desktop/src/fatal-recovery.ts | 62 +++++
apps/desktop/src/ipc.ts | 11 +-
apps/desktop/src/locale.ts | 26 +-
apps/desktop/src/main.ts | 182 +++++---------
apps/desktop/src/preload-app.ts | 29 +--
apps/desktop/src/project-manager.ts | 28 +--
apps/desktop/src/startup-document.ts | 26 --
.../desktop/tests/development-project.spec.ts | 2 +-
.../tests/expected/fatal-dialog-en.txt | 9 +
.../tests/expected/fatal-dialog-zh-CN.txt | 9 +
.../tests/expected/startup-en-profile.txt | 8 -
.../tests/expected/startup-en-restart.txt | 5 -
.../tests/expected/startup-zh-CN-profile.txt | 8 -
.../tests/expected/startup-zh-CN-restart.txt | 5 -
apps/desktop/tests/fatal-recovery.spec.ts | 85 +++++++
apps/desktop/tests/main-startup.spec.ts | 229 +++++++++---------
apps/desktop/tests/preload-app.spec.ts | 30 +--
apps/desktop/tests/project-manager.spec.ts | 78 +++---
apps/desktop/tests/startup-renderer.spec.ts | 175 -------------
apps/web/src/main.ts | 48 ++--
apps/web/tests/desktop-boot.spec.ts | 64 +++++
packages/client/web/README.i18n.yaml | 4 +-
packages/client/web/README.md | 2 +-
packages/client/web/README.zh.md | 2 +-
packages/client/web/src/boot.ts | 12 +-
packages/client/web/tests/boot.client.spec.ts | 47 ++++
45 files changed, 639 insertions(+), 748 deletions(-)
create mode 100644 .agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.i18n.yaml
create mode 100644 .agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.md
create mode 100644 .agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.zh.md
delete mode 100644 apps/desktop/renderer/startup.css
delete mode 100644 apps/desktop/renderer/startup.html
delete mode 100644 apps/desktop/renderer/startup.js
create mode 100644 apps/desktop/src/fatal-recovery.ts
delete mode 100644 apps/desktop/src/startup-document.ts
create mode 100644 apps/desktop/tests/expected/fatal-dialog-en.txt
create mode 100644 apps/desktop/tests/expected/fatal-dialog-zh-CN.txt
delete mode 100644 apps/desktop/tests/expected/startup-en-profile.txt
delete mode 100644 apps/desktop/tests/expected/startup-en-restart.txt
delete mode 100644 apps/desktop/tests/expected/startup-zh-CN-profile.txt
delete mode 100644 apps/desktop/tests/expected/startup-zh-CN-restart.txt
create mode 100644 apps/desktop/tests/fatal-recovery.spec.ts
delete mode 100644 apps/desktop/tests/startup-renderer.spec.ts
create mode 100644 apps/web/tests/desktop-boot.spec.ts
diff --git a/.agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.i18n.yaml
index 03f928b2f2..6a440cf660 100644
--- a/.agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.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/architecture/2026-09-09-desktop-immediate-window-and-direct-start.md
-2026-09-09-desktop-immediate-window-and-direct-start.md: 461f82719fabba67bd93c4c1f2a37d02fac1c06b
-2026-09-09-desktop-immediate-window-and-direct-start.zh.md: efbeeb20993fdc6baeb11ca1ea1d1af5501567dc
+2026-09-09-desktop-immediate-window-and-direct-start.md: 2d325d5c9f9c3fc8dd83816538d0a1b4b4c5480b
+2026-09-09-desktop-immediate-window-and-direct-start.zh.md: 8aebe6bc8c86cc14b78d9dc979eeb902493de8ae
diff --git a/.agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.md b/.agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.md
index 461f82719f..2d325d5c9f 100644
--- a/.agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.md
+++ b/.agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.md
@@ -14,7 +14,7 @@ Waiting for backend readiness leaves users without a window during profile prepa
Electron creates the main window with the packaged Web loading page before profile reconciliation or Host startup. The Web entry draws its boot page before awaiting Host readiness. The owned preload delivers structured boot injections, and the existing document activates its client plugins after they are applied; startup failures display diagnostics and available recovery actions. Closing during loading cancels further startup work and waits for the pending child to exit.
-The main window owns recovery because the failed Host cannot supply its own controls. Error pages retain diagnostics, restart, and reinstallation guidance. Disabling plugins and resetting Desktop are available with loaded runtime metadata and available resources, including in development mode. Reset removes all profile contents except its held lock, without a backup; shared product data and the Harness-home environment file remain intact. The profile directory remains in place so another transaction cannot acquire a replacement lock during cleanup. Self-contained recovery controls use intercepted form navigation when preload is unavailable. A crashed renderer invalidates the navigation cache so the startup page loads again.
+Fatal presentation follows [native Desktop recovery](2026-09-15-desktop-native-fatal-recovery.md). Window timing, direct Host startup, and shutdown ownership remain governed here.
Desktop starts the actual Host through the [shared Web runner](2026-09-10-desktop-web-wrapper.md) after preparing the profile in place. Readiness supplies the authenticated Host URL and boot injections. The shell exchanges the URL for a Host cookie, forwards application HTTP requests, and authenticates direct WebSocket requests only for the owned application origin. This carrier adaptation preserves Web route and stream semantics while allowing static HTML to appear before the Host. Package mutations retain pnpm lifecycle scripts and locking. Failures retain partial changes for explicit repair; there is no automatic profile rollback.
diff --git a/.agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.zh.md b/.agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.zh.md
index efbeeb2099..8aebe6bc8c 100644
--- a/.agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-09-desktop-immediate-window-and-direct-start.zh.md
@@ -14,7 +14,7 @@ profile 修改与恢复遵循[直接修改 profile 决策](2026-09-09-desktop-in
Electron 在 profile 校准或 Host 启动前创建带打包 Web 加载页的主窗口。Web 入口先显示启动页,再等待 Host 就绪。自有 preload 交付结构化启动注入,现有文档应用注入后激活客户端插件;启动失败时显示诊断和可用恢复操作。加载期间关闭窗口会取消后续启动工作,并等待正在启动的子进程退出。
-主窗口提供恢复操作,因为失败的 Host 无法提供自身控件。错误页保留诊断、重启和重装指导。加载了运行时元数据且资源可用时,包括开发模式,才提供禁用插件和重置 Desktop。重置会删除 profile 中除所持锁文件外的所有内容,不保留备份;共享产品数据和 Harness-home 环境文件保持完整。profile 目录保持原位,避免清理期间另一事务获取替代锁。preload 不可用时,独立恢复控件使用被拦截的表单导航。渲染进程崩溃会使导航缓存失效,以重新加载启动页。
+致命错误展示遵循[原生 Desktop 恢复](2026-09-15-desktop-native-fatal-recovery.zh.md)。窗口展示时机、直接启动 Host 和关闭所有权仍由本文规定。
Desktop 原位准备 profile 后,通过[共享 Web runner](2026-09-10-desktop-web-wrapper.zh.md)启动实际 Host。就绪消息提供认证 Host URL 与启动注入。壳使用该 URL 换取 Host cookie,转发应用 HTTP 请求,并仅为归属的应用 origin 认证直接 WebSocket 请求。这一载体适配保留 Web 路由与流语义,同时允许静态 HTML 在 Host 之前显示。包变更保留 pnpm 生命周期脚本与锁。失败保留部分变更以供显式修复,不会自动回滚 profile。
diff --git a/.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.i18n.yaml
index 23f7613c0e..cd8ffea2f6 100644
--- a/.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.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/architecture/2026-09-09-desktop-in-place-profile.md
-2026-09-09-desktop-in-place-profile.md: 425145ad89f9697c2420544b0ce2e70759313ffc
-2026-09-09-desktop-in-place-profile.zh.md: 8b98ec1c2e426b0019115a9a19eb84db4b9caef8
+2026-09-09-desktop-in-place-profile.md: 18158cff7478624f2dffbfbccd987c6a3dccfe9d
+2026-09-09-desktop-in-place-profile.zh.md: 3f20f4ef2594db0714ad200fb590c94fdd4a784e
diff --git a/.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.md b/.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.md
index 425145ad89..18158cff74 100644
--- a/.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.md
+++ b/.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.md
@@ -16,7 +16,7 @@ Package or Host failures retain partial changes for repair and retry. There is n
This supersedes staging and rollback in [the packaging decision](2026-08-25-electron-desktop-packaging-and-updates.md), [the bundled-runtime decision](2026-09-08-desktop-bundled-runtime-and-external-plugins.md), and [the immediate-window decision](2026-09-09-desktop-immediate-window-and-direct-start.md). Host boot follows the [thin-wrapper decision](2026-09-10-desktop-web-wrapper.md); release, module ownership, and window lifecycle remain separate decisions.
-Desktop delegates installation and lifecycle scripts to pnpm, without a pending-operation startup gate, frozen-lockfile reinstall, or automatic rebuild. Failed package operations preserve partial changes and leave disable, remove, reset, and startup retry available. The Host inherits the user environment, and profiles may use directory links. An unchanged legacy Desktop-generated pnpm configuration is replaced with the Web defaults; customized configuration remains user-owned.
+Desktop delegates installation and lifecycle scripts to pnpm, without a pending-operation startup gate, frozen-lockfile reinstall, or automatic rebuild. Failed package operations preserve partial changes and leave disable, remove, and startup retry available. The Host inherits the user environment, and profiles may use directory links. An unchanged legacy Desktop-generated pnpm configuration is replaced with the Web defaults; customized configuration remains user-owned.
## Alternatives considered
diff --git a/.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.zh.md b/.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.zh.md
index 8b98ec1c2e..3f20f4ef25 100644
--- a/.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-09-desktop-in-place-profile.zh.md
@@ -16,7 +16,7 @@ Desktop 停止 Host 后直接修改当前 profile。共享 app-boot 清理在包
本记录取代[打包决策](2026-08-25-electron-desktop-packaging-and-updates.zh.md)、[内置运行时决策](2026-09-08-desktop-bundled-runtime-and-external-plugins.zh.md)及[立即显示窗口决策](2026-09-09-desktop-immediate-window-and-direct-start.zh.md)中的暂存与回滚。Host 启动遵循[薄壳决策](2026-09-10-desktop-web-wrapper.zh.md);发布、模块归属与窗口生命周期仍由各自决策负责。
-Desktop 将安装和生命周期脚本交给 pnpm,不设置待完成操作启动门禁、不强制按锁文件重装,也不自动重建。包操作失败会保留部分变更,仍可禁用、删除、重置和重试启动。Host 继承用户环境,profile 可以使用目录链接。未经修改的旧版 Desktop 生成 pnpm 配置替换为 Web 默认值;自定义配置仍由用户管理。
+Desktop 将安装和生命周期脚本交给 pnpm,不设置待完成操作启动门禁、不强制按锁文件重装,也不自动重建。包操作失败会保留部分变更,仍可禁用、删除和重试启动。Host 继承用户环境,profile 可以使用目录链接。未经修改的旧版 Desktop 生成 pnpm 配置替换为 Web 默认值;自定义配置仍由用户管理。
## 考虑过的替代方案
diff --git a/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.i18n.yaml
index bd784e11ed..3faa08745c 100644
--- a/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.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/architecture/2026-09-10-desktop-web-wrapper.md
-2026-09-10-desktop-web-wrapper.md: 5b8df22e520af752ac1b8fefbf2cf2442ce17eca
-2026-09-10-desktop-web-wrapper.zh.md: 44c79d8c914f0b2bdeb69dbeb09f22ce84efe9c8
+2026-09-10-desktop-web-wrapper.md: 33a1852ef7dbae136370e6ee30f557534ab51751
+2026-09-10-desktop-web-wrapper.zh.md: 7b652fddd0ef92a4797ae4f1a799f9815e787843
diff --git a/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.md b/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.md
index 5b8df22e52..33a1852ef7 100644
--- a/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.md
+++ b/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.md
@@ -38,7 +38,7 @@ This partially supersedes the private composition and portless transport in the
## Consequences
-Desktop inherits Web features through the same boot and serving path. HTTP listener ownership and authentication remain part of application startup. Electron uses the reported Host address and preserves the existing Web document through readiness. The shared Web loading page is available before the Host starts; independent recovery resources remain available when startup fails.
+Desktop inherits Web features through the same boot and serving path. HTTP listener ownership and authentication remain part of application startup. Electron uses the reported Host address and preserves the existing Web document through readiness. The shared Web loading page is available before the Host starts; native recovery remains available when startup fails.
User-selected runtime options, package sources, and permitted lifecycle scripts can affect Host execution, load third-party code, or cause startup failure. Desktop accepts these effects under the same configuration ownership as Web; the signed core runtime does not attest to user-installed plugin code. Package or loading failures retain explicit repair and the independent recovery UI rather than triggering stricter admission checks or automatic rollback.
diff --git a/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.zh.md b/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.zh.md
index 44c79d8c91..7b652fddd0 100644
--- a/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.zh.md
+++ b/.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.zh.md
@@ -38,7 +38,7 @@ App-boot 负责已安装依赖发现、安装目录优先的 bundle 声明解析
## Consequences
-Desktop 通过相同启动与服务路径继承 Web 功能。HTTP 监听归属与认证仍属于应用启动。Electron 使用报告的 Host 地址,并在就绪前后保留现有 Web 文档。共享 Web 加载页在 Host 启动前可用;独立恢复资源在启动失败时仍可用。
+Desktop 通过相同启动与服务路径继承 Web 功能。HTTP 监听归属与认证仍属于应用启动。Electron 使用报告的 Host 地址,并在就绪前后保留现有 Web 文档。共享 Web 加载页在 Host 启动前可用;原生恢复在启动失败时仍可用。
用户选择的运行时选项、包来源及允许的生命周期脚本可以影响 Host 执行、加载第三方代码或导致启动失败。Desktop 按与 Web 相同的配置归属接受这些影响;签名核心运行时不为用户安装的插件代码背书。包操作或加载失败保留显式修复及独立恢复 UI,不触发更严格的准入检查或自动回滚。
diff --git a/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.i18n.yaml
new file mode 100644
index 0000000000..c65b9d5ab8
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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/architecture/2026-09-15-desktop-native-fatal-recovery.md
+2026-09-15-desktop-native-fatal-recovery.md: 2c38433fb1e12b6d292982d709ddd7d53a93851b
+2026-09-15-desktop-native-fatal-recovery.zh.md: 50e352f4378722d08bd5815cf0274e31c57baaac
diff --git a/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.md b/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.md
new file mode 100644
index 0000000000..2c38433fb1
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.md
@@ -0,0 +1,27 @@
+# Agent Note: Native Desktop fatal recovery
+
+Status: implemented
+
+English | [中文](2026-09-15-desktop-native-fatal-recovery.zh.md)
+
+## Problem
+
+A recovery document depends on the renderer and preload whose failure can prevent application startup. Multiple reports from one failed startup can also obscure the original diagnostic and interrupt recovery.
+
+## Decision
+
+Electron owns one native fatal dialog per application process. Explicit main-window creation, document-load, preload, renderer, Web initialization, and backend failures enter this path. Ordinary requests and package operations retain their local error handling; expected cancellation and shutdown do not enter recovery. No elapsed-time heuristic classifies a slow startup as fatal.
+
+The first report claims presentation before awaiting the dialog. Later reports remain in logs. The dialog retains the first diagnostic and offers exit, restart, or disabling third-party bundles followed by a whole-application restart. Disabling writes activation metadata under the existing profile transaction lock after Host shutdown, without requiring runtime initialization or deleting installed files. An explicit recovery-operation failure is presented separately and does not count as another automatic fatal report.
+
+The Web document stays in place. A carrier callback owns startup failure presentation while the shared boot page retains its spinner; ordinary browser boot still renders its own failure report. Only the primary application frame may report a Web boot failure. Desktop has no profile reset or emergency recovery document.
+
+This supersedes recovery-page and reset behavior in the [immediate-window decision](2026-09-09-desktop-immediate-window-and-direct-start.md), whose immediate visibility and direct Host startup rationale remain active. The [in-place profile decision](2026-09-09-desktop-in-place-profile.md) still owns package transactions and partial changes.
+
+## Alternatives considered
+
+A Web modal depends on client initialization, while a second recovery document adds renderer resources and preload recovery paths. Native dialogs remain usable when those components fail. Automatically resetting configuration or restarting on every report can delete user configuration or create restart loops; explicit actions preserve user control.
+
+## Consequences
+
+Recovery cannot report a killed or crashed Electron main process, and a silent startup hang has no automatic timeout prompt. Invalid profile JSON can prevent disabling plugins; exit and restart remain available after the operation reports its failure. Focused lifecycle tests cover fatal signals, cancellation, first-report deduplication, and shutdown ordering; locale expectations record dialog diagnostics and actions, and boot tests retain ordinary browser failure presentation.
diff --git a/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.zh.md b/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.zh.md
new file mode 100644
index 0000000000..50e352f437
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.zh.md
@@ -0,0 +1,27 @@
+# Agent Note: 原生 Desktop 致命错误恢复
+
+Status: implemented
+
+[English](2026-09-15-desktop-native-fatal-recovery.md) | 中文
+
+## Problem
+
+恢复文档依赖渲染器和 preload,而这些组件的故障本身就可能阻止应用启动。同一次启动失败的多个报告还可能掩盖原始诊断并打断恢复操作。
+
+## Decision
+
+Electron 在每个应用进程中提供一次原生致命错误对话框。明确的主窗口创建、文档加载、preload、渲染器、Web 初始化和后端失败进入此路径。普通请求和包操作保留局部错误处理;预期取消和关闭不进入恢复。不通过耗时推断慢启动为致命故障。
+
+首次报告在等待对话框前取得展示权。后续报告保留在日志中。对话框保留首次诊断,提供退出、重启或禁用第三方 bundle 后重启整个应用。禁用操作等待 Host 关闭后,在已有 profile 事务锁内写入启用元数据,不要求运行时初始化,也不删除安装文件。显式恢复操作失败会单独展示,不计为另一次自动致命报告。
+
+Web 文档保留在原位。宿主回调负责启动失败展示,共享启动页保留加载动画;普通浏览器启动仍显示自身的失败报告。只有主应用框架可以上报 Web 启动失败。Desktop 不提供 profile 重置或应急恢复文档。
+
+这取代了[立即显示窗口决策](2026-09-09-desktop-immediate-window-and-direct-start.zh.md)中的恢复页和重置行为;该决策关于立即可见性和直接启动 Host 的理由仍然有效。[原位 profile 决策](2026-09-09-desktop-in-place-profile.zh.md)仍负责包事务和部分变更。
+
+## Alternatives considered
+
+Web 模态框依赖客户端初始化,而第二份恢复文档会增加渲染器资源和 preload 恢复路径。原生对话框在这些组件失败时仍然可用。自动重置配置或每次报告都重启可能删除用户配置或形成重启循环;显式操作保留用户控制权。
+
+## Consequences
+
+恢复功能无法报告 Electron 主进程被终止或崩溃,静默启动挂起也没有自动超时提示。无效的 profile JSON 可能阻止禁用插件;操作报告失败后仍可退出和重启。定向生命周期测试覆盖致命信号、取消、首次报告去重和关闭顺序;语言预期记录对话框诊断和操作,启动测试保留普通浏览器失败展示。
diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml
index 80737cef53..fc29f576eb 100644
--- a/apps/desktop/README.i18n.yaml
+++ b/apps/desktop/README.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 apps/desktop/README.md
-README.md: 2793dae3a6bd0ca9ce5b4a7c13e539a861741994
-README.zh.md: 2beb07eb7580154a3aafb6fbea37157529e8d4f8
+README.md: 4616ec154a73a871a0e995fd231db1ba62e62671
+README.zh.md: 5bfa15dcd309b28fd9f3f4c668417fb98a152b00
diff --git a/apps/desktop/README.md b/apps/desktop/README.md
index 2793dae3a6..4616ec154a 100644
--- a/apps/desktop/README.md
+++ b/apps/desktop/README.md
@@ -29,7 +29,7 @@ Node prepares the bundled interpreters and Python libraries without a system Pyt
| Package sources | Core installation at startup adds work even when offline. | `app.asar/dsh` carries a complete production dependency tree; the profile installs only external plugins. |
| Shared modules | Host APIs can depend on module identity. | The shared profile runner projects missing installation and bundle dependencies inside the Desktop profile; pnpm-managed packages take precedence. |
| State ownership | Sharing executable dependency graphs would let CLI and Desktop change each other's dsh, Cordis, plugin, or native-module versions, while two desktop processes could race on the same profile. | Electron acquires its process-lifetime single-instance lock before any profile access and exclusively owns `$DSH_HOME/profiles/desktop` plus its package-manager state. CLI and Desktop share supported product data under `$DSH_HOME`, but never executable packages, plugin activation, lockfiles, or `node_modules`. |
-| Transport | Reusing Web serving and authentication keeps application behavior in one implementation. | Electron loads the Host’s authenticated HTTP URL directly; child IPC carries lifecycle messages, and the local shell protocol serves startup and management pages. |
+| Transport | Web serving and authentication share one implementation. | Electron loads packaged Web assets; the Host supplies boot injections and authenticated APIs. The shell protocol serves plugin management. |
| Plugin changes | Package installation and Host startup can fail. | Desktop stops the Host and modifies the current profile directly. Failures retain partial changes for explicit repair; there is no automatic profile rollback. |
| Updates | Independent shell and dsh updates would recreate version splits, while unchanged shell blocks should not require a complete transfer. | The Electron shell, matching dsh runtime and pnpm form one signed update unit. Platform update artifacts may reuse unchanged blocks, but runtime version selection never splits from the Desktop release. |
@@ -39,11 +39,11 @@ The [thin-wrapper decision](../../.agents/notes/implemented/architecture/2026-09
Electron owns `$DSH_HOME/profiles/desktop`. Its `dependencies` contains packages installed by pnpm; `dsh.profile.bundles` contains the built-in bundles followed by enabled plugins. The signed application supplies dsh, the private Desktop Host, and their production packages from `resources/app.asar/dsh`. Packaged applications select runtime profile resolution without creating package links; development profiles use filesystem links. Both host and plugins execute in the same Electron Node-mode process; Desktop does not enable `--preserve-symlinks`. The CLI cannot boot or mutate this profile.
-The local startup page exposes startup status and available recovery actions. The product renderer uses the Web application’s HTTP APIs. The separate plugin window receives structured list, install, remove, update, and update-check operations; neither renderer receives filesystem access, raw Electron IPC, a shell, or arbitrary pnpm arguments.
+The application preload exposes boot readiness and fatal startup reporting. The product renderer uses the Web application’s HTTP APIs. The separate plugin window receives structured list, install, remove, update, and update-check operations; neither renderer receives filesystem access, raw Electron IPC, a shell, or arbitrary pnpm arguments.
The product UI retains Web actions, including "Open In..." through the shared authenticated HTTP routes. Desktop uses Web's automatic directory-picker selection and initializes new profiles with the shared Web template's bundles and patch-reload policy.
-Electron chooses typed English or Chinese shell copy from its application locale and falls back to English. Menus, native dialogs, the startup page, and the plugin-management renderer use the same locale payload; the repository Client UI i18n gate checks these desktop sources.
+Electron chooses typed English or Chinese shell copy from its application locale and falls back to English. Menus, native dialogs, and the plugin-management renderer use the same locale payload; the repository Client UI i18n gate checks these desktop sources.
Electron's native Edit menu supplies undo, redo, cut, copy, paste, and select-all commands and platform shortcuts for the focused window. Right-clicking an editable field opens these commands without shortcut labels, with availability supplied by Chromium; selected read-only text offers Copy.
@@ -59,13 +59,13 @@ The signed `resources/app.asar/dsh/desktop-runtime.json` binds the shell version
CLI and Desktop use the same installed-dependency inventory and bundle reconciliation. Bundle declarations resolve with the same installation-first precedence as startup. CLI operations automatically enable installed bundles; Desktop preserves bundles disabled through its UI across updates. Neither path requires readable installed metadata to list or remove a dependency.
-The loading page does not depend on the Host. Errors offer restart and reinstallation guidance. Disabling plugins and resetting Desktop are offered when runtime resources support profile recovery, including development mode; early initialization failures expose restart alone. The plugin manager remains available through the application menu. Plugin changes have no automatic rollback.
+Fatal main-window creation, main-document loading, preload, renderer, Web initialization, or backend failures open one native recovery dialog per application process. It shows the first error and offers Exit, Restart, and Disable all third-party plugins and restart. Startup failures retain the Web loading page and spinner; runtime failures retain the current page. Expected shutdowns, cancelled navigation, ordinary requests, and package-operation errors do not trigger recovery. There is no startup timeout heuristic.
Host error diagnostics retain only the last 64 Ki characters written to stderr. Earlier output is discarded so a long-running Host does not grow the shell’s diagnostic buffer indefinitely.
-Reset deletes every entry in `$DSH_HOME/profiles/desktop` except the held transaction lock, then initializes the built-in profile. It removes Desktop configuration and installed third-party packages without a backup. Shared tasks, settings, and the Harness-home `.env` are untouched. Shell resource and preload failures use a self-contained document with the available recovery actions and diagnostics; its controls do not require preload.
+Recovery waits for Host shutdown before changing plugin activation. Disabling third-party bundles writes the profile under its transaction lock without loading runtime metadata or deleting files. Invalid profile data or write failures are reported as recovery-operation errors; Desktop does not restart as though disabling succeeded. Desktop has no profile-reset action or emergency HTML document.
-Package transactions hold `$DSH_HOME/profiles/desktop/lock` exclusively through pnpm process exit. Before pnpm runs, the shared module-fallback helper removes only its owned links and preserves pnpm-managed directories; the Host recreates needed links on startup. Reset preserves the profile directory and its lock until initialization and Host startup finish. Link cleanup preserves target directories. Native builds follow pnpm’s configured build policy; release preparation owns its separate build-time allowlist.
+Package transactions hold `$DSH_HOME/profiles/desktop/lock` exclusively through pnpm process exit. Before pnpm runs, the shared module-fallback helper removes only its owned links and preserves pnpm-managed directories; the Host recreates needed links on startup. Link cleanup preserves target directories. Native builds follow pnpm’s configured build policy; release preparation owns its separate build-time allowlist.
## Develop
diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md
index 2beb07eb75..5bfa15dcd3 100644
--- a/apps/desktop/README.zh.md
+++ b/apps/desktop/README.zh.md
@@ -29,7 +29,7 @@ Node 准备内置解释器和 Python 库,无需系统 Python 或 pip。[下载
| 包来源 | 即使离线,启动时安装核心依赖也会增加开销。 | `app.asar/dsh` 携带完整生产依赖树;profile 只安装外部插件。 |
| 共享模块 | Host API 可能依赖模块身份。 | 共享 profile runner 在 Desktop profile 内补全安装包与 bundle 缺失的依赖;pnpm 管理的包优先。 |
| 状态归属 | 共享可执行依赖图会让 CLI(命令行界面)与 Desktop 相互改变 dsh、Cordis、插件或原生模块版本,而两个桌面进程还可能争用同一个 profile。 | Electron 在访问任何 profile 前获取进程生命周期单实例锁,并独占 `$DSH_HOME/profiles/desktop` 及其包管理器状态。CLI 与 Desktop 共享 `$DSH_HOME` 下受支持的产品数据,但绝不共享可执行包、插件激活、锁文件或 `node_modules`。 |
-| 传输 | 复用 Web 服务与认证,让应用行为由同一份实现负责。 | Electron 直接加载 Host 的认证 HTTP URL;子进程 IPC 承载生命周期消息,本地壳协议提供启动和管理页面。 |
+| 传输 | Web 服务与认证共享一套实现。 | Electron 加载打包的 Web 资源;Host 提供启动注入和经过认证的 API。shell 协议提供插件管理页面。 |
| 插件变更 | 包安装和 Host 启动可能失败。 | Desktop 停止 Host 后直接修改当前 profile。失败保留部分修改供用户修复,不自动回滚 profile。 |
| 更新 | 桌面壳与 dsh 独立更新会重新产生版本分裂,而桌面壳未变化的数据块不应强制完整传输。 | Electron 壳、匹配的 dsh 运行时与 pnpm 组成一个已签名更新单元。平台更新产物可以复用未变化的数据块,但运行时版本选择绝不脱离 Desktop 发布。 |
@@ -39,11 +39,11 @@ Node 准备内置解释器和 Python 库,无需系统 Python 或 pip。[下载
Electron 拥有 `$DSH_HOME/profiles/desktop`。其 `dependencies` 包含 pnpm 安装的包;`dsh.profile.bundles` 包含内置 bundle,后接已启用插件。签名应用从 `resources/app.asar/dsh` 提供 dsh、私有 Desktop Host 及其生产依赖。打包应用选择 runtime profile 解析,不创建包链接;开发 profile 使用文件系统链接。宿主与插件在同一个 Electron Node 模式进程中执行;Desktop 不启用 `--preserve-symlinks`。CLI 不能启动或修改此 profile。
-本地启动页面展示启动状态及可用恢复操作。产品渲染进程使用 Web 应用的 HTTP API。独立插件窗口接收结构化的列表、安装、移除、更新和检查更新操作;两个渲染进程都不会获得文件系统、原始 Electron IPC、shell 或任意 pnpm 参数访问权。
+应用 preload 暴露启动就绪和致命启动失败上报。产品渲染器使用 Web 应用的 HTTP API。独立插件窗口接收结构化的列表、安装、移除、更新和检查更新操作;两种渲染器都无法访问文件系统、原始 Electron IPC、shell 或任意 pnpm 参数。
产品 UI 保留 Web 操作,包括通过共享认证 HTTP 路由执行的“打开方式…”。Desktop 使用 Web 的自动目录选择机制,并以共享 Web 模板的 bundle 列表和 patch 重载策略初始化新 profile。
-Electron 根据应用 locale 选择类型化的英文或中文桌面壳文案,并以英文作为 fallback。菜单、原生对话框、启动页与插件管理渲染进程使用同一 locale 数据;仓库的 Client UI i18n gate 会检查这些桌面源文件。
+Electron 根据应用语言选择类型化的英文或中文 shell 文案,并回退到英文。菜单、原生对话框和插件管理渲染器使用同一份语言数据;仓库 Client UI i18n 检查覆盖这些桌面端源码。
Electron 原生“编辑”菜单为当前聚焦窗口提供撤销、重做、剪切、复制、粘贴和全选命令及平台快捷键。右键点击可编辑输入区域会打开不带快捷键标注的这些命令,其可用状态由 Chromium 提供;选中的只读文本提供“复制”命令。
@@ -59,13 +59,13 @@ Electron 原生“编辑”菜单为当前聚焦窗口提供撤销、重做、
CLI 与 Desktop 共用已安装依赖清单及 bundle 列表协调逻辑。bundle 声明遵循与启动一致的安装目录优先解析顺序。CLI 操作自动启用已安装 bundle;Desktop 更新后保留通过 UI 禁用的 bundle 状态。两条路径都不要求已安装元数据可读才能列出或移除依赖。
-加载页不依赖 Host。错误页提供重启和重装指导。运行时资源支持 profile 恢复时,即可禁用插件和重置 Desktop,包括开发模式;早期初始化失败只提供重启。应用菜单仍提供插件管理器入口。插件修改不自动回滚。
+主窗口创建、主文档加载、preload、渲染器、Web 初始化或后端的致命失败,会在每个应用进程中打开一次原生恢复对话框。对话框显示首次错误,并提供退出、重启、禁用全部第三方插件并重启。启动失败保留 Web 加载页和动画;运行中失败保留当前页面。预期关闭、取消导航、普通请求和包操作错误不会触发恢复。不通过启动超时推断故障。
Host 错误诊断仅保留 stderr 输出的最后 64 Ki 个字符。更早的输出会被丢弃,避免长期运行的 Host 使壳的诊断缓冲区无限增长。
-重置删除 `$DSH_HOME/profiles/desktop` 中除所持事务锁外的所有条目,然后初始化内置 profile。它删除 Desktop 配置和已安装第三方包,不保留备份。共享任务、设置和 Harness-home `.env` 保持不变。壳资源和 preload 失败时使用独立文档显示可用恢复操作和诊断;其控件不依赖 preload。
+恢复操作等待 Host 关闭后才修改插件启用状态。禁用第三方 bundle 时持有事务锁写入 profile,不加载运行时元数据,也不删除文件。profile 数据无效或写入失败会作为恢复操作错误报告;Desktop 不会假装禁用成功后重启。Desktop 不提供 profile 重置操作或应急 HTML 文档。
-包事务独占 `$DSH_HOME/profiles/desktop/lock` 直到 pnpm 进程退出。pnpm 运行前,共享模块补全 helper 仅移除其拥有的链接,并保留 pnpm 管理的目录;Host 在启动时重新创建所需链接。重置保留 profile 目录与锁,直到初始化和 Host 启动结束。链接清理保留目标目录。原生构建遵循 pnpm 配置的构建策略;发布准备负责独立的构建时许可列表。
+包事务独占 `$DSH_HOME/profiles/desktop/lock`,直到 pnpm 进程退出。pnpm 运行前,共享模块回退辅助函数只删除其拥有的链接,保留 pnpm 管理的目录;Host 在启动时重建所需链接。链接清理保留目标目录。原生构建遵循 pnpm 配置的构建策略;发布准备使用独立的构建期允许列表。
## 开发
diff --git a/apps/desktop/renderer/startup.css b/apps/desktop/renderer/startup.css
deleted file mode 100644
index a15f246ee3..0000000000
--- a/apps/desktop/renderer/startup.css
+++ /dev/null
@@ -1,16 +0,0 @@
-:root { color-scheme: light dark; font-family: system-ui, sans-serif; color: #202124; background: #fafafa; }
-body { margin: 0; min-height: 100vh; display: grid; place-items: center; }
-main { width: min(560px, calc(100vw - 64px)); padding: 40px 0; text-align: center; }
-h1 { margin: 24px 0 12px; font-size: 22px; font-weight: 600; }
-p { color: #666; line-height: 1.6; }
-#spinner { width: 32px; height: 32px; margin: auto; border: 3px solid #dedee3; border-top-color: #4d6bfe; border-radius: 50%; animation: spin 0.9s linear infinite; }
-#error { padding: 16px; border: 1px solid #e0e0e5; border-radius: 10px; max-height: 220px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; text-align: left; font: 13px/1.6 ui-monospace, monospace; }
-#actions { margin-top: 24px; }
-button { margin: 4px; padding: 10px 18px; border: 1px solid transparent; border-radius: 8px; color: #fff; background: #4d6bfe; font: inherit; cursor: pointer; }
-button.secondary { color: inherit; background: transparent; border-color: #c8c8d0; }
-button:disabled { opacity: 0.5; cursor: default; }
-button:focus-visible { outline: 2px solid #4d6bfe; outline-offset: 3px; }
-[hidden] { display: none !important; }
-@keyframes spin { to { transform: rotate(360deg); } }
-@media (prefers-color-scheme: dark) { :root { color: #ededf0; background: #171719; } p { color: #aaaab3; } #error { border-color: #38383f; } #spinner { border-color: #38383f; border-top-color: #6a85ff; } }
-@media (prefers-reduced-motion: reduce) { #spinner { animation: none; } }
diff --git a/apps/desktop/renderer/startup.html b/apps/desktop/renderer/startup.html
deleted file mode 100644
index 098efb4f92..0000000000
--- a/apps/desktop/renderer/startup.html
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-