dsh-code-server-app 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -21,10 +21,47 @@ A static profile plugin (npm package with host + client bundle) that ships the *
21
21
 
22
22
  - Detection: `ctx.inject(['sidebarRightTabs','sidebarRight'], …)` registers the tab type only when the services are ready; if they never appear (or registration fails) nothing is registered and the floating ball fallback stays in place. No version comparison, and the plugin's own activation is never blocked.
23
23
  - 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.
24
- - **Known trade-off**: DSH renders only the active tab's body, so switching away and back remounts the iframe (a full code-server reload; unsaved editor buffers are lost). Float the tab into its own panel or keep it active for long-running sessions.
24
+ - **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).
25
25
  - In sidebar mode the settings card hides "Reserve space above the composer" (floating-window geometry only). "Open in a window (new tab)" still applies to every entry point.
26
26
  - `windowedOpen` has the highest priority: when on, entry buttons always open a browser tab.
27
27
 
28
+ ## Why switching tabs no longer reloads (resident IDE)
29
+
30
+ **The old trap**: DSH's right sidebar (ui-dockkit) renders **only the active tab's body**
31
+ (`TabPanel.tsx:412` → `renderTab(active)`) — switching to another tab unmounts that body in React, which moves the
32
+ iframe out of the document and destroys its browsing context; switching back is a full VS Code reload (unsaved buffers
33
+ lost). Floating the tab into its own panel only worked around it.
34
+
35
+ **What it does now (`src/surface.js` in the client, 0.2.2)**: the plugin takes the iframe **away from React** and turns
36
+ it into a **singleton resident surface**:
37
+
38
+ | Situation | Action | Result |
39
+ |---|---|---|
40
+ | tab becomes active | `host.moveBefore(frame, null)` into the visible dock slot | state-preserving atomic move, **no reload** |
41
+ | 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 |
42
+ | workspace / port changes | assign `src` explicitly | the only normal "reload" entry point |
43
+
44
+ - **Why `moveBefore`**: measured in a real browser (Edge/Chromium 151), a plain `appendChild` move resets the iframe's
45
+ internal timers (i.e. reloads it), while `Element.moveBefore()` (Chromium ≥133) preserves state (a probe counter keeps
46
+ counting 1→2).
47
+ - **Degradation is never silent**: when `moveBefore` is missing, or the host was already detached by React and it throws
48
+ `HierarchyRequestError: invalid hierarchy` (passive effect cleanup runs after DOM removal), the code falls back to
49
+ `appendChild` — one reload, but the frame is **never lost** — and reports `degraded` / `lastMoveError` so the UI can
50
+ say "residency unavailable".
51
+ - **Repaint fix (measured)**: after a long offscreen park, a moved-back cross-origin iframe had correct size, hit
52
+ testing and `visibility`, yet **stopped repainting** (a fully white panel that several tab switches did not fix).
53
+ `translateZ(0)` and `opacity` nudges did nothing; `display:none → forced reflow → restore` inside a single JS task
54
+ revives it without reloading the iframe document, without losing internal state and without a visible flash. Every
55
+ park→dock transition therefore runs one `nudgeRepaint()` (counted as `surfaceSnapshot().nudgeCount` for debugging).
56
+ - **Warm-up**: with `keepResident` (default `true`) the host builds the surface right after plugin start and leaves it
57
+ parked, so the first tab open needs no cold start; preloading never yanks a surface that is currently docked.
58
+ - **Debug handle**: `window.__dshcsSurface` (`snapshot()`, `setParkStrategy('offscreen'|'behind')`, `dock()`, `park()`,
59
+ `nudge()`, `setNudgeEnabled(false)`, `destroy()`).
60
+
61
+ **Measured** (DSH web GUI, real mouse clicks between sidebar tabs): switching away → `docked:false`, same iframe node,
62
+ in-frame probe still alive, `degraded:false`; switching back → `docked:true`, unchanged `src`, IDE pixels and editing
63
+ state preserved (no full reload). Full evidence and probe scripts: `docs/analysis-code-server-as-dsh-plugin.md`.
64
+
28
65
  ## Serving mode (`serve`)
