dsh-code-server-app 0.3.63 → 0.3.66

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/lib/client.js CHANGED
@@ -8,12 +8,29 @@
8
8
  *
9
9
  * 硬性约束(改动前先读;有 scripts/test-client-entry.mjs 守着):
10
10
  * ① 顶层不许出现 import / export / await —— 经典脚本里它们是语法错误;
11
- * ② require(...) 的实参只允许 'react' 与 'react/jsx-runtime'(DSH 冻结模块表);
11
+ * ② require(...) 的实参只允许壳层冻结模块表里的名字('react' / 'react/jsx-runtime' /
12
+ * 'react-dom/client' / '@deepseek-ai/dsh-client-ui-primitives' / '@deepseek-ai/dsh-client-store';
13
+ * test-client-entry.mjs 的白名单与之一致);
12
14
  * ③ 必须保留 window.__ModuleLoader__.load({id, factory}) 这层包装与 module.exports 的
13
15
  * { apply, inject, name } 形状(app/inject/name 的定义在文件末尾的"插件主体"段);
14
16
  * ④ 各段落原来的顶层标识符现在共享同一作用域 —— 新增顶层名字时先搜一遍别撞名
15
17
  * (var/function 撞名是**静默覆盖**,不会报错)。
16
18
  *
19
+ * DSH 支持范围(2026-09-22 收敛;**判据一律是能力是否被声明**,不做版本号比较):
20
+ * · rc 线 0.1.5-rc.x(最新 0.1.5-rc.3 = npm latest/next)
21
+ * 座位 `settings.plugin.item` + 会话列表快照上的 `current` + 数据通道 `settingsScope`
22
+ * · alpha 线 ≥ 0.1.6-alpha.2(最新 0.1.7-alpha.1)
23
+ * 座位 `plugins.bundle.config` + 会话作用域标准 prop `sessionId`
24
+ *
25
+ * **座位与数据通道的分界点不在同一版**,所以代码里是两条独立的轴(详见"设置"段的说明):
26
+ * ① {旧座位, 旧通道} = rc 线;
27
+ * ② {新座位, 旧通道} = **0.1.6-alpha.2**(座位已搬走、`settingsScope` 还在);
28
+ * ③ {新座位, 新通道} = **≥ 0.1.7-alpha.1**(`settingsScope` 被删除,换成 `configForms`)。
29
+ * 静态 `inject` 因此只留普遍存在的 `['slots']`,两个通道都运行时探测 —— 写进 inject 会让整个
30
+ * 条目在没有那个服务的 DSH 上永远 pending(表现是侧栏标签/设置/预热**一起消失**)。
31
+ * 更早的 alpha(0.1.5-alpha.x、0.1.6-alpha.1)形状与 rc 线相同 —— 代码天然走得通,但**不作为目标**,
32
+ * 也不为它们写任何专门分支。本文件里凡出现"哪一版"的说法,都按这两条线命名。
33
+ *
17
34
  * 段落(按依赖顺序):地址语法 → 工作区解析 → 常驻面 → 全屏动作 → 认领类型(与 lib/claim-types.js
18
35
  * 同源的一致性副本)→ 插件主体。
19
36
  */
@@ -126,9 +143,9 @@ function resolveFilePath(parsed, cwd) {
126
143
  // —— 解析"当前工作区目录"(决定 workbench 用哪个 `?folder=` 打开)
127
144
  //
128
145
  // 为什么单独成模块:这段逻辑的真实输入是 **DSH 客户端 store 的形状**,而它随 DSH 版本变过 ——
129
- // · DSH ≤ 0.1.6-alpha.1:`SessionListState` 带 `current`(选中的会话 id 就挂在会话列表快照上,
146
+ // · rc 线(≤ 0.1.5-rc.3,最新 rc 仍是这样):`SessionListState` 带 `current`(选中的会话 id 挂在快照上,
130
147
  // 老写法 `useSessions(s => s).current` 即"当前会话");
131
- // · DSH ≥ 0.1.6-alpha.2:`current` 被移出列表 store(上游 refactor 的注释:view selection remains
148
+ // · alpha 线(≥ 0.1.6-alpha.2,含最新 0.1.7-alpha.1):`current` 被移出列表 store(上游 refactor 注释:view selection remains
132
149
  // outside the Controller;快照只剩 `{ ids, byId, phase, subagentsByParent, jobsBySession }`),
133
150
  // 会话作用域插槽改为用标准 prop `sessionId` 告诉 body"你是哪个会话的" —— 官方自己的右侧栏标签
134
151
  // 就是这么读 cwd 的(ui-deliverables 的 ReviewTab:`useSessions(s => s.byId[sessionId]?.cwd)`)。
@@ -149,7 +166,7 @@ function cwdOf(sessions, sessionId) {
149
166
  return typeof row.cwd === 'string' && row.cwd !== '' ? row.cwd : undefined
150
167
  }
151
168
 
152
- /** 工作区表里"包含该会话"的那个工作区的路径(DSH ≤ 0.1.6-alpha.1 的 currentAddress 语义由它兜底)。 */
169
+ /** 工作区表里"包含该会话"的那个工作区的路径(rc 线的 currentAddress 语义由它兜底)。 */
153
170
  function pathHoldingSession(workspaces, sessionId) {
154
171
  if (workspaces == null || !Array.isArray(workspaces.items)) return undefined
155
172
  if (typeof sessionId !== 'string' || sessionId === '') return undefined
@@ -763,7 +780,7 @@ function intersects(list, group) {
763
780
  // 数据通道:同源 fetch DSH Connection 的共享 /api(status / start / stop / setup / open-file / ui-mode;
764
781
  // web 走 webServer 的 /api 前缀,desktop 走 IPC 帧管道 → 插件不依赖 webServer)。
765
782
  //
766
- // UI 载体只有右侧栏标签(需要 sidebarRightTabs / sidebarRight 服务,DSH ≥ 0.1.5-alpha.1):
783
+ // UI 载体只有右侧栏标签(需要 sidebarRightTabs / sidebarRight 服务;rc 线与 alpha 线都有):
767
784
  // 一段 ctx.sidebarRightTabs.register({ id, kind, priority, patterns, canOpen, title, guide })
768
785
  // → guide 页入口框;并通过 patterns 认领 `dsh-resource://file/**` 文件地址,成为官方 openFile
769
786
  // (产物 chip / "交付"卡片预览 / 正文提及)与任何 openResource(fileAddress) 的落点。
@@ -816,8 +833,8 @@ let React = require('react')
816
833
  }
817
834
 
818
835
  // ---------- 标准 prop 里的会话/工作区快照 ----------
819
- // 取哪条路、退到哪条路**全在本文件的工作区解析段**(纯函数):DSH ≤ 0.1.6-alpha.1 的
820
- // `SessionListState.current` 在 0.1.6-alpha.2 被移出列表 store,会话作用域插槽改用标准 prop
836
+ // 取哪条路、退到哪条路**全在本文件的工作区解析段**(纯函数):rc 线(≤ 0.1.5-rc.3)的
837
+ // `SessionListState.current` 在 alpha 线(≥ 0.1.6-alpha.2)被移出列表 store,会话作用域插槽改用标准 prop
821
838
  // `sessionId` 标识当前会话 —— 只认旧形状会让 cwd 静默变成 undefined(IDE 以空工作区启动)。
822
839
  /** 服务缺失时的占位选择器:**hook 调用次数必须恒定**(React 按位置对账),
823
840
  * 否则"服务晚到"的那一次渲染会抛 "Rendered more hooks than during the previous render"。 */
@@ -958,11 +975,15 @@ let React = require('react')
958
975
  document.head.appendChild(tag)
959
976
  }
960
977
 
961
- // ---------- 右侧栏标签(DSH ≥ 0.1.5-alpha.1:ctx.sidebarRightTabs / ctx.sidebarRight) ----------
978
+ // ---------- 右侧栏标签(rc 线与 alpha 线都有:ctx.sidebarRightTabs / ctx.sidebarRight) ----------
962
979
  // 二段式注册:类型定义进 sidebarRightTabs,body 进 keyed 插槽 sidebar.right.pane.tab。
963
980
  // 服务始终不就绪(旧版 DSH)→ 判定 legacy:只留设置页提示(见 internalApply)。
964
981
  var CS_KIND = 'code-server'
965
982
  var CS_TAB_ID = 'dsh-code-server-app'
983
+ /** **profile 条目 id**(cordis.patch.yml 的 `- id: code-server`)。新通道里它同时是**设置命名空间**:
984
+ * `ctx.configForms.get(ENTRY_ID)` 按条目 id 取表单,`whileServed([ENTRY_ID])` 按它判断
985
+ * "宿主是否正在服务这个条目的配置"。必须与宿主 lib/index.js 的 SETTINGS_NS、行 id 三者一致。 */
986
+ var CS_ENTRY_ID = 'code-server'
966
987
  /** 本插件的**包名**,同时是客户端 bundle 的 loader id(banner 里 `load({id})` 那一个)。
967
988
  * 插件页按包名派发配置区(`renderSlot('plugins.bundle.config', …, { entryKey: pkg.name })`),
968
989
  * 所以这个键必须与安装进来的包名逐字相同 —— 0.3.50 起设置卡搬去插件页就是靠它认领。 */
@@ -1069,7 +1090,7 @@ let React = require('react')
1069
1090
  var navigation = tab.navigation
1070
1091
  var revision = navigation != null && typeof navigation.revision === 'number' ? navigation.revision : 0
1071
1092
  // 地址:文件 tab 可能来自会话树里的别的会话,故优先用地址里的 sessionId 对齐工作区;
1072
- // 页面 tab 用**会话作用域标准 prop** `sessionId`(DSH ≥ 0.1.6-alpha.2 的当前会话信源,
1093
+ // 页面 tab 用**会话作用域标准 prop** `sessionId`(alpha 线 ≥ 0.1.6-alpha.2 的当前会话信源,
1073
1094
  // 老版的 `useSessions().current` 已被上游移除 —— 详见本文件的工作区解析段)。
1074
1095
  var address = navigation != null && typeof navigation.address === 'string' ? navigation.address : ''
1075
1096
  var parsed = isPageAddress(address) ? null : parseFileAddress(address)
@@ -1246,11 +1267,27 @@ let React = require('react')
1246
1267
  }
