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 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 || rec.conflict === true) return false;
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
- /** 计算文件内各差异区域(行范围 + old/new + 状态),用于行内绿标注与 DiffBox。 */
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 lines = splitLines(content);
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(content, entries.map((entry) => entry.hunk));
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(content, location.start) + 1;
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) setMonaco(m);
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 lspFooter = lspLanguage === "lua" || lspLanguage === "csharp" ? react.default.createElement("div", {
5760
- className: "edrv-statusbar",
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, lspFooter);
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", {