29
66
 
30
67
  | Mode | What it does | Requires |
@@ -40,6 +77,11 @@ A static profile plugin (npm package with host + client bundle) that ships the *
40
77
  **WebSockets** cannot be routed because `registerUpgrade` matches exact paths while `/proxy/:port` carries the port in
41
78
  the path (HTTP forwarding works; use `loopback` when you need WS forwarding).
42
79
 
80
+ - In `loopback` mode every upgrade passes a **code-server-equivalent Origin check** (since 0.2.1): when an `Origin`
81
+ header is present its host must equal `Host` (honouring `Forwarded: host=` / `X-Forwarded-Host`, like code-server),
82
+ otherwise the handshake gets `403`; non-browser requests without `Origin` are allowed. Without that check any local
83
+ browser page could complete a handshake against `ws://127.0.0.1:<port>/stable-<commit>` and drive the IDE.
84
+
43
85
 
44
86
  - **Floating ball** (bottom-right, official code-server icon, above the composer): click to **expand the floating window and light it up** (blue glow), click again to **collapse**; **drag to any position** (remembered across refreshes; no accidental click after drag);
45
87
  no sidebar button, no window control button group (the ball is the only entry/toggle); the ball carries a status dot (green = running / amber = starting / red = error);
@@ -121,9 +163,9 @@ pnpm run promote -- <version>
121
163
 
122
164
  ```powershell
123
165
  # no postinstall in the package → no pnpm approve-builds / allowBuilds; one command installs everything
124
- dsh plugin --profile web add dsh-code-server-app@0.2.0
166
+ dsh plugin --profile web add dsh-code-server-app@0.2.1
125
167
  # a local tarball works the same way:
126
- dsh plugin --profile web add C:\Users\User\Desktop\dsh-code-server-app\dsh-code-server-app-0.2.0.tgz
168
+ dsh plugin --profile web add C:\Users\User\Desktop\dsh-code-server-app\dsh-code-server-app-0.2.1.tgz
127
169
  ```
128
170
 
129
171
  Ready to use immediately — **no second step, no "Install environment", no install-guide modal**.
@@ -348,7 +390,10 @@ so users cannot remove it from the extensions panel.
348
390
  escapable by the frame itself); in `loopback` mode the iframe is cross-origin and `sandbox` stays as real protection.
349
391
  - **Single instance across sessions**: one shared IDE per host; switching cwd requires a restart (the sidebar tab /
350
392
  floating window handles it and hints).
351
- - **Sidebar tab switching reloads**: DSH's right sidebar renders only the active tab's body, so switching away and back
352
- remounts the iframe (a full VS Code reload); keep the tab active or float it for long-running sessions.
393
+ - **Sidebar tab switching** (no longer reloads since 0.2.2): DSH's right sidebar renders only the active tab's body, and
394
+ a React unmount moves the iframe away; the plugin keeps it as a singleton resident surface and shuttles it between the
395
+ dock slot and a document-level park container with `Element.moveBefore()` (a state-preserving atomic move), so
396
+ switching tabs or collapsing the sidebar and back **does not reload** it. Browsers without `moveBefore` fall back to
397
+ the old behaviour (`appendChild` → full reload), reported as `degraded`; see "Why switching tabs no longer reloads".
353
398
  - **Remote access**: with `serve: dsh` the browser only needs to reach DSH itself (one port, protected exactly like `/api`);
354
399
  `serve: loopback` stays loopback-only with `auth: none`, and 0.2.0 no longer supports `auth: password`.
package/README.md CHANGED
@@ -32,12 +32,45 @@
32
32
  - 检测方式:`ctx.inject(['sidebarRightTabs','sidebarRight'], …)`——服务就绪才注册标签类型;
33
33
  服务缺失/注册失败则整段不生效,自动回退悬浮球(不按版本号硬判,也不影响插件激活)。
34
34
  - 侧栏标签内即 code-server 页面(iframe),跟随当前会话工作区;面板可折叠/分屏/浮动/全屏(由 DSH 右侧栏提供)。
