dsh-code-server-app 0.3.46 → 0.3.49

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,817 +1,1079 @@
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-*` sub-packages declared **directly on the plugin's dependency table** under their real names (os/cpu-gated per target), with the original import names restored by runtime junctions. 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
- > Since 0.3.22 the ask panel renders the session's new content with **DSH's own Markdown renderer** (the same
16
- > renderer and design tokens as the DSH UI; new content only) and can **answer approval requests in place**
17
- > (writing outside the workspace / running commands). See "Working with DSH: the editor bridge" and section 21 of
18
- > `docs/analysis-code-server-as-dsh-plugin.md`.
19
-
20
- ## UI carrier and required DSH version (0.2.3: right-sidebar DSH only)
21
-
22
- | DSH version | Carrier | Entry points |
23
- |---|---|---|
24
- | **>= 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"** |
25
- | older (no sidebar service) | **Unsupported**: nothing but one notice on the settings page | none (Settings → Plugins → Code Server shows an upgrade notice) |
26
-
27
- - 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).
28
- 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"):
29
- - services are looked up as `ctx.<name>` first and `ctx.get(name)` second, so either context shape registers;
30
- - when the sync probe already sees the services but `inject` never calls back, registration falls back to the sync services after **1.5 s**;
31
- - 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);
32
- - services arriving late automatically revoke the legacy verdict, register the sidebar, and report `{sidebar:true}` so the host re-enables;
33
- - 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".
34
- - **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
35
- - registers only the settings card (an upgrade notice) — no ball, no floating window, **no file-address claim**, no IDE preload;
36
- - 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;
37
- - upgrading DSH needs **no reinstall** — refresh the page and the card turns back into the full settings card.
38
- - 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.
39
- - **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.
40
- 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.
41
- Turn it off in the settings card (`fullscreenOnOpen=false`) to stay side by side.
42
- - How it is done: DSH does **not** expose the mode to plugins — `ctx.sidebarRight` only has `isExpanded`/`toggleExpanded`
43
- (expand/collapse), while push ⟷ fullscreen is recorded in `ui-sidebar-right`'s own store (`actions.setMode`, handed only to
44
- its own seat components). `ctx.layout.openRightbar(track, fullscreen)` is not a control either: it is the channel the seat
45
- **reports** its presentation through (upstream comment: *the occupant reports it; nothing else writes it*).
46
- So the plugin performs the user's own gesture: it locates its own panel with `closest('[data-sidebar-right-panel]')` and
47
- clicks the panel chrome's `[data-sidebar-right-mode="fullscreen"]` button (the exact same path as a manual click, including
48
- the narrow-viewport handling). When the button is missing it keeps the current mode and logs one `console.warn` — panel
49
- rendering is never affected.
50
- - **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).
51
- - The settings card has exactly **three settings**: "**Claim types**", "**Fullscreen on open**" and "Resident in background" — no other rows (0.2.7 removed the "Entry", "dependency install" and "environment check" rows).
52
- 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;
53
- 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).
54
- 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).
55
- To use the IDE in a browser tab, **copy the full address** from the settings card / empty-state hint (it contains the
56
- path token: `http://127.0.0.1:<port>/<token>/`; under `serve: dsh` it is DSH's `/code-server/`) — dropping the token
57
- segment yields a 404.
58
-
59
- ## Opening files (official entry points since 0.2.5)
60
-
61
- DSH names files with **resource addresses**; `openFile` only hands the address to the right sidebar, which decides who draws it:
62
-
63
- ```
64
- DSH's produced-file chip / presented-file card preview / inline prose mention
65
- → openFile(path, { line? }) (provided by ui-chat)
66
- → dsh-resource://file/session/<sessionId>/<path> (or …/file/absolute/<path>)
67
- → ctx.sidebarRight.openResource(address)
68
- → claimed by the tab type whose patterns match (band extension(3) > builtin(2) > fallback(1),
69
- then the longest matching pattern, then registration order)
70
- ```
71
-
72
- This plugin registers:
73
-
74
- | Field | Value | Effect |
75
- |---|---|---|
76
- | `patterns` | `['dsh-resource://file/**']` | claims file addresses (a pattern containing `:` is matched against the **whole address**) |
77
- | `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 |
78
- | `canOpen` | see below | vetoes by the "claim types" setting; unclaimed addresses fall back to DSH's built-in preview |
79
- | `title` | last address segment (= file name) | the tab chip shows the file name; a page tab (`sidebar://code-server`) still reads `Code Server` |
80
-
81
- - **Claim types** (a text box in the settings card, `claimExtensions`, since 0.2.11):
82
- **scope is no longer a thing** — `dsh-resource://file/session/…` and `…/file/absolute/…` are treated alike,
83
- and only the extension decides. Text-box grammar (semicolon-separated; `,`/whitespace/newlines also work;
84
- `py`, `.py` and `*.py` are equivalent; case-insensitive):
85
- - `*` — claim every other type too (catch-all);
86
- - `py` — claim `.py`;
87
- - `!md` — do **not** claim `.md` (**exclusion wins** over both an explicit claim and `*`);
88
- - **default** `*;!md;!markdown;!html;!htm;!png;!jpg;!jpeg;!gif;!webp;!bmp;!ico;!svg;!pdf`
89
- — the four categories DSH's own preview renders well (markdown / html / images / PDF) stay with it, everything
90
- else (code, json/yaml, txt, logs, extension-less files such as `Makefile`, unknown extensions) goes to the IDE;
91
- an empty box claims no files at all (page tabs only).
92
- - Three practical shapes: a plain whitelist (`py;ts`, no `*` → nothing else is claimed), catch-all (`*`),
93
- and catch-all plus exclusions (the default).
94
- - Grammar, default and parsing all live in `lib/claim-types.js` (the host's `Config` default and the client's
95
- `canOpen` share that single file, shipped in the package, so the two cannot drift apart);
96
- unit tests: `scripts/test-claim-types.mjs`.
97
- - **How the tab body locates the file**: it parses `useTabInfo().tab.navigation.address`
98
- (`src/address.js`, same grammar as DSH's `parseFileAddress`), expands a workspace-relative path with that
99
- session's cwd, and posts the absolute path (plus optional `line`) to the host's
100
- `/api/code-server/open-file`; the bundled extension (`dshcs-open-file`) then calls `showTextDocument`
101
- (positioned at the line when given).
102
- - **One address = one tab** (DSH semantics: `contentId` *is* the address): three files mean three chips, but they
103
- share the single resident workbench — switching tabs just re-aims the workbench at the corresponding file.
104
- - **Why the bundled extension stays**: VS Code Web has no official "open this file from outside" API (the only
105
- entry is `?folder=`, which picks the workspace), so aiming the workbench at a file has to be done by an
106
- extension inside the tree. The host writes a signal file, the extension polls it and calls
107
- `showTextDocument`, keeping the signal for retry when no window is connected yet.
108
-
109
- ## Why switching tabs no longer reloads (resident IDE)
110
-
111
- **The old trap**: DSH's right sidebar (ui-dockkit) renders **only the active tab's body**
112
- (`TabPanel.tsx:412` → `renderTab(active)`) — switching to another tab unmounts that body in React, which moves the
113
- iframe out of the document and destroys its browsing context; switching back is a full VS Code reload (unsaved buffers
114
- lost). Floating the tab into its own panel only worked around it.
115
-
116
- **What it does now (`src/surface.js` in the client, 0.2.2)**: the plugin takes the iframe **away from React** and turns
117
- it into a **singleton resident surface**:
118
-
119
- | Situation | Action | Result |
120
- |---|---|---|
121
- | tab becomes active | `host.moveBefore(frame, null)` into the visible dock slot | state-preserving atomic move, **no reload** |
122
- | 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 |
123
- | workspace / port changes | assign `src` explicitly | the only normal "reload" entry point |
124
-
125
- - **Why `moveBefore`**: measured in a real browser (Edge/Chromium 151), a plain `appendChild` move resets the iframe's
126
- internal timers (i.e. reloads it), while `Element.moveBefore()` (Chromium ≥133) preserves state (a probe counter keeps
127
- counting 1→2).
128
- - **Degradation is never silent**: when `moveBefore` is missing, or the host was already detached by React and it throws
129
- `HierarchyRequestError: invalid hierarchy` (passive effect cleanup runs after DOM removal), the code falls back to
130
- `appendChild` — one reload, but the frame is **never lost** — and reports `degraded` / `lastMoveError` so the UI can
131
- say "residency unavailable".
132
- - **Repaint fallback (measured)**: in the real GUI the surface was seen once with correct size, hit testing and
133
- `visibility` that simply **stopped repainting** (a fully white panel, byte-identical screenshots proving no new frame).
134
- `translateZ(0)` and `opacity` nudges did nothing; `display:none → forced reflow → restore` inside a single JS task
135
- restored it without reloading the iframe document, without losing internal state and without a visible flash.
136
- **The trigger could not be reproduced**: in a probe page an offscreen `moveBefore` park of 337 s (past Chrome's
137
- ~5 min cross-origin throttle window) followed by a dock with the fallback disabled still painted normally. It is
138
- therefore kept as a **fallback**: every park→dock transition runs one `nudgeRepaint()` (counted as
139
- `surfaceSnapshot().nudgeCount`; `setNudgeEnabled(false)` A/Bs it live).
140
- - **Warm-up**: with `keepResident` (default `true`) the host builds the surface right after plugin start and leaves it
141
- parked, so the first tab open needs no cold start; preloading never yanks a surface that is currently docked.
142
- - **Debug handle**: `window.__dshcsSurface` (`snapshot()`, `setParkStrategy('offscreen'|'behind')`, `dock()`, `park()`,
143
- `nudge()`, `setNudgeEnabled(false)`, `destroy()`).
144
-
145
- **Measured** (DSH web GUI, real mouse clicks between sidebar tabs): switching away → `docked:false`, same iframe node,
146
- in-frame probe still alive, `degraded:false`; switching back → `docked:true`, unchanged `src`, IDE pixels and editing
147
- state preserved (no full reload). Full evidence and probe scripts: `docs/analysis-code-server-as-dsh-plugin.md`.
148
-
149
- ## Serving mode (`serve`)
150
-
151
- | Mode | What it does | Requires |
152
- |---|---|---|
153
- | **`loopback` (default)** | the plugin listens on its own loopback port (**`port: 0` by default = a random port assigned per start**), the sidebar iframe connects cross-origin, and the URL carries a **random path token** (`http://127.0.0.1:<port>/<token>/`, see "Security model of the loopback port" below); the process can be adopted after a DSH host restart | nothing |
154
- | **`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 |
155
-
156
- ### Security model of the loopback port (since 0.2.14)
157
-
158
- `loopback` is the only transport desktop has (no webServer, no same-origin mount), so it is hardened on its own:
159
-
160
- - **Random port**: `port` defaults to `0` → the OS assigns a free port and the launcher writes the **actual** one to
161
- `$DSH_HOME/code-server/endpoint.json`, which the host reads back. The port therefore changes on every start and the old
162
- "8090 is busy" class of conflicts is gone. Pin `port` explicitly if you need a fixed address.
163
- - **Path token**: a fresh 32-character token (`[0-9A-Za-z_-]`, 24 random bytes) is generated on every **new start**, stored in
164
- `$DSH_HOME/code-server/path-token` (inside the user profile, readable only by the owner under the default ACL), and becomes
165
- the URL path prefix. Requests without that prefix get a plain **404** (nothing reveals that an IDE lives there); a prefix
166
- without the trailing slash is answered with a 302.
167
- - **Why not VS Code's own `connection-token`**: it works through `?tkn=` → 302 + `Set-Cookie: vscode-tkn; SameSite=Lax`.
168
- The desktop iframe is **cross-origin** (`dsh-app://` → `127.0.0.1`), and a Lax cookie is not sent from a cross-site
169
- subframe — the IDE would simply fail to load. A path prefix needs no cookie at all: the workbench derives every asset and
170
- WebSocket URL from `location.pathname` (the same mechanism already proven by mounting under `/code-server/` in `serve: dsh`),
171
- so the prefix rides along on every subrequest and on the WS handshake. (Verified with a real Edge + CDP run: with a random
172
- port and a token, the workbench renders **inside a cross-origin iframe** and establishes its WebSocket.)
173
- - **Host allowlist**: in loopback mode only `127.0.0.1 | localhost | [::1] : <actual port>` is accepted. This is what stops
174
- DNS rebinding, whose requests can arrive without an `Origin` header and therefore slip past the `Origin == Host` check.
175
- - **`Referrer-Policy: no-referrer`**: the token lives in the path, so it must not leak through `Referer` when external resources load.
176
- - **The token never reaches argv or the logs**: command lines are readable by any local process, so it travels through a file;
177
- the log only says "enabled".
178
-
179
- Boundary, stated plainly: this layer stops other local applications, port scanners and browser pages from casually reaching
180
- your IDE. A **malicious program running as the same user** can already read your files and that token file — that is outside
181
- this plugin's threat model.
182
-
183
- - Switch it in `config.serve` in `cordis.patch.yml` or in Settings → Plugins → Code Server (takes effect on the next start).
184
- - Benefits of `dsh`: a single URL/port (remote access to DSH gives you the IDE), no extra loopback listener, authentication on par with DSH.
185
- - Two **known trade-offs** of `dsh`: the iframe shares DSH's origin, so `sandbox` is dropped there (same-origin plus
186
- `allow-same-origin` is escapable by the frame itself; in `loopback` mode the iframe is cross-origin and `sandbox` stays
187
- as real protection — clipboard is still granted via `allow="clipboard-read; clipboard-write"`); and forwarded-port
188
- **WebSockets** cannot be routed because `registerUpgrade` matches exact paths while `/proxy/:port` carries the port in
189
- the path (HTTP forwarding works; use `loopback` when you need WS forwarding).
190
-
191
- - In `loopback` mode every upgrade passes a **code-server-equivalent Origin check** (since 0.2.1): when an `Origin`
192
- header is present its host must equal `Host` (honouring `Forwarded: host=` / `X-Forwarded-Host`, like code-server),
193
- otherwise the handshake gets `403`; non-browser requests without `Origin` are allowed. Without that check any local
194
- browser page could complete a handshake against `ws://127.0.0.1:<port>/stable-<commit>` and drive the IDE.
195
-
196
-
197
- ## Working with DSH: the editor bridge (since 0.3.0, on by default)
198
-
199
- Having the IDE next to DSH and having the agent **know what is going on in the editor** are two different
200
- things. The editor bridge covers the second half: it is a **read-only** channel that hands the agent what
201
- only the editor knows, and lets editor gestures drive the current session.
202
-
203
- | Direction | Capability | Mechanism |
204
- |---|---|---|
205
- | editor → agent | **unsaved buffers** (disk ≠ what the user sees), active file and selection, **language-server diagnostics** with `file:line`, source and code | agent tools `editor_context` / `editor_diagnostics`; plus a notice attached before writing a dirty file |
206
- | editor → DSH | select code → context menu **"DSH: ask about selection"** → an **ask panel** opens (carrying `file:line` and the selection); the question enters the current session as **user input**, and that session's **new content** is rendered in the panel by DSH's own Markdown renderer | extension command `dsh-code-server.askAboutSelection` (one of the **top two** editor context-menu items) + a webview panel + `POST /ask` + the `thread` field of `/sync` |
207
- | DSH → editor (approval) | when the agent wants to **write outside the workspace or run a command**, the approval request shows up as a card in the panel (tool, reason, countdown); "allow once" / "reject" takes effect immediately | the `approvals` field of `/sync` + `POST /approve` (the bridge's **only** non-read-only route; constraints under "Security model") |
208
- | agent → editor | the agent changed a file → a **native diff** opens; if that buffer has unsaved changes you get a warning and **no overwrite** | host watches `tools/result`, the extension polls and opens the diff |
209
-
210
- - The tools are only registered while the bridge is live (so the model never sees an unusable tool), and the
211
- system-prompt section renders only then too.
212
- - **Ask panel** (extension 0.2.0; the official renderer since 0.2.3; a **floating dialog over the DSH UI since
213
- 0.2.5**): the context-menu command no longer opens a panel inside the editor (that is always a tab or a column,
214
- never a dialog) — the plugin's client half pops up a draggable, resizable floating chat window in the DSH page
215
- itself (bottom-right, ✕ closes it), leaving the editor layout alone. Hosts without that capability fall back to
216
- the in-editor webview panel. The selection can still be changed while the dialog stays open.
217
- The two commands **remember their intent** (0.3.21): "ask about selection" carries a **line range + selection text**
218
- only when something is actually selected, while "ask about file" **never carries line numbers or a selection** — the
219
- cursor line is irrelevant to the question and only misleads the agent. With no selection, the selection command also
220
- degrades to the plain file.
221
- - **Injected context is collapsed** (0.3.24): the location line plus the selection code block the bridge adds to the
222
- message are split out into a collapsed Context row (click it to see the code), while the bubble keeps only the user's
223
- own words — the same treatment the DSH UI gives injected context.
224
- - **The panel renders exactly what DSH renders** (0.3.22): the panel bundles DSH's official Markdown renderer
225
- (`MarkdownText` from `@deepseek-ai/dsh-client-ui-primitives`) plus the official design tokens — the same
226
- micromark/mdast pipeline, the same incremental streaming parser, the same shiki highlighting (boot set:
227
- typescript / shellscript / json), KaTeX math and the same heading/table typography. Only **new content** is
228
- rendered (from the moment the panel subscribes); history is **not replayed** and there is no "load earlier".
229
- - **Thinking shows up like in DSH** (0.3.23): assistant reasoning becomes a Think row — **collapsed by default**,
230
- showing its first line (or the latest line while streaming) and expanding on a row click, built from the official
231
- `DisclosureRow` plus the official think icon and typography language.
232
- - **Approvals are handled right in the panel** (0.3.22; window fixed in 0.3.23): while the panel is open, that
233
- session's approval requests ask the panel first (5 minutes by default). Clicking "allow once" / "reject" settles it
234
- immediately; **closing the panel** or letting the window expire hands the request back **unchanged** to the official
235
- path (the DSH UI shows the same card). 0.3.22's 8-second window was far too short for a human — the buttons went
236
- grey before anyone could click (reported as "the approval box stopped working"); the window is now 5 minutes and
237
- closing the panel hands off immediately instead of waiting it out.
238
- **Nothing is ever auto-approved** — `allowed-once` can only come from a click, and there is no "always allow".
239
- - The question enters the DSH session as a **plain user message** (`source: { kind: 'user' }`, host 0.3.19): earlier
240
- versions used `{kind:'plugin'}`, which DSH renders as a *context update* — it did not look like something the user
241
- said. Provenance stays in the first line of the text: `From the editor: <file>[:<line>]`.
242
- - **Read-only with one constrained exception**: the bridge never writes files, applies edits, or runs commands; the
243
- single non-read-only route is `POST /approve`, which can only **answer an approval request that already exists**
244
- (see invariant 2 below). The agent's writes still go through its own `fs` tools; the bridge only *knows about* them
245
- and carries your answer back.
246
- - Status bar shows `$(plug) DSH` while connected (click it for the log in the "DSH Editor Bridge" output channel).
247
- - **The extension ships as a built-in** (fixed in 0.3.12): `dshcs-editor-bridge` is installed into
248
- `<tree>/lib/vscode/extensions/` next to `dshcs-open-file`. 0.3.0–0.3.11 installed it as a *user* extension
249
- instead, and the VS Code server marks any extension that sits in the user extensions folder but in no profile
250
- manifest as removed (`.obsolete`, log line `Marked extension as removed`) and then skips it forever — re-marked on
251
- every start, so **the bridge never reported any state**. To turn the bridge off use the plugin setting
252
- `editorBridge=false` (no mount, no tools) rather than uninstalling the extension from the Extensions view.
253
-
254
- ### The channels (since 0.3.13 over **local IPC**: a Windows named pipe / unix socket)
255
-
256
- ```
257
- extension → host POST /code-server-bridge/sync one round trip: push editor state (+ which session the panel watches) + take events and thread deltas
258
- extension → host POST /code-server-bridge/ask push an editor question into the current session
259
- extension → host POST /code-server-bridge/approve answer an approval request that **already exists** (the only non-read-only route)
260
- extension → host GET /code-server-bridge/health unauthenticated liveness probe
261
- extension → host POST /code-server-bridge/event extension reports open/close etc. (host log tail)
262
- host → extension <extensionsDir>/.dshcs-bridge/bridge.json endpoint + token, re-read every 5s
263
- (the same content is also written **next to the built-in extension** in
264
- `<tree>/lib/vscode/extensions/.dshcs-bridge/` — the env var is only injected when the host
265
- spawns the IDE, and an **adopted** IDE is a process from an earlier start that never saw it,
266
- so the extension must be able to find the config from its own location alone)
267
- ```
268
-
269
- Requests use `http.request({ socketPath })` (`fetch` has no socket support) and **no port is ever opened**.
270
-
271
- The three `/sync` fields the panel actually consumes (0.3.22):
272
-
273
- | Field | Content | How the panel uses it |
274
- |---|---|---|
275
- | `thread` | **new content** entries (user / assistant / tool / approval) of the session the panel watches; bounded: ≤120 entries per session, ≤8000 chars per body, ≤4 watched sessions | assistant bodies go to the official renderer; tools and approvals become compact summary rows |
276
- | `approvals` | pending approval requests `[{id, toolName, reason, at}]` (≤4) | renders the card with a countdown; a click posts `/approve` |
277
- | `approvalHoldMs` / `uiVersion` | the approval window (300000 ms = 5 minutes by default) / the DSH UI version | countdown basis; a renderer-version mismatch is surfaced in the panel |
278
-
279
- > **Why not HTTP (settled in 0.3.13, all three measured)**
280
- > 1. **Desktop has no HTTP surface at all**: the renderer calls `host.fetch()` through Electron IPC
281
- > (`createSharedFetchHandler('/api')` in `apps/desktop-host/src/index.ts:308`) — an in-process call, unreachable
282
- > from another process; the only HTTP a plugin can mount is the web profile's `webServer`.
283
- > 2. **`/api` cannot carry it either**: Connection puts a Host/Origin/cookie fence on `/api`
284
- > (`requestRejection` in `packages/client/connection/src/index.ts` → 401 without a cookie), while the bridge's
285
- > client is a **Node process inside the extension host** — it can never hold a browser cookie. Measured on 0.3.7:
286
- > polling `/api/code-server/bridge/sync` returned either 405 (it reached the launcher/VS Code) or 401 (the fence)
287
- > — the bridge had never actually synced.
288
- > 3. The two ends are **processes on the same machine** anyway (extension host ← the IDE the plugin spawned ← the
289
- > plugin). Local IPC is strictly smaller than a port: no network surface, no Host/Origin confused-deputy path, and
290
- > **web and desktop share one path**. Token auth stays (see below); the Windows pipe name carries a random suffix
291
- > and the POSIX socket file is `chmod 0600`.
292
- >
293
- > History: 0.3.9–0.3.12 mounted it on DSH's `webServer` prefix — which left desktop permanently dormant.
294
-
295
- **Why state is pushed, not pulled**: the extension host is a child process of the VS Code server and **listens on
296
- no port** — the host cannot call into it. Editor state therefore rides the extension's own polling request, and
297
- the host caches it for the tools (at most one 600 ms cycle behind; older than 10 s and the tool says so instead
298
- of passing stale data off as fresh).
299
-
300
- **Why no SSE/WebSocket**: the extension host has no HTTP server of its own; the bridge's shape is one
301
- request/response round trip every 600 ms. Polling also buys two useful properties: it is idempotent (a dropped event
302
- only costs one notification — the data always lives in the editor) and the cached state is inherently fresh.
303
-
304
- ### Security model (five invariants; read before touching `lib/bridge.mjs`)
305
-
306
- The token lives in `<extensionsDir>/.dshcs-bridge/bridge.json`, **readable by any process of the same local
307
- user**, so:
308
-
309
- 1. **`/code-server-bridge/*` is read-only, with `/approve` as the single exception.** No route writes files,
310
- edits documents, runs commands, or spawns processes. A leaked token is therefore bounded to "sees information
311
- that is in the editor" and **can never** become arbitrary file writes or command execution. A whitelist
312
- assertion in `scripts/test-bridge-routes.mjs` guards this.
313
- 2. **The four constraints on `/approve`** (drop one and it becomes an arbitrary-command-execution back door):
314
- (a) it can only **answer** an approval request that already exists — the body is exactly `{id, outcome}`, with
315
- **no free text, paths, or command arguments**, so it can answer questions but never start an action;
316
- (b) `id` must belong to a request this process created and that is **still pending** (single use);
317
- (c) `outcome` accepts only `allowed-once` / `rejected` — there is **no "always allow"**;
318
- (d) when no panel is watching, the panel is closed, or the window (5 minutes by default) expires, the request goes
319
- **back to the official
320
- path** — never auto-approved (DSH's `approval/request` itself fails closed; this bridge can only keep
321
- "nobody answered" as "nobody answered"). `pnpm test:webview` asserts these four plus the host-side whitelist.
322
- 3. **Any request carrying `Origin` gets 403.** Browsers always send one (including a sandboxed iframe's literal
323
- `Origin: null`); the Node extension host never does. Origin is checked **before** the token — otherwise the
324
- bridge would be a "did you guess the token right" oracle for a web page.
325
- 4. **Paths are confined to the editor's current workspace folders.**
326
- 5. **Everything is bounded**: 200 diagnostics, 500-char messages, 256 KB request bodies, a 64-entry event ring,
327
- ≤120 thread entries per session (≤8000 chars each, ≤4 watched sessions) and ≤4 pending approvals.
328
-
329
- This layer stops "another local app or a browser page that got hold of the file". A malicious program running as
330
- the same user could read your files and the token anyway — that is outside this plugin's threat model, exactly
331
- as stated for the loopback port.
332
-
333
- ### Turning it off / diagnostics
334
-
335
- | How | Effect |
336
- |---|---|
337
- | `config.editorBridge: false` in `cordis.patch.yml` | next start writes no `bridge.json` and registers no tools |
338
- | `code-server.editorBridge: false` in the settings document | **immediate**: config removed, tools unregistered, the extension goes dormant |
339
- | disable the `dshcs-editor-bridge` extension inside the IDE | the bridge simply becomes unavailable |
340
-
341
- Diagnostics: `GET /api/code-server/status` exposes
342
- `bridge: { enabled, live, toolsRegistered, supported, url, file }` — **never the token** (that only exists in the file).
343
-
344
- ## Legacy DSH (unsupported since 0.2.3)
345
-
346
- **Behaviour**: when `sidebarRightTabs` / `sidebarRight` cannot be found, the plugin registers a single settings card:
347
-
348
- > **Code Server** — this DSH version is unsupported (no right-sidebar service)
349
- > Since 0.2.3 this plugin no longer supports older DSH versions.
350
- > The right-sidebar plugin services `sidebarRightTabs` / `sidebarRight` were not detected, so the plugin exposes no
351
- > entry point at all (the old floating ball and floating window have been removed) and will not start the IDE in the
352
- > background. Upgrade DSH to a version with the right sidebar (>= 0.1.5-alpha.1): Code Server then appears as a
353
- > right-sidebar tab, this page shows the full settings again, and no reinstall is needed — a page refresh is enough.
354
-
355
- - **No other UI**: no `shell.overlay` registration (floating ball), no file-address claim, no resident preload.
356
- - **Host side**: the client posts `/api/code-server/ui-mode { sidebar:false }`; the host then ① stops auto-prestarting
357
- the IDE (`maybePrestart` returns immediately) and ② **recycles** an instance it had just auto-prestarted (unless it
358
- was adopted), so no unusable IDE process or port is left behind. A user-started/adopted instance is never stopped.
359
- - **Why delete instead of keeping**: the internal floating window was a stopgap from the era of early-2026 DSH builds
360
- without right-sidebar services. The resident surface, clipboard handling, shortcuts and panel collapsing all build on
361
- DSH's right sidebar, so maintaining two carriers costs more than it is worth. Older-DSH users should stay on `0.2.2`
362
- (`dsh plugin --profile web add dsh-code-server-app@0.2.2`).
363
-
364
- ## code-server workspace and process lifecycle
365
-
366
- - code-server's workspace **follows the active DSH session/workspace**: switching sessions/workspaces while the IDE is open moves code-server to the new directory
367
- (resolution order: current session cwd → session's workspace.path → recentWorkspace.path → first workspace.path);
368
- the opened directory is shown inside code-server (`?folder=<cwd>`, the page reloads when following a switch);
369
- implementation note: the iframe `src` must carry `?folder=<cwd>` — code-server's front-end remembers the "last workspace" and restores it by itself;
370
- a bare root URL only shows the previously opened directory and does not follow switches (verified locally).
371
- **Windows path format (verified)**: the `folder` parameter must start with `/` and use forward slashes only, e.g. `/C:/Users/User/Desktop/biss`;
372
- 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),
373
- while `file:///C:/...` reports "Workspace does not exist".
374
- - **The switch is lightweight (since 0.2.12)**: a running instance is **not restarted** when the workspace changes — the host
375
- only updates `state.cwd` and the workbench re-navigates with the new `?folder=` (the workspace directory was always the
376
- client URL's business; the process cwd only affects the server's own relative-path resolution at spawn time). The switch is
377
- much faster and no longer throws away the extension host, background tasks or server-side state.
378
- - In `status`, `cwd` is the current workbench directory; `launchCwd` is the directory the **process was started with**
379
- (diagnostics only; it does not change on a switch).
380
- - The trade-off, stated plainly: background processes/terminals started by the IDE in the **old** directory are no longer
381
- killed automatically (a full restart used to take them with it) — clean them up yourself if needed. That is the same coin
382
- as "nothing is lost".
383
- - The trigger does not depend on the tab being visible: **the tab body is not unmounted while the sidebar is collapsed**,
384
- so it also follows in the background (behaviour deliberately kept in 0.2.12).
385
- - Regression: `scripts/test-workspace-switch.mjs` (5 assertions: adopting an instance, cwd change keeps pid/status, the
386
- process stays alive, same-directory idempotence, no-cwd does not switch; it fails again if the old behaviour returns).
387
- - 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),
388
- 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;
389
- - `node_modules` and the pack-time artifact `vendor/` are git-ignored; after cloning, follow
390
- "Install the plugin (script-free install; code-server bundled)" below — `pnpm install` → `pnpm run build:client` →
391
- `pnpm run vendor:vscode` → `pnpm pack` + `dsh plugin --profile web add`.
392
-
393
- > Verified locally (BM: Windows 11 ARM64): the whole tree/dependency chain hangs directly off the plugin's
394
- > dependency table — the tree package `@jinsiyu/dshcs-vscode-server` (currently 4.137.0, a 50.8 MB tarball),
395
- > the pure-JS inner dependencies plus the 8 platform-independent repacks in `dependencies`, and the 8
396
- > platform-specific repacks (win32-arm64 / win32-x64) in `optionalDependencies` with their own os/cpu gates;
397
- > the original names are restored by junctions created at runtime (`lib/native.js`)
398
- > → healthz 200 → stopped → fully recycled.
399
- > (The 0.1.37-era "one big platform package" layout is gone — see "Upgrading the VS Code tree" below.)
400
-
401
- ## Packaging (how to build the tarball)
402
-
403
- ```powershell
404
- cd C:\Users\User\Desktop\dsh-code-server-app
405
- pnpm install # dev deps (esbuild + the official-renderer bundling deps); allowBuilds is explicit → no postinstall runs
406
- pnpm run build:client # src/factory.js → lib/client.js (not committed; must be built first)
407
- pnpm run build:webview # ask panel: official Markdown renderer + panel shell → webview/thread.{js,css} (not committed; must be built first)
408
- pnpm run vendor:check # optional: show the bundled tree version vs the latest code-server release
409
- pnpm run vendor:vscode # ① produce vendor/vscode (the trimmed VS Code tree, ~197MB)
410
- pnpm run repack:build -- --target win32-arm64,win32-x64 --pack # ② one script builds every sub-package
411
- pnpm run publish:repacks # ③ publish every @jinsiyu/* sub-package (default dist-tag: next)
412
- pnpm pack # ④ → dsh-code-server-app-<version>.tgz (~750KB, including the panel renderer assets)
413
- pnpm run publish:plugin # ⑤ publish the plugin itself (default dist-tag: next)
414
- # once the user has restarted dsh web and confirmed it works, promote latest:
415
- pnpm run promote -- <version>
416
- ```
417
-
418
- > **dist-tag policy (mandatory)**: every release goes to **`next`** and **never touches `latest`**;
419
- > `latest` always points at the most recent *confirmed bug-free* version and is only moved by
420
- > `pnpm run promote -- <version>` (= `npm dist-tag add dsh-code-server-app@<version> latest`)
421
- > **after the user restarts `dsh web` and confirms it works**. That way
422
- > `dsh plugin add dsh-code-server-app` (no version) — and anything else resolving `latest` — never picks up an
423
- > unverified build. Sub-packages (`@jinsiyu/dshcs-*`) are referenced by exact versions,
424
- > so their dist-tags do not affect resolution, but they default to `next` as well.
425
- > Inspect the current tags with `npm dist-tag ls dsh-code-server-app`.
426
-
427
- > `build:webview` bundles DSH's **official** Markdown renderer and design tokens into the panel assets
428
- > (~1.34MB: 996KB JS + 87KB CSS + 254KB KaTeX fonts), so it needs a local DSH deployment: the script reads the
429
- > `@deepseek-ai/dsh-web-frontend` version from that deployment and compares it with the renderer version pinned in
430
- > devDependencies — a mismatch **fails the build** (unless `--allow-version-mismatch`). Same convention as
431
- > `lib/client.js`: the artifacts are not committed and `prepack` rebuilds them.
432
- > Rationale (why not an iframe, where the tokens come from, size trade-offs) is section 21 of
433
- > `docs/analysis-code-server-as-dsh-plugin.md`.
434
-
435
- `repack:build` (`scripts/vendor-repacks.mjs`) is the **single script that produces every sub-package**:
436
-
437
- | Sub-package | Content | os/cpu |
438
- |---|---|---|
439
- | `@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 |
440
- | `@jinsiyu/dshcs-<name>[-win32-<arch>]` ×16 | the VS Code inner packages that need building (node-pty / @vscode/sqlite3 / kerberos / koffi / ssh2 / …) | gated when platform-specific |
441
- | `lib/vendored.json` (**not** a package) | the "original name → repack sub-package" table shipped inside the plugin; `lib/native.js` uses it to create the junctions. Since 0.3.45 there is **no platform aggregator** | — |
442
-
443
- | Goal | Command |
444
- |---|---|
445
- | **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 |
446
- | **Pin a version** | `pnpm run vendor:vscode -- --version 4.137.0` |
447
- | **Snapshot from an existing tree** | `pnpm run vendor:vscode -- --from <code-server dir>` (seconds) |
448
- | **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) |
449
- | **Rebuild only the tree + dependency table** | `node scripts/vendor-repacks.mjs --reuse --target win32-arm64,win32-x64 --pack` (reuses the natives already in `repack/build`; also rewrites `lib/vendored.json` and the plugin dependency table) |
450
- | **Publish sub-packages** | `pnpm run publish:repacks` (`--dry-run` to preview; `--only <substr>` to filter; `--otp <code>` / `--limit N` for 2FA) |
451
- | **Publish the plugin itself** | `pnpm run publish:plugin` (publishes the exact tarball that was verified; no re-packing; default dist-tag `next`) |
452
- | **Promote `latest`** | `pnpm run promote -- <version>` (only after the user restarted and confirmed; `--dry-run` shows the current tags first) |
453
- | **Just report versions** | `pnpm run vendor:check` |
454
-
455
- > `pnpm pack`'s `prepack` runs the vendor-code-server script once; when `vendor/code-server` already exists it is
456
- > a **no-op that takes seconds**, so after ordinary code changes you can just run `pnpm pack` (it will never
457
- > silently upgrade code-server). Upgrading code-server requires an explicit `pnpm run vendor:latest`
458
- > (or `--force` / `--version`) **plus** republishing the sub-packages.
459
-
460
- ## Install the plugin (one command; all dependencies installed by the package manager)
461
-
462
- ```powershell
463
- # no postinstall in the package → no pnpm approve-builds / allowBuilds; one command installs everything
464
- dsh plugin --profile web add dsh-code-server-app@0.2.1
465
- # a local tarball works the same way:
466
- dsh plugin --profile web add C:\Users\User\Desktop\dsh-code-server-app\dsh-code-server-app-0.2.1.tgz
467
- ```
468
-
469
- Ready to use immediately — **no second step, no "Install environment", no install-guide modal**.
470
- The main package is only **~110KB** (the plugin's own code plus the launcher); everything else is dependencies:
471
-
472
- - **the VS Code tree** (`lib/vscode` 196.9MB + `out/browser` + `src/browser`) is a **platform-independent package**
473
- `@jinsiyu/dshcs-vscode-server@<code-server version>` declared in the plugin's `dependencies`; it runs from
474
- `<profile>\node_modules\@jinsiyu\dshcs-vscode-server\vscode` (the legacy full tree at
475
- `@jinsiyu/dshcs-code-server/code-server` is still recognised as a fallback);
476
- - the **pure-JS part** of VS Code's inner dependencies (35 packages: xterm / katex / typescript / ws / tar …) is
477
- declared in the plugin's `dependencies` and installed by pnpm into the profile's `node_modules` (hoisted);
478
- - the **binary part** comes entirely from `@jinsiyu/dshcs-*` sub-packages, declared **directly on the plugin's own
479
- dependency table** (since 0.3.45): the 8 platform-independent repacks (`node-pty` / `koffi` / `ssh2` /
480
- `cpu-features` / `@parcel/watcher` / `@vscode/fs-copyfile` / `@vscode/proxy-agent` / `@microsoft/mxc-sdk`) go into
481
- `dependencies` under their real names; the 8 platform-specific ones (`@vscode/sqlite3` / `spdlog` / `kerberos` /
482
- `deviceid` / `native-watchdog` / `windows-registry` / `windows-process-tree` / `windows-ca-certs`) go into
483
- `optionalDependencies` once per target (real names + their own os/cpu gates), so one command picks the right arch;
484
- the **original names** are restored at runtime by junctions created from `lib/vendored.json`;
485
- - consequently the dependency graph contains **no package with pre/install/postinstall or a `binding.gyp`** →
486
- no profile `allowBuilds`, no build script ever runs, and **the user machine needs no C++ toolchain**;
487
- - **upgrading the plugin no longer re-downloads the tree**: the tree package is cached by version
488
- (~60MB, ~197MB unpacked).
489
-
490
- ### Install mechanism (why it is built this way)
491
-
492
- - **The pnpm 11 hard constraint**: any package in the dependency graph whose manifest has
493
- `preinstall|install|postinstall` (or that ships a `binding.gyp`/`.hooks`) counts as "needs building" and must be
494
- approved by the **host profile's** `pnpm-workspace.yaml` via `allowBuilds`, otherwise `dsh plugin add` exits 1 with
495
- `[ERR_PNPM_IGNORED_BUILDS]`. A dependency's own `pnpm.allowBuilds`, `.npmrc`, `patch:` protocol and
496
- `optionalDependencies` do not help (measured 2026-09, pnpm 11.25);
497
- - **the tree** is prepared at pack time with `npm install code-server@<version> --ignore-scripts` (skipping the official
498
- `sh ./postinstall.sh`, which cannot run on Windows), then `scripts/vendor-vscode-server.mjs` keeps **only the VS Code
499
- tree**: `lib/vscode/**`, `out/browser/**`, `src/browser/**` plus the license files are copied to `vendor/vscode/`, and a
500
- generated root `package.json` records the upstream code-server version. code-server's own `out/node/**` and its 136
501
- runtime dependencies **no longer ship** — they are replaced by `lib/launcher.mjs`;
502
- - **the packages that need a toolchain** are repacked into `@jinsiyu/dshcs-*` by `scripts/vendor-repacks.mjs`:
503
- the compiled package directory is copied and its `scripts` / `files` / `binding.gyp` / `.hooks` / `.npmignore` are
504
- **removed** (the built `.node` and every runtime file stay) → sibling packages in its dependency list become `npm:`
505
- aliases → platform-specific ones get `os`/`cpu` plus a `-<platform>-<arch>` suffix. For win32 targets the script also
506
- verifies each `.node` PE machine (0x8664=x64 / 0xaa64=arm64) so a cross-compiled artifact cannot ship the wrong arch;
507
- - **how the original names come back** (since 0.3.45): a repack's real name is `@<scope>/dshcs-<name>` while VS Code
508
- imports `node-pty` / `@vscode/sqlite3`; pack time writes the "original name → real name" table into
509
- `lib/vendored.json` (shipped with the plugin) and `lib/native.js` creates `<tree>/node_modules/<original name>`
510
- junctions to the real directories (idempotent, self-healing).
511
- **Why the old "platform aggregator + `npm:` aliases" is gone**: pnpm's incremental hoisted install drops those
512
- aliased packages when they sit inside an **optional subtree** (measured: 9 of 16 missing) while dsh-desktop
513
- validates the dependency graph right after the install ⇒ the first install always failed with `requires missing`;
514
- with real-name direct dependencies the same install command plus the validator's own predicate passes end to end
515
- (reproduction in `docs/desktop-first-install-root-cause.md`);
516
- - **resolution path**: the host finds the tree with `require.resolve('@jinsiyu/dshcs-vscode-server/package.json')`
517
- (then the inner `vscode/` directory) and the entry is `vscode/lib/vscode/out/server-main.js`; VS Code's inner deps are
518
- resolved upwards from that root (`vscode/lib/vscode/node_modules` → package `node_modules` → `<profile>/node_modules`).
519
- The legacy full tree (`@jinsiyu/dshcs-code-server/code-server`) is still recognised as a fallback;
520
- - **runtime layout self-healing** (`ensureRuntimeLayout()` in `lib/native.js`, idempotent, run **at activation before
521
- `envCheck` and again before every start**): the host adds two kinds of **junctions** (Windows junctions / POSIX dir
522
- symlinks) into the tree:
523
- 1. `ensureAliasLinks()`: links the 16 **original names** listed in `lib/vendored.json` into `<tree>/node_modules`
524
- — the real-name packages live in the plugin's dependency graph, and `lib/vscode/out/server-main.js` uses
525
- **ESM imports** (ESM ignores `NODE_PATH`), so a missing link means an immediate 500;
526
- 2. `ensureInnerModuleLinks()`: restores VS Code's **inner dependency directories**
527
- `lib/vscode/node_modules` and `lib/vscode/extensions/node_modules` from the two `package.json` files — the trimmed
528
- tree ships neither, and code that builds dependency paths explicitly (e.g. the bundled TypeScript extension looking
529
- for `<ext>/../node_modules/typescript/lib/tsserver.js`) otherwise reports
530
- "VS Code's tsserver was deleted by another application…" (measured with 1.136.1).
531
- > **Size note**: the plugin tarball is **~110KB**; `@jinsiyu/dshcs-vscode-server` is **~60MB** (~197MB unpacked);
532
- > the 16 native packages add ~250MB. A full install downloads roughly 310MB. Neither `vendor/` nor `repack/` is committed to git (see `.gitignore`).
533
-
534
- > **Upgrading from ≤ 0.1.43**: the tree package changed from `@jinsiyu/dshcs-code-server` (the full code-server tree with
535
- > `out/node` and 136 runtime deps) to `@jinsiyu/dshcs-vscode-server` (the trimmed tree). **The new code defaults to
536
- > `serve: loopback`, which behaves exactly like 0.1.43**; switch to `serve: dsh` for same-origin mounting. The install
537
- > command is unchanged (`dsh plugin --profile web add dsh-code-server-app@<version>`), and pnpm drops the old
538
- > `dshcs-code-server` sub-package.
539
- ### Development: install from source (changes take effect immediately)
540
-
541
- ```powershell
542
- dsh plugin --profile web add C:\Users\User\Desktop\dsh-code-server-app
543
- ```
544
-
545
- > A source path installs via `link:`. On a dev machine without `vendor/code-server`, run
546
- > `pnpm run vendor:vscode -- --dev-links` first. Dependencies (inner JS deps + the platform aggregator) are installed by pnpm
547
- > too — but the not-yet-published local `@jinsiyu/*` packages must either be published first, or the
548
- > `repack/tgz/*.tgz` files must be installed into the profile as `file:` dependencies.
549
- >
550
- > **Changing the client bundle**: edit `src/factory.js` then run `pnpm run build:client`
551
- > to regenerate `lib/client.js` (that artifact is not tracked; a browser refresh picks it up — no host restart needed).
552
- > **Changing the ask panel**: edit `assets/extensions/dshcs-editor-bridge/webview/src/*` then run
553
- > `pnpm run build:webview` (same convention: generated, not tracked; the IDE must be restarted once to pick it up,
554
- > because the extension host caches the webview resources).
555
-
556
- ### Pack-machine environment (the user machine needs nothing)
557
-
558
- **A toolchain is needed at pack time only — never on the user machine.**
559
-
560
- | Env | Version / requirement | User machine | Pack machine |
561
- |---|---|---|---|
562
- | Node.js | **v24.x** (latest code-server requirement; v24.13.1 here) | required | required |
563
- | npm / pnpm | npm ships with Node; pnpm comes from DSH | required (installs deps) | required |
564
- | **MSVC build tools** | **VS Community 2026 + C++ desktop workload** | ❌ **not needed** | pack time (16 native packages) |
565
- | **VS Spectre-mitigated libs** | one set for ARM64 **and** one for x86/x64 ("MSVC v14x Spectre-mitigated libs") | ❌ not needed | pack time (otherwise MSB8040) |
566
- | Python | **3.13.x** | ❌ not needed | pack time (node-gyp) |
567
- | node-gyp | **13.x** (older versions don't recognize VS 2026) | ❌ not needed | pack time |
568
-
569
- > **Packing still works without the Spectre libs**: when a package fails to compile, `vendor-repacks.mjs` downgrades
570
- > `SpectreMitigation` to `false` in that architecture's `*.gyp` files and retries (only the Spectre hardening is
571
- > lost, functionality is unaffected) and says so in the log.
572
-
573
- ### Windows native build notes (pack time, verified locally ARM64)
574
-
575
- - **VS needs the Spectre-mitigated libraries** (MSB8040): Visual Studio Installer → Individual components →
576
- "MSVC v14x Spectre-mitigated libs" — **install the ARM64 and the x86/x64 sets separately**.
577
- - **node-gyp 13.x** (9.x does not recognize VS 2026): `npm install -g node-gyp@latest`.
578
- - **x64 cross-compiling**: `vendor-repacks.mjs` uses `npm install --os=win32 --cpu=x64 --ignore-scripts` to fetch
579
- the packages, then `npm rebuild --arch=x64` per package; the resulting PE machine types were verified
580
- (kerberos / sqlite3 / spdlog …).
581
- - Latest code-server requires **Node v24**.
582
- - If you don't need the self-contained install (e.g. a global code-server already exists), skip it:
583
- the plugin falls back to a configured/PATH `bin` (see the "Config" table).
584
-
585
- ### Upgrading the VS Code tree (upstream = a code-server release)
586
-
587
- - **The version is decided at pack time**: `pnpm run vendor:latest` (= `--force`) pulls the tree of the npm **latest**
588
- release; or use `pnpm run vendor:vscode -- --version 4.137.0` / `DSHCS_CODE_SERVER_VERSION`.
589
- With an existing `vendor/vscode`, a plain `pnpm pack` never upgrades (it is a no-op).
590
- **Source-tree precedence (fixed in 0.2.13)**: an explicit `--from` uses that tree, while an explicit
591
- `--force`/`--version` now **always goes to the registry** — before the fix a local source tree won
592
- (`defaultSourceTree()` hit `vendor/code-server` or an installed profile tree first), so the documented
593
- "`vendor:latest` takes latest from the registry" silently kept the old version (hit while upgrading to 0.2.13:
594
- the bundled tree stayed at 4.136.2). A run with neither flag still reuses a local tree to save the download.
595
- - **Sync the inner dependency pins when the tree changes**: in `--reuse` mode the pure-JS install set is read from
596
- the **plugin's package.json**, so update those pins from the new tree's `lib/vscode/package.json` first
597
- (this time: 10 `@xterm/*` beta bumps). The rule is "only move a pin that no longer satisfies the new range",
598
- which keeps already-newer pins such as `cookie`/`ws`/`tar`/`node-addon-api` from being downgraded.
599
- - **Check first**: `pnpm run vendor:check` prints the bundled version / upstream latest.
600
- - **A version bump means rebuilding and republishing the sub-packages** (all with the same script):
601
- 1. `pnpm run repack:build -- --target win32-arm64,win32-x64 --pack` → the new tree package
602
- (`@jinsiyu/dshcs-vscode-server@<new version>`) and the natives rebuilt against the new inner dependencies (the
603
- script also rewrites the plugin's pure-JS `dependencies` and both aggregator versions);
604
- 2. `pnpm run republish:repacks` (`pnpm run publish:repacks`) → publish; then bump the plugin version → `pnpm pack`
605
- → publish the plugin.
606
- - `productPath` (`<quality>-<commit>`, part of the client WebSocket path) is **computed from `lib/vscode/product.json`**,
607
- so upgrading the tree needs no code change — but the routes are registered at activation, so restart `dsh web` afterwards.
608
- - **No runtime auto-upgrade anymore**: nothing fetches latest at startup; the version is fully determined by the bundled artifact.
609
- - Bundled locally right now: the tree of `code-server@4.137.0` (VS Code 1.137.0, `productPath=stable-b11dabda…`).
610
-
611
- ### Compatibility with the old install locations
612
-
613
- Host probe order: `@jinsiyu/dshcs-vscode-server/vscode` (**the real layout since 0.2.0**) >
614
- `@jinsiyu/dshcs-code-server/code-server` (the full tree, 0.1.40–0.1.43) >
615
- `@jinsiyu/dshcs-code-server-<platform>-<arch>/code-server` (the 0.1.37 platform sub-packages) >
616
- the in-package `vendor/vscode` > the in-package `vendor/code-server` (development). The old install root
617
- `<profile>\.code-server-app` is only mentioned in a startup log line; nothing writes to it any more.
618
- ## Settings card (Settings → Plugins → Code Server)
619
-
620
- Modeled after dsh-auto-open-web's custom card, registered on the `settings.plugin.item` slot,
621
- persisted via the official settings domain (`settingsScope`, namespace `code-server`) into the official settings document:
622
-
623
- | Key | Default | Description |
624
- |---|---|---|
625
- | `claimExtensions` | `*;!md;!markdown;!html;!htm;!png;!jpg;!jpeg;!gif;!webp;!bmp;!ico;!svg;!pdf` | **Claim types** (0.2.11, replaces 0.2.5's `fileOpenScope`): decides by extension which files go to VS Code, semicolon-separated; `*` claims every other type, `!ext` excludes (exclusion wins). The default leaves the four categories DSH's preview renders well (markdown/html/images/PDF) to DSH and sends everything else to the IDE; an empty value claims nothing. **Scope (session vs absolute) is no longer distinguished** |
626
- | `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 |
627
- | `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) |
628
-
629
- (Since 0.2.9 the card keeps only those three settings; `windowedOpen` and `reserveComposer` are gone — leftover keys in an old
630
- settings document neither fail nor apply. `serve` remains a key in the settings namespace (usable from a settings document) but
631
- has **no card row** — see "Serving mode".)
632
-
633
- > Card changes take effect immediately via `scope.watch` (the host status API returns `keepResident`, `claimExtensions` and
634
- > `fullscreenOnOpen`; the client applies them at once); no dsh restart needed. **After adding new setting keys, restart dsh web before first use**,
635
- > so the host re-registers the settings namespace (schema includes the new key); otherwise save/validation of the new key won't work.
636
-
637
- Since 0.2.7 the card has **no** "Entry", "dependency install" or "environment check" rows: the entry lives in the sidebar's
638
- guide page (and in DSH's own file clicks), and diagnostics stay out of the UI — the `/api/code-server/status` `env` field still
639
- reports the tree version / `productPath` / server entry, VS Code inner dependencies and **prebuilt native packages**
640
- (platform aggregator name + resolved module count) for scripts, and the DSH host log carries the `[code-server]` lines.
641
-
642
- ## Config (`config` in cordis.patch.yml; all have defaults)
643
-
644
- | Key | Default | Description |
645
- |---|---|---|
646
- | `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 |
647
- | `host` | `127.0.0.1` | Bind address; `auth: none` only allows loopback (localhost/127.0.0.1/::1) |
648
- | `port` | `0` | Port for loopback mode; **`0` = a random free port assigned per start** (the actual one is written to `endpoint.json` and read back by the host). Give an explicit port to pin it; when that port is taken and no valid `pid.json` exists, startup fails with diagnostics instead of killing a stranger |
649
- | `auth` | `none` | `none` \| `password`; non-loopback host automatically requires password |
650
- | `passwordToken` | `''` | Token for password mode (passed to code-server via the `PASSWORD` env var) |
651
- | `userDataDir` | `$DSH_HOME/code-server/user-data` | User-data isolation directory |
652
- | `extensionsDir` | `$DSH_HOME/code-server/extensions` | Extensions directory |
653
- | `readyTimeoutMs` | `60000` | `/healthz` readiness probe timeout |
654
- | `editorBridge` | `true` | **Editor bridge** (since 0.3.0): the read-only channel between the in-tree `dshcs-editor-bridge` extension and the host (see "Working with DSH"). Off = no `bridge.json`, no `editor_context`/`editor_diagnostics`, the extension stays dormant. `code-server.editorBridge` in the settings document toggles it **live** |
655
-
656
- User-level override example (write in `$DSH_HOME/profiles/web/cordis.patch.yml`, using the `- id: code-server` row):
657
-
658
- ```yaml
659
- - id: code-server
660
- config:
661
- port: 8091
662
- # Explicit (overrides dependency-install probing): a globally installed shim, or any entry.js
663
- bin: C:\Users\User\AppData\Roaming\npm\code-server.cmd
664
- ```
665
-
666
- ## JSON API (same-origin fetch; identical paths in web and desktop)
667
-
668
- **No `webServer` dependency**: the host half registers its routes on DSH Connection's shared `/api` channel through
669
- `ctx.connection.fetch.register`. In the web profile Connection mounts the `/api` prefix on webServer itself (with the
670
- Host/Origin fence and browser auth); in the desktop profile `apps/desktop-host` feeds `/api/*` into the same
671
- `createSharedFetchHandler('/api')` (IPC framed pipe, no HTTP server). The client only writes relative paths
672
- (`fetch('/api/code-server/<op>')`), so both carriers behave identically.
673
-
674
- | Method | Path | Description |
675
- |---|---|---|
676
- | 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) |
677
- | POST | `/api/code-server/start` | body `{ cwd? }` (omit cwd to keep the current workspace); idempotent; changing cwd while running only **switches the directory, without restarting the process** (0.2.12) |
678
- | POST | `/api/code-server/stop` | Stop and recycle the process tree |
679
- | 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 |
680
- | 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 |
681
- | GET | `/code-server-bridge/health` | editor-bridge liveness (**unauthenticated**; no editor data). Runs over **local IPC** (named pipe / unix socket), not under `/api`, and needs no `webServer` |
682
- | POST | `/code-server-bridge/sync` | editor bridge: the extension pushes state (`{context, diagnostics, workspace, at}`) and takes back events; `?since=<seq>` is the event cursor. Requires `x-dshcs-bridge-token`, and **any Origin header is 403** |
683
- | POST | `/code-server-bridge/ask` | editor bridge: push an editor question into the current session (`{text, file?, lineStart?, lineEnd?, selection?, languageId?}`); **409** when no session can receive it |
684
- | POST | `/code-server-bridge/event` | editor bridge: extension reports open/close and similar (host log tail). Requires the token |
685
-
686
- > All four bridge routes carry their own token check — they **cannot** rely on DSH's cookie fence, because the
687
- > extension host has no browser cookie — and they are read-only by construction. See "Working with DSH" above.
688
-
689
- > The plugin no longer registers `/code-server/*` webServer-only routes, and the code-server icon is inlined as a data URI
690
- > in the client bundle — the client requests no plugin-owned HTTP resource at all.
691
-
692
- ## DSH Desktop (no webServer)
693
-
694
- - The host half is `inject = ['connection', 'settings']` (**no `webServer`**) — the desktop profile disables webserver/web-runtime
695
- and the plugin still works: `/api/*` requests travel Electron `dsh-app://` protocol handler → IPC framed pipe → `createSharedFetchHandler('/api')`.
696
- - The right-sidebar tab, guide entry box, file-address claim, and settings card behave the same as in web (code-server remains an
697
- 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).
698
- 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
699
- not a regression for desktop; the only difference is the missing `webServer`, where `serve: dsh` falls back to loopback (that path
700
- genuinely needs a webServer).
701
- - **The editor bridge works on desktop since 0.3.13**: it runs over local IPC (named pipe) and does not involve `webServer` at all —
702
- the extension host is a child of the IDE the plugin itself spawned, so both ends are on the same machine. The host injects
703
- `DSHCS_EXTENSIONS_DIR`, the extension finds `bridge.json`, and `/status` reports `bridge.supported=true` with the pipe name.
704
- - Install into the desktop profile through the **desktop plugin manager** (not the CLI, see below).
705
- - **Desktop installs face a 24-hour supply-chain policy (measured 2026-09-10; this is how 0.2.4 got installed)**:
706
- - the CLI path is unavailable: `dsh plugin --profile desktop …` is rejected (*"profile "desktop" is managed exclusively by the
707
- Electron application"*), so desktop installs only go through the app's package transaction (`pnpm add <spec> --save-exact`,
708
- executed in `~/.dsh/desktop/staging/<uuid>/profile` before activation);
709
- - that transaction's pnpm (the app bundles **11.7.0**, patched by DeepSeek) runs a **lockfile supply-chain verification** before
710
- `add` ("Verifying lockfile against supply-chain policies (717 entries)") which requires packages to be **at least 24 h old**,
711
- otherwise `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`;
712
- - **the two stages behave differently (measured)**:
713
- - verifying an **existing lockfile**: `minimumReleaseAgeExclude` is *not* honoured (exact versions and bare package names
714
- were both tried);
715
- - **resolution** (no lockfile to verify, e.g. after `pnpm clean --lockfile`): the list *is* honoured, and pnpm even appends
716
- entries itself (the install log prints *"Added N entries to minimumReleaseAgeExclude…"*);
717
- - so the working recipe for a **just-published** (<24 h) version on desktop is to start from a clean, lockfile-free profile:
718
- 1. `pnpm clean --lockfile` (**note: it also deletes `node_modules`**, leaving the profile to be reinstalled);
719
- 2. with the app's bundled runtime, run `add <spec> --save-exact --trust-lockfile` in the profile directory
720
- (runtime/store/config live under `~/.dsh/desktop/pnpm/{store,cache,state,config,home}`,
721
- `--config.userconfig=…/config/npmrc`, otherwise pnpm fails with `ERR_PNPM_UNEXPECTED_STORE` /
722
- `…UNEXPECTED_VIRTUAL_STORE`);
723
- 3. the app's boot command (`install --offline --frozen-lockfile --trust-lockfile`) then passes (lockfile matches
724
- package.json, packages are in the store); if the plugin name is already in `dsh.profile.bundles` nothing else is needed.
725
- - do **not** try to bypass it with `minimumReleaseAge: 0`: it does clear the check, but that key is **not** one of the policy
726
- sections the app tolerates (`project-manager.ts` ignores only `minimumReleaseAgeExclude:` / `trustPolicyExclude:` and validates
727
- the manifest before every `mutate()`), so persisting it makes the app fail with
728
- *"core package mapping does not match desktop-packages.json"*.
729
- - Alternatively **wait out the 24 h** and install normally from the plugin manager. The web profile is unaffected: its
730
- `pnpm-workspace.yaml` sets `minimumReleaseAge: false`.
731
- - this plugin's closure contains platform native sub-packages (`@jinsiyu/dsh-code-server-runtime-win32-*`) published together with
732
- the plugin itself, so every new version hits that policy on desktop.
733
- - **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):
734
- - symptom: `lib/client.js` in the profile is the new version, yet the renderer keeps running the old code —
735
- `%APPDATA%\@deepseek-ai\dsh-desktop\Code Cache\js` only contains strings unique to the old version (e.g. `dshcs-artifacts`)
736
- and none unique to the new one (`claimExtensions` / `fullscreenOnOpen`), and `Cache\` still holds an old response body referencing
737
- `dsh-code-server-app`. The same applies to first-party plugins (the cached `ui-deliverables` even lacks the current
738
- `data-presented-files-row` marker);
739
- - diagnosis (byte level — do **not** use `Select-String`, which reads files with the console encoding and gives false
740
- negatives on non-ASCII markers): search `Code Cache\js` for an **ASCII** marker unique to the new version
741
- (since 0.2.11 ours is `claimExtensions`); a hit proves the new bundle really was compiled;
742
- - fix: fully close the app, delete the `Cache`, `Code Cache` and `GPUCache` directories, then start it (cache only —
743
- profiles, sessions and settings are untouched):
744
- ```powershell
745
- 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"
746
- ```
747
- - scope: this is not specific to this plugin — **any** client plugin may keep running old code after an upgrade;
748
- after a release, confirm with the marker trick above that the renderer actually swapped bundles.
749
-
750
- ## File-open plumbing (what the plugin itself still does)
751
-
752
- DSH's own chips and preview buttons are what users click (see "Opening files" above); this plugin adds no row of its own.
753
- What remains on the plugin side:
754
-
755
- - the tab body parses `navigation.address` and posts the absolute path (plus an optional `line`) to
756
- `/api/code-server/open-file`, which writes a signal file;
757
- - the bundled `dshcs-open-file` extension polls that file and calls `showTextDocument` — VS Code Web has no official
758
- "open this file from outside" API, so this is the only way to aim the workbench at a file. It is installed as a
759
- **built-in** extension (in `lib/vscode/extensions`), so users cannot remove it from the extensions panel, and the
760
- installer re-syncs it whenever its content changes.
761
-
762
- ## Known limitations
763
-
764
- - **~~The editor bridge needs DSH to provide `webServer`~~ no longer true (fixed in 0.3.13)**: the bridge now runs
765
- over **local IPC** (Windows named pipe / unix socket via `http.request({ socketPath })`), so **web and desktop share
766
- one path**, with no `webServer` and no open port. History: 0.3.9–0.3.12 mounted it under DSH's webServer prefix
767
- (⇒ desktop stayed dormant); up to 0.3.7 it was registered under `/api/code-server/bridge/*` and was killed by
768
- Connection's cookie fence (401). **File opening** was never affected (it uses the signal file).
769
- - **`/code-server-bridge/health`'s `bridge` field does not mean the extension is running** (clarified in 0.3.12):
770
- it only says the bridge *target* is configured. Whether the extension actually runs shows up in the exthost log
771
- or by simply calling `editor_context` — 0.3.0–0.3.11 sat in the state "health says bridge:true, extension never
772
- loaded" (cause above: the user-level install was marked `.obsolete`).
773
- - **Bridged state can lag by up to 600 ms**, and the tools say "stale" rather than serving data older than 10 s.
774
- - **The ask panel renders only "new content" (0.3.22)**: the subscription starts when the panel opens, the history
775
- `records` from `follow`'s opening frame are discarded, and the panel has **no "load earlier"** (the history-paging
776
- API `sessionController.page()` is deliberately not called in this version). Switch to the DSH UI for older content.
777
- - **Panel highlighting ships only DSH's boot grammar set** (typescript / shellscript / json): the rest of the
778
- official grammars load lazily through `import()` (~1.6MB total), and the panel is a single-file IIFE with no
779
- lazy loading, so those languages render as plain text (exactly like DSH's own first render, no errors).
780
- For the full set: `node scripts/build-webview.mjs --all-grammars`.
781
- - **Panel assets are pinned to the DSH version**: the renderer is bundled against the UI version of the deployed
782
- DSH, so after upgrading DSH you must rebuild the panel (`pnpm run build:webview`; the build fails loudly on a
783
- version mismatch). The panel also shows a mismatch notice at runtime instead of silently using the wrong renderer.
784
- - **The approval window in the panel is 5 minutes**: while the panel is open, approvals ask the panel first (the card
785
- shows a countdown); **closing the panel** or letting the 5 minutes run out hands the request back to the DSH UI —
786
- after that, that request can **only** be answered there (the card disappears from the panel and the thread keeps an
787
- audit row).
788
- - **Unsaved buffers are reported, not taken over.** The agent still edits via its own `fs` tools, i.e. against
789
- disk. What the bridge adds is a notice *before* writing a dirty file, a diff *after*, and a warning instead of
790
- an overwrite. It does not decide whether the user saves — that would mean changing the agent's read path,
791
- which is out of scope for this version.
792
-
793
- - ~~No sub-path~~ **no longer true (corrected with measurements in 0.2.0)**: the workbench HTML VS Code renders references
794
- **only relative URLs** (9 references measured, 0 absolute; `serverBasePath="."`, `rootEndpoint="."`), and the client
795
- builds its WebSocket path from `location.pathname + join(serverBasePath ?? '/', <quality>-<commit>)`. The IDE can
796
- therefore be mounted directly under DSH's own `/code-server/*` (`serve: dsh`) — no second port, no HTML rewriting.
797
- Item-by-item evidence: `docs/analysis-code-server-as-dsh-plugin.md`.
798
- - **`serve: dsh` cannot proxy forwarded-port WebSockets**: `registerUpgrade` matches exact paths while `/proxy/:port`
799
- carries the port in the path, so WebSocket forwarding for the Ports panel is unavailable in that mode (HTTP forwarding
800
- works). Use `serve: loopback` when you need it.
801
- - **`serve: dsh` shares DSH's origin**, so the iframe is not sandboxed there (same-origin plus `allow-same-origin` is
802
- escapable by the frame itself); in `loopback` mode the iframe is cross-origin and `sandbox` stays as real protection.
803
- - **Single instance across sessions**: one shared IDE per host; switching cwd only re-navigates the workbench (since 0.2.12 no process restart, so the old directory's background terminals are not collected).
804
- - **Older DSH versions are unsupported (since 0.2.3)**: on a DSH without `sidebarRightTabs` / `sidebarRight` the plugin
805
- offers nothing but an upgrade notice on the settings page; older-DSH users should stay on `0.2.2`
806
- (`dsh plugin --profile web add dsh-code-server-app@0.2.2`).
807
- - **Sidebar tab switching** (no longer reloads since 0.2.2): DSH's right sidebar renders only the active tab's body, and
808
- a React unmount moves the iframe away; the plugin keeps it as a singleton resident surface and shuttles it between the
809
- dock slot and a document-level park container with `Element.moveBefore()` (a state-preserving atomic move), so
810
- switching tabs or collapsing the sidebar and back **does not reload** it. Browsers without `moveBefore` fall back to
811
- the old behaviour (`appendChild` → full reload), reported as `degraded`; see "Why switching tabs no longer reloads".
812
- - **Remote access**: with `serve: dsh` the browser only needs to reach DSH itself (one port, protected exactly like `/api`);
813
- `serve: loopback` binds to loopback only (random port + path token + Host allowlist — see "Security model of the loopback
814
- port"), so use `serve: dsh` for cross-machine access (0.2.0 no longer supports `auth: password`).
815
- - **The loopback token rotates per instance**: port and token change on every new start; adoption after a host restart
816
- matches the live instance through the `endpoint.json` and `path-token` files, so **do not delete those two files**
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-*` sub-packages declared **directly on the plugin's dependency table** under their real names (os/cpu-gated per target), with the original import names restored by runtime junctions. 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
+ > Since 0.3.22 the ask panel renders the session's new content with **DSH's own Markdown renderer** (the same
16
+ > renderer and design tokens as the DSH UI; new content only) and can **answer approval requests in place**
17
+ > (writing outside the workspace / running commands). See "Working with DSH: the editor bridge" and section 21 of
18
+ > `docs/analysis-code-server-as-dsh-plugin.md`.
19
+
20
+ ## UI carrier and required DSH version (0.2.3: right-sidebar DSH only)
21
+
22
+ | DSH version | Carrier | Entry points |
23
+ |---|---|---|
24
+ | **>= 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"** |
25
+ | older (no sidebar service) | **Unsupported**: nothing but one notice on the settings page | none (Settings → Plugins → Code Server shows an upgrade notice) |
26
+
27
+ - 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).
28
+ 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"):
29
+ - services are looked up as `ctx.<name>` first and `ctx.get(name)` second, so either context shape registers;
30
+ - when the sync probe already sees the services but `inject` never calls back, registration falls back to the sync services after **1.5 s**;
31
+ - 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);
32
+ - services arriving late automatically revoke the legacy verdict, register the sidebar, and report `{sidebar:true}` so the host re-enables;
33
+ - 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".
34
+ - **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
35
+ - registers only the settings card (an upgrade notice) — no ball, no floating window, **no file-address claim**, no IDE preload;
36
+ - 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;
37
+ - upgrading DSH needs **no reinstall** — refresh the page and the card turns back into the full settings card.
38
+ - 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.
39
+ - **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.
40
+ 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.
41
+ Turn it off in the settings card (`fullscreenOnOpen=false`) to stay side by side.
42
+ - How it is done: DSH does **not** expose the mode to plugins — `ctx.sidebarRight` only has `isExpanded`/`toggleExpanded`
43
+ (expand/collapse), while push ⟷ fullscreen is recorded in `ui-sidebar-right`'s own store (`actions.setMode`, handed only to
44
+ its own seat components). `ctx.layout.openRightbar(track, fullscreen)` is not a control either: it is the channel the seat
45
+ **reports** its presentation through (upstream comment: *the occupant reports it; nothing else writes it*).
46
+ So the plugin performs the user's own gesture: it locates its own panel with `closest('[data-sidebar-right-panel]')` and
47
+ clicks the panel chrome's `[data-sidebar-right-mode="fullscreen"]` button (the exact same path as a manual click, including
48
+ the narrow-viewport handling). When the button is missing it keeps the current mode and logs one `console.warn` — panel
49
+ rendering is never affected.
50
+ - **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).
51
+ - The settings card has exactly **three settings**: "**Claim types**", "**Fullscreen on open**" and "Resident in background" — no other rows (0.2.7 removed the "Entry", "dependency install" and "environment check" rows).
52
+ 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;
53
+ 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).
54
+ 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).
55
+ To use the IDE in a browser tab, **copy the full address** from the settings card / empty-state hint (it contains the
56
+ path token: `http://127.0.0.1:<port>/<token>/`; under `serve: dsh` it is DSH's `/code-server/`) — dropping the token
57
+ segment yields a 404.
58
+
59
+ ## Opening files (official entry points since 0.2.5)
60
+
61
+ DSH names files with **resource addresses**; `openFile` only hands the address to the right sidebar, which decides who draws it:
62
+
63
+ ```
64
+ DSH's produced-file chip / presented-file card preview / inline prose mention
65
+ → openFile(path, { line? }) (provided by ui-chat)
66
+ → dsh-resource://file/session/<sessionId>/<path> (or …/file/absolute/<path>)
67
+ → ctx.sidebarRight.openResource(address)
68
+ → claimed by the tab type whose patterns match (band extension(3) > builtin(2) > fallback(1),
69
+ then the longest matching pattern, then registration order)
70
+ ```
71
+
72
+ This plugin registers:
73
+
74
+ | Field | Value | Effect |
75
+ |---|---|---|
76
+ | `patterns` | `['dsh-resource://file/**']` | claims file addresses (a pattern containing `:` is matched against the **whole address**) |
77
+ | `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 |
78
+ | `canOpen` | see below | vetoes by the "claim types" setting; unclaimed addresses fall back to DSH's built-in preview |
79
+ | `title` | last address segment (= file name) | the tab chip shows the file name; a page tab (`sidebar://code-server`) still reads `Code Server` |
80
+
81
+ - **Claim types** (a text box in the settings card, `claimExtensions`, since 0.2.11):
82
+ **scope is no longer a thing** — `dsh-resource://file/session/…` and `…/file/absolute/…` are treated alike,
83
+ and only the extension decides. Text-box grammar (semicolon-separated; `,`/whitespace/newlines also work;
84
+ `py`, `.py` and `*.py` are equivalent; case-insensitive):
85
+ - `*` — claim every other type too (catch-all);
86
+ - `py` — claim `.py`;
87
+ - `!md` — do **not** claim `.md` (**exclusion wins** over both an explicit claim and `*`);
88
+ - **default** `*;!md;!markdown;!html;!htm;!png;!jpg;!jpeg;!gif;!webp;!bmp;!ico;!svg;!pdf`
89
+ — the four categories DSH's own preview renders well (markdown / html / images / PDF) stay with it, everything
90
+ else (code, json/yaml, txt, logs, extension-less files such as `Makefile`, unknown extensions) goes to the IDE;
91
+ an empty box claims no files at all (page tabs only).
92
+ - Three practical shapes: a plain whitelist (`py;ts`, no `*` → nothing else is claimed), catch-all (`*`),
93
+ and catch-all plus exclusions (the default).
94
+ - Grammar, default and parsing all live in `lib/claim-types.js` (the host's `Config` default and the client's
95
+ `canOpen` share that single file, shipped in the package, so the two cannot drift apart);
96
+ unit tests: `scripts/test-claim-types.mjs`.
97
+ - **How the tab body locates the file**: it parses `useTabInfo().tab.navigation.address`
98
+ (`src/address.js`, same grammar as DSH's `parseFileAddress`), expands a workspace-relative path with that
99
+ session's cwd, and posts the absolute path (plus optional `line`) to the host's
100
+ `/api/code-server/open-file`; the bundled extension (`dshcs-open-file`) then calls `showTextDocument`
101
+ (positioned at the line when given).
102
+ - **One address = one tab** (DSH semantics: `contentId` *is* the address): three files mean three chips, but they
103
+ share the single resident workbench — switching tabs just re-aims the workbench at the corresponding file.
104
+ - **Why the bundled extension stays**: VS Code Web has no official "open this file from outside" API (the only
105
+ entry is `?folder=`, which picks the workspace), so aiming the workbench at a file has to be done by an
106
+ extension inside the tree. The host writes a signal file, the extension polls it and calls
107
+ `showTextDocument`, keeping the signal for retry when no window is connected yet.
108
+
109
+ ## Why switching tabs no longer reloads (resident IDE)
110
+
111
+ **The old trap**: DSH's right sidebar (ui-dockkit) renders **only the active tab's body**
112
+ (`TabPanel.tsx:412` → `renderTab(active)`) — switching to another tab unmounts that body in React, which moves the
113
+ iframe out of the document and destroys its browsing context; switching back is a full VS Code reload (unsaved buffers
114
+ lost). Floating the tab into its own panel only worked around it.
115
+
116
+ **What it does now (`src/surface.js` in the client, 0.2.2)**: the plugin takes the iframe **away from React** and turns
117
+ it into a **singleton resident surface**:
118
+
119
+ | Situation | Action | Result |
120
+ |---|---|---|
121
+ | tab becomes active | `host.moveBefore(frame, null)` into the visible dock slot | state-preserving atomic move, **no reload** |
122
+ | 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 |
123
+ | workspace / port changes | assign `src` explicitly | the only normal "reload" entry point |
124
+
125
+ - **Why `moveBefore`**: measured in a real browser (Edge/Chromium 151), a plain `appendChild` move resets the iframe's
126
+ internal timers (i.e. reloads it), while `Element.moveBefore()` (Chromium ≥133) preserves state (a probe counter keeps
127
+ counting 1→2).
128
+ - **Degradation is never silent**: when `moveBefore` is missing, or the host was already detached by React and it throws
129
+ `HierarchyRequestError: invalid hierarchy` (passive effect cleanup runs after DOM removal), the code falls back to
130
+ `appendChild` — one reload, but the frame is **never lost** — and reports `degraded` / `lastMoveError` so the UI can
131
+ say "residency unavailable".
132
+ - **Repaint fallback (measured)**: in the real GUI the surface was seen once with correct size, hit testing and
133
+ `visibility` that simply **stopped repainting** (a fully white panel, byte-identical screenshots proving no new frame).
134
+ `translateZ(0)` and `opacity` nudges did nothing; `display:none → forced reflow → restore` inside a single JS task
135
+ restored it without reloading the iframe document, without losing internal state and without a visible flash.
136
+ **The trigger could not be reproduced**: in a probe page an offscreen `moveBefore` park of 337 s (past Chrome's
137
+ ~5 min cross-origin throttle window) followed by a dock with the fallback disabled still painted normally. It is
138
+ therefore kept as a **fallback**: every park→dock transition runs one `nudgeRepaint()` (counted as
139
+ `surfaceSnapshot().nudgeCount`; `setNudgeEnabled(false)` A/Bs it live).
140
+ - **Warm-up**: with `keepResident` (default `true`) the host builds the surface right after plugin start and leaves it
141
+ parked, so the first tab open needs no cold start; preloading never yanks a surface that is currently docked.
142
+ - **Debug handle**: `window.__dshcsSurface` (`snapshot()`, `setParkStrategy('offscreen'|'behind')`, `dock()`, `park()`,
143
+ `nudge()`, `setNudgeEnabled(false)`, `destroy()`).
144
+
145
+ **Measured** (DSH web GUI, real mouse clicks between sidebar tabs): switching away → `docked:false`, same iframe node,
146
+ in-frame probe still alive, `degraded:false`; switching back → `docked:true`, unchanged `src`, IDE pixels and editing
147
+ state preserved (no full reload). Full evidence and probe scripts: `docs/analysis-code-server-as-dsh-plugin.md`.
148
+
149
+ ## Serving mode (`serve`)
150
+
151
+ | Mode | What it does | Requires |
152
+ |---|---|---|
153
+ | **`loopback` (default)** | the plugin listens on its own loopback port (**`port: 0` by default = a random port assigned per start**), the sidebar iframe connects cross-origin, and the URL carries a **random path token** (`http://127.0.0.1:<port>/<token>/`, see "Security model of the loopback port" below); the process can be adopted after a DSH host restart | nothing |
154
+ | **`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 |
155
+
156
+ ### Security model of the loopback port (since 0.2.14)
157
+
158
+ `loopback` is the only transport desktop has (no webServer, no same-origin mount), so it is hardened on its own:
159
+
160
+ - **Random port**: `port` defaults to `0` → the OS assigns a free port and the launcher writes the **actual** one to
161
+ `$DSH_HOME/code-server/endpoint.json`, which the host reads back. The port therefore changes on every start and the old
162
+ "8090 is busy" class of conflicts is gone. Pin `port` explicitly if you need a fixed address.
163
+ - **Path token**: a fresh 32-character token (`[0-9A-Za-z_-]`, 24 random bytes) is generated on every **new start**, stored in
164
+ `$DSH_HOME/code-server/path-token` (inside the user profile, readable only by the owner under the default ACL), and becomes
165
+ the URL path prefix. Requests without that prefix get a plain **404** (nothing reveals that an IDE lives there); a prefix
166
+ without the trailing slash is answered with a 302.
167
+ - **Why not VS Code's own `connection-token`**: it works through `?tkn=` → 302 + `Set-Cookie: vscode-tkn; SameSite=Lax`.
168
+ The desktop iframe is **cross-origin** (`dsh-app://` → `127.0.0.1`), and a Lax cookie is not sent from a cross-site
169
+ subframe — the IDE would simply fail to load. A path prefix needs no cookie at all: the workbench derives every asset and
170
+ WebSocket URL from `location.pathname` (the same mechanism already proven by mounting under `/code-server/` in `serve: dsh`),
171
+ so the prefix rides along on every subrequest and on the WS handshake. (Verified with a real Edge + CDP run: with a random
172
+ port and a token, the workbench renders **inside a cross-origin iframe** and establishes its WebSocket.)
173
+ - **Host allowlist**: in loopback mode only `127.0.0.1 | localhost | [::1] : <actual port>` is accepted. This is what stops
174
+ DNS rebinding, whose requests can arrive without an `Origin` header and therefore slip past the `Origin == Host` check.
175
+ - **`Referrer-Policy: no-referrer`**: the token lives in the path, so it must not leak through `Referer` when external resources load.
176
+ - **The token never reaches argv or the logs**: command lines are readable by any local process, so it travels through a file;
177
+ the log only says "enabled".
178
+
179
+ Boundary, stated plainly: this layer stops other local applications, port scanners and browser pages from casually reaching
180
+ your IDE. A **malicious program running as the same user** can already read your files and that token file — that is outside
181
+ this plugin's threat model.
182
+
183
+ - Switch it in `config.serve` in `cordis.patch.yml` or in Settings → Plugins → Code Server (takes effect on the next start).
184
+ - Benefits of `dsh`: a single URL/port (remote access to DSH gives you the IDE), no extra loopback listener, authentication on par with DSH.
185
+ - Two **known trade-offs** of `dsh`: the iframe shares DSH's origin, so `sandbox` is dropped there (same-origin plus
186
+ `allow-same-origin` is escapable by the frame itself; in `loopback` mode the iframe is cross-origin and `sandbox` stays
187
+ as real protection — clipboard is still granted via `allow="clipboard-read; clipboard-write"`); and forwarded-port
188
+ **WebSockets** cannot be routed because `registerUpgrade` matches exact paths while `/proxy/:port` carries the port in
189
+ the path (HTTP forwarding works; use `loopback` when you need WS forwarding).
190
+
191
+ - In `loopback` mode every upgrade passes a **code-server-equivalent Origin check** (since 0.2.1): when an `Origin`
192
+ header is present its host must equal `Host` (honouring `Forwarded: host=` / `X-Forwarded-Host`, like code-server),
193
+ otherwise the handshake gets `403`; non-browser requests without `Origin` are allowed. Without that check any local
194
+ browser page could complete a handshake against `ws://127.0.0.1:<port>/stable-<commit>` and drive the IDE.
195
+
196
+
197
+ ## Working with DSH: the editor bridge (since 0.3.0, on by default)
198
+
199
+ Having the IDE next to DSH and having the agent **know what is going on in the editor** are two different
200
+ things. The editor bridge covers the second half: it is a **read-only** channel that hands the agent what
201
+ only the editor knows, and lets editor gestures drive the current session.
202
+
203
+ | Direction | Capability | Mechanism |
204
+ |---|---|---|
205
+ | editor → agent | **unsaved buffers** (disk ≠ what the user sees), active file and selection, **language-server diagnostics** with `file:line`, source and code | agent tools `editor_context` / `editor_diagnostics`; plus a notice attached before writing a dirty file |
206
+ | editor → DSH | select code → context menu **"DSH: ask about selection"** → an **ask panel** opens (carrying `file:line` and the selection); the question enters the current session as **user input**, and that session's **new content** is rendered in the panel by DSH's own Markdown renderer | extension command `dsh-code-server.askAboutSelection` (one of the **top two** editor context-menu items) + a webview panel + `POST /ask` + the `thread` field of `/sync` |
207
+ | DSH → editor (approval) | when the agent wants to **write outside the workspace or run a command**, the approval request shows up as a card in the panel (tool, reason, countdown); "allow once" / "reject" takes effect immediately | the `approvals` field of `/sync` + `POST /approve` (the bridge's **only** non-read-only route; constraints under "Security model") |
208
+ | agent → editor | the agent changed a file → a **native diff** opens; if that buffer has unsaved changes you get a warning and **no overwrite** | host watches `tools/result`, the extension polls and opens the diff |
209
+
210
+ - The tools are only registered while the bridge is live (so the model never sees an unusable tool), and the
211
+ system-prompt section renders only then too.
212
+ - **Ask panel** (extension 0.2.0; the official renderer since 0.2.3; a **floating dialog over the DSH UI since
213
+ 0.2.5**): the context-menu command no longer opens a panel inside the editor (that is always a tab or a column,
214
+ never a dialog) — the plugin's client half pops up a draggable, resizable floating chat window in the DSH page
215
+ itself (bottom-right, ✕ closes it), leaving the editor layout alone. Hosts without that capability fall back to
216
+ the in-editor webview panel. The selection can still be changed while the dialog stays open.
217
+ The two commands **remember their intent** (0.3.21): "ask about selection" carries a **line range + selection text**
218
+ only when something is actually selected, while "ask about file" **never carries line numbers or a selection** — the
219
+ cursor line is irrelevant to the question and only misleads the agent. With no selection, the selection command also
220
+ degrades to the plain file.
221
+ - **Injected context is collapsed** (0.3.24): the location line plus the selection code block the bridge adds to the
222
+ message are split out into a collapsed Context row (click it to see the code), while the bubble keeps only the user's
223
+ own words — the same treatment the DSH UI gives injected context.
224
+ - **The panel renders exactly what DSH renders** (0.3.22): the panel bundles DSH's official Markdown renderer
225
+ (`MarkdownText` from `@deepseek-ai/dsh-client-ui-primitives`) plus the official design tokens — the same
226
+ micromark/mdast pipeline, the same incremental streaming parser, the same shiki highlighting (boot set:
227
+ typescript / shellscript / json), KaTeX math and the same heading/table typography. Only **new content** is
228
+ rendered (from the moment the panel subscribes); history is **not replayed** and there is no "load earlier".
229
+ - **Thinking shows up like in DSH** (0.3.23): assistant reasoning becomes a Think row — **collapsed by default**,
230
+ showing its first line (or the latest line while streaming) and expanding on a row click, built from the official
231
+ `DisclosureRow` plus the official think icon and typography language.
232
+ - **Approvals are handled right in the panel** (0.3.22; window fixed in 0.3.23): while the panel is open, that
233
+ session's approval requests ask the panel first (5 minutes by default). Clicking "allow once" / "reject" settles it
234
+ immediately; **closing the panel** or letting the window expire hands the request back **unchanged** to the official
235
+ path (the DSH UI shows the same card). 0.3.22's 8-second window was far too short for a human — the buttons went
236
+ grey before anyone could click (reported as "the approval box stopped working"); the window is now 5 minutes and
237
+ closing the panel hands off immediately instead of waiting it out.
238
+ **Nothing is ever auto-approved** — `allowed-once` can only come from a click, and there is no "always allow".
239
+ - The question enters the DSH session as a **plain user message** (`source: { kind: 'user' }`, host 0.3.19): earlier
240
+ versions used `{kind:'plugin'}`, which DSH renders as a *context update* — it did not look like something the user
241
+ said. Provenance stays in the first line of the text: `From the editor: <file>[:<line>]`.
242
+ - **Read-only with one constrained exception**: the bridge never writes files, applies edits, or runs commands; the
243
+ single non-read-only route is `POST /approve`, which can only **answer an approval request that already exists**
244
+ (see invariant 2 below). The agent's writes still go through its own `fs` tools; the bridge only *knows about* them
245
+ and carries your answer back.
246
+ - Status bar shows `$(plug) DSH` while connected (click it for the log in the "DSH Editor Bridge" output channel).
247
+ - **The extension ships as a built-in** (fixed in 0.3.12): `dshcs-editor-bridge` is installed into
248
+ `<tree>/lib/vscode/extensions/` next to `dshcs-open-file`. 0.3.0–0.3.11 installed it as a *user* extension
249
+ instead, and the VS Code server marks any extension that sits in the user extensions folder but in no profile
250
+ manifest as removed (`.obsolete`, log line `Marked extension as removed`) and then skips it forever — re-marked on
251
+ every start, so **the bridge never reported any state**. To turn the bridge off use the plugin setting
252
+ `editorBridge=false` (no mount, no tools) rather than uninstalling the extension from the Extensions view.
253
+
254
+ ### The channels (since 0.3.13 over **local IPC**: a Windows named pipe / unix socket)
255
+
256
+ ```
257
+ extension → host POST /code-server-bridge/sync one round trip: push editor state (+ which session the panel watches) + take events and thread deltas
258
+ extension → host POST /code-server-bridge/ask push an editor question into the current session
259
+ extension → host POST /code-server-bridge/approve answer an approval request that **already exists** (the only non-read-only route)
260
+ extension → host GET /code-server-bridge/health unauthenticated liveness probe
261
+ extension → host POST /code-server-bridge/event extension reports open/close etc. (host log tail)
262
+ host → extension <extensionsDir>/.dshcs-bridge/bridge.json endpoint + token, re-read every 5s
263
+ (the same content is also written **next to the built-in extension** in
264
+ `<tree>/lib/vscode/extensions/.dshcs-bridge/` — the env var is only injected when the host
265
+ spawns the IDE, and an **adopted** IDE is a process from an earlier start that never saw it,
266
+ so the extension must be able to find the config from its own location alone)
267
+ ```
268
+
269
+ Requests use `http.request({ socketPath })` (`fetch` has no socket support) and **no port is ever opened**.
270
+
271
+ The three `/sync` fields the panel actually consumes (0.3.22):
272
+
273
+ | Field | Content | How the panel uses it |
274
+ |---|---|---|
275
+ | `thread` | **new content** entries (user / assistant / tool / approval) of the session the panel watches; bounded: ≤120 entries per session, ≤8000 chars per body, ≤4 watched sessions | assistant bodies go to the official renderer; tools and approvals become compact summary rows |
276
+ | `approvals` | pending approval requests `[{id, toolName, reason, at}]` (≤4) | renders the card with a countdown; a click posts `/approve` |
277
+ | `approvalHoldMs` / `uiVersion` | the approval window (300000 ms = 5 minutes by default) / the DSH UI version | countdown basis; a renderer-version mismatch is surfaced in the panel |
278
+
279
+ > **Why not HTTP (settled in 0.3.13, all three measured)**
280
+ > 1. **Desktop has no HTTP surface at all**: the renderer calls `host.fetch()` through Electron IPC
281
+ > (`createSharedFetchHandler('/api')` in `apps/desktop-host/src/index.ts:308`) — an in-process call, unreachable
282
+ > from another process; the only HTTP a plugin can mount is the web profile's `webServer`.
283
+ > 2. **`/api` cannot carry it either**: Connection puts a Host/Origin/cookie fence on `/api`
284
+ > (`requestRejection` in `packages/client/connection/src/index.ts` → 401 without a cookie), while the bridge's
285
+ > client is a **Node process inside the extension host** — it can never hold a browser cookie. Measured on 0.3.7:
286
+ > polling `/api/code-server/bridge/sync` returned either 405 (it reached the launcher/VS Code) or 401 (the fence)
287
+ > — the bridge had never actually synced.
288
+ > 3. The two ends are **processes on the same machine** anyway (extension host ← the IDE the plugin spawned ← the
289
+ > plugin). Local IPC is strictly smaller than a port: no network surface, no Host/Origin confused-deputy path, and
290
+ > **web and desktop share one path**. Token auth stays (see below); the Windows pipe name carries a random suffix
291
+ > and the POSIX socket file is `chmod 0600`.
292
+ >
293
+ > History: 0.3.9–0.3.12 mounted it on DSH's `webServer` prefix — which left desktop permanently dormant.
294
+
295
+ **Why state is pushed, not pulled**: the extension host is a child process of the VS Code server and **listens on
296
+ no port** — the host cannot call into it. Editor state therefore rides the extension's own polling request, and
297
+ the host caches it for the tools (at most one 600 ms cycle behind; older than 10 s and the tool says so instead
298
+ of passing stale data off as fresh).
299
+
300
+ **Why no SSE/WebSocket**: the extension host has no HTTP server of its own; the bridge's shape is one
301
+ request/response round trip every 600 ms. Polling also buys two useful properties: it is idempotent (a dropped event
302
+ only costs one notification — the data always lives in the editor) and the cached state is inherently fresh.
303
+
304
+ ### Security model (five invariants; read before touching `lib/bridge.mjs`)
305
+
306
+ The token lives in `<extensionsDir>/.dshcs-bridge/bridge.json`, **readable by any process of the same local
307
+ user**, so:
308
+
309
+ 1. **`/code-server-bridge/*` is read-only, with `/approve` as the single exception.** No route writes files,
310
+ edits documents, runs commands, or spawns processes. A leaked token is therefore bounded to "sees information
311
+ that is in the editor" and **can never** become arbitrary file writes or command execution. A whitelist
312
+ assertion in `scripts/test-bridge-routes.mjs` guards this.
313
+ 2. **The four constraints on `/approve`** (drop one and it becomes an arbitrary-command-execution back door):
314
+ (a) it can only **answer** an approval request that already exists — the body is exactly `{id, outcome}`, with
315
+ **no free text, paths, or command arguments**, so it can answer questions but never start an action;
316
+ (b) `id` must belong to a request this process created and that is **still pending** (single use);
317
+ (c) `outcome` accepts only `allowed-once` / `rejected` — there is **no "always allow"**;
318
+ (d) when no panel is watching, the panel is closed, or the window (5 minutes by default) expires, the request goes
319
+ **back to the official
320
+ path** — never auto-approved (DSH's `approval/request` itself fails closed; this bridge can only keep
321
+ "nobody answered" as "nobody answered"). `pnpm test:webview` asserts these four plus the host-side whitelist.
322
+ 3. **Any request carrying `Origin` gets 403.** Browsers always send one (including a sandboxed iframe's literal
323
+ `Origin: null`); the Node extension host never does. Origin is checked **before** the token — otherwise the
324
+ bridge would be a "did you guess the token right" oracle for a web page.
325
+ 4. **Paths are confined to the editor's current workspace folders.**
326
+ 5. **Everything is bounded**: 200 diagnostics, 500-char messages, 256 KB request bodies, a 64-entry event ring,
327
+ ≤120 thread entries per session (≤8000 chars each, ≤4 watched sessions) and ≤4 pending approvals.
328
+
329
+ This layer stops "another local app or a browser page that got hold of the file". A malicious program running as
330
+ the same user could read your files and the token anyway — that is outside this plugin's threat model, exactly
331
+ as stated for the loopback port.
332
+
333
+ ### Turning it off / diagnostics
334
+
335
+ | How | Effect |
336
+ |---|---|
337
+ | `config.editorBridge: false` in `cordis.patch.yml` | next start writes no `bridge.json` and registers no tools |
338
+ | `code-server.editorBridge: false` in the settings document | **immediate**: config removed, tools unregistered, the extension goes dormant |
339
+ | disable the `dshcs-editor-bridge` extension inside the IDE | the bridge simply becomes unavailable |
340
+
341
+ Diagnostics: `GET /api/code-server/status` exposes
342
+ `bridge: { enabled, live, toolsRegistered, supported, url, file }` — **never the token** (that only exists in the file).
343
+
344
+ ## Legacy DSH (unsupported since 0.2.3)
345
+
346
+ **Behaviour**: when `sidebarRightTabs` / `sidebarRight` cannot be found, the plugin registers a single settings card:
347
+
348
+ > **Code Server** — this DSH version is unsupported (no right-sidebar service)
349
+ > Since 0.2.3 this plugin no longer supports older DSH versions.
350
+ > The right-sidebar plugin services `sidebarRightTabs` / `sidebarRight` were not detected, so the plugin exposes no
351
+ > entry point at all (the old floating ball and floating window have been removed) and will not start the IDE in the
352
+ > background. Upgrade DSH to a version with the right sidebar (>= 0.1.5-alpha.1): Code Server then appears as a
353
+ > right-sidebar tab, this page shows the full settings again, and no reinstall is needed — a page refresh is enough.
354
+
355
+ - **No other UI**: no `shell.overlay` registration (floating ball), no file-address claim, no resident preload.
356
+ - **Host side**: the client posts `/api/code-server/ui-mode { sidebar:false }`; the host then ① stops auto-prestarting
357
+ the IDE (`maybePrestart` returns immediately) and ② **recycles** an instance it had just auto-prestarted (unless it
358
+ was adopted), so no unusable IDE process or port is left behind. A user-started/adopted instance is never stopped.
359
+ - **Why delete instead of keeping**: the internal floating window was a stopgap from the era of early-2026 DSH builds
360
+ without right-sidebar services. The resident surface, clipboard handling, shortcuts and panel collapsing all build on
361
+ DSH's right sidebar, so maintaining two carriers costs more than it is worth. Older-DSH users should stay on `0.2.2`
362
+ (`dsh plugin --profile web add dsh-code-server-app@0.2.2`).
363
+
364
+ ## code-server workspace and process lifecycle
365
+
366
+ - code-server's workspace **follows the active DSH session/workspace**: switching sessions/workspaces while the IDE is open moves code-server to the new directory
367
+ (resolution order: current session cwd → session's `workspace.path` → workspace of the most recently active session → first workspace.path;
368
+ **where "the current session" comes from depends on the DSH version**: ≥ 0.1.6-alpha.2 reads the session-scoped standard prop `sessionId`,
369
+ ≤ 0.1.6-alpha.1 falls back to `current` on the session-list snapshot — see the 0.3.48 bullet below; the logic lives in `src/workspace.js`
370
+ and the contract for both shapes is pinned by `scripts/test-client-bundle-cwd.mjs` directly against the built bundle);
371
+ the opened directory is shown inside code-server (`?folder=<cwd>`, the page reloads when following a switch);
372
+ implementation note: the iframe `src` must carry `?folder=<cwd>` — code-server's front-end remembers the "last workspace" and restores it by itself;
373
+ a bare root URL only shows the previously opened directory and does not follow switches (verified locally).
374
+ **Windows path format (verified)**: the `folder` parameter must start with `/` and use forward slashes only, e.g. `/C:/Users/User/Desktop/biss`;
375
+ 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),
376
+ while `file:///C:/...` reports "Workspace does not exist".
377
+ - **0.3.48 fixes "opening Code Server no longer opens the matching workspace"**: DSH **0.1.6-alpha.2** removed `current`
378
+ from `SessionListState` (upstream refactor: view selection remains outside the Controller), while 0.3.46 and earlier read
379
+ the current session from `useSessions(s => s).current` — so the cwd was always undefined, the client **stopped sending `cwd`**
380
+ to the host, and the IDE started with an **empty workspace** (measured locally: `cwd`/`launchCwd` both empty in
381
+ `$DSH_HOME/code-server/pid.json`, with nothing visible in the UI). Since 0.3.48 it reads the session-scoped standard prop
382
+ `sessionId` (the same source DSH's own right-sidebar tab uses — `ui-deliverables`' ReviewTab does
383
+ `useSessions(s => s.byId[sessionId]?.cwd)`), keeping the old `current` as a backward-compatible fallback; when neither
384
+ source resolves, it **does not guess a directory** (no cwd is sent, the workbench keeps its current one) and logs a
385
+ `[code-server] 未能解析当前工作区目录…` warning — the silence is exactly what made this bug hard to find.
386
+ - **The switch is lightweight (since 0.2.12)**: a running instance is **not restarted** when the workspace changes — the host
387
+ only updates `state.cwd` and the workbench re-navigates with the new `?folder=` (the workspace directory was always the
388
+ client URL's business; the process cwd only affects the server's own relative-path resolution at spawn time). The switch is
389
+ much faster and no longer throws away the extension host, background tasks or server-side state.
390
+ - In `status`, `cwd` is the current workbench directory; `launchCwd` is the directory the **process was started with**
391
+ (diagnostics only; it does not change on a switch).
392
+ - The trade-off, stated plainly: background processes/terminals started by the IDE in the **old** directory are no longer
393
+ killed automatically (a full restart used to take them with it) — clean them up yourself if needed. That is the same coin
394
+ as "nothing is lost".
395
+ - The trigger does not depend on the tab being visible: **the tab body is not unmounted while the sidebar is collapsed**,
396
+ so it also follows in the background (behaviour deliberately kept in 0.2.12).
397
+ - Regression: `scripts/test-workspace-switch.mjs` (5 assertions: adopting an instance, cwd change keeps pid/status, the
398
+ process stays alive, same-directory idempotence, no-cwd does not switch; it fails again if the old behaviour returns).
399
+ - 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),
400
+ 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;
401
+ - `node_modules` and the pack-time artifact `vendor/` are git-ignored; after cloning, follow
402
+ "Install the plugin (script-free install; code-server bundled)" below — `pnpm install` → `pnpm run build:client` →
403
+ `pnpm run vendor:vscode` → `pnpm pack` + `dsh plugin --profile web add`.
404
+
405
+ > Verified locally (BM: Windows 11 ARM64): the whole tree/dependency chain hangs directly off the plugin's
406
+ > dependency table — the tree package `@jinsiyu/dshcs-vscode-server` (currently 4.137.0, a 50.8 MB tarball),
407
+ > the pure-JS inner dependencies plus the 8 platform-independent repacks in `dependencies`, and the 8
408
+ > platform-specific repacks (win32-arm64 / win32-x64) in `optionalDependencies` with their own os/cpu gates;
409
+ > the original names are restored by junctions created at runtime (`lib/native.js`)
410
+ > → healthz 200 → stopped → fully recycled.
411
+ > (The 0.1.37-era "one big platform package" layout is gone — see "Upgrading the VS Code tree" below.)
412
+
413
+ ## Packaging (how to build the tarball)
414
+
415
+ ```powershell
416
+ cd C:\Users\User\Desktop\dsh-code-server-app
417
+ pnpm install # dev deps (esbuild + the official-renderer bundling deps); allowBuilds is explicit → no postinstall runs
418
+ pnpm run build:client # src/factory.js → lib/client.js (not committed; must be built first)
419
+ pnpm run build:webview # ask panel: official Markdown renderer + panel shell → webview/thread.{js,css} (not committed; must be built first)
420
+ pnpm run vendor:check # optional: show the bundled tree version vs the latest code-server release
421
+ pnpm run vendor:vscode # ① produce vendor/vscode (the trimmed VS Code tree, ~197MB)
422
+ pnpm run repack:build -- --target win32-arm64,win32-x64 --pack # ② one script builds every sub-package
423
+ pnpm run publish:repacks # ③ publish every @jinsiyu/* sub-package (default dist-tag: next)
424
+ pnpm pack # ④ → dsh-code-server-app-<version>.tgz (~750KB, including the panel renderer assets)
425
+ pnpm run publish:plugin # ⑤ publish the plugin itself (default dist-tag: next)
426
+ # once the user has restarted dsh web and confirmed it works, promote latest:
427
+ pnpm run promote -- <version>
428
+ ```
429
+
430
+ > **dist-tag policy (mandatory)**: every release goes to **`next`** and **never touches `latest`**;
431
+ > `latest` always points at the most recent *confirmed bug-free* version and is only moved by
432
+ > `pnpm run promote -- <version>` (= `npm dist-tag add dsh-code-server-app@<version> latest`)
433
+ > **after the user restarts `dsh web` and confirms it works**. That way
434
+ > `dsh plugin add dsh-code-server-app` (no version) — and anything else resolving `latest` — never picks up an
435
+ > unverified build. Sub-packages (`@jinsiyu/dshcs-*`) are referenced by exact versions,
436
+ > so their dist-tags do not affect resolution, but they default to `next` as well.
437
+ > Inspect the current tags with `npm dist-tag ls dsh-code-server-app`.
438
+
439
+ > `build:webview` bundles DSH's **official** Markdown renderer and design tokens into the panel assets
440
+ > (~1.34MB: 996KB JS + 87KB CSS + 254KB KaTeX fonts), so it needs a local DSH deployment: the script reads the
441
+ > `@deepseek-ai/dsh-web-frontend` version from that deployment and compares it with the renderer version pinned in
442
+ > devDependencies — a mismatch **fails the build** (unless `--allow-version-mismatch`). Same convention as
443
+ > `lib/client.js`: the artifacts are not committed and `prepack` rebuilds them.
444
+ > Rationale (why not an iframe, where the tokens come from, size trade-offs) is section 21 of
445
+ > `docs/analysis-code-server-as-dsh-plugin.md`.
446
+
447
+ `repack:build` (`scripts/vendor-repacks.mjs`) is the **single script that produces every sub-package**:
448
+
449
+ | Sub-package | Content | os/cpu |
450
+ |---|---|---|
451
+ | `@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 |
452
+ | `@jinsiyu/dshcs-<name>[-win32-<arch>]` ×16 | the VS Code inner packages that need building (node-pty / @vscode/sqlite3 / kerberos / koffi / ssh2 / …) | gated when platform-specific |
453
+ | `lib/vendored.json` (**not** a package) | the "original name → repack sub-package" table shipped inside the plugin; `lib/native.js` uses it to create the junctions. Since 0.3.45 there is **no platform aggregator** | — |
454
+
455
+ | Goal | Command |
456
+ |---|---|
457
+ | **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 |
458
+ | **Pin a version** | `pnpm run vendor:vscode -- --version 4.137.0` |
459
+ | **Snapshot from an existing tree** | `pnpm run vendor:vscode -- --from <code-server dir>` (seconds) |
460
+ | **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) |
461
+ | **Rebuild only the tree + dependency table** | `node scripts/vendor-repacks.mjs --reuse --target win32-arm64,win32-x64 --pack` (reuses the natives already in `repack/build`; also rewrites `lib/vendored.json` and the plugin dependency table) |
462
+ | **Publish sub-packages** | `pnpm run publish:repacks` (`--dry-run` to preview; `--only <substr>` to filter; `--otp <code>` / `--limit N` for 2FA) |
463
+ | **Publish the plugin itself** | `pnpm run publish:plugin` (publishes the exact tarball that was verified; no re-packing; default dist-tag `next`) |
464
+ | **Promote `latest`** | `pnpm run promote -- <version>` (only after the user restarted and confirmed; `--dry-run` shows the current tags first) |
465
+ | **Just report versions** | `pnpm run vendor:check` |
466
+
467
+ > `pnpm pack`'s `prepack` runs the vendor-code-server script once; when `vendor/code-server` already exists it is
468
+ > a **no-op that takes seconds**, so after ordinary code changes you can just run `pnpm pack` (it will never
469
+ > silently upgrade code-server). Upgrading code-server requires an explicit `pnpm run vendor:latest`
470
+ > (or `--force` / `--version`) **plus** republishing the sub-packages.
471
+
472
+ ## GitHub Actions (CI + tag-triggered release)
473
+
474
+ Both workflows live in `.github/workflows/`, and the regression list exists exactly once
475
+ (`scripts/run-all-tests.mjs`, i.e. `pnpm test`):
476
+
477
+ | Workflow | Trigger | What it does |
478
+ |---|---|---|
479
+ | `ci.yml` | push to `main` / PR / manual | `ubuntu-latest` + `windows-latest` matrix: `pnpm install --frozen-lockfile` → `build:client` → `build:webview` → `pnpm test` (the whole suite) → `vendor:check` (report only) → upload `lib/client.js` and the panel assets |
480
+ | `release.yml` | push a `v<version>` tag / manual (rehearsal, never publishes) | prepares `vendor/vscode` **at the version pinned in `dependencies`** → builds → full suite → `pnpm pack` → verifies the tarball manifest → **really installs it twice** (windows-latest proves the 16 win32 sub-packages, ubuntu-latest the 10 Linux ones: each deploys a real DSH, installs via the official path `dsh plugin --profile web add <tgz>`, then runs the `test:installed` + `dump-config` assertions; both legs must pass before anything is published) → publishes to npm **`next`** → creates a GitHub Release with the tgz attached |
481
+ | `linux-repack-probe.yml` | push to this file / manual | **feasibility probe (never publishes; superseded by the Linux legs of `repacks.yml`)**: on Linux, builds the platform-specific repack packages per target (`linux-x64` → `ubuntu-latest`, `linux-arm64` → `ubuntu-24.04-arm`) and reports which modules really produce a `.node` and which are Windows-only. It runs the existing `vendor-repacks.mjs` itself; all writes happen in a copy of the repo under `$RUNNER_TEMP`. **Note**: it emits one notice per module, which hits GitHub's ~20-annotations-per-check-run cap and leaves only the tail; for the full verdict use the Linux legs of `repacks.yml` (one summary line per target) |
482
+ | `repacks.yml` | manual (`publish` and `probe_oidc` both default to **false**, the four `build_*` legs default to **true**) / push to this file / push `.github/oidc-probe.enabled` | **builds and publishes the platform-specific sub-packages** (`@jinsiyu/dshcs-*`): one host-architecture runner per target (`win32-x64` → `windows-latest`, `win32-arm64` → `windows-11-arm`, `linux-x64` → `ubuntu-latest`, `linux-arm64` → `ubuntu-24.04-arm`); by default it only builds and uploads `repack/tgz/*.tgz`, and only publishes to npm (default `next`) when `publish` is checked. Ownership and ordering (**five legs, disjoint sets**): the `independent` leg runs **first** (windows-latest; it produces the **VS Code tree package + the 8 platform-independent repacks**, which are the same artifact for all four targets and are therefore published only once); the four platform-specific legs `needs: independent`, build with `--skip-independent` and publish with `--only <their own target>` ⇒ a broken base layer blocks the rest (no half-published state) and no package name is ever published twice. **Auth**: with no `NPM_TOKEN` it uses OIDC (per-package trust entries, all with workflow `repacks.yml` — see below). The Linux legs additionally verify that the `lib/vendored.json` / `package.json` they generate match the committed ones (the platform policy is meant to be host-independent). A `probe-oidc` job additionally does a **staged-only** probe of that OIDC route, so the channel can be proven without publishing anything real |
483
+
484
+ ### Linux support (x64 / arm64): what changed, what is still missing
485
+
486
+ Supporting Linux is not mainly about "compiling a few more packages" — it is about replacing
487
+ **host scanning** with an **explicit platform policy**:
488
+
489
+ - Upstream packages barely declare `os`/`cpu` (of the 16 modules, only `@vscode/windows-ca-certs` does),
490
+ and the tree manifest lists all 8 native modules as ordinary `dependencies` — so on Linux npm installs
491
+ the Windows-only ones anyway. `analyze()` classifies by "does this host have a `.node`", so **the
492
+ classification drifts with the host**: on Linux `windows-registry` looks platform-independent and would be
493
+ written into `dependencies`, which then makes the Windows runtime look for a `-win32-*` sub-package that
494
+ is not there.
495
+ - Therefore `scripts/repack-platforms.json` is the single, human-reviewed declaration: each module's
496
+ `platform` (does it need per-platform packaging) and `targets` (which targets have a sub-package). The
497
+ generator only reads it, and derives from it: the per-module `targets` in `lib/vendored.json` (at runtime
498
+ `lib/native.js` uses them to **skip modules that do not apply to this platform**, instead of reporting
499
+ `dshcs-vscode-windows-registry-linux-x64` — a name that can never exist — as missing) and the plugin's
500
+ `optionalDependencies` (no longer blindly module × every target).
501
+ - The generator also now: keeps table entries it cannot see on this host (building only the host target with
502
+ `--target` must not drop the other platforms' modules), **skips** a per-platform package when no `.node`
503
+ was produced for that target (rather than publishing an empty shell), and validates ELF `e_machine` for
504
+ Linux targets (mirroring the PE machine check for win32).
505
+ - **Version policy (independent of host *and* of when you run it)**: the version recorded for each module in
506
+ `lib/vendored.json` is **the one we have actually published to the registry**; upstream drift never changes
507
+ it silently. Two measured cases with the same `code-server@4.137.0`: `kerberos` resolves to upstream `2.1.1`
508
+ while we published `2.1.1-dshcs.1`, and `@vscode/proxy-agent` resolved to `0.44.0` for the maintainer but to
509
+ `0.45.0` on a fresh install today (the dependency range allows drift, and `0.45.0` was never published for
510
+ our sub-package). Writing the tree's version would point the plugin's dependency at a version that does not
511
+ exist — an install that simply fails, on a different host or another day.
512
+ **To adopt a newer upstream version**: pin `"version"` for that module in `scripts/repack-platforms.json`,
513
+ re-run `vendor-repacks.mjs`, publish the new sub-packages, then refresh the dependency table and
514
+ `pnpm-lock.yaml`. The generator always prints such drift (`· <module>: 沿用表里已发布的版本 …`), so follow
515
+ that line.
516
+ > Keep the two dependency classes apart: the rule above governs **our own repack sub-packages**
517
+ > (`@jinsiyu/dshcs-*`, whose version must be one we published). **Upstream pure-JS direct dependencies**
518
+ > (the `declare` set: `cookie`, `ws`, `@vscode/proxy-agent`, …) take their version from the source tree —
519
+ > whatever the tree installed came from npm, so following the tree is safe. Differences there are
520
+ > **time**-related (a fresh install of the same commit on another day can differ), which is why
521
+ > `repacks.yml` reports them as a notice rather than a warning.
522
+ - **System headers needed to build the native packages on Linux**: `kerberos` needs the GSSAPI headers
523
+ (`gssapi/gssapi.h`), so both Linux legs run `sudo apt-get install -y libkrb5-dev` and assert the header is
524
+ present. Without it `make` fails outright and the `kerberos` sub-package cannot be produced (exposed by the
525
+ hard gate on 2026-09-16). Do the same before re-packing locally on Linux.
526
+ - **Measured on Linux** (both legs really compile; the verdict line reads
527
+ `[repack] linux-x64: 平台专属产出 5 个(…)`): five modules produce a `.node` —
528
+ `@vscode/deviceid`, `@vscode/native-watchdog`, `@vscode/spdlog`, `@vscode/sqlite3`, `kerberos`;
529
+ `windows-ca-certs` / `windows-process-tree` / `windows-registry` are Windows-only (whitelisted for
530
+ `win32-*` and reported as "excluded by the whitelist" on Linux). The earlier manual probe
531
+ `linux-repack-probe.yml` has been **removed**: its job (build without publishing) is now done by these two
532
+ legs, and its per-module notices hit GitHub's ~20-annotations-per-check-run cap, so only the tail was
533
+ readable.
534
+
535
+ **Linux go-live status (updated 2026-09-17)**:
536
+
537
+ 1. ✅ **The 10 Linux sub-packages are published** (5 modules × x64/arm64). A trust entry cannot exist before
538
+ the package does, so the maintainer did the first publish locally with 2FA (`npm login --auth-type=web`,
539
+ then one `npm publish <tgz>` per package). Versions match `lib/vendored.json` exactly, and
540
+ `os=linux` / `cpu=x64|arm64` were verified against the registry.
541
+ 2. ⏳ **Add one trust entry per new package name** (one command each; browser confirmation is enough,
542
+ **no OTP needed**):
543
+
544
+ ```powershell
545
+ $env:npm_config_auth_type = 'web'; npm login # skip if already logged in
546
+ $names = @(
547
+ '@jinsiyu/dshcs-kerberos-linux-arm64','@jinsiyu/dshcs-kerberos-linux-x64',
548
+ '@jinsiyu/dshcs-vscode-deviceid-linux-arm64','@jinsiyu/dshcs-vscode-deviceid-linux-x64',
549
+ '@jinsiyu/dshcs-vscode-native-watchdog-linux-arm64','@jinsiyu/dshcs-vscode-native-watchdog-linux-x64',
550
+ '@jinsiyu/dshcs-vscode-spdlog-linux-arm64','@jinsiyu/dshcs-vscode-spdlog-linux-x64',
551
+ '@jinsiyu/dshcs-vscode-sqlite3-linux-arm64','@jinsiyu/dshcs-vscode-sqlite3-linux-x64')
552
+ foreach ($n in $names) {
553
+ npm trust github $n --file repacks.yml --repo jinsiyu/dsh-code-server-app --allow-publish -y
554
+ }
555
+ ```
556
+ Afterwards you can **delete `NPM_TOKEN`** from the repository secrets — CI then uses OIDC and will no
557
+ longer hit the "token without bypass 2FA ⇒ EOTP" trap (which only mattered for first-publishing a name).
558
+ 3. ✅ **The dependency table is wired up**: `publishedTargets` in `scripts/repack-platforms.json` now includes
559
+ `linux-*`; `package.json` has **26** `optionalDependencies` (16 win32 + 10 linux, derived from
560
+ "per-module whitelist ∩ published targets", verified item by item by `test-vendored-table.mjs`);
561
+ `pnpm-lock.yaml` was refreshed (10 additions, nothing else changed); and the 10 `@version` pairs were
562
+ added to `minimumReleaseAgeExclude` in `pnpm-workspace.yaml` (freshly published packages are held back
563
+ by the supply-chain cooldown otherwise).
564
+
565
+ > What remains is our own release flow: bump the plugin version → `pnpm pack` → install once locally via
566
+ > `dsh plugin --profile web add <tgz>` → tag it and let `release.yml` publish. On Linux, `lib/native.js`
567
+ > then links the `-linux-*` sub-packages back to their original names through the same junction path
568
+ > Windows already uses.
569
+
570
+ The regression suite (also the single list CI uses) is:
571
+
572
+ ```powershell
573
+ pnpm test # runs them all: scripts/run-all-tests.mjs
574
+ pnpm test:apply # apply() under a stub ctx
575
+ pnpm test:claim-types # claim-type syntax and defaults
576
+ pnpm test:bridge-routes # bridge route whitelist / Origin-vs-token order / token header agreement
577
+ pnpm test:bridge-extension # extension-side pure logic (dirty buffers, diagnostics, diff, delivery, panel state)
578
+ pnpm test:webview # panel bundle: official renderer + tokens, version match, the four /approve constraints
579
+ pnpm test:launcher-routes # launcher HTTP surface (spawns a real process; slow)
580
+ pnpm test:workspace-switch # switching workspaces does not restart the process
581
+ pnpm test:fullscreen # opening the tab goes fullscreen
582
+ pnpm test:vendored # repack table ↔ plugin dependency table (no npm: aliases, no aggregator)
583
+ pnpm test:installed # install smoke: assert on what was **installed into a profile**
584
+ # (default <DSH_HOME>/profiles/web): every `files` entry present, repack
585
+ # packages complete for this platform, no missing natives, the installed
586
+ # copy imports, the tree is in place — nothing the repo suite can see
587
+ ```
588
+
589
+ **The dist-tag policy is unchanged**: `release.yml` only publishes `next` and never touches `latest`; `latest` is
590
+ still moved by hand with `pnpm run promote -- <version>` after a restart of `dsh web` confirms the build is good.
591
+
592
+ Release flow (now it is just a tag):
593
+
594
+ ```powershell
595
+ # 1) bump package.json's version → commit to main → wait for ci.yml to go green
596
+ # 2) install it locally into the web profile, restart DSH, confirm it works
597
+ git tag v0.3.47; git push origin v0.3.47 # 3) release.yml rebuilds the tarball, publishes next, opens a Release
598
+ pnpm run promote -- 0.3.47 # 4) promote latest by hand once confirmed
599
+ ```
600
+
601
+ One-time setup (repository / account side, no files involved):
602
+
603
+ - **npm trusted publishing (recommended, no long-lived credential)** — two equivalent routes to create a trust
604
+ relationship that lets **only this workflow** publish the package:
605
+ - **CLI (one command, dry-run verified locally)**:
606
+
607
+ ```
608
+ npm login # already jinsiyu? skip
609
+ npm trust github dsh-code-server-app --file release.yml \
610
+ --repo jinsiyu/dsh-code-server-app --allow-publish
611
+ ```
612
+
613
+ `--allow-publish` is **required** (without it the entry is created but cannot publish);
614
+ `--file` takes the bare filename (`release.yml`), npm expands it to
615
+ `.github/workflows/release.yml`; the command itself **requires 2FA** (it will ask for an OTP).
616
+ In a sandboxed shell, an `EPERM … npm-cache` error means the npm cache is not writable —
617
+ point it at a writable directory with `npm_config_cache`, and do **not** put `--cache` before
618
+ `trust` (that breaks npm's subcommand parsing with `Unknown positional argument: github`).
619
+ Verify with `npm trust list dsh-code-server-app` (that endpoint can return 403 for older tokens —
620
+ then check the Trusted Publisher list on the npm website instead).
621
+ - **Web UI**: npmjs.com → `dsh-code-server-app` → Settings → Trusted Publisher → GitHub Actions:
622
+ Organization/user = `jinsiyu`, repository = `dsh-code-server-app`, workflow filename = `release.yml`,
623
+ **environment name left empty** (the release job declares no `environment:`).
624
+ - Semantics: this trust relationship means "**anyone with write access to this repository can publish
625
+ this package**" (npm's own wording).
626
+ - Alternative: add an `NPM_TOKEN` repository secret and set the repository variable
627
+ `NPM_AUTH_MODE=token` (that mode does not attach provenance). Note npm is restricting legacy
628
+ 2FA-bypassing tokens, so OIDC is the durable option — once it works, delete the token.
629
+
630
+ **Authentication for `repacks.yml` (the platform-specific sub-packages)** — two routes; the workflow picks
631
+ one itself (with no `NPM_TOKEN` it uses OIDC):
632
+
633
+ - **A. `NPM_TOKEN` (least friction today)**
634
+ 1. npmjs.com → avatar → **Access Tokens** → **Generate New Token** → **Granular Access Token**;
635
+ 2. any name (e.g. `github-actions-repacks`) and an expiry (90 days is fine);
636
+ 3. **Packages and scopes** = **Read and write**, tick only the **`@jinsiyu`** scope (not "All packages");
637
+ 4. you **must** tick **"Bypass two-factor authentication (2FA)"** — otherwise an unattended publish
638
+ stalls waiting for a one-time password;
639
+ 5. the token is shown **once** — copy it immediately;
640
+ 6. GitHub repository → Settings → Secrets and variables → **Actions** → **New repository secret**,
641
+ the name **must** be `NPM_TOKEN` (that is what the workflow reads).
642
+ ⚠️ Per npm's announcements: since 2026-07-31 bypass-2FA tokens can no longer perform account/package
643
+ management, and **from January 2027 they lose direct publish** (only reading private packages plus staging
644
+ a publish, which a maintainer approves with 2FA) — so plan to move to B.
645
+ - **How to read a failed publish** (`publish-repacks.mjs` first runs a token health check: it prints only
646
+ the token's length and shape, never its content, then uses `npm whoami` to prove it authenticates):
647
+ · `E401` / `ENEEDAUTH` ⇒ **the token value is wrong**: surrounding quotes or a trailing newline, a
648
+ truncated paste, or a revoked token ⇒ generate a fresh one and paste it again (a granular token looks
649
+ like `npm_…` followed by a long random string; a classic one is a 36-character UUID);
650
+ · `npm whoami` succeeds but publishing returns **`EOTP` (This operation requires a one-time password)**
651
+ ⇒ **the value is fine**, what is missing is a permission attribute: the token does not have step 4's
652
+ **"Bypass two-factor authentication (2FA)"** ticked. npm also requires 2FA for the **first publish of a
653
+ new package name**, and a trust entry cannot exist before the package does ⇒ the first publish has to be
654
+ done locally with `npm publish <tgz> --access public --tag next` and an OTP (publish only that target's
655
+ own `-<target>` packages; do not re-publish the already-published tree package), after which one trust
656
+ entry per new name switches those packages to OIDC.
657
+ - **B. per-package trusted publishing (the durable route)**: npm trust entries are **per package**
658
+ (since 2026-09 a package may have several, but there is **no scope-level** entry), so these 25 packages
659
+ need 25 entries. Generate the commands, then run them one by one after a single browser authorization:
660
+ ```powershell
661
+ node -e "const t=require('./lib/vendored.json');const p=require('./package.json');const tree=Object.keys(p.dependencies).find(n=>n.endsWith('/dshcs-vscode-server'));const names=[tree,t.modules.flatMap(m=>m.platform?t.targets.map(x=>m.package+'-'+x):m.package)];require('fs').writeFileSync('trust-all.txt',names.flat().map(n=>'npm trust github '+n+' --file repacks.yml --allow-publish -y').join('\n')+'\n')"
662
+ Get-Content trust-all.txt | ForEach-Object { Invoke-Expression $_ }
663
+ ```
664
+ Afterwards delete `NPM_TOKEN` (the workflow then uses OIDC). npm also lets each entry be **staging-only**
665
+ (a version only goes live after you approve it with 2FA) — safer, but each batch then needs manual
666
+ approvals for several versions.
667
+ Verify with `npm trust list <package>`, which should show `file: repacks.yml` and
668
+ `repository: jinsiyu/dsh-code-server-app` (all 25 packages are required — a missing one fails at publish
669
+ time with "no matching trust configuration", and that line shows up as an annotation in the run without
670
+ needing a token).
671
+ - **Verifying that route (without waiting for a real publish)**: `repacks.yml` has a `probe-oidc` job that does a
672
+ **staged** publish for four real sub-package names (`vscode-fs-copyfile`, `node-pty`,
673
+ `kerberos-win32-arm64`, `vscode-server`), with versions like `2.0.1-oidc-probe.<run>`.
674
+ This only works from CI: OIDC tokens are minted at run time, and a trust entry matches on
675
+ repository + **workflow filename** + package name — which is also why the probe has to live in
676
+ `repacks.yml` itself. `npm stage publish` follows exactly the same auth path as `npm publish`, but the
677
+ version lands in the staging queue and **never in the registry's published version list** (the script
678
+ re-checks that with `npm view <pkg> versions`), so no version number is consumed and no dependency
679
+ resolution changes. Trigger it either from Actions → repacks → Run workflow with `probe_oidc` checked, or
680
+ token-free by committing the sentinel `.github/oidc-probe.enabled` and pushing. Results are emitted as
681
+ `::notice::` / `::error::` annotations, readable on a public repo without logging in
682
+ (`GET /repos/jinsiyu/dsh-code-server-app/check-runs/<id>/annotations`). Afterwards **reject** the staged
683
+ probe versions (`npm stage list`, then `npm stage reject <id>`; needs 2FA on your machine) — do **not**
684
+ approve, since approving is what would turn a probe into a real version. Delete the sentinel file to
685
+ return to "no automatic probe".
686
+ - **Optional** repository variable `DSH_UI_VERSION` = the version of `@deepseek-ai/dsh-web-frontend` in the current
687
+ deployment: when set, `release.yml` enforces that the panel renderer matches the deployed UI (the local
688
+ `build:webview` always checks this; a runner has no DSH deployment).
689
+
690
+ Things you must know:
691
+
692
+ - **`release.yml` rebuilds the tarball on the runner; it does not upload the file built on your machine.** Same
693
+ commit + same pinned tree version ⇒ same content, the only differences being the `platform` / `preparedAt` /
694
+ `sizeMB` metadata in `vendor/VENDOR.json` (at runtime only `codeServerVersion` and `productPath` are read).
695
+ That is why step 4 above still installs into the web profile before promoting `latest`.
696
+ - **CI never runs `pnpm pack`**: on a fresh clone its `prepack` pulls `code-server@latest` from the registry, which
697
+ does not match the tree version pinned in `dependencies` (it would break the "tree package is pinned exactly"
698
+ assertion). Packing happens only in `release.yml`, after
699
+ `node scripts/vendor-vscode-server.mjs --version <pinned>`.
700
+ - **Release gates** (any failure stops the run; `next` is never advanced): tag ≠ `package.json.version`, the
701
+ version already exists on npm, the tree version does not match (`test:vendored`), the suite fails, or
702
+ `DSH_UI_VERSION` mismatches.
703
+ - `@deepseek-ai/schemastery` is a **devDependency** (pinned to 3.18.2, the version the deployment uses):
704
+ `lib/index.js` normally takes it from the DSH deployment (in production, the copy hoisted inside the
705
+ profile), and a clean clone / CI runner has no DSH at all — without this devDependency the `apply`-style
706
+ tests throw `schemastery not found`. It never ships to users (devDependencies are not installed for a
707
+ dependency). A CI job that actually deploys DSH is possible (`@deepseek-ai/dsh` is public on npm), but it
708
+ pulls the whole harness (~1.3GB profile), so it belongs in a slower job, not on every push.
709
+ - The reverse case — an **installed copy** (not a repo checkout) has no repo `node_modules` — is why the
710
+ deployment-layout table in `lib/dsh-resolve.mjs` must cover every real layout: since 0.3.49 it knows the
711
+ running deployment (`argv[1]`/`execPath` resolution), `%APPDATA%\npm`, `npm --prefix` (`~/.npm-global`),
712
+ pnpm global, nvm, system `/usr/local|/usr`, and the `$DSH_HOME` profile level. The 0.3.48 Linux install-smoke
713
+ leg (CLI installed into `~/.npm-global`) failed precisely because that table was too narrow (the smoke threw
714
+ `schemastery not found` and `publish` was skipped); regression: `scripts/test-dsh-resolve.mjs` builds each
715
+ layout in a temp directory, since a developer machine never hits them by accident.
716
+ - The first release must use a **version that has never been published** (npm versions are immutable).
717
+ `release.yml` supports a `workflow_dispatch` **rehearsal** (full pipeline, nothing published) — run it once
718
+ before pushing a real tag.
719
+ - `pnpm-lock.yaml` is **committed** now (CI installs with `--frozen-lockfile` and the cache key is derived from
720
+ it); it is not in `package.json`'s `files` allow-list, so it never ships in the npm package.
721
+
722
+ ## Install the plugin (one command; all dependencies installed by the package manager)
723
+
724
+ ```powershell
725
+ # no postinstall in the package → no pnpm approve-builds / allowBuilds; one command installs everything
726
+ dsh plugin --profile web add dsh-code-server-app@0.2.1
727
+ # a local tarball works the same way:
728
+ dsh plugin --profile web add C:\Users\User\Desktop\dsh-code-server-app\dsh-code-server-app-0.2.1.tgz
729
+ ```
730
+
731
+ Ready to use immediately — **no second step, no "Install environment", no install-guide modal**.
732
+ The main package is only **~110KB** (the plugin's own code plus the launcher); everything else is dependencies:
733
+
734
+ - **the VS Code tree** (`lib/vscode` 196.9MB + `out/browser` + `src/browser`) is a **platform-independent package**
735
+ `@jinsiyu/dshcs-vscode-server@<code-server version>` declared in the plugin's `dependencies`; it runs from
736
+ `<profile>\node_modules\@jinsiyu\dshcs-vscode-server\vscode` (the legacy full tree at
737
+ `@jinsiyu/dshcs-code-server/code-server` is still recognised as a fallback);
738
+ - the **pure-JS part** of VS Code's inner dependencies (35 packages: xterm / katex / typescript / ws / tar …) is
739
+ declared in the plugin's `dependencies` and installed by pnpm into the profile's `node_modules` (hoisted);
740
+ - the **binary part** comes entirely from `@jinsiyu/dshcs-*` sub-packages, declared **directly on the plugin's own
741
+ dependency table** (since 0.3.45): the 8 platform-independent repacks (`node-pty` / `koffi` / `ssh2` /
742
+ `cpu-features` / `@parcel/watcher` / `@vscode/fs-copyfile` / `@vscode/proxy-agent` / `@microsoft/mxc-sdk`) go into
743
+ `dependencies` under their real names; the 8 platform-specific ones (`@vscode/sqlite3` / `spdlog` / `kerberos` /
744
+ `deviceid` / `native-watchdog` / `windows-registry` / `windows-process-tree` / `windows-ca-certs`) go into
745
+ `optionalDependencies` once per target (real names + their own os/cpu gates), so one command picks the right arch;
746
+ the **original names** are restored at runtime by junctions created from `lib/vendored.json`;
747
+ - consequently the dependency graph contains **no package with pre/install/postinstall or a `binding.gyp`** →
748
+ no profile `allowBuilds`, no build script ever runs, and **the user machine needs no C++ toolchain**;
749
+ - **upgrading the plugin no longer re-downloads the tree**: the tree package is cached by version
750
+ (~60MB, ~197MB unpacked).
751
+
752
+ ### Install mechanism (why it is built this way)
753
+
754
+ - **The pnpm 11 hard constraint**: any package in the dependency graph whose manifest has
755
+ `preinstall|install|postinstall` (or that ships a `binding.gyp`/`.hooks`) counts as "needs building" and must be
756
+ approved by the **host profile's** `pnpm-workspace.yaml` via `allowBuilds`, otherwise `dsh plugin add` exits 1 with
757
+ `[ERR_PNPM_IGNORED_BUILDS]`. A dependency's own `pnpm.allowBuilds`, `.npmrc`, `patch:` protocol and
758
+ `optionalDependencies` do not help (measured 2026-09, pnpm 11.25);
759
+ - **the tree** is prepared at pack time with `npm install code-server@<version> --ignore-scripts` (skipping the official
760
+ `sh ./postinstall.sh`, which cannot run on Windows), then `scripts/vendor-vscode-server.mjs` keeps **only the VS Code
761
+ tree**: `lib/vscode/**`, `out/browser/**`, `src/browser/**` plus the license files are copied to `vendor/vscode/`, and a
762
+ generated root `package.json` records the upstream code-server version. code-server's own `out/node/**` and its 136
763
+ runtime dependencies **no longer ship** — they are replaced by `lib/launcher.mjs`;
764
+ - **the packages that need a toolchain** are repacked into `@jinsiyu/dshcs-*` by `scripts/vendor-repacks.mjs`:
765
+ the compiled package directory is copied and its `scripts` / `files` / `binding.gyp` / `.hooks` / `.npmignore` are
766
+ **removed** (the built `.node` and every runtime file stay) → sibling packages in its dependency list become `npm:`
767
+ aliases → platform-specific ones get `os`/`cpu` plus a `-<platform>-<arch>` suffix. For win32 targets the script also
768
+ verifies each `.node` PE machine (0x8664=x64 / 0xaa64=arm64) so a cross-compiled artifact cannot ship the wrong arch;
769
+ - **how the original names come back** (since 0.3.45): a repack's real name is `@<scope>/dshcs-<name>` while VS Code
770
+ imports `node-pty` / `@vscode/sqlite3`; pack time writes the "original name → real name" table into
771
+ `lib/vendored.json` (shipped with the plugin) and `lib/native.js` creates `<tree>/node_modules/<original name>`
772
+ junctions to the real directories (idempotent, self-healing).
773
+ **Why the old "platform aggregator + `npm:` aliases" is gone**: pnpm's incremental hoisted install drops those
774
+ aliased packages when they sit inside an **optional subtree** (measured: 9 of 16 missing) while dsh-desktop
775
+ validates the dependency graph right after the install ⇒ the first install always failed with `requires missing`;
776
+ with real-name direct dependencies the same install command plus the validator's own predicate passes end to end
777
+ (reproduction in `docs/desktop-first-install-root-cause.md`);
778
+ - **resolution path**: the host finds the tree with `require.resolve('@jinsiyu/dshcs-vscode-server/package.json')`
779
+ (then the inner `vscode/` directory) and the entry is `vscode/lib/vscode/out/server-main.js`; VS Code's inner deps are
780
+ resolved upwards from that root (`vscode/lib/vscode/node_modules` → package `node_modules` → `<profile>/node_modules`).
781
+ The legacy full tree (`@jinsiyu/dshcs-code-server/code-server`) is still recognised as a fallback;
782
+ - **runtime layout self-healing** (`ensureRuntimeLayout()` in `lib/native.js`, idempotent, run **at activation before
783
+ `envCheck` and again before every start**): the host adds two kinds of **junctions** (Windows junctions / POSIX dir
784
+ symlinks) into the tree:
785
+ 1. `ensureAliasLinks()`: links the 16 **original names** listed in `lib/vendored.json` into `<tree>/node_modules`
786
+ — the real-name packages live in the plugin's dependency graph, and `lib/vscode/out/server-main.js` uses
787
+ **ESM imports** (ESM ignores `NODE_PATH`), so a missing link means an immediate 500;
788
+ 2. `ensureInnerModuleLinks()`: restores VS Code's **inner dependency directories**
789
+ `lib/vscode/node_modules` and `lib/vscode/extensions/node_modules` from the two `package.json` files — the trimmed
790
+ tree ships neither, and code that builds dependency paths explicitly (e.g. the bundled TypeScript extension looking
791
+ for `<ext>/../node_modules/typescript/lib/tsserver.js`) otherwise reports
792
+ "VS Code's tsserver was deleted by another application…" (measured with 1.136.1).
793
+ > **Size note**: the plugin tarball is **~110KB**; `@jinsiyu/dshcs-vscode-server` is **~60MB** (~197MB unpacked);
794
+ > the 16 native packages add ~250MB. A full install downloads roughly 310MB. Neither `vendor/` nor `repack/` is committed to git (see `.gitignore`).
795
+
796
+ > **Upgrading from ≤ 0.1.43**: the tree package changed from `@jinsiyu/dshcs-code-server` (the full code-server tree with
797
+ > `out/node` and 136 runtime deps) to `@jinsiyu/dshcs-vscode-server` (the trimmed tree). **The new code defaults to
798
+ > `serve: loopback`, which behaves exactly like 0.1.43**; switch to `serve: dsh` for same-origin mounting. The install
799
+ > command is unchanged (`dsh plugin --profile web add dsh-code-server-app@<version>`), and pnpm drops the old
800
+ > `dshcs-code-server` sub-package.
801
+ ### Development: install from source (changes take effect immediately)
802
+
803
+ ```powershell
804
+ dsh plugin --profile web add C:\Users\User\Desktop\dsh-code-server-app
805
+ ```
806
+
807
+ > A source path installs via `link:`. On a dev machine without `vendor/code-server`, run
808
+ > `pnpm run vendor:vscode -- --dev-links` first. Dependencies (inner JS deps + the platform aggregator) are installed by pnpm
809
+ > too — but the not-yet-published local `@jinsiyu/*` packages must either be published first, or the
810
+ > `repack/tgz/*.tgz` files must be installed into the profile as `file:` dependencies.
811
+ >
812
+ > **Changing the client bundle**: edit `src/factory.js` then run `pnpm run build:client`
813
+ > to regenerate `lib/client.js` (that artifact is not tracked; a browser refresh picks it up — no host restart needed).
814
+ > **Changing the ask panel**: edit `assets/extensions/dshcs-editor-bridge/webview/src/*` then run
815
+ > `pnpm run build:webview` (same convention: generated, not tracked; the IDE must be restarted once to pick it up,
816
+ > because the extension host caches the webview resources).
817
+
818
+ ### Pack-machine environment (the user machine needs nothing)
819
+
820
+ **A toolchain is needed at pack time only — never on the user machine.**
821
+
822
+ | Env | Version / requirement | User machine | Pack machine |
823
+ |---|---|---|---|
824
+ | Node.js | **v24.x** (latest code-server requirement; v24.13.1 here) | required | required |
825
+ | npm / pnpm | npm ships with Node; pnpm comes from DSH | required (installs deps) | required |
826
+ | **MSVC build tools** | **VS Community 2026 + C++ desktop workload** | ❌ **not needed** | pack time (16 native packages) |
827
+ | **VS Spectre-mitigated libs** | one set for ARM64 **and** one for x86/x64 ("MSVC v14x Spectre-mitigated libs") | ❌ not needed | pack time (otherwise MSB8040) |
828
+ | Python | **3.13.x** | ❌ not needed | pack time (node-gyp) |
829
+ | node-gyp | **13.x** (older versions don't recognize VS 2026) | ❌ not needed | pack time |
830
+
831
+ > **Packing still works without the Spectre libs**: when a package fails to compile, `vendor-repacks.mjs` downgrades
832
+ > `SpectreMitigation` to `false` in that architecture's `*.gyp` files and retries (only the Spectre hardening is
833
+ > lost, functionality is unaffected) and says so in the log.
834
+
835
+ ### Windows native build notes (pack time, verified locally ARM64)
836
+
837
+ - **VS needs the Spectre-mitigated libraries** (MSB8040): Visual Studio Installer → Individual components →
838
+ "MSVC v14x Spectre-mitigated libs" — **install the ARM64 and the x86/x64 sets separately**.
839
+ - **node-gyp 13.x** (9.x does not recognize VS 2026): `npm install -g node-gyp@latest`.
840
+ - **x64 cross-compiling**: `vendor-repacks.mjs` uses `npm install --os=win32 --cpu=x64 --ignore-scripts` to fetch
841
+ the packages, then `npm rebuild --arch=x64` per package; the resulting PE machine types were verified
842
+ (kerberos / sqlite3 / spdlog …).
843
+ - Latest code-server requires **Node v24**.
844
+ - If you don't need the self-contained install (e.g. a global code-server already exists), skip it:
845
+ the plugin falls back to a configured/PATH `bin` (see the "Config" table).
846
+
847
+ ### Upgrading the VS Code tree (upstream = a code-server release)
848
+
849
+ - **The version is decided at pack time**: `pnpm run vendor:latest` (= `--force`) pulls the tree of the npm **latest**
850
+ release; or use `pnpm run vendor:vscode -- --version 4.137.0` / `DSHCS_CODE_SERVER_VERSION`.
851
+ With an existing `vendor/vscode`, a plain `pnpm pack` never upgrades (it is a no-op).
852
+ **Source-tree precedence (fixed in 0.2.13)**: an explicit `--from` uses that tree, while an explicit
853
+ `--force`/`--version` now **always goes to the registry** — before the fix a local source tree won
854
+ (`defaultSourceTree()` hit `vendor/code-server` or an installed profile tree first), so the documented
855
+ "`vendor:latest` takes latest from the registry" silently kept the old version (hit while upgrading to 0.2.13:
856
+ the bundled tree stayed at 4.136.2). A run with neither flag still reuses a local tree to save the download.
857
+ - **Sync the inner dependency pins when the tree changes**: in `--reuse` mode the pure-JS install set is read from
858
+ the **plugin's package.json**, so update those pins from the new tree's `lib/vscode/package.json` first
859
+ (this time: 10 `@xterm/*` beta bumps). The rule is "only move a pin that no longer satisfies the new range",
860
+ which keeps already-newer pins such as `cookie`/`ws`/`tar`/`node-addon-api` from being downgraded.
861
+ - **Check first**: `pnpm run vendor:check` prints the bundled version / upstream latest.
862
+ - **A version bump means rebuilding and republishing the sub-packages** (all with the same script):
863
+ 1. `pnpm run repack:build -- --target win32-arm64,win32-x64 --pack` → the new tree package
864
+ (`@jinsiyu/dshcs-vscode-server@<new version>`) and the natives rebuilt against the new inner dependencies (the
865
+ script also rewrites the plugin's pure-JS `dependencies` and both aggregator versions);
866
+ 2. `pnpm run republish:repacks` (`pnpm run publish:repacks`) → publish; then bump the plugin version → `pnpm pack`
867
+ → publish the plugin.
868
+ - `productPath` (`<quality>-<commit>`, part of the client WebSocket path) is **computed from `lib/vscode/product.json`**,
869
+ so upgrading the tree needs no code change — but the routes are registered at activation, so restart `dsh web` afterwards.
870
+ - **No runtime auto-upgrade anymore**: nothing fetches latest at startup; the version is fully determined by the bundled artifact.
871
+ - Bundled locally right now: the tree of `code-server@4.137.0` (VS Code 1.137.0, `productPath=stable-b11dabda…`).
872
+
873
+ ### Compatibility with the old install locations
874
+
875
+ Host probe order: `@jinsiyu/dshcs-vscode-server/vscode` (**the real layout since 0.2.0**) >
876
+ `@jinsiyu/dshcs-code-server/code-server` (the full tree, 0.1.40–0.1.43) >
877
+ `@jinsiyu/dshcs-code-server-<platform>-<arch>/code-server` (the 0.1.37 platform sub-packages) >
878
+ the in-package `vendor/vscode` > the in-package `vendor/code-server` (development). The old install root
879
+ `<profile>\.code-server-app` is only mentioned in a startup log line; nothing writes to it any more.
880
+ ## Settings card (Settings → Plugins → Code Server)
881
+
882
+ Modeled after dsh-auto-open-web's custom card, registered on the `settings.plugin.item` slot,
883
+ persisted via the official settings domain (`settingsScope`, namespace `code-server`) into the official settings document:
884
+
885
+ | Key | Default | Description |
886
+ |---|---|---|
887
+ | `claimExtensions` | `*;!md;!markdown;!html;!htm;!png;!jpg;!jpeg;!gif;!webp;!bmp;!ico;!svg;!pdf` | **Claim types** (0.2.11, replaces 0.2.5's `fileOpenScope`): decides by extension which files go to VS Code, semicolon-separated; `*` claims every other type, `!ext` excludes (exclusion wins). The default leaves the four categories DSH's preview renders well (markdown/html/images/PDF) to DSH and sends everything else to the IDE; an empty value claims nothing. **Scope (session vs absolute) is no longer distinguished** |
888
+ | `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 |
889
+ | `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) |
890
+
891
+ (Since 0.2.9 the card keeps only those three settings; `windowedOpen` and `reserveComposer` are gone — leftover keys in an old
892
+ settings document neither fail nor apply. `serve` remains a key in the settings namespace (usable from a settings document) but
893
+ has **no card row** — see "Serving mode".)
894
+
895
+ > Card changes take effect immediately via `scope.watch` (the host status API returns `keepResident`, `claimExtensions` and
896
+ > `fullscreenOnOpen`; the client applies them at once); no dsh restart needed. **After adding new setting keys, restart dsh web before first use**,
897
+ > so the host re-registers the settings namespace (schema includes the new key); otherwise save/validation of the new key won't work.
898
+
899
+ Since 0.2.7 the card has **no** "Entry", "dependency install" or "environment check" rows: the entry lives in the sidebar's
900
+ guide page (and in DSH's own file clicks), and diagnostics stay out of the UI — the `/api/code-server/status` `env` field still
901
+ reports the tree version / `productPath` / server entry, VS Code inner dependencies and **prebuilt native packages**
902
+ (platform aggregator name + resolved module count) for scripts, and the DSH host log carries the `[code-server]` lines.
903
+
904
+ ## Config (`config` in cordis.patch.yml; all have defaults)
905
+
906
+ | Key | Default | Description |
907
+ |---|---|---|
908
+ | `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 |
909
+ | `host` | `127.0.0.1` | Bind address; `auth: none` only allows loopback (localhost/127.0.0.1/::1) |
910
+ | `port` | `0` | Port for loopback mode; **`0` = a random free port assigned per start** (the actual one is written to `endpoint.json` and read back by the host). Give an explicit port to pin it; when that port is taken and no valid `pid.json` exists, startup fails with diagnostics instead of killing a stranger |
911
+ | `auth` | `none` | `none` \| `password`; non-loopback host automatically requires password |
912
+ | `passwordToken` | `''` | Token for password mode (passed to code-server via the `PASSWORD` env var) |
913
+ | `userDataDir` | `$DSH_HOME/code-server/user-data` | User-data isolation directory |
914
+ | `extensionsDir` | `$DSH_HOME/code-server/extensions` | Extensions directory |
915
+ | `readyTimeoutMs` | `60000` | `/healthz` readiness probe timeout |
916
+ | `editorBridge` | `true` | **Editor bridge** (since 0.3.0): the read-only channel between the in-tree `dshcs-editor-bridge` extension and the host (see "Working with DSH"). Off = no `bridge.json`, no `editor_context`/`editor_diagnostics`, the extension stays dormant. `code-server.editorBridge` in the settings document toggles it **live** |
917
+
918
+ User-level override example (write in `$DSH_HOME/profiles/web/cordis.patch.yml`, using the `- id: code-server` row):
919
+
920
+ ```yaml
921
+ - id: code-server
922
+ config:
923
+ port: 8091
924
+ # Explicit (overrides dependency-install probing): a globally installed shim, or any entry.js
925
+ bin: C:\Users\User\AppData\Roaming\npm\code-server.cmd
926
+ ```
927
+
928
+ ## JSON API (same-origin fetch; identical paths in web and desktop)
929
+
930
+ **No `webServer` dependency**: the host half registers its routes on DSH Connection's shared `/api` channel through
931
+ `ctx.connection.fetch.register`. In the web profile Connection mounts the `/api` prefix on webServer itself (with the
932
+ Host/Origin fence and browser auth); in the desktop profile `apps/desktop-host` feeds `/api/*` into the same
933
+ `createSharedFetchHandler('/api')` (IPC framed pipe, no HTTP server). The client only writes relative paths
934
+ (`fetch('/api/code-server/<op>')`), so both carriers behave identically.
935
+
936
+ | Method | Path | Description |
937
+ |---|---|---|
938
+ | 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) |
939
+ | POST | `/api/code-server/start` | body `{ cwd? }` (omit cwd to keep the current workspace); idempotent; changing cwd while running only **switches the directory, without restarting the process** (0.2.12) |
940
+ | POST | `/api/code-server/stop` | Stop and recycle the process tree |
941
+ | 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 |
942
+ | 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 |
943
+ | GET | `/code-server-bridge/health` | editor-bridge liveness (**unauthenticated**; no editor data). Runs over **local IPC** (named pipe / unix socket), not under `/api`, and needs no `webServer` |
944
+ | POST | `/code-server-bridge/sync` | editor bridge: the extension pushes state (`{context, diagnostics, workspace, at}`) and takes back events; `?since=<seq>` is the event cursor. Requires `x-dshcs-bridge-token`, and **any Origin header is 403** |
945
+ | POST | `/code-server-bridge/ask` | editor bridge: push an editor question into the current session (`{text, file?, lineStart?, lineEnd?, selection?, languageId?}`); **409** when no session can receive it |
946
+ | POST | `/code-server-bridge/event` | editor bridge: extension reports open/close and similar (host log tail). Requires the token |
947
+
948
+ > All four bridge routes carry their own token check — they **cannot** rely on DSH's cookie fence, because the
949
+ > extension host has no browser cookie — and they are read-only by construction. See "Working with DSH" above.
950
+
951
+ > The plugin no longer registers `/code-server/*` webServer-only routes, and the code-server icon is inlined as a data URI
952
+ > in the client bundle — the client requests no plugin-owned HTTP resource at all.
953
+
954
+ ## DSH Desktop (no webServer)
955
+
956
+ - The host half is `inject = ['connection', 'settings']` (**no `webServer`**) — the desktop profile disables webserver/web-runtime
957
+ and the plugin still works: `/api/*` requests travel Electron `dsh-app://` protocol handler → IPC framed pipe → `createSharedFetchHandler('/api')`.
958
+ - The right-sidebar tab, guide entry box, file-address claim, and settings card behave the same as in web (code-server remains an
959
+ 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).
960
+ 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
961
+ not a regression for desktop; the only difference is the missing `webServer`, where `serve: dsh` falls back to loopback (that path
962
+ genuinely needs a webServer).
963
+ - **The editor bridge works on desktop since 0.3.13**: it runs over local IPC (named pipe) and does not involve `webServer` at all —
964
+ the extension host is a child of the IDE the plugin itself spawned, so both ends are on the same machine. The host injects
965
+ `DSHCS_EXTENSIONS_DIR`, the extension finds `bridge.json`, and `/status` reports `bridge.supported=true` with the pipe name.
966
+ - Install into the desktop profile through the **desktop plugin manager** (not the CLI, see below).
967
+ - **Desktop installs face a 24-hour supply-chain policy (measured 2026-09-10; this is how 0.2.4 got installed)**:
968
+ - the CLI path is unavailable: `dsh plugin --profile desktop …` is rejected (*"profile "desktop" is managed exclusively by the
969
+ Electron application"*), so desktop installs only go through the app's package transaction (`pnpm add <spec> --save-exact`,
970
+ executed in `~/.dsh/desktop/staging/<uuid>/profile` before activation);
971
+ - that transaction's pnpm (the app bundles **11.7.0**, patched by DeepSeek) runs a **lockfile supply-chain verification** before
972
+ `add` ("Verifying lockfile against supply-chain policies (717 entries)") which requires packages to be **at least 24 h old**,
973
+ otherwise `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`;
974
+ - **the two stages behave differently (measured)**:
975
+ - verifying an **existing lockfile**: `minimumReleaseAgeExclude` is *not* honoured (exact versions and bare package names
976
+ were both tried);
977
+ - **resolution** (no lockfile to verify, e.g. after `pnpm clean --lockfile`): the list *is* honoured, and pnpm even appends
978
+ entries itself (the install log prints *"Added N entries to minimumReleaseAgeExclude…"*);
979
+ - so the working recipe for a **just-published** (<24 h) version on desktop is to start from a clean, lockfile-free profile:
980
+ 1. `pnpm clean --lockfile` (**note: it also deletes `node_modules`**, leaving the profile to be reinstalled);
981
+ 2. with the app's bundled runtime, run `add <spec> --save-exact --trust-lockfile` in the profile directory
982
+ (runtime/store/config live under `~/.dsh/desktop/pnpm/{store,cache,state,config,home}`,
983
+ `--config.userconfig=…/config/npmrc`, otherwise pnpm fails with `ERR_PNPM_UNEXPECTED_STORE` /
984
+ `…UNEXPECTED_VIRTUAL_STORE`);
985
+ 3. the app's boot command (`install --offline --frozen-lockfile --trust-lockfile`) then passes (lockfile matches
986
+ package.json, packages are in the store); if the plugin name is already in `dsh.profile.bundles` nothing else is needed.
987
+ - do **not** try to bypass it with `minimumReleaseAge: 0`: it does clear the check, but that key is **not** one of the policy
988
+ sections the app tolerates (`project-manager.ts` ignores only `minimumReleaseAgeExclude:` / `trustPolicyExclude:` and validates
989
+ the manifest before every `mutate()`), so persisting it makes the app fail with
990
+ *"core package mapping does not match desktop-packages.json"*.
991
+ - Alternatively **wait out the 24 h** and install normally from the plugin manager. The web profile is unaffected: its
992
+ `pnpm-workspace.yaml` sets `minimumReleaseAge: false`.
993
+ - this plugin's closure contains platform native sub-packages (`@jinsiyu/dsh-code-server-runtime-win32-*`) published together with
994
+ the plugin itself, so every new version hits that policy on desktop.
995
+ - **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):
996
+ - symptom: `lib/client.js` in the profile is the new version, yet the renderer keeps running the old code —
997
+ `%APPDATA%\@deepseek-ai\dsh-desktop\Code Cache\js` only contains strings unique to the old version (e.g. `dshcs-artifacts`)
998
+ and none unique to the new one (`claimExtensions` / `fullscreenOnOpen`), and `Cache\` still holds an old response body referencing
999
+ `dsh-code-server-app`. The same applies to first-party plugins (the cached `ui-deliverables` even lacks the current
1000
+ `data-presented-files-row` marker);
1001
+ - diagnosis (byte level — do **not** use `Select-String`, which reads files with the console encoding and gives false
1002
+ negatives on non-ASCII markers): search `Code Cache\js` for an **ASCII** marker unique to the new version
1003
+ (since 0.2.11 ours is `claimExtensions`); a hit proves the new bundle really was compiled;
1004
+ - fix: fully close the app, delete the `Cache`, `Code Cache` and `GPUCache` directories, then start it (cache only —
1005
+ profiles, sessions and settings are untouched):
1006
+ ```powershell
1007
+ 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"
1008
+ ```
1009
+ - scope: this is not specific to this plugin — **any** client plugin may keep running old code after an upgrade;
1010
+ after a release, confirm with the marker trick above that the renderer actually swapped bundles.
1011
+
1012
+ ## File-open plumbing (what the plugin itself still does)
1013
+
1014
+ DSH's own chips and preview buttons are what users click (see "Opening files" above); this plugin adds no row of its own.
1015
+ What remains on the plugin side:
1016
+
1017
+ - the tab body parses `navigation.address` and posts the absolute path (plus an optional `line`) to
1018
+ `/api/code-server/open-file`, which writes a signal file;
1019
+ - the bundled `dshcs-open-file` extension polls that file and calls `showTextDocument` — VS Code Web has no official
1020
+ "open this file from outside" API, so this is the only way to aim the workbench at a file. It is installed as a
1021
+ **built-in** extension (in `lib/vscode/extensions`), so users cannot remove it from the extensions panel, and the
1022
+ installer re-syncs it whenever its content changes.
1023
+
1024
+ ## Known limitations
1025
+
1026
+ - **~~The editor bridge needs DSH to provide `webServer`~~ no longer true (fixed in 0.3.13)**: the bridge now runs
1027
+ over **local IPC** (Windows named pipe / unix socket via `http.request({ socketPath })`), so **web and desktop share
1028
+ one path**, with no `webServer` and no open port. History: 0.3.9–0.3.12 mounted it under DSH's webServer prefix
1029
+ (⇒ desktop stayed dormant); up to 0.3.7 it was registered under `/api/code-server/bridge/*` and was killed by
1030
+ Connection's cookie fence (401). **File opening** was never affected (it uses the signal file).
1031
+ - **`/code-server-bridge/health`'s `bridge` field does not mean the extension is running** (clarified in 0.3.12):
1032
+ it only says the bridge *target* is configured. Whether the extension actually runs shows up in the exthost log
1033
+ or by simply calling `editor_context` — 0.3.0–0.3.11 sat in the state "health says bridge:true, extension never
1034
+ loaded" (cause above: the user-level install was marked `.obsolete`).
1035
+ - **Bridged state can lag by up to 600 ms**, and the tools say "stale" rather than serving data older than 10 s.
1036
+ - **The ask panel renders only "new content" (0.3.22)**: the subscription starts when the panel opens, the history
1037
+ `records` from `follow`'s opening frame are discarded, and the panel has **no "load earlier"** (the history-paging
1038
+ API `sessionController.page()` is deliberately not called in this version). Switch to the DSH UI for older content.
1039
+ - **Panel highlighting ships only DSH's boot grammar set** (typescript / shellscript / json): the rest of the
1040
+ official grammars load lazily through `import()` (~1.6MB total), and the panel is a single-file IIFE with no
1041
+ lazy loading, so those languages render as plain text (exactly like DSH's own first render, no errors).
1042
+ For the full set: `node scripts/build-webview.mjs --all-grammars`.
1043
+ - **Panel assets are pinned to the DSH version**: the renderer is bundled against the UI version of the deployed
1044
+ DSH, so after upgrading DSH you must rebuild the panel (`pnpm run build:webview`; the build fails loudly on a
1045
+ version mismatch). The panel also shows a mismatch notice at runtime instead of silently using the wrong renderer.
1046
+ - **The approval window in the panel is 5 minutes**: while the panel is open, approvals ask the panel first (the card
1047
+ shows a countdown); **closing the panel** or letting the 5 minutes run out hands the request back to the DSH UI —
1048
+ after that, that request can **only** be answered there (the card disappears from the panel and the thread keeps an
1049
+ audit row).
1050
+ - **Unsaved buffers are reported, not taken over.** The agent still edits via its own `fs` tools, i.e. against
1051
+ disk. What the bridge adds is a notice *before* writing a dirty file, a diff *after*, and a warning instead of
1052
+ an overwrite. It does not decide whether the user saves — that would mean changing the agent's read path,
1053
+ which is out of scope for this version.
1054
+
1055
+ - ~~No sub-path~~ **no longer true (corrected with measurements in 0.2.0)**: the workbench HTML VS Code renders references
1056
+ **only relative URLs** (9 references measured, 0 absolute; `serverBasePath="."`, `rootEndpoint="."`), and the client
1057
+ builds its WebSocket path from `location.pathname + join(serverBasePath ?? '/', <quality>-<commit>)`. The IDE can
1058
+ therefore be mounted directly under DSH's own `/code-server/*` (`serve: dsh`) — no second port, no HTML rewriting.
1059
+ Item-by-item evidence: `docs/analysis-code-server-as-dsh-plugin.md`.
1060
+ - **`serve: dsh` cannot proxy forwarded-port WebSockets**: `registerUpgrade` matches exact paths while `/proxy/:port`
1061
+ carries the port in the path, so WebSocket forwarding for the Ports panel is unavailable in that mode (HTTP forwarding
1062
+ works). Use `serve: loopback` when you need it.
1063
+ - **`serve: dsh` shares DSH's origin**, so the iframe is not sandboxed there (same-origin plus `allow-same-origin` is
1064
+ escapable by the frame itself); in `loopback` mode the iframe is cross-origin and `sandbox` stays as real protection.
1065
+ - **Single instance across sessions**: one shared IDE per host; switching cwd only re-navigates the workbench (since 0.2.12 no process restart, so the old directory's background terminals are not collected).
1066
+ - **Older DSH versions are unsupported (since 0.2.3)**: on a DSH without `sidebarRightTabs` / `sidebarRight` the plugin
1067
+ offers nothing but an upgrade notice on the settings page; older-DSH users should stay on `0.2.2`
1068
+ (`dsh plugin --profile web add dsh-code-server-app@0.2.2`).
1069
+ - **Sidebar tab switching** (no longer reloads since 0.2.2): DSH's right sidebar renders only the active tab's body, and
1070
+ a React unmount moves the iframe away; the plugin keeps it as a singleton resident surface and shuttles it between the
1071
+ dock slot and a document-level park container with `Element.moveBefore()` (a state-preserving atomic move), so
1072
+ switching tabs or collapsing the sidebar and back **does not reload** it. Browsers without `moveBefore` fall back to
1073
+ the old behaviour (`appendChild` → full reload), reported as `degraded`; see "Why switching tabs no longer reloads".
1074
+ - **Remote access**: with `serve: dsh` the browser only needs to reach DSH itself (one port, protected exactly like `/api`);
1075
+ `serve: loopback` binds to loopback only (random port + path token + Host allowlist — see "Security model of the loopback
1076
+ port"), so use `serve: dsh` for cross-machine access (0.2.0 no longer supports `auth: password`).
1077
+ - **The loopback token rotates per instance**: port and token change on every new start; adoption after a host restart
1078
+ matches the live instance through the `endpoint.json` and `path-token` files, so **do not delete those two files**
817
1079
  (without them the host cannot recognise the old instance and treats the port as foreign).