1247
1268
 
1248
1269
  // ---------- 设置(参照 auto-open-web 的自绘卡片模式) ----------
1249
- // 数据通道:settingsScope(官方 settings 域,命名空间 code-server)—— 两个座位共用。
1250
- // 座位(0.3.50 起两条腿都注册,谁被声明谁生效):
1251
- // · `plugins.bundle.config`(DSH ≥ 0.1.6-alpha.2):插件页按**包名**派发的配置区,页面自带
1252
- // 标题/面包屑,我们只出表单 + 保存控件(seat='bundle-config');
1253
- // · `settings.plugin.item`(DSH ≤ 0.1.6-alpha.1):设置页里自绘的可折叠卡片(已在新版退役)。
1270
+ // **座位**与**数据通道**是两条独立的轴,而且分界点不在同一版(2026-09-22 之后的实测更正):
1271
+ //
1272
+ // 座位(声明驱动:`slots.inject` 只在插槽真被声明时才回调,所以两条腿都注册、谁在谁生效):
1273
+ // · `plugins.bundle.config`(alpha 线 ≥ 0.1.6-alpha.2,含最新 0.1.7-alpha.1):插件页按**包名**派发的配置区,
1274
+ // 页面自带标题/面包屑,我们只出表单 + 保存控件(seat='bundle-config');
1275
+ // · `settings.plugin.item`(rc 线 ≤ 0.1.5-rc.3,最新 rc 仍在用):设置页里自绘的可折叠卡片(alpha 线已退役)。
1276
+ //
1277
+ // 数据通道(能力探测:`configForms` 在就用新通道,否则回退 `settingsScope`):
1278
+ // · 新通道 `ctx.configForms.get(ENTRY_ID)`(alpha 线 ≥ 0.1.7-alpha.1):`settingsScope` 已被删除,
1279
+ // 配置就是**插件条目自己的 Config**(宿主半的字段 `.volatile()`)。表单模型:
1280
+ // `getSnapshot()` → { status, writable, revision, value, base, user }、`subscribe(cb)`、
1281
+ // **唯一写路径** `mutate(ops, revision)`(原子、带 revision 栅栏)、`dispose()`;
1282
+ // 注册还要经 `whileServed([ENTRY_ID], …)` 门禁(宿主只有在服务该条目配置时才让我们注册页面)。
1283
+ // · 旧通道 `ctx.settingsScope.bind({ namespace })`(rc 0.1.5-rc.x;**也含 0.1.6-alpha.2** ——
1284
+ // 那一版座位已经搬到插件页、数据面却还是旧的):卡片直接读写 settings 命名空间。
1285
+ //
1286
+ // 组合起来三种真实形态都要成立:
1287
+ // ① {旧座位, 旧通道} = rc 线;② {新座位, 旧通道} = 0.1.6-alpha.2;③ {新座位, 新通道} = 0.1.7-alpha.1+。
1288
+ // 这也是为什么 `inject` 只留**必然存在**的 `slots`,两个通道都运行时探测:
1289
+ // 把 `settingsScope` 写进 inject 会让整个客户端条目在 0.1.7-alpha.1 上永远 pending
1290
+ // (表现是右侧栏标签、设置卡、常驻预热**全都不存在**,而且只有一条 "1 entry did not activate")。
1254
1291
  var CARD_CSS =
1255
1292
  '.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}' +
1256
1293
  '.dshcs-card:hover{border-color:var(--dsw-alias-label-dimmed,var(--dsw-alias-border-l2))}' +
@@ -1408,177 +1445,305 @@ let React = require('react')
1408
1445
  )
1409
1446
  }
1410
1447
 
