From fdc4887c38e0856e2f884a00fc05ee983db9c163 Mon Sep 17 00:00:00 2001
From: winewill <324803222+winewill@users.noreply.github.com>
Date: Fri, 18 Sep 2026 18:26:40 +0800
Subject: [PATCH 01/26] fix(desktop): embed Windows mandatory updates in the
main window
---
...-movable-mandatory-update-window.i18n.yaml | 4 +-
...6-09-16-movable-mandatory-update-window.md | 1 +
...9-16-movable-mandatory-update-window.zh.md | 1 +
.agents/notes/archived/manifest.json | 3 +
apps/desktop/README.i18n.yaml | 4 +-
apps/desktop/README.md | 2 +-
apps/desktop/README.zh.md | 2 +-
.../renderer/mandatory-update-frame.js | 36 ++++++++++
apps/desktop/renderer/mandatory-update.html | 1 +
apps/desktop/src/mandatory-update-window.ts | 38 ++++++++--
apps/desktop/src/preload-app.ts | 2 +
apps/desktop/src/preload-mandatory-overlay.ts | 69 +++++++++++++++++++
apps/desktop/src/update-overlay.ts | 16 -----
apps/desktop/tests/README.i18n.yaml | 4 +-
apps/desktop/tests/README.md | 2 +-
apps/desktop/tests/README.zh.md | 2 +-
apps/desktop/tests/fixtures/local-updater.mjs | 67 +++++++++---------
.../tests/fixtures/mandatory-frame.mjs | 27 ++++++++
.../tests/fixtures/workspace-updates.mjs | 32 +++++----
.../tests/mandatory-update-window.spec.ts | 35 ++++++++--
apps/desktop/tests/preload-app.spec.ts | 1 +
.../tests/preload-mandatory-overlay.spec.ts | 66 ++++++++++++++++++
apps/desktop/tests/update-overlay.spec.ts | 24 +------
23 files changed, 337 insertions(+), 102 deletions(-)
rename .agents/notes/{implemented => archived}/bug-fix/2026-09-16-movable-mandatory-update-window.i18n.yaml (67%)
rename .agents/notes/{implemented => archived}/bug-fix/2026-09-16-movable-mandatory-update-window.md (98%)
rename .agents/notes/{implemented => archived}/bug-fix/2026-09-16-movable-mandatory-update-window.zh.md (98%)
create mode 100644 apps/desktop/renderer/mandatory-update-frame.js
create mode 100644 apps/desktop/src/preload-mandatory-overlay.ts
create mode 100644 apps/desktop/tests/fixtures/mandatory-frame.mjs
create mode 100644 apps/desktop/tests/preload-mandatory-overlay.spec.ts
diff --git a/.agents/notes/implemented/bug-fix/2026-09-16-movable-mandatory-update-window.i18n.yaml b/.agents/notes/archived/bug-fix/2026-09-16-movable-mandatory-update-window.i18n.yaml
similarity index 67%
rename from .agents/notes/implemented/bug-fix/2026-09-16-movable-mandatory-update-window.i18n.yaml
rename to .agents/notes/archived/bug-fix/2026-09-16-movable-mandatory-update-window.i18n.yaml
index 10665d6e64..e49130d5ee 100644
--- a/.agents/notes/implemented/bug-fix/2026-09-16-movable-mandatory-update-window.i18n.yaml
+++ b/.agents/notes/archived/bug-fix/2026-09-16-movable-mandatory-update-window.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/bug-fix/2026-09-16-movable-mandatory-update-window.md
-2026-09-16-movable-mandatory-update-window.md: 4695074429b76b467e6ec90201632dbb5a007e68
-2026-09-16-movable-mandatory-update-window.zh.md: 3b416c53dc02abce1276bb70a13bb45b4c1afa44
+2026-09-16-movable-mandatory-update-window.md: da996e5b37f990ff773e3ec88de0baed3c4b54f3
+2026-09-16-movable-mandatory-update-window.zh.md: 15fd4eb2520a39d8259321a747b33c833b56cbb1
diff --git a/.agents/notes/implemented/bug-fix/2026-09-16-movable-mandatory-update-window.md b/.agents/notes/archived/bug-fix/2026-09-16-movable-mandatory-update-window.md
similarity index 98%
rename from .agents/notes/implemented/bug-fix/2026-09-16-movable-mandatory-update-window.md
rename to .agents/notes/archived/bug-fix/2026-09-16-movable-mandatory-update-window.md
index 4695074429..da996e5b37 100644
--- a/.agents/notes/implemented/bug-fix/2026-09-16-movable-mandatory-update-window.md
+++ b/.agents/notes/archived/bug-fix/2026-09-16-movable-mandatory-update-window.md
@@ -1,6 +1,7 @@
# Agent Note: Movable mandatory-update window
Status: implemented
+Archived: 2026-09-18
English | [中文](2026-09-16-movable-mandatory-update-window.zh.md)
diff --git a/.agents/notes/implemented/bug-fix/2026-09-16-movable-mandatory-update-window.zh.md b/.agents/notes/archived/bug-fix/2026-09-16-movable-mandatory-update-window.zh.md
similarity index 98%
rename from .agents/notes/implemented/bug-fix/2026-09-16-movable-mandatory-update-window.zh.md
rename to .agents/notes/archived/bug-fix/2026-09-16-movable-mandatory-update-window.zh.md
index 3b416c53dc..15fd4eb252 100644
--- a/.agents/notes/implemented/bug-fix/2026-09-16-movable-mandatory-update-window.zh.md
+++ b/.agents/notes/archived/bug-fix/2026-09-16-movable-mandatory-update-window.zh.md
@@ -1,6 +1,7 @@
# Agent Note: 可移动的强更窗口
Status: implemented
+Archived: 2026-09-18
[English](2026-09-16-movable-mandatory-update-window.md) | 中文
diff --git a/.agents/notes/archived/manifest.json b/.agents/notes/archived/manifest.json
index 926441caeb..f1b6aa7b95 100644
--- a/.agents/notes/archived/manifest.json
+++ b/.agents/notes/archived/manifest.json
@@ -751,6 +751,9 @@
"bug-fix/2026-09-09-content-sized-diagram-previews.i18n.yaml": "sha256:a4404fe8e35ca5b6371f9cdb58fea6724cf3e7737bb2db8a3a321d534454b17c",
"bug-fix/2026-09-09-content-sized-diagram-previews.md": "sha256:5239e98e5c057d6f7e10c3f10a0f67273778d99b0c4f10b31a1921c3c6474b80",
"bug-fix/2026-09-09-content-sized-diagram-previews.zh.md": "sha256:c1ee3790a9d5c3561e1d93d5e649afa904c7d19b9d77248c85ef16daccd953e2",
+ "bug-fix/2026-09-16-movable-mandatory-update-window.i18n.yaml": "sha256:e526ce14ae9557e24364e3686778b130090445ef2152d976b7cc93e76eac6401",
+ "bug-fix/2026-09-16-movable-mandatory-update-window.md": "sha256:67546cea0b3de2b3e32957a508dbde097d91dd919fe067c8dbecb2c65b50c366",
+ "bug-fix/2026-09-16-movable-mandatory-update-window.zh.md": "sha256:20a0aade6ee71c89e84d70e23c89bb51e8a38c674d103dd43022feabb8d27be3",
"feature/2026-06-14-acp-agent-client-protocol.i18n.yaml": "sha256:006795baa43ae962a8d125cc0f1e9f134bc2ee9fb758b6e7669e3fa0126e1918",
"feature/2026-06-14-acp-agent-client-protocol.md": "sha256:6828c0af74bb3fb96206ca6b21c0e56a000b50e4744aad4bc2c05092f3a5a31b",
"feature/2026-06-14-acp-agent-client-protocol.zh.md": "sha256:ba104e841a1fb84edbd3b6c8119d50445b7785255a7a8d13bb9ac8a2cb4d2e69",
diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml
index 191e1afaf1..f918eca2a8 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: 5de3550de473cb71704025e1d60057611b15987c
-README.zh.md: 057094c80e1197e0216f660ea788dabc0d436573
+README.md: a5efa19c205cc4c7abe3525b11ed39927703cb71
+README.zh.md: c2e322b836976ae71592af1962bb7453c1165462
diff --git a/apps/desktop/README.md b/apps/desktop/README.md
index 5de3550de4..a5efa19c20 100644
--- a/apps/desktop/README.md
+++ b/apps/desktop/README.md
@@ -301,7 +301,7 @@ With `feishu-test`, an HTTP 401 JSON response containing `error.code: "UNAUTHENT
Login and policy requests share an in-memory Session, separate from product windows and the updater; restarting requires a new login. Closing cancels login, and navigation failure provides localized retry guidance. Returning to the service triggers a fresh policy query; a redirect, cookie, or HTTP 422 is not a valid policy decision. Cancellation, expiry, and invalid responses retain any known mandatory block. Fixed login outcomes appear in process diagnostics and the optional update journal; cookies, OAuth parameters, and remote error text are not recorded by the login controller. Live Harness gateway/API integration and macOS login qualification remain unverified.
-A flattened `40005` opens a shell-owned modal and refuses subsequent plugin mutations without stopping existing Host tasks. Server title and detail are optional plain text and fall back to client copy; an absent or unapproved download URL hides the external-page actions without clearing the block. On macOS the mandatory overlay fades in and out in place, keeps its backdrop during update-state changes, and redirects parent-window focus and keyboard input to the overlay. On Windows the mandatory window has a native title bar and can be moved, resized, and maximized. Closing it exits the application after cleanup without clearing the update requirement; Esc does not dismiss it. Once installation is approved, installer-owned quit releases the modal before Electron closes windows. Download, file verification including preparation, task inspection, and installation confirmation share this same modal. Only the second user approval permits task teardown and installation; deferral retains the block and package. Restart feedback mentions task stopping only when affected tasks exist. Policy is not persisted across application restarts, and policy responses never revoke or replace an updater artifact.
+A flattened `40005` opens a shell-owned modal and refuses subsequent plugin mutations without stopping existing Host tasks. Server title and detail are optional plain text and fall back to client copy; an absent or unapproved download URL hides the external-page actions without clearing the block. On macOS the mandatory overlay fades in and out in place, keeps its backdrop during update-state changes, and redirects parent-window focus and keyboard input to the overlay. On Windows the isolated application preload mounts a shell-origin frame inside the main window, covering the content below the 40 DIP caption with a backdrop and dialog. It blocks background page input without creating another native window; moving and maximizing affect only the main window. The parent remains enabled for native move, resize, minimize, maximize, and close controls. Closing the application performs cleanup without clearing the update requirement; Esc does not dismiss the overlay. Once installation is approved, installer-owned quit releases the modal before Electron closes windows. Download, file verification including preparation, task inspection, and installation confirmation share this same modal. Only the second user approval permits task teardown and installation; deferral retains the block and package. Restart feedback mentions task stopping only when affected tasks exist. Policy is not persisted across application restarts, and policy responses never revoke or replace an updater artifact.
Failures retain blocking, localized retry guidance, and folded diagnostics inside the modal. The allowed download-page action appears in recovery states, not beside normal download or installation. Requesting the browser immediately exposes a copy alternative even while the OS request is pending; a resolved request does not prove the page opened. Copy failure reveals the complete, read-only address for manual copying. Browser and clipboard outcomes do not overwrite updater errors. Only a fresh valid no-force response clears the block; the top-menu check remains available while blocked.
diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md
index 057094c80e..c2e322b836 100644
--- a/apps/desktop/README.zh.md
+++ b/apps/desktop/README.zh.md
@@ -302,7 +302,7 @@ macOS 打包在组装 App 时、代码签名前写入 `Contents/Resources/app-up
登录和策略请求共用内存 Session,与产品窗口及 updater 隔离;应用重启后需要重新登录。关闭窗口取消登录,导航失败提供本地化重试提示。返回服务后重新查询策略;重定向、Cookie 或 HTTP 422 都不是有效策略决定。取消、登录过期及无效响应均保留已知强更阻塞。固定登录结果写入进程诊断及可选更新日志;登录控制器不记录 Cookie、OAuth 参数或远程错误原文。真实 Harness 网关/API 联调及 macOS 登录验收仍未完成。
-扁平化的 `40005` 打开壳拥有的模态窗口,并拒绝后续插件修改,不停止现有 Host 任务。服务端标题与详情是可选纯文本,缺失时使用客户端兜底文案;缺少下载地址或地址未获批准时隐藏外部页面操作,不解除阻塞。macOS 强更蒙层原位渐入渐出,在更新状态切换时保留蒙层,并将父窗口焦点和键盘输入重定向到蒙层。Windows 强更窗口使用原生标题栏,可拖动、调整大小和最大化。关闭窗口会在完成清理后退出应用,不会解除更新要求;Esc 不会关闭窗口。批准安装后,安装器接管的退出流程会在 Electron 关闭窗口前释放模态窗口。下载、含准备步骤的文件校验、任务检查和安装确认共用同一弹窗。只有第二次用户批准才允许任务收尾和安装;稍后更新保留阻塞与安装包。仅存在受影响任务时,重启文案才提示正在停止任务。策略不跨应用重启持久化,策略响应也不作废或替换 updater 产物。
+扁平化的 `40005` 打开壳拥有的模态窗口,并拒绝后续插件修改,不停止现有 Host 任务。服务端标题与详情是可选纯文本,缺失时使用客户端兜底文案;缺少下载地址或地址未获批准时隐藏外部页面操作,不解除阻塞。macOS 强更蒙层原位渐入渐出,在更新状态切换时保留蒙层,并将父窗口焦点和键盘输入重定向到蒙层。Windows 由隔离的应用 preload 在主窗口内挂载 shell 来源的 frame,以蒙层和弹窗覆盖 40 DIP 顶栏下方的内容区域。它阻止背景页面输入,不创建额外的原生窗口;移动和最大化只作用于主窗口。父窗口保持启用,原生移动、缩放、最小化、最大化和关闭控件仍可操作。退出应用会完成清理,不会解除更新要求;Esc 不会关闭覆盖层。批准安装后,安装器接管的退出流程会在 Electron 关闭窗口前释放模态窗口。下载、含准备步骤的文件校验、任务检查和安装确认共用同一弹窗。只有第二次用户批准才允许任务收尾和安装;稍后更新保留阻塞与安装包。仅存在受影响任务时,重启文案才提示正在停止任务。策略不跨应用重启持久化,策略响应也不作废或替换 updater 产物。
失败时在同一弹窗内保留阻塞、本地化重试提示和折叠诊断。白名单下载页面操作只在恢复状态出现,不与正常下载或安装并列。请求打开浏览器后立即提供复制替代入口,即使系统请求尚未返回;请求成功不证明网页已打开。复制失败时展示完整、只读的地址供手动复制。浏览器与剪贴板结果不覆盖 updater 错误。只有新的有效无需强更响应才解除阻塞;阻塞期间仍可使用顶部菜单检查。
diff --git a/apps/desktop/renderer/mandatory-update-frame.js b/apps/desktop/renderer/mandatory-update-frame.js
new file mode 100644
index 0000000000..af490336a1
--- /dev/null
+++ b/apps/desktop/renderer/mandatory-update-frame.js
@@ -0,0 +1,36 @@
+/** The embedded shell page delegates actions only to its owning application preload. */
+if (window.parent !== window && window.dshMandatoryUpdate === undefined) {
+ let state
+ let sequence = 0
+ const listeners = new Set()
+ const requests = new Map()
+ const initial = Promise.withResolvers()
+ window.addEventListener('message', event => {
+ if (event.source !== window.parent || event.origin !== 'dsh-app://app') return
+ const message = event.data
+ if (message?.type === 'dsh-mandatory-state' && message.state) {
+ state = message.state
+ initial.resolve(state)
+ for (const listener of listeners) listener(state)
+ }
+ if (message?.type === 'dsh-mandatory-result') {
+ const request = requests.get(message.id)
+ if (!request) return
+ requests.delete(message.id)
+ if (message.ok) request.resolve()
+ else request.reject(new Error('Update action failed'))
+ }
+ })
+ window.dshMandatoryUpdate = {
+ status: () => state ? Promise.resolve(state) : initial.promise,
+ subscribe: listener => { listeners.add(listener); return () => listeners.delete(listener) },
+ action: (action, version, revision) => {
+ const id = ++sequence
+ const request = Promise.withResolvers()
+ requests.set(id, request)
+ window.parent.postMessage({ type: 'dsh-mandatory-action', id, action, version, revision }, 'dsh-app://app')
+ return request.promise
+ },
+ }
+ window.parent.postMessage({ type: 'dsh-mandatory-ready' }, 'dsh-app://app')
+}
diff --git a/apps/desktop/renderer/mandatory-update.html b/apps/desktop/renderer/mandatory-update.html
index 673c5d0953..f009ab8a37 100644
--- a/apps/desktop/renderer/mandatory-update.html
+++ b/apps/desktop/renderer/mandatory-update.html
@@ -32,6 +32,7 @@
+