dsh-code-server-app 0.2.7 → 0.2.9

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