1411
- /** 设置卡片页面:useState+useEffect 订阅 settingsScope(不投机 useSyncExternalStore
1412
- * 对 snapshot 引用稳定性的要求;不建中间 store)。 */
1413
- function csSettingsPage(props) {
1414
- try {
1415
- var scope = props.scope
1416
- var [snapshot, setSnapshot] = React.useState(function () { return scope !== undefined ? scope.getSnapshot() : null })
1417
- var [draft, setDraft] = React.useState(null) // null | { keepResident, fileOpenScope }(未保存草稿)
1418
- var [saving, setSaving] = React.useState(false)
1419
- var [failed, setFailed] = React.useState(false)
1420
- // 共享 store:读取当前 UI 载体状态(右侧栏是否就绪 / 是否旧版 DSH),必须放在所有提前 return 之前
1421
- var liveStore = useStore()
1422
- // 常驻面实时状态(停靠/停放/降级):卡片里显示,便于判断"切标签是否还会重载"
1423
- var [surfaceState, setSurfaceState] = React.useState(surfaceSnapshot())
1424
- React.useEffect(function () {
1425
- return subscribeSurface(function (snap) { setSurfaceState(snap) })
1426
- }, [])
1427
- React.useEffect(function () {
1428
- if (scope === undefined || typeof scope.subscribe !== 'function') return
1429
- function onUpdate() {
1430
- try { setSnapshot(scope.getSnapshot()) } catch (e) { /* ignore */ }
1431
- }
1432
- var dispose = scope.subscribe(onUpdate)
1433
- return function () { if (typeof dispose === 'function') dispose() }
1434
- }, [scope])
1435
- if (scope === undefined) {
1436
- console.error('[code-server] card scope missing')
1437
- return null
1448
+ // ---------- 字段规格 + 新通道的表单控制器(configForms;alpha 线 ≥ 0.1.7-alpha.1)----------
1449
+ /**
1450
+ * 表单字段规格表:**两条通道共用**的"怎么读当前值 / 怎么解析草稿 / 怎么比较"。
1451
+ *
1452
+ * 字段名与宿主 `Config`(lib/index.js 的 SETTING_FIELDS)一一对应 —— 少一个就是"设置里少一项",
1453
+ * 多一个就是"宿主暴露了没人画的字段"。`read` 里的兜底值必须与宿主的 schema 默认值一致
1454
+ * (claimExtensions 用本文件同源的 DEFAULT_CLAIM_EXTENSIONS,其余与 Config 的 default 对齐)。
1455
+ */
1456
+ var CS_FIELDS = [
1457
+ {
1458
+ field: 'claimExtensions', kind: 'text',
1459
+ read: function (v) { return typeof v.claimExtensions === 'string' ? v.claimExtensions : DEFAULT_CLAIM_EXTENSIONS },
1460
+ display: function (v) { return typeof v === 'string' ? v : '' },
1461
+ parse: function (text) { return normalizeClaimExtensions(text) },
1462
+ same: function (a, b) { return normalizeClaimExtensions(a) === normalizeClaimExtensions(b) },
1463
+ },
1464
+ {
1465
+ field: 'fullscreenOnOpen', kind: 'bool',
1466
+ read: function (v) { return v.fullscreenOnOpen !== false },
1467
+ display: function (v) { return v !== false },
1468
+ parse: function (draft) { return draft === true },
1469
+ same: function (a, b) { return (a !== false) === (b !== false) },
1470
+ },
1471
+ {
1472
+ field: 'fim', kind: 'bool',
1473
+ read: function (v) { return v.fim === true },
1474
+ display: function (v) { return v === true },
1475
+ parse: function (draft) { return draft === true },
1476
+ same: function (a, b) { return (a === true) === (b === true) },
1477
+ },
1478
+ {
1479
+ field: 'fimDebounceMs', kind: 'number',
1480
+ read: function (v) { return normalizeFimDebounce(v.fimDebounceMs) },
1481
+ display: function (v) { return v === undefined || v === null ? '' : String(v) },
1482
+ parse: function (text) { return normalizeFimDebounce(text) },
1483
+ same: function (a, b) { return normalizeFimDebounce(a) === normalizeFimDebounce(b) },
1484
+ },
1485
+ {
1486
+ field: 'fimMultiline', kind: 'bool',
1487
+ read: function (v) { return v.fimMultiline !== false },
1488
+ display: function (v) { return v !== false },
1489
+ parse: function (draft) { return draft === true },
1490
+ same: function (a, b) { return (a !== false) === (b !== false) },
1491
+ },
1492
+ {
1493
+ field: 'fimDisableGlobs', kind: 'text',
1494
+ read: function (v) { return typeof v.fimDisableGlobs === 'string' ? v.fimDisableGlobs : '' },
1495
+ display: function (v) { return typeof v === 'string' ? v : '' },
1496
+ parse: function (text) { return typeof text === 'string' ? text : '' },
1497
+ same: function (a, b) { return String(a === undefined ? '' : a) === String(b === undefined ? '' : b) },
1498
+ },
1499
+ {
1500
+ field: 'keepResident', kind: 'bool',
1501
+ read: function (v) { return v.keepResident !== false },
1502
+ display: function (v) { return v !== false },
1503
+ parse: function (draft) { return draft === true },
1504
+ same: function (a, b) { return (a !== false) === (b !== false) },
1505
+ },
1506
+ ]
1507
+ /** 快照里的"值对象"(缺失时给空对象,让各字段的 read 兜底到默认值)。 */
1508
+ function csValueObject(snapshot) {
1509
+ return snapshot !== undefined && snapshot !== null
1510
+ && snapshot.value !== undefined && snapshot.value !== null && typeof snapshot.value === 'object'
1511
+ ? snapshot.value : {}
1512
+ }
1513
+ /** 某字段在设置里是否被用户显式改过(user 层的**键存在**即"已覆盖",不看值)。 */
1514
+ function csOverridden(user, field) {
1515
+ return user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, field)
1516
+ }
1517
+
1518
+ /**
1519
+ * 壳层冻结模块表里的快照 store(`@deepseek-ai/dsh-client-store`,壳层基线模块)。
1520
+ * 拿不到就返回 null —— 调用方据此退回旧通道,**绝不让整个客户端半部因为一个可选数据面加载失败**。
1521
+ */
1522
+ var csStoreFactory
1523
+ function csSnapshotStore(initial) {
1524
+ if (csStoreFactory === undefined) {
1525
+ try {
1526
+ var mod = require('@deepseek-ai/dsh-client-store')
1527
+ csStoreFactory = mod !== null && mod !== undefined && typeof mod.createSnapshotStore === 'function'
1528
+ ? mod.createSnapshotStore : null
1529
+ } catch (e) {
1530
+ csStoreFactory = null
1438
1531
  }
1439
- if (snapshot === null || snapshot === undefined || snapshot.status !== 'ready') {
1440
- // 尚未就绪(连接/镜像加载中):静默留空,官方插槽边界接管;不显示占位
1441
- if (snapshot !== null && snapshot !== undefined && snapshot.status === 'unavailable') {
1442
- console.warn('[code-server] settings scope unavailable:', snapshot.status)
1532
+ if (csStoreFactory === null) {
1533
+ console.warn('[code-server] @deepseek-ai/dsh-client-store 不在壳层模块表里:配置表单退回旧通道')
1534
+ }
1535
+ }
1536
+ return csStoreFactory === null ? null : csStoreFactory(initial)
1537
+ }
1538
+
1539
+ /**
1540
+ * 新通道的表单控制器(官方 `ConfigForm` 的暂存编辑器等价物)。
1541
+ *
1542
+ * 语义与官方一致,这也是它存在的理由:
1543
+ * · 字段值 = **用户层 over 组合层 over schema 默认**(`getSnapshot().value`),`user` 的键存在即"已覆盖";
1544
+ * · 只有**保存**才写,而且一次 `mutate(ops, revision)` **原子提交全部暂存编辑**,带暂存起点读到的
1545
+ * revision 作栅栏;宿主是唯一权威(返回布尔即接受与否),被拒时**保留草稿**;
1546
+ * · 暂存在控制器里(不在组件里):组件只从 store 的投影渲染,写只经 action —— 与官方页面同构。
1547
+ */
1548
+ function csConfigFormController(form) {
1549
+ var staged = new Map() // field -> { clear: boolean, display: any }
1550
+ var listeners = new Set()
1551
+ var baseline = null // 首次编辑前读到的快照(revision 栅栏的来源)
1552
+ var saving = false
1553
+ var failed = false
1554
+ function publish() { listeners.forEach(function (fn) { fn() }) }
1555
+ var unsubscribe = form !== null && form !== undefined && typeof form.subscribe === 'function'
1556
+ ? form.subscribe(function () { publish() })
1557
+ : function () {}
1558
+ var snapshotOf = function () { return form.getSnapshot() }
1559
+ function specOf(field) {
1560
+ for (var i = 0; i < CS_FIELDS.length; i += 1) if (CS_FIELDS[i].field === field) return CS_FIELDS[i]
1561
+ return null
1562
+ }
1563
+ /** 计划:暂存编辑 → 有序 op 列表(撤销 = unset;草稿与存量等价 = 不发 op)。 */
1564
+ function plan() {
1565
+ var snapshot = snapshotOf()
1566
+ var value = csValueObject(snapshot)
1567
+ var user = snapshot !== undefined && snapshot !== null ? snapshot.user : undefined
1568
+ var out = []
1569
+ CS_FIELDS.forEach(function (spec) {
1570
+ if (!staged.has(spec.field)) return
1571
+ var entry = staged.get(spec.field)
1572
+ if (entry.clear === true) {
1573
+ // 恢复默认 = 移除覆盖层(回落到组合层/schema 默认);本来就没有覆盖 ⇒ 无需写。
1574
+ if (csOverridden(user, spec.field)) out.push({ field: spec.field, op: { op: 'unset', path: [spec.field] } })
1575
+ return
1443
1576
  }
1444
- return null
1577
+ var wire = spec.parse(entry.display)
1578
+ if (wire === undefined) { out.push({ field: spec.field }); return } // 不可解析:草稿留着,保存被拒
1579
+ if (spec.same(wire, spec.read(value))) return
1580
+ out.push({ field: spec.field, op: { op: 'set', path: [spec.field], value: wire } })
1581
+ })
1582
+ return out
1583
+ }
1584
+ function projection() {
1585
+ var snapshot = snapshotOf()
1586
+ var value = csValueObject(snapshot)
1587
+ var user = snapshot !== undefined && snapshot !== null ? snapshot.user : undefined
1588
+ var values = {}
1589
+ var overridden = {}
1590
+ CS_FIELDS.forEach(function (spec) {
1591
+ values[spec.field] = staged.has(spec.field)
1592
+ ? staged.get(spec.field).display
1593
+ : spec.display(spec.read(value))
1594
+ overridden[spec.field] = csOverridden(user, spec.field)
1595
+ })
1596
+ var items = plan()
1597
+ return {
1598
+ available: snapshot !== undefined && snapshot !== null && snapshot.status === 'ready',
1599
+ writable: snapshot !== undefined && snapshot !== null && snapshot.writable === true,
1600
+ dirty: items.length > 0,
1601
+ invalid: items.some(function (item) { return item.op === undefined }),
1602
+ saving: saving,
1603
+ failed: failed,
1604
+ values: values,
1605
+ overridden: overridden,
1445
1606
  }
1446
- var value = snapshot.value !== undefined && snapshot.value !== null ? snapshot.value : {}
1447
- var user = snapshot.user
1448
- var loaded = {
1449
- keepResident: value.keepResident !== false,
1450
- claimExtensions: typeof value.claimExtensions === 'string' ? value.claimExtensions : DEFAULT_CLAIM_EXTENSIONS,
1451
- fullscreenOnOpen: value.fullscreenOnOpen !== false,
1452
- fim: value.fim === true,
1453
- fimDebounceMs: normalizeFimDebounce(value.fimDebounceMs),
1454
- fimMultiline: value.fimMultiline !== false,
1455
- fimDisableGlobs: typeof value.fimDisableGlobs === 'string' ? value.fimDisableGlobs : '',
1456
1607
  }
1457
- // ---- 旧版 DSH(无右侧栏服务):本页是唯一的提示出口,不显示任何设置项 ----
1458
- if (liveStore != null && liveStore.sidebarUi === 'legacy') {
1459
- return React.createElement(csCard, {
1460
- title: 'Code Server',
1461
- description: '当前 DSH 版本不受支持(缺少右侧栏服务)',
1462
- defaultOpen: true,
1463
- noticeOnly: true,
1464
- state: { available: true, writable: false, dirty: false, invalid: false, saving: false, failed: false },
1465
- },
1466
- React.createElement('div', { className: 'dshcs-field' },
1467
- React.createElement('div', { className: 'dshcs-legacy' }, '本插件自 0.2.3 起不再兼容旧版 DSH。'),
1468
- React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 0 } },
1469
- '未检测到右侧栏插件服务 sidebarRightTabs / sidebarRight,因此插件不提供任何入口'
1470
- + '(旧版的悬浮球与浮动窗口已移除),也不会后台启动 IDE。'
1471
- + '\n升级 DSH 到带右侧栏的版本(≥ 0.1.5-alpha.1)后,Code Server 会出现在右侧栏标签里,'
1472
- + '本页同时显示完整设置项;升级后无需重装本插件,刷新页面即可。'
1473
- )
1474
- )
1475
- )
1608
+ function stage(field, entry) {
1609
+ if (baseline === null) baseline = snapshotOf()
1610
+ staged.set(field, entry)
1611
+ failed = false
1612
+ publish()
1476
1613
  }
1477
- var overriddenKeep = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'keepResident')
1478
- var overriddenClaim = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'claimExtensions')
1479
- var overriddenFullscreen = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'fullscreenOnOpen')
1480
- var overriddenFim = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'fim')
1481
- var overriddenFimDebounce = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'fimDebounceMs')
1482
- var overriddenFimMultiline = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'fimMultiline')
1483
- var overriddenFimGlobs = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'fimDisableGlobs')
1484
- var dirty = draft !== null && (draft.keepResident !== loaded.keepResident
1485
- || normalizeClaimExtensions(draft.claimExtensions) !== normalizeClaimExtensions(loaded.claimExtensions)
1486
- || draft.fullscreenOnOpen !== loaded.fullscreenOnOpen
1487
- || draft.fim !== loaded.fim
1488
- || normalizeFimDebounce(draft.fimDebounceMs) !== normalizeFimDebounce(loaded.fimDebounceMs)
1489
- || draft.fimMultiline !== loaded.fimMultiline
1490
- || draft.fimDisableGlobs !== loaded.fimDisableGlobs)
1491
- var saveDisabled = !dirty || saving
1492
- var state = {
1493
- available: true,
1494
- writable: snapshot.writable,
1495
- dirty: dirty,
1496
- invalid: false,
1497
- saving: saving,
1498
- failed: failed,
1614
+ function discard() {
1615
+ if (staged.size === 0 && failed !== true) return
1616
+ staged.clear()
1617
+ baseline = null
1618
+ failed = false
1619
+ publish()
1499
1620
  }
1500
- // 保存/恢复成功后,把 host 最新 status 推给模块共享 store → 常驻预热等立即按新设置生效。
1501
- function syncStatusToStore() {
1502
- api('/code-server/status').then(function (s) {
1503
- if (s != null && typeof s === 'object') setState({ status: s })
1504
- }).catch(function () { /* 失败由下次轮询兜底 */ })
1621
+ function save() {
1622
+ var items = plan()
1623
+ var snapshot = snapshotOf()
1624
+ if (items.length === 0 || saving === true) return Promise.resolve(false)
1625
+ if (snapshot !== undefined && snapshot !== null && snapshot.writable !== true) return Promise.resolve(false)
1626
+ if (items.some(function (item) { return item.op === undefined })) return Promise.resolve(false)
1627
+ var ops = items.map(function (item) { return item.op })
1628
+ var revision = baseline !== null && baseline !== undefined ? baseline.revision : undefined
1629
+ saving = true
1630
+ failed = false
1631
+ publish()
1632
+ return Promise.resolve()
1633
+ .then(function () { return form.mutate(ops, revision) })
1634
+ .then(function (landed) {
1635
+ if (landed === true) { staged.clear(); baseline = null } else { failed = true }
1636
+ return landed === true
1637
+ })
1638
+ .catch(function () { failed = true; return false })
1639
+ .then(function (ok) { saving = false; publish(); return ok })
1505
1640
  }
1506
- async function doSave() {
1507
- if (dirty && !saving) {
1508
- setSaving(true); setFailed(false)
1509
- try {
1510
- var d = draft !== null ? draft : loaded
1511
- await props.scope.set('keepResident', d.keepResident === true)
1512
- await props.scope.set('claimExtensions', normalizeClaimExtensions(d.claimExtensions))
1513
- await props.scope.set('fullscreenOnOpen', d.fullscreenOnOpen === true)
1514
- await props.scope.set('fim', d.fim === true)
1515
- await props.scope.set('fimDebounceMs', normalizeFimDebounce(d.fimDebounceMs))
1516
- await props.scope.set('fimMultiline', d.fimMultiline === true)
1517
- await props.scope.set('fimDisableGlobs', typeof d.fimDisableGlobs === 'string' ? d.fimDisableGlobs : '')
1518
- setDraft(null)
1519
- syncStatusToStore()
1520
- } catch (e) {
1521
- setFailed(true)
1641
+ return {
1642
+ projection: projection,
1643
+ /** bind 出的是 DSH 的 hook 源(壳层按 hooks 键名派生成 `props.use<Key>`)。 */
1644
+ bind: function () {
1645
+ var store = csSnapshotStore(projection())
1646
+ if (store === null) return null
1647
+ listeners.add(function () { store.set(projection()) })
1648
+ return store
1649
+ },
1650
+ actions: function () {
1651
+ return {
1652
+ edit: function (field, display) { stage(field, { clear: false, display: display }) },
1653
+ resetField: function (field) {
1654
+ var spec = specOf(field)
1655
+ if (spec === null) return
1656
+ var snapshot = snapshotOf()
1657
+ var base = snapshot !== undefined && snapshot !== null
1658
+ && snapshot.base !== undefined && snapshot.base !== null && typeof snapshot.base === 'object'
1659
+ ? snapshot.base : {}
1660
+ stage(field, { clear: true, display: spec.display(spec.read(base)) })
1661
+ },
1662
+ save: save,
1663
+ discard: discard,
1522
1664
  }
1523
- setSaving(false)
1524
- }
1665
+ },
1666
+ dispose: function () {
1667
+ unsubscribe()
1668
+ listeners.clear()
1669
+ },
1525
1670
  }
1526
- async function doReset() {
1527
- if (saving || snapshot.writable !== true) return
1528
- setSaving(true); setFailed(false)
1529
- try {
1530
- // unset 清除 user 覆盖层 → 回到 base 默认值;成功后镜像同步
1531
- if (typeof props.scope.unset === 'function') {
1532
- await props.scope.unset('keepResident')
1533
- await props.scope.unset('claimExtensions')
1534
- await props.scope.unset('fullscreenOnOpen')
1535
- await props.scope.unset('fim')
1536
- await props.scope.unset('fimDebounceMs')
1537
- await props.scope.unset('fimMultiline')
1538
- await props.scope.unset('fimDisableGlobs')
1539
- } else {
1540
- await props.scope.set('keepResident', value.keepResident === undefined || value.keepResident === null ? true : value.keepResident)
1541
- await props.scope.set('claimExtensions', typeof value.claimExtensions === 'string' ? normalizeClaimExtensions(value.claimExtensions) : DEFAULT_CLAIM_EXTENSIONS)
1542
- await props.scope.set('fullscreenOnOpen', value.fullscreenOnOpen === undefined || value.fullscreenOnOpen === null ? true : value.fullscreenOnOpen)
1543
- await props.scope.set('fim', value.fim === true)
1544
- await props.scope.set('fimDebounceMs', normalizeFimDebounce(value.fimDebounceMs))
1545
- await props.scope.set('fimMultiline', value.fimMultiline !== false)
1546
- await props.scope.set('fimDisableGlobs', typeof value.fimDisableGlobs === 'string' ? value.fimDisableGlobs : '')
1547
- }
1548
- setDraft(null)
1549
- syncStatusToStore()
1550
- } catch (e) {
1551
- setFailed(true)
1552
- }
1553
- setSaving(false)
1671
+ }
1672
+
1673
+ // ---------- 两条通道共用的呈现层 ----------
1674
+ /** 文案常量:两个外壳(设置页卡片 / 插件页配置区)必须逐字一致,否则"同一项设置在两个位置说法不同"。 */
1675
+ var CS_UNSAVED_LABEL = '未保存'
1676
+ var CS_READONLY_LABEL = '本部署的设置为只读。'
1677
+ var CS_SAVE_FAILED_LABEL = '本部署没有接受这些值,已保留供你修改。'
1678
+ var CS_DISCARD_LABEL = '放弃修改'
1679
+ var CS_SAVE_LABEL = '保存'
1680
+ var CS_SAVING_LABEL = '保存中…'
1681
+ /** 旧版 DSH(无右侧栏服务)的提示文案 —— 本页是那种部署里唯一的提示出口。 */
1682
+ var CS_LEGACY_TITLE = 'Code Server'
1683
+ var CS_LEGACY_DESC = '当前 DSH 版本不受支持(缺少右侧栏服务)'
1684
+ var CS_LEGACY_LEAD = '本插件自 0.2.3 起不再兼容旧版 DSH。'
1685
+ var CS_LEGACY_BODY = '未检测到右侧栏插件服务 sidebarRightTabs / sidebarRight,因此插件不提供任何入口'
1686
+ + '(旧版的悬浮球与浮动窗口已移除),也不会后台启动 IDE。'
1687
+ + '\n升级 DSH 到 rc 线(0.1.5-rc.x)或 0.1.6-alpha.2 起的 alpha 线后,Code Server 会出现在右侧栏标签里,'
1688
+ + '本页同时显示完整设置项;升级后无需重装本插件,刷新页面即可。'
1689
+
1690
+ /** 保存/恢复成功后,把 host 最新 status 推给模块共享 store → 常驻预热等立即按新设置生效。 */
1691
+ function csSyncStatusToStore() {
1692
+ api('/code-server/status').then(function (s) {
1693
+ if (s != null && typeof s === 'object') setState({ status: s })
1694
+ }).catch(function () { /* 失败由下次轮询兜底 */ })
1695
+ }
1696
+ /** 常驻面状态那行提示(两个通道共用)。 */
1697
+ function csSurfaceHintText(surfaceState) {
1698
+ if (!supportsMoveBefore) {
1699
+ return '当前浏览器不支持 Element.moveBefore(Chromium <133)→ 常驻不可用,切标签仍会整页重载(升级浏览器后自动生效)'
1554
1700
  }
1555
- var fields = [
1701
+ return '常驻面:' + (surfaceState.docked ? '已停靠' : (surfaceState.ready ? '已停放(后台运行中)' : '未启动'))
1702
+ + (surfaceState.degraded ? ' · 本次发生过降级重载' : '')
1703
+ }
1704
+ /** 面缺失时的占位 hook:hook 调用次数恒定(仍走一次 useSyncExternalStore),但永不订阅、恒返回 undefined。 */
1705
+ function csNoopSubscribe() { return function () {} }
1706
+ function csUndefinedSnapshot() { return undefined }
1707
+ function csAbsentHook() { return React.useSyncExternalStore(csNoopSubscribe, csUndefinedSnapshot) }
1708
+
1709
+ /**
1710
+ * 表单主体(**两条通道共用**):只认归一化视图与处理器,不知道数据从哪来。
1711
+ * @param view {writable, values, overridden, dirty, invalid, saving, failed}
1712
+ * @param handlers {onEdit(field, value), onResetField(field), onSave, onDiscard}
1713
+ * @param extras {surfaceHint} 常驻面那行提示(需要组件里的实时状态,故由调用方算好传进来)
1714
+ */
1715
+ function csSettingsFields(view, handlers, extras) {
1716
+ var writable = view.writable === true
1717
+ var reset = function (field) {
1718
+ return React.createElement(csBadges, {
1719
+ overridden: true, disabled: !writable,
1720
+ overriddenLabel: '已覆盖', resetLabel: '恢复默认',
1721
+ onReset: function () { handlers.onResetField(field) },
1722
+ })
1723
+ }
1724
+ var edit = function (field) {
1725
+ return function (v) { handlers.onEdit(field, v) }
1726
+ }
1727
+ return [
1556
1728
  // 一行一个可改设置(0.2.11 起):认领类型 + 打开即全屏 + FIM 补全(0.3.61,含 0.3.62 的三个子项)
1557
1729
  // + 后台常驻。入口/依赖安装/环境检测三行已移除(入口在右侧栏「开始」页的 guide 入口框;诊断看 host 日志)。
1558
1730
  React.createElement('div', { className: 'dshcs-field' },
1559
1731
  React.createElement('div', { className: 'dshcs-fieldHead' },
1560
1732
  React.createElement('span', { className: 'dshcs-fieldLabel' }, '认领类型(按扩展名,分号分隔)'),
1561
- overriddenClaim === true
1562
- ? React.createElement(csBadges, {
1563
- overridden: true, disabled: snapshot.writable !== true,
1564
- overriddenLabel: '已覆盖', resetLabel: '恢复默认',
1565
- onReset: function () { doReset() },
1566
- })
1567
- : null
1733
+ view.overridden.claimExtensions === true ? reset('claimExtensions') : null
1568
1734
  ),
1569
1735
  React.createElement(csTextarea, {
1570
- value: draft !== null ? draft.claimExtensions : loaded.claimExtensions,
1571
- disabled: snapshot.writable !== true,
1736
+ value: view.values.claimExtensions,
1737
+ disabled: !writable,
1572
1738
  placeholder: '例如 py;ts;!md(留空 = 不认领任何文件)',
1573
- onChange: function (v) { setDraft(function (prev) { return Object.assign({}, prev !== null ? prev : loaded, { claimExtensions: v }) }); setFailed(false) },
1739
+ onChange: edit('claimExtensions'),
1574
1740
  }),
1575
1741
  React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 4 } },
1576
1742
  '写扩展名(不带点,大小写随意);`*` = 其余类型也认领;`!ext` = 不认领(排除优先)。'
1577
1743
  + '不再区分会话内/绝对路径 —— 所有 `dsh-resource://file/**` 一视同仁,未认领的落回 DSH 自带预览。'),
1578
1744
  React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 4 } },
1579
- '实际规则:' + describeClaimPolicy(draft !== null ? draft.claimExtensions : loaded.claimExtensions)
1580
- + (normalizeClaimExtensions(draft !== null ? draft.claimExtensions : loaded.claimExtensions)
1581
- !== (draft !== null ? draft.claimExtensions : loaded.claimExtensions) ? '(保存后归一化)' : '')),
1745
+ '实际规则:' + describeClaimPolicy(view.values.claimExtensions)
1746
+ + (normalizeClaimExtensions(view.values.claimExtensions) !== view.values.claimExtensions ? '(保存后归一化)' : '')),
1582
1747
  // 提示块(0.3.52):分三行写清楚 + 默认值**折成多行**显示。
1583
1748
  // 以前这里是一整行"… · 默认 <700 字符>" —— `;` 在 CSS 里不是断行点,那串既不折行也复制不全,
1584
1749
  // 直接把卡片撑破。现在:三组各自成行(与 lib/claim-types.js 的常量同源),换行与分号等价,
@@ -1597,18 +1762,12 @@ let React = require('react')
1597
1762
  React.createElement('div', { className: 'dshcs-field' },
1598
1763
  React.createElement('div', { className: 'dshcs-fieldHead' },
1599
1764
  React.createElement('span', { className: 'dshcs-fieldLabel' }, '打开即全屏(右侧栏铺满窗口)'),
1600
- overriddenFullscreen === true
1601
- ? React.createElement(csBadges, {
1602
- overridden: true, disabled: snapshot.writable !== true,
1603
- overriddenLabel: '已覆盖', resetLabel: '恢复默认',
1604
- onReset: function () { doReset() },
1605
- })
1606
- : null
1765
+ view.overridden.fullscreenOnOpen === true ? reset('fullscreenOnOpen') : null
1607
1766
  ),
1608
1767
  React.createElement(csCheck, {
1609
- checked: draft !== null ? draft.fullscreenOnOpen : loaded.fullscreenOnOpen,
1610
- disabled: snapshot.writable !== true,
1611
- onChange: function (v) { setDraft(function (prev) { return Object.assign({}, prev !== null ? prev : loaded, { fullscreenOnOpen: v === true }) }); setFailed(false) },
1768
+ checked: view.values.fullscreenOnOpen,
1769
+ disabled: !writable,
1770
+ onChange: edit('fullscreenOnOpen'),
1612
1771
  }, '打开 Code Server 标签(含点开产物/交付文件)时,自动把右侧栏从"与对话并排"切到全屏;'
1613
1772
  + '想同时看对话就关掉它,或随时点右侧栏的「退出全屏」—— 本次打开不会被抢回去'),
1614
1773
  React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 4 } },
