dsh-code-server-app 0.3.58 → 0.3.63
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 +215 -98
- package/README.md +186 -94
- package/THIRD_PARTY_NOTICES.md +60 -0
- package/assets/extensions/dshcs-editor-bridge/extension.js +192 -222
- package/assets/extensions/dshcs-editor-bridge/lib/bridge-client.js +43 -39
- 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-session.mjs +55 -5
- package/lib/bridge.mjs +14 -7
- package/lib/client.js +1061 -90
- package/lib/fim-adapter.mjs +603 -0
- package/lib/index.js +366 -205
- package/package.json +21 -41
- package/vendor/VENDOR.json +5 -5
- package/assets/extensions/dshcs-editor-bridge/lib/ask-panel.js +0 -416
- package/assets/extensions/dshcs-editor-bridge/webview/THIRD-PARTY.md +0 -22
- package/assets/extensions/dshcs-editor-bridge/webview/src/app.jsx +0 -242
- package/assets/extensions/dshcs-editor-bridge/webview/src/approval.jsx +0 -61
- package/assets/extensions/dshcs-editor-bridge/webview/src/official-tokens.css +0 -576
- package/assets/extensions/dshcs-editor-bridge/webview/src/panel.css +0 -380
- package/assets/extensions/dshcs-editor-bridge/webview/src/thread.jsx +0 -160
- package/assets/extensions/dshcs-editor-bridge/webview/thread.css +0 -1
- package/assets/extensions/dshcs-editor-bridge/webview/thread.js +0 -465
|
@@ -13,13 +13,16 @@
|
|
|
13
13
|
// 就什么都不做。状态栏不显示任何东西,更不弹通知。
|
|
14
14
|
// 3. **绝不覆盖未保存改动**:agent 改了文件而该文档是脏的就只告警 + 给 diff,由用户决定。
|
|
15
15
|
//
|
|
16
|
-
// 「问 DSH
|
|
17
|
-
// -
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
16
|
+
// 「问 DSH」对话框(0.2.0 起;0.3.59 起是**唯一**的提问 UI):
|
|
17
|
+
// - 提问:右键命令只向宿主上报意图(`POST /event {kind:'ask-open', mode}`),对话框开在 **DSH 页面**里;
|
|
18
|
+
// 提问正文由插件的客户端半部经 `/api/code-server/ask/send` 投递(host 侧以**用户输入**进对话);
|
|
19
|
+
// - 回答:host 用 `sessionController.follow` 把该会话的**新内容**投影成条目,对话框轮询
|
|
20
|
+
// `/api/code-server/ask/state` 取回,面板用 DSH 官方 markdown 渲染器显示 —— 面板本体在客户端
|
|
21
|
+
// 半部(lib/client.js):手写、不打包,直接用 DSH 页面模块表里那一份渲染器;
|
|
22
|
+
// - 授权(写工作区外文件 / 执行命令):host 把待决请求放进同一份状态,用户在对话框上
|
|
23
|
+
// 「允许一次 / 拒绝」,客户端半部调 `/api/code-server/ask/approve` 提交;
|
|
24
|
+
// - 编辑器里**不再有**自己的提问面板:0.3.59 删掉了那份 webview 产物(thread.js/css),
|
|
25
|
+
// 探测不到对话框心跳时只弹一条提示,不开一扇用户看不见的窗。
|
|
23
26
|
//
|
|
24
27
|
// 只依赖 `vscode` 与 Node 内置模块;纯逻辑在 lib/ 下且不 require('vscode'),便于单测。
|
|
25
28
|
|
|
@@ -27,7 +30,6 @@
|
|
|
27
30
|
|
|
28
31
|
const vscode = require('vscode');
|
|
29
32
|
const crypto = require('crypto');
|
|
30
|
-
const fs = require('fs');
|
|
31
33
|
const path = require('path');
|
|
32
34
|
|
|
33
35
|
const {
|
|
@@ -36,15 +38,18 @@ const {
|
|
|
36
38
|
createClient,
|
|
37
39
|
} = require('./lib/bridge-client.js');
|
|
38
40
|
const { createProjector } = require('./lib/context-model.js');
|
|
39
|
-
const { chooseOldSide, createDiffCache, describeChange } = require('./lib/diff-model.js');
|
|
40
41
|
const {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
FIM_DEBOUNCE_DEFAULT_MS,
|
|
43
|
+
shouldRequest,
|
|
44
|
+
buildPayload,
|
|
45
|
+
fingerprint,
|
|
46
|
+
createCompletionCache,
|
|
47
|
+
formatTokens,
|
|
48
|
+
describeFimUsage,
|
|
49
|
+
matchDisabledGlob,
|
|
50
|
+
toSingleLine,
|
|
51
|
+
} = require('./lib/fim-completion.js');
|
|
52
|
+
const { chooseOldSide, createDiffCache, describeChange } = require('./lib/diff-model.js');
|
|
48
53
|
|
|
49
54
|
/** 状态栏项(仅在桥连通时显示)。 */
|
|
50
55
|
let statusBar = null;
|
|
@@ -56,16 +61,8 @@ let client = null;
|
|
|
56
61
|
let pollTimer = null;
|
|
57
62
|
/** 上次成功轮询的时间(状态栏 tooltip 用)。 */
|
|
58
63
|
let lastPollAt = 0;
|
|
59
|
-
/**
|
|
60
|
-
let askPanel = null;
|
|
61
|
-
/** 宿主是否支持 DSH 页面里的悬浮对话框(0.2.5 探测;0.3.26 起不再自动启用)。 */
|
|
64
|
+
/** 宿主是否已经把"对话框活着"证明给我们看(host 侧 askDialogLive:5 秒内收到过 /ask/state 轮询)。 */
|
|
62
65
|
let askDialogSupported = false;
|
|
63
|
-
/** 上一份对话流快照(host 只在变化时回传,这里兜住"没变化"的那些轮询)。 */
|
|
64
|
-
let lastThreadSnapshot;
|
|
65
|
-
/** 本扩展**当前持有**的对话流修订号(随请求上报;-1 = 一份都还没有 ⇒ host 必须发). */
|
|
66
|
-
let lastThreadRev = -1;
|
|
67
|
-
/** 扩展根目录(activate 时记下;面板要按它取 webview 产物 URI)。 */
|
|
68
|
-
let extensionRoot = null;
|
|
69
66
|
/** 是否已经确认过 host 端点可达(避免把"IDE 刚起、扩展先加载"误判为断线)。 */
|
|
70
67
|
let connected = false;
|
|
71
68
|
/** 诊断集合缓存:host 请求时现算,这里只做"有没有变化"的计数上报。 */
|
|
@@ -79,6 +76,119 @@ const diffTextStore = new Map();
|
|
|
79
76
|
/** 同一个文件的 diff tab 不重复开。 */
|
|
80
77
|
const openDiffTabs = new Map();
|
|
81
78
|
|
|
79
|
+
// ---------------------------------------------------------------- FIM(幽灵)补全:实验性
|
|
80
|
+
//
|
|
81
|
+
// 这条能力**只在宿主说 enabled=true 时才注册 provider**(宿主的 /sync 每趟都带上来),
|
|
82
|
+
// 所以"设置里没开"不会让编辑器每敲一个字就发一次注定被拒的请求。
|
|
83
|
+
//
|
|
84
|
+
// 三条与宿主一致的分工:
|
|
85
|
+
// - **该不该问**在这里判(纯逻辑 shouldRequest):不值得问的位置根本不发请求 ——
|
|
86
|
+
// 实测模型在被问到"不该补的地方"时会硬凑(2/2),所以判据必须前置;
|
|
87
|
+
// - **问什么**在这里裁(窗口内的前后文),宿主侧再兜一层同样的上限;
|
|
88
|
+
// - **问过没有**用本地缓存答(相同前后文 + 语言)。
|
|
89
|
+
/** 宿主报来的 FIM 状态与用量({enabled:false} 表示设置里没开)。 */
|
|
90
|
+
let fimSnapshot = { enabled: false };
|
|
91
|
+
/** 已注册的 provider disposer(null = 未注册)。 */
|
|
92
|
+
let fimProvider = null;
|
|
93
|
+
/** 扩展上下文(activate 里赋值):provider 需要在轮询里注册/注销,而轮询拿不到 context 参数。 */
|
|
94
|
+
let extensionContext = null;
|
|
95
|
+
/** 前后文 → 补全文本 的有界缓存。 */
|
|
96
|
+
const fimCache = createCompletionCache();
|
|
97
|
+
|
|
98
|
+
/** 取消感知的停顿:打字过程中的请求全丢掉(宿主侧也有速率闸,但那是最后一道)。
|
|
99
|
+
* 停顿毫秒数由设置在宿主侧提供(0.3.62),每趟 /sync 带上来;拿不到就用默认值。 */
|
|
100
|
+
async function fimWait(token) {
|
|
101
|
+
const configured = Number.isFinite(fimSnapshot.debounceMs) ? fimSnapshot.debounceMs : FIM_DEBOUNCE_DEFAULT_MS;
|
|
102
|
+
const deadline = Date.now() + configured;
|
|
103
|
+
while (Date.now() < deadline) {
|
|
104
|
+
if (token.isCancellationRequested === true) return false;
|
|
105
|
+
// 40ms 一跳:足够细,且不会为一次补全拉起一串定时器。
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
107
|
+
}
|
|
108
|
+
return token.isCancellationRequested !== true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 取光标前后的窗口(只按行取,不把整份文档摸一遍 —— 大文件上每次击键都 getText() 是灾难)。 */
|
|
112
|
+
function fimWindows(document, position) {
|
|
113
|
+
const startLine = Math.max(0, position.line - 120);
|
|
114
|
+
const endLine = Math.min(document.lineCount - 1, position.line + 40);
|
|
115
|
+
const prefixText = document.getText(new vscode.Range(new vscode.Position(startLine, 0), position));
|
|
116
|
+
const afterEnd = document.lineAt(endLine).range.end;
|
|
117
|
+
const suffixText = document.getText(new vscode.Range(position, afterEnd));
|
|
118
|
+
const lineTextBefore = document.lineAt(position.line).text.slice(0, position.character);
|
|
119
|
+
return { prefixText, suffixText, lineTextBefore };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 内联补全 provider。返回 null = 这次不补(与"补一个空串"是两回事)。 */
|
|
123
|
+
function createFimProvider() {
|
|
124
|
+
return {
|
|
125
|
+
async provideInlineCompletionItems(document, position, _context, token) {
|
|
126
|
+
if (fimSnapshot.enabled !== true) return null;
|
|
127
|
+
const editor = vscode.window.activeTextEditor;
|
|
128
|
+
const windows = fimWindows(document, position);
|
|
129
|
+
const decision = shouldRequest({
|
|
130
|
+
selectionEmpty: editor === undefined || editor === null ? true : editor.selection.isEmpty,
|
|
131
|
+
scheme: document.uri.scheme,
|
|
132
|
+
lineCount: document.lineCount,
|
|
133
|
+
prefixText: windows.prefixText,
|
|
134
|
+
suffixText: windows.suffixText,
|
|
135
|
+
lineTextBefore: windows.lineTextBefore,
|
|
136
|
+
});
|
|
137
|
+
if (decision.ask !== true) return null;
|
|
138
|
+
const payload = buildPayload({
|
|
139
|
+
prefixText: windows.prefixText,
|
|
140
|
+
suffixText: windows.suffixText,
|
|
141
|
+
language: document.languageId,
|
|
142
|
+
path: document.uri.scheme === 'file' ? document.uri.fsPath : document.uri.toString(),
|
|
143
|
+
});
|
|
144
|
+
// 按 glob 禁用(0.3.62):**在发请求之前**判 —— 这是这一层存在的主要理由(不花冤枉钱)。
|
|
145
|
+
// 宿主侧还会再判一次(服务端那道),所以即使这里漏了也不会真的发出去。
|
|
146
|
+
const blocked = matchDisabledGlob(payload.path, fimSnapshot.disableGlobs);
|
|
147
|
+
if (blocked !== null) return null;
|
|
148
|
+
const key = fingerprint(payload.prompt, payload.suffix, payload.language);
|
|
149
|
+
const cached = fimCache.get(key);
|
|
150
|
+
if (cached !== null) {
|
|
151
|
+
return cached === '' ? null : [new vscode.InlineCompletionItem(cached, new vscode.Range(position, position))];
|
|
152
|
+
}
|
|
153
|
+
if ((await fimWait(token)) !== true) return null;
|
|
154
|
+
if (client === null || client.isDormant()) return null;
|
|
155
|
+
if (typeof client.complete !== 'function') return null;
|
|
156
|
+
const result = await client.complete(payload);
|
|
157
|
+
if (token.isCancellationRequested === true) return null;
|
|
158
|
+
if (result.ok !== true) {
|
|
159
|
+
if (result.status !== 409 && result.status !== 429) log(`补全失败:${result.error ?? '未知'}(status=${result.status ?? 0})`);
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
// 多行关掉时宿主已经裁过一道;这里再裁一次是防"宿主版本比扩展旧"(随包分发的静态文件可能不同步)。
|
|
163
|
+
const raw = typeof result.text === 'string' ? result.text : '';
|
|
164
|
+
const text = fimSnapshot.multiline === false ? toSingleLine(raw) : raw;
|
|
165
|
+
fimCache.set(key, text);
|
|
166
|
+
if (text.trim() === '') return null; // 模型说"这里不用补"是合法回答
|
|
167
|
+
log(`补全 ${text.length} 字符(${result.ms ?? '?'}ms,缓存${result.usage?.cacheReadTokens ?? 0} tok)`);
|
|
168
|
+
return [new vscode.InlineCompletionItem(text, new vscode.Range(position, position))];
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 按宿主状态注册/注销 provider(状态翻转时才动)。 */
|
|
174
|
+
function syncFimProvider() {
|
|
175
|
+
const want = fimSnapshot.enabled === true;
|
|
176
|
+
if (want && fimProvider === null) {
|
|
177
|
+
fimProvider = vscode.languages.registerInlineCompletionItemProvider({ pattern: '**' }, createFimProvider());
|
|
178
|
+
if (extensionContext !== null) extensionContext.subscriptions.push(fimProvider);
|
|
179
|
+
log('FIM 补全(实验性)已启用:开始提供内联补全');
|
|
180
|
+
} else if (!want && fimProvider !== null) {
|
|
181
|
+
try {
|
|
182
|
+
fimProvider.dispose();
|
|
183
|
+
} catch {
|
|
184
|
+
// 已被 VS Code 回收
|
|
185
|
+
}
|
|
186
|
+
fimProvider = null;
|
|
187
|
+
fimCache.clear();
|
|
188
|
+
log('FIM 补全已关闭:不再提供内联补全');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
82
192
|
/** 把"改动前的文本"登记进 store,返回它的 docId。 */
|
|
83
193
|
function registerDiffText(text) {
|
|
84
194
|
const docId = crypto.createHash('sha256').update(text ?? '').digest('hex');
|
|
@@ -397,17 +507,6 @@ async function pollOnce() {
|
|
|
397
507
|
}),
|
|
398
508
|
diagnostics,
|
|
399
509
|
at: Date.now(),
|
|
400
|
-
// 面板声明它要看哪个会话:host 据此对齐 `sessionController.follow` 订阅,
|
|
401
|
-
// 并用"有没有人在看"决定授权请求是先问面板还是直接交给 DSH 界面
|
|
402
|
-
// (见 host 侧 lib/bridge-thread.mjs / lib/bridge-approval.mjs)。
|
|
403
|
-
watch: askPanel !== null && askPanel.state.sessionId !== null ? [askPanel.state.sessionId] : [],
|
|
404
|
-
// 声明"本扩展懂对话流修订号"(0.2.9):host 才会在没变化时省略 thread 省流量。
|
|
405
|
-
// 不声明(旧扩展)时 host 照旧发整份快照 —— 否则旧扩展会误报"宿主没有对话流能力"。
|
|
406
|
-
threadRev: lastThreadRev,
|
|
407
|
-
// 声明"我这边能把授权卡片画出来"(0.3.30):面板开着 **且** 对话流正常(不是那条"宿主没有对话流能力"
|
|
408
|
-
// 的坏状态)。宿主只在它为 true 时才敢抢答授权 —— 否则"抢过来却没人看得见"就是用户遇到的
|
|
409
|
-
// "没弹出授权"(官方卡片被我们吞了,面板又不画)。
|
|
410
|
-
approvalsUi: askPanel !== null && askPanel.state.available === true,
|
|
411
510
|
};
|
|
412
511
|
const result = await client.sync(payload);
|
|
413
512
|
if (result.ok !== true) {
|
|
@@ -429,28 +528,22 @@ async function pollOnce() {
|
|
|
429
528
|
await handleEvent(event);
|
|
430
529
|
}
|
|
431
530
|
if ((result.events ?? []).length > 0) client.persist();
|
|
432
|
-
//
|
|
531
|
+
// FIM(实验性):宿主每趟带状态与用量上来。只在**翻转**时注册/注销 provider,
|
|
532
|
+
// 但状态栏每趟都刷 —— 用户要看到的是"这轮补全花了多少 token",它一直在变。
|
|
533
|
+
const nextFim = result.fim !== null && typeof result.fim === 'object' ? result.fim : { enabled: false };
|
|
534
|
+
const wasEnabled = fimSnapshot.enabled === true;
|
|
535
|
+
fimSnapshot = nextFim;
|
|
536
|
+
if ((nextFim.enabled === true) !== wasEnabled) syncFimProvider();
|
|
537
|
+
updateStatusBar();
|
|
538
|
+
// 能力探测(0.3.24):宿主证明"DSH 页面里的对话框活着"⇒ 右键提问走 ask-open 事件。
|
|
539
|
+
// 0.3.59 起这是**唯一**的提问 UI(编辑器里的 webview 面板已退役):探测不到就只弹一条提示,
|
|
540
|
+
// 绝不退回到一扇用户看不见的窗。
|
|
433
541
|
if (result.askDialog === true && !askDialogSupported) {
|
|
434
542
|
askDialogSupported = true;
|
|
435
|
-
log('
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
// 0.3.27 起 host **没变化就不带 thread**(省掉每 600ms 一份 ~1MB 的快照,那会把 /sync 拖成超时,
|
|
440
|
-
// 于是 approvals 也一起丢掉、卡片永远不出现)⇒ 这里沿用上一份快照。
|
|
441
|
-
if (askPanel !== null) {
|
|
442
|
-
const thread = result.thread !== undefined
|
|
443
|
-
? result.thread
|
|
444
|
-
: (result.threadRev !== undefined ? lastThreadSnapshot : undefined);
|
|
445
|
-
if (result.thread !== undefined) lastThreadSnapshot = result.thread;
|
|
446
|
-
if (Number.isSafeInteger(result.threadRev)) lastThreadRev = result.threadRev;
|
|
447
|
-
// 刚开面板、手里还没有任何快照,而 host 说"没变化"(它只看到我们持有 -1)→ 等下一份,别误报
|
|
448
|
-
// "宿主没有对话流能力"(0.3.29)。
|
|
449
|
-
if (result.thread === undefined && lastThreadSnapshot === undefined) {
|
|
450
|
-
refreshAskPanel();
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
if (applySync(askPanel.state, { ...result, thread })) refreshAskPanel();
|
|
543
|
+
log('宿主已证明「问 DSH」对话框活着:右键提问将打开 DSH 页面里的对话框');
|
|
544
|
+
} else if (result.askDialog !== true && askDialogSupported) {
|
|
545
|
+
askDialogSupported = false;
|
|
546
|
+
log('宿主不再报告对话框心跳(页面关了/刷新过):右键提问会给出提示');
|
|
454
547
|
}
|
|
455
548
|
}
|
|
456
549
|
|
|
@@ -468,8 +561,23 @@ function startPolling(context) {
|
|
|
468
561
|
function updateStatusBar() {
|
|
469
562
|
if (statusBar === null) return;
|
|
470
563
|
if (connected && client !== null && client.config !== null) {
|
|
471
|
-
|
|
472
|
-
|
|
564
|
+
// FIM(实验性)开着时,状态栏多一格 token 计数 —— 这条链路的调用**不进 DSH 的计量**
|
|
565
|
+
// (一次性调用不是 loop 请求,不进会话日志 ⇒ token-meter 看不到它),所以这里是用户唯一能看到
|
|
566
|
+
// "补全花了多少"的地方(设置卡里也有同样一份,见 README)。
|
|
567
|
+
const fimOn = fimSnapshot !== null && fimSnapshot.enabled === true;
|
|
568
|
+
const used = fimOn ? Number(fimSnapshot.totalTokens ?? 0) : 0;
|
|
569
|
+
statusBar.text = fimOn
|
|
570
|
+
? (used > 0 ? `$(plug) DSH $(zap) ${formatTokens(used)}` : '$(plug) DSH $(zap)')
|
|
571
|
+
: '$(plug) DSH';
|
|
572
|
+
const lines = [
|
|
573
|
+
`编辑器桥已连接:${client.config.pipe}`,
|
|
574
|
+
`上次轮询:${lastPollAt === 0 ? '—' : new Date(lastPollAt).toLocaleTimeString()}`,
|
|
575
|
+
'',
|
|
576
|
+
...describeFimUsage(fimSnapshot),
|
|
577
|
+
'',
|
|
578
|
+
'点击查看日志',
|
|
579
|
+
];
|
|
580
|
+
statusBar.tooltip = lines.join('\n');
|
|
473
581
|
statusBar.command = 'dsh-code-server.showBridgeLog';
|
|
474
582
|
statusBar.show();
|
|
475
583
|
} else {
|
|
@@ -504,152 +612,14 @@ function captureAskContext(mode) {
|
|
|
504
612
|
};
|
|
505
613
|
}
|
|
506
614
|
|
|
507
|
-
/**
|
|
508
|
-
function
|
|
509
|
-
|
|
510
|
-
void askPanel.panel.webview.postMessage(panelPayload(askPanel.state));
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/** 面板进入错误态:状态行 + 最新一条本地提问都标上原因(绝不静默)。 */
|
|
514
|
-
function failAskPanel(message) {
|
|
515
|
-
if (askPanel === null) return;
|
|
516
|
-
failPanel(askPanel.state, message);
|
|
615
|
+
/** 提问对话框不可用时的提示(0.3.59 起不再退回编辑器面板 —— 那一半已退役)。 */
|
|
616
|
+
function notifyAskDialogUnavailable() {
|
|
617
|
+
const message = '「问 DSH」对话框不可用:请在 DSH 页面里打开(或刷新)Code Server 标签后重试。';
|
|
517
618
|
log(message);
|
|
518
|
-
|
|
619
|
+
void vscode.window.showInformationMessage(message);
|
|
519
620
|
}
|
|
520
621
|
|
|
521
|
-
/**
|
|
522
|
-
async function sendAskFromPanel(text) {
|
|
523
|
-
if (askPanel === null || client === null) return;
|
|
524
|
-
// 用面板自己的意图(selection / file)取上下文:文件级提问永远不带行号。
|
|
525
|
-
const context = captureAskContext(askPanel.mode) ?? askPanel.state.context;
|
|
526
|
-
pendingQuestion(askPanel.state, text, context);
|
|
527
|
-
refreshAskPanel();
|
|
528
|
-
try {
|
|
529
|
-
const result = await client.ask({
|
|
530
|
-
text,
|
|
531
|
-
file: context === null ? null : context.file,
|
|
532
|
-
lineStart: context === null ? null : context.lineStart,
|
|
533
|
-
lineEnd: context === null ? null : context.lineEnd,
|
|
534
|
-
selection: context === null ? null : context.selection,
|
|
535
|
-
languageId: context === null ? null : context.languageId,
|
|
536
|
-
});
|
|
537
|
-
if (result.ok === true) {
|
|
538
|
-
askPanel.state.sessionId = typeof result.sessionId === 'string' ? result.sessionId : null;
|
|
539
|
-
askPanel.state.status = 'thinking';
|
|
540
|
-
log(`已投递编辑器消息(session=${result.sessionId ?? '?'})`);
|
|
541
|
-
} else {
|
|
542
|
-
failAskPanel(`未投递:${result.error ?? '未知原因'}`);
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
} catch (error) {
|
|
546
|
-
const status = error && error.status;
|
|
547
|
-
failAskPanel(status === 409
|
|
548
|
-
? 'DSH 里没有可投递的会话:请先在 DSH 里打开或新建一个会话。'
|
|
549
|
-
: (status === 401 || status === 503
|
|
550
|
-
? '编辑器桥尚未就绪,请稍后重试。'
|
|
551
|
-
: `投递失败:${error && error.message ? error.message : error}`));
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
refreshAskPanel();
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* 面板对一条授权请求的决策 → `POST /approve`。
|
|
559
|
-
*
|
|
560
|
-
* 只接受白名单的两个值(host 侧同样只认这两个);失败必须说出来:面板里点不动的时候,
|
|
561
|
-
* 用户唯一的线索就是这行状态/日志。
|
|
562
|
-
*/
|
|
563
|
-
async function decideApproval(id, outcome) {
|
|
564
|
-
if (askPanel === null || client === null) return;
|
|
565
|
-
if (outcome !== 'allowed-once' && outcome !== 'rejected') return;
|
|
566
|
-
try {
|
|
567
|
-
await client.approve(id, outcome);
|
|
568
|
-
log(`面板授权决策已提交:${id} → ${outcome}`);
|
|
569
|
-
} catch (error) {
|
|
570
|
-
const status = error && error.status;
|
|
571
|
-
const message = status === 409
|
|
572
|
-
? '这条授权请求已经过期或已被处理(DSH 界面里会弹同一张卡片)。'
|
|
573
|
-
: (status === 401 || status === 503
|
|
574
|
-
? '编辑器桥尚未就绪,授权没有提交。'
|
|
575
|
-
: `授权提交失败:${error && error.message ? error.message : error}`);
|
|
576
|
-
// 卡片已经没意义了:本地撤掉,避免用户反复点。
|
|
577
|
-
askPanel.state.approvals = askPanel.state.approvals.filter((item) => item.id !== id);
|
|
578
|
-
failAskPanel(message);
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
refreshAskPanel();
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* 打开(或聚焦)「问 DSH」面板。
|
|
586
|
-
* @param {'selection'|'file'} mode 提问意图(决定发送时带不带行号/选区),见 captureAskContext。
|
|
587
|
-
*/
|
|
588
|
-
function openAskPanel(mode = 'selection') {
|
|
589
|
-
const contextInfo = captureAskContext(mode);
|
|
590
|
-
if (askPanel === null) {
|
|
591
|
-
// webview 产物是构建出来的(scripts/build-webview.mjs)。缺了就别开一个空白面板。
|
|
592
|
-
const webviewDir = vscode.Uri.joinPath(extensionRoot, 'webview');
|
|
593
|
-
const bundle = vscode.Uri.joinPath(webviewDir, 'thread.js');
|
|
594
|
-
const style = vscode.Uri.joinPath(webviewDir, 'thread.css');
|
|
595
|
-
if (!fs.existsSync(bundle.fsPath) || !fs.existsSync(style.fsPath)) {
|
|
596
|
-
const message = `面板产物缺失(${bundle.fsPath}):请重新安装插件,或在源码里跑 pnpm run build:webview`;
|
|
597
|
-
log(message);
|
|
598
|
-
void vscode.window.showErrorMessage(message);
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
const panel = vscode.window.createWebviewPanel(
|
|
602
|
-
'dshAsk',
|
|
603
|
-
'DSH 对话',
|
|
604
|
-
// 对话框形状(0.2.4):开在**当前编辑器组**,占满工作台宽度 —— 不再用 `Beside` 挤成一条侧栏。
|
|
605
|
-
// 面板内容自己居中限宽,看起来就是编辑器上的一扇对话窗;右上角的 ✕ 关掉它。
|
|
606
|
-
{ viewColumn: vscode.ViewColumn.Active, preserveFocus: false },
|
|
607
|
-
{
|
|
608
|
-
enableScripts: true,
|
|
609
|
-
retainContextWhenHidden: true,
|
|
610
|
-
// 只允许加载扩展自己的 webview 目录(产物 + 字体),不给别的本地文件开口子。
|
|
611
|
-
localResourceRoots: [webviewDir],
|
|
612
|
-
},
|
|
613
|
-
);
|
|
614
|
-
askPanel = { panel, state: createPanelState(), mode };
|
|
615
|
-
panel.webview.html = renderPanelHtml({
|
|
616
|
-
cspSource: panel.webview.cspSource,
|
|
617
|
-
nonce: crypto.randomBytes(16).toString('base64url'),
|
|
618
|
-
scriptUri: panel.webview.asWebviewUri(bundle).toString(),
|
|
619
|
-
styleUri: panel.webview.asWebviewUri(style).toString(),
|
|
620
|
-
});
|
|
621
|
-
panel.webview.onDidReceiveMessage((message) => {
|
|
622
|
-
if (message === null || typeof message !== 'object') return;
|
|
623
|
-
if (message.type === 'ready') {
|
|
624
|
-
refreshAskPanel();
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
if (message.type === 'ask' && typeof message.text === 'string') {
|
|
628
|
-
const text = message.text.trim();
|
|
629
|
-
if (text !== '') void sendAskFromPanel(text);
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
|
-
if (message.type === 'approve' && typeof message.id === 'string') {
|
|
633
|
-
void decideApproval(message.id, message.outcome);
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
if (message.type === 'close') {
|
|
637
|
-
// 面板上的 ✕:关掉它(= 不再看这个会话 ⇒ 待决授权立刻交回 DSH 界面,不用干等窗口)。
|
|
638
|
-
panel.dispose();
|
|
639
|
-
}
|
|
640
|
-
});
|
|
641
|
-
panel.onDidDispose(() => {
|
|
642
|
-
askPanel = null;
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
askPanel.state.context = contextInfo;
|
|
646
|
-
askPanel.mode = mode;
|
|
647
|
-
// 已经开着就提到前面(对话框不重复开)。
|
|
648
|
-
askPanel.panel.reveal(vscode.ViewColumn.Active, false);
|
|
649
|
-
refreshAskPanel();
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/** 选中内容 → DSH(编辑器→DSH 的主入口):打开面板并带上当前上下文。 */
|
|
622
|
+
/** 选中内容 → DSH(编辑器→DSH 的主入口):请宿主打开对话框并带上当前上下文。 */
|
|
653
623
|
async function askAboutSelection() {
|
|
654
624
|
await openAskPanelFor('selection');
|
|
655
625
|
}
|
|
@@ -659,7 +629,7 @@ async function askAboutFile() {
|
|
|
659
629
|
await openAskPanelFor('file');
|
|
660
630
|
}
|
|
661
631
|
|
|
662
|
-
/** 两个命令的公共前置检查(桥可用 + 有活动编辑器)。 */
|
|
632
|
+
/** 两个命令的公共前置检查(桥可用 + 有活动编辑器 + 对话框活着)。 */
|
|
663
633
|
async function openAskPanelFor(mode) {
|
|
664
634
|
if (client === null || client.isDormant()) {
|
|
665
635
|
vscode.window.showInformationMessage('编辑器桥未启用:请在 DSH 里打开 Code Server 标签后重试。');
|
|
@@ -669,25 +639,27 @@ async function openAskPanelFor(mode) {
|
|
|
669
639
|
vscode.window.showInformationMessage('没有活动的编辑器:请先打开一个文件。');
|
|
670
640
|
return;
|
|
671
641
|
}
|
|
672
|
-
// 0.3.
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
} catch (error) {
|
|
687
|
-
log('ask-open 失败(' + (error && error.message ? error.message : error) + ') → 退回编辑器面板');
|
|
642
|
+
// 0.3.59:提问 UI 只有一处 —— **DSH 页面里的悬浮对话框**(面板本体在插件客户端半部:手写、不打包,
|
|
643
|
+
// 直接用页面模块表里的官方渲染器)。编辑器里的 webview 面板已退役:那份 1.4MB 产物 + esbuild +
|
|
644
|
+
// 十几个打包依赖,只为了兜底一条"宿主证明不了对话框活着"的路 —— 而那种情况下用户在编辑器里
|
|
645
|
+
// 看到的其实也不是他想要的对话(而且我们在 0.3.30 之后就从没走到过那条路)。
|
|
646
|
+
// 所以现在:拿不到心跳就**明确说清怎么办**,不再开一扇看不见的窗。
|
|
647
|
+
if (!askDialogSupported) {
|
|
648
|
+
notifyAskDialogUnavailable();
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
const result = await client.askOpen(mode);
|
|
653
|
+
if (result !== null && result.ok === true) {
|
|
654
|
+
log(`已请宿主打开「问 DSH」对话框(mode=${mode})`);
|
|
655
|
+
return;
|
|
688
656
|
}
|
|
657
|
+
log(`宿主没有打开对话框(${result !== null && result.error ? result.error : '未知原因'})`);
|
|
658
|
+
notifyAskDialogUnavailable();
|
|
659
|
+
} catch (error) {
|
|
660
|
+
log(`ask-open 失败(${error && error.message ? error.message : error})`);
|
|
661
|
+
notifyAskDialogUnavailable();
|
|
689
662
|
}
|
|
690
|
-
openAskPanel(mode);
|
|
691
663
|
}
|
|
692
664
|
|
|
693
665
|
function showBridgeLog() {
|
|
@@ -697,12 +669,10 @@ function showBridgeLog() {
|
|
|
697
669
|
// ---------------------------------------------------------------- 激活
|
|
698
670
|
|
|
699
671
|
function activate(context) {
|
|
672
|
+
extensionContext = context;
|
|
700
673
|
output = vscode.window.createOutputChannel('DSH Editor Bridge');
|
|
701
674
|
context.subscriptions.push(output);
|
|
702
675
|
|
|
703
|
-
// 面板 webview 的产物按扩展根目录取 URI(0.2.3 起正文走官方渲染器,产物是打包出来的)。
|
|
704
|
-
extensionRoot = context.extensionUri;
|
|
705
|
-
|
|
706
676
|
statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 90);
|
|
707
677
|
context.subscriptions.push(statusBar);
|
|
708
678
|
|
|
@@ -35,6 +35,9 @@ const STATE_FILENAME = 'extension-state.json';
|
|
|
35
35
|
// 请求超时(0.2.8 起 8s):3s 在实测里太紧 —— 对话流快照一大,宿主序列化就要几秒,
|
|
36
36
|
// 超时会让扩展**丢整份响应**(包括 approvals),表现就是"授权卡片一直不出现"。
|
|
37
37
|
const REQUEST_TIMEOUT_MS = 8000;
|
|
38
|
+
/** 补全请求的超时:**独立且更短**。宿主侧自己在 4s 放弃,这里给 6s 只是别先于宿主超时 ——
|
|
39
|
+
* 补全的价值在"比你打字快",8s 那个值对它毫无意义,而挂住的请求会挡住下一次补全。 */
|
|
40
|
+
const COMPLETE_TIMEOUT_MS = 6000;
|
|
38
41
|
/** 轮询间隔:host 侧事件只是"去看一眼这个文件"的提示,600ms 足够且几乎无开销。 */
|
|
39
42
|
const POLL_INTERVAL_MS = 600;
|
|
40
43
|
/** 宿主配置重读间隔(端点/令牌轮换后最多这么久恢复)。 */
|
|
@@ -285,21 +288,16 @@ function createClient(options) {
|
|
|
285
288
|
// 症状就是"每个 IDE 会话只收到第一条 diff")。退回 0 重新对齐即可自愈。
|
|
286
289
|
const lastSeq = body !== null && Number.isSafeInteger(body.lastSeq) ? body.lastSeq : null;
|
|
287
290
|
if (lastSeq !== null && lastSeq < since) since = 0;
|
|
288
|
-
// 0.
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
// - approvals:待决授权请求(工作区外写入 / 命令执行),面板就地作答;
|
|
292
|
-
// - uiVersion:DSH 界面的版本(面板据此提示"渲染器与界面版本不一致")。
|
|
293
|
-
// 全部原样透传,解析与合并交给 lib/ask-panel.js 的纯模型。
|
|
291
|
+
// 0.3.59:对话流 / 待决授权**不再经这条通道**(那是编辑器 webview 面板时代的分工)。
|
|
292
|
+
// 现在唯一的面板在 DSH 页面里、由插件的客户端半部直接读 `/api/code-server/ask/state`,
|
|
293
|
+
// 扩展这边只留下能力位:宿主有没有证明"对话框活着"。
|
|
294
294
|
return {
|
|
295
295
|
ok: true,
|
|
296
296
|
events,
|
|
297
|
-
thread: body !== null && body.thread !== undefined ? body.thread : undefined,
|
|
298
|
-
approvals: body !== null && Array.isArray(body.approvals) ? body.approvals : [],
|
|
299
|
-
approvalHoldMs: body !== null && Number.isSafeInteger(body.approvalHoldMs) ? body.approvalHoldMs : undefined,
|
|
300
|
-
uiVersion: body !== null && typeof body.uiVersion === 'string' ? body.uiVersion : undefined,
|
|
301
|
-
// 能力探测(0.3.24):宿主支持 DSH 页面里的悬浮对话框 ⇒ 提问改走 ask-open 事件(不开 webview 面板)。
|
|
302
297
|
askDialog: body !== null && body.askDialog === true,
|
|
298
|
+
// FIM(实验性)状态与用量:宿主每趟都带上来,扩展据此决定"要不要注册补全 provider"
|
|
299
|
+
// 以及状态栏上那串数字怎么显示。没开时是 {enabled:false}(不是 null)。
|
|
300
|
+
fim: body !== null && typeof body.fim === 'object' && body.fim !== null ? body.fim : { enabled: false },
|
|
303
301
|
};
|
|
304
302
|
} catch (error) {
|
|
305
303
|
return { ok: false, error: error.message, status: error.status, code: error.code };
|
|
@@ -316,35 +314,11 @@ function createClient(options) {
|
|
|
316
314
|
body: JSON.stringify({ kind: 'ask-open', mode: mode === 'file' ? 'file' : 'selection' }),
|
|
317
315
|
});
|
|
318
316
|
},
|
|
319
|
-
/** 把"选中内容 + 问题"投给 DSH 的当前会话。 */
|
|
320
|
-
async ask(payload) {
|
|
321
|
-
return request(`${BRIDGE_BASE}/ask`, {
|
|
322
|
-
method: 'POST',
|
|
323
|
-
headers: { 'content-type': 'application/json' },
|
|
324
|
-
body: JSON.stringify(payload),
|
|
325
|
-
});
|
|
326
|
-
},
|
|
327
|
-
/**
|
|
328
|
-
* 回答一条待决的授权请求(桥里**唯一**的非只读路由)。
|
|
329
|
-
*
|
|
330
|
-
* 约束在 host 侧写死:只认本进程发起、仍未决的 id(单次使用),outcome 只接受
|
|
331
|
-
* `allowed-once` / `rejected`,不接受任何自由文本 —— 这条路由只能"回答问题",不能"发起动作"。
|
|
332
|
-
*
|
|
333
|
-
* @param {string} id 授权请求 id(来自 /sync 的 approvals)
|
|
334
|
-
* @param {'allowed-once'|'rejected'} outcome
|
|
335
|
-
*/
|
|
336
|
-
async approve(id, outcome) {
|
|
337
|
-
return request(`${BRIDGE_BASE}/approve`, {
|
|
338
|
-
method: 'POST',
|
|
339
|
-
headers: { 'content-type': 'application/json' },
|
|
340
|
-
body: JSON.stringify({ id, outcome }),
|
|
341
|
-
});
|
|
342
|
-
},
|
|
343
317
|
/**
|
|
344
318
|
* 取一份"写前原文"快照(0.3.55):事件里带的是不透明 key,文本单独取。
|
|
345
319
|
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
320
|
+
* 为什么不把文本塞进事件:一趟 /sync 要驮的东西越少越好 —— 塞进 ~1MB 文本会把那一趟拖成超时,
|
|
321
|
+
* 而超时意味着事件也一起丢(用户看到的症状就是"diff 不弹")。
|
|
348
322
|
*
|
|
349
323
|
* 取不到(404 = 过期/被淘汰/宿主重启)时**返回 null 而不是抛**:调用方据此回退到缓冲区或缓存。
|
|
350
324
|
*
|
|
@@ -360,11 +334,40 @@ function createClient(options) {
|
|
|
360
334
|
return null;
|
|
361
335
|
}
|
|
362
336
|
},
|
|
337
|
+
/**
|
|
338
|
+
* 求一次 FIM(幽灵)补全(实验性)。
|
|
339
|
+
*
|
|
340
|
+
* **不抛**:补全失败是常态(没开、太快、模型抽风),抛异常会让 provider 每次都写错误日志。
|
|
341
|
+
* 返回值统一是 `{ok, text?, usage?, ms?, error?, status?}` —— 调用方只看 `ok`。
|
|
342
|
+
* 超时是独立的短超时(宿主侧 4s 就放弃了,这里给到 6s 只是别先于宿主超时)。
|
|
343
|
+
*/
|
|
344
|
+
async complete(payload) {
|
|
345
|
+
try {
|
|
346
|
+
const body = await requestImpl({
|
|
347
|
+
socketPath: (refreshConfig(true) ?? {}).pipe,
|
|
348
|
+
path: `${BRIDGE_BASE}/complete`,
|
|
349
|
+
method: 'POST',
|
|
350
|
+
headers: { [TOKEN_HEADER]: config === null ? '' : config.token, 'content-type': 'application/json' },
|
|
351
|
+
body: JSON.stringify(payload),
|
|
352
|
+
timeoutMs: COMPLETE_TIMEOUT_MS,
|
|
353
|
+
});
|
|
354
|
+
if (body === null || typeof body !== 'object') return { ok: false, error: '空响应' };
|
|
355
|
+
const status = body.status === undefined ? 0 : body.status;
|
|
356
|
+
if (status < 200 || status >= 300) {
|
|
357
|
+
const reason = body.json !== null && typeof body.json.error === 'string' ? body.json.error : `HTTP ${status}`;
|
|
358
|
+
return { ok: false, error: reason, status };
|
|
359
|
+
}
|
|
360
|
+
const json = body.json === null || typeof body.json !== 'object' ? {} : body.json;
|
|
361
|
+
if (json.ok !== true) return { ok: false, error: typeof json.error === 'string' ? json.error : '宿主未返回补全', status };
|
|
362
|
+
return { ok: true, text: typeof json.text === 'string' ? json.text : '', usage: json.usage ?? null, ms: json.ms ?? null };
|
|
363
|
+
} catch (error) {
|
|
364
|
+
return { ok: false, error: error && error.message ? error.message : String(error), status: error && error.status };
|
|
365
|
+
}
|
|
366
|
+
},
|
|
363
367
|
/** 把游标落盘(实例重启后不重复播报旧事件)。 */
|
|
364
368
|
persist() {
|
|
365
369
|
return writeState(extensionsDir, { since, pid: config === null ? null : config.pid });
|
|
366
|
-
},
|
|
367
|
-
/** 从磁盘恢复游标(实例没换才生效)。 */
|
|
370
|
+
}, /** 从磁盘恢复游标(实例没换才生效)。 */
|
|
368
371
|
restore() {
|
|
369
372
|
const saved = readState(extensionsDir);
|
|
370
373
|
if (config !== null && saved.pid === config.pid) since = saved.since;
|
|
@@ -383,6 +386,7 @@ module.exports = {
|
|
|
383
386
|
POLL_INTERVAL_MS,
|
|
384
387
|
CONFIG_REREAD_MS,
|
|
385
388
|
REQUEST_TIMEOUT_MS,
|
|
389
|
+
COMPLETE_TIMEOUT_MS,
|
|
386
390
|
BridgeError,
|
|
387
391
|
isBridgeEndpoint,
|
|
388
392
|
defaultExtensionsDir,
|