From a5e54e99d354758a90b6f1668e19984263d3bba1 Mon Sep 17 00:00:00 2001 From: winewill Date: Thu, 5 Nov 2026 16:46:06 +0800 Subject: [PATCH 1/5] fix(desktop): stabilize update progress and explain active downloads --- apps/desktop/README.i18n.yaml | 4 +- apps/desktop/README.md | 2 + apps/desktop/README.zh.md | 2 + apps/desktop/src/locale.ts | 8 +++ apps/desktop/src/main.ts | 11 ++++ apps/desktop/tests/main-startup.spec.ts | 50 +++++++++++++++++++ .../ui-settings-general/README.i18n.yaml | 4 +- packages/client/ui-settings-general/README.md | 2 +- .../client/ui-settings-general/README.zh.md | 2 +- .../client/DesktopUpdateIndicator.module.css | 6 +++ .../src/client/DesktopUpdateIndicator.tsx | 2 +- 11 files changed, 86 insertions(+), 7 deletions(-) diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml index dc6cb5f9e0..0d74c521bd 100644 --- a/apps/desktop/README.i18n.yaml +++ b/apps/desktop/README.i18n.yaml @@ -15,8 +15,8 @@ en: 127ea90553cb6af7 zh: 7b1c5aec8558c684 /deepseek-harness-desktop/application-overview: - en: d4afa46403997884 - zh: 9b9fe126afda70f0 + en: 2eb4f59268d09949 + zh: 68bb4f758bd35f63 /deepseek-harness-desktop/terminal-command: en: 5f2d25a1d8472647 zh: 2acb5c5d7d1917e2 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 68596d719f..c2e2b14f02 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -31,6 +31,8 @@ Desktop runs the Harness workspace in a native application. This reference cover ## Application overview +Selecting **Check for Updates…** during a download or verification shows the current operation and explains that installation is available after the download finishes. Closing this informational dialog leaves the download running; repeated clicks focus the open dialog. + Electron caches the last confirmed UI language in `userData/desktop/locale-cache` so startup recovery dialogs can use it before Host settings are available. Missing, invalid, or unreadable caches fall back to the system language; cache write failures do not block startup. A successful Host startup or a UI language change refreshes the cache. Until then, a changed system language may differ from the cached language. Desktop analytics follows the [product collection policy](../../packages/client/product-analytics/README.md), including its live application setting. Web usage is excluded. Update installation waits for its local analytics intake before locking API admission and stopping the Host. Intake has a one-second deadline; failure does not prevent installation, and collector delivery is not awaited. diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md index 13661f0590..3050448898 100644 --- a/apps/desktop/README.zh.md +++ b/apps/desktop/README.zh.md @@ -39,6 +39,8 @@ Electron 在 `userData/desktop/locale-cache` 缓存最近确认的界面语言 Desktop 绑定 `127.0.0.1`,即 Electron 为就绪 URL 与 WebSocket 凭据过滤器拨号的地址。WebSocket 流按 Host 监听器使用 `ws:` 或 `wss:`;附加凭据要求 authority 与 scheme 均匹配。 +下载或校验期间选择“检查更新…”会显示当前操作,并说明下载完成后可安装更新。关闭此提示不会停止下载;重复点击会聚焦已打开的弹窗。 + 应用菜单第一项“**关于 DeepSeek Harness**”打开 Electron 原生关于面板,展示应用图标、产品名称和当前安装的发布版本。菜单文案跟随桌面壳的语言。macOS 的隐藏、隐藏其他、显示全部和退出条目使用本地化文案,隐藏和退出条目包含 DeepSeek Harness 产品名称。这些条目保留原生动作和快捷键。macOS 从应用包读取图标,因此未打包的开发启动会显示 Electron 图标;Windows 使用随包分发的 PNG。 Desktop 的本地原生目录流程打开绑定应用窗口的 Electron 文件夹对话框,并先恢复、显示和聚焦该窗口。并发请求共用一个对话框;取消不返回路径,失败后可以重试。普通 Web 使用 Host 选择器。浏览模式列出 Host 目录。Linux 缺少 zenity 或 kdialog 时,自动选择使用浏览模式,不使用 Electron 对话框。 diff --git a/apps/desktop/src/locale.ts b/apps/desktop/src/locale.ts index 050548e879..d06eb79ada 100644 --- a/apps/desktop/src/locale.ts +++ b/apps/desktop/src/locale.ts @@ -138,6 +138,7 @@ export const en = { updateClose: 'Close', updateAcknowledge: 'OK', updateLater: 'Update later', + updateDownloadInProgress: 'Downloading update', updateDownloading: 'Downloading {percent}%…', updateVerifying: 'Verifying update files…', updateInstalling: 'Preparing to restart…', @@ -331,6 +332,7 @@ export const zh = { updateClose: '关闭', updateAcknowledge: '确定', updateLater: '稍后更新', + updateDownloadInProgress: '正在下载更新', updateDownloading: '正在下载 {percent}%…', updateVerifying: '正在校验更新文件…', updateInstalling: '正在准备重启…', @@ -522,6 +524,7 @@ export const ja = { updateClose: '閉じる', updateAcknowledge: 'OK', updateLater: '後でアップデート', + updateDownloadInProgress: 'アップデートをダウンロード中', updateDownloading: 'ダウンロード中 {percent}%…', updateVerifying: 'アップデートファイルを検証中…', updateInstalling: '再起動の準備中…', @@ -713,6 +716,7 @@ export const es = { updateClose: 'Cerrar', updateAcknowledge: 'OK', updateLater: 'Actualizar más tarde', + updateDownloadInProgress: 'Descargando actualización', updateDownloading: 'Descargando {percent}%…', updateVerifying: 'Verificando archivos de actualización...', updateInstalling: 'Preparándose para reiniciar…', @@ -904,6 +908,7 @@ export const fr = { updateClose: 'Fermer', updateAcknowledge: 'OK', updateLater: 'Mettre à jour plus tard', + updateDownloadInProgress: 'Téléchargement de la mise à jour', updateDownloading: 'Téléchargement de {percent}%…', updateVerifying: 'Vérification des fichiers de mise à jour…', updateInstalling: 'Préparation du redémarrage…', @@ -1095,6 +1100,7 @@ export const de = { updateClose: 'Schließen', updateAcknowledge: 'OK', updateLater: 'Später aktualisieren', + updateDownloadInProgress: 'Update wird heruntergeladen', updateDownloading: '{percent}% wird heruntergeladen…', updateVerifying: 'Aktualisierungsdateien werden überprüft…', updateInstalling: 'Neustart wird vorbereitet…', @@ -1286,6 +1292,7 @@ export const ru = { updateClose: 'Закрыть', updateAcknowledge: 'OK', updateLater: 'Обновить позже', + updateDownloadInProgress: 'Загрузка обновления', updateDownloading: 'Загрузка {percent}%…', updateVerifying: 'Проверка файлов обновлений…', updateInstalling: 'Подготовка к перезапуску…', @@ -1477,6 +1484,7 @@ export const pt = { updateClose: 'Fechar', updateAcknowledge: 'OK', updateLater: 'Atualizar mais tarde', + updateDownloadInProgress: 'Baixando atualização', updateDownloading: 'Baixando {percent}%…', updateVerifying: 'Verificando arquivos de atualização…', updateInstalling: 'Preparando para reiniciar…', diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index dd3141228b..36858b3166 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -1000,6 +1000,17 @@ async function main(): Promise { if (authenticationOperation !== undefined) { policyAuth?.focus(); updateDialog.focus() } + if (manual && !isMandatory() && (updates.state.phase === 'downloading' || updates.state.phase === 'verifying')) { + if (updateDialog.isOpen) { + updateDialog.focus() + return Promise.resolve() + } + return ordinaryMessageBox({ type: 'info', title: locale.messages.updateCheckTitle, + message: updates.state.phase === 'downloading' ? locale.messages.updateDownloadInProgress : locale.messages.updateVerifying, + detail: locale.messages.updateDetail, buttons: [locale.messages.updateClose], cancelId: 0, + }).then(() => {}).catch((error: unknown) => showUpdateFailure({ phase: 'error', failedOperation: 'check', + message: desktopErrorState(error).message })) + } let failedOperation: 'check' | 'download' | 'install' = 'check' promptOperation ??= Promise.resolve().then(async () => { if (manual) updateJournal?.action('check-requested') diff --git a/apps/desktop/tests/main-startup.spec.ts b/apps/desktop/tests/main-startup.spec.ts index 69ec6c9af9..35dc745102 100644 --- a/apps/desktop/tests/main-startup.spec.ts +++ b/apps/desktop/tests/main-startup.spec.ts @@ -2189,6 +2189,18 @@ describe('desktop main startup', () => { await started.promise const repeated = invoke(DESKTOP_IPC.updatesOpen, 'app') expect(harness.dialog.showMessageBox).not.toHaveBeenCalled() + const checks = harness.updateCheck.mock.calls.length + const check = applicationMenuItems().find(item => item.label === en.checkUpdatesMenu)!.click as () => void + check() + await vi.waitFor(() => { expect(harness.dialog.showMessageBox).toHaveBeenCalledWith(expect.objectContaining({ + message: en.updateDownloadInProgress, detail: en.updateDetail, buttons: [en.updateClose], + })) }) + expect(harness.updateCheck).toHaveBeenCalledTimes(checks) + harness.shellDialog.isOpen = true + check() + expect(harness.shellDialog.focus).toHaveBeenCalled() + expect(harness.dialog.showMessageBox).toHaveBeenCalledOnce() + harness.shellDialog.isOpen = false expect(harness.updateDownload).toHaveBeenCalledExactlyOnceWith('1.0.1-nightly.1', 0) expect(harness.updateInstall).not.toHaveBeenCalled() downloading.resolve({ phase: 'ready', version: '1.0.1-nightly.1' }) @@ -2198,6 +2210,44 @@ describe('desktop main startup', () => { expect(harness.updateInstall).toHaveBeenCalledOnce() }) + it.each(['downloading', 'verifying'] as const)('explains %s when checking from the menu without another update operation', async (phase) => { + await readyForUpdate() + harness.updateState = { phase, version: '1.0.1-nightly.1', percent: 21 } + const checks = harness.updateCheck.mock.calls.length + const check = applicationMenuItems().find(item => item.label === en.checkUpdatesMenu)!.click as () => void + check() + await vi.waitFor(() => { expect(harness.dialog.showMessageBox).toHaveBeenCalledWith(expect.objectContaining({ + message: phase === 'downloading' ? en.updateDownloadInProgress : en.updateVerifying, + detail: en.updateDetail, buttons: [en.updateClose], cancelId: 0, + })) }) + expect(harness.updateCheck).toHaveBeenCalledTimes(checks) + expect(harness.updateDownload).not.toHaveBeenCalled() + expect(harness.updateInstall).not.toHaveBeenCalled() + expect(harness.updateState.phase).toBe(phase) + check() + await vi.waitFor(() => { expect(harness.dialog.showMessageBox).toHaveBeenCalledTimes(2) }) + expect(harness.updateCheck).toHaveBeenCalledTimes(checks) + expect(harness.updateDownload).not.toHaveBeenCalled() + expect(harness.updateInstall).not.toHaveBeenCalled() + expect(harness.updateState.phase).toBe(phase) + }) + + it('reports a failed download-status dialog without restarting the update', async () => { + await readyForUpdate() + harness.updateState = { phase: 'downloading', version: '1.0.1-nightly.1', percent: 21 } + const checks = harness.updateCheck.mock.calls.length + harness.dialog.showMessageBox.mockRejectedValueOnce(new Error('overlay unavailable')) + const check = applicationMenuItems().find(item => item.label === en.checkUpdatesMenu)!.click as () => void + check() + await vi.waitFor(() => { expect(harness.dialog.showMessageBox).toHaveBeenLastCalledWith(expect.objectContaining({ + type: 'error', title: en.updateFailedTitle, technicalDetails: 'overlay unavailable', + })) }) + expect(harness.updateCheck).toHaveBeenCalledTimes(checks) + expect(harness.updateDownload).not.toHaveBeenCalled() + expect(harness.updateInstall).not.toHaveBeenCalled() + expect(harness.updateState.phase).toBe('downloading') + }) + it('does not lock or stop tasks when restart confirmation is dismissed', async () => { const host = await readyForUpdate() harness.dialog.showMessageBox.mockResolvedValueOnce({ response: 1 }) diff --git a/packages/client/ui-settings-general/README.i18n.yaml b/packages/client/ui-settings-general/README.i18n.yaml index 216983e29e..dbd632c372 100644 --- a/packages/client/ui-settings-general/README.i18n.yaml +++ b/packages/client/ui-settings-general/README.i18n.yaml @@ -15,8 +15,8 @@ en: 83760d53b3ee5fb8 zh: 30dd8802d4ffdcb7 /deepseek-ai-dsh-client-ui-settings-general/use-this-package: - en: d427d20b4dab6653 - zh: 84e80592f0a30a9f + en: 11d1e170042c3de0 + zh: 0e157a95b0ca8068 /deepseek-ai-dsh-client-ui-settings-general/use-this-package/the-general-section: en: de35e867b2a56d73 zh: 6d4e5c912636f889 diff --git a/packages/client/ui-settings-general/README.md b/packages/client/ui-settings-general/README.md index bca8bb88ee..357e22f8ff 100644 --- a/packages/client/ui-settings-general/README.md +++ b/packages/client/ui-settings-general/README.md @@ -39,7 +39,7 @@ When the page reconnects to a Host running another release, a **Service version When the section navigation exceeds the panel's available height, the list scrolls independently of the settings content and keeps the Settings title fixed; in browsers below 768px the strip scrolls along its own axis the same way. -In Desktop, the account-row update control shows availability, progress, verification, readiness, and persistent retry feedback. It shares the connection indicator’s 28px height, 8px corners, 14px icon slot, 4px icon gap, and 12px medium text with an 18px line height; update-state border, fill, and text colors remain independently defined. Retry text and its dot use the same brand blue as other update labels; numeric download progress has no ellipsis. The preload carries semantic phase, version, progress, and classified failures; the component resolves every visible and accessible string from the active `settings` locale, including after an in-application language change. Selecting an available update starts downloading; installation requires a separate shell-owned confirmation. A collapsed sidebar shows the same status as a brand-blue dot on its top expand button, including failures. Connection feedback takes priority except during shell-reported installation, when the expected backend disconnect must not hide update status. Failure restores connection feedback. Both controls share one carrier subscription; browser code cannot choose packages or authorize installation. [Desktop updates](../../../apps/desktop/README.md) owns the release workflow. Checking, verifying, and preparing-to-restart tooltips show only the supplied version; without a version, they retain the state label. Desktop update tooltips preserve the complete version and prerelease suffix without adding v or V; their error summaries match the shell update dialogs, including guidance to exit fullscreen and retry after a fullscreen-transition timeout. +In Desktop, the account-row update control shows availability, progress, verification, readiness, and persistent retry feedback. It shares the connection indicator’s 28px height, 8px corners, 14px icon slot, 4px icon gap, and 12px medium text with an 18px line height; update-state border, fill, and text colors remain independently defined. Retry text and its dot use the same brand blue as other update labels; numeric download progress has no ellipsis and uses tabular digits with space for three digits and the percent sign to keep its width stable. The preload carries semantic phase, version, progress, and classified failures; the component resolves every visible and accessible string from the active `settings` locale, including after an in-application language change. Selecting an available update starts downloading; installation requires a separate shell-owned confirmation. A collapsed sidebar shows the same status as a brand-blue dot on its top expand button, including failures. Connection feedback takes priority except during shell-reported installation, when the expected backend disconnect must not hide update status. Failure restores connection feedback. Both controls share one carrier subscription; browser code cannot choose packages or authorize installation. [Desktop updates](../../../apps/desktop/README.md) owns the release workflow. Checking, verifying, and preparing-to-restart tooltips show only the supplied version; without a version, they retain the state label. Desktop update tooltips preserve the complete version and prerelease suffix without adding v or V; their error summaries match the shell update dialogs, including guidance to exit fullscreen and retry after a fullscreen-transition timeout. Settings visibility and section selection live in the shell owner store. The shell supplies the effective Settings binding to the contributed launcher for menu keycaps and `aria-keyshortcuts`; the fallback button uses the same binding for hover and keyboard-focus hints and `aria-keyshortcuts`. The Settings command (Desktop: `Mod+,`; Windows/macOS Web: `Mod+Alt+,`) toggles the dialog when Settings is in front or no modal is open; the sidebar control opens the same dialog. The command cannot open or close Settings while the shortcut reference or another modal is in front. Held-key repeats do nothing. The account menu opens General settings; closing the dialog clears the section selection. Onboarding can open a specified section. Initial focus lands on the selected section in the navigation, or on the title when no sections are available, without drawing a focus outline. The section buttons precede header actions and Close in the Tab order. Tab and directional navigation retain their visible focus indicators. `Mod+/` can open the shortcut reference above Settings; `Escape` closes the top dialog and restores focus to its invoking control. Return focus omits outlines after shortcut, Escape, or pointer dismissal. diff --git a/packages/client/ui-settings-general/README.zh.md b/packages/client/ui-settings-general/README.zh.md index e014b6f91f..9ddeb13ce2 100644 --- a/packages/client/ui-settings-general/README.zh.md +++ b/packages/client/ui-settings-general/README.zh.md @@ -39,7 +39,7 @@ kind: "package-reference" 当分区导航超出面板可用高度时,列表独立于设置内容滚动,并保持「设置」标题固定;浏览器宽度低于 768px 时,条带以同样方式沿自身轴滚动。 -在 Desktop 中,账户行更新控件显示可用版本、进度、验证、就绪和持久重试反馈。它与连接指示器共用 28px 高度、8px 圆角、14px 图标占位、4px 图文间距,以及 12px 中等字重文字和 18px 行高;更新状态的边框、填充和文字颜色独立定义。重试文字及其圆点与其他更新文案使用相同的品牌蓝色;数字下载进度不带省略号。预加载仅传递语义化阶段、版本、进度和失败类别;组件从当前 `settings` locale 解析全部可见与无障碍文案,并在应用内语言变化后同步更新。选择可用更新即开始下载;安装需要独立的壳拥有的确认。侧栏收起时,其顶部展开按钮以品牌蓝色圆点显示相同状态,包括失败状态。连接反馈通常优先展示,但壳报告正在安装时,预期的后端断开不能遮盖更新状态;失败后恢复连接反馈。两个控件共用一次载体订阅;浏览器代码不能选择安装包或授权安装。[Desktop 更新](../../../apps/desktop/README.zh.md)负责发布流程。检查中、校验中和准备重启状态的悬浮提示只显示已提供的版本号;没有版本号时保留状态文案。桌面更新提示完整保留版本号及预发布后缀,不额外添加 v 或 V;错误提示与桌面升级弹窗保持一致,包括全屏切换超时后退出全屏并重试的指引。 +在 Desktop 中,账户行更新控件显示可用版本、进度、验证、就绪和持久重试反馈。它与连接指示器共用 28px 高度、8px 圆角、14px 图标占位、4px 图文间距,以及 12px 中等字重文字和 18px 行高;更新状态的边框、填充和文字颜色独立定义。重试文字及其圆点与其他更新文案使用相同的品牌蓝色;数字下载进度不带省略号,采用等宽数字并预留三位数字及百分号的空间,使宽度保持稳定。预加载仅传递语义化阶段、版本、进度和失败类别;组件从当前 `settings` locale 解析全部可见与无障碍文案,并在应用内语言变化后同步更新。选择可用更新即开始下载;安装需要独立的壳拥有的确认。侧栏收起时,其顶部展开按钮以品牌蓝色圆点显示相同状态,包括失败状态。连接反馈通常优先展示,但壳报告正在安装时,预期的后端断开不能遮盖更新状态;失败后恢复连接反馈。两个控件共用一次载体订阅;浏览器代码不能选择安装包或授权安装。[Desktop 更新](../../../apps/desktop/README.zh.md)负责发布流程。检查中、校验中和准备重启状态的悬浮提示只显示已提供的版本号;没有版本号时保留状态文案。桌面更新提示完整保留版本号及预发布后缀,不额外添加 v 或 V;错误提示与桌面升级弹窗保持一致,包括全屏切换超时后退出全屏并重试的指引。 设置的可见状态和分区选择由 shell owner store 保存。外壳将设置命令的有效绑定传给入口贡献,用于菜单键帽和 `aria-keyshortcuts`;回退按钮使用相同绑定提供悬停、键盘聚焦提示和 `aria-keyshortcuts`。设置位于最前方或没有模态弹窗时,设置命令(Desktop 为 `Mod+,`,Windows/macOS Web 为 `Mod+Alt+,`)切换设置弹窗的开关;侧栏控件打开同一弹窗。快捷键速查或其他模态弹窗位于前方时,该命令不能打开或关闭设置。长按产生的重复按键不执行操作。账号菜单打开通用设置;关闭弹窗会清空分区选择。引导流程可以打开指定分区。初始焦点落在导航中选中的分区;没有可用分区时落在标题上,均不绘制焦点外轮廓线。Tab 顺序中,分区按钮先于头部操作和关闭按钮。Tab 和方向键导航保留可见焦点提示。`Mod+/` 可在设置上方打开快捷键速查;`Escape` 关闭最上层弹窗,焦点返回触发该弹窗的控件。通过快捷键、Esc 或鼠标关闭后的回焦均不绘制外轮廓线。 diff --git a/packages/client/ui-settings-general/src/client/DesktopUpdateIndicator.module.css b/packages/client/ui-settings-general/src/client/DesktopUpdateIndicator.module.css index b5b0eea658..2a503fb967 100644 --- a/packages/client/ui-settings-general/src/client/DesktopUpdateIndicator.module.css +++ b/packages/client/ui-settings-general/src/client/DesktopUpdateIndicator.module.css @@ -20,6 +20,12 @@ cursor: pointer; } +.progress { + min-inline-size: calc(3ch + 1em); + font-variant-numeric: tabular-nums; + text-align: end; +} + .icon { display: grid; place-items: center; diff --git a/packages/client/ui-settings-general/src/client/DesktopUpdateIndicator.tsx b/packages/client/ui-settings-general/src/client/DesktopUpdateIndicator.tsx index ae22d5c6fb..01376efb34 100644 --- a/packages/client/ui-settings-general/src/client/DesktopUpdateIndicator.tsx +++ b/packages/client/ui-settings-general/src/client/DesktopUpdateIndicator.tsx @@ -80,7 +80,7 @@ export function DesktopUpdateIndicator({ wide, hidden, t, view, onOpen }: { {error ? : busy ? : } - {label} + {label} } From f823dac5c7886268daf18a4add90290c0d6dbd4f Mon Sep 17 00:00:00 2001 From: winewill Date: Fri, 6 Nov 2026 13:54:41 +0800 Subject: [PATCH 2/5] fix(desktop): open download status from progress indicator --- apps/desktop/README.i18n.yaml | 4 +-- apps/desktop/README.md | 2 +- apps/desktop/README.zh.md | 2 +- apps/desktop/src/locale.ts | 8 ++++++ apps/desktop/src/main.ts | 4 +-- apps/desktop/tests/main-startup.spec.ts | 10 +++++-- .../ui-settings-general/README.i18n.yaml | 4 +-- packages/client/ui-settings-general/README.md | 2 +- .../client/ui-settings-general/README.zh.md | 2 +- .../src/client/DesktopUpdateIndicator.tsx | 3 +- .../src/client/desktop-update-source.ts | 17 ++++++++--- .../tests/desktop-update.client.spec.tsx | 28 ++++++++++++++++++- 12 files changed, 67 insertions(+), 19 deletions(-) diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml index 001d99f3cc..cecb1d621f 100644 --- a/apps/desktop/README.i18n.yaml +++ b/apps/desktop/README.i18n.yaml @@ -57,8 +57,8 @@ en: f1302ed4a3ab58bb zh: cb53b9b349815028 /deepseek-harness-desktop/updates: - en: 67bf0a18faa7aa76 - zh: 782e14ce62bb939f + en: 838bfe3de424dde5 + zh: cb21ab47e20247e6 /deepseek-harness-desktop/updates/local-desktop-settings: en: dbe57394894b7368 zh: a3bb788c2ce631a4 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index cb1f40f739..d0a92b83bd 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -284,7 +284,7 @@ Packaged applications check fixed Nightly asynchronously at startup. Ordinary po `DSH_DESKTOP_UPDATE_CHECK_INTERVAL_MS` configures the ordinary base interval, and `DSH_DESKTOP_UPDATE_CHECK_MAX_BACKOFF_MS` configures the cap; both accept integer milliseconds from 1000 through 2147483647, with the cap at least the interval. An omitted cap defaults to the larger of one hour and the interval. `DSH_DESKTOP_UPDATE_CHECK_JITTER` sets the fractional jitter from 0 through 1, defaulting to `0.2`; the final delay is at least one second and never exceeds the cap. These settings do not change mandatory-policy polling or authorize download retries. -The lower-left account row displays localized availability, a spinner with download percentage, verification, readiness, or a persistent red retry action with an accessible tooltip. This Web-embedded copy follows the active in-application language; native dialogs use the Desktop shell locale. The collapsed sidebar shows a dot on its top expand button. Connection status takes priority. Selecting an available release starts downloading immediately. Successful preparation automatically opens a shell-owned restart confirmation; closing it retains readiness without reopening the dialog. Selecting the ready entry opens confirmation again. Running agents, queued input, and running or stopping jobs trigger an interruption warning in that confirmation. API requests alone do not trigger the warning. After approval, the Host locks new requests, drains admitted requests, and rechecks tasks, including work created by an admitted write. A drain that exceeds the control-request deadline refuses installation and unlocks admission. Unknown task status, newly started work without interruption approval, or unsuccessful graceful teardown prevents installation. The Host exits as soon as it acknowledges completed teardown, so handles left open by plugins do not count against the graceful deadline. Ordinary quit asks about interruptible work as described under Closing the window and quitting, then hides the product window before stopping the Host, ignores new focus requests during teardown, and never installs an update. The next launch reconciles the version-bound runtime through the existing startup and recovery path. +The lower-left account row displays localized availability, a spinner with download percentage, verification, readiness, or a persistent red retry action with an accessible tooltip. This Web-embedded copy follows the active in-application language; native dialogs use the Desktop shell locale. The collapsed sidebar shows a dot on its top expand button. Connection status takes priority. Selecting an available release starts downloading immediately. Selecting its download percentage opens the same status dialog as Check for Updates, explaining that downloading runs in the background and the app remains usable. Successful preparation automatically opens a shell-owned restart confirmation; closing it retains readiness without reopening the dialog. Selecting the ready entry opens confirmation again. Running agents, queued input, and running or stopping jobs trigger an interruption warning in that confirmation. API requests alone do not trigger the warning. After approval, the Host locks new requests, drains admitted requests, and rechecks tasks, including work created by an admitted write. A drain that exceeds the control-request deadline refuses installation and unlocks admission. Unknown task status, newly started work without interruption approval, or unsuccessful graceful teardown prevents installation. The Host exits as soon as it acknowledges completed teardown, so handles left open by plugins do not count against the graceful deadline. Ordinary quit asks about interruptible work as described under Closing the window and quitting, then hides the product window before stopping the Host, ignores new focus requests during teardown, and never installs an update. The next launch reconciles the version-bound runtime through the existing startup and recovery path. If task teardown fails after confirmed Host exit, installation is refused and the shell restores the current-version Host before allowing another restart confirmation. Installer launch failure after a clean Host stop uses the same recovery. After the replacement Host authenticates, the shell reloads the existing application URL so the Web page obtains its current port, cookie, and boot injections; page-load failure opens native fatal recovery. An unconfirmed process exit never permits a replacement Host. The downloaded target remains available for retry. A known mandatory policy remains blocking throughout recovery; unsuccessful Host restoration opens the native fatal-recovery dialog. diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md index 444e6658d4..24f35fc8ea 100644 --- a/apps/desktop/README.zh.md +++ b/apps/desktop/README.zh.md @@ -284,7 +284,7 @@ macOS 在安装准备阶段等待产品主窗口退出全屏,再显示重启 `DSH_DESKTOP_UPDATE_CHECK_INTERVAL_MS` 配置常规基础间隔,`DSH_DESKTOP_UPDATE_CHECK_MAX_BACKOFF_MS` 配置上限;两者均接受 1000 至 2147483647 的整数毫秒数,且上限不能小于间隔。省略上限时取一小时与间隔中的较大值。`DSH_DESKTOP_UPDATE_CHECK_JITTER` 配置 0 至 1 的抖动比例,默认 `0.2`;最终延迟至少一秒,且不超过上限。这些配置不改变强更策略轮询,也不授权下载重试。 -左下角账户行显示本地化的更新可用状态、加载图标与下载百分比、验证、就绪状态,或带可访问提示的持久红色重试操作。嵌入 Web 界面的文案跟随应用内当前语言;原生弹窗使用 Desktop 壳语言。侧栏收起时,顶部展开按钮显示圆点。连接状态优先展示。选择可用版本即开始下载。准备成功后自动打开壳拥有的重启确认;关闭后保留就绪状态,不重复弹窗。选择就绪入口可再次打开确认。运行中的 agent、排队输入,以及运行中或停止中的后台任务都会在该确认中触发中断警告。仅有 API 请求不会触发警告。用户批准后,Host 锁定新请求,等待已接收的请求结束,再检查任务,包括已接收写操作创建的工作。等待超过控制请求截止时间时,拒绝安装并解除准入锁。任务状态未知、未获中断授权的新任务,或未成功完成正常收尾,都会阻止安装。Host 确认收尾完成后立即退出,插件遗留的打开句柄不会占用正常收尾期限。常规退出先按"关闭窗口与退出"一节所述询问可中断的工作,再在停止 Host 前隐藏产品窗口,在收尾期间忽略新的聚焦请求,且从不安装更新。下次启动通过已有的启动与恢复流程校准版本绑定的运行时。 +左下角账户行显示本地化的更新可用状态、加载图标与下载百分比、验证、就绪状态,或带可访问提示的持久红色重试操作。嵌入 Web 界面的文案跟随应用内当前语言;原生弹窗使用 Desktop 壳语言。侧栏收起时,顶部展开按钮显示圆点。连接状态优先展示。选择可用版本即开始下载。点击下载百分比会打开与“检查更新”相同的状态提示,说明更新正在后台下载,可以继续使用应用。准备成功后自动打开壳拥有的重启确认;关闭后保留就绪状态,不重复弹窗。选择就绪入口可再次打开确认。运行中的 agent、排队输入,以及运行中或停止中的后台任务都会在该确认中触发中断警告。仅有 API 请求不会触发警告。用户批准后,Host 锁定新请求,等待已接收的请求结束,再检查任务,包括已接收写操作创建的工作。等待超过控制请求截止时间时,拒绝安装并解除准入锁。任务状态未知、未获中断授权的新任务,或未成功完成正常收尾,都会阻止安装。Host 确认收尾完成后立即退出,插件遗留的打开句柄不会占用正常收尾期限。常规退出先按"关闭窗口与退出"一节所述询问可中断的工作,再在停止 Host 前隐藏产品窗口,在收尾期间忽略新的聚焦请求,且从不安装更新。下次启动通过已有的启动与恢复流程校准版本绑定的运行时。 若任务收尾失败但已确认 Host 退出,安装会被拒绝,壳会在允许再次确认重启前恢复当前版本的 Host。Host 正常停止后的安装器启动失败使用同一恢复路径。替代 Host 启动并完成认证后,壳重新加载原有应用地址,让 Web 页面获取当前端口、Cookie 和启动注入数据;页面加载失败时打开原生致命故障恢复弹窗。未确认进程退出时,绝不允许启动替代 Host。已下载目标保留以供重试。已知强更策略在恢复过程中继续阻塞;Host 恢复失败打开原生致命故障恢复弹窗。 diff --git a/apps/desktop/src/locale.ts b/apps/desktop/src/locale.ts index d06eb79ada..3e2db64e24 100644 --- a/apps/desktop/src/locale.ts +++ b/apps/desktop/src/locale.ts @@ -139,6 +139,7 @@ export const en = { updateAcknowledge: 'OK', updateLater: 'Update later', updateDownloadInProgress: 'Downloading update', + updateDownloadDetail: 'The update is downloading in the background. You can continue using the app.', updateDownloading: 'Downloading {percent}%…', updateVerifying: 'Verifying update files…', updateInstalling: 'Preparing to restart…', @@ -333,6 +334,7 @@ export const zh = { updateAcknowledge: '确定', updateLater: '稍后更新', updateDownloadInProgress: '正在下载更新', + updateDownloadDetail: '更新正在后台下载,你可以继续使用应用。', updateDownloading: '正在下载 {percent}%…', updateVerifying: '正在校验更新文件…', updateInstalling: '正在准备重启…', @@ -525,6 +527,7 @@ export const ja = { updateAcknowledge: 'OK', updateLater: '後でアップデート', updateDownloadInProgress: 'アップデートをダウンロード中', + updateDownloadDetail: 'アップデートをバックグラウンドでダウンロードしています。アプリは引き続き使用できます。', updateDownloading: 'ダウンロード中 {percent}%…', updateVerifying: 'アップデートファイルを検証中…', updateInstalling: '再起動の準備中…', @@ -717,6 +720,7 @@ export const es = { updateAcknowledge: 'OK', updateLater: 'Actualizar más tarde', updateDownloadInProgress: 'Descargando actualización', + updateDownloadDetail: 'La actualización se está descargando en segundo plano. Puede seguir usando la aplicación.', updateDownloading: 'Descargando {percent}%…', updateVerifying: 'Verificando archivos de actualización...', updateInstalling: 'Preparándose para reiniciar…', @@ -909,6 +913,7 @@ export const fr = { updateAcknowledge: 'OK', updateLater: 'Mettre à jour plus tard', updateDownloadInProgress: 'Téléchargement de la mise à jour', + updateDownloadDetail: 'La mise à jour se télécharge en arrière-plan. Vous pouvez continuer à utiliser l’application.', updateDownloading: 'Téléchargement de {percent}%…', updateVerifying: 'Vérification des fichiers de mise à jour…', updateInstalling: 'Préparation du redémarrage…', @@ -1101,6 +1106,7 @@ export const de = { updateAcknowledge: 'OK', updateLater: 'Später aktualisieren', updateDownloadInProgress: 'Update wird heruntergeladen', + updateDownloadDetail: 'Das Update wird im Hintergrund heruntergeladen. Sie können die App weiter verwenden.', updateDownloading: '{percent}% wird heruntergeladen…', updateVerifying: 'Aktualisierungsdateien werden überprüft…', updateInstalling: 'Neustart wird vorbereitet…', @@ -1293,6 +1299,7 @@ export const ru = { updateAcknowledge: 'OK', updateLater: 'Обновить позже', updateDownloadInProgress: 'Загрузка обновления', + updateDownloadDetail: 'Обновление загружается в фоновом режиме. Вы можете продолжать пользоваться приложением.', updateDownloading: 'Загрузка {percent}%…', updateVerifying: 'Проверка файлов обновлений…', updateInstalling: 'Подготовка к перезапуску…', @@ -1485,6 +1492,7 @@ export const pt = { updateAcknowledge: 'OK', updateLater: 'Atualizar mais tarde', updateDownloadInProgress: 'Baixando atualização', + updateDownloadDetail: 'A atualização está sendo baixada em segundo plano. Você pode continuar usando o aplicativo.', updateDownloading: 'Baixando {percent}%…', updateVerifying: 'Verificando arquivos de atualização…', updateInstalling: 'Preparando para reiniciar…', diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 290b91e3d5..c30d7cb5cf 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -1012,14 +1012,14 @@ async function main(): Promise { if (authenticationOperation !== undefined) { policyAuth?.focus(); updateDialog.focus() } - if (manual && !isMandatory() && (updates.state.phase === 'downloading' || updates.state.phase === 'verifying')) { + if (!isMandatory() && (updates.state.phase === 'downloading' || updates.state.phase === 'verifying')) { if (updateDialog.isOpen) { updateDialog.focus() return Promise.resolve() } return ordinaryMessageBox({ type: 'info', title: locale.messages.updateCheckTitle, message: updates.state.phase === 'downloading' ? locale.messages.updateDownloadInProgress : locale.messages.updateVerifying, - detail: locale.messages.updateDetail, buttons: [locale.messages.updateClose], cancelId: 0, + detail: updates.state.phase === 'downloading' ? locale.messages.updateDownloadDetail : locale.messages.updateDetail, buttons: [locale.messages.updateClose], cancelId: 0, }).then(() => {}).catch((error: unknown) => showUpdateFailure({ phase: 'error', failedOperation: 'check', message: desktopErrorState(error).message })) } diff --git a/apps/desktop/tests/main-startup.spec.ts b/apps/desktop/tests/main-startup.spec.ts index 8b8d2fb69f..6d47ffbd9f 100644 --- a/apps/desktop/tests/main-startup.spec.ts +++ b/apps/desktop/tests/main-startup.spec.ts @@ -2259,12 +2259,16 @@ describe('desktop main startup', () => { const action = invoke(DESKTOP_IPC.updatesOpen, 'app') await started.promise const repeated = invoke(DESKTOP_IPC.updatesOpen, 'app') - expect(harness.dialog.showMessageBox).not.toHaveBeenCalled() + await repeated + expect(harness.dialog.showMessageBox).toHaveBeenCalledWith(expect.objectContaining({ + message: en.updateDownloadInProgress, detail: en.updateDownloadDetail, + })) + harness.dialog.showMessageBox.mockClear() const checks = harness.updateCheck.mock.calls.length const check = applicationMenuItems().find(item => item.label === en.checkUpdatesMenu)!.click as () => void check() await vi.waitFor(() => { expect(harness.dialog.showMessageBox).toHaveBeenCalledWith(expect.objectContaining({ - message: en.updateDownloadInProgress, detail: en.updateDetail, buttons: [en.updateClose], + message: en.updateDownloadInProgress, detail: en.updateDownloadDetail, buttons: [en.updateClose], })) }) expect(harness.updateCheck).toHaveBeenCalledTimes(checks) harness.shellDialog.isOpen = true @@ -2289,7 +2293,7 @@ describe('desktop main startup', () => { check() await vi.waitFor(() => { expect(harness.dialog.showMessageBox).toHaveBeenCalledWith(expect.objectContaining({ message: phase === 'downloading' ? en.updateDownloadInProgress : en.updateVerifying, - detail: en.updateDetail, buttons: [en.updateClose], cancelId: 0, + detail: phase === 'downloading' ? en.updateDownloadDetail : en.updateDetail, buttons: [en.updateClose], cancelId: 0, })) }) expect(harness.updateCheck).toHaveBeenCalledTimes(checks) expect(harness.updateDownload).not.toHaveBeenCalled() diff --git a/packages/client/ui-settings-general/README.i18n.yaml b/packages/client/ui-settings-general/README.i18n.yaml index dbd632c372..fc1c4c4111 100644 --- a/packages/client/ui-settings-general/README.i18n.yaml +++ b/packages/client/ui-settings-general/README.i18n.yaml @@ -15,8 +15,8 @@ en: 83760d53b3ee5fb8 zh: 30dd8802d4ffdcb7 /deepseek-ai-dsh-client-ui-settings-general/use-this-package: - en: 11d1e170042c3de0 - zh: 0e157a95b0ca8068 + en: 8ae337011190f5f3 + zh: 508eca60c6fcdc34 /deepseek-ai-dsh-client-ui-settings-general/use-this-package/the-general-section: en: de35e867b2a56d73 zh: 6d4e5c912636f889 diff --git a/packages/client/ui-settings-general/README.md b/packages/client/ui-settings-general/README.md index 357e22f8ff..cb8873ba9b 100644 --- a/packages/client/ui-settings-general/README.md +++ b/packages/client/ui-settings-general/README.md @@ -39,7 +39,7 @@ When the page reconnects to a Host running another release, a **Service version When the section navigation exceeds the panel's available height, the list scrolls independently of the settings content and keeps the Settings title fixed; in browsers below 768px the strip scrolls along its own axis the same way. -In Desktop, the account-row update control shows availability, progress, verification, readiness, and persistent retry feedback. It shares the connection indicator’s 28px height, 8px corners, 14px icon slot, 4px icon gap, and 12px medium text with an 18px line height; update-state border, fill, and text colors remain independently defined. Retry text and its dot use the same brand blue as other update labels; numeric download progress has no ellipsis and uses tabular digits with space for three digits and the percent sign to keep its width stable. The preload carries semantic phase, version, progress, and classified failures; the component resolves every visible and accessible string from the active `settings` locale, including after an in-application language change. Selecting an available update starts downloading; installation requires a separate shell-owned confirmation. A collapsed sidebar shows the same status as a brand-blue dot on its top expand button, including failures. Connection feedback takes priority except during shell-reported installation, when the expected backend disconnect must not hide update status. Failure restores connection feedback. Both controls share one carrier subscription; browser code cannot choose packages or authorize installation. [Desktop updates](../../../apps/desktop/README.md) owns the release workflow. Checking, verifying, and preparing-to-restart tooltips show only the supplied version; without a version, they retain the state label. Desktop update tooltips preserve the complete version and prerelease suffix without adding v or V; their error summaries match the shell update dialogs, including guidance to exit fullscreen and retry after a fullscreen-transition timeout. +In Desktop, the account-row update control shows availability, progress, verification, readiness, and persistent retry feedback. It shares the connection indicator’s 28px height, 8px corners, 14px icon slot, 4px icon gap, and 12px medium text with an 18px line height; update-state border, fill, and text colors remain independently defined. Retry text and its dot use the same brand blue as other update labels; numeric download progress has no ellipsis and uses tabular digits with space for three digits and the percent sign to keep its width stable. The preload carries semantic phase, version, progress, and classified failures; the component resolves every visible and accessible string from the active `settings` locale, including after an in-application language change. Selecting the download percentage opens the shell’s download-status dialog; closing it leaves the download running. Selecting an available update starts downloading; installation requires a separate shell-owned confirmation. A collapsed sidebar shows the same status as a brand-blue dot on its top expand button, including failures. Connection feedback takes priority except during shell-reported installation, when the expected backend disconnect must not hide update status. Failure restores connection feedback. Both controls share one carrier subscription; browser code cannot choose packages or authorize installation. [Desktop updates](../../../apps/desktop/README.md) owns the release workflow. Checking, verifying, and preparing-to-restart tooltips show only the supplied version; without a version, they retain the state label. Desktop update tooltips preserve the complete version and prerelease suffix without adding v or V; their error summaries match the shell update dialogs, including guidance to exit fullscreen and retry after a fullscreen-transition timeout. Settings visibility and section selection live in the shell owner store. The shell supplies the effective Settings binding to the contributed launcher for menu keycaps and `aria-keyshortcuts`; the fallback button uses the same binding for hover and keyboard-focus hints and `aria-keyshortcuts`. The Settings command (Desktop: `Mod+,`; Windows/macOS Web: `Mod+Alt+,`) toggles the dialog when Settings is in front or no modal is open; the sidebar control opens the same dialog. The command cannot open or close Settings while the shortcut reference or another modal is in front. Held-key repeats do nothing. The account menu opens General settings; closing the dialog clears the section selection. Onboarding can open a specified section. Initial focus lands on the selected section in the navigation, or on the title when no sections are available, without drawing a focus outline. The section buttons precede header actions and Close in the Tab order. Tab and directional navigation retain their visible focus indicators. `Mod+/` can open the shortcut reference above Settings; `Escape` closes the top dialog and restores focus to its invoking control. Return focus omits outlines after shortcut, Escape, or pointer dismissal. diff --git a/packages/client/ui-settings-general/README.zh.md b/packages/client/ui-settings-general/README.zh.md index 9ddeb13ce2..9cf20eea0e 100644 --- a/packages/client/ui-settings-general/README.zh.md +++ b/packages/client/ui-settings-general/README.zh.md @@ -39,7 +39,7 @@ kind: "package-reference" 当分区导航超出面板可用高度时,列表独立于设置内容滚动,并保持「设置」标题固定;浏览器宽度低于 768px 时,条带以同样方式沿自身轴滚动。 -在 Desktop 中,账户行更新控件显示可用版本、进度、验证、就绪和持久重试反馈。它与连接指示器共用 28px 高度、8px 圆角、14px 图标占位、4px 图文间距,以及 12px 中等字重文字和 18px 行高;更新状态的边框、填充和文字颜色独立定义。重试文字及其圆点与其他更新文案使用相同的品牌蓝色;数字下载进度不带省略号,采用等宽数字并预留三位数字及百分号的空间,使宽度保持稳定。预加载仅传递语义化阶段、版本、进度和失败类别;组件从当前 `settings` locale 解析全部可见与无障碍文案,并在应用内语言变化后同步更新。选择可用更新即开始下载;安装需要独立的壳拥有的确认。侧栏收起时,其顶部展开按钮以品牌蓝色圆点显示相同状态,包括失败状态。连接反馈通常优先展示,但壳报告正在安装时,预期的后端断开不能遮盖更新状态;失败后恢复连接反馈。两个控件共用一次载体订阅;浏览器代码不能选择安装包或授权安装。[Desktop 更新](../../../apps/desktop/README.zh.md)负责发布流程。检查中、校验中和准备重启状态的悬浮提示只显示已提供的版本号;没有版本号时保留状态文案。桌面更新提示完整保留版本号及预发布后缀,不额外添加 v 或 V;错误提示与桌面升级弹窗保持一致,包括全屏切换超时后退出全屏并重试的指引。 +在 Desktop 中,账户行更新控件显示可用版本、进度、验证、就绪和持久重试反馈。它与连接指示器共用 28px 高度、8px 圆角、14px 图标占位、4px 图文间距,以及 12px 中等字重文字和 18px 行高;更新状态的边框、填充和文字颜色独立定义。重试文字及其圆点与其他更新文案使用相同的品牌蓝色;数字下载进度不带省略号,采用等宽数字并预留三位数字及百分号的空间,使宽度保持稳定。预加载仅传递语义化阶段、版本、进度和失败类别;组件从当前 `settings` locale 解析全部可见与无障碍文案,并在应用内语言变化后同步更新。选择下载百分比会打开壳的下载状态提示,关闭提示不影响下载。选择可用更新即开始下载;安装需要独立的壳拥有的确认。侧栏收起时,其顶部展开按钮以品牌蓝色圆点显示相同状态,包括失败状态。连接反馈通常优先展示,但壳报告正在安装时,预期的后端断开不能遮盖更新状态;失败后恢复连接反馈。两个控件共用一次载体订阅;浏览器代码不能选择安装包或授权安装。[Desktop 更新](../../../apps/desktop/README.zh.md)负责发布流程。检查中、校验中和准备重启状态的悬浮提示只显示已提供的版本号;没有版本号时保留状态文案。桌面更新提示完整保留版本号及预发布后缀,不额外添加 v 或 V;错误提示与桌面升级弹窗保持一致,包括全屏切换超时后退出全屏并重试的指引。 设置的可见状态和分区选择由 shell owner store 保存。外壳将设置命令的有效绑定传给入口贡献,用于菜单键帽和 `aria-keyshortcuts`;回退按钮使用相同绑定提供悬停、键盘聚焦提示和 `aria-keyshortcuts`。设置位于最前方或没有模态弹窗时,设置命令(Desktop 为 `Mod+,`,Windows/macOS Web 为 `Mod+Alt+,`)切换设置弹窗的开关;侧栏控件打开同一弹窗。快捷键速查或其他模态弹窗位于前方时,该命令不能打开或关闭设置。长按产生的重复按键不执行操作。账号菜单打开通用设置;关闭弹窗会清空分区选择。引导流程可以打开指定分区。初始焦点落在导航中选中的分区;没有可用分区时落在标题上,均不绘制焦点外轮廓线。Tab 顺序中,分区按钮先于头部操作和关闭按钮。Tab 和方向键导航保留可见焦点提示。`Mod+/` 可在设置上方打开快捷键速查;`Escape` 关闭最上层弹窗,焦点返回触发该弹窗的控件。通过快捷键、Esc 或鼠标关闭后的回焦均不绘制外轮廓线。 diff --git a/packages/client/ui-settings-general/src/client/DesktopUpdateIndicator.tsx b/packages/client/ui-settings-general/src/client/DesktopUpdateIndicator.tsx index 01376efb34..4fcc71f12a 100644 --- a/packages/client/ui-settings-general/src/client/DesktopUpdateIndicator.tsx +++ b/packages/client/ui-settings-general/src/client/DesktopUpdateIndicator.tsx @@ -73,9 +73,10 @@ export function DesktopUpdateIndicator({ wide, hidden, t, view, onOpen }: { const label = failed ? retryLabel : copy.label const error = failed || state?.phase === 'error' || state?.configurationError !== undefined const busy = opening || (state !== undefined && state.configurationError === undefined && BUSY_PHASES.has(state.phase)) + const disabled = opening || (busy && state?.phase !== 'downloading') return