dsh-code-server-app 0.3.69 → 0.3.71
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 +169 -69
- package/README.md +130 -63
- package/cordis.patch.yml +1 -2
- package/lib/client.js +103 -273
- package/lib/index.js +46 -136
- package/lib/vendor.js +7 -44
- package/lib/vendored.json +1 -1
- package/package.json +7 -5
- package/scripts/vendor-repacks.mjs +18 -1
- package/vendor/VENDOR.json +5 -5
package/lib/client.js
CHANGED
|
@@ -16,20 +16,14 @@
|
|
|
16
16
|
* ④ 各段落原来的顶层标识符现在共享同一作用域 —— 新增顶层名字时先搜一遍别撞名
|
|
17
17
|
* (var/function 撞名是**静默覆盖**,不会报错)。
|
|
18
18
|
*
|
|
19
|
-
* DSH 支持范围(2026-
|
|
20
|
-
* ·
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* ① {旧座位, 旧通道} = rc 线;
|
|
27
|
-
* ② {新座位, 旧通道} = **0.1.6-alpha.2**(座位已搬走、`settingsScope` 还在);
|
|
28
|
-
* ③ {新座位, 新通道} = **≥ 0.1.7-alpha.1**(`settingsScope` 被删除,换成 `configForms`)。
|
|
29
|
-
* 静态 `inject` 因此只留普遍存在的 `['slots']`,两个通道都运行时探测 —— 写进 inject 会让整个
|
|
19
|
+
* DSH 支持范围(2026-10-01 收敛成**一代**;判据一律是能力是否被声明,不做版本号比较):
|
|
20
|
+
* · DSH ≥ 0.2.0-rc.2 —— web 与 desktop 现在都是这一代:`sidebarRightTabs`(标签类型 + `keepMounted`)、
|
|
21
|
+
* `useTabInfo()`、插件页座位 `plugins.bundle.config`、会话作用域标准 prop `sessionId`、通道 `configForms`。
|
|
22
|
+
* 旧代的兼容分支已随本轮清理删除(rc 线 `0.1.5-rc.x` / alpha 线 `0.1.6-alpha.2…0.1.7-rc.x`):
|
|
23
|
+
* 座位 `settings.plugin.item`、通道 `settingsScope`、会话快照上的 `current`、`recentWorkspaceId` 都不再支持;
|
|
24
|
+
* 旧版 DSH 用户钉旧插件版本(见 README「旧版 DSH」一段)。
|
|
25
|
+
* 静态 `inject` 因此只留普遍存在的 `['slots']`,`configForms` 一律运行时探测 —— 写进 inject 会让整个
|
|
30
26
|
* 条目在没有那个服务的 DSH 上永远 pending(表现是侧栏标签/设置/预热**一起消失**)。
|
|
31
|
-
* 更早的 alpha(0.1.5-alpha.x、0.1.6-alpha.1)形状与 rc 线相同 —— 代码天然走得通,但**不作为目标**,
|
|
32
|
-
* 也不为它们写任何专门分支。本文件里凡出现"哪一版"的说法,都按这两条线命名。
|
|
33
27
|
*
|
|
34
28
|
* 段落(按依赖顺序):地址语法 → 工作区解析 → 常驻面 → 全屏动作 → 认领类型(与 lib/claim-types.js
|
|
35
29
|
* 同源的一致性副本)→ 插件主体。
|
|
@@ -143,16 +137,13 @@ function resolveFilePath(parsed, cwd) {
|
|
|
143
137
|
// —— 解析"当前工作区目录"(决定 workbench 用哪个 `?folder=` 打开)
|
|
144
138
|
//
|
|
145
139
|
// 为什么单独成模块:这段逻辑的真实输入是 **DSH 客户端 store 的形状**,而它随 DSH 版本变过 ——
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
// 发 cwd,IDE 以**空工作区**启动(pid.json 里 cwd/launchCwd 双空)。所以这里按
|
|
154
|
-
// 「会话标准 prop → 旧版 current → 工作区表补位」的顺序取,并把判断留在纯函数里(可离线单测,
|
|
155
|
-
// 见 scripts/test-workspace-cwd.mjs;hook 一律留在组件内,本模块不碰 React)。
|
|
140
|
+
// 所以判断全留在这个纯函数里(可离线单测,见 scripts/test-workspace-cwd.mjs;hook 一律留在组件内,
|
|
141
|
+
// 本模块不碰 React)。
|
|
142
|
+
// 0.2 这一代:会话作用域插槽用标准 prop `sessionId` 告诉 body"你是哪个会话的",官方自己的右侧栏标签
|
|
143
|
+
// 就是这么读 cwd 的(ui-sidebar-files 的 FilesBody:`useSessions(s => s.byId[sessionId]?.cwd)`);
|
|
144
|
+
// 快照形状 `{ ids, byId, phase, subagentsByParent, jobsBySession }`,**选中的会话 id 不再挂在列表快照上**
|
|
145
|
+
// (`current` 那条兜底已随旧代清理删除 —— 它正是 0.3.46 那类"客户端不再发 cwd、IDE 以空工作区启动"的根源)。
|
|
146
|
+
// 取 cwd 的顺序:会话 cwd → 工作区表里含该会话的工作区 → 最近活跃会话的工作区 → 工作区表第一个。
|
|
156
147
|
//
|
|
157
148
|
// 全部输入都是"可能不存在"的:拿不到就返回 undefined,调用方不传 cwd(/start 不带 cwd = 保留当前
|
|
158
149
|
// 目录),**绝不猜一个目录** —— 猜错会把 IDE 开到别人的工作区里。
|
|
@@ -193,37 +184,21 @@ function pickWorkspaceCwd(input) {
|
|
|
193
184
|
var workspaces = opts.workspaces == null ? null : opts.workspaces
|
|
194
185
|
var sessionId = typeof opts.sessionId === 'string' && opts.sessionId !== '' ? opts.sessionId : undefined
|
|
195
186
|
|
|
196
|
-
// 1) 会话自带的 cwd(
|
|
187
|
+
// 1) 会话自带的 cwd(标准 prop `sessionId` → 会话行上的 cwd)
|
|
197
188
|
var fromSession = cwdOf(sessions, sessionId)
|
|
198
189
|
if (fromSession !== undefined) return fromSession
|
|
199
190
|
|
|
200
|
-
// 2)
|
|
201
|
-
var legacyCurrent = sessions == null ? undefined : sessions.current
|
|
202
|
-
var fromLegacy = cwdOf(sessions, legacyCurrent)
|
|
203
|
-
if (fromLegacy !== undefined) return fromLegacy
|
|
204
|
-
|
|
205
|
-
// 3) 工作区表:该会话所属的工作区
|
|
191
|
+
// 2) 工作区表:该会话所属的工作区
|
|
206
192
|
var fromWorkspace = pathHoldingSession(workspaces, sessionId)
|
|
207
193
|
if (fromWorkspace !== undefined) return fromWorkspace
|
|
208
|
-
var fromLegacyWorkspace = pathHoldingSession(workspaces, typeof legacyCurrent === 'string' ? legacyCurrent : undefined)
|
|
209
|
-
if (fromLegacyWorkspace !== undefined) return fromLegacyWorkspace
|
|
210
194
|
|
|
211
|
-
//
|
|
195
|
+
// 3) 根作用域(后台预热)没有 sessionId:退到"最近活跃的那个会话"所属的工作区。
|
|
212
196
|
// host 的会话列表按活跃度排序(list() 文档:ordered by activity),所以 ids[0] 就是它。
|
|
213
197
|
var recent = sessions != null && Array.isArray(sessions.ids) ? sessions.ids[0] : undefined
|
|
214
198
|
var fromRecent = pathHoldingSession(workspaces, typeof recent === 'string' ? recent : undefined)
|
|
215
199
|
if (fromRecent !== undefined) return fromRecent
|
|
216
200
|
|
|
217
|
-
//
|
|
218
|
-
if (workspaces != null && Array.isArray(workspaces.items) && workspaces.recentWorkspaceId !== undefined) {
|
|
219
|
-
for (var j = 0; j < workspaces.items.length; j += 1) {
|
|
220
|
-
var item = workspaces.items[j]
|
|
221
|
-
if (item == null || item.workspaceId !== workspaces.recentWorkspaceId) continue
|
|
222
|
-
if (typeof item.path === 'string' && item.path !== '') return item.path
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// 6) 最后兜底:工作区表的第一个(hook 拿不到选中态时的既有行为)
|
|
201
|
+
// 4) 最后兜底:工作区表的第一个(hook 拿不到选中态时的既有行为)
|
|
227
202
|
if (workspaces != null && Array.isArray(workspaces.items) && workspaces.items.length > 0) {
|
|
228
203
|
var first = workspaces.items[0]
|
|
229
204
|
if (first != null && typeof first.path === 'string' && first.path !== '') return first.path
|
|
@@ -254,12 +229,27 @@ const supportsMoveBefore = typeof Element !== 'undefined'
|
|
|
254
229
|
|
|
255
230
|
var PARK_OFFSCREEN_LEFT = -20000
|
|
256
231
|
|
|
232
|
+
/** 停放超过这个时长,回归时直接重新加载同一 URL(而不是只补一次重绘)。
|
|
233
|
+
* 依据:2026-10-01 的 desktop 实例 —— 面被停放到离屏后,里面那条到 IDE 的 WebSocket
|
|
234
|
+
* 22.8 小时零收包、1952 条消息无 ack,`nudgeRepaint()` 只能修合成、救不回半死连接。
|
|
235
|
+
* 10 分钟的取值:短于它的来回切换(最常见的"看一眼别的标签")仍然零重载,保持本插件的承诺。 */
|
|
236
|
+
var PARKED_RELOAD_MS = 10 * 60 * 1000
|
|
237
|
+
|
|
238
|
+
/** 桌面载体(Electron / DSH desktop):离屏 iframe 在这里会被更激进地挂起,
|
|
239
|
+
* 预热很可能换来一个"一出生就冻死"的会话 ⇒ 桌面下不预热到离屏。 */
|
|
240
|
+
function desktopCarrier() {
|
|
241
|
+
if (typeof globalThis !== 'undefined' && globalThis.dshDesktop != null) return true
|
|
242
|
+
return typeof navigator !== 'undefined' && /Electron/i.test(navigator.userAgent)
|
|
243
|
+
}
|
|
244
|
+
|
|
257
245
|
var surfaceState = {
|
|
258
246
|
park: null,
|
|
259
247
|
frame: null,
|
|
260
248
|
currentSrc: null,
|
|
261
249
|
sameOrigin: null,
|
|
262
250
|
owner: null,
|
|
251
|
+
/** 最后一次停放的时刻(ms):停放期间 iframe 可能被挂起,回归时据此决定要不要重新加载。 */
|
|
252
|
+
parkedAt: null,
|
|
263
253
|
/** 最后一次有效停靠尺寸:停放时沿用它,避免 VS Code 触发 0×0 重排。 */
|
|
264
254
|
lastRect: null,
|
|
265
255
|
/** 'offscreen'(默认,省 CPU)| 'behind'(停在原位、压在面板下,规避不可见 iframe 节流)。 */
|
|
@@ -283,6 +273,7 @@ function snapshot() {
|
|
|
283
273
|
parkStrategy: surfaceState.parkStrategy,
|
|
284
274
|
degraded: surfaceState.degraded,
|
|
285
275
|
preloaded: surfaceState.preloaded,
|
|
276
|
+
parkedAt: surfaceState.parkedAt,
|
|
286
277
|
supportsMoveBefore: supportsMoveBefore,
|
|
287
278
|
nudgeCount: surfaceState.nudgeCount,
|
|
288
279
|
lastNudgeAt: surfaceState.lastNudgeAt,
|
|
@@ -459,8 +450,17 @@ function dockInto(host, owner) {
|
|
|
459
450
|
markDocked()
|
|
460
451
|
surfaceState.park.setAttribute('aria-hidden', 'true')
|
|
461
452
|
moveInto(host)
|
|
462
|
-
|
|
463
|
-
|
|
453
|
+
var parkedFor = wasParked && typeof surfaceState.parkedAt === 'number' ? Date.now() - surfaceState.parkedAt : 0
|
|
454
|
+
surfaceState.parkedAt = null
|
|
455
|
+
if (parkedFor > PARKED_RELOAD_MS) {
|
|
456
|
+
// 停放太久:补重绘救不回被挂起的那条连接(见 PARKED_RELOAD_MS 的注释),只能重新导航同一 URL,
|
|
457
|
+
// 让 workbench 重新握手 + 从服务端恢复状态(IDE 进程本身没重启,缓冲区/终端都还在)。
|
|
458
|
+
console.warn('[code-server] 常驻面停放 ' + Math.round(parkedFor / 1000) + 's 后回归:重新加载以免沿用半死的连接')
|
|
459
|
+
if (surfaceState.currentSrc !== null) surfaceState.frame.src = surfaceState.currentSrc
|
|
460
|
+
} else if (wasParked) {
|
|
461
|
+
// 从停放回到停靠:补一次重绘唤醒(见文件头"另一个实测坑")。
|
|
462
|
+
nudgeRepaint()
|
|
463
|
+
}
|
|
464
464
|
notify()
|
|
465
465
|
}
|
|
466
466
|
|
|
@@ -472,6 +472,7 @@ function parkSurface(owner) {
|
|
|
472
472
|
applyParkStyle()
|
|
473
473
|
markParked()
|
|
474
474
|
surfaceState.park.setAttribute('aria-hidden', 'true')
|
|
475
|
+
surfaceState.parkedAt = Date.now()
|
|
475
476
|
moveInto(surfaceState.park)
|
|
476
477
|
notify()
|
|
477
478
|
}
|
|
@@ -777,7 +778,7 @@ function intersects(list, group) {
|
|
|
777
778
|
// 形态:window.__ModuleLoader__.load({id,factory}) 的经典脚本;factory 内静态 require('react')
|
|
778
779
|
// (DSH 冻结模块表)。0.3.58 起**不再构建** —— 这个文件就是源码,改它即改客户端。
|
|
779
780
|
//
|
|
780
|
-
// 数据通道:同源 fetch DSH Connection 的共享 /api(status / start / stop /
|
|
781
|
+
// 数据通道:同源 fetch DSH Connection 的共享 /api(status / start / stop / open-file / ui-mode;
|
|
781
782
|
// web 走 webServer 的 /api 前缀,desktop 走 IPC 帧管道 → 插件不依赖 webServer)。
|
|
782
783
|
//
|
|
783
784
|
// UI 载体只有右侧栏标签(需要 sidebarRightTabs / sidebarRight 服务;rc 线与 alpha 线都有):
|
|
@@ -857,6 +858,10 @@ let React = require('react')
|
|
|
857
858
|
if (status == null || typeof status.url !== 'string' || status.url === '') return null
|
|
858
859
|
var tag = status.pid != null ? String(status.pid) : (status.startedAt != null ? String(status.startedAt) : '0')
|
|
859
860
|
var url = status.url + '?s=' + encodeURIComponent(tag)
|
|
861
|
+
// 版本标记:插件版本或 VS Code 树变了 ⇒ URL 变 ⇒ 常驻 iframe 自动换页。
|
|
862
|
+
// 只靠 `s=<pid>` 挡不住"升级后 IDE 进程被 adopt(pid 不变)而页面还是旧的"这种情况
|
|
863
|
+
// (2026-10-01:面里跑着 0.3.68 + 4.138 的页面,endpoint.json 已是 4.139.1)。
|
|
864
|
+
if (typeof status.htmlTag === 'string' && status.htmlTag !== '') url += '&v=' + encodeURIComponent(status.htmlTag)
|
|
860
865
|
var dir = typeof cwd === 'string' && cwd !== '' ? cwd : (status != null && status.cwd != null ? status.cwd : null)
|
|
861
866
|
if (dir == null || dir === '') return url
|
|
862
867
|
var normalized = dir.replace(/\\/g, '/')
|
|
@@ -875,6 +880,8 @@ let React = require('react')
|
|
|
875
880
|
var pageUrl = props.pageUrl
|
|
876
881
|
var reloadTick = props.reloadTick
|
|
877
882
|
var owner = props.owner
|
|
883
|
+
/** 本 body 是否"活跃"(缺省 true = 旧形状:没有可见性信号时按老行为停靠)。 */
|
|
884
|
+
var active = props.active !== false
|
|
878
885
|
var hostRef = React.useRef(null)
|
|
879
886
|
var running = status != null && status.ok === true && status.running === true
|
|
880
887
|
var starting = status != null && status.status === 'starting'
|
|
@@ -891,8 +898,14 @@ let React = require('react')
|
|
|
891
898
|
return
|
|
892
899
|
}
|
|
893
900
|
ensureSurface({ src: pageUrl, sameOrigin: sameOrigin })
|
|
901
|
+
// 后台保留的 body 不许持有面(会抢空可见面板);它只把面**留在停放区**,
|
|
902
|
+
// 等自己变活跃时这一次 effect 会因 active 变化重跑并停靠。
|
|
903
|
+
if (active !== true) {
|
|
904
|
+
parkSurface(owner)
|
|
905
|
+
return
|
|
906
|
+
}
|
|
894
907
|
if (hostRef.current !== null) dockInto(hostRef.current, owner)
|
|
895
|
-
}, [running, pageUrl, sameOrigin, reloadTick, owner])
|
|
908
|
+
}, [running, pageUrl, sameOrigin, reloadTick, owner, active])
|
|
896
909
|
|
|
897
910
|
// 停放必须用 **layout** cleanup:React 在提交阶段先跑 layout effect 销毁、再摘除 DOM,
|
|
898
911
|
// 此时 iframe 仍连着文档 → moveBefore 成功(状态保持);若放到 passive cleanup,
|
|
@@ -948,6 +961,9 @@ let React = require('react')
|
|
|
948
961
|
var url = running ? buildPageUrl(status, cwd) : null
|
|
949
962
|
React.useEffect(function () {
|
|
950
963
|
if (!keep || !running || url === null) return
|
|
964
|
+
// 桌面载体上不用"离屏预热":Electron 会挂起离屏 iframe,预热等于提前造一个冻死的会话
|
|
965
|
+
// (2026-10-01 实测)。首次点开时 body 会正常加载并停靠,代价只是首次多等一次加载。
|
|
966
|
+
if (desktopCarrier()) return
|
|
951
967
|
preloadSurface({ src: url, sameOrigin: status != null && status.serve === 'dsh' })
|
|
952
968
|
}, [keep, running, url, status != null ? status.serve : null])
|
|
953
969
|
return null
|
|
@@ -980,9 +996,9 @@ let React = require('react')
|
|
|
980
996
|
// 服务始终不就绪(旧版 DSH)→ 判定 legacy:只留设置页提示(见 internalApply)。
|
|
981
997
|
var CS_KIND = 'code-server'
|
|
982
998
|
var CS_TAB_ID = 'dsh-code-server-app'
|
|
983
|
-
/** **profile 条目 id**(cordis.patch.yml 的 `- id: code-server`)
|
|
999
|
+
/** **profile 条目 id**(cordis.patch.yml 的 `- id: code-server`)。它同时是**设置命名空间**:
|
|
984
1000
|
* `ctx.configForms.get(ENTRY_ID)` 按条目 id 取表单,`whileServed([ENTRY_ID])` 按它判断
|
|
985
|
-
* "宿主是否正在服务这个条目的配置"。必须与宿主 lib/index.js
|
|
1001
|
+
* "宿主是否正在服务这个条目的配置"。必须与宿主 `lib/index.js` 导出的 `name` 以及行 id 三者一致。 */
|
|
986
1002
|
var CS_ENTRY_ID = 'code-server'
|
|
987
1003
|
/** 本插件的**包名**,同时是客户端 bundle 的 loader id(banner 里 `load({id})` 那一个)。
|
|
988
1004
|
* 插件页按包名派发配置区(`renderSlot('plugins.bundle.config', …, { entryKey: pkg.name })`),
|
|
@@ -1111,6 +1127,15 @@ let React = require('react')
|
|
|
1111
1127
|
var rootRef = React.useRef(null)
|
|
1112
1128
|
var forcedFullscreenRef = React.useRef(false)
|
|
1113
1129
|
var visible = info.tab != null && info.tab.visible === true
|
|
1130
|
+
// **停靠仲裁(2026-10-01,回归修复)**:宿主会**同时保留多个** body —— 0.2 的 `keepMounted`
|
|
1131
|
+
// 让每个打开过本标签的 Session 的正文都留在树里(后台的也在)。而 iframe 只有一个:
|
|
1132
|
+
// 谁最后 `dockInto` 谁赢,于是后台那个保留 body 一重渲染就把可见面板里的面**抢空**(现象:
|
|
1133
|
+
// 切换标签/工作区回来一片空)。所以只有"活跃"的 body 才许持有面:
|
|
1134
|
+
// · 优先用 0.2 的 `useTabInfo().active`(官方 TabSlot 给的"本标签是否活跃");
|
|
1135
|
+
// · 退到记录上的 `visible`(老形状);
|
|
1136
|
+
// · 两个信号都没有(更老的壳层)⇒ 按"活跃"处理,保持以前的行为不变。
|
|
1137
|
+
var hasActiveSignal = info.active !== undefined || (info.tab != null && info.tab.visible !== undefined)
|
|
1138
|
+
var active = hasActiveSignal ? (info.active === true || visible === true) : true
|
|
1114
1139
|
var fullscreen = info.sidebar != null && info.sidebar.fullscreen === true
|
|
1115
1140
|
// 单标签页(0.3.57):登记自己的"座位"(能关掉自己的那个函数 + 所在窗格),
|
|
1116
1141
|
// 并在**首次可见**时把同窗格里的旧 code-server 标签页收掉 —— 见 closeSiblingTabs 的注释。
|
|
@@ -1191,6 +1216,7 @@ let React = require('react')
|
|
|
1191
1216
|
return React.createElement('div', { className: 'dshcs-tabroot', 'data-code-server-tab': 'body', ref: rootRef },
|
|
1192
1217
|
React.createElement(CodeServerSurface, {
|
|
1193
1218
|
status: status, pageUrl: buildPageUrl(status, cwd), reloadTick: tick, owner: 'tab:' + tab.id,
|
|
1219
|
+
active: active,
|
|
1194
1220
|
})
|
|
1195
1221
|
)
|
|
1196
1222
|
}
|
|
@@ -1231,6 +1257,12 @@ let React = require('react')
|
|
|
1231
1257
|
// 第三方 openResource(fileAddress) 都会落到本 tab。含 `:` 的 pattern 按整址 glob 匹配。
|
|
1232
1258
|
patterns: ['dsh-resource://file/**'],
|
|
1233
1259
|
canOpen: function (a) { return claimsAddress(parseFileAddress(a), claimPolicy()) },
|
|
1260
|
+
// **宿主级保活(DSH 0.2 起的契约)**:声明后,已访问过的正文在**切 tab / 切 Session / 收起 /
|
|
1261
|
+
// 停靠与浮窗切换**期间由右侧栏自己保留(`TabSlot` 看到 `keepMounted === true` 就 `retainTab`),
|
|
1262
|
+
// 不必再靠本插件把 iframe 搬进离屏 park 来硬扛 —— 那正是 2026-10-01 那次故障的来源:
|
|
1263
|
+
// 离屏 iframe 被挂起,里面到 IDE 的 WebSocket 静默半开(22.8 小时零收包、1952 条消息无 ack),
|
|
1264
|
+
// 界面"活着但不会动"。旧壳层不认识这个字段,直接忽略 ⇒ 无需能力探测。
|
|
1265
|
+
keepMounted: true,
|
|
1234
1266
|
// 文件 tab 用文件名当 chip 文本;页面 tab(openTab/openResource 指定 kind)仍是产品名
|
|
1235
1267
|
title: function (a) {
|
|
1236
1268
|
if (isPageAddress(a) || a === undefined || a === null || a === '') return 'Code Server'
|
|
@@ -1268,26 +1300,18 @@ let React = require('react')
|
|
|
1268
1300
|
}
|
|
1269
1301
|
|
|
1270
1302
|
// ---------- 设置(参照 auto-open-web 的自绘卡片模式) ----------
|
|
1271
|
-
//
|
|
1272
|
-
//
|
|
1273
|
-
//
|
|
1274
|
-
// · `plugins.bundle.config`(alpha 线 ≥ 0.1.6-alpha.2,含最新 0.1.7-alpha.1):插件页按**包名**派发的配置区,
|
|
1275
|
-
// 页面自带标题/面包屑,我们只出表单 + 保存控件(seat='bundle-config');
|
|
1276
|
-
// · `settings.plugin.item`(rc 线 ≤ 0.1.5-rc.3,最新 rc 仍在用):设置页里自绘的可折叠卡片(alpha 线已退役)。
|
|
1303
|
+
// 座位(声明驱动:`slots.inject` 只在插槽真被声明时才回调)只有 `plugins.bundle.config` 一条:
|
|
1304
|
+
// 插件页按**包名**派发配置区,页面自带标题/面包屑,我们只出表单 + 保存控件(seat='bundle-config')。
|
|
1305
|
+
// 旧座位 `settings.plugin.item`(设置页里的自绘卡片)已随旧代清理删除。
|
|
1277
1306
|
//
|
|
1278
|
-
//
|
|
1279
|
-
//
|
|
1280
|
-
//
|
|
1281
|
-
//
|
|
1282
|
-
//
|
|
1283
|
-
// 注册还要经 `whileServed([ENTRY_ID], …)` 门禁(宿主只有在服务该条目配置时才让我们注册页面)。
|
|
1284
|
-
// · 旧通道 `ctx.settingsScope.bind({ namespace })`(rc 0.1.5-rc.x;**也含 0.1.6-alpha.2** ——
|
|
1285
|
-
// 那一版座位已经搬到插件页、数据面却还是旧的):卡片直接读写 settings 命名空间。
|
|
1307
|
+
// 数据通道(能力探测)只有 `ctx.configForms.get(ENTRY_ID)` 一条:配置就是**插件条目自己的 Config**
|
|
1308
|
+
// (宿主半的字段 `.volatile()`)。表单模型:`getSnapshot()` → { status, writable, revision, value, base, user }、
|
|
1309
|
+
// `subscribe(cb)`、**唯一写路径** `mutate(ops, revision)`(原子、带 revision 栅栏)、`dispose()`;
|
|
1310
|
+
// 注册还要经 `whileServed([ENTRY_ID], …)` 门禁(宿主只有在服务该条目配置时才让我们注册页面)。
|
|
1311
|
+
// 旧通道 `ctx.settingsScope.bind({ namespace })` 同样已删除(旧代专属)。
|
|
1286
1312
|
//
|
|
1287
|
-
//
|
|
1288
|
-
//
|
|
1289
|
-
// 这也是为什么 `inject` 只留**必然存在**的 `slots`,两个通道都运行时探测:
|
|
1290
|
-
// 把 `settingsScope` 写进 inject 会让整个客户端条目在 0.1.7-alpha.1 上永远 pending
|
|
1313
|
+
// 这也是为什么 `inject` 只留**必然存在**的 `slots`,`configForms` 一律运行时探测:
|
|
1314
|
+
// 把它写进 inject 会让整个客户端条目在没有该服务的宿主上永远 pending
|
|
1291
1315
|
// (表现是右侧栏标签、设置卡、常驻预热**全都不存在**,而且只有一条 "1 entry did not activate")。
|
|
1292
1316
|
var CARD_CSS =
|
|
1293
1317
|
'.dshcs-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}' +
|
|
@@ -1685,7 +1709,7 @@ let React = require('react')
|
|
|
1685
1709
|
var CS_LEGACY_LEAD = '本插件自 0.2.3 起不再兼容旧版 DSH。'
|
|
1686
1710
|
var CS_LEGACY_BODY = '未检测到右侧栏插件服务 sidebarRightTabs / sidebarRight,因此插件不提供任何入口'
|
|
1687
1711
|
+ '(旧版的悬浮球与浮动窗口已移除),也不会后台启动 IDE。'
|
|
1688
|
-
+ '\n升级 DSH 到
|
|
1712
|
+
+ '\n升级 DSH 到 0.2.0-rc.2 及以上后,Code Server 会出现在右侧栏标签里,'
|
|
1689
1713
|
+ '本页同时显示完整设置项;升级后无需重装本插件,刷新页面即可。'
|
|
1690
1714
|
|
|
1691
1715
|
/** 保存/恢复成功后,把 host 最新 status 推给模块共享 store → 常驻预热等立即按新设置生效。 */
|
|
@@ -1953,177 +1977,6 @@ let React = require('react')
|
|
|
1953
1977
|
return csConfigPage(view, handlers, csSettingsFields(view, handlers, { surfaceHint: csSurfaceHintText(surfaceState) }))
|
|
1954
1978
|
}
|
|
1955
1979
|
|
|
1956
|
-
/** 设置卡片页面:useState+useEffect 订阅 settingsScope(不投机 useSyncExternalStore
|
|
1957
|
-
* 对 snapshot 引用稳定性的要求;不建中间 store)。 */
|
|
1958
|
-
function csSettingsPage(props) {
|
|
1959
|
-
try {
|
|
1960
|
-
var scope = props.scope
|
|
1961
|
-
var [snapshot, setSnapshot] = React.useState(function () { return scope !== undefined ? scope.getSnapshot() : null })
|
|
1962
|
-
var [draft, setDraft] = React.useState(null) // null | { keepResident, fileOpenScope }(未保存草稿)
|
|
1963
|
-
var [saving, setSaving] = React.useState(false)
|
|
1964
|
-
var [failed, setFailed] = React.useState(false)
|
|
1965
|
-
// 共享 store:读取当前 UI 载体状态(右侧栏是否就绪 / 是否旧版 DSH),必须放在所有提前 return 之前
|
|
1966
|
-
var liveStore = useStore()
|
|
1967
|
-
// 常驻面实时状态(停靠/停放/降级):卡片里显示,便于判断"切标签是否还会重载"
|
|
1968
|
-
var [surfaceState, setSurfaceState] = React.useState(surfaceSnapshot())
|
|
1969
|
-
React.useEffect(function () {
|
|
1970
|
-
return subscribeSurface(function (snap) { setSurfaceState(snap) })
|
|
1971
|
-
}, [])
|
|
1972
|
-
React.useEffect(function () {
|
|
1973
|
-
if (scope === undefined || typeof scope.subscribe !== 'function') return
|
|
1974
|
-
function onUpdate() {
|
|
1975
|
-
try { setSnapshot(scope.getSnapshot()) } catch (e) { /* ignore */ }
|
|
1976
|
-
}
|
|
1977
|
-
var dispose = scope.subscribe(onUpdate)
|
|
1978
|
-
return function () { if (typeof dispose === 'function') dispose() }
|
|
1979
|
-
}, [scope])
|
|
1980
|
-
if (scope === undefined) {
|
|
1981
|
-
console.error('[code-server] card scope missing')
|
|
1982
|
-
return null
|
|
1983
|
-
}
|
|
1984
|
-
if (snapshot === null || snapshot === undefined || snapshot.status !== 'ready') {
|
|
1985
|
-
// 尚未就绪(连接/镜像加载中):静默留空,官方插槽边界接管;不显示占位
|
|
1986
|
-
if (snapshot !== null && snapshot !== undefined && snapshot.status === 'unavailable') {
|
|
1987
|
-
console.warn('[code-server] settings scope unavailable:', snapshot.status)
|
|
1988
|
-
}
|
|
1989
|
-
return null
|
|
1990
|
-
}
|
|
1991
|
-
var value = snapshot.value !== undefined && snapshot.value !== null ? snapshot.value : {}
|
|
1992
|
-
var user = snapshot.user
|
|
1993
|
-
// 当前生效值(每个字段的兜底与宿主 schema 默认值同源,见 CS_FIELDS.read)。
|
|
1994
|
-
var loaded = {}
|
|
1995
|
-
CS_FIELDS.forEach(function (spec) { loaded[spec.field] = spec.read(value) })
|
|
1996
|
-
// ---- 旧版 DSH(无右侧栏服务):本页是唯一的提示出口,不显示任何设置项 ----
|
|
1997
|
-
if (liveStore != null && liveStore.sidebarUi === 'legacy') {
|
|
1998
|
-
return csLegacyNotice('card')
|
|
1999
|
-
}
|
|
2000
|
-
// 归一化视图:新通道那份由 configForms 的投影直接给出(形状与此完全相同)⇒ 表单主体只认这一层。
|
|
2001
|
-
var view = {
|
|
2002
|
-
available: true,
|
|
2003
|
-
writable: snapshot.writable === true,
|
|
2004
|
-
values: {},
|
|
2005
|
-
overridden: {},
|
|
2006
|
-
}
|
|
2007
|
-
CS_FIELDS.forEach(function (spec) {
|
|
2008
|
-
view.values[spec.field] = draft !== null ? draft[spec.field] : loaded[spec.field]
|
|
2009
|
-
view.overridden[spec.field] = csOverridden(user, spec.field)
|
|
2010
|
-
})
|
|
2011
|
-
view.dirty = draft !== null && CS_FIELDS.some(function (spec) {
|
|
2012
|
-
return !spec.same(spec.parse(spec.display(view.values[spec.field])), loaded[spec.field])
|
|
2013
|
-
})
|
|
2014
|
-
view.invalid = false
|
|
2015
|
-
view.saving = saving
|
|
2016
|
-
view.failed = failed
|
|
2017
|
-
var saveDisabled = view.dirty !== true || saving
|
|
2018
|
-
var state = {
|
|
2019
|
-
available: true,
|
|
2020
|
-
writable: view.writable,
|
|
2021
|
-
dirty: view.dirty,
|
|
2022
|
-
invalid: view.invalid,
|
|
2023
|
-
saving: view.saving,
|
|
2024
|
-
failed: view.failed,
|
|
2025
|
-
}
|
|
2026
|
-
// 保存/恢复成功后,把 host 最新 status 推给模块共享 store → 常驻预热等立即按新设置生效。
|
|
2027
|
-
async function doSave() {
|
|
2028
|
-
if (view.dirty && !saving) {
|
|
2029
|
-
setSaving(true); setFailed(false)
|
|
2030
|
-
try {
|
|
2031
|
-
var d = draft !== null ? draft : loaded
|
|
2032
|
-
await props.scope.set('keepResident', d.keepResident === true)
|
|
2033
|
-
await props.scope.set('claimExtensions', normalizeClaimExtensions(d.claimExtensions))
|
|
2034
|
-
await props.scope.set('fullscreenOnOpen', d.fullscreenOnOpen === true)
|
|
2035
|
-
await props.scope.set('fim', d.fim === true)
|
|
2036
|
-
await props.scope.set('fimDebounceMs', normalizeFimDebounce(d.fimDebounceMs))
|
|
2037
|
-
await props.scope.set('fimMultiline', d.fimMultiline === true)
|
|
2038
|
-
await props.scope.set('fimDisableGlobs', typeof d.fimDisableGlobs === 'string' ? d.fimDisableGlobs : '')
|
|
2039
|
-
setDraft(null)
|
|
2040
|
-
csSyncStatusToStore()
|
|
2041
|
-
} catch (e) {
|
|
2042
|
-
setFailed(true)
|
|
2043
|
-
}
|
|
2044
|
-
setSaving(false)
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
async function doReset() {
|
|
2048
|
-
if (saving || snapshot.writable !== true) return
|
|
2049
|
-
setSaving(true); setFailed(false)
|
|
2050
|
-
try {
|
|
2051
|
-
// unset 清除 user 覆盖层 → 回到 base 默认值;成功后镜像同步
|
|
2052
|
-
if (typeof props.scope.unset === 'function') {
|
|
2053
|
-
await props.scope.unset('keepResident')
|
|
2054
|
-
await props.scope.unset('claimExtensions')
|
|
2055
|
-
await props.scope.unset('fullscreenOnOpen')
|
|
2056
|
-
await props.scope.unset('fim')
|
|
2057
|
-
await props.scope.unset('fimDebounceMs')
|
|
2058
|
-
await props.scope.unset('fimMultiline')
|
|
2059
|
-
await props.scope.unset('fimDisableGlobs')
|
|
2060
|
-
} else {
|
|
2061
|
-
await props.scope.set('keepResident', value.keepResident === undefined || value.keepResident === null ? true : value.keepResident)
|
|
2062
|
-
await props.scope.set('claimExtensions', typeof value.claimExtensions === 'string' ? normalizeClaimExtensions(value.claimExtensions) : DEFAULT_CLAIM_EXTENSIONS)
|
|
2063
|
-
await props.scope.set('fullscreenOnOpen', value.fullscreenOnOpen === undefined || value.fullscreenOnOpen === null ? true : value.fullscreenOnOpen)
|
|
2064
|
-
await props.scope.set('fim', value.fim === true)
|
|
2065
|
-
await props.scope.set('fimDebounceMs', normalizeFimDebounce(value.fimDebounceMs))
|
|
2066
|
-
await props.scope.set('fimMultiline', value.fimMultiline !== false)
|
|
2067
|
-
await props.scope.set('fimDisableGlobs', typeof value.fimDisableGlobs === 'string' ? value.fimDisableGlobs : '')
|
|
2068
|
-
}
|
|
2069
|
-
setDraft(null)
|
|
2070
|
-
csSyncStatusToStore()
|
|
2071
|
-
} catch (e) {
|
|
2072
|
-
setFailed(true)
|
|
2073
|
-
}
|
|
2074
|
-
setSaving(false)
|
|
2075
|
-
}
|
|
2076
|
-
/** 旧通道的处理器:草稿仍在组件里(旧通道没有 mutate,只能逐字段 set/unset)。 */
|
|
2077
|
-
var handlers = {
|
|
2078
|
-
onEdit: function (field, next) {
|
|
2079
|
-
setDraft(function (prev) {
|
|
2080
|
-
var base = Object.assign({}, prev !== null ? prev : loaded)
|
|
2081
|
-
base[field] = next
|
|
2082
|
-
return base
|
|
2083
|
-
})
|
|
2084
|
-
setFailed(false)
|
|
2085
|
-
},
|
|
2086
|
-
// 旧通道的"恢复默认"是**整份** unset(0.3.x 起的行为);新通道按官方契约是逐字段 unset。
|
|
2087
|
-
onResetField: function () { doReset() },
|
|
2088
|
-
onSave: function () { doSave() },
|
|
2089
|
-
onDiscard: function () { setDraft(null); setFailed(false) },
|
|
2090
|
-
}
|
|
2091
|
-
var fields = csSettingsFields(view, handlers, { surfaceHint: csSurfaceHintText(surfaceState) })
|
|
2092
|
-
// ---- 插件页的配置区(alpha 线 ≥ 0.1.6-alpha.2):页面自己画标题/图标/面包屑,
|
|
2093
|
-
// 把 `plugins.bundle.config`(按包名为键)渲染在描述与行之间,只问 `view: 'page'`;
|
|
2094
|
-
// 所以这里不再套可折叠卡片(标题会与页面标题重复),只出表单 + 自己的保存控件。
|
|
2095
|
-
// 注意:0.1.6-alpha.2 的**座位**已是这一条,而**数据通道**还是旧的 settingsScope ⇒
|
|
2096
|
-
// 同一个 seat 必须两种通道都答得起(新通道那份见 ConfigFormsEntry)。
|
|
2097
|
-
if (props.seat === 'bundle-config') {
|
|
2098
|
-
return csConfigPage(view, handlers, fields)
|
|
2099
|
-
}
|
|
2100
|
-
return React.createElement(csCard, {
|
|
2101
|
-
title: CS_LEGACY_TITLE,
|
|
2102
|
-
description: '入口:右侧栏标签(需 DSH 带 sidebarRight/sidebarRightTabs,即 rc 线或 0.1.6-alpha.2 起的 alpha 线);认领类型决定哪些文件交给 Code Server 打开',
|
|
2103
|
-
state: state,
|
|
2104
|
-
unsavedLabel: CS_UNSAVED_LABEL, readOnlyLabel: CS_READONLY_LABEL,
|
|
2105
|
-
saveFailedLabel: CS_SAVE_FAILED_LABEL,
|
|
2106
|
-
discardLabel: CS_DISCARD_LABEL, saveLabel: CS_SAVE_LABEL, savingLabel: CS_SAVING_LABEL,
|
|
2107
|
-
onSave: handlers.onSave, onDiscard: handlers.onDiscard,
|
|
2108
|
-
}, ...fields)
|
|
2109
|
-
} catch (e) {
|
|
2110
|
-
// 渲染异常:记日志不打断;官方插槽对异常有边界,卡片留空即可
|
|
2111
|
-
console.error('[code-server] card render error:', e !== null && e !== undefined && e.message !== undefined ? e.message : String(e))
|
|
2112
|
-
return null
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
/** 插件页配置区的入口组件(**旧通道**;0.3.50 起)。
|
|
2117
|
-
* 用它的场合:座位是 `plugins.bundle.config` 但数据面还是 `settingsScope` —— 也就是
|
|
2118
|
-
* rc 线之外**还包括 0.1.6-alpha.2**(那一版座位已搬、通道未换)。新通道见 ConfigFormsEntry。
|
|
2119
|
-
* 上游契约:同一个 entry 会被要两种视图 —— `summary`(标题下的一句话)与 `page`(表单,
|
|
2120
|
-
* 自带保存控件)。插件页对 `plugins.bundle.config` 只用 `page`,但两种都答得起才算合格。
|
|
2121
|
-
* 本组件自身不调 hook(两个分支都不调),真正的设置表单在 csSettingsPage 里(独立组件边界)。 */
|
|
2122
|
-
function BundleConfigEntry(props) {
|
|
2123
|
-
if (props != null && props.view === 'summary') return CS_CONFIG_SUMMARY
|
|
2124
|
-
return React.createElement(csSettingsPage, Object.assign({}, props, { seat: 'bundle-config' }))
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
1980
|
// ---------- 「问 DSH」对话框(0.3.24;0.3.59 起面板本体就是这份源码)----------
|
|
2128
1981
|
// 对话**不在编辑器的侧栏/tab 里**,而是作为一个浮在 DSH 页面上的对话框(用户的原话:
|
|
2129
1982
|
// "对话不要用侧边栏,还是改成对话框形式")。这里就是那个对话框:**外壳**(标题栏 / 拖动 / 关闭)
|
|
@@ -3202,14 +3055,14 @@ let React = require('react')
|
|
|
3202
3055
|
setState({ status: s })
|
|
3203
3056
|
}).catch(function () { /* 首次失败由后续轮询补救 */ })
|
|
3204
3057
|
|
|
3205
|
-
// ----
|
|
3206
|
-
//
|
|
3207
|
-
//
|
|
3208
|
-
//
|
|
3058
|
+
// ---- 设置:单一座位 `plugins.bundle.config`(DSH 0.2 起;旧座位 `settings.plugin.item` 与旧通道
|
|
3059
|
+
// `settingsScope` 已随老版本线一并删除 —— 两套 UI(web/desktop)现在都是 0.2 这一代) ----
|
|
3060
|
+
// 通道用能力探测:`configForms` 在就用它;没有(宿主没服务本条目)就只打日志、不注册页面。
|
|
3061
|
+
// 探测只经 `ctx.get`:`configForms` **不能**写进静态 inject(会让没有这个服务的宿主上整条目永远 pending),
|
|
3062
|
+
// 而未声明服务的属性访问在 DSH 里会抛 —— 所以这里绝不用 ctx.configForms。
|
|
3209
3063
|
var formsSvc = ctx.get('configForms')
|
|
3210
|
-
var scope = null
|
|
3211
3064
|
if (formsSvc !== undefined && formsSvc !== null && typeof formsSvc.get === 'function') {
|
|
3212
|
-
//
|
|
3065
|
+
// 配置就是本条目自己的 volatile Config。
|
|
3213
3066
|
var form = formsSvc.get(CS_ENTRY_ID)
|
|
3214
3067
|
var controller = csConfigFormController(form)
|
|
3215
3068
|
var face = null
|
|
@@ -3217,7 +3070,7 @@ let React = require('react')
|
|
|
3217
3070
|
if (store !== null) {
|
|
3218
3071
|
face = Object.assign({ hooks: { codeServerForm: store } }, controller.actions())
|
|
3219
3072
|
} else {
|
|
3220
|
-
console.
|
|
3073
|
+
console.error('[code-server] 快照 store 不可用:配置区不注册(表单无法读写)')
|
|
3221
3074
|
}
|
|
3222
3075
|
// 表单订阅属于本 fiber:插件卸载/热重载时释放,避免监听泄漏到服务缓存的表单上。
|
|
3223
3076
|
ctx.effect(function () { return function () { controller.dispose() } }, 'code-server: config form subscription')
|
|
@@ -3237,30 +3090,7 @@ let React = require('react')
|
|
|
3237
3090
|
console.log('[code-server] client config page registered: plugins.bundle.config[' + PKG_NAME + '] via configForms')
|
|
3238
3091
|
}
|
|
3239
3092
|
} else {
|
|
3240
|
-
|
|
3241
|
-
var settingsScope = ctx.get('settingsScope')
|
|
3242
|
-
if (settingsScope !== undefined && settingsScope !== null && typeof settingsScope.bind === 'function') {
|
|
3243
|
-
scope = settingsScope.bind({ namespace: CS_ENTRY_ID })
|
|
3244
|
-
}
|
|
3245
|
-
if (scope === null) {
|
|
3246
|
-
console.error('[code-server] 没有可用的配置数据通道(configForms / settingsScope 都不可见):配置区不注册')
|
|
3247
|
-
}
|
|
3248
|
-
if (scope !== null) {
|
|
3249
|
-
// ②-a 插件页的配置区(alpha 线 ≥ 0.1.6-alpha.2):`settings.plugin.item` 退役,上游把插件配置搬到
|
|
3250
|
-
// 插件页 —— 键是**包名**(页面按 `pkg.name` 派发),只问 `view: 'page'`。插件页只在
|
|
3251
|
-
// `ledger.bundles.has(包名)` 时才渲染那一块,所以键写错就等于"设置又不见了"。
|
|
3252
|
-
// 0.1.6-alpha.2 用的就是这一条(座位新、通道旧)。
|
|
3253
|
-
slots.inject('plugins.bundle.config', () => slots.register(
|
|
3254
|
-
{ name: 'plugins.bundle.config', key: PKG_NAME, inject: function () { return { scope: scope } } },
|
|
3255
|
-
BundleConfigEntry
|
|
3256
|
-
))
|
|
3257
|
-
// ②-b 设置页里的插件卡片(rc 线 ≤ 0.1.5-rc.3,最新 rc 仍在用;也是没有右侧栏服务的旧 DSH 里唯一的提示出口)。
|
|
3258
|
-
// `slots.inject` 只在插槽真被声明时才回调 ⇒ 新 DSH 上这一条自然不生效。
|
|
3259
|
-
slots.inject('settings.plugin.item', () => slots.register(
|
|
3260
|
-
{ name: 'settings.plugin.item', key: CS_ENTRY_ID, label: 'Code Server', inject: function () { return { scope: scope } } },
|
|
3261
|
-
csSettingsPage
|
|
3262
|
-
))
|
|
3263
|
-
}
|
|
3093
|
+
console.error('[code-server] configForms 不可见(宿主没在服务本条目配置):配置区不注册')
|
|
3264
3094
|
}
|
|
3265
3095
|
|
|
3266
3096
|
// ---- 能力探测:只有带右侧栏服务(sidebarRightTabs/sidebarRight;rc 线与 alpha 线都有)才注册可用 UI;探测不到就只留设置页提示 ----
|
|
@@ -3354,9 +3184,9 @@ let React = require('react')
|
|
|
3354
3184
|
}, LEGACY_REPORT_MS)
|
|
3355
3185
|
}
|
|
3356
3186
|
|
|
3357
|
-
// 静态 inject
|
|
3358
|
-
//
|
|
3359
|
-
//
|
|
3187
|
+
// 静态 inject 只列**必然存在**的服务。`configForms`(设置通道)是**运行时探测**的:
|
|
3188
|
+
// 把它写进 inject,客户端条目会在没有那个服务的宿主上永远 pending ——
|
|
3189
|
+
// 表现是右侧栏标签、设置卡、常驻预热**一起消失**,日志里只有一句 "1 entry did not activate"。
|
|
3360
3190
|
const inject = ['slots']
|
|
3361
3191
|
const name = 'code-server'
|
|
3362
3192
|
|