dsh-vscode-mode 0.1.58 → 0.1.59
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 +450 -16
- package/lib/client.js.map +1 -1
- package/lib/index.js +354 -10
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/ai/inline.ts +192 -0
- package/src/ai/llmTypes.ts +83 -0
- package/src/ai/rpc.ts +35 -0
- package/src/client/ai/inlineProvider.ts +190 -0
- package/src/client/state/records.ts +1 -1
- package/src/client/state/regions.ts +13 -6
- package/src/client/ui/AiSettings.ts +104 -0
- package/src/client/ui/EditorView.ts +65 -4
- package/src/client/ui/McpSettings.ts +3 -1
- package/src/fileOpenSettings.ts +64 -3
- package/src/host-deps.d.ts +3 -0
- package/src/index.ts +7 -2
- package/src/rpc.ts +5 -1
- package/src/shared/ai.ts +158 -0
- package/src/shared/diff.ts +18 -0
- package/src/shared/rpc.ts +9 -0
- package/src/store.ts +5 -2
package/lib/client.js
CHANGED
|
@@ -1849,6 +1849,22 @@ window.__ModuleLoader__.load({
|
|
|
1849
1849
|
if (idx === 0 && record.callHunk && typeof record.callHunk.newText === "string") return record.callHunk;
|
|
1850
1850
|
return null;
|
|
1851
1851
|
}
|
|
1852
|
+
/**
|
|
1853
|
+
* 差异比较口径归一化:剥离 UTF-8 BOM、统一 CRLF 为 LF。
|
|
1854
|
+
* 背景:外部工具(pwsh/编辑器)可能改变 BOM/行尾,导致 after 指纹与 hunk
|
|
1855
|
+
* 定位在内容语义未变的情况下误报 conflict,把待确认修改从 UI 排除。
|
|
1856
|
+
* @author ddj 2026年09月09号
|
|
1857
|
+
*/
|
|
1858
|
+
function normalizeForCompare(text) {
|
|
1859
|
+
return text.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
1860
|
+
}
|
|
1861
|
+
/** 归一化一个 hunk 的两侧文本(与 normalizeForCompare 同口径)。 */
|
|
1862
|
+
function normalizeHunk(hunk) {
|
|
1863
|
+
return {
|
|
1864
|
+
oldText: hunk.oldText === null ? null : normalizeForCompare(hunk.oldText),
|
|
1865
|
+
newText: normalizeForCompare(hunk.newText)
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1852
1868
|
/** splitLines 的空文本语义:真实空文件没有一行变更内容。 */
|
|
1853
1869
|
function splitLines(text) {
|
|
1854
1870
|
return text.length ? text.split("\n") : [];
|
|
@@ -1979,7 +1995,7 @@ window.__ModuleLoader__.load({
|
|
|
1979
1995
|
}
|
|
1980
1996
|
/** 记录是否仍有待处理差异(superseded / 全部已决策 = false)。 */
|
|
1981
1997
|
function isRecPending(rec) {
|
|
1982
|
-
if (!rec || rec.superseded === true
|
|
1998
|
+
if (!rec || rec.superseded === true) return false;
|
|
1983
1999
|
const perHunk = Array.isArray(rec.decisions?.perHunk) ? rec.decisions.perHunk : [];
|
|
1984
2000
|
if (perHunk.length) {
|
|
1985
2001
|
for (let i = 0; i < perHunk.length; i++) if (perHunk[i] !== "accepted" && perHunk[i] !== "rejected" && !noopHunk(rec, (rec.hunks || [])[i])) return true;
|
|
@@ -2050,11 +2066,18 @@ window.__ModuleLoader__.load({
|
|
|
2050
2066
|
shift: prefix
|
|
2051
2067
|
};
|
|
2052
2068
|
}
|
|
2053
|
-
/**
|
|
2069
|
+
/**
|
|
2070
|
+
* 计算文件内各差异区域(行范围 + old/new + 状态),用于行内绿标注与 DiffBox。
|
|
2071
|
+
* 定位统一基于归一化文本(剥 BOM、CRLF→LF):外部工具可能改变行尾/BOM,
|
|
2072
|
+
* 与 edit 工具的 LF hunk 口径不一致会导致定位失败(差异被误标 stale)。
|
|
2073
|
+
* 行号按 \n 计数,归一化不改变行号,展示语义不变。
|
|
2074
|
+
* @author ddj 2026年09月09号
|
|
2075
|
+
*/
|
|
2054
2076
|
function diffRegions(records, content) {
|
|
2055
2077
|
const regions = [];
|
|
2056
2078
|
if (content === null) return regions;
|
|
2057
|
-
const
|
|
2079
|
+
const normalized = normalizeForCompare(content);
|
|
2080
|
+
const lines = splitLines(normalized);
|
|
2058
2081
|
for (const rec of records) {
|
|
2059
2082
|
if (rec.create) {
|
|
2060
2083
|
for (let i = 0; i < rec.hunks.length; i++) {
|
|
@@ -2081,10 +2104,10 @@ window.__ModuleLoader__.load({
|
|
|
2081
2104
|
const hunk = preciseHunk(rec, i);
|
|
2082
2105
|
if (hunk && !noopHunk(rec, hunk)) entries.push({
|
|
2083
2106
|
idx: i,
|
|
2084
|
-
hunk
|
|
2107
|
+
hunk: normalizeHunk(hunk)
|
|
2085
2108
|
});
|
|
2086
2109
|
}
|
|
2087
|
-
const locations = locateHunks(
|
|
2110
|
+
const locations = locateHunks(normalized, entries.map((entry) => entry.hunk));
|
|
2088
2111
|
for (let i = 0; i < entries.length; i++) {
|
|
2089
2112
|
const entry = entries[i];
|
|
2090
2113
|
const location = locations[i];
|
|
@@ -2103,7 +2126,7 @@ window.__ModuleLoader__.load({
|
|
|
2103
2126
|
});
|
|
2104
2127
|
continue;
|
|
2105
2128
|
}
|
|
2106
|
-
const start = countLinesBefore(
|
|
2129
|
+
const start = countLinesBefore(normalized, location.start) + 1;
|
|
2107
2130
|
const trimmed = trimCommonLines(entry.hunk.oldText === null ? [] : entry.hunk.oldText.split("\n"), entry.hunk.newText.split("\n"));
|
|
2108
2131
|
const regionStart = start + trimmed.shift;
|
|
2109
2132
|
regions.push({
|
|
@@ -3770,7 +3793,7 @@ window.__ModuleLoader__.load({
|
|
|
3770
3793
|
tokenTypes: [...LSP_SEMANTIC_TOKEN_TYPES],
|
|
3771
3794
|
tokenModifiers: [...LSP_SEMANTIC_TOKEN_MODIFIERS]
|
|
3772
3795
|
};
|
|
3773
|
-
let registered = false;
|
|
3796
|
+
let registered$1 = false;
|
|
3774
3797
|
const disposables = [];
|
|
3775
3798
|
/** 目标文件打开并定位(复用现有 openFileAt 的 edrv:open-editor 事件通道)。 */
|
|
3776
3799
|
function openAt(path, line, column) {
|
|
@@ -3787,8 +3810,8 @@ window.__ModuleLoader__.load({
|
|
|
3787
3810
|
}
|
|
3788
3811
|
/** 注册全部 Monaco LSP provider 与文档跟踪(幂等)。 */
|
|
3789
3812
|
function registerLspProviders(monaco) {
|
|
3790
|
-
if (registered) return;
|
|
3791
|
-
registered = true;
|
|
3813
|
+
if (registered$1) return;
|
|
3814
|
+
registered$1 = true;
|
|
3792
3815
|
const attachModel = (model) => {
|
|
3793
3816
|
if (!model || model.uri.scheme !== "edrv") return;
|
|
3794
3817
|
const path = pathOfModel(model);
|
|
@@ -4033,7 +4056,7 @@ window.__ModuleLoader__.load({
|
|
|
4033
4056
|
* 数据来源于语义 token(每 model+version 缓存 60s),不做逐词 LSP 查询。
|
|
4034
4057
|
* 作者 ddj 2026-09-02
|
|
4035
4058
|
*/
|
|
4036
|
-
const CACHE_TTL_MS = 6e4;
|
|
4059
|
+
const CACHE_TTL_MS$1 = 6e4;
|
|
4037
4060
|
/**
|
|
4038
4061
|
* 给一个 Monaco 编辑器绑定 Ctrl+hover 下划线提示(幂等)。
|
|
4039
4062
|
* @author ddj 2026年09月02号
|
|
@@ -4063,7 +4086,7 @@ window.__ModuleLoader__.load({
|
|
|
4063
4086
|
const rangesOf = async (model) => {
|
|
4064
4087
|
const path = pathOfModel(model);
|
|
4065
4088
|
const version = model.getVersionId?.() ?? 0;
|
|
4066
|
-
if (cache.path === path && cache.version === version && cache.ranges && Date.now() - cache.at < CACHE_TTL_MS) return cache.ranges;
|
|
4089
|
+
if (cache.path === path && cache.version === version && cache.ranges && Date.now() - cache.at < CACHE_TTL_MS$1) return cache.ranges;
|
|
4067
4090
|
const res = await fetchSemanticTokens(path, model.getValue()).catch(() => null);
|
|
4068
4091
|
const ranges = res && Array.isArray(res.data) ? decodeSemanticTokens(res.data) : [];
|
|
4069
4092
|
cache = {
|
|
@@ -4140,6 +4163,237 @@ window.__ModuleLoader__.load({
|
|
|
4140
4163
|
refreshStatus(true);
|
|
4141
4164
|
}
|
|
4142
4165
|
//#endregion
|
|
4166
|
+
//#region src/shared/ai.ts
|
|
4167
|
+
/**
|
|
4168
|
+
* dsh-vscode-mode shared — AI 内联补全契约与纯函数(双面,禁 node/react)。
|
|
4169
|
+
* 只放"过 RPC 的载荷形状"与无副作用变换:窗口裁剪、prompt 拼装、输出清理。
|
|
4170
|
+
* host 与语言模型之间的流式交互在 host 侧 src/ai/inline.ts。
|
|
4171
|
+
* 作者 ddj
|
|
4172
|
+
*/
|
|
4173
|
+
/** 上下文窗口:前缀字符上限(含未保存编辑的当前文档)。 */
|
|
4174
|
+
const AI_PREFIX_MAX = 2e3;
|
|
4175
|
+
/**
|
|
4176
|
+
* 按 AI_PREFIX_MAX/AI_SUFFIX_MAX 裁剪前后缀(client 侧发请求前调用)。
|
|
4177
|
+
* @author ddj
|
|
4178
|
+
* @param prefix 光标前全文
|
|
4179
|
+
* @param suffix 光标后全文
|
|
4180
|
+
* @returns 裁剪后的窗口
|
|
4181
|
+
*/
|
|
4182
|
+
function trimInlineWindow(prefix, suffix) {
|
|
4183
|
+
const pre = typeof prefix === "string" ? prefix : "";
|
|
4184
|
+
const suf = typeof suffix === "string" ? suffix : "";
|
|
4185
|
+
return {
|
|
4186
|
+
prefix: pre.length > 2e3 ? pre.slice(pre.length - AI_PREFIX_MAX) : pre,
|
|
4187
|
+
suffix: suf.length > 500 ? suf.slice(0, 500) : suf
|
|
4188
|
+
};
|
|
4189
|
+
}
|
|
4190
|
+
/**
|
|
4191
|
+
* 是否值得发起补全请求:前缀达标且非纯空白(空行/行首不请求)。
|
|
4192
|
+
* @author ddj
|
|
4193
|
+
* @param prefix 光标前文本
|
|
4194
|
+
* @returns 是否可请求
|
|
4195
|
+
*/
|
|
4196
|
+
function inlineWorth(prefix) {
|
|
4197
|
+
const pre = typeof prefix === "string" ? prefix : "";
|
|
4198
|
+
if (pre.length < 4) return false;
|
|
4199
|
+
return pre.trim().length > 0;
|
|
4200
|
+
}
|
|
4201
|
+
//#endregion
|
|
4202
|
+
//#region src/client/ai/inlineProvider.ts
|
|
4203
|
+
/**
|
|
4204
|
+
* dsh-vscode-mode client — AI 内联补全(ghost text)provider。
|
|
4205
|
+
* 速度设计:内部 150ms 去抖(Monaco 逐键调用 provider,不逐键打 LLM)、
|
|
4206
|
+
* 在飞取消(新请求 abort 旧的)+ 序号守卫(过期响应丢弃)、
|
|
4207
|
+
* 位置+前缀缓存(dismiss 后同位置重查不重复计费)、前后缀窗口裁剪。
|
|
4208
|
+
* 状态经 edrv:ai-status 事件广播给编辑器底部状态栏;
|
|
4209
|
+
* 差异审查期间静默(edrv 差异块存在时返回空,不与 Keep/Undo 浮层打架)。
|
|
4210
|
+
* 作者 ddj
|
|
4211
|
+
*/
|
|
4212
|
+
/**
|
|
4213
|
+
* 广播补全状态给编辑器底部状态栏(EditorView 监听 edrv:ai-status 渲染)。
|
|
4214
|
+
* @author ddj
|
|
4215
|
+
* @param state busy=请求中 ok=完成(detail.ms 耗时) error=失败(detail.note) idle=就绪 config=开关变化
|
|
4216
|
+
* @param detail 附加信息
|
|
4217
|
+
*/
|
|
4218
|
+
function emitAiStatus(state, detail) {
|
|
4219
|
+
try {
|
|
4220
|
+
window.dispatchEvent(new CustomEvent("edrv:ai-status", { detail: {
|
|
4221
|
+
state,
|
|
4222
|
+
detail
|
|
4223
|
+
} }));
|
|
4224
|
+
} catch {}
|
|
4225
|
+
}
|
|
4226
|
+
/** 去抖等待(毫秒):Monaco 逐键调用 provider,静默该时长才真正发 RPC;
|
|
4227
|
+
* 网关 TTFT 本身 3.5s+(debug 实证),client 侧去抖压到 150ms 不再叠加明显延迟。 */
|
|
4228
|
+
const DEBOUNCE_MS$1 = 150;
|
|
4229
|
+
/** 缓存容量上限(LRU 超限即整体清空;键空间小,够用)。 */
|
|
4230
|
+
const CACHE_MAX = 30;
|
|
4231
|
+
/** 缓存 TTL(毫秒)。 */
|
|
4232
|
+
const CACHE_TTL_MS = 6e4;
|
|
4233
|
+
let registered = false;
|
|
4234
|
+
let seq = 0;
|
|
4235
|
+
let inFlight = null;
|
|
4236
|
+
let debounceTimer = null;
|
|
4237
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4238
|
+
/** 上次开关状态(configUpdate 后经事件刷新,关闭时立即静默)。 */
|
|
4239
|
+
let enabled = false;
|
|
4240
|
+
/** 供设置面板保存后同步开关状态(不重载页面即时生效)。 */
|
|
4241
|
+
function setAiInlineEnabled(value) {
|
|
4242
|
+
enabled = value === true;
|
|
4243
|
+
emitAiStatus("config", { enabled });
|
|
4244
|
+
if (!enabled) {
|
|
4245
|
+
seq += 1;
|
|
4246
|
+
cache.clear();
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
/** 当前开关(EditorView 装配时决定是否注册)。 */
|
|
4250
|
+
function aiInlineEnabled() {
|
|
4251
|
+
return enabled;
|
|
4252
|
+
}
|
|
4253
|
+
/** 初始化开关(启动时从 configGet 拉一次)。 */
|
|
4254
|
+
async function initAiInlineState() {
|
|
4255
|
+
try {
|
|
4256
|
+
const res = await rpc("edrv.ai.configGet", {});
|
|
4257
|
+
setAiInlineEnabled(res?.ok && res.enabled === true);
|
|
4258
|
+
} catch {
|
|
4259
|
+
setAiInlineEnabled(false);
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
/**
|
|
4263
|
+
* 缓存键:路径 + 行:列 + 前缀尾 24 字符(同位置不同文本区分)。
|
|
4264
|
+
* @author ddj
|
|
4265
|
+
*/
|
|
4266
|
+
function cacheKey(path, line, column, prefix) {
|
|
4267
|
+
return path + ":" + line + ":" + column + ":" + String(prefix || "").slice(-24);
|
|
4268
|
+
}
|
|
4269
|
+
/**
|
|
4270
|
+
* 差异审查期间静默:编辑器上存在 AI 补全应避让的 pending 差异装饰时返回 true。
|
|
4271
|
+
* 通过编辑器实例上的 edrv 差异装饰类名判定,缺失视为无差异。
|
|
4272
|
+
* @author ddj
|
|
4273
|
+
*/
|
|
4274
|
+
function diffPending(editor) {
|
|
4275
|
+
try {
|
|
4276
|
+
return ((editor?.getModel?.())?.getAllDecorations?.() ?? []).some((d) => String(d?.options?.className || "").includes("edrv-mn-"));
|
|
4277
|
+
} catch {
|
|
4278
|
+
return false;
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
/**
|
|
4282
|
+
* 注册 AI 内联补全 provider(幂等;Monaco 就绪后调用一次)。
|
|
4283
|
+
* @author ddj
|
|
4284
|
+
* @param monaco window.monaco
|
|
4285
|
+
*/
|
|
4286
|
+
function registerAiInline(monaco) {
|
|
4287
|
+
if (registered || !monaco?.languages?.registerInlineCompletionsProvider) return;
|
|
4288
|
+
registered = true;
|
|
4289
|
+
monaco.languages.registerInlineCompletionsProvider("*", {
|
|
4290
|
+
async provideInlineCompletions(model, position, context, token) {
|
|
4291
|
+
const t0 = Date.now();
|
|
4292
|
+
if (!enabled || token?.isCancellationRequested) return { items: [] };
|
|
4293
|
+
const path = decodeURIComponent(String(model?.uri?.path || "").replace(/^\//, ""));
|
|
4294
|
+
if (!path || model.uri.scheme !== "edrv") return { items: [] };
|
|
4295
|
+
const editor = monacoRef?.activeEditor;
|
|
4296
|
+
if (editor && diffPending(editor)) return { items: [] };
|
|
4297
|
+
const offset = model.getOffsetAt(position);
|
|
4298
|
+
const value = model.getValue();
|
|
4299
|
+
const trimmed = trimInlineWindow(value.slice(0, offset), value.slice(offset));
|
|
4300
|
+
if (!inlineWorth(trimmed.prefix)) return { items: [] };
|
|
4301
|
+
const key = cacheKey(path, position.lineNumber, position.column, trimmed.prefix);
|
|
4302
|
+
const hit = cache.get(key);
|
|
4303
|
+
if (hit && Date.now() - hit.at < CACHE_TTL_MS) return hit.items.length ? { items: hit.items } : { items: [] };
|
|
4304
|
+
return new Promise((resolve) => {
|
|
4305
|
+
seq += 1;
|
|
4306
|
+
const mySeq = seq;
|
|
4307
|
+
if (inFlight) {
|
|
4308
|
+
try {
|
|
4309
|
+
inFlight.abort();
|
|
4310
|
+
} catch {}
|
|
4311
|
+
inFlight = null;
|
|
4312
|
+
}
|
|
4313
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
4314
|
+
debounceTimer = setTimeout(async () => {
|
|
4315
|
+
debounceTimer = null;
|
|
4316
|
+
if (mySeq !== seq || token?.isCancellationRequested) {
|
|
4317
|
+
resolve({ items: [] });
|
|
4318
|
+
return;
|
|
4319
|
+
}
|
|
4320
|
+
const controller = new AbortController();
|
|
4321
|
+
inFlight = controller;
|
|
4322
|
+
emitAiStatus("busy");
|
|
4323
|
+
try {
|
|
4324
|
+
const data = await (await fetch(RPC_PATH, {
|
|
4325
|
+
method: "POST",
|
|
4326
|
+
headers: { "content-type": "application/json" },
|
|
4327
|
+
body: JSON.stringify({
|
|
4328
|
+
method: "edrv.ai.inline",
|
|
4329
|
+
args: {
|
|
4330
|
+
path,
|
|
4331
|
+
prefix: trimmed.prefix,
|
|
4332
|
+
suffix: trimmed.suffix
|
|
4333
|
+
}
|
|
4334
|
+
}),
|
|
4335
|
+
signal: controller.signal
|
|
4336
|
+
})).json();
|
|
4337
|
+
if (mySeq !== seq || !data?.ok || typeof data.text !== "string" || !data.text) {
|
|
4338
|
+
cache.set(key, {
|
|
4339
|
+
items: [],
|
|
4340
|
+
at: Date.now()
|
|
4341
|
+
});
|
|
4342
|
+
if (data?.note) emitAiStatus("error", { note: String(data.note) });
|
|
4343
|
+
else emitAiStatus("idle");
|
|
4344
|
+
resolve({ items: [] });
|
|
4345
|
+
return;
|
|
4346
|
+
}
|
|
4347
|
+
const items = [{
|
|
4348
|
+
insertText: data.text,
|
|
4349
|
+
range: void 0
|
|
4350
|
+
}];
|
|
4351
|
+
cacheSet(key, items);
|
|
4352
|
+
emitAiStatus("ok", { ms: Date.now() - t0 });
|
|
4353
|
+
resolve({ items });
|
|
4354
|
+
} catch (error) {
|
|
4355
|
+
emitAiStatus("error", { note: String(error && error.message || error).slice(0, 80) });
|
|
4356
|
+
resolve({ items: [] });
|
|
4357
|
+
} finally {
|
|
4358
|
+
if (inFlight === controller) inFlight = null;
|
|
4359
|
+
}
|
|
4360
|
+
}, DEBOUNCE_MS$1);
|
|
4361
|
+
const guard = setInterval(() => {
|
|
4362
|
+
if (mySeq !== seq || token?.isCancellationRequested) {
|
|
4363
|
+
clearInterval(guard);
|
|
4364
|
+
if (debounceTimer) {
|
|
4365
|
+
clearTimeout(debounceTimer);
|
|
4366
|
+
debounceTimer = null;
|
|
4367
|
+
}
|
|
4368
|
+
resolve({ items: [] });
|
|
4369
|
+
} else if (!debounceTimer) clearInterval(guard);
|
|
4370
|
+
}, 120);
|
|
4371
|
+
});
|
|
4372
|
+
},
|
|
4373
|
+
freeInlineCompletions() {}
|
|
4374
|
+
});
|
|
4375
|
+
}
|
|
4376
|
+
/** 缓存写入(超限整体清空,LRU 简化)。 */
|
|
4377
|
+
function cacheSet(key, items) {
|
|
4378
|
+
if (cache.size >= CACHE_MAX) cache.clear();
|
|
4379
|
+
cache.set(key, {
|
|
4380
|
+
items,
|
|
4381
|
+
at: Date.now()
|
|
4382
|
+
});
|
|
4383
|
+
}
|
|
4384
|
+
/** EditorView ref 注入口(差异静默判定用当前编辑器)。 */
|
|
4385
|
+
let monacoRef = null;
|
|
4386
|
+
/** EditorView 装配后调用(注册 provider + 初始化开关)。 */
|
|
4387
|
+
function setupAiInline(monaco) {
|
|
4388
|
+
monacoRef = monaco && { activeEditor: null };
|
|
4389
|
+
if (!registered) registerAiInline(monaco);
|
|
4390
|
+
initAiInlineState();
|
|
4391
|
+
}
|
|
4392
|
+
/** EditorView 每次创建/切换编辑器实例时刷新引用。 */
|
|
4393
|
+
function trackAiEditor(editor) {
|
|
4394
|
+
if (monacoRef) monacoRef.activeEditor = editor;
|
|
4395
|
+
}
|
|
4396
|
+
//#endregion
|
|
4143
4397
|
//#region src/client/ui/EditorView.ts
|
|
4144
4398
|
/**
|
|
4145
4399
|
* dsh-vscode-mode client — EditorView:中央 VSCode 式文件编辑器(编排层)。
|
|
@@ -4179,6 +4433,12 @@ window.__ModuleLoader__.load({
|
|
|
4179
4433
|
const pdfHostRef = react.default.useRef(null);
|
|
4180
4434
|
const [status, setStatus] = react.default.useState("");
|
|
4181
4435
|
const [lspServers, setLspServers] = react.default.useState([]);
|
|
4436
|
+
const [aiStatus, setAiStatus] = react.default.useState({
|
|
4437
|
+
state: "idle",
|
|
4438
|
+
detail: null,
|
|
4439
|
+
enabled: false
|
|
4440
|
+
});
|
|
4441
|
+
const aiStatusTimer = react.default.useRef(null);
|
|
4182
4442
|
const [error, setError] = react.default.useState(null);
|
|
4183
4443
|
const [loadError, setLoadError] = react.default.useState(null);
|
|
4184
4444
|
const [loadStage, setLoadStage] = react.default.useState({
|
|
@@ -4650,6 +4910,39 @@ window.__ModuleLoader__.load({
|
|
|
4650
4910
|
clearInterval(timer);
|
|
4651
4911
|
};
|
|
4652
4912
|
}, [sessionId]);
|
|
4913
|
+
react.default.useEffect(() => {
|
|
4914
|
+
const apply = (state, detail) => {
|
|
4915
|
+
if (aiStatusTimer.current) {
|
|
4916
|
+
clearTimeout(aiStatusTimer.current);
|
|
4917
|
+
aiStatusTimer.current = null;
|
|
4918
|
+
}
|
|
4919
|
+
setAiStatus((prev) => ({
|
|
4920
|
+
state,
|
|
4921
|
+
detail: detail ?? null,
|
|
4922
|
+
enabled: state === "config" ? detail?.enabled === true : prev.enabled
|
|
4923
|
+
}));
|
|
4924
|
+
if (state === "ok") aiStatusTimer.current = setTimeout(() => setAiStatus((p) => p.state === "ok" ? {
|
|
4925
|
+
...p,
|
|
4926
|
+
state: "idle"
|
|
4927
|
+
} : p), 3e3);
|
|
4928
|
+
else if (state === "error") aiStatusTimer.current = setTimeout(() => setAiStatus((p) => p.state === "error" ? {
|
|
4929
|
+
...p,
|
|
4930
|
+
state: "idle"
|
|
4931
|
+
} : p), 1e4);
|
|
4932
|
+
};
|
|
4933
|
+
const onAiStatus = (e) => {
|
|
4934
|
+
if (e?.detail?.state) apply(e.detail.state, e.detail.detail);
|
|
4935
|
+
};
|
|
4936
|
+
const onAiConfig = (e) => apply("config", { enabled: e?.detail?.enabled === true });
|
|
4937
|
+
window.addEventListener("edrv:ai-status", onAiStatus);
|
|
4938
|
+
window.addEventListener("edrv:ai-config", onAiConfig);
|
|
4939
|
+
apply("config", { enabled: aiInlineEnabled() });
|
|
4940
|
+
return () => {
|
|
4941
|
+
window.removeEventListener("edrv:ai-status", onAiStatus);
|
|
4942
|
+
window.removeEventListener("edrv:ai-config", onAiConfig);
|
|
4943
|
+
if (aiStatusTimer.current) clearTimeout(aiStatusTimer.current);
|
|
4944
|
+
};
|
|
4945
|
+
}, []);
|
|
4653
4946
|
react.default.useEffect(() => {
|
|
4654
4947
|
const onOpen = (e) => {
|
|
4655
4948
|
const p = e?.detail?.path;
|
|
@@ -4934,7 +5227,9 @@ window.__ModuleLoader__.load({
|
|
|
4934
5227
|
}));
|
|
4935
5228
|
};
|
|
4936
5229
|
loadMonaco(onProgress).then((m) => {
|
|
4937
|
-
if (alive)
|
|
5230
|
+
if (!alive) return;
|
|
5231
|
+
setMonaco(m);
|
|
5232
|
+
setupAiInline(m);
|
|
4938
5233
|
}).catch((e) => {
|
|
4939
5234
|
if (alive) {
|
|
4940
5235
|
setMonacoErr(String(e?.message ?? e));
|
|
@@ -5134,7 +5429,8 @@ window.__ModuleLoader__.load({
|
|
|
5134
5429
|
renderWhitespace: "selection",
|
|
5135
5430
|
smoothScrolling: true,
|
|
5136
5431
|
cursorBlinking: "smooth",
|
|
5137
|
-
padding: { top: side ? 6 : 8 }
|
|
5432
|
+
padding: { top: side ? 6 : 8 },
|
|
5433
|
+
inlineSuggest: { enabled: true }
|
|
5138
5434
|
});
|
|
5139
5435
|
ed.onDidChangeModelContent(() => {
|
|
5140
5436
|
if (!ed.getModel() || programmaticRef.current) return;
|
|
@@ -5220,6 +5516,10 @@ window.__ModuleLoader__.load({
|
|
|
5220
5516
|
});
|
|
5221
5517
|
bindLspEditor(ed);
|
|
5222
5518
|
bindLspUnderline(ed, m);
|
|
5519
|
+
trackAiEditor(ed);
|
|
5520
|
+
ed.addCommand(m.KeyMod.Alt | m.KeyCode.Backslash, () => {
|
|
5521
|
+
ed.trigger("edrv-ai", "editor.action.inlineSuggest.trigger", null);
|
|
5522
|
+
});
|
|
5223
5523
|
const hideSoon = () => {
|
|
5224
5524
|
if (hoverEditorRef.current || hoverPanelRef.current || hideTimerRef.current) return;
|
|
5225
5525
|
hideTimerRef.current = setTimeout(() => {
|
|
@@ -5756,10 +6056,35 @@ window.__ModuleLoader__.load({
|
|
|
5756
6056
|
none: "未配置"
|
|
5757
6057
|
}[lspServer.source] ?? lspServer.source) : "LSP " + lspLanguage + " · 未启动";
|
|
5758
6058
|
const lspProgress = typeof lspServer?.progress === "number" ? Math.round(lspServer.progress) : null;
|
|
5759
|
-
const
|
|
5760
|
-
className: "edrv-
|
|
6059
|
+
const lspSeg = lspLanguage === "lua" || lspLanguage === "csharp" ? react.default.createElement("span", {
|
|
6060
|
+
className: "edrv-status-seg",
|
|
6061
|
+
style: {
|
|
6062
|
+
display: "inline-flex",
|
|
6063
|
+
alignItems: "center",
|
|
6064
|
+
gap: "10px",
|
|
6065
|
+
minWidth: 0,
|
|
6066
|
+
flex: "1 1 auto"
|
|
6067
|
+
},
|
|
5761
6068
|
title: lspServer?.progressMessage || lspLabel
|
|
5762
6069
|
}, react.default.createElement("span", { className: "edrv-lsp-status-dot " + (lspServer?.phase === "ready" ? "ready" : lspServer?.phase === "indexing" || lspServer?.phase === "starting" ? "busy" : "idle") }), react.default.createElement("span", { className: "edrv-sp-lsp" }, lspLabel), lspServer?.progressMessage ? react.default.createElement("span", { className: "edrv-sp-progress-message" }, lspServer.progressMessage) : null, lspProgress !== null ? react.default.createElement("span", { className: "edrv-sp-progress" }, lspProgress + "%") : null) : null;
|
|
6070
|
+
const aiEnabled = aiStatus.enabled;
|
|
6071
|
+
const aiState = aiStatus.state;
|
|
6072
|
+
const aiDotCls = aiState === "busy" ? "busy" : aiState === "ok" ? "ready" : aiState === "error" ? "idle" : "ready";
|
|
6073
|
+
const aiMsText = aiStatus.detail && aiStatus.detail.ms != null ? (aiStatus.detail.ms / 1e3).toFixed(1) + "s" : "";
|
|
6074
|
+
const aiNote = aiStatus.detail && aiStatus.detail.note ? String(aiStatus.detail.note) : "";
|
|
6075
|
+
const aiLabel = !aiEnabled ? "AI 补全 · 关" : aiState === "busy" ? "AI 补全请求中…" : aiState === "ok" ? "AI 补全完成" + (aiMsText ? " · " + aiMsText : "") : aiState === "error" ? "AI 补全失败 · " + aiNote.slice(0, 48) : "AI 补全就绪";
|
|
6076
|
+
const aiSeg = active && aiEnabled !== null ? react.default.createElement("span", {
|
|
6077
|
+
className: "edrv-status-seg",
|
|
6078
|
+
style: {
|
|
6079
|
+
display: "inline-flex",
|
|
6080
|
+
alignItems: "center",
|
|
6081
|
+
gap: "10px",
|
|
6082
|
+
minWidth: 0,
|
|
6083
|
+
flex: "0 0 auto"
|
|
6084
|
+
},
|
|
6085
|
+
title: aiState === "error" && aiNote ? aiNote : "AI 自动补全(设置页「AI 补全」可配置)"
|
|
6086
|
+
}, react.default.createElement("span", { className: "edrv-lsp-status-dot " + (!aiEnabled ? "idle" : aiDotCls) }), react.default.createElement("span", { className: "edrv-sp-lsp" }, aiLabel)) : null;
|
|
6087
|
+
const statusBar = lspSeg || aiSeg ? react.default.createElement("div", { className: "edrv-statusbar" }, lspSeg, aiSeg) : null;
|
|
5763
6088
|
const navBackTarget = navRef.current.peekBack();
|
|
5764
6089
|
const navForwardTarget = navRef.current.peekForward();
|
|
5765
6090
|
const navNameOf = (entry) => entry && entry.path ? String(entry.path).split(/[\\/]/).pop() : "";
|
|
@@ -6038,7 +6363,7 @@ window.__ModuleLoader__.load({
|
|
|
6038
6363
|
flexDirection: "column",
|
|
6039
6364
|
overflow: "hidden"
|
|
6040
6365
|
}
|
|
6041
|
-
}, pathBar, tabRow, sideHintEl, editorArea,
|
|
6366
|
+
}, pathBar, tabRow, sideHintEl, editorArea, statusBar);
|
|
6042
6367
|
const editorRow = react.default.createElement("div", { className: "edrv-editor-row" }, sidebarPanels ? react.default.createElement(SidebarView, {
|
|
6043
6368
|
registry: sidebarPanels,
|
|
6044
6369
|
ctx: sidebarCtx,
|
|
@@ -7947,6 +8272,111 @@ window.__ModuleLoader__.load({
|
|
|
7947
8272
|
}, busy === "add" ? "校验中…" : "添加")), react.default.createElement("small", null, "安装 = 复制到 <项目>/Packages/com.dsh.editor(内嵌包);更新 = 整目录替换;卸载 = 删除该目录。Unity 打开时切回窗口自动刷新生效。插件卸载/重载时会自动清理并恢复右键菜单注册(「移除注册」后不再恢复)。")));
|
|
7948
8273
|
}
|
|
7949
8274
|
//#endregion
|
|
8275
|
+
//#region src/client/ui/AiSettings.ts
|
|
8276
|
+
/**
|
|
8277
|
+
* dsh-vscode-mode client — AI 补全设置卡片。
|
|
8278
|
+
* 开关 + 模型下拉(edrv.ai.models 目录,按 provider 分组,默认「自动」)+
|
|
8279
|
+
* 思考强度下拉(随所选模型的 efforts 元数据动态渲染:无档位隐藏;含「跟随默认」)。
|
|
8280
|
+
* 保存走 edrv.ai.configUpdate;保存后广播 edrv:ai-config 事件让 provider 即时生效。
|
|
8281
|
+
* 作者 ddj
|
|
8282
|
+
*/
|
|
8283
|
+
/**
|
|
8284
|
+
* AI 补全设置卡片(设置页「AI 补全」Tab 主体)。
|
|
8285
|
+
* @author ddj
|
|
8286
|
+
*/
|
|
8287
|
+
function AiSettings() {
|
|
8288
|
+
const [cfg, setCfg] = react.default.useState(null);
|
|
8289
|
+
const [dir, setDir] = react.default.useState(null);
|
|
8290
|
+
const [note, setNote] = react.default.useState("");
|
|
8291
|
+
const [error, setError] = react.default.useState("");
|
|
8292
|
+
const [busy, setBusy] = react.default.useState(false);
|
|
8293
|
+
react.default.useEffect(() => {
|
|
8294
|
+
Promise.all([rpc("edrv.ai.configGet", {}), rpc("edrv.ai.models", {})]).then(([c, d]) => {
|
|
8295
|
+
if (c.ok) setCfg({
|
|
8296
|
+
enabled: c.enabled,
|
|
8297
|
+
provider: c.provider,
|
|
8298
|
+
model: c.model,
|
|
8299
|
+
effort: c.effort
|
|
8300
|
+
});
|
|
8301
|
+
else setError(c.error || "配置读取失败");
|
|
8302
|
+
if (d.ok) setDir(d);
|
|
8303
|
+
else setError(d.error || "模型目录读取失败");
|
|
8304
|
+
}).catch((e) => setError(String(e)));
|
|
8305
|
+
}, []);
|
|
8306
|
+
/** 所选 provider 下的模型条目(自动路由时取首组供档位参考)。 */
|
|
8307
|
+
const modelsOf = (provider) => {
|
|
8308
|
+
if (!dir?.providers?.length) return [];
|
|
8309
|
+
if (!provider) return dir.providers[0]?.models ?? [];
|
|
8310
|
+
return dir.providers.find((p) => p.id === provider)?.models ?? [];
|
|
8311
|
+
};
|
|
8312
|
+
/** 档位可选项:模型无 reasoning 元数据 → 空数组(选择器隐藏)。 */
|
|
8313
|
+
const efforts = (() => {
|
|
8314
|
+
const models = modelsOf(cfg?.provider);
|
|
8315
|
+
return models.find((m) => m.model === cfg?.model)?.efforts ?? models[0]?.efforts ?? [];
|
|
8316
|
+
})();
|
|
8317
|
+
const save = async (patch) => {
|
|
8318
|
+
setBusy(true);
|
|
8319
|
+
setError("");
|
|
8320
|
+
try {
|
|
8321
|
+
const res = await rpc("edrv.ai.configUpdate", patch);
|
|
8322
|
+
if (!res.ok) throw new Error(res.error || "保存失败");
|
|
8323
|
+
setCfg({
|
|
8324
|
+
enabled: res.enabled,
|
|
8325
|
+
provider: res.provider,
|
|
8326
|
+
model: res.model,
|
|
8327
|
+
effort: res.effort
|
|
8328
|
+
});
|
|
8329
|
+
setAiInlineEnabled(res.enabled);
|
|
8330
|
+
setNote("已保存");
|
|
8331
|
+
window.dispatchEvent(new CustomEvent("edrv:ai-config", { detail: { enabled: res.enabled } }));
|
|
8332
|
+
} catch (e) {
|
|
8333
|
+
setError(String(e));
|
|
8334
|
+
} finally {
|
|
8335
|
+
setBusy(false);
|
|
8336
|
+
}
|
|
8337
|
+
};
|
|
8338
|
+
const toggle = () => save({ enabled: !cfg.enabled });
|
|
8339
|
+
const pickModel = (value) => {
|
|
8340
|
+
if (!value) return save({
|
|
8341
|
+
provider: "",
|
|
8342
|
+
model: ""
|
|
8343
|
+
});
|
|
8344
|
+
const idx = value.indexOf("/");
|
|
8345
|
+
const provider = value.slice(0, idx);
|
|
8346
|
+
const model = value.slice(idx + 1);
|
|
8347
|
+
const keep = (modelsOf(provider).find((m) => m.model === model)?.efforts ?? []).some((e) => e.id === cfg.effort);
|
|
8348
|
+
save(keep ? {
|
|
8349
|
+
provider,
|
|
8350
|
+
model
|
|
8351
|
+
} : {
|
|
8352
|
+
provider,
|
|
8353
|
+
model,
|
|
8354
|
+
effort: ""
|
|
8355
|
+
});
|
|
8356
|
+
};
|
|
8357
|
+
if (!cfg) return react.default.createElement("div", { className: "vsm-mcp-empty" }, error || "正在读取 AI 补全配置…");
|
|
8358
|
+
const modelValue = cfg.provider && cfg.model ? cfg.provider + "/" + cfg.model : "";
|
|
8359
|
+
return react.default.createElement("section", { className: "vsm-lsp-card" }, react.default.createElement("h3", null, "AI 自动补全(实验)"), react.default.createElement("p", { className: "vsm-lsp-note" }, "编辑停顿后由模型生成内联建议(ghost text),Tab 接受,Alt+\\ 手动触发。每次补全是一次模型调用;建议选择非推理模型,且思考强度选「跟随默认」或最低档以获得更快响应。"), react.default.createElement("div", { className: "vsm-lsp-row" }, react.default.createElement("button", {
|
|
8360
|
+
className: cfg.enabled ? "vsm-primary" : "",
|
|
8361
|
+
disabled: busy,
|
|
8362
|
+
onClick: toggle
|
|
8363
|
+
}, cfg.enabled ? "已开启(点击关闭)" : "已关闭(点击开启)")), react.default.createElement("div", { className: "vsm-lsp-row" }, react.default.createElement("label", null, "模型", react.default.createElement("select", {
|
|
8364
|
+
value: modelValue,
|
|
8365
|
+
disabled: busy || !dir?.providers?.length,
|
|
8366
|
+
onChange: (e) => pickModel(e.target.value)
|
|
8367
|
+
}, react.default.createElement("option", { value: "" }, dir?.providers?.length ? "自动(取第一个可用模型)" : "无可用模型"), (dir?.providers ?? []).flatMap((p) => p.models.map((m) => react.default.createElement("option", {
|
|
8368
|
+
key: p.id + "/" + m.model,
|
|
8369
|
+
value: p.id + "/" + m.model
|
|
8370
|
+
}, p.name + " · " + m.name))))), efforts.length ? react.default.createElement("label", null, "思考强度", react.default.createElement("select", {
|
|
8371
|
+
value: cfg.effort,
|
|
8372
|
+
disabled: busy,
|
|
8373
|
+
onChange: (e) => save({ effort: e.target.value })
|
|
8374
|
+
}, react.default.createElement("option", { value: "" }, "跟随默认"), efforts.map((e) => react.default.createElement("option", {
|
|
8375
|
+
key: e.id,
|
|
8376
|
+
value: e.id
|
|
8377
|
+
}, e.name)))) : null), note && react.default.createElement("div", { className: "vsm-compat-item ok" }, react.default.createElement("span", { className: "vsm-compat-name" }, note)), error && react.default.createElement("div", { className: "vsm-mcp-error vsm-mcp-banner" }, error));
|
|
8378
|
+
}
|
|
8379
|
+
//#endregion
|
|
7950
8380
|
//#region src/client/ui/McpSettings.ts
|
|
7951
8381
|
/**
|
|
7952
8382
|
* dsh-vscode-mode client — VSCodeMode 设置区:通用 / 快捷键 / MCP 管理 / 语言服务器 / 性能优化 / 兼容性。
|
|
@@ -8458,6 +8888,7 @@ window.__ModuleLoader__.load({
|
|
|
8458
8888
|
});
|
|
8459
8889
|
else if (tab === "compat") body = react.default.createElement(CompatSection, { getSummary: compatSummary });
|
|
8460
8890
|
else if (tab === "lsp") body = react.default.createElement(LspSettings, null);
|
|
8891
|
+
else if (tab === "ai") body = react.default.createElement(AiSettings, null);
|
|
8461
8892
|
else body = react.default.createElement(PerfSettings, null);
|
|
8462
8893
|
return react.default.createElement("section", { className: "vsm-mcp-page" }, react.default.createElement("header", { className: "vsm-mcp-header" }, react.default.createElement("div", null, react.default.createElement("h2", null, "VSCodeMode"), react.default.createElement("p", null, "管理当前 profile 与各项目的 Model Context Protocol 服务。"))), react.default.createElement("nav", { className: "vsm-mcp-tabs" }, react.default.createElement("button", {
|
|
8463
8894
|
className: tab === "general" ? "active" : "",
|
|
@@ -8472,6 +8903,9 @@ window.__ModuleLoader__.load({
|
|
|
8472
8903
|
className: tab === "lsp" ? "active" : "",
|
|
8473
8904
|
onClick: () => setTab("lsp")
|
|
8474
8905
|
}, "语言服务器"), react.default.createElement("button", {
|
|
8906
|
+
className: tab === "ai" ? "active" : "",
|
|
8907
|
+
onClick: () => setTab("ai")
|
|
8908
|
+
}, "AI 补全"), react.default.createElement("button", {
|
|
8475
8909
|
className: tab === "perf" ? "active" : "",
|
|
8476
8910
|
onClick: () => setTab("perf")
|
|
8477
8911
|
}, "性能优化"), react.default.createElement("button", {
|