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 defaults to port 19387, separate from Web’s 3080; a webserver.config.port patch can override it.
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.
Creator and the Web Plugin Manager use Desktop’s bundled pnpm under Electron Node mode without requiring pnpm on PATH. The private Node launcher environment applies only to package operations.
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.
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 and a packaged usage description.
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.
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 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 registers office-docx, office-pptx, and office-xlsx by default. The skills use the bundled Python libraries for creation and focused edits, then reopen the files and run a shared structural checker before delivery. PowerPoint creation and editing use python-pptx. Skill resources are copied to runtime/office-skills outside ASAR so Python can read the checker. An available render_document tool can add visual inspection; its absence does not prevent authoring or delivery. See the Office skill package for checks 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. 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.
| 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.
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 or mutate this profile.
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. 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 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. Core packages are never copied into profile storage or installed by pnpm at first launch.
- 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.
- 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-fallbackprojections 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). - Changes to Electron's Node version, platform, or architecture preserve installed plugins. Native incompatibilities surface during loading and can be repaired through pnpm.
- 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.
- 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.
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.
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.
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 version decision explains why the channel does not supply the prerelease identifier.
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, update origin, and mode-specific 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 and explicit unsigned builds 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.
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. It preserves runtime JavaScript, 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 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 loaded skills 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 Windows 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 obtains CLI paths from the loaded skill and runs capabilities and DOCX conversion with an empty 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
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. 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 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. 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 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. 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 requires DSH_DESKTOP_APP_ID and the normal build dependencies, including Python and Visual C++ build tools for native modules. Set PYTHON to the Python executable when it is absent from PATH. It writes the installer to .desktop-build/targets/win-x64/unsigned-artifacts/, omits automatic-update configuration, strips signing credentials, and creates no release completion record. It does not require EV credentials or an update origin. The signed packaging and upload commands retain their release requirements.
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; 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. The installer decision records the NSIS integration and release checks.
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.
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 and 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. On macOS it signs and verifies 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.
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.
Mandatory update policy
The mandatory client decision owns policy polling and the blocking window. 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. The selected origin is required before preparation or signing, including unsigned 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 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_HOMEwith 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.tscompares the recorded payload architecture against the hostprocess.arch, so theload_workspace_dependenciestool 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.