@@ -1618,18 +1777,12 @@ let React = require('react')
1618
1777
  React.createElement('div', { className: 'dshcs-field' },
1619
1778
  React.createElement('div', { className: 'dshcs-fieldHead' },
1620
1779
  React.createElement('span', { className: 'dshcs-fieldLabel' }, 'FIM 补全(实验性)'),
1621
- overriddenFim === true
1622
- ? React.createElement(csBadges, {
1623
- overridden: true, disabled: snapshot.writable !== true,
1624
- overriddenLabel: '已覆盖', resetLabel: '恢复默认',
1625
- onReset: function () { doReset() },
1626
- })
1627
- : null
1780
+ view.overridden.fim === true ? reset('fim') : null
1628
1781
  ),
1629
1782
  React.createElement(csCheck, {
1630
- checked: draft !== null ? draft.fim : loaded.fim,
1631
- disabled: snapshot.writable !== true,
1632
- onChange: function (v) { setDraft(function (prev) { return Object.assign({}, prev !== null ? prev : loaded, { fim: v === true }) }); setFailed(false) },
1783
+ checked: view.values.fim,
1784
+ disabled: !writable,
1785
+ onChange: edit('fim'),
1633
1786
  }, '开启后,在编辑器里打字停顿约 0.3 秒会向模型要一次「续写」,以灰字显示在光标后:Tab 接受、Esc 丢弃。默认关闭'),
1634
1787
  React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 4 } },
