dsh-code-server-app 0.3.59 → 0.3.65
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/LICENSE-Apache-2.0.txt +201 -0
- package/README.en.md +100 -16
- package/README.md +79 -16
- package/THIRD_PARTY_NOTICES.md +60 -0
- package/assets/extensions/dshcs-editor-bridge/extension.js +149 -2
- package/assets/extensions/dshcs-editor-bridge/lib/bridge-client.js +38 -2
- package/assets/extensions/dshcs-editor-bridge/lib/fim-completion.js +264 -0
- package/assets/extensions/dshcs-editor-bridge/package.json +2 -2
- package/lib/bridge.mjs +6 -3
- package/lib/client.js +163 -19
- package/lib/fim-adapter.mjs +603 -0
- package/lib/index.js +294 -1
- package/package.json +17 -14
- package/vendor/VENDOR.json +5 -5
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// dshcs-editor-bridge / lib/fim-completion.js —— 纯逻辑:FIM 补全的请求决策与载荷构造
|
|
2
|
+
//
|
|
3
|
+
// 来源与许可:本文件**为独立实现,不含 continuedev/continue 的源码**;其门控/窗口/缓存的设计与
|
|
4
|
+
// 设置项划分**参考**了该项目(Apache License 2.0, Copyright 2023 Continue)。详见 `THIRD_PARTY_NOTICES.md`。
|
|
5
|
+
//
|
|
6
|
+
// **不 require('vscode')**:入参都是纯文本/数字,所以能在没有 VS Code 的环境里单测
|
|
7
|
+
// (scripts/test-fim.mjs 就是这么用的)。与编辑器交互的部分在 extension.js(glue)里。
|
|
8
|
+
//
|
|
9
|
+
// 这个文件回答三个问题(顺序就是补全一次要过的三关):
|
|
10
|
+
// 1. **该不该问**(`shouldRequest`)—— 模型在被问到"不该补的地方"时会硬凑(实测 2/2),
|
|
11
|
+
// 所以"不值得问"必须在**发请求之前**判掉,而不是拿到回答再丢;
|
|
12
|
+
// 2. **问什么**(`buildPayload`)—— 只送光标附近的窗口,不是整份文件;
|
|
13
|
+
// 3. **问过没有**(`createCompletionCache`)—— 同样的前后文重复问是纯浪费(实测 temperature=0
|
|
14
|
+
// 下两次回答逐字相同 ⇒ 缓存命中率会很高)。
|
|
15
|
+
//
|
|
16
|
+
// 窗口常量与宿主侧 `lib/fim-adapter.mjs` **同名同值**:扩展是随包分发的静态文件、不能 import
|
|
17
|
+
// 宿主代码,所以一致性靠 scripts/test-fim.mjs 的断言钉住(与 bridge-client.js 的桥常量同一套做法)。
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
/** 与宿主 lib/fim-adapter.mjs 保持一致(改一边就要改另一边,测试会拦)。 */
|
|
22
|
+
const FIM_MAX_PREFIX_CHARS = 6000;
|
|
23
|
+
const FIM_MAX_SUFFIX_CHARS = 2000;
|
|
24
|
+
const FIM_MAX_PREFIX_LINES = 120;
|
|
25
|
+
const FIM_MAX_SUFFIX_LINES = 40;
|
|
26
|
+
|
|
27
|
+
/** 停顿多久才真的发请求(毫秒)。打字过程中的请求全是浪费,而且会被宿主的速率闸挡掉。 */
|
|
28
|
+
const FIM_DEBOUNCE_MS = 250;
|
|
29
|
+
|
|
30
|
+
/** 超过这个行数的文档不参与补全(大文件里补全的收益低;用行数而不是字符数是为了避免
|
|
31
|
+
* 每次击键都 `document.getText()` 把整份文档摸一遍)。 */
|
|
32
|
+
const FIM_MAX_DOCUMENT_LINES = 20000;
|
|
33
|
+
|
|
34
|
+
/** 缓存条目上限与有效期(键 = 前后文指纹;命中直接给结果,不再问模型)。 */
|
|
35
|
+
const FIM_CACHE_MAX = 24;
|
|
36
|
+
const FIM_CACHE_TTL_MS = 120000;
|
|
37
|
+
|
|
38
|
+
/** 把文本裁到窗口内(与宿主侧 `trimWindow` 同语义:字符与行数取先到者)。 */
|
|
39
|
+
function trimWindow(text, maxChars, maxLines, fromEnd) {
|
|
40
|
+
if (typeof text !== 'string' || text === '') return '';
|
|
41
|
+
let out = text;
|
|
42
|
+
if (maxLines > 0) {
|
|
43
|
+
const split = out.split('\n');
|
|
44
|
+
if (split.length > maxLines) out = (fromEnd ? split.slice(split.length - maxLines) : split.slice(0, maxLines)).join('\n');
|
|
45
|
+
}
|
|
46
|
+
if (maxChars > 0 && out.length > maxChars) out = fromEnd ? out.slice(out.length - maxChars) : out.slice(0, maxChars);
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 该不该为这次光标位置请求补全。
|
|
52
|
+
*
|
|
53
|
+
* 判据都是"**便宜且不会错判**"的那些 —— 宁可不补,也不要补错(补错一次,用户就关掉它)。
|
|
54
|
+
*
|
|
55
|
+
* @param {{selectionEmpty: boolean, scheme: string, lineCount: number,
|
|
56
|
+
* prefixText: string, suffixText: string, lineTextBefore: string}} input
|
|
57
|
+
* @returns {{ask: boolean, reason: string}}
|
|
58
|
+
*/
|
|
59
|
+
function shouldRequest(input) {
|
|
60
|
+
const value = input === null || input === undefined ? {} : input;
|
|
61
|
+
if (value.selectionEmpty !== true) return { ask: false, reason: '有选区(用户在选中内容,不是在打字)' };
|
|
62
|
+
if (value.scheme !== 'file') return { ask: false, reason: `非文件文档(${value.scheme ?? '未知'})` };
|
|
63
|
+
if (!(Number.isFinite(value.lineCount) && value.lineCount > 0)) return { ask: false, reason: '空文档' };
|
|
64
|
+
if (value.lineCount > FIM_MAX_DOCUMENT_LINES) return { ask: false, reason: '文档过大' };
|
|
65
|
+
if (typeof value.prefixText !== 'string' || typeof value.suffixText !== 'string') return { ask: false, reason: '拿不到前后文' };
|
|
66
|
+
// 光标前整行都是空白、且前面也没有内容 ⇒ 没什么可续的(新建的空文件、文件开头按回车)。
|
|
67
|
+
if (typeof value.lineTextBefore === 'string' && value.lineTextBefore.trim() === '') {
|
|
68
|
+
if (value.prefixText.trim() === '' && value.suffixText.trim() === '') return { ask: false, reason: '空上下文' };
|
|
69
|
+
}
|
|
70
|
+
return { ask: true, reason: 'ok' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 前后文的指纹(缓存键):只看窗口内的内容,与路径/语言一起构成键。 */
|
|
74
|
+
function fingerprint(prefix, suffix, language) {
|
|
75
|
+
const text = `${language ?? ''}\u0000${prefix ?? ''}\u0000${suffix ?? ''}`;
|
|
76
|
+
let hash = 2166136261;
|
|
77
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
78
|
+
hash ^= text.charCodeAt(i);
|
|
79
|
+
hash = Math.imul(hash, 16777619);
|
|
80
|
+
}
|
|
81
|
+
return `${text.length}:${(hash >>> 0).toString(36)}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 构造发往宿主的载荷(只含窗口内的前后文)。
|
|
86
|
+
* @returns {{prompt: string, suffix: string, language: string, path: string}}
|
|
87
|
+
*/
|
|
88
|
+
function buildPayload({ prefixText, suffixText, language, path }) {
|
|
89
|
+
return {
|
|
90
|
+
prompt: trimWindow(prefixText ?? '', FIM_MAX_PREFIX_CHARS, FIM_MAX_PREFIX_LINES, true),
|
|
91
|
+
suffix: trimWindow(suffixText ?? '', FIM_MAX_SUFFIX_CHARS, FIM_MAX_SUFFIX_LINES, false),
|
|
92
|
+
language: typeof language === 'string' ? language : '',
|
|
93
|
+
path: typeof path === 'string' ? path : '',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 有界 + 带 TTL 的结果缓存。 */
|
|
98
|
+
function createCompletionCache({ max = FIM_CACHE_MAX, ttlMs = FIM_CACHE_TTL_MS, now = () => Date.now() } = {}) {
|
|
99
|
+
const store = new Map();
|
|
100
|
+
return {
|
|
101
|
+
get(key) {
|
|
102
|
+
const hit = store.get(key);
|
|
103
|
+
if (hit === undefined) return null;
|
|
104
|
+
if (now() - hit.at > ttlMs) {
|
|
105
|
+
store.delete(key);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
store.delete(key);
|
|
109
|
+
store.set(key, hit);
|
|
110
|
+
return hit.text;
|
|
111
|
+
},
|
|
112
|
+
set(key, text) {
|
|
113
|
+
if (typeof key !== 'string' || key === '' || typeof text !== 'string') return false;
|
|
114
|
+
store.delete(key);
|
|
115
|
+
store.set(key, { text, at: now() });
|
|
116
|
+
while (store.size > max) {
|
|
117
|
+
const oldest = store.keys().next();
|
|
118
|
+
if (oldest.done === true) break;
|
|
119
|
+
store.delete(oldest.value);
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
},
|
|
123
|
+
size() {
|
|
124
|
+
return store.size;
|
|
125
|
+
},
|
|
126
|
+
clear() {
|
|
127
|
+
store.clear();
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 数字 → 状态栏用短串(1.2k / 12k / 1.2M)。 */
|
|
133
|
+
function formatTokens(n) {
|
|
134
|
+
if (!Number.isFinite(n) || n <= 0) return '0';
|
|
135
|
+
if (n < 1000) return String(Math.round(n));
|
|
136
|
+
if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
|
|
137
|
+
if (n < 1000000) return `${Math.round(n / 1000)}k`;
|
|
138
|
+
return `${(n / 1000000).toFixed(1)}M`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 状态栏 tooltip 里那段 FIM 说明(纯字符串组装,便于单测)。 */
|
|
142
|
+
function describeFimUsage(fim) {
|
|
143
|
+
if (fim === null || typeof fim !== 'object') return [];
|
|
144
|
+
if (fim.enabled !== true) return ['FIM 补全:已关闭(设置里可开,实验性)'];
|
|
145
|
+
const lines = [
|
|
146
|
+
`FIM 补全(实验性):已开启${fim.available === false ? ` —— 不可用:${fim.reason ?? '原因未知'}` : ''}`,
|
|
147
|
+
`调用 ${fim.calls ?? 0} 次(成功 ${fim.ok ?? 0}/失败 ${fim.failed ?? 0})`,
|
|
148
|
+
`输入 ${formatTokens(fim.inputTokens)} tok · 输出 ${formatTokens(fim.outputTokens)} tok · 缓存命中 ${formatTokens(fim.cacheReadTokens)} tok`,
|
|
149
|
+
`停顿 ${Number.isFinite(fim.debounceMs) ? Math.round(fim.debounceMs) : FIM_DEBOUNCE_DEFAULT_MS}ms · `
|
|
150
|
+
+ `${fim.multiline === false ? '仅单行' : '允许多行'}`
|
|
151
|
+
+ `${Array.isArray(fim.disableGlobs) && fim.disableGlobs.length > 0 ? ` · 禁用 ${fim.disableGlobs.length} 条 glob` : ''}`,
|
|
152
|
+
];
|
|
153
|
+
if (Number.isFinite(fim.lastMs)) lines.push(`最近一次 ${Math.round(fim.lastMs)}ms`);
|
|
154
|
+
if (typeof fim.lastError === 'string' && fim.lastError !== '') lines.push(`最近错误:${fim.lastError}`);
|
|
155
|
+
return lines;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------- 与宿主同语义的两件小事
|
|
159
|
+
//
|
|
160
|
+
// 为什么在这里**再实现一遍**:扩展是随包分发的静态文件,不能 import 宿主代码(ESM/CJS + 树内外),
|
|
161
|
+
// 而这两个判据必须在"发请求之前/收响应之后"本地生效 —— 一致性由 scripts/test-fim.mjs 用同一组
|
|
162
|
+
// 语料对两边做等价断言(与桥常量、窗口常量同一套做法)。
|
|
163
|
+
|
|
164
|
+
/** 与宿主 lib/fim-adapter.mjs 的 FIM_DEBOUNCE_DEFAULT_MS 同值(宿主会在 /sync 里给实际值)。 */
|
|
165
|
+
const FIM_DEBOUNCE_DEFAULT_MS = 250;
|
|
166
|
+
|
|
167
|
+
/** 解析 glob 清单(与宿主 compileGlobList 同语义)。 */
|
|
168
|
+
function compileGlobList(text) {
|
|
169
|
+
if (typeof text !== 'string' || text.trim() === '') return [];
|
|
170
|
+
const seen = new Set();
|
|
171
|
+
const out = [];
|
|
172
|
+
for (const raw of text.split(/[;,\s]+/)) {
|
|
173
|
+
const item = raw.trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
174
|
+
if (item === '' || seen.has(item)) continue;
|
|
175
|
+
seen.add(item);
|
|
176
|
+
out.push(item);
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** glob → 正则(与宿主 globToRegExp 同语义:`*` 不跨目录、`**` 跨目录、不含 `/` 只匹配文件名)。 */
|
|
182
|
+
function globToRegExp(glob) {
|
|
183
|
+
let g = typeof glob === 'string' ? glob.trim().replace(/\\/g, '/').replace(/^\.\//, '') : '';
|
|
184
|
+
if (g === '') return null;
|
|
185
|
+
if (g.endsWith('/')) g += '**';
|
|
186
|
+
const anchored = g.includes('/');
|
|
187
|
+
let out = '';
|
|
188
|
+
for (let i = 0; i < g.length; i += 1) {
|
|
189
|
+
const ch = g[i];
|
|
190
|
+
if (ch === '*') {
|
|
191
|
+
if (g[i + 1] === '*') {
|
|
192
|
+
if (g[i + 2] === '/') {
|
|
193
|
+
out += '(?:.*/)?';
|
|
194
|
+
i += 2;
|
|
195
|
+
} else {
|
|
196
|
+
out += '.*';
|
|
197
|
+
i += 1;
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
out += '[^/]*';
|
|
201
|
+
}
|
|
202
|
+
} else if (ch === '?') {
|
|
203
|
+
out += '[^/]';
|
|
204
|
+
} else {
|
|
205
|
+
out += ch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { re: new RegExp(`^${out}$`), anchored };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** 路径是否命中禁用清单(返回命中的模式或 null)。
|
|
212
|
+
* 含 `/` 的模式按**完整路径与各层尾段**各试一次(与宿主 lib/fim-adapter.mjs 同语义:
|
|
213
|
+
* 用户写 `vendor/**` 的直觉是"任何层级下的 vendor",而绝对路径永远以 `/` 或盘符开头)。 */
|
|
214
|
+
function matchDisabledGlob(filePath, globs) {
|
|
215
|
+
if (typeof filePath !== 'string' || filePath === '') return null;
|
|
216
|
+
const list = Array.isArray(globs) ? globs : compileGlobList(globs);
|
|
217
|
+
if (list.length === 0) return null;
|
|
218
|
+
const full = filePath.replace(/\\/g, '/');
|
|
219
|
+
const slash = full.lastIndexOf('/');
|
|
220
|
+
const base = slash === -1 ? full : full.slice(slash + 1);
|
|
221
|
+
for (const glob of list) {
|
|
222
|
+
const parsed = globToRegExp(glob);
|
|
223
|
+
if (parsed === null) continue;
|
|
224
|
+
if (!parsed.anchored) {
|
|
225
|
+
if (parsed.re.test(base)) return glob;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (parsed.re.test(full)) return glob;
|
|
229
|
+
for (let i = full.indexOf('/'); i !== -1; i = full.indexOf('/', i + 1)) {
|
|
230
|
+
if (parsed.re.test(full.slice(i + 1))) return glob;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** 只保留第一行(宿主在关掉多行时也会做;这里是第二道)。 */
|
|
237
|
+
function toSingleLine(text) {
|
|
238
|
+
if (typeof text !== 'string' || text === '') return '';
|
|
239
|
+
const first = text.split('\n')[0];
|
|
240
|
+
return first.trim() === '' ? '' : first;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = {
|
|
244
|
+
FIM_MAX_PREFIX_CHARS,
|
|
245
|
+
FIM_MAX_SUFFIX_CHARS,
|
|
246
|
+
FIM_MAX_PREFIX_LINES,
|
|
247
|
+
FIM_MAX_SUFFIX_LINES,
|
|
248
|
+
FIM_DEBOUNCE_MS,
|
|
249
|
+
FIM_DEBOUNCE_DEFAULT_MS,
|
|
250
|
+
FIM_MAX_DOCUMENT_LINES,
|
|
251
|
+
FIM_CACHE_MAX,
|
|
252
|
+
FIM_CACHE_TTL_MS,
|
|
253
|
+
trimWindow,
|
|
254
|
+
shouldRequest,
|
|
255
|
+
fingerprint,
|
|
256
|
+
buildPayload,
|
|
257
|
+
createCompletionCache,
|
|
258
|
+
formatTokens,
|
|
259
|
+
describeFimUsage,
|
|
260
|
+
compileGlobList,
|
|
261
|
+
globToRegExp,
|
|
262
|
+
matchDisabledGlob,
|
|
263
|
+
toSingleLine,
|
|
264
|
+
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dshcs-editor-bridge",
|
|
3
3
|
"displayName": "DSH Editor Bridge",
|
|
4
|
-
"description": "
|
|
5
|
-
"version": "0.2.
|
|
4
|
+
"description": "对工作区只读地把编辑器状态(未保存缓冲区、诊断、活动选区)提供给 DSH(不写文件、不执行命令);右键提问打开 DSH 页面里的悬浮对话对话框(0.3.59 起是唯一路径:面板在插件客户端半部,正文/思考/上下文都用页面模块表里的官方渲染器,并可就地回答授权请求;拿不到对话框心跳时只给一条提示)。由 dsh-code-server-app 插件安装,可禁用。另有**实验性**的 FIM 内联补全(灰字续写):默认关闭,在插件设置卡里开启;关闭时本扩展连 provider 都不注册,不会发出任何补全请求。",
|
|
5
|
+
"version": "0.2.20",
|
|
6
6
|
"publisher": "dsh-code-server-app",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"private": true,
|
package/lib/bridge.mjs
CHANGED
|
@@ -31,11 +31,14 @@
|
|
|
31
31
|
*
|
|
32
32
|
* ## 安全不变量(改这个文件之前先读这几条)
|
|
33
33
|
*
|
|
34
|
-
* 1. **`BRIDGE_BASE`(`/code-server-bridge`)
|
|
34
|
+
* 1. **`BRIDGE_BASE`(`/code-server-bridge`)命名空间**是**四条只读路由 + 一条有界的模型调用**:
|
|
35
35
|
* `/health`(无鉴权探活)、`/sync`(上报编辑器状态 + 取事件)、`/old`(取写前原文快照)、
|
|
36
|
-
* `/event`(上报意图,如"请宿主打开对话框")
|
|
36
|
+
* `/event`(上报意图,如"请宿主打开对话框"),以及 **`/complete`**(0.3.61,**实验性 FIM 补全**):
|
|
37
|
+
* 它把光标前后的两段文本交给模型换回一段补全文本 —— **仍然不写文件、不执行命令**,
|
|
38
|
+
* 而且默认关(设置里开启前一律 403),有速率/并发/长度/超时上限(见 lib/index.js 的
|
|
39
|
+
* handleBridgeComplete 与 lib/fim-adapter.mjs)。这里**不允许**出现写文件、改文档、执行命令、
|
|
37
40
|
* 拉起进程的路由 —— `bridge.json` 里那个令牌对本机同用户进程可读,爆炸半径必须封在
|
|
38
|
-
* "泄露编辑器里的信息",绝不能变成任意文件写 / 任意命令执行。
|
|
41
|
+
* "泄露编辑器里的信息 + 替用户花一点补全 token",绝不能变成任意文件写 / 任意命令执行。
|
|
39
42
|
* 提问与授权答复不走这里:它们在 DSH 同源的 `/api/code-server/ask/*` 上,调用方是 DSH 页面里的
|
|
40
43
|
* 插件客户端(吃 DSH 自己的 cookie/Origin 校验)。
|
|
41
44
|
* 2. **授权答复的四条约束**(在 `/api/code-server/ask/approve` 上,缺一条就等于开了任意命令执行的后门,
|
package/lib/client.js
CHANGED
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
* ④ 各段落原来的顶层标识符现在共享同一作用域 —— 新增顶层名字时先搜一遍别撞名
|
|
15
15
|
* (var/function 撞名是**静默覆盖**,不会报错)。
|
|
16
16
|
*
|
|
17
|
+
* DSH 支持范围(2026-09-22 收敛;判据一律是**能力是否被声明**,不做版本号比较):
|
|
18
|
+
* · rc 线 0.1.5-rc.x(最新 0.1.5-rc.3 = npm latest/next)
|
|
19
|
+
* 旧座位 `settings.plugin.item` + 会话列表快照上的 `current`
|
|
20
|
+
* · alpha 线 ≥ 0.1.6-alpha.2(最新 0.1.7-alpha.1)
|
|
21
|
+
* 新座位 `plugins.bundle.config` + 会话作用域标准 prop `sessionId`
|
|
22
|
+
* 更早的 alpha(0.1.5-alpha.x、0.1.6-alpha.1)形状与 rc 线相同 —— 代码天然走得通,但**不作为目标**,
|
|
23
|
+
* 也不为它们写任何专门分支。本文件里凡出现"哪一版"的说法,都按这两条线命名。
|
|
24
|
+
*
|
|
17
25
|
* 段落(按依赖顺序):地址语法 → 工作区解析 → 常驻面 → 全屏动作 → 认领类型(与 lib/claim-types.js
|
|
18
26
|
* 同源的一致性副本)→ 插件主体。
|
|
19
27
|
*/
|
|
@@ -126,9 +134,9 @@ function resolveFilePath(parsed, cwd) {
|
|
|
126
134
|
// —— 解析"当前工作区目录"(决定 workbench 用哪个 `?folder=` 打开)
|
|
127
135
|
//
|
|
128
136
|
// 为什么单独成模块:这段逻辑的真实输入是 **DSH 客户端 store 的形状**,而它随 DSH 版本变过 ——
|
|
129
|
-
// ·
|
|
137
|
+
// · rc 线(≤ 0.1.5-rc.3,最新 rc 仍是这样):`SessionListState` 带 `current`(选中的会话 id 挂在快照上,
|
|
130
138
|
// 老写法 `useSessions(s => s).current` 即"当前会话");
|
|
131
|
-
// ·
|
|
139
|
+
// · alpha 线(≥ 0.1.6-alpha.2,含最新 0.1.7-alpha.1):`current` 被移出列表 store(上游 refactor 注释:view selection remains
|
|
132
140
|
// outside the Controller;快照只剩 `{ ids, byId, phase, subagentsByParent, jobsBySession }`),
|
|
133
141
|
// 会话作用域插槽改为用标准 prop `sessionId` 告诉 body"你是哪个会话的" —— 官方自己的右侧栏标签
|
|
134
142
|
// 就是这么读 cwd 的(ui-deliverables 的 ReviewTab:`useSessions(s => s.byId[sessionId]?.cwd)`)。
|
|
@@ -149,7 +157,7 @@ function cwdOf(sessions, sessionId) {
|
|
|
149
157
|
return typeof row.cwd === 'string' && row.cwd !== '' ? row.cwd : undefined
|
|
150
158
|
}
|
|
151
159
|
|
|
152
|
-
/** 工作区表里"包含该会话"的那个工作区的路径(
|
|
160
|
+
/** 工作区表里"包含该会话"的那个工作区的路径(rc 线的 currentAddress 语义由它兜底)。 */
|
|
153
161
|
function pathHoldingSession(workspaces, sessionId) {
|
|
154
162
|
if (workspaces == null || !Array.isArray(workspaces.items)) return undefined
|
|
155
163
|
if (typeof sessionId !== 'string' || sessionId === '') return undefined
|
|
@@ -660,6 +668,18 @@ function normalizeClaimExtensions(text) {
|
|
|
660
668
|
return seen.join(';')
|
|
661
669
|
}
|
|
662
670
|
|
|
671
|
+
/**
|
|
672
|
+
* FIM 停顿毫秒数的归一化(0.3.62):与宿主 lib/fim-adapter.mjs 的 `clampDebounce` **同语义**。
|
|
673
|
+
* 客户端也夹一次的理由:输入框里可以是任何东西(空、负数、10^9),用户在点保存之前就该看到
|
|
674
|
+
* "保存后会变成什么",而不是保存完发现值被宿主悄悄改掉 —— 这与认领类型那行显示"实际规则"同一考虑。
|
|
675
|
+
*/
|
|
676
|
+
function normalizeFimDebounce(value) {
|
|
677
|
+
if (value === undefined || value === null || value === '') return 250
|
|
678
|
+
var n = typeof value === 'number' ? value : Number(value)
|
|
679
|
+
if (!isFinite(n)) return 250
|
|
680
|
+
return Math.min(3000, Math.max(100, Math.round(n)))
|
|
681
|
+
}
|
|
682
|
+
|
|
663
683
|
/**
|
|
664
684
|
* 解析策略文本。
|
|
665
685
|
* @param text - 归一化前或后的文本都接受。
|
|
@@ -751,7 +771,7 @@ function intersects(list, group) {
|
|
|
751
771
|
// 数据通道:同源 fetch DSH Connection 的共享 /api(status / start / stop / setup / open-file / ui-mode;
|
|
752
772
|
// web 走 webServer 的 /api 前缀,desktop 走 IPC 帧管道 → 插件不依赖 webServer)。
|
|
753
773
|
//
|
|
754
|
-
// UI 载体只有右侧栏标签(需要 sidebarRightTabs / sidebarRight
|
|
774
|
+
// UI 载体只有右侧栏标签(需要 sidebarRightTabs / sidebarRight 服务;rc 线与 alpha 线都有):
|
|
755
775
|
// 一段 ctx.sidebarRightTabs.register({ id, kind, priority, patterns, canOpen, title, guide })
|
|
756
776
|
// → guide 页入口框;并通过 patterns 认领 `dsh-resource://file/**` 文件地址,成为官方 openFile
|
|
757
777
|
// (产物 chip / "交付"卡片预览 / 正文提及)与任何 openResource(fileAddress) 的落点。
|
|
@@ -804,8 +824,8 @@ let React = require('react')
|
|
|
804
824
|
}
|
|
805
825
|
|
|
806
826
|
// ---------- 标准 prop 里的会话/工作区快照 ----------
|
|
807
|
-
// 取哪条路、退到哪条路**全在本文件的工作区解析段**(纯函数):
|
|
808
|
-
// `SessionListState.current` 在 0.1.6-alpha.2
|
|
827
|
+
// 取哪条路、退到哪条路**全在本文件的工作区解析段**(纯函数):rc 线(≤ 0.1.5-rc.3)的
|
|
828
|
+
// `SessionListState.current` 在 alpha 线(≥ 0.1.6-alpha.2)被移出列表 store,会话作用域插槽改用标准 prop
|
|
809
829
|
// `sessionId` 标识当前会话 —— 只认旧形状会让 cwd 静默变成 undefined(IDE 以空工作区启动)。
|
|
810
830
|
/** 服务缺失时的占位选择器:**hook 调用次数必须恒定**(React 按位置对账),
|
|
811
831
|
* 否则"服务晚到"的那一次渲染会抛 "Rendered more hooks than during the previous render"。 */
|
|
@@ -946,7 +966,7 @@ let React = require('react')
|
|
|
946
966
|
document.head.appendChild(tag)
|
|
947
967
|
}
|
|
948
968
|
|
|
949
|
-
// ---------- 右侧栏标签(
|
|
969
|
+
// ---------- 右侧栏标签(rc 线与 alpha 线都有:ctx.sidebarRightTabs / ctx.sidebarRight) ----------
|
|
950
970
|
// 二段式注册:类型定义进 sidebarRightTabs,body 进 keyed 插槽 sidebar.right.pane.tab。
|
|
951
971
|
// 服务始终不就绪(旧版 DSH)→ 判定 legacy:只留设置页提示(见 internalApply)。
|
|
952
972
|
var CS_KIND = 'code-server'
|
|
@@ -1057,7 +1077,7 @@ let React = require('react')
|
|
|
1057
1077
|
var navigation = tab.navigation
|
|
1058
1078
|
var revision = navigation != null && typeof navigation.revision === 'number' ? navigation.revision : 0
|
|
1059
1079
|
// 地址:文件 tab 可能来自会话树里的别的会话,故优先用地址里的 sessionId 对齐工作区;
|
|
1060
|
-
// 页面 tab 用**会话作用域标准 prop** `sessionId`(
|
|
1080
|
+
// 页面 tab 用**会话作用域标准 prop** `sessionId`(alpha 线 ≥ 0.1.6-alpha.2 的当前会话信源,
|
|
1061
1081
|
// 老版的 `useSessions().current` 已被上游移除 —— 详见本文件的工作区解析段)。
|
|
1062
1082
|
var address = navigation != null && typeof navigation.address === 'string' ? navigation.address : ''
|
|
1063
1083
|
var parsed = isPageAddress(address) ? null : parseFileAddress(address)
|
|
@@ -1236,9 +1256,9 @@ let React = require('react')
|
|
|
1236
1256
|
// ---------- 设置(参照 auto-open-web 的自绘卡片模式) ----------
|
|
1237
1257
|
// 数据通道:settingsScope(官方 settings 域,命名空间 code-server)—— 两个座位共用。
|
|
1238
1258
|
// 座位(0.3.50 起两条腿都注册,谁被声明谁生效):
|
|
1239
|
-
// · `plugins.bundle.config`(
|
|
1259
|
+
// · `plugins.bundle.config`(alpha 线 ≥ 0.1.6-alpha.2,含最新 0.1.7-alpha.1):插件页按**包名**派发的配置区,页面自带
|
|
1240
1260
|
// 标题/面包屑,我们只出表单 + 保存控件(seat='bundle-config');
|
|
1241
|
-
// · `settings.plugin.item`(
|
|
1261
|
+
// · `settings.plugin.item`(rc 线 ≤ 0.1.5-rc.3,最新 rc 仍在用):设置页里自绘的可折叠卡片(alpha 线已退役)。
|
|
1242
1262
|
var CARD_CSS =
|
|
1243
1263
|
'.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}' +
|
|
1244
1264
|
'.dshcs-card:hover{border-color:var(--dsw-alias-label-dimmed,var(--dsw-alias-border-l2))}' +
|
|
@@ -1325,6 +1345,20 @@ let React = require('react')
|
|
|
1325
1345
|
React.createElement('span', { className: 'dshcs-hint' }, props.children)
|
|
1326
1346
|
)
|
|
1327
1347
|
}
|
|
1348
|
+
/** 数字输入(0.3.62 起,用于"停顿毫秒数"这类有上下限的整数)。
|
|
1349
|
+
* 为什么不用文本框:用户在点保存**之前**就该看到 min/max 这层约束;真正的夹取在
|
|
1350
|
+
* 保存时 `normalizeFimDebounce`(与宿主 clampDebounce 同语义),两者一致才不会"保存后被悄悄改掉"。 */
|
|
1351
|
+
function csNumber(props) {
|
|
1352
|
+
return React.createElement('label', { className: 'dshcs-text' },
|
|
1353
|
+
React.createElement('input', {
|
|
1354
|
+
type: 'number',
|
|
1355
|
+
value: props.value === undefined || props.value === null ? '' : String(props.value),
|
|
1356
|
+
min: props.min, max: props.max, step: props.step === undefined ? 10 : props.step,
|
|
1357
|
+
disabled: props.disabled === true, spellCheck: false, autoComplete: 'off',
|
|
1358
|
+
onChange: function (event) { props.onChange(event.target.value) },
|
|
1359
|
+
})
|
|
1360
|
+
)
|
|
1361
|
+
}
|
|
1328
1362
|
/** 多行文本输入(0.3.52 起,用于"认领类型"这类清单)。
|
|
1329
1363
|
* 为什么不是单行 input:0.3.51 之后默认值有 98 条排除项(≈700 字符),单行框只能看到开头,
|
|
1330
1364
|
* 剩下全靠横向滚动 —— 既看不全也改不动。这里改用 textarea,并**按内容自动长高**(上限 12 行),
|
|
@@ -1423,6 +1457,10 @@ let React = require('react')
|
|
|
1423
1457
|
keepResident: value.keepResident !== false,
|
|
1424
1458
|
claimExtensions: typeof value.claimExtensions === 'string' ? value.claimExtensions : DEFAULT_CLAIM_EXTENSIONS,
|
|
1425
1459
|
fullscreenOnOpen: value.fullscreenOnOpen !== false,
|
|
1460
|
+
fim: value.fim === true,
|
|
1461
|
+
fimDebounceMs: normalizeFimDebounce(value.fimDebounceMs),
|
|
1462
|
+
fimMultiline: value.fimMultiline !== false,
|
|
1463
|
+
fimDisableGlobs: typeof value.fimDisableGlobs === 'string' ? value.fimDisableGlobs : '',
|
|
1426
1464
|
}
|
|
1427
1465
|
// ---- 旧版 DSH(无右侧栏服务):本页是唯一的提示出口,不显示任何设置项 ----
|
|
1428
1466
|
if (liveStore != null && liveStore.sidebarUi === 'legacy') {
|
|
@@ -1438,7 +1476,7 @@ let React = require('react')
|
|
|
1438
1476
|
React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 0 } },
|
|
1439
1477
|
'未检测到右侧栏插件服务 sidebarRightTabs / sidebarRight,因此插件不提供任何入口'
|
|
1440
1478
|
+ '(旧版的悬浮球与浮动窗口已移除),也不会后台启动 IDE。'
|
|
1441
|
-
+ '\n升级 DSH
|
|
1479
|
+
+ '\n升级 DSH 到 rc 线(0.1.5-rc.x)或 0.1.6-alpha.2 起的 alpha 线后,Code Server 会出现在右侧栏标签里,'
|
|
1442
1480
|
+ '本页同时显示完整设置项;升级后无需重装本插件,刷新页面即可。'
|
|
1443
1481
|
)
|
|
1444
1482
|
)
|
|
@@ -1447,9 +1485,17 @@ let React = require('react')
|
|
|
1447
1485
|
var overriddenKeep = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'keepResident')
|
|
1448
1486
|
var overriddenClaim = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'claimExtensions')
|
|
1449
1487
|
var overriddenFullscreen = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'fullscreenOnOpen')
|
|
1488
|
+
var overriddenFim = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'fim')
|
|
1489
|
+
var overriddenFimDebounce = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'fimDebounceMs')
|
|
1490
|
+
var overriddenFimMultiline = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'fimMultiline')
|
|
1491
|
+
var overriddenFimGlobs = user !== undefined && user !== null && Object.prototype.hasOwnProperty.call(user, 'fimDisableGlobs')
|
|
1450
1492
|
var dirty = draft !== null && (draft.keepResident !== loaded.keepResident
|
|
1451
1493
|
|| normalizeClaimExtensions(draft.claimExtensions) !== normalizeClaimExtensions(loaded.claimExtensions)
|
|
1452
|
-
|| draft.fullscreenOnOpen !== loaded.fullscreenOnOpen
|
|
1494
|
+
|| draft.fullscreenOnOpen !== loaded.fullscreenOnOpen
|
|
1495
|
+
|| draft.fim !== loaded.fim
|
|
1496
|
+
|| normalizeFimDebounce(draft.fimDebounceMs) !== normalizeFimDebounce(loaded.fimDebounceMs)
|
|
1497
|
+
|| draft.fimMultiline !== loaded.fimMultiline
|
|
1498
|
+
|| draft.fimDisableGlobs !== loaded.fimDisableGlobs)
|
|
1453
1499
|
var saveDisabled = !dirty || saving
|
|
1454
1500
|
var state = {
|
|
1455
1501
|
available: true,
|
|
@@ -1473,6 +1519,10 @@ let React = require('react')
|
|
|
1473
1519
|
await props.scope.set('keepResident', d.keepResident === true)
|
|
1474
1520
|
await props.scope.set('claimExtensions', normalizeClaimExtensions(d.claimExtensions))
|
|
1475
1521
|
await props.scope.set('fullscreenOnOpen', d.fullscreenOnOpen === true)
|
|
1522
|
+
await props.scope.set('fim', d.fim === true)
|
|
1523
|
+
await props.scope.set('fimDebounceMs', normalizeFimDebounce(d.fimDebounceMs))
|
|
1524
|
+
await props.scope.set('fimMultiline', d.fimMultiline === true)
|
|
1525
|
+
await props.scope.set('fimDisableGlobs', typeof d.fimDisableGlobs === 'string' ? d.fimDisableGlobs : '')
|
|
1476
1526
|
setDraft(null)
|
|
1477
1527
|
syncStatusToStore()
|
|
1478
1528
|
} catch (e) {
|
|
@@ -1490,10 +1540,18 @@ let React = require('react')
|
|
|
1490
1540
|
await props.scope.unset('keepResident')
|
|
1491
1541
|
await props.scope.unset('claimExtensions')
|
|
1492
1542
|
await props.scope.unset('fullscreenOnOpen')
|
|
1543
|
+
await props.scope.unset('fim')
|
|
1544
|
+
await props.scope.unset('fimDebounceMs')
|
|
1545
|
+
await props.scope.unset('fimMultiline')
|
|
1546
|
+
await props.scope.unset('fimDisableGlobs')
|
|
1493
1547
|
} else {
|
|
1494
1548
|
await props.scope.set('keepResident', value.keepResident === undefined || value.keepResident === null ? true : value.keepResident)
|
|
1495
1549
|
await props.scope.set('claimExtensions', typeof value.claimExtensions === 'string' ? normalizeClaimExtensions(value.claimExtensions) : DEFAULT_CLAIM_EXTENSIONS)
|
|
1496
1550
|
await props.scope.set('fullscreenOnOpen', value.fullscreenOnOpen === undefined || value.fullscreenOnOpen === null ? true : value.fullscreenOnOpen)
|
|
1551
|
+
await props.scope.set('fim', value.fim === true)
|
|
1552
|
+
await props.scope.set('fimDebounceMs', normalizeFimDebounce(value.fimDebounceMs))
|
|
1553
|
+
await props.scope.set('fimMultiline', value.fimMultiline !== false)
|
|
1554
|
+
await props.scope.set('fimDisableGlobs', typeof value.fimDisableGlobs === 'string' ? value.fimDisableGlobs : '')
|
|
1497
1555
|
}
|
|
1498
1556
|
setDraft(null)
|
|
1499
1557
|
syncStatusToStore()
|
|
@@ -1503,8 +1561,8 @@ let React = require('react')
|
|
|
1503
1561
|
setSaving(false)
|
|
1504
1562
|
}
|
|
1505
1563
|
var fields = [
|
|
1506
|
-
//
|
|
1507
|
-
// (入口在右侧栏「开始」页的 guide 入口框;诊断看 host
|
|
1564
|
+
// 一行一个可改设置(0.2.11 起):认领类型 + 打开即全屏 + FIM 补全(0.3.61,含 0.3.62 的三个子项)
|
|
1565
|
+
// + 后台常驻。入口/依赖安装/环境检测三行已移除(入口在右侧栏「开始」页的 guide 入口框;诊断看 host 日志)。
|
|
1508
1566
|
React.createElement('div', { className: 'dshcs-field' },
|
|
1509
1567
|
React.createElement('div', { className: 'dshcs-fieldHead' },
|
|
1510
1568
|
React.createElement('span', { className: 'dshcs-fieldLabel' }, '认领类型(按扩展名,分号分隔)'),
|
|
@@ -1565,6 +1623,92 @@ let React = require('react')
|
|
|
1565
1623
|
'只影响"打开那一刻":切走再切回、再次打开文件 tab 会重新切全屏;'
|
|
1566
1624
|
+ 'DSH 未向插件开放模式接口,本项由点击右侧栏自身的「全屏」按钮实现。')
|
|
1567
1625
|
),
|
|
1626
|
+
React.createElement('div', { className: 'dshcs-field' },
|
|
1627
|
+
React.createElement('div', { className: 'dshcs-fieldHead' },
|
|
1628
|
+
React.createElement('span', { className: 'dshcs-fieldLabel' }, 'FIM 补全(实验性)'),
|
|
1629
|
+
overriddenFim === true
|
|
1630
|
+
? React.createElement(csBadges, {
|
|
1631
|
+
overridden: true, disabled: snapshot.writable !== true,
|
|
1632
|
+
overriddenLabel: '已覆盖', resetLabel: '恢复默认',
|
|
1633
|
+
onReset: function () { doReset() },
|
|
1634
|
+
})
|
|
1635
|
+
: null
|
|
1636
|
+
),
|
|
1637
|
+
React.createElement(csCheck, {
|
|
1638
|
+
checked: draft !== null ? draft.fim : loaded.fim,
|
|
1639
|
+
disabled: snapshot.writable !== true,
|
|
1640
|
+
onChange: function (v) { setDraft(function (prev) { return Object.assign({}, prev !== null ? prev : loaded, { fim: v === true }) }); setFailed(false) },
|
|
1641
|
+
}, '开启后,在编辑器里打字停顿约 0.3 秒会向模型要一次「续写」,以灰字显示在光标后:Tab 接受、Esc 丢弃。默认关闭'),
|
|
1642
|
+
React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 4 } },
|
|
1643
|
+
'实验性功能,默认关闭。三点须知:'
|
|
1644
|
+
+ '① 它是本插件**唯一会把内容发出去**的能力 —— 每次补全会把光标附近的一小段代码(有上限)交给模型,'
|
|
1645
|
+
+ '桥的其余部分只读编辑器状态、不外发内容;'
|
|
1646
|
+
+ '② 这条调用不计入 DSH 自己的 token 计量(它不是一次会话请求),'
|
|
1647
|
+
+ '用量只在 IDE 状态栏的 DSH 项上是可见的;'
|
|
1648
|
+
+ '③ 模型偶尔会在"并不需要补"的位置硬凑一段,忽略即可(Esc 或继续打字都会让它消失)。'
|
|
1649
|
+
+ '关掉本项后扩展会立刻注销补全 provider,不再发出任何补全请求。')
|
|
1650
|
+
),
|
|
1651
|
+
React.createElement('div', { className: 'dshcs-field' },
|
|
1652
|
+
React.createElement('div', { className: 'dshcs-fieldHead' },
|
|
1653
|
+
React.createElement('span', { className: 'dshcs-fieldLabel' }, 'FIM · 停顿毫秒数(停下多久才请求)'),
|
|
1654
|
+
overriddenFimDebounce === true
|
|
1655
|
+
? React.createElement(csBadges, {
|
|
1656
|
+
overridden: true, disabled: snapshot.writable !== true,
|
|
1657
|
+
overriddenLabel: '已覆盖', resetLabel: '恢复默认',
|
|
1658
|
+
onReset: function () { doReset() },
|
|
1659
|
+
})
|
|
1660
|
+
: null
|
|
1661
|
+
),
|
|
1662
|
+
React.createElement(csNumber, {
|
|
1663
|
+
value: draft !== null ? draft.fimDebounceMs : loaded.fimDebounceMs,
|
|
1664
|
+
min: 100, max: 3000, step: 50, disabled: snapshot.writable !== true,
|
|
1665
|
+
onChange: function (v) { setDraft(function (prev) { return Object.assign({}, prev !== null ? prev : loaded, { fimDebounceMs: v }) }); setFailed(false) },
|
|
1666
|
+
}),
|
|
1667
|
+
React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 4 } },
|
|
1668
|
+
'有效范围 100–3000ms,保存时按它夹取(与宿主同一套规则)。实测这条端点往返 112–416ms,'
|
|
1669
|
+
+ '所以停顿基本就是"感知到的延迟":太小会在打字过程中反复触发,太大要等很久才出现灰字。')
|
|
1670
|
+
),
|
|
1671
|
+
React.createElement('div', { className: 'dshcs-field' },
|
|
1672
|
+
React.createElement('div', { className: 'dshcs-fieldHead' },
|
|
1673
|
+
React.createElement('span', { className: 'dshcs-fieldLabel' }, 'FIM · 允许多行补全'),
|
|
1674
|
+
overriddenFimMultiline === true
|
|
1675
|
+
? React.createElement(csBadges, {
|
|
1676
|
+
overridden: true, disabled: snapshot.writable !== true,
|
|
1677
|
+
overriddenLabel: '已覆盖', resetLabel: '恢复默认',
|
|
1678
|
+
onReset: function () { doReset() },
|
|
1679
|
+
})
|
|
1680
|
+
: null
|
|
1681
|
+
),
|
|
1682
|
+
React.createElement(csCheck, {
|
|
1683
|
+
checked: draft !== null ? draft.fimMultiline : loaded.fimMultiline,
|
|
1684
|
+
disabled: snapshot.writable !== true,
|
|
1685
|
+
onChange: function (v) { setDraft(function (prev) { return Object.assign({}, prev !== null ? prev : loaded, { fimMultiline: v === true }) }); setFailed(false) },
|
|
1686
|
+
}, '允许补出多行(关掉后只取第一行;首行为空 = 这次不补)'),
|
|
1687
|
+
React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 4 } },
|
|
1688
|
+
'默认开。实测模型在"并不需要补"的位置会硬凑一段,多行会把这种噪声放大 —— 想更克制就关掉它。')
|
|
1689
|
+
),
|
|
1690
|
+
React.createElement('div', { className: 'dshcs-field' },
|
|
1691
|
+
React.createElement('div', { className: 'dshcs-fieldHead' },
|
|
1692
|
+
React.createElement('span', { className: 'dshcs-fieldLabel' }, 'FIM · 按 glob 禁用(分号或换行分隔)'),
|
|
1693
|
+
overriddenFimGlobs === true
|
|
1694
|
+
? React.createElement(csBadges, {
|
|
1695
|
+
overridden: true, disabled: snapshot.writable !== true,
|
|
1696
|
+
overriddenLabel: '已覆盖', resetLabel: '恢复默认',
|
|
1697
|
+
onReset: function () { doReset() },
|
|
1698
|
+
})
|
|
1699
|
+
: null
|
|
1700
|
+
),
|
|
1701
|
+
React.createElement(csTextarea, {
|
|
1702
|
+
value: draft !== null ? draft.fimDisableGlobs : loaded.fimDisableGlobs,
|
|
1703
|
+
disabled: snapshot.writable !== true,
|
|
1704
|
+
placeholder: '例如 *.md;**/dist/**;vendor/**',
|
|
1705
|
+
onChange: function (v) { setDraft(function (prev) { return Object.assign({}, prev !== null ? prev : loaded, { fimDisableGlobs: v }) }); setFailed(false) },
|
|
1706
|
+
}),
|
|
1707
|
+
React.createElement('div', { className: 'dshcs-hint', style: { marginTop: 4 } },
|
|
1708
|
+
'留空 = 不禁用。语法:`*` 不跨目录、`**` 跨目录、不含 `/` 的模式只匹配文件名、`/` 结尾视作 `/**`。'
|
|
1709
|
+
+ '例:`*.md`(所有 Markdown)、`vendor/**`、`**/dist/**`、`**/*.min.js`。'
|
|
1710
|
+
+ '两边都会判:扩展侧先判(根本不发请求),宿主侧再判一遍。')
|
|
1711
|
+
),
|
|
1568
1712
|
React.createElement('div', { className: 'dshcs-field' },
|
|
1569
1713
|
React.createElement('div', { className: 'dshcs-fieldHead' },
|
|
1570
1714
|
React.createElement('span', { className: 'dshcs-fieldLabel' }, '后台常驻(切标签不重载)'),
|
|
@@ -1588,7 +1732,7 @@ let React = require('react')
|
|
|
1588
1732
|
: '当前浏览器不支持 Element.moveBefore(Chromium <133)→ 常驻不可用,切标签仍会整页重载(升级浏览器后自动生效)')
|
|
1589
1733
|
)
|
|
1590
1734
|
]
|
|
1591
|
-
// ---- 插件页的配置区(
|
|
1735
|
+
// ---- 插件页的配置区(alpha 线 ≥ 0.1.6-alpha.2)----
|
|
1592
1736
|
// 上游把插件设置从"设置 → 插件"搬到**插件页**:页面自己画标题/图标/面包屑,把
|
|
1593
1737
|
// `plugins.bundle.config`(按包名为键)渲染在描述与行之间,只问 `view: 'page'`。
|
|
1594
1738
|
// 所以这里不再套可折叠卡片(标题会与页面标题重复),只出表单 + 自己的保存控件
|
|
@@ -1615,7 +1759,7 @@ let React = require('react')
|
|
|
1615
1759
|
}
|
|
1616
1760
|
return React.createElement(csCard, {
|
|
1617
1761
|
title: 'Code Server',
|
|
1618
|
-
description: '入口:右侧栏标签(DSH
|
|
1762
|
+
description: '入口:右侧栏标签(需 DSH 带 sidebarRight/sidebarRightTabs,即 rc 线或 0.1.6-alpha.2 起的 alpha 线);认领类型决定哪些文件交给 Code Server 打开',
|
|
1619
1763
|
state: state,
|
|
1620
1764
|
unsavedLabel: '未保存', readOnlyLabel: '本部署的设置为只读。',
|
|
1621
1765
|
saveFailedLabel: '本部署没有接受这些值,已保留供你修改。',
|
|
@@ -2723,21 +2867,21 @@ let React = require('react')
|
|
|
2723
2867
|
}).catch(function () { /* 首次失败由后续轮询补救 */ })
|
|
2724
2868
|
|
|
2725
2869
|
// ---- 设置:两条腿都注册,谁被声明谁生效(两次注册不会同时出现) ----
|
|
2726
|
-
// ① 插件页的配置区(
|
|
2870
|
+
// ① 插件页的配置区(alpha 线 ≥ 0.1.6-alpha.2,含最新 0.1.7-alpha.1):`settings.plugin.item` 退役,上游把插件配置搬到
|
|
2727
2871
|
// 插件页 —— 键是**包名**(页面按 `pkg.name` 派发),只问 `view: 'page'`。插件页只在
|
|
2728
2872
|
// `ledger.bundles.has(包名)` 时才渲染那一块,所以键写错就等于"设置又不见了"。
|
|
2729
2873
|
slots.inject('plugins.bundle.config', () => slots.register(
|
|
2730
2874
|
{ name: 'plugins.bundle.config', key: PKG_NAME, inject: function () { return { scope: scope } } },
|
|
2731
2875
|
BundleConfigEntry
|
|
2732
2876
|
))
|
|
2733
|
-
// ② 设置页里的插件卡片(
|
|
2877
|
+
// ② 设置页里的插件卡片(rc 线 ≤ 0.1.5-rc.3,最新 rc 仍在用;也是没有右侧栏服务的旧 DSH 里唯一的提示出口)。
|
|
2734
2878
|
// `slots.inject` 只在插槽真被声明时才回调 ⇒ 新 DSH 上这一条自然不生效。
|
|
2735
2879
|
slots.inject('settings.plugin.item', () => slots.register(
|
|
2736
2880
|
{ name: 'settings.plugin.item', key: 'code-server', label: 'Code Server', inject: function () { return { scope: scope } } },
|
|
2737
2881
|
csSettingsPage
|
|
2738
2882
|
))
|
|
2739
2883
|
|
|
2740
|
-
// ---- 能力探测:只有带右侧栏服务(
|
|
2884
|
+
// ---- 能力探测:只有带右侧栏服务(sidebarRightTabs/sidebarRight;rc 线与 alpha 线都有)才注册可用 UI;探测不到就只留设置页提示 ----
|
|
2741
2885
|
var modernSettled = false
|
|
2742
2886
|
|
|
2743
2887
|
function onModernUi(sctx, via) {
|