Files
deepseek-harness/apps/desktop/README.md
T

125 KiB
Raw Blame History

DeepSeek Harness Desktop

English | 中文

Desktop analytics follows the product collection policy, 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.

The desktop application is an Electron shell around the complete dsh Web application. An Electron RunAsNode child starts the shared profile runner, and Electron immediately loads the packaged Web entry at dsh-app://app/. Its shared loading page waits for Host boot injections, then starts the client without navigating to another document. Electron forwards application HTTP requests to the authenticated Web Host, dropping connection-level response headers (transfer-encoding, connection, keep-alive) that describe the Node fetch rather than the resource, and marking plugin bundle responses no-store because their per-launch revisions would only accumulate in Chromium's disk cache; WebSocket streams connect to that Host with credentials attached only for the owned application window. Node IPC carries boot injections, readiness, and shutdown. Desktop listens on an OS-assigned port by default, so it cannot collide with Web’s 3080 or a reserved port; a webserver.config.port patch can override it.

Desktop binds 127.0.0.1, the address Electron dials for the ready URL and the WebSocket credential filter. WebSocket streams use ws: or wss: according to the Host listener; credentials require matching authority and scheme.

The first application-menu command, About DeepSeek Harness, opens Electron's native About panel with the application icon, product name, and installed release version. The menu follows the Desktop shell locale. On macOS, Hide, Hide Others, Show All, and Quit use localized labels; Hide and Quit include the DeepSeek Harness product name. These commands retain their native actions and shortcuts. macOS reads the icon from its application bundle, so an unpackaged development launch displays Electron's icon; Windows receives the packaged PNG.

Desktop’s local native directory flow opens an Electron folder dialog attached to the application window, restoring, showing, and focusing that window first. Concurrent requests share one dialog; cancellation returns no path and failures remain retryable. Ordinary Web uses the Host chooser. Browse mode lists Host directories. On Linux without zenity or kdialog, automatic selection uses browse instead of the Electron dialog.

Desktop ships one pnpm distribution at resources/runtime/primary-runtime/dependencies/pnpm. Build-time production installation, the installed dsh CLI, Creator and the Web Plugin Manager execute its bin/pnpm.mjs with Electron Node mode; package operations do not require pnpm on PATH. The private Node launcher environment applies only to package operations. Agent workspace dependencies copy the same self-contained primary-runtime payload into the Harness home and execute pnpm with its standalone Node.

Embedded Platform documents use a persistent WebContentsView partition keyed by a hash of the Platform origin and stable account ID. localStorage preferences, including dismissed notices, survive view closure and application restart; different accounts and origins use separate storage. The Host supplies the account ID from its latest successful profile read; while no ID is known, the document opens in a disposable partition that keeps no preference across opens. Opening a persistent partition clears cookies, filesystem, IndexedDB, Cache Storage, the HTTP and shader caches, service workers, and HTTP authentication before the document loads; a disposable partition clears all of its storage. Closing a view destroys the document, removes its request interceptors, and schedules the same cleanup, so authentication left by an unclean exit is cleared before the next document loads. The next open and application shutdown await that cleanup, and an update install waits for it before the installer takes over the exit. A failed cleanup rejects that open and blocks the same account until a later cleanup succeeds; other accounts keep opening. Sign-out destroys the document but retains account-scoped preferences for the next sign-in. A document already open in a disposable partition stays mounted when the account ID arrives for the same credential; the next open uses the account-scoped partition. The storage decision describes the retention policy. Host sends account credentials through private Node IPC; account RPC and the Harness renderer receive no token. Before page scripts execute, the Platform preload performs one synchronous IPC to read the prepared main-process credential. It exposes displayMode, synchronous getAuthToken() and getLocale() getters, and onLocaleChange(listener), which returns an unsubscribe function. Both getters read preload memory without further IPC. The bootstrap includes the resolved Desktop language (zh_CN or en_US); Settings language changes update the preload cache and notify the open Platform document without reloading. Platform applies this language before its first render and does not persist it as a browser preference. The main-process handler performs only sender validation and an in-memory read; it never waits for Host, disk, or network. Trusted-page initialization failures retain embedded mode and make the getter throw, preventing browser-credential fallback. Only the owned Platform main frame on the configured issuer origin can initialize. Sign-out, credential replacement, Host shutdown, and view closure destroy the document. Cross-origin document navigation is blocked. HTTPS links requesting a new window open in the system browser without the embedded session or token; other schemes and URL credentials are rejected. Native views occupy the viewport below the Account feature’s return bar.

Desktop Host Platform API requests and update-policy requests identify the installed client with the same Platform client headers: platform, client version, locale, timezone offset in seconds, and a bundle id that is intentionally empty. Account operations take the calling UI's identity per call; the account provider owns API-only header configuration. Update policy additionally reports architecture, update channel, and bundled runtime version. The native account-state decoder accepts no-response sign-in failures and preserves the error code in the UI-safe projection.

A server-expired account credential returns to Welcome when no official API key is configured; an available API key keeps the workspace open. Explicit sign-out follows the same rule. Both Welcome and the workspace display the localized expiry notice.

Desktop microphone access is restricted to audio requests from the primary dsh-app://app frame. macOS uses system microphone authorization, a packaged usage description, and com.apple.security.device.audio-input in the main application and Helper signatures.

Press F12 (Fn+F12 on media-key keyboards), Command+Option+I on macOS, or Ctrl+Shift+I on Windows to toggle DevTools for the focused application page, including in packaged builds. These native shortcuts use hidden application-menu items. Update overlays and packaged embedded browser guests disable DevTools.

Terminal command

The application menu's Manage dsh Command… entry, immediately below Check for Updates…, shows the current command and offers Install, Repair, and Remove. The command uses Desktop's installed runtime and the ordinary dsh CLI, including when the Desktop application is closed. Open a new terminal after installation and run dsh --version.

On macOS, installation creates /usr/local/bin/dsh and requests administrator authentication when the directory requires it. Shell startup files are unchanged. On Windows, the management dialog registers the command in the current user's PATH. A pre-existing command requires confirmation before switching; repairing Desktop's own selected command does not repeat that confirmation. The macOS link preserves and restores the displaced launcher; Windows preserves other PATH entries, including entries that predate registration. A command with higher PATH precedence is reported with its location. After moving the application, use Repair on macOS or Install from the new location on Windows. Remove leaves unrelated installations intact.

Command registration is optional after installing Desktop. Use Manage dsh Command… → Remove before uninstalling Desktop to remove its CLI registration; the application uninstaller does not remove it. Finish CLI commands before updating or uninstalling Desktop. The CLI runtime version follows the installed Desktop release. See bundled command runtime for Desktop plugin commands and runtime limitations.

Closing the window and quitting

Closing the main window (macOS close button and ⌘W; Windows ×, Alt+F4, and the taskbar Close window command) hides it; Windows asks for acknowledgement before the first hide. The page and the Host keep running, tasks continue, and the next show presents the same document with its session, drafts, and scroll position; a fullscreen macOS window leaves fullscreen before hiding. The window returns through the Dock icon, a second launch, or dsh://open on macOS, and through the tray on Windows. Minimize is unchanged. Closing the welcome window before the workspace opens quits on Windows and, on macOS, keeps the application in the Dock without a window.

Windows keeps a tray icon for the whole run. Its tooltip is the product name, a single click shows and focuses the window, and the context menu offers Open DeepSeek Harness and Quit DeepSeek Harness in the shell locale. Before the first hide, the shared update-dialog overlay explains that running tasks continue and the window can be reopened from the system tray. Confirm hides the window and writes background-close-confirmed under Electron userData; Escape, dismissal, or dialog failure keeps the window visible and does not record acknowledgement. Repeated close requests focus the existing shell dialog. In-place updates retain the marker and uninstall removes it. The old background-notice-shown marker does not suppress this confirmation. Closing the window sends no system notification. The tray bitmaps are resources/tray-windows.ico, rendered from resources/icon-windows.svg at 16, 20, 24, 32, 40, 48, and 64 pixels by pnpm run render:tray-icon, and packaged as resources/tray.ico. macOS ships no menu bar icon.

Every ordinary quit entry — ⌘Q, the application menu, the Dock menu, the Windows tray and caption Application menu, and the quits caused by closing the mandatory-update or 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 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.

The tray renderer enlarges the whale by 20% around the tile center while preserving the background and aspect ratio; application and installer icons retain their original proportions.

Key technical decisions

The original artwork lives in resources/icon.png and resources/icon.svg; platform adaptations retain the whale and gradients in resources/icon-windows.* and resources/icon-macos.*. Export each platform SVG as a transparent 1024×1024 PNG. Electron-builder generates the multi-size ICO for the Windows application, installer, and uninstaller (Windows icon requirements). The installation pages use matching artwork in both themes; the uninstaller's welcome and finish pages share installer/assets/uninstaller-sidebar.png, converted to a 164×314 BMP during preparation.

Shortcut overrides are stored in app.getPath('userData')/keybindings.json, separately from DSH_HOME. The main process validates and serializes changes before publishing accepted bindings. Failed reads retain the last accepted bindings and block edits, including Restore All; unreadable and future-version files remain unchanged. Development can isolate these preferences with DSH_DESKTOP_USER_DATA_DIR; the launcher prints its resolved path. See the shortcut service for format and conflict semantics.

The macOS File menu displays the accepted single-key binding, including arrow keys, and routes Close Page or Window through the client page owner. On Windows and macOS, all complete accepted bindings are intercepted before main-document, embedded-frame, and browser-guest input, including editing and terminal input. Recording and input-method composition remain protected. Update overlays block product shortcuts and editing-key delivery to the parent window and its browser guests from creation until the last overlay closes. Every overlay opening or closing invalidates pending chord state. The main process shares one overlay owner between update dialogs and shortcut input through explicit creation and input-state capabilities. Two-key chords leave the first key’s initial press available to the page and do not intercept its release; complete chords and their repeats are consumed. The renderer leaves configurable binding dispatch to the native adapter. Chords have no native menu accelerator. Accepted commands are forwarded once through the trusted preload. Linux dispatches main-document shortcuts through the DOM and forwards accepted embedded-frame bindings to the client resolver. Closing the last window preserves macOS application lifetime; Windows exits the desktop instance and stops its tasks. The close decision records this lifecycle choice.

The macOS PNG uses an inset rounded background for legacy ICNS packaging, with representations up to 1024 pixels. It is a flattened icon, not an Icon Composer document. Apple's app icon guidance describes unmasked layers for Icon Composer; those inputs require a separate macOS export and must not reuse the rounded ICNS artwork. Verify Finder and Dock appearance on supported macOS versions before release.

Bundled workspace dependencies

electron-builder copies only the manifest's dependencies into app.asar/node_modules, so the Electron main bundle lib/main.js inlines its workspace devDependencies and leaves electron, Node builtins, and those dependencies as its only bare imports; a sandboxed preload may leave only electron, events, timers, and url, the modules its require polyfill resolves. The main bundle resolves the inlined packages from their lib/ output, so the root build:lib:host bundles apps/desktop after the concurrent workspace tsdown pass, and desktop-bundle-imports fails any Desktop bundle whose static, dynamic, or require() import the packaged application cannot resolve. Without that check an import rolldown could not resolve ships as an external specifier and fails at launch with ERR_MODULE_NOT_FOUND. The archived bundle-order decision records the alternatives.