1635
1788
  '实验性功能,默认关闭。三点须知:'
@@ -1643,18 +1796,12 @@ let React = require('react')
1643
1796
  React.createElement('div', { className: 'dshcs-field' },
1644
1797
  React.createElement('div', { className: 'dshcs-fieldHead' },
1645
1798
  React.createElement('span', { className: 'dshcs-fieldLabel' }, 'FIM · 停顿毫秒数(停下多久才请求)'),
1646
- overriddenFimDebounce === true
1647
- ? React.createElement(csBadges, {
1648
- overridden: true, disabled: snapshot.writable !== true,
1649
- overriddenLabel: '已覆盖', resetLabel: '恢复默认',
1650
- onReset: function () { doReset() },
1651
- })
1652
- : null
1799
+ view.overridden.fimDebounceMs === true ? reset('fimDebounceMs') : null
1653
1800
  ),
1654
1801
  React.createElement(csNumber, {
1655
- value: draft !== null ? draft.fimDebounceMs : loaded.fimDebounceMs,
1656
- min: 100, max: 3000, step: 50, disabled: snapshot.writable !== true,
1657
- onChange: function (v) { setDraft(function (prev) { return Object.assign({}, prev !== null ? prev : loaded, { fimDebounceMs: v }) }); setFailed(false) },
1802
+ value: view.values.fimDebounceMs,
1803
+ min: 100, max: 3000, step: 50, disabled: !writable,
1804
+ onChange: edit('fimDebounceMs'),
1658
1805
  }),
1659
1806
  React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 4 } },
1660
1807
  '有效范围 100–3000ms,保存时按它夹取(与宿主同一套规则)。实测这条端点往返 112–416ms,'
@@ -1663,18 +1810,12 @@ let React = require('react')
1663
1810
  React.createElement('div', { className: 'dshcs-field' },
1664
1811
  React.createElement('div', { className: 'dshcs-fieldHead' },
1665
1812
  React.createElement('span', { className: 'dshcs-fieldLabel' }, 'FIM · 允许多行补全'),
1666
- overriddenFimMultiline === true
1667
- ? React.createElement(csBadges, {
1668
- overridden: true, disabled: snapshot.writable !== true,
1669
- overriddenLabel: '已覆盖', resetLabel: '恢复默认',
1670
- onReset: function () { doReset() },
1671
- })
1672
- : null
1813
+ view.overridden.fimMultiline === true ? reset('fimMultiline') : null
1673
1814
  ),
1674
1815
  React.createElement(csCheck, {
1675
- checked: draft !== null ? draft.fimMultiline : loaded.fimMultiline,
1676
- disabled: snapshot.writable !== true,
1677
- onChange: function (v) { setDraft(function (prev) { return Object.assign({}, prev !== null ? prev : loaded, { fimMultiline: v === true }) }); setFailed(false) },
1816
+ checked: view.values.fimMultiline,
1817
+ disabled: !writable,
1818
+ onChange: edit('fimMultiline'),
1678
1819
  }, '允许补出多行(关掉后只取第一行;首行为空 = 这次不补)'),
1679
1820
  React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 4 } },
1680
1821
  '默认开。实测模型在"并不需要补"的位置会硬凑一段,多行会把这种噪声放大 —— 想更克制就关掉它。')
@@ -1682,19 +1823,13 @@ let React = require('react')
1682
1823
  React.createElement('div', { className: 'dshcs-field' },
1683
1824
  React.createElement('div', { className: 'dshcs-fieldHead' },
1684
1825
  React.createElement('span', { className: 'dshcs-fieldLabel' }, 'FIM · 按 glob 禁用(分号或换行分隔)'),
1685
- overriddenFimGlobs === true
1686
- ? React.createElement(csBadges, {
1687
- overridden: true, disabled: snapshot.writable !== true,
1688
- overriddenLabel: '已覆盖', resetLabel: '恢复默认',
1689
- onReset: function () { doReset() },
1690
- })
1691
- : null
1826
+ view.overridden.fimDisableGlobs === true ? reset('fimDisableGlobs') : null
1692
1827
  ),
1693
1828
  React.createElement(csTextarea, {
1694
- value: draft !== null ? draft.fimDisableGlobs : loaded.fimDisableGlobs,
1695
- disabled: snapshot.writable !== true,
1829
+ value: view.values.fimDisableGlobs,
1830
+ disabled: !writable,
1696
1831
  placeholder: '例如 *.md;**/dist/**;vendor/**',
1697
- onChange: function (v) { setDraft(function (prev) { return Object.assign({}, prev !== null ? prev : loaded, { fimDisableGlobs: v }) }); setFailed(false) },
1832
+ onChange: edit('fimDisableGlobs'),
1698
1833
  }),
1699
1834
  React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 4 } },
1700
1835
  '留空 = 不禁用。语法:`*` 不跨目录、`**` 跨目录、不含 `/` 的模式只匹配文件名、`/` 结尾视作 `/**`。'
