dsh-code-server-app 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -1,520 +1,522 @@
1
- # dsh-code-server-app — Integrate code-server (VS Code in the browser) into DSH
2
-
3
- > Source repository: see `repository` / `homepage` in `package.json`.
4
-
5
- > ## ⚠️ Extension Marketplace Note (important)
6
- >
7
- > - **code-server's extension store is [Open VSX](https://open-vsx.org/), not the Microsoft Visual Studio Marketplace**;
8
- > - Microsoft's Marketplace terms **prohibit third-party products (including code-server) from using its API**, so code-server cannot query Microsoft's extension list;
9
- > - As a result, Microsoft **commercial/proprietary** extensions (e.g. **GitHub Copilot, the Remote series like Remote-SSH, Azure tools, IntelliCode**) are **not available** in the store — this is Microsoft's distribution policy, not a defect;
10
- > - Microsoft **open-source** extensions (Python, TypeScript debugger, ESLint, …) are mirrored on Open VSX and install normally by search;
11
- > - **If you need a proprietary Microsoft extension**: download the `.vsix` from the Marketplace page and install it manually with `code-server --install-extension <file>` (or drop it into `--extensions-dir`).
12
-
13
- A static profile plugin (npm package with host + client bundle) that ships the **VS Code server tree** from a [code-server](https://github.com/coder/code-server) release as a **platform-independent dependency package** (pack-time artifact `vendor/vscode` → `@jinsiyu/dshcs-vscode-server`, no install scripts, no postinstall). The code-server **Node service layer is replaced by the plugin's own `lib/launcher.mjs`**: it drives `<tree>/lib/vscode/out/server-main.js` (`loadCodeWithNls()` / `createServer()` / `handleRequest()` / `handleUpgrade()`) directly and re-adds the few HTTP endpoints code-server used to provide (`/healthz`, `/manifest.json`, `/_static/*`, `/proxy/:port`). The 16 native modules (node-pty / @vscode/sqlite3 / spdlog / …) come from `@jinsiyu/dshcs-*-win32-<arch>` platform packages selected automatically per architecture by the platform aggregator. VS Code's inner dependencies and the prebuilt native modules are **all installed by the package manager together with the plugin** — no global npm install, no `bin` configuration, no profile config changes, no second install command, **no argon2/C++ toolchain**.
14
-
15
- ## UI carrier and required DSH version (0.2.3: right-sidebar DSH only)
16
-
17
- | DSH version | Carrier | Entry points |
18
- |---|---|---|
19
- | **>= 0.1.5-alpha.1** (has `sidebarRight` / `sidebarRightTabs`) | **Right-sidebar tab** (kind `code-server`, chip `Code Server`), which also **claims file addresses** (see below) | ① DSH's own **produced-file chips / presented-file card previews / inline file names in prose** (since 0.2.5, via the official `openFile` → file address → this tab); ② the **Code Server box** on the sidebar's guide ("开始") page; ③ Settings → Plugins → Code Server → **"Open in right sidebar"** |
20
- | older (no sidebar service) | **Unsupported**: nothing but one notice on the settings page | none (Settings → Plugins → Code Server shows an upgrade notice) |
21
-
22
- - Detection: first a synchronous `ctx.get('sidebarRightTabs') / ctx.get('sidebarRight')` probe; because the services may come up after this plugin, `ctx.inject(['sidebarRightTabs','sidebarRight'], …)` is awaited and a **2.5 s timeout marks the DSH as legacy** (no version comparison, and the plugin's own activation is never blocked).
23
- Since 0.2.4 that verdict is **reversible** and registration no longer relies on `ctx` property access (which on desktop silently skipped registration — the symptom was "settings card looks normal but the sidebar has no entry"):
24
- - services are looked up as `ctx.<name>` first and `ctx.get(name)` second, so either context shape registers;
25
- - when the sync probe already sees the services but `inject` never calls back, registration falls back to the sync services after **1.5 s**;
26
- - at 2.5 s only the settings notice appears; only after **10 s** does the client tell the host to recycle/stop prestarting (so a slow host is not punished);
27
- - services arriving late automatically revoke the legacy verdict, register the sidebar, and report `{sidebar:true}` so the host re-enables;
28
- - a failed registration is no longer silent: it logs an error and the card's entry row says "right-sidebar services were found but the tab could not be registered".
29
- - **0.2.3 dropped legacy-DSH compatibility**: the floating ball and the internal floating window are **deleted**. When the DSH is detected as legacy the plugin
30
- - registers only the settings card (an upgrade notice) — no ball, no floating window, **no file-address claim**, no IDE preload;
31
- - reports `/api/code-server/ui-mode { sidebar:false }` to the host (after the 10 s grace above); the host then **recycles an instance it auto-prestarted** and stops prestarting (a user-started/adopted instance is never touched), and `{sidebar:true}` reverses that if the services show up later;
32
- - upgrading DSH needs **no reinstall** — refresh the page and the card turns back into the full settings card.
33
- - The sidebar tab hosts the code-server page (iframe) and follows the current session workspace; the panel can be collapsed/split/floated/fullscreened by DSH's right sidebar.
34
- - **Resident IDE (0.2.2, on by default)**: switching to another tab or collapsing the sidebar and coming back **no longer reloads** code-server — unsaved editor buffers, terminals and debug sessions all stay put (see "Why switching tabs no longer reloads" below).
35
- - The settings card has exactly **two settings**: "**Claim scope**" and "Resident in background"; plus one "Entry" action row and two read-only info sections (dependency install, environment check).
36
- The old `windowedOpen` (open in a window) and `reserveComposer` were **removed in 0.2.6**: leftover keys in an old settings document neither fail nor apply (they are no longer part of the schema).
37
- To use the IDE in a browser tab, visit the loopback address `http://127.0.0.1:<port>/` (or DSH's `/code-server/` under `serve: dsh`).
38
-
39
- ## Opening files (official entry points since 0.2.5)
40
-
41
- DSH names files with **resource addresses**; `openFile` only hands the address to the right sidebar, which decides who draws it:
42
-
43
- ```
44
- DSH's produced-file chip / presented-file card preview / inline prose mention
45
- → openFile(path, { line? }) (provided by ui-chat)
46
- dsh-resource://file/session/<sessionId>/<path> (or …/file/absolute/<path>)
47
- ctx.sidebarRight.openResource(address)
48
- claimed by the tab type whose patterns match (band extension(3) > builtin(2) > fallback(1),
49
- then the longest matching pattern, then registration order)
50
- ```
51
-
52
- This plugin registers:
53
-
54
- | Field | Value | Effect |
55
- |---|---|---|
56
- | `patterns` | `['dsh-resource://file/**']` | claims file addresses (a pattern containing `:` is matched against the **whole address**) |
57
- | `priority` | `'extension'` | beats the built-in plain-text preview, which sits in `fallback` on purpose — DSH's own comment calls that band "the position VS Code's text editor holds among its editors", i.e. one any more specific type should beat |
58
- | `canOpen` | see below | vetoes by the "claim scope" setting; unclaimed addresses fall back to DSH's built-in preview |
59
- | `title` | last address segment (= file name) | the tab chip shows the file name; a page tab (`sidebar://code-server`) still reads `Code Server` |
60
-
61
- - **Claim scope** (switchable in the settings card, `fileOpenScope`):
62
- - `session` (default) — claim only `dsh-resource://file/session/…` (everything a session produces: deliverables, declared deliveries, prose mentions, tool views);
63
- - `all` also claim session-less `dsh-resource://file/absolute/…` addresses.
64
- - **How the tab body locates the file**: it parses `useTabInfo().tab.navigation.address`
65
- (`src/address.js`, same grammar as DSH's `parseFileAddress`), expands a workspace-relative path with that
66
- session's cwd, and posts the absolute path (plus optional `line`) to the host's
67
- `/api/code-server/open-file`; the bundled extension (`dshcs-open-file`) then calls `showTextDocument`
68
- (positioned at the line when given).
69
- - **One address = one tab** (DSH semantics: `contentId` *is* the address): three files mean three chips, but they
70
- share the single resident workbench — switching tabs just re-aims the workbench at the corresponding file.
71
- - **Why the bundled extension stays**: VS Code Web has no official "open this file from outside" API (the only
72
- entry is `?folder=`, which picks the workspace), so aiming the workbench at a file has to be done by an
73
- extension inside the tree. The host writes a signal file, the extension polls it and calls
74
- `showTextDocument`, keeping the signal for retry when no window is connected yet.
75
-
76
- ## Why switching tabs no longer reloads (resident IDE)
77
-
78
- **The old trap**: DSH's right sidebar (ui-dockkit) renders **only the active tab's body**
79
- (`TabPanel.tsx:412` → `renderTab(active)`) — switching to another tab unmounts that body in React, which moves the
80
- iframe out of the document and destroys its browsing context; switching back is a full VS Code reload (unsaved buffers
81
- lost). Floating the tab into its own panel only worked around it.
82
-
83
- **What it does now (`src/surface.js` in the client, 0.2.2)**: the plugin takes the iframe **away from React** and turns
84
- it into a **singleton resident surface**:
85
-
86
- | Situation | Action | Result |
87
- |---|---|---|
88
- | tab becomes active | `host.moveBefore(frame, null)` into the visible dock slot | state-preserving atomic move, **no reload** |
89
- | tab deactivates / sidebar collapses | move back into a document-level park container (offscreen, keeps last docked size, `inert` + `aria-hidden`) | never destroyed, keeps running in the background |
90
- | workspace / port changes | assign `src` explicitly | the only normal "reload" entry point |
91
-
92
- - **Why `moveBefore`**: measured in a real browser (Edge/Chromium 151), a plain `appendChild` move resets the iframe's
93
- internal timers (i.e. reloads it), while `Element.moveBefore()` (Chromium ≥133) preserves state (a probe counter keeps
94
- counting 1→2).
95
- - **Degradation is never silent**: when `moveBefore` is missing, or the host was already detached by React and it throws
96
- `HierarchyRequestError: invalid hierarchy` (passive effect cleanup runs after DOM removal), the code falls back to
97
- `appendChild` one reload, but the frame is **never lost** and reports `degraded` / `lastMoveError` so the UI can
98
- say "residency unavailable".
99
- - **Repaint fallback (measured)**: in the real GUI the surface was seen once with correct size, hit testing and
100
- `visibility` that simply **stopped repainting** (a fully white panel, byte-identical screenshots proving no new frame).
101
- `translateZ(0)` and `opacity` nudges did nothing; `display:none forced reflow restore` inside a single JS task
102
- restored it without reloading the iframe document, without losing internal state and without a visible flash.
103
- **The trigger could not be reproduced**: in a probe page an offscreen `moveBefore` park of 337 s (past Chrome's
104
- ~5 min cross-origin throttle window) followed by a dock with the fallback disabled still painted normally. It is
105
- therefore kept as a **fallback**: every park→dock transition runs one `nudgeRepaint()` (counted as
106
- `surfaceSnapshot().nudgeCount`; `setNudgeEnabled(false)` A/Bs it live).
107
- - **Warm-up**: with `keepResident` (default `true`) the host builds the surface right after plugin start and leaves it
108
- parked, so the first tab open needs no cold start; preloading never yanks a surface that is currently docked.
109
- - **Debug handle**: `window.__dshcsSurface` (`snapshot()`, `setParkStrategy('offscreen'|'behind')`, `dock()`, `park()`,
110
- `nudge()`, `setNudgeEnabled(false)`, `destroy()`).
111
-
112
- **Measured** (DSH web GUI, real mouse clicks between sidebar tabs): switching away → `docked:false`, same iframe node,
113
- in-frame probe still alive, `degraded:false`; switching back → `docked:true`, unchanged `src`, IDE pixels and editing
114
- state preserved (no full reload). Full evidence and probe scripts: `docs/analysis-code-server-as-dsh-plugin.md`.
115
-
116
- ## Serving mode (`serve`)
117
-
118
- | Mode | What it does | Requires |
119
- |---|---|---|
120
- | **`loopback` (default)** | the plugin listens on its own loopback port (`host:port`) and the sidebar iframe connects cross-origin; the process can be adopted after a DSH host restart | nothing |
121
- | **`dsh`** | the IDE is mounted on **DSH's own HTTP port** at `/code-server/*` (HTTP prefix route) plus `/code-server/<quality>-<commit>` (exact WebSocket route), forwarded to the launcher's **named pipe**; **no extra port**; every request (including the WS handshake) first passes `ctx.connection.requestRejection()` — the same Host/Origin fence and browser-cookie authentication as `/api` | DSH providing `webServer` (web profile); desktop falls back to loopback automatically |
122
-
123
- - Switch it in `config.serve` in `cordis.patch.yml` or in Settings Plugins Code Server (takes effect on the next start).
124
- - Benefits of `dsh`: a single URL/port (remote access to DSH gives you the IDE), no extra loopback listener, authentication on par with DSH.
125
- - Two **known trade-offs** of `dsh`: the iframe shares DSH's origin, so `sandbox` is dropped there (same-origin plus
126
- `allow-same-origin` is escapable by the frame itself; in `loopback` mode the iframe is cross-origin and `sandbox` stays
127
- as real protection clipboard is still granted via `allow="clipboard-read; clipboard-write"`); and forwarded-port
128
- **WebSockets** cannot be routed because `registerUpgrade` matches exact paths while `/proxy/:port` carries the port in
129
- the path (HTTP forwarding works; use `loopback` when you need WS forwarding).
130
-
131
- - In `loopback` mode every upgrade passes a **code-server-equivalent Origin check** (since 0.2.1): when an `Origin`
132
- header is present its host must equal `Host` (honouring `Forwarded: host=` / `X-Forwarded-Host`, like code-server),
133
- otherwise the handshake gets `403`; non-browser requests without `Origin` are allowed. Without that check any local
134
- browser page could complete a handshake against `ws://127.0.0.1:<port>/stable-<commit>` and drive the IDE.
135
-
136
-
137
- ## Legacy DSH (unsupported since 0.2.3)
138
-
139
- **Behaviour**: when `sidebarRightTabs` / `sidebarRight` cannot be found, the plugin registers a single settings card:
140
-
141
- > **Code Server** this DSH version is unsupported (no right-sidebar service)
142
- > Since 0.2.3 this plugin no longer supports older DSH versions.
143
- > The right-sidebar plugin services `sidebarRightTabs` / `sidebarRight` were not detected, so the plugin exposes no
144
- > entry point at all (the old floating ball and floating window have been removed) and will not start the IDE in the
145
- > background. Upgrade DSH to a version with the right sidebar (>= 0.1.5-alpha.1): Code Server then appears as a
146
- > right-sidebar tab, this page shows the full settings again, and no reinstall is needed a page refresh is enough.
147
-
148
- - **No other UI**: no `shell.overlay` registration (floating ball), no file-address claim, no resident preload.
149
- - **Host side**: the client posts `/api/code-server/ui-mode { sidebar:false }`; the host then ① stops auto-prestarting
150
- the IDE (`maybePrestart` returns immediately) and **recycles** an instance it had just auto-prestarted (unless it
151
- was adopted), so no unusable IDE process or port is left behind. A user-started/adopted instance is never stopped.
152
- - **Why delete instead of keeping**: the internal floating window was a stopgap from the era of early-2026 DSH builds
153
- without right-sidebar services. The resident surface, clipboard handling, shortcuts and panel collapsing all build on
154
- DSH's right sidebar, so maintaining two carriers costs more than it is worth. Older-DSH users should stay on `0.2.2`
155
- (`dsh plugin --profile web add dsh-code-server-app@0.2.2`).
156
-
157
- ## code-server workspace and process lifecycle
158
-
159
- - code-server's workspace **follows the active DSH session/workspace**: switching sessions/workspaces while the IDE is open restarts code-server to the new directory
160
- (resolution order: current session cwd → session's workspace.path → recentWorkspace.path → first workspace.path);
161
- the opened directory is shown inside code-server (`?folder=<cwd>`, the page reloads when following a switch);
162
- implementation note: the iframe `src` must carry `?folder=<cwd>` code-server's front-end remembers the "last workspace" and restores it by itself;
163
- a bare root URL only shows the previously opened directory and does not follow switches (verified locally).
164
- **Windows path format (verified)**: the `folder` parameter must start with `/` and use forward slashes only, e.g. `/C:/Users/User/Desktop/biss`;
165
- a bare Windows path (`C:\...`) is parsed as a URI scheme and the drive letter is stripped (page shows `\Users\User\...` with an empty file tree),
166
- while `file:///C:/...` reports "Workspace does not exist".
167
- - Process lifecycle is managed by the host plugin: startup writes `$DSH_HOME/code-server/pid.json`, stop kills the tree (`taskkill /T` or process-group SIGKILL),
168
- crash/exit updates status live; after a DSH host restart the plugin **adopts** a still-running instance (verifies pid + `/healthz`), without duplicate start or killing unrelated processes;
169
- - `node_modules` and the pack-time artifact `vendor/` are git-ignored; after cloning, follow
170
- "Install the plugin (script-free install; code-server bundled)" below `pnpm install` `pnpm run build:client`
171
- `pnpm run vendor:vscode` `pnpm pack` + `dsh plugin --profile web add`.
172
-
173
- > Verified locally (BM: Windows 11 ARM64): `code-server@4.136.2` (with Code 1.136.1) bundled in the plugin,
174
- > placed offline at activation → VS Code internal deps installed → started → healthz 200 →
175
- > cwd switch restart while running stopped fully recycled.
176
-
177
- ## Packaging (how to build the tarball)
178
-
179
- ```powershell
180
- cd C:\Users\User\Desktop\dsh-code-server-app
181
- pnpm install # dev deps (esbuild + motion); allowBuilds is explicit → no postinstall runs
182
- pnpm run build:client # src/factory.js → lib/client.js (not committed; must be built first)
183
- pnpm run vendor:check # optional: show the bundled tree version vs the latest code-server release
184
- pnpm run vendor:vscode # produce vendor/vscode (the trimmed VS Code tree, ~197MB)
185
- pnpm run repack:build -- --target win32-arm64,win32-x64 --pack # one script builds every sub-package
186
- pnpm run publish:repacks # publish every @jinsiyu/* sub-package (default dist-tag: next)
187
- pnpm pack # dsh-code-server-app-<version>.tgz (~107KB)
188
- pnpm run publish:plugin # publish the plugin itself (default dist-tag: next)
189
- # once the user has restarted dsh web and confirmed it works, promote latest:
190
- pnpm run promote -- <version>
191
- ```
192
-
193
- > **dist-tag policy (mandatory)**: every release goes to **`next`** and **never touches `latest`**;
194
- > `latest` always points at the most recent *confirmed bug-free* version and is only moved by
195
- > `pnpm run promote -- <version>` (= `npm dist-tag add dsh-code-server-app@<version> latest`)
196
- > **after the user restarts `dsh web` and confirms it works**. That way
197
- > `dsh plugin add dsh-code-server-app` (no version) — and anything else resolving `latest` — never picks up an
198
- > unverified build. Sub-packages (`@jinsiyu/dshcs-*`, the aggregators) are referenced by exact/caret versions,
199
- > so their dist-tags do not affect resolution, but they default to `next` as well.
200
- > Inspect the current tags with `npm dist-tag ls dsh-code-server-app`.
201
-
202
- `repack:build` (`scripts/vendor-repacks.mjs`) is the **single script that produces every sub-package**:
203
-
204
- | Sub-package | Content | os/cpu |
205
- |---|---|---|
206
- | `@jinsiyu/dshcs-vscode-server@<code-server version>` | the trimmed VS Code tree (`lib/vscode` + `out/browser` + `src/browser`; **without** code-server's `out/node` and its 136 runtime deps) | platform-independent |
207
- | `@jinsiyu/dshcs-<name>[-win32-<arch>]` ×24 | the VS Code inner packages that need building (node-pty / @vscode/sqlite3 / kerberos / koffi / ssh2 / …) | gated when platform-specific |
208
- | `@jinsiyu/dsh-code-server-runtime-win32-<arch>` | platform aggregator: its `dependencies` map those 16 natives back to their original names via `npm:` aliases | win32-<arch> |
209
-
210
- | Goal | Command |
211
- |---|---|
212
- | **Build from the latest upstream release** | `pnpm run vendor:latest` (= `--force`): pulls `code-server@latest`'s tree into `vendor/vscode`; afterwards you **must** re-run `repack:build` and republish every sub-package |
213
- | **Pin a version** | `pnpm run vendor:vscode -- --version 4.136.2` |
214
- | **Snapshot from an existing tree** | `pnpm run vendor:vscode -- --from <code-server dir>` (seconds) |
215
- | **Rebuild every sub-package** | `pnpm run repack:build -- --target win32-arm64,win32-x64 --pack` (without `--from` it npm-installs and compiles the source tree itself — slow) |
216
- | **Rebuild only the tree/aggregator packages** | `node scripts/vendor-repacks.mjs --reuse --target win32-arm64,win32-x64 --pack` (reuses the natives already in `repack/build`) |
217
- | **Publish sub-packages** | `pnpm run publish:repacks` (`--dry-run` to preview; `--only <substr>` to filter; `--otp <code>` / `--limit N` for 2FA) |
218
- | **Publish the plugin itself** | `pnpm run publish:plugin` (publishes the exact tarball that was verified; no re-packing; default dist-tag `next`) |
219
- | **Promote `latest`** | `pnpm run promote -- <version>` (only after the user restarted and confirmed; `--dry-run` shows the current tags first) |
220
- | **Just report versions** | `pnpm run vendor:check` |
221
-
222
- > `pnpm pack`'s `prepack` runs the vendor-code-server script once; when `vendor/code-server` already exists it is
223
- > a **no-op that takes seconds**, so after ordinary code changes you can just run `pnpm pack` (it will never
224
- > silently upgrade code-server). Upgrading code-server requires an explicit `pnpm run vendor:latest`
225
- > (or `--force` / `--version`) **plus** republishing the sub-packages.
226
-
227
- ## Install the plugin (one command; all dependencies installed by the package manager)
228
-
229
- ```powershell
230
- # no postinstall in the package → no pnpm approve-builds / allowBuilds; one command installs everything
231
- dsh plugin --profile web add dsh-code-server-app@0.2.1
232
- # a local tarball works the same way:
233
- dsh plugin --profile web add C:\Users\User\Desktop\dsh-code-server-app\dsh-code-server-app-0.2.1.tgz
234
- ```
235
-
236
- Ready to use immediately — **no second step, no "Install environment", no install-guide modal**.
237
- The main package is only **~110KB** (the plugin's own code plus the launcher); everything else is dependencies:
238
-
239
- - **the VS Code tree** (`lib/vscode` 196.9MB + `out/browser` + `src/browser`) is a **platform-independent package**
240
- `@jinsiyu/dshcs-vscode-server@<code-server version>` declared in the plugin's `dependencies`; it runs from
241
- `<profile>\node_modules\@jinsiyu\dshcs-vscode-server\vscode` (the legacy full tree at
242
- `@jinsiyu/dshcs-code-server/code-server` is still recognised as a fallback);
243
- - the **pure-JS part** of VS Code's inner dependencies (35 packages: xterm / katex / typescript / ws / tar …) is
244
- declared in the plugin's `dependencies` and installed by pnpm into the profile's `node_modules` (hoisted);
245
- - the **binary part** comes entirely from `@jinsiyu/dshcs-*` platform packages: 16 native packages mapped back to their
246
- **original names** (`node-pty` / `@vscode/sqlite3` / `@vscode/spdlog` / …) by the **platform aggregator**
247
- `@jinsiyu/dsh-code-server-runtime-win32-<arch>` using `npm:` aliases; the aggregators sit in the plugin's
248
- `optionalDependencies`, so pnpm auto-selects the right platform;
249
- - consequently the dependency graph contains **no package with pre/install/postinstall or a `binding.gyp`** →
250
- no profile `allowBuilds`, no build script ever runs, and **the user machine needs no C++ toolchain**;
251
- - **upgrading the plugin no longer re-downloads the tree**: the tree package is cached by version
252
- (~60MB, ~197MB unpacked).
253
-
254
- ### Install mechanism (why it is built this way)
255
-
256
- - **The pnpm 11 hard constraint**: any package in the dependency graph whose manifest has
257
- `preinstall|install|postinstall` (or that ships a `binding.gyp`/`.hooks`) counts as "needs building" and must be
258
- approved by the **host profile's** `pnpm-workspace.yaml` via `allowBuilds`, otherwise `dsh plugin add` exits 1 with
259
- `[ERR_PNPM_IGNORED_BUILDS]`. A dependency's own `pnpm.allowBuilds`, `.npmrc`, `patch:` protocol and
260
- `optionalDependencies` do not help (measured 2026-09, pnpm 11.25);
261
- - **the tree** is prepared at pack time with `npm install code-server@<version> --ignore-scripts` (skipping the official
262
- `sh ./postinstall.sh`, which cannot run on Windows), then `scripts/vendor-vscode-server.mjs` keeps **only the VS Code
263
- tree**: `lib/vscode/**`, `out/browser/**`, `src/browser/**` plus the license files are copied to `vendor/vscode/`, and a
264
- generated root `package.json` records the upstream code-server version. code-server's own `out/node/**` and its 136
265
- runtime dependencies **no longer ship** they are replaced by `lib/launcher.mjs`;
266
- - **the packages that need a toolchain** are repacked into `@jinsiyu/dshcs-*` by `scripts/vendor-repacks.mjs`:
267
- the compiled package directory is copied and its `scripts` / `files` / `binding.gyp` / `.hooks` / `.npmignore` are
268
- **removed** (the built `.node` and every runtime file stay) sibling packages in its dependency list become `npm:`
269
- aliases platform-specific ones get `os`/`cpu` plus a `-<platform>-<arch>` suffix. For win32 targets the script also
270
- verifies each `.node` PE machine (0x8664=x64 / 0xaa64=arm64) so a cross-compiled artifact cannot ship the wrong arch;
271
- - **the platform aggregator** maps those repacks back to their original names (e.g.
272
- `"node-pty": "npm:@jinsiyu/dshcs-node-pty@1.2.0-beta.15"`), so VS Code's `import('node-pty')` needs no change; the
273
- aggregator is itself `os`/`cpu` gated, and the plugin declares both win32-arm64 and win32-x64 in
274
- `optionalDependencies`, so one command picks the right one;
275
- - **resolution path**: the host finds the tree with `require.resolve('@jinsiyu/dshcs-vscode-server/package.json')`
276
- (then the inner `vscode/` directory) and the entry is `vscode/lib/vscode/out/server-main.js`; VS Code's inner deps are
277
- resolved upwards from that root (`vscode/lib/vscode/node_modules` → package `node_modules` → `<profile>/node_modules`).
278
- The legacy full tree (`@jinsiyu/dshcs-code-server/code-server`) is still recognised as a fallback;
279
- - **runtime layout self-healing** (`ensureRuntimeLayout()` in `lib/native.js`, idempotent, run **at activation before
280
- `envCheck` and again before every start**): the host adds two kinds of **junctions** (Windows junctions / POSIX dir
281
- symlinks) into the tree:
282
- 1. `ensureAliasLinks()`: re-links the native aliases the aggregator carries into `<tree>/node_modules` pnpm nests
283
- `os`/`cpu`-gated packages under the aggregator's own `node_modules`, and `lib/vscode/out/server-main.js` uses
284
- **ESM imports** (ESM ignores `NODE_PATH`), so a missing link means an immediate 500;
285
- 2. `ensureInnerModuleLinks()`: restores VS Code's **inner dependency directories**
286
- `lib/vscode/node_modules` and `lib/vscode/extensions/node_modules` from the two `package.json` files the trimmed
287
- tree ships neither, and code that builds dependency paths explicitly (e.g. the bundled TypeScript extension looking
288
- for `<ext>/../node_modules/typescript/lib/tsserver.js`) otherwise reports
289
- "VS Code's tsserver was deleted by another application…" (measured with 1.136.1).
290
- > **Size note**: the plugin tarball is **~110KB**; `@jinsiyu/dshcs-vscode-server` is **~60MB** (~197MB unpacked);
291
- > the 16 native packages add ~250MB. A full install downloads roughly 310MB. Neither `vendor/` nor `repack/` is committed to git (see `.gitignore`).
292
-
293
- > **Upgrading from 0.1.43**: the tree package changed from `@jinsiyu/dshcs-code-server` (the full code-server tree with
294
- > `out/node` and 136 runtime deps) to `@jinsiyu/dshcs-vscode-server` (the trimmed tree). **The new code defaults to
295
- > `serve: loopback`, which behaves exactly like 0.1.43**; switch to `serve: dsh` for same-origin mounting. The install
296
- > command is unchanged (`dsh plugin --profile web add dsh-code-server-app@<version>`), and pnpm drops the old
297
- > `dshcs-code-server` sub-package.
298
- ### Development: install from source (changes take effect immediately)
299
-
300
- ```powershell
301
- dsh plugin --profile web add C:\Users\User\Desktop\dsh-code-server-app
302
- ```
303
-
304
- > A source path installs via `link:`. On a dev machine without `vendor/code-server`, run
305
- > `pnpm run vendor:vscode -- --dev-links` first. Dependencies (inner JS deps + the platform aggregator) are installed by pnpm
306
- > too but the not-yet-published local `@jinsiyu/*` packages must either be published first, or the
307
- > `repack/tgz/*.tgz` files must be installed into the profile as `file:` dependencies.
308
- >
309
- > **Changing the client bundle**: edit `src/factory.js` then run `pnpm run build:client`
310
- > to regenerate `lib/client.js` (that artifact is not tracked; a browser refresh picks it up — no host restart needed).
311
- > Window animations are driven by the embedded `motion`; feel parameters live in `winPhysics` (one spot) in `src/factory.js`.
312
-
313
- ### Pack-machine environment (the user machine needs nothing)
314
-
315
- **A toolchain is needed at pack time only — never on the user machine.**
316
-
317
- | Env | Version / requirement | User machine | Pack machine |
318
- |---|---|---|---|
319
- | Node.js | **v24.x** (latest code-server requirement; v24.13.1 here) | required | required |
320
- | npm / pnpm | npm ships with Node; pnpm comes from DSH | required (installs deps) | required |
321
- | **MSVC build tools** | **VS Community 2026 + C++ desktop workload** | **not needed** | pack time (16 native packages) |
322
- | **VS Spectre-mitigated libs** | one set for ARM64 **and** one for x86/x64 ("MSVC v14x Spectre-mitigated libs") | not needed | pack time (otherwise MSB8040) |
323
- | Python | **3.13.x** | ❌ not needed | pack time (node-gyp) |
324
- | node-gyp | **13.x** (older versions don't recognize VS 2026) | ❌ not needed | pack time |
325
-
326
- > **Packing still works without the Spectre libs**: when a package fails to compile, `vendor-repacks.mjs` downgrades
327
- > `SpectreMitigation` to `false` in that architecture's `*.gyp` files and retries (only the Spectre hardening is
328
- > lost, functionality is unaffected) and says so in the log.
329
-
330
- ### Windows native build notes (pack time, verified locally ARM64)
331
-
332
- - **VS needs the Spectre-mitigated libraries** (MSB8040): Visual Studio Installer → Individual components →
333
- "MSVC v14x Spectre-mitigated libs" — **install the ARM64 and the x86/x64 sets separately**.
334
- - **node-gyp 13.x** (9.x does not recognize VS 2026): `npm install -g node-gyp@latest`.
335
- - **x64 cross-compiling**: `vendor-repacks.mjs` uses `npm install --os=win32 --cpu=x64 --ignore-scripts` to fetch
336
- the packages, then `npm rebuild --arch=x64` per package; the resulting PE machine types were verified
337
- (kerberos / sqlite3 / spdlog …).
338
- - Latest code-server requires **Node v24**.
339
- - If you don't need the self-contained install (e.g. a global code-server already exists), skip it:
340
- the plugin falls back to a configured/PATH `bin` (see the "Config" table).
341
-
342
- ### Upgrading the VS Code tree (upstream = a code-server release)
343
-
344
- - **The version is decided at pack time**: `pnpm run vendor:latest` (= `--force`) pulls the tree of the npm **latest**
345
- release; or use `pnpm run vendor:vscode -- --version 4.136.2` / `DSHCS_CODE_SERVER_VERSION`.
346
- With an existing `vendor/vscode`, a plain `pnpm pack` never upgrades (it is a no-op).
347
- - **Check first**: `pnpm run vendor:check` prints the bundled version / upstream latest.
348
- - **A version bump means rebuilding and republishing the sub-packages** (all with the same script):
349
- 1. `pnpm run repack:build -- --target win32-arm64,win32-x64 --pack` the new tree package
350
- (`@jinsiyu/dshcs-vscode-server@<new version>`) and the natives rebuilt against the new inner dependencies (the
351
- script also rewrites the plugin's pure-JS `dependencies` and both aggregator versions);
352
- 2. `pnpm run republish:repacks` (`pnpm run publish:repacks`) publish; then bump the plugin version `pnpm pack`
353
- publish the plugin.
354
- - `productPath` (`<quality>-<commit>`, part of the client WebSocket path) is **computed from `lib/vscode/product.json`**,
355
- so upgrading the tree needs no code change — but the routes are registered at activation, so restart `dsh web` afterwards.
356
- - **No runtime auto-upgrade anymore**: nothing fetches latest at startup; the version is fully determined by the bundled artifact.
357
- - Bundled locally right now: the tree of `code-server@4.136.2` (VS Code 1.136.1, `productPath=stable-8d5f383f…`).
358
-
359
- ### Compatibility with the old install locations
360
-
361
- Host probe order: `@jinsiyu/dshcs-vscode-server/vscode` (**the real layout since 0.2.0**) >
362
- `@jinsiyu/dshcs-code-server/code-server` (the full tree, 0.1.40–0.1.43) >
363
- `@jinsiyu/dshcs-code-server-<platform>-<arch>/code-server` (the 0.1.37 platform sub-packages) >
364
- the in-package `vendor/vscode` > the in-package `vendor/code-server` (development). The old install root
365
- `<profile>\.code-server-app` is only mentioned in a startup log line; nothing writes to it any more.
366
- ## Settings card (Settings Plugins Code Server)
367
-
368
- Modeled after dsh-auto-open-web's custom card, registered on the `settings.plugin.item` slot,
369
- persisted via the official settings domain (`settingsScope`, namespace `code-server`) into the official settings document:
370
-
371
- | Key | Default | Description |
372
- |---|---|---|
373
- | `fileOpenScope` | `session` | **Claim scope** (0.2.5): `session` claims only session-scoped file addresses (`dsh-resource://file/session/…` — DSH's produced files, declared deliveries, prose mentions); `all` also claims session-less `…/file/absolute/…`. Unclaimed addresses fall back to DSH's built-in preview |
374
- | `keepResident` | `true` | **Resident in background**: on, the host preloads the IDE into a parked surface right after start — switching tabs or collapsing the sidebar never reloads it and the first open needs no cold start; off loads it only when the panel is opened (saves memory) |
375
-
376
- (Since 0.2.6 the card keeps only those two settings; `windowedOpen` and `reserveComposer` are goneleftover keys in an old
377
- settings document neither fail nor apply. `serve` remains a key in the settings namespace (usable from a settings document) but
378
- has **no card row**see "Serving mode".)
379
-
380
- > Card changes take effect immediately via `scope.watch` (the host status API returns `keepResident` and
381
- > `fileOpenScope`; the client applies them at once); no dsh restart needed. **After adding new setting keys, restart dsh web before first use**,
382
- > so the host re-registers the settings namespace (schema includes the new key); otherwise save/validation of the new key won't work.
383
-
384
- The bottom of the card is **Environment check** (click "Check environment" to read the host `status.env`): entry,
385
- the tree version / `productPath` / server entry, VS Code inner dependencies, and **prebuilt native packages** (platform aggregator name +
386
- resolved module count). Since 0.1.36 there is no "Install environment" button dependencies are installed by the
387
- package manager, and the card only reports the result.
388
-
389
- ## Config (`config` in cordis.patch.yml; all have defaults)
390
-
391
- | Key | Default | Description |
392
- |---|---|---|
393
- | `bin` | `code-server` (placeholder) | Launch priority: explicit `bin` in config > the tree package `@jinsiyu/dshcs-code-server/code-server/out/node/entry.js` > the old platform sub-packages `@jinsiyu/dshcs-code-server-<platform>-<arch>` > the in-package `vendor/code-server` > the old install root `.code-server-app` > plugin-internal `node_modules` > `code-server` on PATH. None present → startup error with troubleshooting hints |
394
- | `host` | `127.0.0.1` | Bind address; `auth: none` only allows loopback (localhost/127.0.0.1/::1) |
395
- | `port` | `8090` | Port; on conflict startup fails with diagnostics (no automatic port change) |
396
- | `auth` | `none` | `none` \| `password`; non-loopback host automatically requires password |
397
- | `passwordToken` | `''` | Token for password mode (passed to code-server via the `PASSWORD` env var) |
398
- | `userDataDir` | `$DSH_HOME/code-server/user-data` | User-data isolation directory |
399
- | `extensionsDir` | `$DSH_HOME/code-server/extensions` | Extensions directory |
400
- | `readyTimeoutMs` | `60000` | `/healthz` readiness probe timeout |
401
-
402
- User-level override example (write in `$DSH_HOME/profiles/web/cordis.patch.yml`, using the `- id: code-server` row):
403
-
404
- ```yaml
405
- - id: code-server
406
- config:
407
- port: 8091
408
- # Explicit (overrides dependency-install probing): a globally installed shim, or any entry.js
409
- bin: C:\Users\User\AppData\Roaming\npm\code-server.cmd
410
- ```
411
-
412
- ## JSON API (same-origin fetch; identical paths in web and desktop)
413
-
414
- **No `webServer` dependency**: the host half registers its routes on DSH Connection's shared `/api` channel through
415
- `ctx.connection.fetch.register`. In the web profile Connection mounts the `/api` prefix on webServer itself (with the
416
- Host/Origin fence and browser auth); in the desktop profile `apps/desktop-host` feeds `/api/*` into the same
417
- `createSharedFetchHandler('/api')` (IPC framed pipe, no HTTP server). The client only writes relative paths
418
- (`fetch('/api/code-server/<op>')`), so both carriers behave identically.
419
-
420
- | Method | Path | Description |
421
- |---|---|---|
422
- | GET | `/api/code-server/status` | `{ ok, running, status, host, port, pid, cwd, url, version, error, logTail, adopted }` (also `env` environment check and the `setup` compatibility field) |
423
- | POST | `/api/code-server/start` | body `{ cwd? }` (omit cwd to keep the current workspace); idempotent |
424
- | POST | `/api/code-server/stop` | Stop and recycle the process tree |
425
- | POST | `/api/code-server/setup` | **Compatibility no-op**: since 0.1.36 dependencies are installed by the package manager, so this only re-runs the env self-check and returns |
426
- | POST | `/api/code-server/open-file` | body `{ file }` — writes the signal consumed by the built-in `dshcs-open-file` extension to open the file in code-server |
427
-
428
- > The plugin no longer registers `/code-server/*` webServer-only routes, and the code-server icon is inlined as a data URI
429
- > in the client bundle — the client requests no plugin-owned HTTP resource at all.
430
-
431
- ## DSH Desktop (no webServer)
432
-
433
- - The host half is `inject = ['connection', 'settings']` (**no `webServer`**) — the desktop profile disables webserver/web-runtime
434
- and the plugin still works: `/api/*` requests travel Electron `dsh-app://` protocol handler → IPC framed pipe → `createSharedFetchHandler('/api')`.
435
- - The right-sidebar tab, guide entry box, file-address claim, and settings card behave the same as in web (code-server remains an
436
- iframe to the local `http://127.0.0.1:<port>`; the desktop renderer uses `webSecurity: true` with no CSP, so the cross-origin iframe loads).
437
- The desktop build ships `dsh-client-ui-sidebar-right` in its seed package set as well, so the 0.2.3 "right-sidebar DSH only" rule is
438
- not a regression for desktop; the only difference is the missing `webServer`, where `serve: dsh` falls back to loopback.
439
- - Install into the desktop profile through the **desktop plugin manager** (not the CLI, see below).
440
- - **Desktop installs face a 24-hour supply-chain policy (measured 2026-09-10; this is how 0.2.4 got installed)**:
441
- - the CLI path is unavailable: `dsh plugin --profile desktop …` is rejected (*"profile "desktop" is managed exclusively by the
442
- Electron application"*), so desktop installs only go through the app's package transaction (`pnpm add <spec> --save-exact`,
443
- executed in `~/.dsh/desktop/staging/<uuid>/profile` before activation);
444
- - that transaction's pnpm (the app bundles **11.7.0**, patched by DeepSeek) runs a **lockfile supply-chain verification** before
445
- `add` ("Verifying lockfile against supply-chain policies (717 entries)") which requires packages to be **at least 24 h old**,
446
- otherwise `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`;
447
- - **the two stages behave differently (measured)**:
448
- - verifying an **existing lockfile**: `minimumReleaseAgeExclude` is *not* honoured (exact versions and bare package names
449
- were both tried);
450
- - **resolution** (no lockfile to verify, e.g. after `pnpm clean --lockfile`): the list *is* honoured, and pnpm even appends
451
- entries itself (the install log prints *"Added N entries to minimumReleaseAgeExclude…"*);
452
- - so the working recipe for a **just-published** (<24 h) version on desktop is to start from a clean, lockfile-free profile:
453
- 1. `pnpm clean --lockfile` (**note: it also deletes `node_modules`**, leaving the profile to be reinstalled);
454
- 2. with the app's bundled runtime, run `add <spec> --save-exact --trust-lockfile` in the profile directory
455
- (runtime/store/config live under `~/.dsh/desktop/pnpm/{store,cache,state,config,home}`,
456
- `--config.userconfig=…/config/npmrc`, otherwise pnpm fails with `ERR_PNPM_UNEXPECTED_STORE` /
457
- `…UNEXPECTED_VIRTUAL_STORE`);
458
- 3. the app's boot command (`install --offline --frozen-lockfile --trust-lockfile`) then passes (lockfile matches
459
- package.json, packages are in the store); if the plugin name is already in `dsh.profile.bundles` nothing else is needed.
460
- - do **not** try to bypass it with `minimumReleaseAge: 0`: it does clear the check, but that key is **not** one of the policy
461
- sections the app tolerates (`project-manager.ts` ignores only `minimumReleaseAgeExclude:` / `trustPolicyExclude:` and validates
462
- the manifest before every `mutate()`), so persisting it makes the app fail with
463
- *"core package mapping does not match desktop-packages.json"*.
464
- - Alternatively **wait out the 24 h** and install normally from the plugin manager. The web profile is unaffected: its
465
- `pnpm-workspace.yaml` sets `minimumReleaseAge: false`.
466
- - this plugin's closure contains platform native sub-packages (`@jinsiyu/dsh-code-server-runtime-win32-*`) published together with
467
- the plugin itself, so every new version hits that policy on desktop.
468
- - **Desktop client bundles are cached by Electron; restarting the app does not guarantee a new one** (measured 2026-09, hit while shipping 0.2.5):
469
- - symptom: `lib/client.js` in the profile is the new version, yet the renderer keeps running the old code —
470
- `%APPDATA%\@deepseek-ai\dsh-desktop\Code Cache\js` only contains strings unique to the old version (e.g. `dshcs-artifacts`)
471
- and none unique to the new one (`fileOpenScope`), and `Cache\` still holds an old response body referencing
472
- `dsh-code-server-app`. The same applies to first-party plugins (the cached `ui-deliverables` even lacks the current
473
- `data-presented-files-row` marker);
474
- - diagnosis (byte level do **not** use `Select-String`, which reads files with the console encoding and gives false
475
- negatives on non-ASCII markers): search `Code Cache\js` for an **ASCII** marker unique to the new version
476
- (ours is `fileOpenScope`); a hit proves the new bundle really was compiled;
477
- - fix: fully close the app, delete the `Cache`, `Code Cache` and `GPUCache` directories, then start it (cache only
478
- profiles, sessions and settings are untouched):
479
- ```powershell
480
- Remove-Item -Recurse -Force "$env:APPDATA\@deepseek-ai\dsh-desktop\Cache","$env:APPDATA\@deepseek-ai\dsh-desktop\Code Cache","$env:APPDATA\@deepseek-ai\dsh-desktop\GPUCache"
481
- ```
482
- - scope: this is not specific to this plugin — **any** client plugin may keep running old code after an upgrade;
483
- after a release, confirm with the marker trick above that the renderer actually swapped bundles.
484
-
485
- ## File-open plumbing (what the plugin itself still does)
486
-
487
- DSH's own chips and preview buttons are what users click (see "Opening files" above); this plugin adds no row of its own.
488
- What remains on the plugin side:
489
-
490
- - the tab body parses `navigation.address` and posts the absolute path (plus an optional `line`) to
491
- `/api/code-server/open-file`, which writes a signal file;
492
- - the bundled `dshcs-open-file` extension polls that file and calls `showTextDocument` VS Code Web has no official
493
- "open this file from outside" API, so this is the only way to aim the workbench at a file. It is installed as a
494
- **built-in** extension (in `lib/vscode/extensions`), so users cannot remove it from the extensions panel, and the
495
- installer re-syncs it whenever its content changes.
496
-
497
- ## Known limitations
498
-
499
- - ~~No sub-path~~ **no longer true (corrected with measurements in 0.2.0)**: the workbench HTML VS Code renders references
500
- **only relative URLs** (9 references measured, 0 absolute; `serverBasePath="."`, `rootEndpoint="."`), and the client
501
- builds its WebSocket path from `location.pathname + join(serverBasePath ?? '/', <quality>-<commit>)`. The IDE can
502
- therefore be mounted directly under DSH's own `/code-server/*` (`serve: dsh`) — no second port, no HTML rewriting.
503
- Item-by-item evidence: `docs/analysis-code-server-as-dsh-plugin.md`.
504
- - **`serve: dsh` cannot proxy forwarded-port WebSockets**: `registerUpgrade` matches exact paths while `/proxy/:port`
505
- carries the port in the path, so WebSocket forwarding for the Ports panel is unavailable in that mode (HTTP forwarding
506
- works). Use `serve: loopback` when you need it.
507
- - **`serve: dsh` shares DSH's origin**, so the iframe is not sandboxed there (same-origin plus `allow-same-origin` is
508
- escapable by the frame itself); in `loopback` mode the iframe is cross-origin and `sandbox` stays as real protection.
509
- - **Single instance across sessions**: one shared IDE per host; switching cwd requires a restart (the sidebar tab
510
- handles it and hints).
511
- - **Older DSH versions are unsupported (since 0.2.3)**: on a DSH without `sidebarRightTabs` / `sidebarRight` the plugin
512
- offers nothing but an upgrade notice on the settings page; older-DSH users should stay on `0.2.2`
513
- (`dsh plugin --profile web add dsh-code-server-app@0.2.2`).
514
- - **Sidebar tab switching** (no longer reloads since 0.2.2): DSH's right sidebar renders only the active tab's body, and
515
- a React unmount moves the iframe away; the plugin keeps it as a singleton resident surface and shuttles it between the
516
- dock slot and a document-level park container with `Element.moveBefore()` (a state-preserving atomic move), so
517
- switching tabs or collapsing the sidebar and back **does not reload** it. Browsers without `moveBefore` fall back to
518
- the old behaviour (`appendChild` full reload), reported as `degraded`; see "Why switching tabs no longer reloads".
519
- - **Remote access**: with `serve: dsh` the browser only needs to reach DSH itself (one port, protected exactly like `/api`);
1
+ # dsh-code-server-app — Integrate code-server (VS Code in the browser) into DSH
2
+
3
+ > Source repository: see `repository` / `homepage` in `package.json`.
4
+
5
+ > ## ⚠️ Extension Marketplace Note (important)
6
+ >
7
+ > - **code-server's extension store is [Open VSX](https://open-vsx.org/), not the Microsoft Visual Studio Marketplace**;
8
+ > - Microsoft's Marketplace terms **prohibit third-party products (including code-server) from using its API**, so code-server cannot query Microsoft's extension list;
9
+ > - As a result, Microsoft **commercial/proprietary** extensions (e.g. **GitHub Copilot, the Remote series like Remote-SSH, Azure tools, IntelliCode**) are **not available** in the store — this is Microsoft's distribution policy, not a defect;
10
+ > - Microsoft **open-source** extensions (Python, TypeScript debugger, ESLint, …) are mirrored on Open VSX and install normally by search;
11
+ > - **If you need a proprietary Microsoft extension**: download the `.vsix` from the Marketplace page and install it manually with `code-server --install-extension <file>` (or drop it into `--extensions-dir`).
12
+
13
+ A static profile plugin (npm package with host + client bundle) that ships the **VS Code server tree** from a [code-server](https://github.com/coder/code-server) release as a **platform-independent dependency package** (pack-time artifact `vendor/vscode` → `@jinsiyu/dshcs-vscode-server`, no install scripts, no postinstall). The code-server **Node service layer is replaced by the plugin's own `lib/launcher.mjs`**: it drives `<tree>/lib/vscode/out/server-main.js` (`loadCodeWithNls()` / `createServer()` / `handleRequest()` / `handleUpgrade()`) directly and re-adds the few HTTP endpoints code-server used to provide (`/healthz`, `/manifest.json`, `/_static/*`, `/proxy/:port`). The 16 native modules (node-pty / @vscode/sqlite3 / spdlog / …) come from `@jinsiyu/dshcs-*-win32-<arch>` platform packages selected automatically per architecture by the platform aggregator. VS Code's inner dependencies and the prebuilt native modules are **all installed by the package manager together with the plugin** — no global npm install, no `bin` configuration, no profile config changes, no second install command, **no argon2/C++ toolchain**.
14
+
15
+ ## UI carrier and required DSH version (0.2.3: right-sidebar DSH only)
16
+
17
+ | DSH version | Carrier | Entry points |
18
+ |---|---|---|
19
+ | **>= 0.1.5-alpha.1** (has `sidebarRight` / `sidebarRightTabs`) | **Right-sidebar tab** (kind `code-server`, chip `Code Server`), which also **claims file addresses** (see below) | ① DSH's own **produced-file chips / presented-file card previews / inline file names in prose** (since 0.2.5, via the official `openFile` → file address → this tab); ② the **Code Server box** on the sidebar's guide ("开始") page; ③ Settings → Plugins → Code Server → **"Open in right sidebar"** |
20
+ | older (no sidebar service) | **Unsupported**: nothing but one notice on the settings page | none (Settings → Plugins → Code Server shows an upgrade notice) |
21
+
22
+ - Detection: first a synchronous `ctx.get('sidebarRightTabs') / ctx.get('sidebarRight')` probe; because the services may come up after this plugin, `ctx.inject(['sidebarRightTabs','sidebarRight'], …)` is awaited and a **2.5 s timeout marks the DSH as legacy** (no version comparison, and the plugin's own activation is never blocked).
23
+ Since 0.2.4 that verdict is **reversible** and registration no longer relies on `ctx` property access (which on desktop silently skipped registration — the symptom was "settings card looks normal but the sidebar has no entry"):
24
+ - services are looked up as `ctx.<name>` first and `ctx.get(name)` second, so either context shape registers;
25
+ - when the sync probe already sees the services but `inject` never calls back, registration falls back to the sync services after **1.5 s**;
26
+ - at 2.5 s only the settings notice appears; only after **10 s** does the client tell the host to recycle/stop prestarting (so a slow host is not punished);
27
+ - services arriving late automatically revoke the legacy verdict, register the sidebar, and report `{sidebar:true}` so the host re-enables;
28
+ - a failed registration is no longer silent: it logs an error and the card's entry row says "right-sidebar services were found but the tab could not be registered".
29
+ - **0.2.3 dropped legacy-DSH compatibility**: the floating ball and the internal floating window are **deleted**. When the DSH is detected as legacy the plugin
30
+ - registers only the settings card (an upgrade notice) — no ball, no floating window, **no file-address claim**, no IDE preload;
31
+ - reports `/api/code-server/ui-mode { sidebar:false }` to the host (after the 10 s grace above); the host then **recycles an instance it auto-prestarted** and stops prestarting (a user-started/adopted instance is never touched), and `{sidebar:true}` reverses that if the services show up later;
32
+ - upgrading DSH needs **no reinstall** — refresh the page and the card turns back into the full settings card.
33
+ - The sidebar tab hosts the code-server page (iframe) and follows the current session workspace; the panel can be collapsed/split/floated/fullscreened by DSH's right sidebar.
34
+ - **Resident IDE (0.2.2, on by default)**: switching to another tab or collapsing the sidebar and coming back **no longer reloads** code-server — unsaved editor buffers, terminals and debug sessions all stay put (see "Why switching tabs no longer reloads" below).
35
+ - The settings card has exactly **two settings**: "**Claim scope**" and "Resident in background" no other rows (0.2.7 removed the "Entry", "dependency install" and "environment check" rows).
36
+ Open the IDE from the **Code Server box** on the sidebar's guide page, or by clicking DSH's own produced-file chips / delivered-file previews / inline file names;
37
+ diagnostics stay out of the UI — the `[code-server]` lines in the DSH host log are the place to look (`/api/code-server/status` still returns `env` for scripts).
38
+ The old `windowedOpen` (open in a window) and `reserveComposer` were **removed in 0.2.6**: leftover keys in an old settings document neither fail nor apply (they are no longer part of the schema).
39
+ To use the IDE in a browser tab, visit the loopback address `http://127.0.0.1:<port>/` (or DSH's `/code-server/` under `serve: dsh`).
40
+
41
+ ## Opening files (official entry points since 0.2.5)
42
+
43
+ DSH names files with **resource addresses**; `openFile` only hands the address to the right sidebar, which decides who draws it:
44
+
45
+ ```
46
+ DSH's produced-file chip / presented-file card preview / inline prose mention
47
+ openFile(path, { line? }) (provided by ui-chat)
48
+ dsh-resource://file/session/<sessionId>/<path> (or …/file/absolute/<path>)
49
+ ctx.sidebarRight.openResource(address)
50
+ → claimed by the tab type whose patterns match (band extension(3) > builtin(2) > fallback(1),
51
+ then the longest matching pattern, then registration order)
52
+ ```
53
+
54
+ This plugin registers:
55
+
56
+ | Field | Value | Effect |
57
+ |---|---|---|
58
+ | `patterns` | `['dsh-resource://file/**']` | claims file addresses (a pattern containing `:` is matched against the **whole address**) |
59
+ | `priority` | `'extension'` | beats the built-in plain-text preview, which sits in `fallback` on purpose DSH's own comment calls that band "the position VS Code's text editor holds among its editors", i.e. one any more specific type should beat |
60
+ | `canOpen` | see below | vetoes by the "claim scope" setting; unclaimed addresses fall back to DSH's built-in preview |
61
+ | `title` | last address segment (= file name) | the tab chip shows the file name; a page tab (`sidebar://code-server`) still reads `Code Server` |
62
+
63
+ - **Claim scope** (switchable in the settings card, `fileOpenScope`):
64
+ - `session` (default) claim only `dsh-resource://file/session/…` (everything a session produces: deliverables, declared deliveries, prose mentions, tool views);
65
+ - `all` also claim session-less `dsh-resource://file/absolute/…` addresses.
66
+ - **How the tab body locates the file**: it parses `useTabInfo().tab.navigation.address`
67
+ (`src/address.js`, same grammar as DSH's `parseFileAddress`), expands a workspace-relative path with that
68
+ session's cwd, and posts the absolute path (plus optional `line`) to the host's
69
+ `/api/code-server/open-file`; the bundled extension (`dshcs-open-file`) then calls `showTextDocument`
70
+ (positioned at the line when given).
71
+ - **One address = one tab** (DSH semantics: `contentId` *is* the address): three files mean three chips, but they
72
+ share the single resident workbench switching tabs just re-aims the workbench at the corresponding file.
73
+ - **Why the bundled extension stays**: VS Code Web has no official "open this file from outside" API (the only
74
+ entry is `?folder=`, which picks the workspace), so aiming the workbench at a file has to be done by an
75
+ extension inside the tree. The host writes a signal file, the extension polls it and calls
76
+ `showTextDocument`, keeping the signal for retry when no window is connected yet.
77
+
78
+ ## Why switching tabs no longer reloads (resident IDE)
79
+
80
+ **The old trap**: DSH's right sidebar (ui-dockkit) renders **only the active tab's body**
81
+ (`TabPanel.tsx:412` `renderTab(active)`) — switching to another tab unmounts that body in React, which moves the
82
+ iframe out of the document and destroys its browsing context; switching back is a full VS Code reload (unsaved buffers
83
+ lost). Floating the tab into its own panel only worked around it.
84
+
85
+ **What it does now (`src/surface.js` in the client, 0.2.2)**: the plugin takes the iframe **away from React** and turns
86
+ it into a **singleton resident surface**:
87
+
88
+ | Situation | Action | Result |
89
+ |---|---|---|
90
+ | tab becomes active | `host.moveBefore(frame, null)` into the visible dock slot | state-preserving atomic move, **no reload** |
91
+ | tab deactivates / sidebar collapses | move back into a document-level park container (offscreen, keeps last docked size, `inert` + `aria-hidden`) | never destroyed, keeps running in the background |
92
+ | workspace / port changes | assign `src` explicitly | the only normal "reload" entry point |
93
+
94
+ - **Why `moveBefore`**: measured in a real browser (Edge/Chromium 151), a plain `appendChild` move resets the iframe's
95
+ internal timers (i.e. reloads it), while `Element.moveBefore()` (Chromium ≥133) preserves state (a probe counter keeps
96
+ counting 1→2).
97
+ - **Degradation is never silent**: when `moveBefore` is missing, or the host was already detached by React and it throws
98
+ `HierarchyRequestError: invalid hierarchy` (passive effect cleanup runs after DOM removal), the code falls back to
99
+ `appendChild` one reload, but the frame is **never lost** and reports `degraded` / `lastMoveError` so the UI can
100
+ say "residency unavailable".
101
+ - **Repaint fallback (measured)**: in the real GUI the surface was seen once with correct size, hit testing and
102
+ `visibility` that simply **stopped repainting** (a fully white panel, byte-identical screenshots proving no new frame).
103
+ `translateZ(0)` and `opacity` nudges did nothing; `display:none forced reflow restore` inside a single JS task
104
+ restored it without reloading the iframe document, without losing internal state and without a visible flash.
105
+ **The trigger could not be reproduced**: in a probe page an offscreen `moveBefore` park of 337 s (past Chrome's
106
+ ~5 min cross-origin throttle window) followed by a dock with the fallback disabled still painted normally. It is
107
+ therefore kept as a **fallback**: every park→dock transition runs one `nudgeRepaint()` (counted as
108
+ `surfaceSnapshot().nudgeCount`; `setNudgeEnabled(false)` A/Bs it live).
109
+ - **Warm-up**: with `keepResident` (default `true`) the host builds the surface right after plugin start and leaves it
110
+ parked, so the first tab open needs no cold start; preloading never yanks a surface that is currently docked.
111
+ - **Debug handle**: `window.__dshcsSurface` (`snapshot()`, `setParkStrategy('offscreen'|'behind')`, `dock()`, `park()`,
112
+ `nudge()`, `setNudgeEnabled(false)`, `destroy()`).
113
+
114
+ **Measured** (DSH web GUI, real mouse clicks between sidebar tabs): switching away → `docked:false`, same iframe node,
115
+ in-frame probe still alive, `degraded:false`; switching back → `docked:true`, unchanged `src`, IDE pixels and editing
116
+ state preserved (no full reload). Full evidence and probe scripts: `docs/analysis-code-server-as-dsh-plugin.md`.
117
+
118
+ ## Serving mode (`serve`)
119
+
120
+ | Mode | What it does | Requires |
121
+ |---|---|---|
122
+ | **`loopback` (default)** | the plugin listens on its own loopback port (`host:port`) and the sidebar iframe connects cross-origin; the process can be adopted after a DSH host restart | nothing |
123
+ | **`dsh`** | the IDE is mounted on **DSH's own HTTP port** at `/code-server/*` (HTTP prefix route) plus `/code-server/<quality>-<commit>` (exact WebSocket route), forwarded to the launcher's **named pipe**; **no extra port**; every request (including the WS handshake) first passes `ctx.connection.requestRejection()` the same Host/Origin fence and browser-cookie authentication as `/api` | DSH providing `webServer` (web profile); desktop falls back to loopback automatically |
124
+
125
+ - Switch it in `config.serve` in `cordis.patch.yml` or in Settings Plugins Code Server (takes effect on the next start).
126
+ - Benefits of `dsh`: a single URL/port (remote access to DSH gives you the IDE), no extra loopback listener, authentication on par with DSH.
127
+ - Two **known trade-offs** of `dsh`: the iframe shares DSH's origin, so `sandbox` is dropped there (same-origin plus
128
+ `allow-same-origin` is escapable by the frame itself; in `loopback` mode the iframe is cross-origin and `sandbox` stays
129
+ as real protection clipboard is still granted via `allow="clipboard-read; clipboard-write"`); and forwarded-port
130
+ **WebSockets** cannot be routed because `registerUpgrade` matches exact paths while `/proxy/:port` carries the port in
131
+ the path (HTTP forwarding works; use `loopback` when you need WS forwarding).
132
+
133
+ - In `loopback` mode every upgrade passes a **code-server-equivalent Origin check** (since 0.2.1): when an `Origin`
134
+ header is present its host must equal `Host` (honouring `Forwarded: host=` / `X-Forwarded-Host`, like code-server),
135
+ otherwise the handshake gets `403`; non-browser requests without `Origin` are allowed. Without that check any local
136
+ browser page could complete a handshake against `ws://127.0.0.1:<port>/stable-<commit>` and drive the IDE.
137
+
138
+
139
+ ## Legacy DSH (unsupported since 0.2.3)
140
+
141
+ **Behaviour**: when `sidebarRightTabs` / `sidebarRight` cannot be found, the plugin registers a single settings card:
142
+
143
+ > **Code Server** this DSH version is unsupported (no right-sidebar service)
144
+ > Since 0.2.3 this plugin no longer supports older DSH versions.
145
+ > The right-sidebar plugin services `sidebarRightTabs` / `sidebarRight` were not detected, so the plugin exposes no
146
+ > entry point at all (the old floating ball and floating window have been removed) and will not start the IDE in the
147
+ > background. Upgrade DSH to a version with the right sidebar (>= 0.1.5-alpha.1): Code Server then appears as a
148
+ > right-sidebar tab, this page shows the full settings again, and no reinstall is needed a page refresh is enough.
149
+
150
+ - **No other UI**: no `shell.overlay` registration (floating ball), no file-address claim, no resident preload.
151
+ - **Host side**: the client posts `/api/code-server/ui-mode { sidebar:false }`; the host then stops auto-prestarting
152
+ the IDE (`maybePrestart` returns immediately) and **recycles** an instance it had just auto-prestarted (unless it
153
+ was adopted), so no unusable IDE process or port is left behind. A user-started/adopted instance is never stopped.
154
+ - **Why delete instead of keeping**: the internal floating window was a stopgap from the era of early-2026 DSH builds
155
+ without right-sidebar services. The resident surface, clipboard handling, shortcuts and panel collapsing all build on
156
+ DSH's right sidebar, so maintaining two carriers costs more than it is worth. Older-DSH users should stay on `0.2.2`
157
+ (`dsh plugin --profile web add dsh-code-server-app@0.2.2`).
158
+
159
+ ## code-server workspace and process lifecycle
160
+
161
+ - code-server's workspace **follows the active DSH session/workspace**: switching sessions/workspaces while the IDE is open restarts code-server to the new directory
162
+ (resolution order: current session cwd session's workspace.path recentWorkspace.path first workspace.path);
163
+ the opened directory is shown inside code-server (`?folder=<cwd>`, the page reloads when following a switch);
164
+ implementation note: the iframe `src` must carry `?folder=<cwd>` code-server's front-end remembers the "last workspace" and restores it by itself;
165
+ a bare root URL only shows the previously opened directory and does not follow switches (verified locally).
166
+ **Windows path format (verified)**: the `folder` parameter must start with `/` and use forward slashes only, e.g. `/C:/Users/User/Desktop/biss`;
167
+ a bare Windows path (`C:\...`) is parsed as a URI scheme and the drive letter is stripped (page shows `\Users\User\...` with an empty file tree),
168
+ while `file:///C:/...` reports "Workspace does not exist".
169
+ - Process lifecycle is managed by the host plugin: startup writes `$DSH_HOME/code-server/pid.json`, stop kills the tree (`taskkill /T` or process-group SIGKILL),
170
+ crash/exit updates status live; after a DSH host restart the plugin **adopts** a still-running instance (verifies pid + `/healthz`), without duplicate start or killing unrelated processes;
171
+ - `node_modules` and the pack-time artifact `vendor/` are git-ignored; after cloning, follow
172
+ "Install the plugin (script-free install; code-server bundled)" below — `pnpm install` → `pnpm run build:client` →
173
+ `pnpm run vendor:vscode` `pnpm pack` + `dsh plugin --profile web add`.
174
+
175
+ > Verified locally (BM: Windows 11 ARM64): `code-server@4.136.2` (with Code 1.136.1) bundled in the plugin,
176
+ > placed offline at activation → VS Code internal deps installed → started → healthz 200 →
177
+ > cwd switch restart while running → stopped → fully recycled.
178
+
179
+ ## Packaging (how to build the tarball)
180
+
181
+ ```powershell
182
+ cd C:\Users\User\Desktop\dsh-code-server-app
183
+ pnpm install # dev deps (esbuild + motion); allowBuilds is explicit no postinstall runs
184
+ pnpm run build:client # src/factory.js lib/client.js (not committed; must be built first)
185
+ pnpm run vendor:check # optional: show the bundled tree version vs the latest code-server release
186
+ pnpm run vendor:vscode # produce vendor/vscode (the trimmed VS Code tree, ~197MB)
187
+ pnpm run repack:build -- --target win32-arm64,win32-x64 --pack # one script builds every sub-package
188
+ pnpm run publish:repacks # publish every @jinsiyu/* sub-package (default dist-tag: next)
189
+ pnpm pack # dsh-code-server-app-<version>.tgz (~107KB)
190
+ pnpm run publish:plugin # publish the plugin itself (default dist-tag: next)
191
+ # once the user has restarted dsh web and confirmed it works, promote latest:
192
+ pnpm run promote -- <version>
193
+ ```
194
+
195
+ > **dist-tag policy (mandatory)**: every release goes to **`next`** and **never touches `latest`**;
196
+ > `latest` always points at the most recent *confirmed bug-free* version and is only moved by
197
+ > `pnpm run promote -- <version>` (= `npm dist-tag add dsh-code-server-app@<version> latest`)
198
+ > **after the user restarts `dsh web` and confirms it works**. That way
199
+ > `dsh plugin add dsh-code-server-app` (no version) and anything else resolving `latest` never picks up an
200
+ > unverified build. Sub-packages (`@jinsiyu/dshcs-*`, the aggregators) are referenced by exact/caret versions,
201
+ > so their dist-tags do not affect resolution, but they default to `next` as well.
202
+ > Inspect the current tags with `npm dist-tag ls dsh-code-server-app`.
203
+
204
+ `repack:build` (`scripts/vendor-repacks.mjs`) is the **single script that produces every sub-package**:
205
+
206
+ | Sub-package | Content | os/cpu |
207
+ |---|---|---|
208
+ | `@jinsiyu/dshcs-vscode-server@<code-server version>` | the trimmed VS Code tree (`lib/vscode` + `out/browser` + `src/browser`; **without** code-server's `out/node` and its 136 runtime deps) | platform-independent |
209
+ | `@jinsiyu/dshcs-<name>[-win32-<arch>]` ×24 | the VS Code inner packages that need building (node-pty / @vscode/sqlite3 / kerberos / koffi / ssh2 / …) | gated when platform-specific |
210
+ | `@jinsiyu/dsh-code-server-runtime-win32-<arch>` | platform aggregator: its `dependencies` map those 16 natives back to their original names via `npm:` aliases | win32-<arch> |
211
+
212
+ | Goal | Command |
213
+ |---|---|
214
+ | **Build from the latest upstream release** | `pnpm run vendor:latest` (= `--force`): pulls `code-server@latest`'s tree into `vendor/vscode`; afterwards you **must** re-run `repack:build` and republish every sub-package |
215
+ | **Pin a version** | `pnpm run vendor:vscode -- --version 4.136.2` |
216
+ | **Snapshot from an existing tree** | `pnpm run vendor:vscode -- --from <code-server dir>` (seconds) |
217
+ | **Rebuild every sub-package** | `pnpm run repack:build -- --target win32-arm64,win32-x64 --pack` (without `--from` it npm-installs and compiles the source tree itself slow) |
218
+ | **Rebuild only the tree/aggregator packages** | `node scripts/vendor-repacks.mjs --reuse --target win32-arm64,win32-x64 --pack` (reuses the natives already in `repack/build`) |
219
+ | **Publish sub-packages** | `pnpm run publish:repacks` (`--dry-run` to preview; `--only <substr>` to filter; `--otp <code>` / `--limit N` for 2FA) |
220
+ | **Publish the plugin itself** | `pnpm run publish:plugin` (publishes the exact tarball that was verified; no re-packing; default dist-tag `next`) |
221
+ | **Promote `latest`** | `pnpm run promote -- <version>` (only after the user restarted and confirmed; `--dry-run` shows the current tags first) |
222
+ | **Just report versions** | `pnpm run vendor:check` |
223
+
224
+ > `pnpm pack`'s `prepack` runs the vendor-code-server script once; when `vendor/code-server` already exists it is
225
+ > a **no-op that takes seconds**, so after ordinary code changes you can just run `pnpm pack` (it will never
226
+ > silently upgrade code-server). Upgrading code-server requires an explicit `pnpm run vendor:latest`
227
+ > (or `--force` / `--version`) **plus** republishing the sub-packages.
228
+
229
+ ## Install the plugin (one command; all dependencies installed by the package manager)
230
+
231
+ ```powershell
232
+ # no postinstall in the package → no pnpm approve-builds / allowBuilds; one command installs everything
233
+ dsh plugin --profile web add dsh-code-server-app@0.2.1
234
+ # a local tarball works the same way:
235
+ dsh plugin --profile web add C:\Users\User\Desktop\dsh-code-server-app\dsh-code-server-app-0.2.1.tgz
236
+ ```
237
+
238
+ Ready to use immediately — **no second step, no "Install environment", no install-guide modal**.
239
+ The main package is only **~110KB** (the plugin's own code plus the launcher); everything else is dependencies:
240
+
241
+ - **the VS Code tree** (`lib/vscode` 196.9MB + `out/browser` + `src/browser`) is a **platform-independent package**
242
+ `@jinsiyu/dshcs-vscode-server@<code-server version>` declared in the plugin's `dependencies`; it runs from
243
+ `<profile>\node_modules\@jinsiyu\dshcs-vscode-server\vscode` (the legacy full tree at
244
+ `@jinsiyu/dshcs-code-server/code-server` is still recognised as a fallback);
245
+ - the **pure-JS part** of VS Code's inner dependencies (35 packages: xterm / katex / typescript / ws / tar …) is
246
+ declared in the plugin's `dependencies` and installed by pnpm into the profile's `node_modules` (hoisted);
247
+ - the **binary part** comes entirely from `@jinsiyu/dshcs-*` platform packages: 16 native packages mapped back to their
248
+ **original names** (`node-pty` / `@vscode/sqlite3` / `@vscode/spdlog` / …) by the **platform aggregator**
249
+ `@jinsiyu/dsh-code-server-runtime-win32-<arch>` using `npm:` aliases; the aggregators sit in the plugin's
250
+ `optionalDependencies`, so pnpm auto-selects the right platform;
251
+ - consequently the dependency graph contains **no package with pre/install/postinstall or a `binding.gyp`**
252
+ no profile `allowBuilds`, no build script ever runs, and **the user machine needs no C++ toolchain**;
253
+ - **upgrading the plugin no longer re-downloads the tree**: the tree package is cached by version
254
+ (~60MB, ~197MB unpacked).
255
+
256
+ ### Install mechanism (why it is built this way)
257
+
258
+ - **The pnpm 11 hard constraint**: any package in the dependency graph whose manifest has
259
+ `preinstall|install|postinstall` (or that ships a `binding.gyp`/`.hooks`) counts as "needs building" and must be
260
+ approved by the **host profile's** `pnpm-workspace.yaml` via `allowBuilds`, otherwise `dsh plugin add` exits 1 with
261
+ `[ERR_PNPM_IGNORED_BUILDS]`. A dependency's own `pnpm.allowBuilds`, `.npmrc`, `patch:` protocol and
262
+ `optionalDependencies` do not help (measured 2026-09, pnpm 11.25);
263
+ - **the tree** is prepared at pack time with `npm install code-server@<version> --ignore-scripts` (skipping the official
264
+ `sh ./postinstall.sh`, which cannot run on Windows), then `scripts/vendor-vscode-server.mjs` keeps **only the VS Code
265
+ tree**: `lib/vscode/**`, `out/browser/**`, `src/browser/**` plus the license files are copied to `vendor/vscode/`, and a
266
+ generated root `package.json` records the upstream code-server version. code-server's own `out/node/**` and its 136
267
+ runtime dependencies **no longer ship** they are replaced by `lib/launcher.mjs`;
268
+ - **the packages that need a toolchain** are repacked into `@jinsiyu/dshcs-*` by `scripts/vendor-repacks.mjs`:
269
+ the compiled package directory is copied and its `scripts` / `files` / `binding.gyp` / `.hooks` / `.npmignore` are
270
+ **removed** (the built `.node` and every runtime file stay) sibling packages in its dependency list become `npm:`
271
+ aliases platform-specific ones get `os`/`cpu` plus a `-<platform>-<arch>` suffix. For win32 targets the script also
272
+ verifies each `.node` PE machine (0x8664=x64 / 0xaa64=arm64) so a cross-compiled artifact cannot ship the wrong arch;
273
+ - **the platform aggregator** maps those repacks back to their original names (e.g.
274
+ `"node-pty": "npm:@jinsiyu/dshcs-node-pty@1.2.0-beta.15"`), so VS Code's `import('node-pty')` needs no change; the
275
+ aggregator is itself `os`/`cpu` gated, and the plugin declares both win32-arm64 and win32-x64 in
276
+ `optionalDependencies`, so one command picks the right one;
277
+ - **resolution path**: the host finds the tree with `require.resolve('@jinsiyu/dshcs-vscode-server/package.json')`
278
+ (then the inner `vscode/` directory) and the entry is `vscode/lib/vscode/out/server-main.js`; VS Code's inner deps are
279
+ resolved upwards from that root (`vscode/lib/vscode/node_modules` package `node_modules` `<profile>/node_modules`).
280
+ The legacy full tree (`@jinsiyu/dshcs-code-server/code-server`) is still recognised as a fallback;
281
+ - **runtime layout self-healing** (`ensureRuntimeLayout()` in `lib/native.js`, idempotent, run **at activation before
282
+ `envCheck` and again before every start**): the host adds two kinds of **junctions** (Windows junctions / POSIX dir
283
+ symlinks) into the tree:
284
+ 1. `ensureAliasLinks()`: re-links the native aliases the aggregator carries into `<tree>/node_modules` — pnpm nests
285
+ `os`/`cpu`-gated packages under the aggregator's own `node_modules`, and `lib/vscode/out/server-main.js` uses
286
+ **ESM imports** (ESM ignores `NODE_PATH`), so a missing link means an immediate 500;
287
+ 2. `ensureInnerModuleLinks()`: restores VS Code's **inner dependency directories**
288
+ `lib/vscode/node_modules` and `lib/vscode/extensions/node_modules` from the two `package.json` files — the trimmed
289
+ tree ships neither, and code that builds dependency paths explicitly (e.g. the bundled TypeScript extension looking
290
+ for `<ext>/../node_modules/typescript/lib/tsserver.js`) otherwise reports
291
+ "VS Code's tsserver was deleted by another application…" (measured with 1.136.1).
292
+ > **Size note**: the plugin tarball is **~110KB**; `@jinsiyu/dshcs-vscode-server` is **~60MB** (~197MB unpacked);
293
+ > the 16 native packages add ~250MB. A full install downloads roughly 310MB. Neither `vendor/` nor `repack/` is committed to git (see `.gitignore`).
294
+
295
+ > **Upgrading from 0.1.43**: the tree package changed from `@jinsiyu/dshcs-code-server` (the full code-server tree with
296
+ > `out/node` and 136 runtime deps) to `@jinsiyu/dshcs-vscode-server` (the trimmed tree). **The new code defaults to
297
+ > `serve: loopback`, which behaves exactly like 0.1.43**; switch to `serve: dsh` for same-origin mounting. The install
298
+ > command is unchanged (`dsh plugin --profile web add dsh-code-server-app@<version>`), and pnpm drops the old
299
+ > `dshcs-code-server` sub-package.
300
+ ### Development: install from source (changes take effect immediately)
301
+
302
+ ```powershell
303
+ dsh plugin --profile web add C:\Users\User\Desktop\dsh-code-server-app
304
+ ```
305
+
306
+ > A source path installs via `link:`. On a dev machine without `vendor/code-server`, run
307
+ > `pnpm run vendor:vscode -- --dev-links` first. Dependencies (inner JS deps + the platform aggregator) are installed by pnpm
308
+ > too — but the not-yet-published local `@jinsiyu/*` packages must either be published first, or the
309
+ > `repack/tgz/*.tgz` files must be installed into the profile as `file:` dependencies.
310
+ >
311
+ > **Changing the client bundle**: edit `src/factory.js` then run `pnpm run build:client`
312
+ > to regenerate `lib/client.js` (that artifact is not tracked; a browser refresh picks it up — no host restart needed).
313
+ > Window animations are driven by the embedded `motion`; feel parameters live in `winPhysics` (one spot) in `src/factory.js`.
314
+
315
+ ### Pack-machine environment (the user machine needs nothing)
316
+
317
+ **A toolchain is needed at pack time only never on the user machine.**
318
+
319
+ | Env | Version / requirement | User machine | Pack machine |
320
+ |---|---|---|---|
321
+ | Node.js | **v24.x** (latest code-server requirement; v24.13.1 here) | required | required |
322
+ | npm / pnpm | npm ships with Node; pnpm comes from DSH | required (installs deps) | required |
323
+ | **MSVC build tools** | **VS Community 2026 + C++ desktop workload** | ❌ **not needed** | pack time (16 native packages) |
324
+ | **VS Spectre-mitigated libs** | one set for ARM64 **and** one for x86/x64 ("MSVC v14x Spectre-mitigated libs") | ❌ not needed | pack time (otherwise MSB8040) |
325
+ | Python | **3.13.x** | ❌ not needed | pack time (node-gyp) |
326
+ | node-gyp | **13.x** (older versions don't recognize VS 2026) | not needed | pack time |
327
+
328
+ > **Packing still works without the Spectre libs**: when a package fails to compile, `vendor-repacks.mjs` downgrades
329
+ > `SpectreMitigation` to `false` in that architecture's `*.gyp` files and retries (only the Spectre hardening is
330
+ > lost, functionality is unaffected) and says so in the log.
331
+
332
+ ### Windows native build notes (pack time, verified locally ARM64)
333
+
334
+ - **VS needs the Spectre-mitigated libraries** (MSB8040): Visual Studio Installer Individual components
335
+ "MSVC v14x Spectre-mitigated libs" **install the ARM64 and the x86/x64 sets separately**.
336
+ - **node-gyp 13.x** (9.x does not recognize VS 2026): `npm install -g node-gyp@latest`.
337
+ - **x64 cross-compiling**: `vendor-repacks.mjs` uses `npm install --os=win32 --cpu=x64 --ignore-scripts` to fetch
338
+ the packages, then `npm rebuild --arch=x64` per package; the resulting PE machine types were verified
339
+ (kerberos / sqlite3 / spdlog ).
340
+ - Latest code-server requires **Node v24**.
341
+ - If you don't need the self-contained install (e.g. a global code-server already exists), skip it:
342
+ the plugin falls back to a configured/PATH `bin` (see the "Config" table).
343
+
344
+ ### Upgrading the VS Code tree (upstream = a code-server release)
345
+
346
+ - **The version is decided at pack time**: `pnpm run vendor:latest` (= `--force`) pulls the tree of the npm **latest**
347
+ release; or use `pnpm run vendor:vscode -- --version 4.136.2` / `DSHCS_CODE_SERVER_VERSION`.
348
+ With an existing `vendor/vscode`, a plain `pnpm pack` never upgrades (it is a no-op).
349
+ - **Check first**: `pnpm run vendor:check` prints the bundled version / upstream latest.
350
+ - **A version bump means rebuilding and republishing the sub-packages** (all with the same script):
351
+ 1. `pnpm run repack:build -- --target win32-arm64,win32-x64 --pack` the new tree package
352
+ (`@jinsiyu/dshcs-vscode-server@<new version>`) and the natives rebuilt against the new inner dependencies (the
353
+ script also rewrites the plugin's pure-JS `dependencies` and both aggregator versions);
354
+ 2. `pnpm run republish:repacks` (`pnpm run publish:repacks`) publish; then bump the plugin version `pnpm pack`
355
+ publish the plugin.
356
+ - `productPath` (`<quality>-<commit>`, part of the client WebSocket path) is **computed from `lib/vscode/product.json`**,
357
+ so upgrading the tree needs no code change — but the routes are registered at activation, so restart `dsh web` afterwards.
358
+ - **No runtime auto-upgrade anymore**: nothing fetches latest at startup; the version is fully determined by the bundled artifact.
359
+ - Bundled locally right now: the tree of `code-server@4.136.2` (VS Code 1.136.1, `productPath=stable-8d5f383f…`).
360
+
361
+ ### Compatibility with the old install locations
362
+
363
+ Host probe order: `@jinsiyu/dshcs-vscode-server/vscode` (**the real layout since 0.2.0**) >
364
+ `@jinsiyu/dshcs-code-server/code-server` (the full tree, 0.1.40–0.1.43) >
365
+ `@jinsiyu/dshcs-code-server-<platform>-<arch>/code-server` (the 0.1.37 platform sub-packages) >
366
+ the in-package `vendor/vscode` > the in-package `vendor/code-server` (development). The old install root
367
+ `<profile>\.code-server-app` is only mentioned in a startup log line; nothing writes to it any more.
368
+ ## Settings card (Settings Plugins Code Server)
369
+
370
+ Modeled after dsh-auto-open-web's custom card, registered on the `settings.plugin.item` slot,
371
+ persisted via the official settings domain (`settingsScope`, namespace `code-server`) into the official settings document:
372
+
373
+ | Key | Default | Description |
374
+ |---|---|---|
375
+ | `fileOpenScope` | `session` | **Claim scope** (0.2.5): `session` claims only session-scoped file addresses (`dsh-resource://file/session/…` — DSH's produced files, declared deliveries, prose mentions); `all` also claims session-less `…/file/absolute/…`. Unclaimed addresses fall back to DSH's built-in preview |
376
+ | `keepResident` | `true` | **Resident in background**: on, the host preloads the IDE into a parked surface right after startswitching tabs or collapsing the sidebar never reloads it and the first open needs no cold start; off loads it only when the panel is opened (saves memory) |
377
+
378
+ (Since 0.2.6 the card keeps only those two settings; `windowedOpen` and `reserveComposer` are gone leftover keys in an old
379
+ settings document neither fail nor apply. `serve` remains a key in the settings namespace (usable from a settings document) but
380
+ has **no card row** see "Serving mode".)
381
+
382
+ > Card changes take effect immediately via `scope.watch` (the host status API returns `keepResident` and
383
+ > `fileOpenScope`; the client applies them at once); no dsh restart needed. **After adding new setting keys, restart dsh web before first use**,
384
+ > so the host re-registers the settings namespace (schema includes the new key); otherwise save/validation of the new key won't work.
385
+
386
+ Since 0.2.7 the card has **no** "Entry", "dependency install" or "environment check" rows: the entry lives in the sidebar's
387
+ guide page (and in DSH's own file clicks), and diagnostics stay out of the UI — the `/api/code-server/status` `env` field still
388
+ reports the tree version / `productPath` / server entry, VS Code inner dependencies and **prebuilt native packages**
389
+ (platform aggregator name + resolved module count) for scripts, and the DSH host log carries the `[code-server]` lines.
390
+
391
+ ## Config (`config` in cordis.patch.yml; all have defaults)
392
+
393
+ | Key | Default | Description |
394
+ |---|---|---|
395
+ | `bin` | `code-server` (placeholder) | Launch priority: explicit `bin` in config > the tree package `@jinsiyu/dshcs-code-server/code-server/out/node/entry.js` > the old platform sub-packages `@jinsiyu/dshcs-code-server-<platform>-<arch>` > the in-package `vendor/code-server` > the old install root `.code-server-app` > plugin-internal `node_modules` > `code-server` on PATH. None present startup error with troubleshooting hints |
396
+ | `host` | `127.0.0.1` | Bind address; `auth: none` only allows loopback (localhost/127.0.0.1/::1) |
397
+ | `port` | `8090` | Port; on conflict startup fails with diagnostics (no automatic port change) |
398
+ | `auth` | `none` | `none` \| `password`; non-loopback host automatically requires password |
399
+ | `passwordToken` | `''` | Token for password mode (passed to code-server via the `PASSWORD` env var) |
400
+ | `userDataDir` | `$DSH_HOME/code-server/user-data` | User-data isolation directory |
401
+ | `extensionsDir` | `$DSH_HOME/code-server/extensions` | Extensions directory |
402
+ | `readyTimeoutMs` | `60000` | `/healthz` readiness probe timeout |
403
+
404
+ User-level override example (write in `$DSH_HOME/profiles/web/cordis.patch.yml`, using the `- id: code-server` row):
405
+
406
+ ```yaml
407
+ - id: code-server
408
+ config:
409
+ port: 8091
410
+ # Explicit (overrides dependency-install probing): a globally installed shim, or any entry.js
411
+ bin: C:\Users\User\AppData\Roaming\npm\code-server.cmd
412
+ ```
413
+
414
+ ## JSON API (same-origin fetch; identical paths in web and desktop)
415
+
416
+ **No `webServer` dependency**: the host half registers its routes on DSH Connection's shared `/api` channel through
417
+ `ctx.connection.fetch.register`. In the web profile Connection mounts the `/api` prefix on webServer itself (with the
418
+ Host/Origin fence and browser auth); in the desktop profile `apps/desktop-host` feeds `/api/*` into the same
419
+ `createSharedFetchHandler('/api')` (IPC framed pipe, no HTTP server). The client only writes relative paths
420
+ (`fetch('/api/code-server/<op>')`), so both carriers behave identically.
421
+
422
+ | Method | Path | Description |
423
+ |---|---|---|
424
+ | GET | `/api/code-server/status` | `{ ok, running, status, host, port, pid, cwd, url, version, error, logTail, adopted }` (also `env` environment check and the `setup` compatibility field) |
425
+ | POST | `/api/code-server/start` | body `{ cwd? }` (omit cwd to keep the current workspace); idempotent |
426
+ | POST | `/api/code-server/stop` | Stop and recycle the process tree |
427
+ | POST | `/api/code-server/setup` | **Compatibility no-op**: since 0.1.36 dependencies are installed by the package manager, so this only re-runs the env self-check and returns |
428
+ | POST | `/api/code-server/open-file` | body `{ file }` — writes the signal consumed by the built-in `dshcs-open-file` extension to open the file in code-server |
429
+
430
+ > The plugin no longer registers `/code-server/*` webServer-only routes, and the code-server icon is inlined as a data URI
431
+ > in the client bundle — the client requests no plugin-owned HTTP resource at all.
432
+
433
+ ## DSH Desktop (no webServer)
434
+
435
+ - The host half is `inject = ['connection', 'settings']` (**no `webServer`**) the desktop profile disables webserver/web-runtime
436
+ and the plugin still works: `/api/*` requests travel Electron `dsh-app://` protocol handler IPC framed pipe `createSharedFetchHandler('/api')`.
437
+ - The right-sidebar tab, guide entry box, file-address claim, and settings card behave the same as in web (code-server remains an
438
+ iframe to the local `http://127.0.0.1:<port>`; the desktop renderer uses `webSecurity: true` with no CSP, so the cross-origin iframe loads).
439
+ The desktop build ships `dsh-client-ui-sidebar-right` in its seed package set as well, so the 0.2.3 "right-sidebar DSH only" rule is
440
+ not a regression for desktop; the only difference is the missing `webServer`, where `serve: dsh` falls back to loopback.
441
+ - Install into the desktop profile through the **desktop plugin manager** (not the CLI, see below).
442
+ - **Desktop installs face a 24-hour supply-chain policy (measured 2026-09-10; this is how 0.2.4 got installed)**:
443
+ - the CLI path is unavailable: `dsh plugin --profile desktop …` is rejected (*"profile "desktop" is managed exclusively by the
444
+ Electron application"*), so desktop installs only go through the app's package transaction (`pnpm add <spec> --save-exact`,
445
+ executed in `~/.dsh/desktop/staging/<uuid>/profile` before activation);
446
+ - that transaction's pnpm (the app bundles **11.7.0**, patched by DeepSeek) runs a **lockfile supply-chain verification** before
447
+ `add` ("Verifying lockfile against supply-chain policies (717 entries)") which requires packages to be **at least 24 h old**,
448
+ otherwise `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`;
449
+ - **the two stages behave differently (measured)**:
450
+ - verifying an **existing lockfile**: `minimumReleaseAgeExclude` is *not* honoured (exact versions and bare package names
451
+ were both tried);
452
+ - **resolution** (no lockfile to verify, e.g. after `pnpm clean --lockfile`): the list *is* honoured, and pnpm even appends
453
+ entries itself (the install log prints *"Added N entries to minimumReleaseAgeExclude…"*);
454
+ - so the working recipe for a **just-published** (<24 h) version on desktop is to start from a clean, lockfile-free profile:
455
+ 1. `pnpm clean --lockfile` (**note: it also deletes `node_modules`**, leaving the profile to be reinstalled);
456
+ 2. with the app's bundled runtime, run `add <spec> --save-exact --trust-lockfile` in the profile directory
457
+ (runtime/store/config live under `~/.dsh/desktop/pnpm/{store,cache,state,config,home}`,
458
+ `--config.userconfig=…/config/npmrc`, otherwise pnpm fails with `ERR_PNPM_UNEXPECTED_STORE` /
459
+ `…UNEXPECTED_VIRTUAL_STORE`);
460
+ 3. the app's boot command (`install --offline --frozen-lockfile --trust-lockfile`) then passes (lockfile matches
461
+ package.json, packages are in the store); if the plugin name is already in `dsh.profile.bundles` nothing else is needed.
462
+ - do **not** try to bypass it with `minimumReleaseAge: 0`: it does clear the check, but that key is **not** one of the policy
463
+ sections the app tolerates (`project-manager.ts` ignores only `minimumReleaseAgeExclude:` / `trustPolicyExclude:` and validates
464
+ the manifest before every `mutate()`), so persisting it makes the app fail with
465
+ *"core package mapping does not match desktop-packages.json"*.
466
+ - Alternatively **wait out the 24 h** and install normally from the plugin manager. The web profile is unaffected: its
467
+ `pnpm-workspace.yaml` sets `minimumReleaseAge: false`.
468
+ - this plugin's closure contains platform native sub-packages (`@jinsiyu/dsh-code-server-runtime-win32-*`) published together with
469
+ the plugin itself, so every new version hits that policy on desktop.
470
+ - **Desktop client bundles are cached by Electron; restarting the app does not guarantee a new one** (measured 2026-09, hit while shipping 0.2.5):
471
+ - symptom: `lib/client.js` in the profile is the new version, yet the renderer keeps running the old code
472
+ `%APPDATA%\@deepseek-ai\dsh-desktop\Code Cache\js` only contains strings unique to the old version (e.g. `dshcs-artifacts`)
473
+ and none unique to the new one (`fileOpenScope`), and `Cache\` still holds an old response body referencing
474
+ `dsh-code-server-app`. The same applies to first-party plugins (the cached `ui-deliverables` even lacks the current
475
+ `data-presented-files-row` marker);
476
+ - diagnosis (byte level — do **not** use `Select-String`, which reads files with the console encoding and gives false
477
+ negatives on non-ASCII markers): search `Code Cache\js` for an **ASCII** marker unique to the new version
478
+ (ours is `fileOpenScope`); a hit proves the new bundle really was compiled;
479
+ - fix: fully close the app, delete the `Cache`, `Code Cache` and `GPUCache` directories, then start it (cache only —
480
+ profiles, sessions and settings are untouched):
481
+ ```powershell
482
+ Remove-Item -Recurse -Force "$env:APPDATA\@deepseek-ai\dsh-desktop\Cache","$env:APPDATA\@deepseek-ai\dsh-desktop\Code Cache","$env:APPDATA\@deepseek-ai\dsh-desktop\GPUCache"
483
+ ```
484
+ - scope: this is not specific to this plugin — **any** client plugin may keep running old code after an upgrade;
485
+ after a release, confirm with the marker trick above that the renderer actually swapped bundles.
486
+
487
+ ## File-open plumbing (what the plugin itself still does)
488
+
489
+ DSH's own chips and preview buttons are what users click (see "Opening files" above); this plugin adds no row of its own.
490
+ What remains on the plugin side:
491
+
492
+ - the tab body parses `navigation.address` and posts the absolute path (plus an optional `line`) to
493
+ `/api/code-server/open-file`, which writes a signal file;
494
+ - the bundled `dshcs-open-file` extension polls that file and calls `showTextDocument` VS Code Web has no official
495
+ "open this file from outside" API, so this is the only way to aim the workbench at a file. It is installed as a
496
+ **built-in** extension (in `lib/vscode/extensions`), so users cannot remove it from the extensions panel, and the
497
+ installer re-syncs it whenever its content changes.
498
+
499
+ ## Known limitations
500
+
501
+ - ~~No sub-path~~ **no longer true (corrected with measurements in 0.2.0)**: the workbench HTML VS Code renders references
502
+ **only relative URLs** (9 references measured, 0 absolute; `serverBasePath="."`, `rootEndpoint="."`), and the client
503
+ builds its WebSocket path from `location.pathname + join(serverBasePath ?? '/', <quality>-<commit>)`. The IDE can
504
+ therefore be mounted directly under DSH's own `/code-server/*` (`serve: dsh`) no second port, no HTML rewriting.
505
+ Item-by-item evidence: `docs/analysis-code-server-as-dsh-plugin.md`.
506
+ - **`serve: dsh` cannot proxy forwarded-port WebSockets**: `registerUpgrade` matches exact paths while `/proxy/:port`
507
+ carries the port in the path, so WebSocket forwarding for the Ports panel is unavailable in that mode (HTTP forwarding
508
+ works). Use `serve: loopback` when you need it.
509
+ - **`serve: dsh` shares DSH's origin**, so the iframe is not sandboxed there (same-origin plus `allow-same-origin` is
510
+ escapable by the frame itself); in `loopback` mode the iframe is cross-origin and `sandbox` stays as real protection.
511
+ - **Single instance across sessions**: one shared IDE per host; switching cwd requires a restart (the sidebar tab
512
+ handles it and hints).
513
+ - **Older DSH versions are unsupported (since 0.2.3)**: on a DSH without `sidebarRightTabs` / `sidebarRight` the plugin
514
+ offers nothing but an upgrade notice on the settings page; older-DSH users should stay on `0.2.2`
515
+ (`dsh plugin --profile web add dsh-code-server-app@0.2.2`).
516
+ - **Sidebar tab switching** (no longer reloads since 0.2.2): DSH's right sidebar renders only the active tab's body, and
517
+ a React unmount moves the iframe away; the plugin keeps it as a singleton resident surface and shuttles it between the
518
+ dock slot and a document-level park container with `Element.moveBefore()` (a state-preserving atomic move), so
519
+ switching tabs or collapsing the sidebar and back **does not reload** it. Browsers without `moveBefore` fall back to
520
+ the old behaviour (`appendChild` → full reload), reported as `degraded`; see "Why switching tabs no longer reloads".
521
+ - **Remote access**: with `serve: dsh` the browser only needs to reach DSH itself (one port, protected exactly like `/api`);
520
522
  `serve: loopback` stays loopback-only with `auth: none`, and 0.2.0 no longer supports `auth: password`.