Signed Windows packaging scans the primary runtime and application production dependencies for PE content, including files without a conventional extension. The final scan covers the entire unpacked application. Directory links, malformed MZ files and non-PE .exe, .dll or .pyd files stop packaging; data files beginning with MZ are also rejected unless they contain a valid PE header. It preserves valid vendor signatures and signs unsigned code before sealing runtime hashes or executing smoke checks. Public-key inspection uses batches of 32 files with at most four processes; hardware-token signing remains serial, and each new signature must match the configured certificate and carry a timestamp. Hardware signing and verification failures stop the run; isolated timestamp requests follow the bounded policy below. Electron-builder preserves copied runtime executables only after signature and exact-byte verification. A final PE-signature audit and a fresh-cache ASAR payload and Host smoke must pass before the release completion record is written. Development, preparation-only and unsigned builds do not use the hardware token and can be blocked by Windows code-integrity policy; no build mode disables that policy. Passing smoke checks does not establish compatibility with every enterprise policy.

Desktop carries independent Python, Node.js and pnpm distributions. Python includes numpy, pandas, python-docx, python-pptx, openpyxl, Pillow, lxml and XlsxWriter with their complete dependencies. The load_workspace_dependencies tool installs this payload offline on first use under $DSH_HOME/dsh-runtimes/dsh-primary-runtime (normally ~/.dsh/dsh-runtimes/dsh-primary-runtime) and returns absolute interpreter, pnpm script and library paths plus pythonDistributions, the bundled distribution names and versions. The version report excludes user-installed additions. Office tasks prefer these libraries unless user or workspace instructions select another environment. Execute the pnpm script with the returned Node executable. The returned Node library directory is reserved for bundled libraries, not pnpm's global installation directory.

Desktop exposes the compact office router and named authoring/operation subskills. Authoring uses bundled Python libraries and the shared structural checker; operation skills invoke the stable dsoffice command. Skills live in runtime/office-skills outside ASAR so Python can read resources. See the Office skill package for progressive loading and limitations.

The payload follows the Desktop release. runtime.json records the Desktop version, target, top-level interpreter/package-manager versions and the Python distribution map, and a digest of the selected target’s locked payload inputs and assembly format. Distribution names use PEP 503 normalization; duplicate normalized names reject the manifest. Legacy components manifests remain readable through normalization, with their existing library-version consistency checks. Matching installations are reused; a dependency or archive change replaces the directory after a complete staged copy even when the Desktop version stays unchanged. Older manifests without a digest are replaced on their next installation. User-added Python packages remain only while the payload identity matches. A failed directory replacement retains the previous installation; Windows may refuse replacement while an interpreter is still running.

The private Desktop runtime/bin directory is added only to package-installation processes, not the Host PATH inherited by PTC and agent shells. This tool does not change PATH, environment variables or user package-manager configuration. pnpm retains its own defaults and user settings for global packages, executable entries and its store, including native errors when the environment does not support global installation. There is no separate dependency updater. The primary-runtime decision records these choices.

Node prepares the bundled interpreters and Python libraries without a system Python or pip. The download lock pins interpreter archives, Python distribution versions and target-specific wheel URLs and hashes; the shared builder resolves pnpm from the root development dependency pin. The root package-manager version and the Desktop pin are checked for agreement. Wheel filenames and distribution versions must agree for every target. Preserve key order within the selected target, wheel records and distribution map, plus wheel-entry order; these affect payload identity, while top-level lock key order does not. Library wheels unpack into site-packages, retaining auxiliary files under each wheel's .data/scripts directory without generating command wrappers. Other installation schemes are rejected. Native-target checks verify the locked wheel set and versions, permit the interpreter's bundled pip, and check the Python version, Office document read/write operations and dependency completeness without writing bytecode after staging cleanup and again after macOS signing. The standalone Node executable receives the JIT entitlement required by V8; the x64 target also receives com.apple.security.cs.allow-unsigned-executable-memory for executable code pages, including under Rosetta. ARM64 Node and Office helpers retain JIT-only permissions. Cross-target execution and signed installation require the target release host. Both dev:desktop and start:desktop prepare .desktop-build/targets/<target>/runtime/primary-runtime before launching Electron; first use may download locked dependencies. An unfinished preparation cannot report a successful launcher exit.

The shared Desktop/SDK runtime builder omits directories named tests beneath NumPy and pandas before copying or signing the payload, and removes their rows from each distribution’s RECORD. It retains numpy.testing, pandas.testing, pandas._testing, native extensions and licenses. Complete upstream suites are unavailable; native checks verify the retained testing APIs, absence of test directories and existence of recorded files.

Decision Why Direct consequence
Release identity The shell API, Web client, backend, and plugin graph are qualified as one combination; independent versions would create untested combinations and ambiguous update availability. Electron and @deepseek-ai/dsh always have the same exact version. A dsh upgrade is a Desktop release, even when the shell code is unchanged.
Runtime The application must run without a system Node.js or pnpm installation. dsh runs under Electron with ELECTRON_RUN_AS_NODE=1 and --expose-internals and every package operation uses the bundled pnpm. Package-manager configuration and the Host environment follow the user's settings. Package scripts use a node shell launcher that forwards to Electron.
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.
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 Web serving and authentication share one implementation. Electron loads packaged Web assets; the Host supplies boot injections and authenticated APIs.
Plugin changes Desktop and Web need the same installation and activation behavior. The main application uses the shared Web Plugin Manager and bundled pnpm.
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.

The thin-wrapper decision owns shared Web behavior and Desktop adapters. The Electron packaging and update decision owns release identity, signing, and update qualification.

Welcome loads the shared Toast palette and shadow tokens, with system typography for body-mounted notifications.

Bundled command runtime

The installed resources/runtime/cli/bin/dsh shell script (dsh.cmd on Windows) runs the ordinary CLI dispatcher using Desktop's Electron executable and bundled pnpm. It works while Desktop is closed and retains the Electron runtime limitations. Ordinary profile, configuration, and plugin commands use the same implementation as npm dsh; the command does not open Desktop.

To manage Desktop's plugins, launch Desktop once to initialize its profile, fully quit the application, then run dsh plugin --profile desktop add <package>, list, or remove <package>. Reopen Desktop to use the changes. Package operations retain the shared profile write lock and compatibility checks. The installed command refuses an uninitialized Desktop profile instead of creating an ordinary CLI profile in its place.

Finish CLI commands before updating or uninstalling Desktop; the installer does not coordinate with running commands. The CLI does not supply Office authoring-skill resource paths. Desktop's existing Office setup and Web attachment conversion retain their own resource configuration. prepare:runtime copies the launch scripts without compiling a native CLI launcher.

Installation ownership

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 this profile. The Desktop-installed command can manage its plugins while the application is quit; npm-installed dsh cannot mutate it.

The application preload exposes boot readiness, fatal startup reporting, native directory selection, the __DSH_HOST_PATHS__ bridge for composer path references, and the lease-scoped Browser bridge only to dsh-app://app documents. The same preload forwards the main-process machine description from dshDesktop.deviceInfo() as name=value fields separated by ; : platform, os, app_arch (the architecture the application binary runs as, which differs from the hardware architecture under emulation), cpu, and memory_gib (total physical memory in GiB with one decimal). A field is omitted when its source is unavailable. The description carries no hostname, user name, or serial number. Product documents use the shared authenticated HTTP APIs and receive the Desktop marker, update presentation, and an action that opens native confirmation; they cannot choose artifacts or authorize installation. Plugin management uses the Web application's authenticated HTTP APIs. Electron serves update-dialog documents and assets locally at dsh-app://shell/, independently of Host readiness. Electron exposes no plugin-management IPC or separate management document. No renderer receives filesystem access, raw Electron IPC, a shell, or arbitrary pnpm arguments.

Only the main application window enables <webview>. Guest attachment must match a main-issued lease and partition; guests keep sandbox, context isolation and Web security without Node integration or guest preload. Browser IPC listeners are created only for the application document. Sidebar Browser describes storage grouping and guest limitations; Host authentication remains required independently of URL filtering.

The dsh-app://shell/ origin serves packaged update documents, scripts, and styles without contacting the Host. Static requests retain GET/HEAD, path-containment, and MIME handling; each update document keeps its isolated preload and owned-window IPC checks.

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.

Update copy preserves the complete version, including prerelease suffixes, without adding v or V. When no update is available, the dialog title reports the result and the body shows the current version.

Electron chooses typed English or Chinese shell copy from its application locale and falls back to English. The macOS application bundle declares English and Simplified Chinese in CFBundleLocalizations, allowing macOS to match the initial application language to the user’s preferred languages. Saved Client UI language preferences still take precedence for the main interface. On Windows, the main document's language updates desktop menus, recovery and update prompts. The repository Client UI i18n gate checks desktop sources.

Windows and macOS relay native fullscreen transitions and the state after each page load to the renderer, so overlays can adapt their caption clearance.

Windows uses a 40-DIP caption with native window controls and colors synchronized from the application palette. Localized Application and Edit entries beside the sidebar toggle open native popup menus. They mount only after the application frame publishes its shell overlay seat, and remain absent during startup loading. Application provides Check for Updates and Exit; Edit provides undo, redo, cut, copy, paste, delete, and select all by sending the corresponding keys to the focused editor, independently of custom shortcut bindings. Plugin management uses the main application's Plugins page. No separate native menu row appears on Alt. Other platforms retain their native menus. Editable fields retain keyboard commands and a context menu without shortcut labels; Chromium supplies command availability, and selected read-only text offers Copy.

On macOS the custom menu retains Electron's standard Window menu and application hide commands, including Minimize (⌘M) and Hide (⌘H). Linux keeps the application and Edit menus.

Runtime and plugin activation

The signed resources/app.asar/dsh/desktop-runtime.json binds the shell version, Electron's Node version, platform, architecture, shared package versions, and final file inventory. Startup reads the metadata and checks shared package records. Release schema, shell version, target compatibility, and file integrity are verified during packaging. Reintroducing startup release comparisons requires a concrete installation failure that packaging validation and actual Host diagnostics cannot adequately explain. Core packages are never copied into profile storage or installed by pnpm at first launch.

  1. The main window loads the shared Web loading page offscreen from packaged static assets before profile preparation or backend startup. Shared profile initialization creates missing manifest, empty user patch, and pnpm workspace files without overwriting existing files.
  2. Before Host startup, Desktop validates the runtime descriptor and prepares the profile without modifying installed packages, declarations, or the lockfile; it removes only the .dsh-module-fallback projections earlier link-backend launches wrote, and startup never runs pnpm. Packages inside the profile keep native precedence, and installation package names resolve through the runtime resolution (lookup order; cleanup removal).
  3. Changes to Electron's Node version, platform, or architecture preserve installed plugins. Native incompatibilities surface during loading and can be repaired through pnpm.
  4. The main application’s Plugins page uses the shared plugin manager against the Desktop profile. Package operations use bundled pnpm with normal user and profile configuration.
  5. The shared manager owns installation errors, activation, and restart requirements. Native recovery can disable third-party bundles even when the Host cannot start.

