diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml index 899796c03a..c29e75c550 100644 --- a/apps/desktop/README.i18n.yaml +++ b/apps/desktop/README.i18n.yaml @@ -21,8 +21,8 @@ en: 5f2d25a1d8472647 zh: 2acb5c5d7d1917e2 /deepseek-harness-desktop/closing-the-window-and-quitting: - en: bb35a022c37955c9 - zh: ab2ef22d398d83a0 + en: ae6be855e19da24e + zh: 60829ab66093bd49 /deepseek-harness-desktop/key-technical-decisions: en: 3724c53da4f7647b zh: 76ae549d43c99bb4 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index bf36a7b9fe..79680870c8 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -75,7 +75,7 @@ Windows keeps a tray icon for the whole run. Its tooltip is the product name, a Every ordinary quit entry — ⌘Q, the application menu, the Dock menu, the Windows tray and caption Application menu, and the quit caused by closing the welcome window — asks the Host what it would interrupt. The Host answers over the private IPC channel with two facts: active tasks under the same rule as the update restart check (running agents including subagents and approval waits, queued messages, running or stopping jobs), and armed scheduled reminders reported by the `schedule` family of `workspace/session-activity` for the sessions loaded during this run. Neither fact, the quit proceeds silently. Otherwise a native message box without an owner window — hidden windows stay hidden — shows **Quit DeepSeek Harness?** with one of three localized explanations: running tasks will be interrupted, scheduled tasks will not run while the application is closed, or both. Quit is the default button and Cancel answers Esc; macOS places Cancel left of Quit, Windows places Quit left of Cancel, and the Windows task dialog shows the application icon and stays light regardless of the application theme. A Host that has not reached ready or has failed cannot run tasks, so the quit proceeds without asking. An inspection failure or a Host that misses the two-second deadline is treated as running tasks. While the box is open, another quit request joins it instead of stacking a second one (macOS also raises it; Electron exposes no handle to the Windows task dialog), the explanation does not change when work starts or ends, Quit stops the application without re-inspecting, and Cancel changes nothing. Cancelling a quit that closing the welcome window started shows the welcome window again. -The mandatory update confirmation includes the installation wait notice on Windows; macOS uses the shorter restart explanation. +The mandatory update confirmation includes the installation wait notice on Windows; macOS uses the shorter restart explanation. During a confirmed Windows restart, the installer can display progress in a separate process. Installation and relaunch continue if that window is unavailable or stops responding. The confirmation is skipped when the installer restart already confirmed task interruption, when the fatal-recovery dialog exits or restarts, when the development Restart App and Host command runs, and during operating-system shutdown, restart, or log-off: Windows sets this on the definitive session-end message; macOS sets it on the power-off notification, and because another application can still cancel that shutdown, the next focus or show of the main window clears it. Installer-owned quit cancels any pending ordinary quit decision; late inspection results and dialog answers cannot open another confirmation or repeat cleanup. A user-initiated update download that finishes while the window is hidden defers its Install and Restart confirmation until the window is shown again; the mandatory flow keeps its taskbar and Dock attention. The Windows installer and uninstaller tell a user whose application is still running to quit it from the system tray. Desktop does not enable scheduled tasks by default, so the scheduled-task explanations appear only once that feature is on; reminders fire only in loaded sessions, and unloaded sessions are neither counted nor resumed until they are opened. diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md index 6241072116..9afc4531a5 100644 --- a/apps/desktop/README.zh.md +++ b/apps/desktop/README.zh.md @@ -75,7 +75,7 @@ Windows 在整个运行期间常驻托盘图标。悬停提示为产品名,单 所有普通退出入口——⌘Q、应用菜单、Dock 菜单、Windows 托盘和标题栏"应用程序"菜单,以及关闭欢迎窗口引起的退出——都先向 Host 查询退出会中断什么。Host 通过私有 IPC 通道回答两项事实:与更新重启检查同一口径的运行中任务(运行中的 agent,包括子代理和等待审批的回合、排队消息、运行中或停止中的后台任务),以及本次运行中已加载会话里由 `workspace/session-activity` 的 `schedule` family 报告的已挂定时器的提醒。两项都没有时直接退出,不弹框。否则弹出一个没有父窗口的原生消息框——隐藏的窗口保持隐藏——标题为**退出 DeepSeek Harness?**,正文为三种本地化说明之一:正在运行的任务将会中断、应用关闭期间定时任务不会运行,或两者兼有。"退出"是默认按钮,Esc 等同"取消";macOS 上"取消"在"退出"左侧,Windows 上"退出"在"取消"左侧,Windows 任务对话框显示应用图标且不跟随应用主题、始终为浅色。Host 尚未就绪或已失败时不可能有任务在跑,直接退出。查询失败或 Host 超过两秒截止时间未答复,按运行中任务处理。弹框打开期间,再次请求退出只会并入同一弹框而不叠加新弹框(macOS 上还会把它提到前面;Electron 不暴露 Windows 任务对话框的句柄);任务开始或结束不会改变文案;点"退出"不再重新查询即停止应用;点"取消"不发生任何变化。取消由关闭欢迎窗口引起的退出时,欢迎窗口会重新显示。 -强制更新的安装确认在 Windows 上额外显示等待提示,macOS 只显示简短的重启说明。 +强制更新的安装确认在 Windows 上额外显示等待提示,macOS 只显示简短的重启说明。 Windows 确认重启后,安装器可在独立进程中显示更新提示。提示窗无法打开或失去响应时,安装和重新启动仍会继续。 以下情况跳过确认:安装更新的重启已确认过任务中断、致命错误恢复对话框中的退出或重启、开发版"重启应用与 Host"命令,以及操作系统关机、重启或注销:Windows 在确定性的会话结束消息上设置该状态;macOS 在关机通知上设置,而其他应用仍可能取消这次关机,因此主窗口下一次获得焦点或显示时会清除它。安装器接管退出时会取消尚未结束的普通退出决策;晚到的查询结果和弹框答复不会再次打开确认框或重复清理。窗口隐藏期间完成的用户主动发起的更新下载,把"安装并重启"确认推迟到窗口再次显示时;强制更新流程沿用其任务栏和 Dock 提醒。Windows 安装程序和卸载程序在应用仍在运行时提示用户先在系统托盘中退出。Desktop 默认未开启定时任务,定时任务的说明只在该功能开启后出现;提醒只在已加载的会话中触发,未加载的会话既不计入,也要等到打开后才会继续。 diff --git a/apps/desktop/installer/locales/de_DE.json b/apps/desktop/installer/locales/de_DE.json index 4f4cf337d8..595002b284 100644 --- a/apps/desktop/installer/locales/de_DE.json +++ b/apps/desktop/installer/locales/de_DE.json @@ -1,8 +1,7 @@ { "nsisLanguage": "GERMAN", "strings": { - "INSTALLER_UPDATE_WAIT": "Update wird installiert…", - "INSTALLER_UPDATE_REOPEN": "Die App startet danach neu.", + "INSTALLER_UPDATE_WAIT": "Das Update wird installiert. Anschließend startet die App automatisch neu.", "INSTALLER_INSTALL": "Jetzt installieren", "INSTALLER_FINISH": "Fertigstellen", "INSTALLER_NATIVE_PROGRESS": "Installation wird vorbereitet… %d%%", diff --git a/apps/desktop/installer/locales/en_US.json b/apps/desktop/installer/locales/en_US.json index ff66b2a61a..73af2ede5e 100644 --- a/apps/desktop/installer/locales/en_US.json +++ b/apps/desktop/installer/locales/en_US.json @@ -1,8 +1,7 @@ { "nsisLanguage": "ENGLISH", "strings": { - "INSTALLER_UPDATE_WAIT": "Installing update…", - "INSTALLER_UPDATE_REOPEN": "The app will reopen automatically.", + "INSTALLER_UPDATE_WAIT": "Installing the update. The app will restart automatically when the update is complete.", "INSTALLER_INSTALL": "Install now", "INSTALLER_FINISH": "Finish", "INSTALLER_NATIVE_PROGRESS": "Preparing installation… %d%%", diff --git a/apps/desktop/installer/locales/es_ES.json b/apps/desktop/installer/locales/es_ES.json index a435101736..99e5abe63e 100644 --- a/apps/desktop/installer/locales/es_ES.json +++ b/apps/desktop/installer/locales/es_ES.json @@ -1,8 +1,7 @@ { "nsisLanguage": "SPANISHINTERNATIONAL", "strings": { - "INSTALLER_UPDATE_WAIT": "Instalando actualización…", - "INSTALLER_UPDATE_REOPEN": "La aplicación se abrirá sola.", + "INSTALLER_UPDATE_WAIT": "Instalando la actualización. La aplicación se reiniciará automáticamente al terminar.", "INSTALLER_INSTALL": "Instalar ahora", "INSTALLER_FINISH": "Finalizar", "INSTALLER_NATIVE_PROGRESS": "Preparando la instalación… %d%%", diff --git a/apps/desktop/installer/locales/fr_FR.json b/apps/desktop/installer/locales/fr_FR.json index 17b14b680d..167922256b 100644 --- a/apps/desktop/installer/locales/fr_FR.json +++ b/apps/desktop/installer/locales/fr_FR.json @@ -1,8 +1,7 @@ { "nsisLanguage": "FRENCH", "strings": { - "INSTALLER_UPDATE_WAIT": "Installation en cours…", - "INSTALLER_UPDATE_REOPEN": "Redémarrage automatique.", + "INSTALLER_UPDATE_WAIT": "Installation de la mise à jour en cours. L’application redémarrera automatiquement.", "INSTALLER_INSTALL": "Installer maintenant", "INSTALLER_FINISH": "Terminer", "INSTALLER_NATIVE_PROGRESS": "Préparation de l'installation… %d%%", diff --git a/apps/desktop/installer/locales/ja_JP.json b/apps/desktop/installer/locales/ja_JP.json index afddce9f17..72ef1cc641 100644 --- a/apps/desktop/installer/locales/ja_JP.json +++ b/apps/desktop/installer/locales/ja_JP.json @@ -1,8 +1,7 @@ { "nsisLanguage": "JAPANESE", "strings": { - "INSTALLER_UPDATE_WAIT": "アップデートを適用中…", - "INSTALLER_UPDATE_REOPEN": "完了後、自動的に起動します。", + "INSTALLER_UPDATE_WAIT": "アップデートをインストールしています。完了するとアプリが自動的に再起動します。", "INSTALLER_INSTALL": "今すぐインストール", "INSTALLER_FINISH": "完了", "INSTALLER_NATIVE_PROGRESS": "インストールを準備しています… %d%%", diff --git a/apps/desktop/installer/locales/pt_BR.json b/apps/desktop/installer/locales/pt_BR.json index a1728036dc..93aacd5dc8 100644 --- a/apps/desktop/installer/locales/pt_BR.json +++ b/apps/desktop/installer/locales/pt_BR.json @@ -1,8 +1,7 @@ { "nsisLanguage": "PORTUGUESEBR", "strings": { - "INSTALLER_UPDATE_WAIT": "Instalando atualização…", - "INSTALLER_UPDATE_REOPEN": "O aplicativo reabrirá sozinho.", + "INSTALLER_UPDATE_WAIT": "Instalando a atualização. O aplicativo será reiniciado automaticamente ao concluir.", "INSTALLER_INSTALL": "Instale agora", "INSTALLER_FINISH": "Concluir", "INSTALLER_NATIVE_PROGRESS": "Preparando a instalação… %d%%", diff --git a/apps/desktop/installer/locales/ru_RU.json b/apps/desktop/installer/locales/ru_RU.json index 1f3d39660c..20345ea37c 100644 --- a/apps/desktop/installer/locales/ru_RU.json +++ b/apps/desktop/installer/locales/ru_RU.json @@ -1,8 +1,7 @@ { "nsisLanguage": "RUSSIAN", "strings": { - "INSTALLER_UPDATE_WAIT": "Установка обновления…", - "INSTALLER_UPDATE_REOPEN": "Приложение откроется само.", + "INSTALLER_UPDATE_WAIT": "Идёт установка обновления. После установки приложение перезапустится автоматически.", "INSTALLER_INSTALL": "Установить сейчас", "INSTALLER_FINISH": "Готово", "INSTALLER_NATIVE_PROGRESS": "Подготовка установки… %d%%", diff --git a/apps/desktop/installer/locales/zh_CN.json b/apps/desktop/installer/locales/zh_CN.json index 7df49dc940..7951f34340 100644 --- a/apps/desktop/installer/locales/zh_CN.json +++ b/apps/desktop/installer/locales/zh_CN.json @@ -1,8 +1,7 @@ { "nsisLanguage": "SIMPCHINESE", "strings": { - "INSTALLER_UPDATE_WAIT": "正在安装更新…", - "INSTALLER_UPDATE_REOPEN": "更新完成后将自动重启应用。", + "INSTALLER_UPDATE_WAIT": "正在安装更新,更新完成后将自动重启应用。", "INSTALLER_INSTALL": "立即安装", "INSTALLER_FINISH": "完成", "INSTALLER_NATIVE_PROGRESS": "正在准备安装… %d%%", diff --git a/apps/desktop/installer/restart-banner.h b/apps/desktop/installer/restart-banner.h new file mode 100644 index 0000000000..f508cfdf5c --- /dev/null +++ b/apps/desktop/installer/restart-banner.h @@ -0,0 +1,112 @@ +namespace restart_banner { +constexpr wchar_t stateKey[] = L"HarnessInstaller.RestartBanner"; +struct Style { + DLGPROC previous; + HBRUSH brush; + HFONT font; + HFONT titleFont; + COLORREF background; + COLORREF foreground; +}; + +inline INT_PTR CALLBACK DialogProc(HWND window, UINT message, WPARAM wp, LPARAM lp) { + auto* state = static_cast(GetPropW(window, stateKey)); + if (message == WM_NCCALCSIZE && wp) { + SetWindowLongPtrW(window, DWLP_MSGRESULT, 0); + return TRUE; + } + // Static labels pass pointer hits to the dialog. Let Windows own the move loop. + if (message == WM_NCHITTEST) { + SetWindowLongPtrW(window, DWLP_MSGRESULT, HTCAPTION); + return TRUE; + } + if (message == WM_PAINT || message == WM_PRINTCLIENT) { + PAINTSTRUCT paint{}; + HDC dc = message == WM_PAINT ? BeginPaint(window, &paint) : reinterpret_cast(wp); + RECT bounds{}; + GetClientRect(window, &bounds); + FillRect(dc, &bounds, state->brush); + + if (message == WM_PAINT) EndPaint(window, &paint); + return TRUE; + } + if (message == WM_CTLCOLORDLG || message == WM_CTLCOLORSTATIC) { + const auto dc = reinterpret_cast(wp); + SetTextColor(dc, state->foreground); + SetBkColor(dc, state->background); + return reinterpret_cast(state->brush); + } + auto result = state->previous(window, message, wp, lp); + if (message == WM_NCDESTROY) { + RemovePropW(window, stateKey); + DeleteObject(state->font); + DeleteObject(state->brush); + DeleteObject(state->titleFont); + delete state; + } + return result; +} +} + +// Size the localized paragraph with the font and wrapping used by its static control. +static void InstallerStyleRestartBanner(HWND window, LPCWSTR text, + LPCWSTR, LPCWSTR face, int logicalFontSize, COLORREF background, COLORREF foreground) { + const HWND label = GetDlgItem(window, 1030); + const HWND icon = GetDlgItem(window, 0xffff); + if (!label || !icon) return; + const UINT dpi = GetDpiForWindow(window); + const auto px = [dpi](int value) { return MulDiv(value, dpi, 96); }; + HMODULE module; + if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN, + reinterpret_cast(&restart_banner::DialogProc), &module)) return; + auto* state = new (std::nothrow) restart_banner::Style{ + reinterpret_cast(GetWindowLongPtrW(window, DWLP_DLGPROC)), CreateSolidBrush(background), + CreateFontW(-px(logicalFontSize), 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, + OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH, face), CreateFontW(-px(16), 0, 0, 0, FW_SEMIBOLD, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH, face), background, foreground}; + if (!state) return; + if (!state->brush || !state->titleFont || !state->font || !SetPropW(window, restart_banner::stateKey, state)) { + DeleteObject(state->font); + DeleteObject(state->brush); + DeleteObject(state->titleFont); + delete state; + return; + } + SetWindowLongPtrW(window, DWLP_DLGPROC, reinterpret_cast(&restart_banner::DialogProc)); + const int padding = px(20), gap = px(12), iconSize = px(32), width = px(388); + RECT paragraph{0, 0, width - 2 * padding - gap - iconSize, 0}; + HDC dc = GetDC(window); + HGDIOBJ previous = SelectObject(dc, state->font); + DrawTextW(dc, text, -1, ¶graph, DT_CALCRECT | DT_WORDBREAK | DT_NOPREFIX | DT_EDITCONTROL); + SelectObject(dc, previous); + ReleaseDC(window, dc); + const int titleHeight = px(22), titleGap = px(4); + const int textHeight = titleHeight + titleGap + paragraph.bottom; + const int height = 2 * padding + std::max(iconSize, textHeight); + // The stock Banner dialog has a raised system edge outside the themed client area. + SetWindowLongPtrW(window, GWL_STYLE, GetWindowLongPtrW(window, GWL_STYLE) & ~DS_MODALFRAME); + SetWindowLongPtrW(window, GWL_EXSTYLE, GetWindowLongPtrW(window, GWL_EXSTYLE) & + ~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE)); + const BOOL dark = background != RGB(255, 255, 255); + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, &dark, sizeof(dark)); + ApplyInstallerDwmFrame(window); + RECT client{}, outer{}; + GetClientRect(window, &client); + GetWindowRect(window, &outer); + const int newWidth = width + outer.right - outer.left - client.right; + const int newHeight = height + outer.bottom - outer.top - client.bottom; + SetWindowPos(window, nullptr, (outer.left + outer.right - newWidth) / 2, + (outer.top + outer.bottom - newHeight) / 2, newWidth, newHeight, SWP_NOACTIVATE | SWP_NOZORDER); + const HWND title = GetDlgItem(window, 76); + SetWindowLongPtrW(title, GWL_STYLE, (GetWindowLongPtrW(title, GWL_STYLE) & ~SS_TYPEMASK) | SS_LEFT | SS_NOPREFIX); + SetWindowTextW(title, L"DeepSeek Harness"); + SendMessageW(title, WM_SETFONT, reinterpret_cast(state->titleFont), FALSE); + SetWindowPos(title, nullptr, padding + iconSize + gap, (height - textHeight) / 2, + width - 2 * padding - gap - iconSize, titleHeight, SWP_NOZORDER | SWP_SHOWWINDOW); + const LONG_PTR style = GetWindowLongPtrW(label, GWL_STYLE); + SetWindowLongPtrW(label, GWL_STYLE, (style & ~SS_TYPEMASK) | SS_LEFT | SS_NOPREFIX | SS_EDITCONTROL); + SendMessageW(label, WM_SETFONT, reinterpret_cast(state->font), FALSE); + SetWindowPos(icon, nullptr, padding, (height - iconSize) / 2, iconSize, iconSize, SWP_NOZORDER); + SetWindowPos(label, nullptr, padding + iconSize + gap, (height - textHeight) / 2 + titleHeight + titleGap, + width - 2 * padding - gap - iconSize, paragraph.bottom, SWP_NOZORDER); + RedrawWindow(window, nullptr, nullptr, RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN | RDW_UPDATENOW); +} diff --git a/apps/desktop/installer/restart-status.h b/apps/desktop/installer/restart-status.h new file mode 100644 index 0000000000..06a5560eb0 --- /dev/null +++ b/apps/desktop/installer/restart-status.h @@ -0,0 +1,132 @@ +#include +#include +#include + +namespace restart_status { +struct Launch { + std::wstring command; + std::wstring program; + HANDLE job; +}; + +// Process creation can involve security software. The installer never joins this thread. +static DWORD WINAPI LaunchWorker(void* data) { + std::unique_ptr launch(static_cast(data)); + JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{}; + limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + SIZE_T bytes = 0; + InitializeProcThreadAttributeList(nullptr, 1, 0, &bytes); + auto* attributes = static_cast(HeapAlloc(GetProcessHeap(), 0, bytes)); + bool started = false; + if (attributes && SetInformationJobObject(launch->job, JobObjectExtendedLimitInformation, &limits, sizeof(limits)) && + InitializeProcThreadAttributeList(attributes, 1, 0, &bytes)) { + if (UpdateProcThreadAttribute(attributes, 0, PROC_THREAD_ATTRIBUTE_JOB_LIST, &launch->job, sizeof(HANDLE), nullptr, nullptr)) { + STARTUPINFOEXW startup{}; + startup.StartupInfo.cb = sizeof(startup); + startup.lpAttributeList = attributes; + PROCESS_INFORMATION process{}; + started = CreateProcessW(launch->program.c_str(), &launch->command[0], nullptr, nullptr, FALSE, + EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr, &startup.StartupInfo, &process) != FALSE; + if (started) { + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + } + } + DeleteProcThreadAttributeList(attributes); + } + if (attributes) HeapFree(GetProcessHeap(), 0, attributes); + CloseHandle(launch->job); + return 0; +} + +static INT_PTR CALLBACK Dialog(HWND window, UINT message, WPARAM, LPARAM) { + if (message == WM_CLOSE) { DestroyWindow(window); return TRUE; } + if (message == WM_DESTROY) { PostQuitMessage(0); return TRUE; } + return FALSE; +} +} + +// The shell host is a member of a private job from its first instruction. Parent exit +// terminates even a stuck window, without a child-to-installer IPC or acknowledgment. +extern "C" __declspec(dllexport) HANDLE __cdecl InstallerStartRestartStatus(LPCWSTR installer, LPCWSTR theme, + int language, LPCWSTR text) noexcept { + try { + HMODULE module{}; + if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN, + reinterpret_cast(&restart_status::LaunchWorker), &module)) return nullptr; + wchar_t dll[MAX_PATH]{}, system[MAX_PATH]{}; + const DWORD length = GetModuleFileNameW(module, dll, ARRAYSIZE(dll)); + const UINT sysLength = GetSystemDirectoryW(system, ARRAYSIZE(system)); + if (!length || length >= ARRAYSIZE(dll) || !sysLength || sysLength >= ARRAYSIZE(system)) return nullptr; + auto launch = std::make_unique(); + launch->program = std::wstring(system) + L"\\rundll32.exe"; + launch->command = L"\"" + launch->program + L"\" \"" + dll + L"\",ShowRestartStatus \"" + + installer + L"\" " + std::to_wstring(language) + L" " + theme + L" " + text; + HANDLE owner = CreateJobObjectW(nullptr, nullptr); + if (!owner) return nullptr; + if (!DuplicateHandle(GetCurrentProcess(), owner, GetCurrentProcess(), &launch->job, 0, FALSE, DUPLICATE_SAME_ACCESS)) { + CloseHandle(owner); + return nullptr; + } + auto* args = launch.release(); + HANDLE thread = CreateThread(nullptr, 0, restart_status::LaunchWorker, args, 0, nullptr); + if (thread) { + CloseHandle(thread); + return owner; + } + CloseHandle(owner); + CloseHandle(args->job); + delete args; + } catch (const std::bad_alloc&) { + // A missing status window does not change the installation. + } + return nullptr; +} + +#pragma comment(linker, "/EXPORT:ShowRestartStatusW=_ShowRestartStatusW@16") +extern "C" void CALLBACK ShowRestartStatusW(HWND, HINSTANCE instance, LPWSTR raw, int) { + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX); + if (!raw || *raw != L'"') return; + wchar_t* end = wcschr(raw + 1, L'"'); + if (!end || end[1] != L' ') return; + *end = 0; + const wchar_t* installer = raw + 1; + wchar_t* theme{}; + const long language = wcstol(end + 2, &theme, 10); + if (*theme != L' ') return; + wchar_t* text = wcschr(++theme, L' '); + if (!text) return; + *text++ = 0; + const bool dark = wcscmp(theme, L"dark") == 0; + wchar_t locale[LOCALE_NAME_MAX_LENGTH]{}; + LCIDToLocaleName(MAKELCID(language, SORT_DEFAULT), locale, ARRAYSIZE(locale), 0); + SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + struct { DLGTEMPLATE dialog; WORD menu, cls, title; } layout{}; + layout.dialog.style = WS_POPUP | DS_CENTER | DS_MODALFRAME; + layout.dialog.dwExtendedStyle = WS_EX_APPWINDOW; + layout.dialog.cx = 180; + layout.dialog.cy = 50; + HWND window = CreateDialogIndirectParamW(instance, &layout.dialog, nullptr, restart_status::Dialog, 0); + if (!window) return; + SetWindowTextW(window, L"DeepSeek Harness"); + const DWORD style = WS_CHILD | WS_VISIBLE; + HWND image = CreateWindowW(L"STATIC", L"", style | SS_ICON, 0,0,0,0, window, + reinterpret_cast(0xffff), instance, nullptr); + HWND title = CreateWindowW(L"STATIC", L"", style, 0,0,0,0, window, reinterpret_cast(76), instance, nullptr); + HWND label = CreateWindowW(L"STATIC", text, style, 0,0,0,0, window, reinterpret_cast(1030), instance, nullptr); + if (!image || !title || !label) return; + HICON large{}, smallIcon{}; + ExtractIconExW(installer, 0, &large, &smallIcon, 1); + SendMessageW(image, STM_SETICON, reinterpret_cast(large), 0); + SendMessageW(window, WM_SETICON, ICON_BIG, reinterpret_cast(large)); + SendMessageW(window, WM_SETICON, ICON_SMALL, reinterpret_cast(smallIcon)); + InstallerStyleRestartBanner(window, text, locale, L"Microsoft YaHei UI", 14, + dark ? RGB(21,21,23) : RGB(255,255,255), dark ? RGB(255,255,255) : RGB(15,17,21)); + if (!GetPropW(window, restart_banner::stateKey)) return; + ShowWindow(window, SW_SHOWNOACTIVATE); + MSG message{}; + while (GetMessageW(&message, nullptr, 0, 0) > 0) { + TranslateMessage(&message); + DispatchMessageW(&message); + } +} diff --git a/apps/desktop/installer/window-frame.cpp b/apps/desktop/installer/window-frame.cpp index 9c9a9e4f81..67ec64ce40 100644 --- a/apps/desktop/installer/window-frame.cpp +++ b/apps/desktop/installer/window-frame.cpp @@ -10,6 +10,24 @@ #include #include #include +static HRESULT ApplyInstallerDwmFrame(HWND window) { + // DWM retains native transitions; WM_NCCALCSIZE removes the visible caption. + SetWindowLongW(window, GWL_STYLE, GetWindowLongW(window, GWL_STYLE) | WS_THICKFRAME | WS_CAPTION); + const DWMNCRENDERINGPOLICY policy = DWMNCRP_ENABLED; + HRESULT result = DwmSetWindowAttribute(window, DWMWA_NCRENDERING_POLICY, &policy, sizeof(policy)); + if (FAILED(result)) return result; + const DWORD rounded = 2; + // Windows 10 does not support the Windows 11 corner preference. + DwmSetWindowAttribute(window, 33, &rounded, sizeof(rounded)); + const MARGINS margins = {1, 1, 1, 1}; + result = DwmExtendFrameIntoClientArea(window, &margins); + SetWindowPos(window, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + return result; +} + +#include "restart-banner.h" +#include "restart-status.h" #include "progress.h" #include "extract.h" #include "extract-report.h" @@ -419,19 +437,7 @@ extern "C" __declspec(dllexport) HRESULT __cdecl InstallerApplyFrame(HWND window ShowWindow(child, SW_HIDE); } } - // DWM retains native transitions; WM_NCCALCSIZE removes the visible caption. - SetWindowLongW(window, GWL_STYLE, GetWindowLongW(window, GWL_STYLE) | WS_THICKFRAME | WS_CAPTION); - const DWMNCRENDERINGPOLICY policy = DWMNCRP_ENABLED; - HRESULT result = DwmSetWindowAttribute(window, DWMWA_NCRENDERING_POLICY, &policy, sizeof(policy)); - if (FAILED(result)) return result; - const DWORD rounded = 2; - // Windows 10 does not support the Windows 11 corner preference. - DwmSetWindowAttribute(window, 33, &rounded, sizeof(rounded)); - const MARGINS margins = {1, 1, 1, 1}; - result = DwmExtendFrameIntoClientArea(window, &margins); - SetWindowPos(window, nullptr, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); - return result; + return ApplyInstallerDwmFrame(window); } // Present the first interactive page after resource preparation without keeping the installer topmost. diff --git a/apps/desktop/scripts/installer.nsh b/apps/desktop/scripts/installer.nsh index 5ac72336da..219522f1ee 100644 --- a/apps/desktop/scripts/installer.nsh +++ b/apps/desktop/scripts/installer.nsh @@ -244,6 +244,11 @@ ManifestDPIAware true ${EndIf} !insertmacro InstallerPublishStage 4 !insertmacro dshFinishDirectories + ${If} $RestartStatusJob != "" + ${AndIf} $RestartStatusJob != 0 + System::Call 'kernel32::CloseHandle(p $RestartStatusJob)' + StrCpy $RestartStatusJob 0 + ${EndIf} ; Standard uninstall-entry metadata read by inventory tools; the upstream template records it only under its private key. WriteRegStr SHELL_CONTEXT "${UNINSTALL_REGISTRY_KEY}" InstallLocation "$INSTDIR" ${If} $0 == 1 diff --git a/apps/desktop/scripts/restart-banner.nsh b/apps/desktop/scripts/restart-banner.nsh index 04d646a0ae..931fa3c20f 100644 --- a/apps/desktop/scripts/restart-banner.nsh +++ b/apps/desktop/scripts/restart-banner.nsh @@ -1,22 +1,23 @@ +!ifndef BUILD_UNINSTALLER +Var RestartStatusJob +!endif + !macro ShowRestartBanner - Banner::show /NOUNLOAD /set 76 "$(INSTALLER_UPDATE_WAIT)" /set 1030 "$(INSTALLER_UPDATE_REOPEN)" "${PRODUCT_NAME}" Push $0 - Push $1 - Push $2 - Push $3 - Banner::getWindow /NOUNLOAD - Pop $0 - ; The shell and the dialog image use different icon sizes from the same installer. - System::Call 'shell32::ExtractIconExW(w "$EXEPATH", i 0, *p .r1, *p .r2, i 1) i.r3' - ${If} $1 != 0 - ${AndIf} $2 != 0 - SendMessage $0 ${WM_SETICON} 1 $1 - SendMessage $0 ${WM_SETICON} 0 $2 - GetDlgItem $3 $0 -1 - SendMessage $3 0x170 $1 0 + ${If} ${Errors} + StrCpy $0 1 + ${Else} + StrCpy $0 0 ${EndIf} - Pop $3 - Pop $2 + Push $1 + StrCpy $1 0 + System::Call '$PLUGINSDIR\window-frame.dll::InstallerStartRestartStatus(w "$EXEPATH", w "$InstallerTheme", i $LANGUAGE, w "$(INSTALLER_UPDATE_WAIT)") p.r1 ?c' + StrCpy $RestartStatusJob $1 Pop $1 + ${If} $0 == 1 + SetErrors + ${Else} + ClearErrors + ${EndIf} Pop $0 !macroend