# dsh-auto-open-web A persistent plugin for the `dsh web` profile that automatically opens the DSH Web GUI in an app-style window (or browser tab) on profile start, with a configuration form (manually maintained browser path, etc.). On dsh ≥ 0.1.5 that entry point is the bundle's detail page on the **Plugins page**; on rc.7–0.1.4 it is the **Settings → Plugin configuration** card. ## Behavior On startup (after the HTTP service binds and the actual listening port is known), the window type is selected by `windowKind`: 1. **WebView2 host** (`windowKind: webview2`, default, Windows only): launches the bundled `DshAppWindow.exe` (WinForms + WebView2, own process, no tab/address bar), **loading the GUI root address directly** (no iframe, no wrapper page, no injected scripts). **Taskbar/window icon = DSH icon** (the window is owned by the host process, which sets `Form.Icon` directly, independent of browser taskbar identity rules). **Exits with DSH** (the host watches the parent process PID). **Remembers window size/position/maximized state** (`%LOCALAPPDATA%\DeepSeekHarness\window-state.json`, saved on close and restored on start; falls back to centering when the display layout changes). 2. **Browser app window** (`windowKind: browser`): a **dedicated Edge/Chrome instance** via `--app` (`--user-data-dir=~/.dsh/-app-profile`, isolated process tree and storage, **shares no processes/Cookies/cache with the normal browser**; `--no-first-run` skips the first-run welcome page). **Exits with DSH (including force-kill)**: the browser instance is placed into a **Job Object** (`JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, koffi-driven `JOBOBJECT_EXTENDED_LIMIT_INFORMATION` structure, 144 bytes verified); whether DSH exits normally or is force-killed (`taskkill /F`, crash, shutdown, etc.), the Windows kernel terminates every process in the job when its last handle closes — the whole dedicated instance process tree dies with it, with no reliance on any exit event. Two extra safety nets: on normal DSH exit a `process 'exit'` handler kills the dedicated instance process tree (matching only our own user-data-dir, never the normal browser); instances left behind by a force-kill are cleaned up before the next launch. 3. If the selected type is unavailable (host missing / browser not found / non-Windows, etc.) → **falls back to the official default-browser handoff** — the URL is given to the OS default browser (plain tab), so the user can always reach the GUI. The implementation mirrors exactly how the DSH core (web-app bundle) opens the page at startup: it prefers the `open` package shipped with the DSH deployment (win32 = PowerShell Start, darwin = `open`, linux = `xdg-open`); when the package is unavailable it falls back to a native platform launch (win32 = `cmd /c start`, darwin = `open`, linux = `xdg-open`). `appWindow: false` behaves **exactly like the official core**: the GUI is opened in the system default browser (plain tab, official `open` method). 4. **`--no-open` / SSH sessions (official identical suppression)**: `dsh web --no-open` (`webStartup.openBrowser === false`) or an SSH session (`SSH_CONNECTION`/`SSH_TTY` environment variables, the same source as the official `launchedThroughSsh`) opens **nothing** — the plugin reads the **same source** as the official core (the `webStartup` service provided by web-startup) and suppresses opening the same way. The port comes from the real listening value of the webServer service (`--port` overrides and `--port 0` both work). **No waiting needed**: the plugin declares webServer as a hard dependency (`inject`), so Cordis only activates it after the webServer plugin's `Service.init()` completes (HTTP socket bound, port written) — the port is directly available in `apply`. Both modes close with DSH: the webview2 host watches the parent process; the browser dedicated instance is ended by the Job Object (also effective on force-kill) plus exit cleanup. > **Relationship with DSH's core browser handoff**: the DSH core (web-app bundle) opens the GUI > in the system default browser at startup by default (`openBrowser: true` — a plain tab/window, > not an app window). Installing this plugin flips `web-runtime.openBrowser` to `false` via the > plugin's bundle patch, so the opening behavior is fully owned by the plugin: with > `appWindow: true` the app-style window opens and no ordinary browser page pops up; with > `appWindow: false` the plugin performs **the same default-browser handoff as the official core** > (open package → native platform launch), matching the no-plugin behavior. With > `dsh web --no-open` (or in an SSH session) the plugin, like the official core, opens > nothing. Uninstalling the plugin restores the official default. ### WebView2 host requirements (webview2 mode only) - Windows 10 1803+ / Windows 11 / Windows Server 2016+ (Win7/8.1 reached end of support in 2023-01, see Microsoft announcements) - WebView2 Runtime (evergreen, usually preinstalled with Edge; verified with 151.x locally) - **No .NET 10 runtime required**: since 0.1.15 the host targets **.NET Framework 4.7.2**, built into Windows 10 1803+ / Windows 11 (4.8.x on Win11) — no .NET Core/10 runtime or self-contained publish is needed. The build machine needs a .NET SDK and the .NET Framework 4.x targeting packs (installed with VS/SDK; without them add the `Microsoft.NETFramework.ReferenceAssemblies` NuGet package to the csproj). - **DPI awareness (since 0.1.20)**: the host declares **PerMonitorV2** (`host/app.manifest` plus the WinForms `DpiAwareness` section in `App.config`). .NET Framework WinForms is DPI-unaware by default, so on 125%/150% scaled displays Windows renders the window at 96 DPI and bitmap-stretches it (DPI virtualization) → blurry text and web content. Declaring awareness renders natively at the monitor's real DPI, keeping text crisp. ## Configuration Two equivalent ways: 1. **Settings form** (recommended) — two supported version lines; the plugin registers for both and whichever exists wins: - **Newest version (dsh ≥ 0.1.6-alpha, incl. 0.1.7-alpha.1)**: **Plugins page → `dsh-auto-open-web` → this plugin's row → Configure**. The form opens per **row** (slot `plugins.row.config`, key `#` = `dsh-auto-open-web#auto-open-web`); the page supplies the data (`form = { state, mutate }` — the host-projected row Config snapshot plus atomic writes) and the plugin only draws the controls. Only **Save** is offered — leaving the page drops staged edits. - **Latest rc line (0.1.5-rc.x)**: Settings → Plugin configuration → the "自动打开网页" collapsible card (slot `settings.plugin.item`, keyed by settings namespace). Data goes through the host's `settings.register` plus the client `settingsScope`. The two lines' client services **do not coexist** (new `configForms` ↔ rc-line `settingsScope`), so the plugin reads both **lazily** and declares only the shared services in `inject` (`slots`/`locale`/`connection`/`remote`) — a hard inject would leave the plugin pending forever on the other line and the settings UI would never appear. Earlier versions (0.1.0-rc.*, 0.1.1-rc.*, 0.1.2-*, 0.1.3-alpha, 0.1.5-alpha, 0.1.6-alpha.1) are **no longer guaranteed**. Both entry points edit `appWindow` (independent app window), `windowKind` (WebView2 host / browser app window), `browserPath` (browser executable, with a native "Browse" file dialog; located below the window-type field and enabled only when "Browser app window" is selected), `exitOnWindowClose` (exit DSH when the window closes, off by default). After saving, values take precedence over row configuration (on the newest version they land in that row's user layer; on the rc line in the `auto-open-web` settings namespace); once saved, settings take precedence over row configuration. The host keeps only the "Browse" / "Test" helper routes (capabilities the official channel cannot cover). 2. **Row configuration** (`cordis.patch.yml`): acts as the startup seed, effective until the settings card is saved. | Field | Default | Description | | --- | --- | --- | | `appWindow` | `true` | Automatically open the independent app window on start; `false` matches the official core — the GUI opens in the system default browser (plain tab, official `open` method) | | `windowKind` | `webview2` | `webview2` = WebView2 host (own process, DSH taskbar icon, exits with DSH); `browser` = dedicated `--app` browser instance. If the selected type is unavailable, it falls back to the default-browser handoff (official open method, plain tab) | | `exitOnWindowClose` | `false` | **(Experimental)** Exit DSH when the auto-opened window closes (off by default; only effective while `appWindow` is on). Triggered only when the window process exits **normally** (user closes the window) → `process.exit(0)`; startup failures/crashes/force-kills (non-zero exit code) do not trigger, preventing accidental exits. **Takes effect immediately in the current session after saving** (the exit listener is always registered; behavior is driven by a live flag), no restart needed | | `browserPath` | `''` | Manual browser executable path (single entry, e.g. `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`; used only in browser mode), preferred over the built-in candidates Edge → Chrome; a non-existent path is skipped with a warning. The "Browse" button on the card opens a **native file dialog**: same mechanism as the official workspace directory picker (child process + koffi-driven `IFileOpenDialog`; the dialog is the child's first window and is automatically brought to front; no PowerShell). The "Test" button **actually launches** a dedicated `--app` test instance (separate user-data-dir `~/.dsh/-test-profile`, never pollutes the real instance): after confirming the browser main process stays alive it reports success, then automatically ends that test process tree after a few seconds of display (exact pid, never touches the real instance; the test instance is also placed in the Job Object when available as an exit safety net); the test uses the currently typed path (works even when unsaved), and failures show the reason | ### `browserPath` row configuration example Override the row's config by id in `~/.dsh/profiles/web/cordis.patch.yml` (the override replaces the whole config; fields not listed fall back to defaults): ```yaml - id: auto-open-web config: browserPath: 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe' ``` ## Packaging and installation This package is a **bundle**: an npm package carrying a configuration layer — `dsh.bundle` in `package.json` declares the patch file (`cordis.patch.yml`), and a profile activates the plugin row by package name when installed. Published to the **npm registry** (`dsh-auto-open-web@0.1.5`) and **GitHub** (https://github.com/jinsiyu/dsh-auto-open-web, `main` branch). ### Packaging ```bash cd dsh-auto-open-web pnpm pack # the prepack hook compiles the WebView2 host first (dotnet publish), producing dsh-auto-open-web-0.1.5.tgz ``` ### Installation (pick one) **Option 1: source checkout link (development; changes take effect immediately)** ```bash # absolute path to avoid pnpm self-linking dsh plugin --profile web add C:\path\to\dsh-auto-open-web ``` **Option 2: tarball (published artifact, recommended for delivery; no build permission needed)** ```bash dsh plugin --profile web add ./dsh-auto-open-web-0.1.5.tgz ``` **Option 3: npm registry (after publishing)** ```bash dsh plugin --profile web add dsh-auto-open-web ``` **Option 4: GitHub source** ```bash dsh plugin --profile web add github:jinsiyu/dsh-auto-open-web#main ``` ### Uninstall ```bash dsh plugin --profile web remove dsh-auto-open-web # removes the dependency and its configuration layer together ``` ### Effect and layer order After installation: pnpm adds the package to `profiles/web/node_modules`, and `dsh` appends `dsh-auto-open-web` to `dsh.profile.bundles`; at startup the bundle's `cordis.patch.yml` inserts the plugin row (`name: auto-open-web`, resolved by package name). After restarting `dsh web`, the configuration form shows up at the matching entry point (dsh ≥ 0.1.5: the `dsh-auto-open-web` bundle's detail page on the Plugins page; earlier versions: the Settings → Plugin configuration card). The client bundle is scanned into the browser manifest at startup via the modules line, per the `dsh.client` declaration; on the client the plugin registers into **both** `plugins.bundle.config` (new) and `settings.plugin.item` (old), and the slot machinery decides which one takes effect (a slot that does not exist stays pending — no error, no interference). The effective configuration is composed layer by layer in this order (later layers win per row, replacing the whole row's config rather than deep-merging): each bundle's patch (in bundles list order) → the profile's own `cordis.patch.yml` → the global `$DSH_HOME/cordis.patch.yml` → the `--patch` overlay. Users can override this package's row in their own profile's `cordis.patch.yml` without touching the package. ### Notes - **Zero dependencies**: every runtime dependency is provided by the **DSH deployment** and declared as an optional peer (no install warnings): - `@deepseek-ai/dsh` (the host; declares the compatibility range **`>=0.1.5-rc.1 <0.2.0`** — covering the **latest rc line (0.1.5-rc.x)** and the **newest version (≥0.1.6-alpha, incl. 0.1.7-alpha.1)**; older 0.1.0-rc.*/0.1.1-rc.*/0.1.2-*/0.1.3-alpha/0.1.5-alpha releases are no longer guaranteed. The snapshot store uses `@deepseek-ai/dsh-client-store` only — the rc.8-era `dsh-client-runtime` fallback was removed; the client registers the newest version's row slot (`plugins.row.config`) and the rc line's settings slot (`settings.plugin.item`); no runtime version check is performed) - `@deepseek-ai/schemastery` (config schema validator; resolved at runtime: normal import first, then the DSH deployment copy under the Windows global npm layout) - `koffi` (Windows only: Job Object / process verification / native file dialog; same runtime deployment-copy resolution, failure only degrades) So no platform ever hits build-script blocking — HarmonyOS and other environments without koffi prebuilds install out of the box; posix platforms use the posix fallback adapter (browser mode works; webview2 / native dialog unavailable), and the platform adapter is loaded conditionally so win32.js is never evaluated on posix. - This package does not depend on `@deepseek-ai/cordis` (zero code references; the plugin identity comes from the `dsh.bundle` / `dsh.client` fields, and Cordis is provided by the DSH deployment at runtime), so installation produces no peer warnings. - **The npm package already contains the compiled WebView2 host** (built by the prepack hook before publishing); the **GitHub `main` branch and source-checkout installs do not** include `host-publish/` (build artifacts are .gitignore'd): for webview2 mode, run `pnpm run build:host` inside `node_modules/dsh-auto-open-web` first (requires the .NET SDK); browser mode needs no build. - If installing by editing `package.json` manually (not via the `dsh plugin` command), you must add both the `dependencies` entry and `dsh.profile.bundles`; when using a local `file:` dependency, `dsh web` normalizes `file:` to `^0.1.5` at startup, which does not affect runtime. ## Icons - GUI page icon: the GUI ships its own `/favicon.svg` (same as index.html). - **Taskbar/window icon (WebView2 host)**: the host process sets `Form.Icon` directly to the plugin-generated DSH .ico (`~/.dsh/auto-open-web-icon.ico`), independent of browser taskbar identity rules. The .ico is built by fetching the local `favicon.svg` and rasterizing it with **sharp** (bundled with the deployment, resolved upward at runtime, not declared as a dependency) into 16/32/48/64/128/256 PNGs. - **Fixed-icon policy (since 0.1.14)**: once written, the .ico is cached and reused — later starts return the existing non-empty cache directly (no favicon fetch / rasterization, so the icon cannot vanish on a transient favicon/sharp failure, and starts are faster); it is generated only when the cache is missing. **Extra safety net**: `host/icon.ico` (same source as the cache) is embedded into the host exe via ``, so even with a missing cache the window/taskbar shows the DSH icon instead of the default one. ## Platform support - Windows: `windowKind: webview2` (default, DSH taskbar icon) or `windowKind: browser` (dedicated `--app` instance); falls back to the default-browser handoff (official open method, plain tab) if the selected type is unavailable - macOS/Linux: `webview2` mode is unavailable (falls back to the default-browser handoff); `browser` mode is untested (dedicated `--app` instance) ## Edge cases - Normal restart: in webview2 mode the old host window exits with the old DSH process; the new DSH opens a new host window - `browser` mode: after a restart the old window stays as-is (needs a manual refresh; may briefly coexist with the new window); when DSH is force-killed (`taskkill /F`, crash), the dedicated instance is ended by the Job Object without leftovers - Plugin removed: no injected code, no leftover routes, zero residual impact