The Web plugin UI owns the management interface. Desktop profile initialization and recovery preserve installed plugin files.

Fatal main-window creation, main-document loading, preload, renderer, Web initialization, or backend failures open one native recovery dialog per application process. It shows a bounded tail of the first error, notes any truncation, and offers Exit, Restart, and Disable third-party plugins, back up profile patch, and restart. Startup failures retain the Web loading page and spinner; runtime failures retain the current page. Expected shutdowns, cancelled navigation, and ordinary requests do not trigger recovery. The shared Web plugin manager reports package-operation errors; Host startup failures after plugin changes enter native recovery. There is no startup timeout heuristic. A listener failure containing listen EADDRINUSE replaces the diagnostics and reinstall advice with guidance to quit other running DSH instances, and offers only Exit and Restart.

Native dialog details include at most 1,200 UTF-16 code units and eight diagnostic lines, plus the path of the crash report described below when one was written. 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.

Before the first fatal dialog opens, Electron writes a crash report to the platform logs directory (app.getPath('logs'): ~/Library/Logs/DeepSeek Harness on macOS, logs under the application’s userData directory on Windows and Linux), waiting at most one second for the write; a slow or failed write shows the dialog without a path. The file crash-<UTC time>-<source>.log records the source (host for a Host exit, web-boot for a renderer boot failure, renderer for a renderer or document failure, main for a shell error), whether the backend had reached ready, application and runtime versions, the error with its enumerable properties and cause chain (cut at 256 KiB), the Host’s own inspected error (at most 64 KiB) when the Host reported a startup failure over IPC before exiting, and the primary window’s recent error-level console output (at most 64 KiB). A Host exit report therefore contains the retained stderr tail, which may include plugin output. A fatal failure during shutdown writes a report without opening a dialog. Files are owner-only where the platform supports it; startup keeps the ten newest reports and removes older ones without touching other files in the directory.

Recovery waits for Host shutdown before changing plugin activation. The native recovery action calls the shared app-boot recovery function under the profile transaction lock. It disables third-party bundles and renames the profile’s cordis.patch.yml to cordis.patch.yml.bak-<timestamp> (with an ordinal on collisions) without parsing it; the next startup creates an empty patch. Installed packages and earlier backups remain. The home-level patch is unchanged. The Electron console records the backup path (or its absence) and the unchanged home-level patch. Invalid profile data, rename failures, or write failures are reported as recovery-operation errors; completed changes remain, and Desktop does not restart as though recovery succeeded. Desktop has no profile-reset action or emergency HTML document.

Host environment