@@ -1704,59 +1839,271 @@ let React = require('react')
1704
1839
  React.createElement('div', { className: 'dshcs-field' },
1705
1840
  React.createElement('div', { className: 'dshcs-fieldHead' },
1706
1841
  React.createElement('span', { className: 'dshcs-fieldLabel' }, '后台常驻(切标签不重载)'),
1707
- overriddenKeep === true
1708
- ? React.createElement(csBadges, {
1709
- overridden: true, disabled: snapshot.writable !== true,
1710
- overriddenLabel: '已覆盖', resetLabel: '恢复默认',
1711
- onReset: function () { doReset() },
1712
- })
1713
- : null
1842
+ view.overridden.keepResident === true ? reset('keepResident') : null
1714
1843
  ),
1715
1844
  React.createElement(csCheck, {
1716
- checked: draft !== null ? draft.keepResident : loaded.keepResident,
1717
- disabled: snapshot.writable !== true,
1718
- onChange: function (v) { setDraft(function (prev) { return Object.assign({}, prev !== null ? prev : loaded, { keepResident: v === true }) }); setFailed(false) },
1845
+ checked: view.values.keepResident,
1846
+ disabled: !writable,
1847
+ onChange: edit('keepResident'),
1719
1848
  }, '开启后宿主启动即把 IDE 加载到后台"停放区":切换右侧栏标签、收起/展开侧栏、拖成浮动窗口都不再重载,首次打开免等待;关闭则只在打开面板时加载(省内存)'),
1720
- React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 4 } },
1721
- supportsMoveBefore
1722
- ? '常驻面:' + (surfaceState.docked ? '已停靠' : (surfaceState.ready ? '已停放(后台运行中)' : '未启动'))
1723
- + (surfaceState.degraded ? ' · 本次发生过降级重载' : '')
1724
- : '当前浏览器不支持 Element.moveBefore(Chromium <133)→ 常驻不可用,切标签仍会整页重载(升级浏览器后自动生效)')
1725
- )
1849
+ React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 4 } }, extras.surfaceHint)
1850
+ ),
1726
1851
  ]
1727
- // ---- 插件页的配置区(DSH ≥ 0.1.6-alpha.2)----
1728
- // 上游把插件设置从"设置 → 插件"搬到**插件页**:页面自己画标题/图标/面包屑,把
1729
- // `plugins.bundle.config`(按包名为键)渲染在描述与行之间,只问 `view: 'page'`。
1730
- // 所以这里不再套可折叠卡片(标题会与页面标题重复),只出表单 + 自己的保存控件
1731
- // (契约原文:the form with its own save control)。
1732
- if (props.seat === 'bundle-config') {
1852
+ }
1853
+
1854
+ /** 插件页配置区的外壳(0.3.50):页面自己画标题/图标/面包屑,这里只排表单 + 保存控件。 */
1855
+ function csConfigPage(view, handlers, fields) {
1856
+ return React.createElement('div', { className: 'dshcs-cfgpage', 'data-code-server-config': 'page' },
1857
+ view.writable !== true
1858
+ ? React.createElement('p', { className: 'dshcs-cardReadOnly', role: 'status' }, CS_READONLY_LABEL)
1859
+ : null,
1860
+ ...fields,
1861
+ view.failed === true
1862
+ ? React.createElement('p', { className: 'dshcs-cardFailed', role: 'status' }, CS_SAVE_FAILED_LABEL)
1863
+ : null,
1864
+ React.createElement('div', { className: 'dshcs-cfgfoot' },
1865
+ React.createElement(csBtn, {
1866
+ disabled: view.dirty !== true || view.saving === true,
1867
+ onClick: handlers.onDiscard,
1868
+ }, CS_DISCARD_LABEL),
1869
+ React.createElement(csBtn, {
1870
+ variant: 'primary',
1871
+ disabled: view.dirty !== true || view.invalid === true || view.saving === true,
1872
+ onClick: handlers.onSave,
1873
+ }, view.saving === true ? CS_SAVING_LABEL : CS_SAVE_LABEL)
1874
+ )
1875
+ )
1876
+ }
1877
+
1878
+ /** 旧版 DSH(无右侧栏服务)的提示:两个座位各自的外壳 + 同一段文案。 */
1879
+ function csLegacyNotice(seat) {
1880
+ var body = React.createElement('div', { className: 'dshcs-field' },
1881
+ React.createElement('div', { className: 'dshcs-legacy' }, CS_LEGACY_LEAD),
1882
+ React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 0 } }, CS_LEGACY_BODY)
1883
+ )
1884
+ if (seat === 'page') {
1733
1885
  return React.createElement('div', { className: 'dshcs-cfgpage', 'data-code-server-config': 'page' },
1734
- snapshot.writable !== true
1735
- ? React.createElement('p', { className: 'dshcs-cardReadOnly', role: 'status' }, '本部署的设置为只读。')
1736
- : null,
1737
- ...fields,
1738
- failed === true
1739
- ? React.createElement('p', { className: 'dshcs-cardFailed', role: 'status' }, '本部署没有接受这些值,已保留供你修改。')
1740
- : null,
1741
- React.createElement('div', { className: 'dshcs-cfgfoot' },
1742
- React.createElement(csBtn, {
1743
- disabled: !dirty || saving,
1744
- onClick: function () { setDraft(null); setFailed(false) },
1745
- }, '放弃修改'),
1746
- React.createElement(csBtn, {
1747
- variant: 'primary', disabled: saveDisabled, onClick: function () { doSave() },
1748
- }, saving === true ? '保存中…' : '保存')
1749
- )
1886
+ React.createElement('p', { className: 'dshcs-cardReadOnly', role: 'status' }, CS_LEGACY_DESC),
1887
+ body
1750
1888
  )
1751
1889
  }