35
- - **已知取舍**:DSH 只渲染「当前激活标签」的 body,切到别的标签再切回会重挂 iframe
36
- (code-server 整页重载,未保存的编辑缓冲区会丢);需要长驻会话时请把该标签**浮动**出来或保持激活。
35
+ - **IDE 常驻(0.2.2 起,默认开)**:切到别的标签/收起侧栏再回来**不再重载** code-server——
36
+ 未保存的编辑缓冲区、终端、调试会话都留在原处(见下方「为什么切标签不再重载」)
37
37
  - 设置卡片在侧栏模式下隐藏「保留输入框上方空间」(只对浮窗有意义);
38
38
  「窗口化打开(新标签页)」仍然生效(开启后各入口改为浏览器新标签页打开)。
39
39
  - `windowedOpen` 优先级最高:开启时入口按钮一律新开浏览器标签页。
40
40
 
41
+ ## 为什么切标签不再重载(IDE 常驻)
42
+
43
+ **过去的坑**:DSH 的右侧栏(ui-dockkit)`TabPanel` **只渲染当前激活标签的 body**
44
+ (`TabPanel.tsx:412` → `renderTab(active)`)——切到别的标签 = React 卸载该 body = iframe 被移出文档 =
45
+ 浏览上下文销毁,切回来就是一次完整的 VS Code 重载(未保存的缓冲区丢失)。把标签浮动成独立面板只是绕开它,
46
+ 并没有解决。
47
+
48
+ **现在的做法(客户端 `src/surface.js`,0.2.2)**:插件把 iframe **从 React 手里接管**,做成**单例常驻面**:
49
+
50
+ | 场景 | 动作 | 结果 |
51
+ |---|---|---|
52
+ | 标签激活 | `host.moveBefore(frame, null)` 移进当前可见的停靠位 | 状态保持型原子移动,**不重载** |
53
+ | 标签失活 / 收起侧栏 | 移回文档级 park 容器(离屏、保留最后停靠尺寸、`inert` + `aria-hidden`) | 面不销毁,后台继续跑 |
54
+ | 工作区 / 端口变化 | 显式设置 `src` | 这是唯一正常的"重载"入口 |
55
+
56
+ - **为什么是 `moveBefore`**:浏览器实测(Edge/Chromium 151)普通 `appendChild` 移动 iframe 会让内部计时器**归零**
57
+ (等价重载),而 `Element.moveBefore()`(Chromium ≥133)保持状态(计时器 1→2 连续)。
58
+ - **降级不静默**:`moveBefore` 缺失、或宿主已被 React 摘除而抛 `HierarchyRequestError: invalid hierarchy`
59
+ (passive effect cleanup 晚于 DOM 卸载)时,退回 `appendChild`——会重载一次,但**绝不丢帧**;
60
+ 状态里 `degraded`/`lastMoveError` 明示,界面据此提示"常驻不可用"。
61
+ - **重绘修复(实测坑)**:跨源 iframe 离屏停放较久后被移回,元素尺寸、命中测试、`visibility` 全部正常,
62
+ 但**画面不再重绘**(面板一片白,多切几次标签也不恢复)。`translateZ(0)`、`opacity` 微调都无效;
63
+ `display:none → 强制重排 → 还原`(同一个 JS 任务内)可唤醒,且 iframe 文档不重载、内部状态不变、无可见闪烁。
64
+ 因此每次「停放 → 停靠」都补一次 `nudgeRepaint()`(`surfaceSnapshot().nudgeCount` 计数,便于排障)。
65
+ - **后台预热**:配置 `keepResident`(默认 `true`)时,宿主在插件启动后就把面建好并停在停放区,
66
+ 首次点开标签无需冷启动等待;`preload` 不会把正在使用的面拽走。
67
+ - **排障句柄**:控制台可用 `window.__dshcsSurface`(`snapshot()` / `setParkStrategy('offscreen'|'behind')` /
68
+ `dock()` / `park()` / `nudge()` / `setNudgeEnabled(false)` / `destroy()`)。
69
+
70
+ **实测记录**(DSH web GUI,sidebar 标签间真实鼠标切换):切走 → `docked:false`、iframe 仍为同一节点、内部探针存活、
71
+ `degraded:false`;切回 → `docked:true`、`src` 不变、IDE 画面与编辑状态保持(无整页重载)。
72
+ 完整证据与探针脚本见 `docs/analysis-code-server-as-dsh-plugin.md`。
73
+
41
74
  ## 服务方式(serve)