macOS and Linux GUI launches inherit only the session manager's environment, without variables exported by shell startup files. Before the first Host starts, Desktop runs the account's login shell (from the user database, not $SHELL) once as <shell> -ilc and reads its env -0 output between delimiters, so ~/.zprofile and ~/.zshrc (or the shell's equivalents) take effect for the Host, agent shells, terminals, and profile configuration. Reading runs in parallel with profile preparation. The probe has no terminal input and sets DISABLE_AUTO_UPDATE=true, ZSH_TMUX_AUTOSTARTED=true, and ZSH_TMUX_AUTOSTART=false to keep oh-my-zsh and tmux plugins from blocking. Shell values replace inherited values except PWD, OLDPWD, SHLVL, _, those probe variables, and launcher-owned DSH_* and ELECTRON_* names; Desktop resolves paths such as DSH_HOME before the read, so the Host keeps the same values. The read completes at the closing delimiter, so background processes started by startup files may keep running with their output discarded. A candidate that fails to start, exits nonzero, prints no delimited output, or exceeds DSH_DESKTOP_LOGIN_SHELL_TIMEOUT_MS (integer milliseconds from 1000 through 2147483647, default 10000; its process group is killed) is logged as a warning, and Desktop tries /bin/zsh, /bin/bash, and /bin/sh in turn; when all fail the Host uses the inherited environment. The dump command uses POSIX syntax, so an account shell such as csh, tcsh, or nushell fails and the Host receives the environment from the first system shell's startup files instead. Quitting Desktop during the read kills the running probe's process group. The result is read once per application process, so changes to shell startup files apply after quitting and reopening Desktop. Windows GUI launches already inherit the user and system environment variables from the registry, so Windows skips this step.

Develop

The development application menu offers Reload Page (Cmd+R on macOS, Ctrl+R elsewhere) and Restart App and Host. Restart waits for Host shutdown before relaunching Electron and starting a new Host; neither action rebuilds source files.

dev:desktop builds the current Host, client bundles, Web frontend, and Electron shell, projects the built CLI and private Desktop Host packages with their workspace dependencies into a disposable desktop npm project, and launches Electron without resolving dsh from npm:

pnpm run dev:desktop

Development Harness state defaults to apps/desktop/.desktop-build/development/home, the disposable npm project lives at apps/desktop/.desktop-build/development/project, and Electron browser data lives at apps/desktop/.desktop-build/development/electron-user-data. Sessions, settings, credentials, package links, and browser data therefore stay out of the user's normal Harness home. An explicit DSH_HOME replaces only the development Harness home. Renderer DevTools opens automatically; Main, Renderer, and dsh Host debugging listen on ports 9229, 9222, and 9230. DSH_DESKTOP_MAIN_INSPECT_PORT, DSH_DESKTOP_RENDERER_DEBUG_PORT, and DSH_DESKTOP_HOST_INSPECT_PORT replace those ports, while DSH_DESKTOP_OPEN_DEVTOOLS=0 keeps the detached Renderer tools closed.

After an explicit build, start:desktop reconstructs the disposable project and launches the existing artifacts without building again:

pnpm run start:desktop

The Web counterparts are pnpm run dev:web and pnpm run start:web, documented in the development guide. Workspace development runs the current CLI and private Desktop Host packages under Electron RunAsNode. Plugin management and recovery use $DSH_HOME/profiles/desktop, separate from the disposable workspace runtime. The Host uses runtime module resolution in both development and packaged builds without creating official-package fallback links; developer-installed packages, including links, retain native priority. Use an unpacked application to exercise Electron RunAsNode, bundled pnpm, bundled dsh resources, plugin installation and repair paths.

The native/renderer keyboard tests compile as part of the repository Client typecheck. Their Desktop imports are limited to Cordis-free input, persistence, IPC, browser-guest, and overlay modules.

Dependency installation does not run the locked Electron package's separate binary installer. Before running the Windows console-signal tests directly, prepare the locked Electron binary with pnpm --filter @deepseek-ai/dsh-desktop exec install-electron from the repository root. For downloads through a proxy, enable ELECTRON_GET_USE_PROXY=1 for that installer process. Windows coverage aggregates require successful provisioning before the instrumented unit inventory starts.

Startup onboarding

The API-key input starts empty and uses autocomplete="new-password" to ask Chromium not to autofill saved login passwords.

Repeated launches and dsh://open keep the workspace hidden until the startup credential check or a welcome action permits entry. Entering from Welcome places keyboard focus on the document without selecting a sidebar control; Tab navigation remains available.

Desktop checks configured model API-key references after the Host starts and before opening the workspace. With no configured key, the welcome window offers the API-key page. Save and continue writes through the existing credential service using the official DeepSeek provider's configured reference, then opens the workspace. Set up later opens the workspace without saving the draft or a completion setting; the next process launch checks credentials again. Back to sign in returns to the entry and clears the unsaved key and validation message. Buttons keep their labels and block competing actions while saving or opening the workspace. The Desktop preload marker suppresses the Web credential dialog, while retaining the Models settings page and the welcome notice.

The welcome window reads the shared locale.preference before it appears. An explicit English or Chinese choice wins; otherwise Desktop picks the first supported OS language and falls back to English. The main UI reads the same preference and OS language order through its isolated preload before mounting. Language changes in Settings update the shell’s current dictionary and menu; automatic selection writes no preference. The welcome window has no language selector.

The browser-login waiting page offers a copy-link action for the current pending authorization, a loading indicator, and cancellation. Clipboard failures leave the copy action available for retry. Copy feedback resets after two seconds; the copied state disables the link until it resets. Welcome text uses Montserrat Light with system fallbacks; large action buttons keep the system font, while text buttons use Montserrat Light. English introductory copy is 24px throughout. Chinese introductory copy is 24px with a 26px product name. Login action buttons are 240px wide with 14px labels. Authorization status headings use 20px Montserrat Regular text. The API-key page uses a 20px heading and a 14px back action, with 84px between the secondary button’s bottom edge and the window bottom.

Welcome window appearance

The welcome window follows system appearance with the design’s Platform light/dark colors and shows the 600 × 700 entry layout and the API-key form, with native window controls, a draggable title area, a local brand SVG, system sans-serif fallbacks, and locally bundled Montserrat Light for non-button text. The window uses macOS menu vibrancy or Windows acrylic with the onboarding window tint: 40% white in light mode and 50% rgb(24 25 28) in dark mode. The local React welcome entry bundles React and the shared StateDot loading indicator with its CSS; it uses the isolated preload without loading the main Web application. The entry, sign-in status and API-key pages share a fixed bottom action row; the back-to-sign-in link sits below it. Buttons share the platform motion timings, and Reduce Motion disables their transitions. The OS owns blur strength and outer corners. macOS Reduce Transparency suppresses translucency, and Increase Contrast forces that setting on. Save and continue writes to the development credential store; Set up later opens the real workspace without saving a key or completion flag. The generated project links the declared workspace dependency closure as well as pnpm’s hoisted packages, so unhoisted configured plugins remain resolvable. The window note owns the material and onboarding decisions.

Package

Release versions

Before each Desktop packaging run, first confirm the complete version string with the current user. Check the selected deployment, dsh base version, retained release records, and published objects, then propose the exact version for approval. Do not start packaging until the user confirms that version; the deployment setting alone does not authorize a version choice.

Record the current dsh version as the base. A production Desktop release uses that exact version, including any alpha, beta, or rc identifiers. A test release preserves the complete prerelease base and appends .YYYYMMDD.index; a stable base uses -test.YYYYMMDD.index instead.

dsh base Production Desktop Test Desktop example
0.1.6-alpha.1 0.1.6-alpha.1 0.1.6-alpha.1.20260916.1
0.1.6-beta.2 0.1.6-beta.2 0.1.6-beta.2.20260916.1
0.1.6-rc.3 0.1.6-rc.3 0.1.6-rc.3.20260916.1
0.1.6 0.1.6 0.1.6-test.20260916.1

Use the actual creation date in Asia/Shanghai. For each base and date, start the index at 1 and increment after checking retained release records and published objects; never reuse a published version. Test distribution does not publish the corresponding unsuffixed base.

Manual Windows CI uses a separate unpublished build identifier: auto appends the UTC date, workflow run number and attempt (.YYYYMMDD.run.attempt, with -test for a stable base). For example, 0.2.1-alpha.1.20261008.42.2. These numbers are not reserved release sequence numbers.

Pass the confirmed version to the packaging command as --build-version, which reaches the artifact names, the update feed, and the upload validation as one value. The manifests keep the product version, so a test build no longer rewrites the release family and leaves nothing to revert:

pnpm --dir apps/desktop run package:win:x64 --build-version 0.1.6-alpha.1.20260916.1

--build-version auto proposes the next index for today, reading the published objects in the destination bucket and falling back to this target's local output directory when no bucket is configured or the listing cannot finish within its deadline. Confirm the proposal it prints before an upload; a run script forwards -- on its own, and the packaging entry accepts it either way.

A production release publishes the product version and takes no --build-version. Uploading one tags the packaged commit as desktop-v<version> once the artifacts are public; a build from a modified checkout is not tagged, and a tagging failure prints the command to run by hand rather than failing a completed upload. Test and local builds are deliberately left untagged, and every artifact records dshBuildCommit and dshBuildDirty in its manifest so a build handed over directly remains traceable.

Version derivation does not change the fixed update channel or nightly.yml / nightly-mac.yml filenames. SemVer orders 0.1.6-alpha.1 < 0.1.6-alpha.1.20260916.1 < 0.1.6-alpha.2, and a stable base's test version precedes that stable release. Clients only accept a greater version: replacing a feed cannot move an installed higher version to a lower corrected version. Such clients need manual installation; keep automatic downgrade disabled. The archived version decision explains why the channel does not supply the prerelease identifier.

Except for local unsigned macOS builds, packaging, upload, and manual macOS signature verification read apps/desktop/.env.windows or .env.macos, selected by target platform. Copy the Windows template or macOS template and fill in the local settings; Git ignores both local files, and packaged artifacts exclude them. Release fields come only from the target file, without fallback to system or shell variables; PATH, proxies, and build-tool settings remain inherited. The published version is an argument rather than a release field, and upload reads it from the completion record the packaging run wrote. Files use UTF-8 with optional BOM; relative certificate, SignTool, Apple API key, and keychain paths resolve from apps/desktop, values are not shell-expanded, and passwords containing # or spaces need quotes. CI also creates the target file before invoking packaging.

Every package command checks the application ID and its mode-specific update and signing configuration before building or downloading, then probes the external tools the run will use: the archive reader, and on a Windows target the installer compiler. macOS checks the identity, Team ID, one complete notarization strategy, readable local CSC_LINK p12 file, explicit CSC_KEY_PASSWORD, and referenced API key and keychain files; Windows checks the public code-signing certificate, SignTool file, container name, and PIN format. Windows preparation-only builds and explicit unsigned builds on either platform do not require signing credentials. Configuration checks do not authenticate the PIN, log in to the token, unlock a keychain, or contact Apple; actual signing and notarization perform those checks. --build-version auto does contact the destination bucket, including under --check. Run the same checks separately:

pnpm --dir apps/desktop run check:package

prepare:desktop is not a prerequisite:

pnpm run package:desktop

Release automation uses fixed target commands so runtime preparation, dsh preparation, and electron-builder receive the same platform and architecture:

pnpm run package:desktop:mac:arm64
pnpm run package:desktop:mac:x64
pnpm run package:desktop:win:x64

The macOS arm64 command requires Apple Silicon. The macOS x64 command runs on Intel macOS or Apple Silicon with Rosetta. The Windows x64 command requires Windows x64. Linux is not a supported Desktop release target.

Each target owns its packed package inputs, prepared runtime, package set, dsh tree, pnpm preparation state, unpacked application, update metadata, and final artifacts under apps/desktop/.desktop-build/targets/<target>/. The Electron archive cache remains shared under .desktop-build/downloads because every archive name includes its version, platform, and architecture and is verified before extraction. A target build never consumes another target's mutable preparation state.

Local macOS DMG without release signing

After pnpm install, run one command from the repository root. No .env.macos, Apple account, certificate, or update service configuration is required:

pnpm run package:desktop:mac:arm64:unsigned
pnpm run package:desktop:mac:x64:unsigned

Add --check to validate local configuration and tools without building, or --dir to stop at the unpacked application. The DMG is apps/desktop/.desktop-build/targets/<target>/unsigned-artifacts/deepseek-harness-<version>-mac-<arch>-unsigned.dmg. After all checks pass, the command prints DMG: followed by its absolute path. Open it and copy the application to a local test directory.

These commands ignore .env.macos and inherited release settings, use com.deepseek.harness as the app ID, and omit automatic-update and mandatory-update configuration. Build tools, standard network proxies, and DSH_DESKTOP_NPM_REGISTRY remain available from the shell. They skip Developer ID signing, Apple notarization, temporary signing keychains, and notarization proxy changes. Electron receives a local ad-hoc signature without certificates so the modified executable can run; the DMG is unsigned. Both preparation and the assembled application still run runtime integrity and smoke checks. Local builds omit update feeds, ZIPs, and release completion records, and the upload commands cannot publish them. macOS may require approval in System Settings → Privacy & Security to open an unnotarized application.

Local Official bundles from a loopback registry

The test DMG excludes the provider runtimes the Official on-demand catalog advertises, and a release publishes matching bundle versions to npm. To install locally built bundles before that release, start a loopback registry that packs and serves the catalog's complete local dependency closure:

pnpm run dev:bundle-registry

The command validates the catalog and the workspace inventory, runs pnpm run build:official, packs the closure into a new dist/test-bundles/<run-id>/ directory, writes bundle-registry.json last as the completion marker, and only then prints the artifact path, the registry URL, and the exact name@version specs. It serves 127.0.0.1 on an allocated port under a fresh namespace until Ctrl+C or SIGTERM, and stopping it leaves both the artifacts and the installed packages on disk. It needs no .env.macos, Apple credentials, or npm credentials, and it does not change pnpm run package:desktop:unsigned.

In the packaged application, open Plugins → Add plugin, paste one printed spec, choose Custom address, paste the printed registry URL, and install. A custom registry is asked alone, so this process must answer every dependency: repository-owned packages come from the run's own archives, and third-party packages keep npm's real metadata and bytes through a fixed redirect to registry.npmjs.org, which still requires network access. The installed entry stays Official because its package name is in the catalog. The registry choice is remembered in this browser; choose an offered registry again to switch back.

A run's artifacts are immutable, so a rebuild serves a new namespace and a new URL, and the Official Update control does not appear while the installed and running versions match. The profile records the previous run's tarball URL in pnpm-lock.yaml, and pnpm verifies recorded entries, so installing or reinstalling that bundle against a new URL is refused with ERR_PNPM_TARBALL_URL_MISMATCH and can leave the earlier files in place. To test changed code at the same version, clear the stale resolution first — pnpm clean --lockfile in $DSH_HOME/profiles/<profile>, or install into a fresh profile — then install again from the new URL. bundle-registry.json records each archive's version, filename, size, and SHA-512 integrity together with the packing checkout's commit and dirty flag.

The served time is the real packing time, so pnpm 11's default 24-hour minimumReleaseAge records the exemption it granted in the profile's pnpm-workspace.yaml (minimumReleaseAgeExclude); an explicitly stricter policy there fails the installation with pnpm's own message instead of being overridden. A @scope:registry route in ~/.npmrc or the profile's .npmrc overrides --registry for that scope, so it must not route the served scope.

Runtime file selection

Desktop packs workspace packages locally and installs external dependencies through the target's bundled Node and pnpm. Desktop's file policy then filters the immutable resources/app.asar/dsh/node_modules copy before signing and integrity sealing. It omits TypeScript declarations, recognized JavaScript/CSS/TypeScript source maps, TypeScript build caches, Domino's test directory, selected native compiler outputs, and node-pty prebuilds for other platforms. Non-Windows targets also omit node-pty/third_party/conpty; Windows targets retain that directory. It preserves runtime JavaScript, target native modules and their DLL/EXE helpers, WASM, unknown assets, licenses, and notices. Dependency manifests pass through electron-builder's metadata cleanup before integrity sealing, so archiving preserves their recorded bytes. The policy does not alter npm tarballs, the bundled package manager, or user-installed plugin files.

The Electron shell file list excludes recognized JavaScript/CSS/TypeScript source maps and TypeScript build caches from the separate resources/app.asar/node_modules dependency collection. These exclusions preserve runtime code, native modules, WASM, data files (including ordinary .map assets), licenses, and notices; shell archive tests check retained bytes and exclusions.

The Office conversion provider carries the target’s declared native engine, or WASM when the kit declares no matching native target. Preparation rejects a missing target engine before packaging. The complete Office dependencies, including CLI, JavaScript libraries, and the selected engine’s executables, data, licenses, and notices, are unpacked under resources/app.asar.unpacked/dsh/node_modules/. The Desktop Host resolves engine manifests to these physical directories and supplies the Office command provider with absolute standalone Node and unpacked CLI paths. Node lives in resources/runtime/primary-runtime/dependencies/node/bin/; the CLI is the unpacked @deepseek-ai/libreoffice-kit/lib/cli.js. On macOS, the native helper receives the JIT entitlement required by LibreOffice’s UNO bridge.

The packaged application runs compiled JavaScript and pre-generated Typert metadata; it does not compile TypeScript plugins. Source-level debugger navigation and editor declarations remain available in development packages. Copy-policy tests cover exclusions and retained assets; The payload smoke runs under Electron RunAsNode before the Host smoke and final inventory verification. The payload smoke resolves the search tool’s ripgrep executable and verifies text search and file enumeration. Signed Windows builds run these checks after dependency signing; other builds run them during prepare:dsh. The Host smoke creates DOCX, XLSX, and PPTX inputs with bundled Python, converts each through the real Office provider, and checks PDF output. Every assembled application, including directory and unsigned builds, repeats the payload and Host checks against ASAR. Archive integrity checks compare the complete archived descriptor with preparation, file bytes and membership in both archive and unpacked trees, recorded executable flags for archived files, and physical permissions for unpacked files. The Host smoke also runs dsoffice capabilities and DOCX conversion through the registered shell environment with an empty caller PATH. Conversion failures stop packaging before a release record is written; macOS DMG/ZIP builds run these checks before notarization.

Windows release qualification also runs directory and replacement checks manually after the Desktop build. Set $Makensis, $SevenZip, and $PluginDir to the pinned builder’s NSIS compiler, 7-Zip executable, and x86-unicode NSIS plugin directory; pass the prepared window-frame.dll as -FrameLibrary to exercise the native extraction path and its failure report as well. From the repository root, run the command below. It verifies directory replacement and rollback, and both locked-file replacement modes; it is not part of the unit-test lane.

pwsh -NoProfile -File apps/desktop/scripts/smoke-windows.ps1 -Makensis $Makensis -SevenZip $SevenZip -PluginDir $PluginDir -FrameLibrary apps/desktop/.desktop-build/targets/win-x64/installer-ui/window-frame.dll

The Windows installer checks for a running application at startup and after destination selection, before extracting the new version beside the installation directory. It checks again before replacing directories through same-volume renames. A running application blocks installation; update launches allow up to ten seconds for it to exit. Same-path upgrades preserve the old directory until promotion succeeds; extraction failure leaves it intact, and promotion failure attempts to restore it. The installer removes the old backup before launch. Forced termination or power loss can leave .new-* or .old-* directories; installation-location and scope migrations retain electron-builder's old-uninstaller flow.

When extraction fails, the installer writes a report with the 7-Zip result and its complete error output to the updater cache directory as %LOCALAPPDATA%\<package-derived name>-updater\installer-logs\extract-failure-<timestamp>.log (currently @deepseek-aidsh-desktop-updater) and shows the first error line with a Copy error details button; silent installs write only the report. Unsigned Windows builds (DSH_DESKTOP_UNSIGNED=1) name their installer deepseek-harness-<version>-win-x64-unsigned.exe so they cannot be mistaken for release artifacts.

Upload updates

Fixed installer downloads use pnpm run upload:latest:mac:arm64, pnpm run upload:latest:mac:x64, or pnpm run upload:latest:win:x64 from the repository root. They upload the completed target's DMG or EXE to desktop/dsh-latest-macos-arm64.dmg, desktop/dsh-latest-macos-x64.dmg, or desktop/dsh-latest-windows-x64.exe. Production URLs start with https://download.deepseek.com/; test URLs use DOWNLOAD_TEST_ORIGIN. Both deployments reuse the target dotenv's deployment, bucket, and credentials, all normal release validation, and the upload audit below. Each command replaces one fixed object without publishing feeds, blockmaps, or release tags. Test fixed downloads are shared across release IDs in the test bucket; the packaged update URL still requires its original release ID. Publication is explicit, including prerelease versions. Deployment infrastructure must configure revalidation or purge cached fixed downloads after replacement. Local loopback tests cover the upload; release operators own real COS and CDN acceptance.

Test and production uploads through upload:* retain a fresh .desktop-build/upload-records/<environment>-<target>-* directory after release preflight. plan.json records destination, version, every file's size/SHA-512, and published YAML bytes; flushed events.jsonl records PUT intent and available response status/request ID; result.json records completion or the last failed stage. A missing final result means interruption or unavailable storage, not success. No credential values, authorization headers, or raw SDK errors are recorded. Audit-write failures stop later PUTs. Every object is one streamed Tencent COS PUT with an explicit length and Content-MD5; the COS SDK repeats a request only when its body is not a stream, and this uploader does not retry either. Retain partial records and inspect remote state before another operation: a timeout or failed receipt write does not prove that the object was not stored. These records are local, not tamper-proof or automatically backed up; archive each release's records with its build evidence in controlled storage. Public CDN readback remains separate release qualification, explicitly marked not-performed in the upload result.

Windows operators can keep a CLIXML object with DPAPI-encrypted SecretId and SecretKey SecureString fields outside the repository. The credential launcher requires an explicit -CredentialFile and -Environment production or test; without -Upload, it only verifies decryption and injection into a local Node child, with no network request. It requires Node on PATH and the Windows user and machine that encrypted the file. Plaintext, empty, and whitespace-only fields fail. The parent environment is unchanged; the child receives only the selected COS pair after unrelated secrets and Node preload options are removed. Raw child stderr is suppressed and credential values in stdout are redacted. This check does not prove COS authorization. An explicit upload additionally requires -Upload -Target <target> -Bucket <bucket> and the normal completed-release prerequisites below; actual cloud upload remains release-operator qualification. Add -Latest to that upload invocation to replace the fixed installer without publishing update feeds or a release tag. This launcher supports permanent keys, not STS credentials. An explicit upload requires the selected deployment and bucket to match the target dotenv and completed package record before network writes; it uses the DPAPI credential pair even if the dotenv contains other COS keys.

DSH_DESKTOP_AUTO_UPDATE_ENV selects test or production for both the URL embedded during packaging and the later COS upload; an absent value selects test. Test packaging requires its HTTPS origin in DOWNLOAD_TEST_ORIGIN; production uses https://download.deepseek.com. Upload requires the selected bucket in DOWNLOAD_TEST_COS_BUCKET or DOWNLOAD_PROD_COS_BUCKET. Production feeds use dsh-desk/feeds/<target>/ and binaries use dsh-desk/bin/<target>/. Test releases require DOWNLOAD_TEST_RELEASE_ID: 32 lowercase hexadecimal characters inserted as dsh-desk/<release-id>/feeds/<target>/ and dsh-desk/<release-id>/bin/<target>/. YAML references, stable-channel aliases, and blockmaps stay inside that release directory. Targets are mac-arm64, mac-x64, and win-x64.

The update destination and upload credentials follow the selected deployment:

Environment Public origin COS bucket COS credentials
test or unset DOWNLOAD_TEST_ORIGIN DOWNLOAD_TEST_COS_BUCKET DOWNLOAD_TEST_COS_SECRET_ID, DOWNLOAD_TEST_COS_SECRET_KEY
production https://download.deepseek.com DOWNLOAD_PROD_COS_BUCKET DOWNLOAD_PROD_COS_SECRET_ID, DOWNLOAD_PROD_COS_SECRET_KEY

Generate a fresh ID for each test release batch with the command below and copy its output into DOWNLOAD_TEST_RELEASE_ID in .env.macos or .env.windows. These Git-ignored platform files own the value; shell variables do not override it, and dotenv values are not shell-expanded. Keep the ID through packaging, upload, and retries; upload rejects a completion record with a different update URL. To test an upgrade between versions, reuse the installed client’s ID for the later version. Production does not use this field.

node --input-type=module -e "import { randomBytes } from 'node:crypto'; console.log(randomBytes(16).toString('hex'))"

Distribute each test batch through its complete download links. Installed test clients retain their batch feed and do not discover a new ID automatically. Format validation cannot establish randomness; use the generator output. Random paths reduce guessing, not access by link holders; withdrawing a batch requires deleting its COS objects and purging its CDN directory.

Configure the update origin and selected COS bucket, SecretId, and SecretKey in the target dotenv file, then package and upload the same target:

pnpm run package:desktop:mac:arm64
pnpm run upload:mac:arm64

The target dotenv file for internal-test packaging explicitly sets DSH_DESKTOP_AUTO_UPDATE_ENV=test and DOWNLOAD_TEST_ORIGIN=https://download-test.deepseek.com; uploads use DOWNLOAD_TEST_COS_BUCKET=bj-toc-download-test-1320056602 and separate test credentials. Both test and production use the fixed Nightly channel; deployment selection does not enable channel switching.

Early internal-test packages use the test deployment. Select production explicitly only for a production release; changing upload credentials does not retarget an existing package. Packaging requires no COS credentials, disables electron-builder publishing, strips COS credentials from child processes, and records completion only after signing and notarization succeed. Upload validates this record, deployment, target, shared version, filenames, sizes, and SHA-512 before reading credentials. Packages and blockmaps upload before YAML; historical objects are retained. Every release publishes nightly.yml or nightly-mac.yml; stable releases also publish latest.yml or latest-mac.yml pointing at the same artifacts. Published YAML uses absolute binary URLs. The uploader leaves Cache-Control unset, including the empty header the COS SDK would otherwise add: deployment infrastructure owns cache policy, with uncached feeds and separately configurable binary caching. Serialize publication per target and verify public artifacts and feed contents before release qualification.

The signed macOS configuration uses the required release environment instead of accepting whichever certificate appears first in a keychain. It rejects empty values, a malformed Team ID, a signing identity that includes electron-builder's unsupported Developer ID Application: prefix, and incomplete notarization credentials. Signed macOS packaging requires the configured identity and its private key. Runtime preparation applies that identity, a secure timestamp, and hardened runtime to every embedded Mach-O file; after signing the application, a deep strict check rejects any other leaf authority or Team ID before artifact creation. The signed fixed-target macOS installer commands create separate copies of the signed application and run two artifact lanes concurrently. One lane notarizes and staples the App before generating the ZIP and its update metadata. The other encloses its signed App copy in a signed DMG, then notarizes, staples, and verifies the DMG; its inner App has no individually stapled ticket. Both lanes must finish successfully before their artifacts reach the final directory and the release completion record is written. Signed directory-only commands also require notarization credentials and wait for Apple notarization and App stapling. The parallel notarization decision owns copy isolation and container ticket semantics. CSC_LINK must name a local p12 containing the Developer ID Application certificate and private key; URLs and Base64 inputs are not supported. CSC_KEY_PASSWORD is its export password, not an Apple account or login password; an explicitly empty value supports an unencrypted p12. Before building, packaging creates and unlocks a private temporary keychain, imports the p12, authorizes signing, and signs a small probe. Runtime and App signing use this keychain explicitly; existing login keychains require no setup or manual unlocking. Child processes receive its path without the p12 password. The keychain is deleted after success or ordinary failure; CI must clean temporary credentials after forced termination. CI writes the certificate and .env.macos from its secret store, restricts file access, and deletes both after the job. Ambient CSC_NAME and certificate discovery order do not select the release owner. Notary credentials may instead use electron-builder's complete Apple ID or keychain-profile strategy. The two macOS identity variables are also required when repeating the application check manually with pnpm --dir apps/desktop run verify:mac-signature -- <path-to-app>.

macOS signing visits real files without following Framework symlink aliases. PAK resources retain all shipped languages and are sealed by the enclosing Framework or application signature instead of receiving individual signatures. The release policy owns the dependency patch and verification requirements.

macOS runtime preparation caches verified single-architecture Mach-O signatures in .desktop-build/targets/<target>/signature-cache. Keys include input bytes and permissions, the signing probe’s actual leaf certificate, identifier, entitlement bytes, macOS version, and signing tools and policy. Git commits do not determine reuse: uncommitted byte changes invalidate the affected file. Every hit verifies cached bytes, the strict signature, certificate, identifier, entitlements, secure timestamp and hardened runtime before replacing an unchanged input. Universal binaries are signed each time. Runtime integrity and smoke checks, App signing, and Apple notarization still run. The cache requires trusted local build storage. Cache corruption or unsafe links fail the build; stop packaging before deleting the affected cache directory and retrying. Successful signing stages prune complete entries to a one-GiB payload budget; interrupted temporary entries require manual removal. Concurrent eviction can fail a reader safely. Logs report hit, miss and uncached counts.

Mac package commands print DESKTOP_PACKAGING_RECORD with a unique directory under apps/desktop/.desktop-build/packaging-runs/. After reading local configuration, each run retains run.json (published version, product version, Git commit, dirty-worktree flag, target and Node version), events.jsonl (timestamped phases, durations, parallel output attribution and proxy restoration), redacted child stdout.log / stderr.log, and result.json (overall outcome, stage results and artifact directory). Events also record pack concurrency and whether each proxy is configured. Logs survive failures and later builds; no automatic deletion runs. Missing result.json means completion is unconfirmed. Known credential values are redacted; environments and notarytool authentication arguments are not recorded. Runtime preparation records package staging, installation, materialization, signing, manifests, smoke checks, descriptor verification and cleanup separately. Nested phase durations overlap and must not be added together.

App and DMG notarization each record a submission ID and separate notarytool:upload:* and notarytool:wait:* durations. Upload uses submit --no-wait and includes authentication, local validation, transfer and server acceptance; it is not pure transfer time. Waiting starts after that command returns and includes polling and remaining Apple processing; Apple can start processing before upload submission returns. Apple status and diagnostic logs are retained, including rejection; signature checks, acceptance checks and stapling remain owned by @electron/notarize. Directory-only package commands use this same measured App notarization path. Individual subprocess output remains live in the terminal, while phase failures and nested errors are retained in the journal. Both platforms record parent-process packaging failures and print redacted diagnostics, including proxy recovery instructions, to the terminal. The configuration-only check:package command creates no run log.

Mac packaging reads three tuning fields from .env.macos:

Setting Default Scope
DSH_DESKTOP_MACOS_PACK_CONCURRENCY 4 Workers for first-party and vendor workspace tarballs; requires a positive integer.
DSH_DESKTOP_MACOS_DOWNLOAD_PROXY Empty HTTP/HTTPS proxy origin for Electron, runtime assets, pnpm installation and builder downloads.
DSH_DESKTOP_MACOS_NOTARIZATION_PROXY Empty HTTP proxy origin for Apple tools through temporary system proxy settings.

The proxy fields are independent and reject URL credentials, paths, queries and fragments. Empty values preserve inherited networking. An explicit download proxy replaces child-process proxy variables and bypasses local hosts only; it does not change system settings. Windows and standalone release:pack retain their existing concurrency defaults. Company proxy addresses belong in the ignored local file; see internal documentation for addresses.

Apple tooling uses the active macOS network service's HTTP/HTTPS proxies. Configured notarization routing checks proxy reachability, saves that service's settings, enables the proxy around both artifact lanes, and restores the saved settings after both lanes settle. An originally disabled proxy with an empty server and port zero is restored by disabling it; the temporary server and port may remain stored but inactive. Directory-only builds apply it to App notarization after the signed directory build. This temporarily affects other applications and requires permission to change system proxies; PAC, auto-discovery, SOCKS and authenticated proxy configurations must be disabled first. Packaging and recovery acquire the same per-user POSIX file lock before reading recovery data or changing proxies; process exit releases ownership, while the lock file remains in place. The lock loads @deepseek-ai/node-addon-system/flock at first use rather than at script start, so check:package and the packaging entry load on a checkout that has not built native/system; when the host addon binary or the entry's JavaScript is missing at lock time, the loader runs pnpm run build:native-system and pnpm --dir native/system run build:ts before locking, so recovery works on such a checkout as well. This prevents overlapping proxy transactions across checkouts; other users and network-setting tools must not change these settings concurrently. SIGINT/SIGTERM wait for active work before restoration. After forced termination or a restoration error, stop any remaining notarization processes and run pnpm --dir apps/desktop run restore:mac-proxy; the saved record remains until restoration succeeds. Configuration checks validate URL syntax without changing system settings or contacting the proxy.

Unsigned Windows test installer

On Windows x64, use the complete unsigned packaging command for local installation testing:

pnpm run package:desktop:win:x64:unsigned

The command reads .env.windows and requires DSH_DESKTOP_APP_ID and the mandatory-update policy settings, including a real HTTPS service origin. The local run and CI use Node 24; the remaining tools are the root packageManager version of pnpm, Git, PowerShell, tar, Python, Visual C++ Build Tools and a Windows SDK. Set PYTHON when Python is absent from PATH. It writes the installer to .desktop-build/targets/win-x64/unsigned-artifacts/, omits ordinary automatic-update configuration, strips signing credentials, and creates no release completion record. EV and upload credentials are unnecessary. Signed packaging and upload retain their release requirements.

Run pnpm install --frozen-lockfile, then pnpm run package:desktop:win:x64:unsigned --check to validate configuration and tooling before the full command above. Packaging performs its own complete build and isolated runtime smoke without an online model. Stage logs remain under .desktop-build/packaging-runs/; a successful build alone does not qualify the installer. Dependency installation and runtime preparation need access to npm, GitHub release assets and nodejs.org; use the existing registry and proxy configuration when the network requires it. Caches and tool locations belong to the build machine, not tracked personal paths.

The manual Windows workflow selects a self-hosted Windows x64 runner with the dedicated dsh-win-package-trial label and calls the same unsigned command. Provision PowerShell 7 and the tools listed above before starting the runner. It requires the policy deployment and HTTPS origin; test deployments also require comma-separated login origins. These public inputs generate a temporary .env.windows without signing or upload credentials. build_version defaults to auto, using the CI identifier described in Release versions. An explicit version uses the existing product-version validator. The resolved version appears in the run summary before packaging; manifests remain unchanged.

After this workflow reaches the default branch, select it in Actions, choose the source branch, fill the inputs and click Run workflow. It has no push or PR trigger and leaves existing automatic CI unchanged. Packaging logs and successful EXE artifacts are retained for 14 days; installation, signing and publication are separate operations. Run it once from the default branch and inspect the retained logs and installer before relying on Actions packaging. Checkout cleans the runner-owned source directory, including ignored files; never point it at a development checkout. Runs share one concurrency group without cancelling an active build; only one run can remain pending, and a newer dispatch replaces that pending run. Diagnostics are uploaded only after a successful clean checkout, and generated settings are removed even when preparation fails after writing them.

Windows installer interface

The Windows installer uses native NSIS pages with light and dark palettes, system shadows, an editable installation directory, and a finish page whose launch checkbox is selected by default. Installation is restricted to the current user. Clicking Install or pressing Enter validates the current path; drive roots remain invalid after normalization, including repeated trailing separators. New destinations must be empty, and nonempty destinations must be registered installations. Running executables at the affected installation path produce a native prompt and remain running; same-named applications in other directories do not block installation. Silent updates wait up to ten seconds for the affected application to exit, then stop with exit code 2 if it is still running.

The theme follows Windows at startup; /THEME=light, /THEME=dark, and /THEME=auto select a palette explicitly. The window appears after its branded controls are ready. When the welcome page first appears, the installer moves above ordinary windows once and flashes its taskbar button if another window has focus; it does not remain always on top. Progress reads the pinned 7-Zip extractor’s percentage; directory promotion, registration, and cleanup retain bounded estimates. The weighted percentage does not predict remaining time. After NSIS reports success, the bar fills over 600 ms and displays 100% briefly before the finish page appears; the transition targets 750 ms. The finish page preserves the window position. Finish dismisses the installer before launching the installed executable; a launch failure restores the page for retry. Directory replacement and failure recovery follow the installation flow described above. First-launch profile preparation remains a separate Desktop operation.

Windows packaging compiles an x86 Win32/GDI+ helper with Visual C++ Build Tools and a Windows SDK; signed builds sign this helper through the configured Windows signer. The preparation hook leaves production dependency collection to electron-builder on every platform.

Run pnpm --dir apps/desktop run test:installer from the repository root on an interactive Windows x64 desktop to build and exercise a small native test payload through the production installer configuration. Each run uses a unique product identity and sequentially exercises English-only and Chinese-only installer variants, selecting test labels from the displayed welcome button. Both variants install into private directories and uninstall after testing; screenshots and results remain under .desktop-build/installer-tests/. The checks include upgrades to registered paths with trailing separators and rejection of drive roots. The optional --signed flag uses the Windows EV configuration below to sign test executables and the helper before embedding them; it does not enable an update feed.

The Windows uninstaller removes the Electron user-data directory (browser storage and caches under %APPDATA%, nested under the package scope), the %APPDATA% product directory, and the updater download cache under %LOCALAPPDATA% together with the application, then removes ordinary empty scope directories below %APPDATA%. The Harness home (~/.dsh or DSH_HOME: sessions, settings, credentials, plugins) is never touched, and a DSH_HOME published as a Windows environment variable additionally protects any target overlapping it. Silent uninstalls remove the same data; an uninstaller started with --updated or /KEEP_APP_DATA, as electron-builder does for in-place updates and for replacing an older installation from another directory, retains it. Removal runs through the native helper, which refuses protected Windows folders and paths overlapping the installation or the home, requires a fixed local drive, leaves a linked root or ancestor in place, unlinks reparse points without entering their targets, clears read-only attributes, and keeps deleting siblings after a locked file; residue never stops uninstallation and is not reported. Installation records InstallLocation on the Windows uninstall entry as standard inventory metadata; on Windows 11, Uninstall in the Start menu context menu opens the installed-apps list for every Win32 application, and only MSIX packages uninstall directly from there. The uninstaller declares DPI awareness and uses Microsoft YaHei UI for Chinese. This behavior is Windows-only.

Use node apps/desktop/scripts/test-windows-installer.mjs --uninstall-only --compile-only to compile isolated English and Chinese fixtures with a per-run scoped package name. Omit --compile-only to run the native remover regression and the interactive, silent, --updated, /KEEP_APP_DATA and DSH_HOME-inside-Electron-data checks against seeded data. Compilation does not establish installed-uninstall behavior.

Windows EV signing

Runtime signing shares complete signed files across the current Windows account's worktrees. DSH_DESKTOP_WINDOWS_SIGNATURE_CACHE_DIR in .env.windows selects an absolute fixed-drive directory; the default is %USERPROFILE%\.dsh-desktop-signing\signature-cache\v1. Cache directories must belong to the current account and exclude other ordinary accounts from their access permissions; linked paths are rejected. Entries identify the original bytes, public certificate and signing toolchain. Every restoration checks the digest, Windows trust, timestamp and certificate before replacing the unsigned file; invalid entries stop packaging without hardware fallback. Cache age alone does not trigger signing. The cache trusts programs running as the same account and does not defend against administrators. The runtime signature cache decision owns qualification and design limits.

Preflight, primary-runtime signing, application-runtime signing and artifact creation each hold the account's signing-stage lock until their supervised child processes settle. Other builds wait before entering those stages; compilation and preparation do not hold the lock. Runtime signing workers own their locks, so terminating only the outer packaging process does not unlock a surviving cache user. Migration and maintenance acquire the same lock; older or external signing commands do not participate and must be scheduled separately. Stage waiting is outside the preflight deadline. Closing the stage handle releases ordinary contention without removing the separate hardware-attempt interlock; a retained hardware failure still requires operator recovery.

Cache-hit copying, hashing and per-file trust checks run with DSH_DESKTOP_WINDOWS_SIGNATURE_CACHE_CONCURRENCY workers from .env.windows (default 4, integer 1–8). All restores and their post-verification finish before cache misses enter serial hardware signing. A restore or verification failure stops new dispatch and drains active work before releasing the stage lock; hardware signing, runtime smoke checks and final integrity validation remain required.

Each runtime stage writes SIGNATURE_CACHE_SUMMARY to stdout and its packaging journal, including the actual directory, policy identity, hit/miss counts, newly published and retained entries, signing requests, avoided signing requests and validation failures. Timings distinguish signing, trust verification of restored and newly signed files, and restoration; restoration includes its trust check and staging cleanup. These counters exclude preflight, final artifact signing and the outer runtime signature checks. Stage lock events record waiting separately. Concurrent timings sum per-file work rather than stage wall time.

Use pnpm --dir apps/desktop run cache:windows-signatures --usage to inspect structurally complete-entry bytes and counts, or --from <absolute-old-cache> to import an explicitly selected cache owned by the same account. Migration leaves the source unchanged, skips staging names, rejects corrupt entries and retains valid existing entries even when their timestamp bytes differ. --clear explicitly removes complete entries under the stage lock; no automatic capacity eviction runs. --directory <absolute-cache> selects a maintenance destination without loading release credentials. Incomplete staging entries remain untouched and are counted separately; inspect them only after all builds have stopped. These commands never clear hardware failure evidence. Default storage is outside AppData so MSIX launcher virtualization does not split the account's cache; redirected overrides fail explicitly.

Signed Windows builds run a supervised signing preflight before compilation or dependency preparation. Static configuration, certificate validity, audit storage, compiler availability and any retained signing interlock are checked without token access. The local .NET Framework C# compiler creates a small private probe; the production signer signs it once, and verification requires the configured certificate and a timestamp before building continues. The probe is never executed. The overall 60-second preflight deadline includes timestamp attempts; deadline expiry, hardware signing errors and verification failures stop the run without another hardware attempt. Success proves the current signing path works, not that the PIN was independently authenticated: SafeNet may reuse login state. Do not log out or repeat authentication to test the PIN. --check, preparation-only and --unsigned modes do not run this hardware preflight; unsigned artifacts remain ineligible for release upload. Automated regression tests use a fake signer; release operators qualify real hardware separately.

Windows NSIS uploads require the generated, nonempty .exe.blockmap beside the installer. The blockmap is uploaded before channel YAML; NSIS installer metadata does not require the embedded blockMapSize used by the separate web-installer format. File-plan tests use the pinned builder's blockmap generator, not a hand-authored embedded-map field.

Signed Windows configuration derives the updater's publisherName from the same public certificate's CN, O, and C attributes. Each must be present, nonempty, and single-valued. These identity attributes allow certificate renewal without pinning a leaf thumbprint. The installed application's app-update.yml carries the expected publisher; the downloaded feed does not choose it. Unsigned test builds omit updater configuration. See the signature qualification record for real-file verification and its limits.

For this project's SafeNet token, SignTool Error: No private key is available. indicates an incorrect PIN. Stop all signing attempts immediately and wait for the user to correct the PIN before continuing. Five incorrect PIN attempts lock the token. Do not retry packaging or signing probes after this error. The signer serializes token operations and rejects all queued tasks after the first failure.

Windows package commands print DESKTOP_PACKAGING_RECORD with a unique directory under .desktop-build/packaging-runs/. Each run retains run.json, timestamped events.jsonl, redacted stdout.log and stderr.log, and result.json. A signing failure also writes fatal.json and notifies the parent through stderr; the supervisor immediately requests termination of its stage process tree and waits for exit. Failed stages cannot start subsequent stages or create a release completion record. Journal failures also stop the run. Termination errors remain failures and require operator inspection; a missing final record means completion was not established.

Hardware signing requires a supervised run. Before invoking the command interpreter, the signer atomically acquires %USERPROFILE%/.dsh-desktop-signing/attempt.json and records the attempt. Only successful signing followed by verification of the configured primary signature releases that file; timestamps are completed afterward without hardware access. Failure, interruption, an existing interlock, or unavailable audit storage prevents further hardware access, including from another signer instance, process, or checkout under the same Windows account. There is no timed recovery or automatic retry. An administrator must inspect the retained evidence and token state before explicitly authorizing interlock recovery; logging into the token or replacing a PIN file does not clear it. The records distinguish signing intent, command-interpreter PID, and completion; they do not measure internal CSP/token authentication attempts. Command arguments, PINs, and credential environments are not recorded. Independent Windows accounts and unrelated signing programs are outside this interlock.

Windows packaging fixes the 7-Zip filter to BCJ for compatibility with the bundled NSIS decoder. This preserves ARM64 binaries carried by dependencies in x64 installers; automatic ARM64 filtering produces entries that this decoder cannot extract.

NSIS removes its temporary extraction tree during installation, before the completion page or an automatic launch. The installed production packages remain ordinary files; startup does not extract them again. Installation still writes the complete application tree.

Fill in .env.windows with DSH_DESKTOP_WINDOWS_CER_FILE (public EV leaf certificate), DSH_DESKTOP_WINDOWS_SIGNTOOL (SafeNet-compatible SignTool), DSH_DESKTOP_WINDOWS_KEY_CONTAINER (matching private-key container), and DSH_DESKTOP_WINDOWS_TOKEN_PIN (Token Password). The private key stays on the USB token; keep the certificate and local credential file out of Git.

pnpm run package:desktop:win:x64

Insert and unlock the token before packaging. The electron-builder hook passes each artifact to the CRLF scripts/windows-sign.cmd, which invokes the configured SignTool once with /f, SafeNet /kc "[{{PIN}}]=container", /csp "eToken Base Cryptographic Provider", a SHA-256 file digest, without a timestamp. The hook then completes a DigiCert SHA-256 RFC 3161 timestamp on isolated copies without signing credentials. The hook never substitutes electron-builder's bundled SignTool and never retries a failed signing request. Windows release packaging fails instead of emitting unsigned artifacts when the SignTool, certificate, container, PIN, token, or signature is unavailable.

Timestamp completion retries only a normally exited timestamp command returning failure or warning, at most three times with one- and two-second delays. Each attempt starts from the same verified primary signature. Launch errors, uncertain termination and verification failures stop immediately. SignTool operates on short private paths; publication copies verified bytes to the target volume before atomic replacement. Final Windows trust, certificate, timestamp and normalized full-file equality are required. Exhaustion stops packaging and preserves evidence without another hardware call. See the signature completion decision.

The PIN cannot contain ], a quote, or a line break because those characters delimit the SafeNet /kc value or its CMD argument. The CMD disables delayed expansion so a PIN containing ! reaches SafeNet unchanged. Packaging withholds every DSH_DESKTOP_WINDOWS_* field from build and runtime-preparation subprocesses, gives signing preflight, the dedicated runtime signing stages and electron-builder only the four configured inputs, gives the signing CMD only the validated signing fields in an otherwise scrubbed environment, clears those fields before SignTool starts, and redacts SignTool diagnostics. SafeNet still requires the PIN in the SignTool process command line. The local .env.windows stores the PIN in plaintext and needs restricted file access; CI uses a temporary file and deletes it after the job. Do not commit or share its contents or print credentials in logs. Configuration checks consume no token PIN attempts; signing still stops the batch on its first failure.

