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 +1078 -816
- package/README.md +1035 -787
- package/assets/extensions/dshcs-editor-bridge/webview/THIRD-PARTY.md +6 -5
- package/assets/extensions/dshcs-editor-bridge/webview/src/official-tokens.css +3 -1
- package/assets/extensions/dshcs-editor-bridge/webview/thread.css +1 -1
- package/assets/extensions/dshcs-editor-bridge/webview/thread.js +52 -52
- package/lib/client.js +2 -2
- package/lib/dsh-resolve.mjs +106 -13
- package/lib/index.js +2339 -2320
- package/lib/launcher.mjs +549 -549
- package/lib/native.js +28 -1
- package/lib/serve-dsh.mjs +162 -162
- package/lib/vendored.json +53 -9
- package/package.json +43 -26
- package/scripts/vendor-repacks.mjs +406 -39
- package/vendor/VENDOR.json +4 -4
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 →
|
|
368
|
-
the
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
>
|
|
431
|
-
> `
|
|
432
|
-
>
|
|
433
|
-
> `
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
|
450
|
-
|
|
451
|
-
|
|
|
452
|
-
|
|
|
453
|
-
| **
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
`
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
`
|
|
497
|
-
|
|
498
|
-
`
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
`
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
(
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
`
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
-
|
|
576
|
-
|
|
577
|
-
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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).
|