42
75
 
43
76
  | 方式 | 说明 | 需要 |
@@ -53,6 +86,10 @@
53
86
  2. 转发端口(Ports 面板)的 **WebSocket** 无法用精确升级路由覆盖(端口号在路径里)→ 该功能在 `dsh` 模式下不可用;
54
87
  HTTP 转发端口正常;需要端口转发 WS 时请用 `loopback` 模式。
55
88
 
89
+ - `loopback` 模式下 upgrade 会做 **code-server 同款 Origin 校验**(0.2.1 起):带 `Origin` 时其 host 必须等于 `Host`
90
+ (含 `Forwarded: host=` / `X-Forwarded-Host` 的反代语义),否则回 `403`;缺 `Origin` 的非浏览器请求放行。
91
+ 没有这道检查时,本机任意浏览器页面都能对 `ws://127.0.0.1:<port>/stable-<commit>` 完成握手并驱动 IDE。
92
+
56
93
  ## 悬浮球 / 浮窗(仅旧版 DSH 回退路径)
57
94
 
58
95
  - **右下角悬浮球**(code-server 官方图标,输入框上方):点击**展开浮窗并亮起**(蓝色光环),再点击**收起并复原**;
@@ -136,9 +173,9 @@ pnpm run promote -- <version>
136
173
 
137
174
  ```powershell
138
175
  # 包内无 postinstall → 无需 pnpm approve-builds / allowBuilds;一条命令装完
139
- dsh plugin --profile web add dsh-code-server-app@0.2.0
176
+ dsh plugin --profile web add dsh-code-server-app@0.2.1
140
177
  # 本地 tarball 同理:
141
- dsh plugin --profile web add C:\Users\User\Desktop\dsh-code-server-app\dsh-code-server-app-0.2.0.tgz
178
+ dsh plugin --profile web add C:\Users\User\Desktop\dsh-code-server-app\dsh-code-server-app-0.2.1.tgz
142
179
  ```
143
180
 
144
181
  装完即用,**没有第二步、没有「安装环境」、不弹安装指引**。主包约 **110KB**(插件自身代码 + launcher),
@@ -355,8 +392,10 @@ desktop profile 由 `apps/desktop-host` 把 `/api/*` 交给同一个 `createShar
355
392
  - **`serve: dsh` 的 iframe 与 DSH 同源** → 该模式不挂 `sandbox`(同源 + `allow-same-origin` 可被 frame 自行摘除);
356
393
  `loopback` 模式跨源,`sandbox` 作为真防护保留。
357
394
  - **跨会话单实例**:host 级共享一份 IDE;切换 cwd 需重启实例(右侧栏标签/浮窗自动处理并提示)。
358
- - **侧栏标签切换重载**:DSH 右侧栏只渲染当前激活标签的 body,切走再切回会重挂 iframe(VS Code 整页重载);
359
- 长驻会话请保持该标签激活或将其浮动为独立面板。
395
+ - **侧栏标签切换**(0.2.2 起不再重载):DSH 右侧栏只渲染当前激活标签的 body,React 卸载会移走 iframe;
396
+ 插件把 iframe 收成单例常驻面,用 `Element.moveBefore()`(状态保持型原子移动)在停靠位与文档级停放区之间搬,
397
+ 切标签/收起侧栏再回来**不重载**。不支持 `moveBefore` 的浏览器退回旧行为(`appendChild` → 整页重载),
398
+ 状态里以 `degraded` 明示;详见下方「为什么切标签不再重载」。
360
399
  - **远程访问**:`serve: dsh` 下浏览器只需能到达 DSH 本身(单一端口,认证与 `/api` 同级);
361
400
  `serve: loopback` 默认仅回环、`auth: none`,跨机访问请改用 `serve: dsh`
362
401
  (0.2.0 起不再支持 `auth: password`)。