Create a runnable application directory instead of an installer by using the matching :dir command, such as:

pnpm run package:desktop:dir
pnpm run package:desktop:mac:arm64:dir

To inspect or troubleshoot the prepared host-target resources without invoking electron-builder, stop the same pipeline after preparation:

pnpm run prepare:desktop

This diagnostic command is an alternative stopping point, not the first half of a two-command build. A later package:desktop* command repeats the official build and preparation so it cannot consume stale dsh packages, runtime files, or dsh content.

Desktop runtime preparation

Desktop patch policy explicitly classifies every workspace patch as shared, workspace-only, or runtime-only. Shared entries reuse the root patch bytes and verify their hash against the root lockfile; runtime-only entries use separate repository files without changing workspace installation. Workspace-only patches belong to build tools or dependencies already embedded in client bundles. Unclassified or stale entries, missing files, changed hashes, incompatible resolved versions, and workspace-only packages entering the runtime stop packaging. The preparation script writes only to the temporary project; pnpm applies the selected patches and rejects application failures.

Desktop dependency overrides are declared separately in the patch-policy module, not inferred from patch versions. The pi-ai constraint keeps its runtime version at the version qualified by the shared patch. Desktop still resolves a fresh runtime lockfile for each build, then installs with --frozen-lockfile; dependency resolution across separate builds is not yet reproducible. CLI packed-installation tests and other distribution flows retain their own configuration.