1752
1890
  return React.createElement(csCard, {
1753
- title: 'Code Server',
1754
- description: '入口:右侧栏标签(DSH ≥ 0.1.5-alpha.1);认领类型决定哪些文件交给 Code Server 打开',
1891
+ title: CS_LEGACY_TITLE,
1892
+ description: CS_LEGACY_DESC,
1893
+ defaultOpen: true,
1894
+ noticeOnly: true,
1895
+ state: { available: true, writable: false, dirty: false, invalid: false, saving: false, failed: false },
1896
+ }, body)
1897
+ }
1898
+
1899
+ /**
1900
+ * 新通道的配置页(alpha 线 ≥ 0.1.7-alpha.1;座位 `plugins.bundle.config`,键 = **包名**)。
1901
+ *
1902
+ * 数据面是 `ctx.configForms.get(ENTRY_ID)`,组件经 `props.useCodeServerForm`(壳层按注入面的
1903
+ * `hooks.codeServerForm` 派生成 `use<Key>`)拿投影,写**只**经 `props.edit/resetField/save/discard` ——
1904
+ * 保存最终落到一次原子的 `form.mutate(ops, revision)`。组件里不持有草稿(与旧通道不同)。
1905
+ */
1906
+ function ConfigFormsEntry(props) {
1907
+ // hook 调用次数必须恒定(React 按位置对账):注入面缺失时也走一个"同形但不订阅"的 hook。
1908
+ var useForm = typeof props.useCodeServerForm === 'function' ? props.useCodeServerForm : csAbsentHook
1909
+ var state = useForm(function (snapshot) { return snapshot })
1910
+ var liveStore = useStore()
1911
+ // 常驻面实时状态(与旧通道同一段:两个外壳里的提示要一致)
1912
+ var [surfaceState, setSurfaceState] = React.useState(surfaceSnapshot())
1913
+ React.useEffect(function () {
1914
+ return subscribeSurface(function (snap) { setSurfaceState(snap) })
1915
+ }, [])
1916
+ // 页面离开即丢弃暂存编辑(插件页的同一约定:只有保存才写入)。
1917
+ var discardRef = React.useRef(props.discard)
1918
+ discardRef.current = props.discard
1919
+ React.useEffect(function () {
1920
+ return function () { if (typeof discardRef.current === 'function') discardRef.current() }
1921
+ }, [])
1922
+ if (props.view === 'summary') return CS_CONFIG_SUMMARY
1923
+ if (liveStore != null && liveStore.sidebarUi === 'legacy') return csLegacyNotice('page')
1924
+ if (state === null || state === undefined || state.available !== true) {
1925
+ // 条目未加载 / 配置还没到:官方插槽边界接管,这里给一句能读懂的话(不显示占位骨架)。
1926
+ return React.createElement('p', { className: 'dshcs-cardReadOnly', role: 'status' },
1927
+ '宿主还没有提供本插件的配置(条目未加载或仍在加载),暂时无法编辑。')
1928
+ }
1929
+ var view = {
1930
+ available: true,
1931
+ writable: state.writable === true,
1932
+ values: state.values,
1933
+ overridden: state.overridden,
1934
+ dirty: state.dirty === true,
1935
+ invalid: state.invalid === true,
1936
+ saving: state.saving === true,
1937
+ failed: state.failed === true,
1938
+ }
1939
+ var handlers = {
1940
+ onEdit: function (field, v) { if (typeof props.edit === 'function') props.edit(field, v) },
1941
+ onResetField: function (field) { if (typeof props.resetField === 'function') props.resetField(field) },
1942
+ onSave: function () {
1943
+ if (typeof props.save !== 'function') return
1944
+ // 保存成功后宿主状态(serve/常驻/认领类型)会变:立刻拉一次,别等下一趟轮询。
1945
+ var out = props.save()
1946
+ if (out !== null && out !== undefined && typeof out.then === 'function') {
1947
+ out.then(function (ok) { if (ok === true) csSyncStatusToStore() }, function () { /* 失败已由控制器记账 */ })
1948
+ }
1949
+ },
1950
+ onDiscard: function () { if (typeof props.discard === 'function') props.discard() },
1951
+ }
1952
+ return csConfigPage(view, handlers, csSettingsFields(view, handlers, { surfaceHint: csSurfaceHintText(surfaceState) }))
1953
+ }
1954
+
1955
+ /** 设置卡片页面:useState+useEffect 订阅 settingsScope(不投机 useSyncExternalStore
1956
+ * 对 snapshot 引用稳定性的要求;不建中间 store)。 */
1957
+ function csSettingsPage(props) {
1958
+ try {
1959
+ var scope = props.scope
1960
+ var [snapshot, setSnapshot] = React.useState(function () { return scope !== undefined ? scope.getSnapshot() : null })
1961
+ var [draft, setDraft] = React.useState(null) // null | { keepResident, fileOpenScope }(未保存草稿)
1962
+ var [saving, setSaving] = React.useState(false)
1963
+ var [failed, setFailed] = React.useState(false)
1964
+ // 共享 store:读取当前 UI 载体状态(右侧栏是否就绪 / 是否旧版 DSH),必须放在所有提前 return 之前
1965
+ var liveStore = useStore()
1966
+ // 常驻面实时状态(停靠/停放/降级):卡片里显示,便于判断"切标签是否还会重载"
1967
+ var [surfaceState, setSurfaceState] = React.useState(surfaceSnapshot())
1968
+ React.useEffect(function () {
1969
+ return subscribeSurface(function (snap) { setSurfaceState(snap) })
1970
+ }, [])
1971
+ React.useEffect(function () {
1972
+ if (scope === undefined || typeof scope.subscribe !== 'function') return
1973
+ function onUpdate() {
1974
+ try { setSnapshot(scope.getSnapshot()) } catch (e) { /* ignore */ }
1975
+ }
1976
+ var dispose = scope.subscribe(onUpdate)
1977
+ return function () { if (typeof dispose === 'function') dispose() }
1978
+ }, [scope])
1979
+ if (scope === undefined) {
1980
+ console.error('[code-server] card scope missing')
1981
+ return null
1982
+ }
1983
+ if (snapshot === null || snapshot === undefined || snapshot.status !== 'ready') {
1984
+ // 尚未就绪(连接/镜像加载中):静默留空,官方插槽边界接管;不显示占位
1985
+ if (snapshot !== null && snapshot !== undefined && snapshot.status === 'unavailable') {
1986
+ console.warn('[code-server] settings scope unavailable:', snapshot.status)
1987
+ }
1988
+ return null
1989
+ }
1990
+ var value = snapshot.value !== undefined && snapshot.value !== null ? snapshot.value : {}
1991
+ var user = snapshot.user
1992
+ // 当前生效值(每个字段的兜底与宿主 schema 默认值同源,见 CS_FIELDS.read)。
1993
+ var loaded = {}
1994
+ CS_FIELDS.forEach(function (spec) { loaded[spec.field] = spec.read(value) })
1995
+ // ---- 旧版 DSH(无右侧栏服务):本页是唯一的提示出口,不显示任何设置项 ----
1996
+ if (liveStore != null && liveStore.sidebarUi === 'legacy') {
1997
+ return csLegacyNotice('card')
1998
+ }
1999
+ // 归一化视图:新通道那份由 configForms 的投影直接给出(形状与此完全相同)⇒ 表单主体只认这一层。
2000
+ var view = {
2001
+ available: true,
2002
+ writable: snapshot.writable === true,
2003
+ values: {},
2004
+ overridden: {},
2005
+ }
2006
+ CS_FIELDS.forEach(function (spec) {
2007
+ view.values[spec.field] = draft !== null ? draft[spec.field] : loaded[spec.field]
2008
+ view.overridden[spec.field] = csOverridden(user, spec.field)
2009
+ })
2010
+ view.dirty = draft !== null && CS_FIELDS.some(function (spec) {
2011
+ return !spec.same(spec.parse(spec.display(view.values[spec.field])), loaded[spec.field])
2012
+ })
2013
+ view.invalid = false
2014
+ view.saving = saving
2015
+ view.failed = failed
2016
+ var saveDisabled = view.dirty !== true || saving
2017
+ var state = {
2018
+ available: true,
2019
+ writable: view.writable,
2020
+ dirty: view.dirty,
2021
+ invalid: view.invalid,
2022
+ saving: view.saving,
2023
+ failed: view.failed,
2024
+ }
2025
+ // 保存/恢复成功后,把 host 最新 status 推给模块共享 store → 常驻预热等立即按新设置生效。
2026
+ async function doSave() {
2027
+ if (view.dirty && !saving) {
2028
+ setSaving(true); setFailed(false)
2029
+ try {
2030
+ var d = draft !== null ? draft : loaded
2031
+ await props.scope.set('keepResident', d.keepResident === true)
2032
+ await props.scope.set('claimExtensions', normalizeClaimExtensions(d.claimExtensions))
2033
+ await props.scope.set('fullscreenOnOpen', d.fullscreenOnOpen === true)
2034
+ await props.scope.set('fim', d.fim === true)
2035
+ await props.scope.set('fimDebounceMs', normalizeFimDebounce(d.fimDebounceMs))
2036
+ await props.scope.set('fimMultiline', d.fimMultiline === true)
2037
+ await props.scope.set('fimDisableGlobs', typeof d.fimDisableGlobs === 'string' ? d.fimDisableGlobs : '')
2038
+ setDraft(null)
2039
+ csSyncStatusToStore()
2040
+ } catch (e) {
2041
+ setFailed(true)
2042
+ }
2043
+ setSaving(false)
2044
+ }
2045
+ }
2046
+ async function doReset() {
2047
+ if (saving || snapshot.writable !== true) return
2048
+ setSaving(true); setFailed(false)
2049
+ try {
2050
+ // unset 清除 user 覆盖层 → 回到 base 默认值;成功后镜像同步
2051
+ if (typeof props.scope.unset === 'function') {
2052
+ await props.scope.unset('keepResident')
2053
+ await props.scope.unset('claimExtensions')
2054
+ await props.scope.unset('fullscreenOnOpen')
2055
+ await props.scope.unset('fim')
2056
+ await props.scope.unset('fimDebounceMs')
2057
+ await props.scope.unset('fimMultiline')
2058
+ await props.scope.unset('fimDisableGlobs')
2059
+ } else {
2060
+ await props.scope.set('keepResident', value.keepResident === undefined || value.keepResident === null ? true : value.keepResident)
2061
+ await props.scope.set('claimExtensions', typeof value.claimExtensions === 'string' ? normalizeClaimExtensions(value.claimExtensions) : DEFAULT_CLAIM_EXTENSIONS)
2062
+ await props.scope.set('fullscreenOnOpen', value.fullscreenOnOpen === undefined || value.fullscreenOnOpen === null ? true : value.fullscreenOnOpen)
2063
+ await props.scope.set('fim', value.fim === true)
2064
+ await props.scope.set('fimDebounceMs', normalizeFimDebounce(value.fimDebounceMs))
2065
+ await props.scope.set('fimMultiline', value.fimMultiline !== false)
2066
+ await props.scope.set('fimDisableGlobs', typeof value.fimDisableGlobs === 'string' ? value.fimDisableGlobs : '')
2067
+ }
2068
+ setDraft(null)
2069
+ csSyncStatusToStore()
2070
+ } catch (e) {
2071
+ setFailed(true)
2072
+ }
2073
+ setSaving(false)
2074
+ }
2075
+ /** 旧通道的处理器:草稿仍在组件里(旧通道没有 mutate,只能逐字段 set/unset)。 */
2076
+ var handlers = {
2077
+ onEdit: function (field, next) {
2078
+ setDraft(function (prev) {
2079
+ var base = Object.assign({}, prev !== null ? prev : loaded)
2080
+ base[field] = next
2081
+ return base
2082
+ })
2083
+ setFailed(false)
2084
+ },
2085
+ // 旧通道的"恢复默认"是**整份** unset(0.3.x 起的行为);新通道按官方契约是逐字段 unset。
2086
+ onResetField: function () { doReset() },
2087
+ onSave: function () { doSave() },
2088
+ onDiscard: function () { setDraft(null); setFailed(false) },
2089
+ }
2090
+ var fields = csSettingsFields(view, handlers, { surfaceHint: csSurfaceHintText(surfaceState) })
2091
+ // ---- 插件页的配置区(alpha 线 ≥ 0.1.6-alpha.2):页面自己画标题/图标/面包屑,
2092
+ // 把 `plugins.bundle.config`(按包名为键)渲染在描述与行之间,只问 `view: 'page'`;
2093
+ // 所以这里不再套可折叠卡片(标题会与页面标题重复),只出表单 + 自己的保存控件。
2094
+ // 注意:0.1.6-alpha.2 的**座位**已是这一条,而**数据通道**还是旧的 settingsScope ⇒
2095
+ // 同一个 seat 必须两种通道都答得起(新通道那份见 ConfigFormsEntry)。
2096
+ if (props.seat === 'bundle-config') {
2097
+ return csConfigPage(view, handlers, fields)
2098
+ }
2099
+ return React.createElement(csCard, {
2100
+ title: CS_LEGACY_TITLE,
2101
+ description: '入口:右侧栏标签(需 DSH 带 sidebarRight/sidebarRightTabs,即 rc 线或 0.1.6-alpha.2 起的 alpha 线);认领类型决定哪些文件交给 Code Server 打开',
1755
2102
  state: state,
1756
- unsavedLabel: '未保存', readOnlyLabel: '本部署的设置为只读。',
1757
- saveFailedLabel: '本部署没有接受这些值,已保留供你修改。',
1758
- discardLabel: '放弃修改', saveLabel: '保存', savingLabel: '保存中…',
1759
- onSave: doSave, onDiscard: function () { setDraft(null); setFailed(false) },
2103
+ unsavedLabel: CS_UNSAVED_LABEL, readOnlyLabel: CS_READONLY_LABEL,
2104
+ saveFailedLabel: CS_SAVE_FAILED_LABEL,
2105
+ discardLabel: CS_DISCARD_LABEL, saveLabel: CS_SAVE_LABEL, savingLabel: CS_SAVING_LABEL,
2106
+ onSave: handlers.onSave, onDiscard: handlers.onDiscard,
1760
2107
  }, ...fields)
1761
2108
  } catch (e) {
1762
2109
  // 渲染异常:记日志不打断;官方插槽对异常有边界,卡片留空即可
@@ -1765,7 +2112,9 @@ let React = require('react')
1765
2112
  }
1766
2113
  }
