# dsh-workspace-native-open A **workspace menu enhancement** plugin for DeepSeek Harness (DSH): when the Web GUI is accessed over a loopback address, the workspace row's ⋯ menu gains three native actions — **Reveal in File Explorer**, **Open in Terminal**, and **Open in VS Code** — executed on the host machine. ## ✨ Features | Feature | Description | | --- | --- | | Reveal in File Explorer | Opens the workspace directory with the OS file manager (`explorer.exe` / `open` / `xdg-open`, with WSL path translation) | | Open in Terminal | Opens a new terminal window at the workspace directory. Windows: `pwsh` > `powershell` (probed via `where.exe`; Windows 10+ always ships PowerShell); WSL: path translated via `wslpath` and opened through the Windows console; other platforms: `$TERMINAL` > `bash` | | Open in VS Code | Always shown — the plugin never checks whether VS Code is installed; it just attempts to open the directory and silently ignores failure (per requirement). On WSL the Linux-side `code` script is preferred, falling back to `code.cmd` on the translated path | | Loopback-only | The menu items only appear when the page is served from `localhost` / `127.0.0.1` / `::1` / `[::1]` (plus the built-in `dsh.localhost`); the host endpoint additionally rejects any request whose `Host` header is outside this whitelist — remote or reverse-proxied access never exposes host-side actions | | Silent by default, logged when it matters | Failures never disturb you, but every request, probe result, spawn failure, and launcher exit code is logged with the `workspace-native-open` prefix for diagnosis | | Path-separator safe | Paths travel as `argv` arrays (never shell-string concatenation); both `/` and `\` separators work on Windows; PowerShell single-quoted literals protect spaces and special characters | ## 📦 Install ### Option A: install directly from GitHub ```sh dsh plugin --profile web add github:JoeyLearnsToCode/dsh-workspace-native-open ``` ### Option B: local link (development) ```sh dsh plugin --profile web add link:/path/to/dsh-workspace-native-open ``` ### Option C: manual deployment 1. Make the package resolvable by the harness (e.g. place it in `node_modules`) and register it in the profile's `cordis.patch.yml`: ```yaml - insert: - id: workspace-native-open name: 'dsh-workspace-native-open' ``` 2. Restart `dsh web` > This plugin targets the Web profile (`dsh --profile web`) and requires the `webServer` service provided by the `dsh-web-app` bundle. ## 🔧 How it works A single host-side plugin with two halves: - **Host** — registers `POST /api/plugin/workspace-native-open` on the `webServer` service. The handler rejects requests whose `Host` header is outside the loopback whitelist, validates the action whitelist, enforces a JSON content-type fence (the same CSRF stance as `/api/*`), and verifies the path is an existing absolute directory before executing the native open action fire-and-forget. Request bodies are size- and time-bounded (413/408). - **Client** — subscribes to the `webserver/index-inject` event and injects a small classic script + stylesheet into every `index.html`. The script activates only on whitelisted loopback hostnames (the same list as the host side, inlined at build time): it captures clicks on the workspace ⋯ button (identified by its `aria-label` — dsh exposes no stable id/class/data attribute on the button or menu; CSS-module classes are content-hashed) and pairs the click with the portal menu that appears right after it, so no menu-content text matching is needed. Labels follow dsh's Language setting via `` (the translations live in `package.nls.zh.json` / `package.nls.en.json` and are inlined at build time), themed with the menu's own CSS variables. The workspace row → path mapping extracts the workspace name from the row's `aria-label` and matches it against the `title` in the `/api/workspace.list` response — robust against search filtering, add/remove, and reordering. DOM-order alignment is kept only as a fallback for same-name workspaces or a changed label format. ### Platform notes - **Terminal on Windows** — the target shell is probed in `pwsh` > `powershell` order (`where.exe`, full paths; Windows 10+ always ships Windows PowerShell, so there is no cmd fallback), then opened in a new window via `Start-Process -WorkingDirectory` from a hidden launcher. Console programs must NOT be spawned with `detached: true` on Windows — `DETACHED_PROCESS` makes them exit silently without running. - **Terminal and Code on WSL** — the path is translated with `wslpath` and handed to the Windows side (a bare `bash`/`code` spawn has no console on WSL and would be invisible); the `.exe` probe suffix pins the Windows binaries instead of any Linux-side PowerShell. For Code, the Linux-side `code` script is tried first so WSL remoting keeps the native WSL path. - **Code on Windows** — Node ≥ 20.12 refuses to spawn `.cmd` shims directly (CVE-2024-27980 hardening), so `code.cmd` is invoked through a PowerShell launcher (`& 'code.cmd' 'dir'` — PowerShell resolves `code.cmd` from PATH itself, so the plugin never probes whether VS Code is installed). - **Lifecycle** — launchers exit immediately, so the windows they open are naturally detached from the dsh process tree and survive dsh restarts. ## 🐛 Troubleshooting Every decision is logged through the harness logger under the `workspace-native-open` prefix: - request received / action taken (info) - probe results, spawn failures with reasons, launcher exit codes and stderr (warn) Failures are also mirrored to the browser console (`[dsh-native-open] …`) and carried in the endpoint's JSON error field. - `403 not allowed from this host` — the request's `Host` header is outside the loopback whitelist; open the page via a whitelisted hostname, or extend `LOOPBACK_HOSTNAMES` in `src/index.ts` and rebuild. - `408 body timeout` — the request body stalled; the endpoint drops the connection after 10 s (and rejects bodies over 64 KB with `413`). ## 📝 Known limitations - The menu items are injected into the workspace menu via DOM observation (the trigger button's `aria-label`, click-causality pairing with the portal menu, and the menu's CSS variables); they are disabled (greyed) if the workspace path cannot be resolved at click time. - The plugin is host-side only — no `dsh.client` bundle, no build pipeline for browser assets. - Remote / LAN access never shows the items by design and the endpoint refuses non-whitelisted `Host` headers; `dsh.localhost` is already whitelisted — to allow another local hostname, extend `LOOPBACK_HOSTNAMES` in `src/index.ts` and rebuild. - On WSL, Terminal opens a Windows console at the translated `\\wsl$` path — the same Windows-desktop handoff convention as the explorer action. ## 🛠 Development ```sh # edit src/index.ts, then rebuild the loader artifact bun run build ``` - `src/index.ts` — the plugin source (loaded as a dsh bundle via `dsh.bundle.patch`). - `lib/index.js` — the built ESM artifact referenced by `package.json#main`. - `cordis.patch.yml` — the bundle patch that inserts the `workspace-native-open` row. - `package.nls.zh.json` / `package.nls.en.json` — the Simplified Chinese / English translation catalog (menu labels and page-matching literals), inlined into `lib/index.js` at build time. ## 📄 License [MIT](LICENSE)