Every package command builds the repository, packs the first-party production closures rooted at dsh and the private Desktop Host, and prepares the target Electron distribution plus primary-runtime with its shared pnpm CLI. prepare:dsh installs the production graph once at build time, prepares materialized packages for electron-builder to archive under app.asar/dsh, removes package-manager metadata, and writes desktop-runtime.json with shared package versions and final file hashes. Signed macOS builds sign and verify native files before inventory generation; electron-builder excludes this already-signed tree from nested re-signing. Resource mappings explicitly include dsh/node_modules, which the default root-directory filter omits; the prepared runtime inventory is checked after native signing. Native executables and libraries are unpacked beside ASAR; Python, standalone Node and pnpm remain in external runtime resources. Windows packaging checks every prepared PE against its ASAR unpacked entry and byte-identical disk copy, including unsigned builds. Builder glob rules match braces in PE filenames as single-character wildcards, so a matching neighboring file may also be unpacked. Prepared runtime smoke uses the verified target descriptor rather than the build host architecture. Signed installer, notarization, installed upgrade, and target-specific native-module qualification require the release environment.

Signed macOS packaging writes Contents/Resources/app-update.yml while assembling the App and before code signing, including the directory build that feeds the parallel ZIP and DMG lanes. The signing hook verifies the exact feed and updater cache directory. Both lane copies and the promoted App are checked again before the release completion record is written; a missing or mismatched configuration prevents artifact promotion and therefore prevents upload.