1767
2114
 
1768
- /** 插件页配置区的入口组件(0.3.50)。
2115
+ /** 插件页配置区的入口组件(**旧通道**;0.3.50 起)。
2116
+ * 用它的场合:座位是 `plugins.bundle.config` 但数据面还是 `settingsScope` —— 也就是
2117
+ * rc 线之外**还包括 0.1.6-alpha.2**(那一版座位已搬、通道未换)。新通道见 ConfigFormsEntry。
1769
2118
  * 上游契约:同一个 entry 会被要两种视图 —— `summary`(标题下的一句话)与 `page`(表单,
1770
2119
  * 自带保存控件)。插件页对 `plugins.bundle.config` 只用 `page`,但两种都答得起才算合格。
1771
2120
  * 本组件自身不调 hook(两个分支都不调),真正的设置表单在 csSettingsPage 里(独立组件边界)。 */
@@ -2844,36 +3193,76 @@ let React = require('react')
2844
3193
  console.error('[code-server] slots service unavailable')
2845
3194
  return
2846
3195
  }
2847
- // settings scope 提前绑定(设置卡共用)
2848
- var settingsScope = ctx.settingsScope
2849
- var scope = null
2850
- if (settingsScope !== undefined && typeof settingsScope.bind === 'function') {
2851
- scope = settingsScope.bind({ namespace: 'code-server' })
2852
- } else {
2853
- console.error('[code-server] settingsScope unavailable (inject missing?); settings card disabled')
2854
- return
2855
- }
2856
- // 预拉取 status(设置卡/预热/侧栏 body 共用)
3196
+ // 预拉取 status(设置卡/预热/侧栏 body 共用)。**放在通道探测之前**:0.1.7-alpha.1 上
3197
+ // 旧通道(settingsScope)已经不存在,配置区只是"少一块",侧栏标签/常驻预热必须照旧起来
3198
+ // (0.3.50~0.3.65 的写法在这里提前 return,把整个右侧栏一起带走了 —— 那就是线上那次
3199
+ // "Failed to load plugins / 1 entry did not activate" 之后的第二层故障)。
2857
3200
  api('/code-server/status').then(function (s) {
2858
3201
  setState({ status: s })
2859
3202
  }).catch(function () { /* 首次失败由后续轮询补救 */ })
2860
3203
 
2861
- // ---- 设置:两条腿都注册,谁被声明谁生效(两次注册不会同时出现) ----
2862
- // ① 插件页的配置区(DSH ≥ 0.1.6-alpha.2):`settings.plugin.item` 退役,上游把插件配置搬到
2863
- // 插件页 —— 键是**包名**(页面按 `pkg.name` 派发),只问 `view: 'page'`。插件页只在
2864
- // `ledger.bundles.has(包名)` 时才渲染那一块,所以键写错就等于"设置又不见了"。
2865
- slots.inject('plugins.bundle.config', () => slots.register(
2866
- { name: 'plugins.bundle.config', key: PKG_NAME, inject: function () { return { scope: scope } } },
2867
- BundleConfigEntry
2868
- ))
2869
- // ② 设置页里的插件卡片(DSH ≤ 0.1.6-alpha.1;也是极旧 DSH 里唯一的提示出口)。
2870
- // `slots.inject` 只在插槽真被声明时才回调 ⇒ 新 DSH 上这一条自然不生效。
2871
- slots.inject('settings.plugin.item', () => slots.register(
2872
- { name: 'settings.plugin.item', key: 'code-server', label: 'Code Server', inject: function () { return { scope: scope } } },
2873
- csSettingsPage
2874
- ))
2875
-
2876
- // ---- 能力探测:只有带右侧栏服务(DSH ≥ 0.1.5-alpha.1)才注册可用 UI;探测不到就只留设置页提示 ----
3204
+ // ---- 设置:两个**座位** × 两条**数据通道**(见本段开头的说明) ----
3205
+ // 通道用能力探测,座位用声明驱动:`slots.inject` 只在插槽真被声明时才回调 ⇒ 两条腿都注册。
3206
+ // 探测只经 `ctx.get`:这两个服务**不能**写进静态 inject(写进去会让另一条线上的条目永远 pending),
3207
+ // 而未声明的服务在 DSH 里**属性访问会抛** —— 所以这里绝不用 ctx.configForms / ctx.settingsScope。
3208
+ var formsSvc = ctx.get('configForms')
3209
+ var scope = null
3210
+ if (formsSvc !== undefined && formsSvc !== null && typeof formsSvc.get === 'function') {
3211
+ // ① 新通道(alpha 线 ≥ 0.1.7-alpha.1):配置是本条目自己的 volatile Config。
3212
+ var form = formsSvc.get(CS_ENTRY_ID)
3213
+ var controller = csConfigFormController(form)
3214
+ var face = null
3215
+ var store = controller.bind()
3216
+ if (store !== null) {
3217
+ face = Object.assign({ hooks: { codeServerForm: store } }, controller.actions())
3218
+ } else {
3219
+ console.warn('[code-server] 快照 store 不可用:配置区退回旧通道(没有就只剩提示)')
3220
+ }
3221
+ // 表单订阅属于本 fiber:插件卸载/热重载时释放,避免监听泄漏到服务缓存的表单上。
3222
+ ctx.effect(function () { return function () { controller.dispose() } }, 'code-server: config form subscription')
3223
+ if (face !== null) {
3224
+ // `whileServed` 是**门禁**:宿主只有在服务该条目配置时才让我们注册页面
3225
+ // (没加载这个插件的部署看不到任何痕迹;宿主不再服务时自动撤下)。
3226
+ ctx.effect(function () {
3227
+ return formsSvc.whileServed([CS_ENTRY_ID], function () {
3228
+ return slots.inject('plugins.bundle.config', function () {
3229
+ return slots.register(
3230
+ { name: 'plugins.bundle.config', key: PKG_NAME, inject: function () { return face } },
3231
+ ConfigFormsEntry
3232
+ )
3233
+ })
3234
+ })
3235
+ }, 'code-server: plugins.bundle.config page (configForms)')
3236
+ console.log('[code-server] client config page registered: plugins.bundle.config[' + PKG_NAME + '] via configForms')
3237
+ }
3238
+ } else {
3239
+ // ② 旧通道(rc 0.1.5-rc.x;**也含 0.1.6-alpha.2** —— 那一版座位已搬到插件页,数据面还是旧的)。
3240
+ var settingsScope = ctx.get('settingsScope')
3241
+ if (settingsScope !== undefined && settingsScope !== null && typeof settingsScope.bind === 'function') {
3242
+ scope = settingsScope.bind({ namespace: CS_ENTRY_ID })
3243
+ }
3244
+ if (scope === null) {
3245
+ console.error('[code-server] 没有可用的配置数据通道(configForms / settingsScope 都不可见):配置区不注册')
3246
+ }
3247
+ if (scope !== null) {
3248
+ // ②-a 插件页的配置区(alpha 线 ≥ 0.1.6-alpha.2):`settings.plugin.item` 退役,上游把插件配置搬到
3249
+ // 插件页 —— 键是**包名**(页面按 `pkg.name` 派发),只问 `view: 'page'`。插件页只在
3250
+ // `ledger.bundles.has(包名)` 时才渲染那一块,所以键写错就等于"设置又不见了"。
3251
+ // 0.1.6-alpha.2 用的就是这一条(座位新、通道旧)。
3252
+ slots.inject('plugins.bundle.config', () => slots.register(
3253
+ { name: 'plugins.bundle.config', key: PKG_NAME, inject: function () { return { scope: scope } } },
3254
+ BundleConfigEntry
3255
+ ))
3256
+ // ②-b 设置页里的插件卡片(rc 线 ≤ 0.1.5-rc.3,最新 rc 仍在用;也是没有右侧栏服务的旧 DSH 里唯一的提示出口)。
3257
+ // `slots.inject` 只在插槽真被声明时才回调 ⇒ 新 DSH 上这一条自然不生效。
3258
+ slots.inject('settings.plugin.item', () => slots.register(
3259
+ { name: 'settings.plugin.item', key: CS_ENTRY_ID, label: 'Code Server', inject: function () { return { scope: scope } } },
3260
+ csSettingsPage
3261
+ ))
3262
+ }
3263
+ }
3264
+
3265
+ // ---- 能力探测:只有带右侧栏服务(sidebarRightTabs/sidebarRight;rc 线与 alpha 线都有)才注册可用 UI;探测不到就只留设置页提示 ----
2877
3266
  var modernSettled = false
2878
3267
 
2879
3268
  function onModernUi(sctx, via) {
@@ -2964,7 +3353,10 @@ let React = require('react')
2964
3353
  }, LEGACY_REPORT_MS)
2965
3354
  }
2966
3355
 
2967
- const inject = ['slots', 'settingsScope']
3356
+ // 静态 inject 只列**两条线都必然存在**的服务。`configForms` / `settingsScope` 都是
3357
+ // **运行时探测**的:把它们写进 inject,客户端条目会在没有那个服务的 DSH 上永远 pending
3358
+ // (0.1.7-alpha.1 上 `settingsScope` 已被删除 ⇒ 整个条目不激活、右侧栏与设置一起消失)。
3359
+ const inject = ['slots']
2968
3360
  const name = 'code-server'
2969
3361
 
2970
3362
  // 入口导出(与原 `export { apply, inject, name }` 等价;banner/footer 的 CJS 形态要求)