An unpacked artifact contains Electron, the materialized dsh production tree, pnpm, and the shell application. Installer size and filesystem size differ; release qualification measures both, plus the profile’s plugin storage and first-launch latency. The runtime trades more application files for eliminating core package installation on the user’s machine.

Updates

On Windows, the downloaded-update confirmation explains that the application closes during installation, reopens automatically, and should not be launched again while updating. An installer restart carrying --updated raises and focuses the main window once when startup opens the workspace directly, without enabling always-on-top. Startup into the welcome page discards that request so a later login keeps its normal activation behavior. Ordinary launches and other platforms do not use this foregrounding step.

Native update overlays wait for a ready document and a visible parent, and reappear when that parent is shown again. Closing an overlay releases its input interception and parent listeners. The local window qualification exercises these transitions without starting a workspace.

Packaged applications check fixed Nightly asynchronously at startup. Ordinary polling uses a ten-minute base interval with independently sampled ±20% jitter. Each check failure doubles the base delay up to one hour; success resets it. The randomized delay is bounded by that cap and starts after all joined callers settle. Foreground and system-resume checks respect the same monotonic deadline; the localized Check for Updates menu item, including in the Windows caption's Application menu, runs immediately and joins an in-flight check. A newly received mandatory policy also requests an immediate feed check. Automatic checks never open dialogs or download packages. Manual checks display checking, failure, or no-update feedback with the installed version. Ordinary update dialogs fade in and out in place; consecutive prompts replace the card content and reset its scroll position while retaining the dark translucent backdrop without blurring the parent page.

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

Confirmed Host exit without successful task teardown displays localized recovery guidance in both ordinary and mandatory update dialogs. A typed preparation cause selects that guidance in each locale; changing translated wording cannot reclassify the failure. “View technical details” is collapsed by default and exposes only exit status, signal, shutdown acknowledgement, and deadline facts, not plugin stderr. Expanding it neither retries nor authorizes installation.

Local desktop settings

The shell configuration decision defines Host-independent ownership and the criteria for evaluating existing settings for migration.

The Electron main process reads app.getPath('userData')/desktop/settings.json once at startup, independently of Host and Cordis configuration. The default packaged paths are %APPDATA%\@deepseek-ai\dsh-desktop\desktop\settings.json on Windows and ~/Library/Application Support/@deepseek-ai/dsh-desktop/desktop/settings.json on macOS. Development uses the userData path printed by its launcher. DSH_HOME does not relocate this file.

On first launch, including after an upgrade, Desktop creates this document if it is absent:

{
  "updates": {
    "allowTestAuthPopupWindow": false
  }
}

Existing documents are never overwritten; an omitted updates object or field resolves to false. Set the field to true to permit test authentication dialogs, then fully quit and restart the application. Set it back to false and restart to suppress them again. Creation, read, or validation failures use the default false without blocking startup. Desktop attempts to warn through the Electron console with the file path; unavailable logging does not block startup. Correct the file and restart to apply an explicit setting. Unknown fields are preserved. Installation updates retain this user-data file; Windows uninstallation removes it under the uninstall rules.

Mandatory update policy

Packaging reads .env.windows or .env.macos: DSH_DESKTOP_AUTO_UPDATE_ENV=test (the default) selects DSH_DESKTOP_MANDATORY_UPDATE_TEST_ORIGIN; production selects DSH_DESKTOP_MANDATORY_UPDATE_PROD_ORIGIN. The templates leave both origins blank; fill in the selected deployment origin in the ignored local dotenv file. Local unsigned macOS builds omit this policy and do not read either file. Other modes require the selected origin before preparation or signing, including unsigned Windows and preparation-only builds; the unselected origin is optional. These settings never fall back to the parent environment or the other deployment. Packaging embeds the selected policy with the application ID; packaged applications ignore runtime overrides.

DSH_DESKTOP_MANDATORY_UPDATE_CONFIG JSON supplies test login origins and optional polling and download-page options; packaging rejects origin and authentication inside it. The page allowlist defaults to the selected service origin; explicitly allow other approved download-page origins when needed. Test builds select feishu-test and require allowedAuthOrigins in DSH_DESKTOP_MANDATORY_UPDATE_CONFIG; production selects anonymous and rejects that field. Each login origin must be an HTTPS origin without credentials, path, query, or fragment. The login window permits document navigation only to the selected policy origin and these configured origins. Policy requests reject redirects; only test authentication carries gateway cookies. Unpackaged development instead reads a complete policy JSON from this variable and requires DSH_DESKTOP_APP_ID; absent JSON disables development policy queries, and only anonymous development permits HTTP 127.0.0.1. A user-initiated ordinary check triggers policy work concurrently but never waits for or reports a policy failure. Only a confirmed blocking decision cancels ordinary dialogs. Test authentication waits until the active ordinary dialog finishes, and cancellation or failure does not discard the updater result.

Resolved policy field Meaning and default
origin Required HTTPS API origin, without credentials, path, query, or fragment; requests use /api/v0/check_client_update
allowedPageOrigins Nonempty array of exact HTTPS origins; packaging defaults to the selected API origin; subdomains and alternate ports are not implied
authentication Packaging selects feishu-test for test and anonymous for production; unpackaged development defaults to anonymous
allowedAuthOrigins Nonempty array of exact HTTPS login document origins for test authentication; forbidden for production
intervalMs Polling interval; default 600000
timeoutMs Request deadline; default 15000
maxBackoffMs Maximum failed-request interval including jitter; default 3600000, at least intervalMs
jitter Random additional interval fraction; default 0.2, allowed range 0 through 1

Durations are integers from 1000 through 2147483647 milliseconds. Startup and scheduled polling are independent of business requests; foreground/resume checks respect the next due time, while manual checks bypass it and join any request in flight. The client sends the installed platform, architecture, DSH_CLIENT_VERSION, bundled dsh version, current locale and UTC offset, an empty bundle ID, and fixed Nightly. It uses no business login credentials or installation ID.

With feishu-test, an HTTP 401 JSON response containing error.code: "UNAUTHENTICATED" offers login only when the local updates.allowTestAuthPopupWindow setting is true. It defaults to false, suppressing the entire authentication dialog flow during startup, manual update checks, mandatory-update refreshes, and deferred or repeated failures. Test gateway cookies are process-local. With popups disabled at startup, a gateway requiring authentication cannot provide new mandatory-update decisions during that process; enable the setting, restart and sign in to obtain them. Policy requests still enforce gateway authentication, and an authentication failure never clears a known mandatory block. Ordinary updates and product account authentication remain independent. Production keeps its anonymous policy requests regardless of this setting.

When permitted, login is offered during user-initiated checks and the packaged application's initial startup check, without waiting for the local backend. A localized explanation identifies the test build, the need for Feishu authentication, and that login neither downloads nor installs updates. Confirmation closes the explanation before opening a sandboxed window at the configured origin’s root, not a response-provided login URL. Concurrent checks reuse the entire confirmation/login operation and focus its existing window. Press F12 in the test login window to open detached DevTools for diagnosis. Cancellation does not trigger repeated prompts from periodic or foreground checks; users can retry manually.

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 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. Caption menus retain mouse and keyboard operation; Web controls in the caption remain reachable. The shared DOM is a presentation constraint, not a security boundary: product scripts can hide the overlay but cannot clear main-process policy or authorize installation through it. Update actions and state use a private MessageChannel between the isolated preload and shell frame. 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.

Background mandatory-installation confirmation requests Windows taskbar attention or an informational macOS Dock bounce plus one silent notification per readiness episode. It does not restore or focus the app. Notification clicks only return to current confirmation. Foreground return, installation, policy clearance, and shutdown clear owned reminders. System permissions and focus modes can suppress notifications; installed Windows and macOS notification qualification remains required.

Local updater qualification

Ordinary update HTTP requests have a per-connection inactivity deadline: no response headers or no further response bytes for 60000 ms fails the operation. DSH_DESKTOP_UPDATE_HTTP_IDLE_TIMEOUT_MS accepts an integer from 1000 through 2147483647 to adjust it; active downloads have no total-duration deadline. Failed downloads retain the retry indicator and require another user action.

On Windows with workspace dependencies installed, run this command from the repository root:

node apps/desktop/node_modules/pnpm/bin/pnpm.mjs --dir apps/desktop run test:updates:local

The command builds the Desktop shell and runs its coordinator with real Electron HTTP requests and NsisUpdater against a private loopback server. It checks user-authorized full downloads, SHA-512 rejection, explicit retry, concurrent request coalescing, feed replacement, and installation handoff. It also opens the real mandatory-update renderer with its sandboxed preload and checks button actions, close/Esc prevention, text-only content, a stalled policy request, and policy clearance. Success prints LOCAL_UPDATER_RESULT and exits with code zero; functional failure exits nonzero. Each invocation owns a random port and temporary user-data/cache directory, closes the listener, waits for Electron exit, and removes its temporary files. Reports and available screenshots remain in a unique .desktop-build/qualification/local-updater-* directory. Screenshot failure is recorded separately, never reported as visual acceptance. No COS or signing credentials are required.

The downloaded bytes are inert, and the installation call is recorded rather than executed. The test substitutes browser opening and clipboard writing to avoid external navigation and clipboard changes. It does not boot the full product workspace, qualify a real installer or restart, verify publisher signatures, or exercise differential updates or macOS. Stalled policy requests, feed requests, and payload transfers exercise real deadlines and recovery. The actual ordinary dialog verifies isolated preload loading, card geometry, an unfiltered parent page, cancellation, task-warning choices, and explicit installation approval; account-row component tests provide separate evidence. The local qualification decision and verification record preserve these limits; production release requirements remain unchanged.

Low-level development overrides

An unpackaged Electron process uses .desktop-build/development/project under its application directory as its development project. DSH_DESKTOP_PNPM_ENTRY and DSH_DESKTOP_DSH_DIR are optional overrides with application-path defaults. DSH_DESKTOP_PRIMARY_RUNTIME_DIR is required for every unpackaged launch: the development launchers (dev:desktop, start:desktop, and the workspace-update qualification runner) set it to the primary-runtime directory of the target they prepared, and a launch without it fails with the fatal startup dialog. The launcher must set it because the shell cannot derive that directory from process.arch: the build target fixes Windows to x64 while the host may be arm64. Packaged applications ignore these variables, resolve signed resources from process.resourcesPath, and use the managed Desktop profile.

Known limitations

  • Account sign-in is not connected; the Sign in button is disabled. Windows material rendering still requires platform QA.

  • Release signing, notarization, update hosting, and previous-version installed-artifact qualification require the production release environment.

  • Dependency lifecycle scripts follow pnpm’s build permissions; Desktop provides no separate approval dialog.

  • The desktop shell shares sessions, settings, credentials, workspaces, and storage under $DSH_HOME with CLI dsh, while executable packages, plugin activation, and lockfiles remain separate.

  • Unpackaged startup on an Electron win32-arm64 host now succeeds, but the payload remains x64: the architecture check in packages/skill/tool-workspace-dependencies/src/index.ts compares the recorded payload architecture against the host process.arch, so the load_workspace_dependencies tool can still reject the primary runtime.

The app-only dshOnboarding.hasApiKey() preload method returns the welcome backend’s current API-key presence boolean; native login and onboarding share credential discovery, and only the owned application main frame may invoke it.

Sign in opens the configured platform page in the system browser. The Host owns PKCE and a temporary loopback callback, saves the credential before entering the workspace, and redirects the browser to the platform completion page. Opened and copied authorization links carry the effective Desktop theme as theme=light or theme=dark; system resolves at the time of the action. Cancel withdraws the local attempt even if the platform page later approves it. Settings offers Account sign-out; without a separate API key, sign-out returns to the welcome window. Successful browser sign-in switches Welcome to the workspace without activating the application; the completion page’s dsh://open link brings it to the foreground. Packaged applications register dsh://open to show the window without passing credentials. On macOS, the development launcher prepares an ad-hoc-signed Harness Dev.app under .desktop-build/development, declares dsh in its Info.plist, and registers it with Launch Services. It loads the current workspace and records the selected development home, browser-data path, and debug settings for cold starts. Starting this bundle makes it the default dsh:// handler; starting the packaged application registers the packaged handler again. The generated bundle does not contain account tokens and requires the workspace and prepared runtime to remain available.

When an expired account returns to Welcome, the main process retains a one-time notification until the renderer requests it over window-owned IPC. Reloading Welcome does not repeat the notice; manual sign-out and cold startup do not create it.

An expired login displays a timeout heading with explicit Sign in again and Add API Key actions. Opening the API-key form dismisses the authorization view; late account-state notifications do not replace an in-progress key entry.

Embedded Platform views remain hidden until document loading completes so the renderer loading indicator stays visible. Closing or replacing a pending view prevents it from appearing later. Reloading or replacing the owning application document, renderer termination, and window closure also destroy the native view without relying on React cleanup.

Private Platform deployment headers are injected by the embedded browser session only for its configured origin, including document and API requests. Cookie overrides merge by name. Cross-origin requests discard deployment headers; bootstrap exposes only origin, token, and resolved language.

The account provider’s embeddedPageDist configuration adds a dist query parameter to embedded Usage and Top-up URLs. Its default is empty; private frontend branch selectors belong in the local profile patch. It does not change API URLs or credential delivery.

Dev Note

Pre-launch CDN and capacity decisions are tracked in the Desktop update proposal.