dsh-vscode-mode 0.1.57 → 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
@@ -107,6 +107,92 @@ window.__ModuleLoader__.load({
107
107
  } catch (e) {}
108
108
  }
109
109
  //#endregion
110
+ //#region src/shared/logger.ts
111
+ /**
112
+ * dsh-vscode-mode — 统一日志核心(双面共享,平台中立)。
113
+ * 输出规范:
114
+ * - 格式:`[dsh-vscode-mode][:scope] 消息`;前缀只在模块内部拼接,业务代码禁止手写前缀
115
+ * - 级别语义:debug=诊断明细 / info=装配与路由里程碑 / warn=降级与兼容回退 / error=失败
116
+ * - 出口:host 面经 bindHostLog(ctx) 绑定 ctx.logger(缺失回退 console);client 面固定 console
117
+ * - 诊断文件通道(edrv.debug → ~/.dsh/dsh-vscode-mode/logs/)见 debugLog.ts,与本模块前缀共用
118
+ * 新增日志一律 `import { log } from '../log.js'`(client 为 './log.js')后调 log.debug/info/warn/error;
119
+ * 子域日志用 log.child('scope'),禁止另起 console。
120
+ * 作者 ddj 2026-09-08
121
+ */
122
+ /** 插件统一日志前缀(与包安装名一致)。 */
123
+ const LOG_PREFIX = "[dsh-vscode-mode]";
124
+ /** 默认级别到 console 方法名的映射。 */
125
+ const CONSOLE_METHODS = {
126
+ debug: "debug",
127
+ info: "info",
128
+ warn: "warn",
129
+ error: "error"
130
+ };
131
+ /**
132
+ * 默认出口:console(级别直映方法;方法缺失时静默丢弃,不抛错)。
133
+ * @author ddj 2026年09月08号
134
+ * @returns console 出口
135
+ */
136
+ function consoleSink() {
137
+ return (level, line) => {
138
+ const emit = console[CONSOLE_METHODS[level]];
139
+ if (typeof emit === "function") emit.call(console, line);
140
+ };
141
+ }
142
+ /**
143
+ * 拼一行日志:`[dsh-vscode-mode][:scope] 消息`(scope 为空省略冒号段)。
144
+ * @author ddj 2026年09月08号
145
+ * @param scope 子域(可空)
146
+ * @param message 消息文本
147
+ * @returns 整行文本
148
+ */
149
+ function formatLine(scope, message) {
150
+ const text = String(message);
151
+ return scope ? LOG_PREFIX + ":" + scope + " " + text : LOG_PREFIX + " " + text;
152
+ }
153
+ /**
154
+ * 日志器工厂(内部):emit 时读共享出口状态,保证父 bind 后子实例跟随。
155
+ * @author ddj 2026年09月08号
156
+ * @param state 共享出口状态
157
+ * @param scope 子域文本(可空)
158
+ * @returns 日志器
159
+ */
160
+ function makeLogger(state, scope) {
161
+ const emit = (level, message) => state.sink(level, formatLine(scope, message));
162
+ return {
163
+ debug: (message) => emit("debug", message),
164
+ info: (message) => emit("info", message),
165
+ warn: (message) => emit("warn", message),
166
+ error: (message) => emit("error", message),
167
+ bind(next) {
168
+ state.sink = next;
169
+ },
170
+ child(next) {
171
+ return makeLogger(state, scope ? scope + "." + next : next);
172
+ }
173
+ };
174
+ }
175
+ /**
176
+ * 创建插件日志器(默认 console 出口;host 面单例由 bindHostLog 换到 ctx.logger)。
177
+ * @author ddj 2026年09月08号
178
+ * @param sink 初始出口(缺省 console)
179
+ * @param scope 初始子域(业务方一般经 child 派生,不直接传)
180
+ * @returns 日志器
181
+ */
182
+ function createLogger(sink = consoleSink(), scope = "") {
183
+ return makeLogger({ sink }, scope);
184
+ }
185
+ //#endregion
186
+ //#region src/client/log.ts
187
+ /**
188
+ * dsh-vscode-mode client — 日志单例(全 client 面唯一日志入口)。
189
+ * client 无 ctx.logger 服务,固定 console 出口;格式/级别语义与 host 面一致(shared/logger.ts)。
190
+ * 用法:`import { log } from './log.js'` → `log.debug/info/warn/error(msg)`;子域用 `log.child('scope')`。
191
+ * 作者 ddj 2026-09-08
192
+ */
193
+ /** client 面插件日志单例。 */
194
+ const log = createLogger();
195
+ //#endregion
110
196
  //#region src/client/sidebarBridge.ts
111
197
  /**
112
198
  * dsh-vscode-mode client — 侧边栏编辑区桥:可选探测 dsh-better-sidebar 的
@@ -224,7 +310,7 @@ window.__ModuleLoader__.load({
224
310
  try {
225
311
  return ensureSideEditor(path, focusDiff) === true;
226
312
  } catch (error) {
227
- console.warn("[dsh-vscode-mode] 侧栏编辑器打开失败(" + String(error) + "),已回退");
313
+ log.warn("侧栏编辑器打开失败(" + String(error) + "),已回退");
228
314
  return false;
229
315
  }
230
316
  }
@@ -260,7 +346,7 @@ window.__ModuleLoader__.load({
260
346
  try {
261
347
  registerLegacyFallback();
262
348
  } catch (error) {
263
- console.warn("[dsh-vscode-mode] 回退注册失败(" + String(error) + ")");
349
+ log.warn("回退注册失败(" + String(error) + ")");
264
350
  }
265
351
  }
266
352
  return false;
@@ -282,7 +368,7 @@ window.__ModuleLoader__.load({
282
368
  cwd: scope?.cwd
283
369
  });
284
370
  } catch (error) {
285
- console.warn("[dsh-vscode-mode] openTab 失败(" + String(error) + ")");
371
+ log.warn("openTab 失败(" + String(error) + ")");
286
372
  return false;
287
373
  }
288
374
  if (sideEditorMounted) window.dispatchEvent(new CustomEvent("edrv:open-editor", { detail: {
@@ -303,13 +389,13 @@ window.__ModuleLoader__.load({
303
389
  component: renderTab
304
390
  });
305
391
  } catch (error) {
306
- console.warn("[dsh-vscode-mode] 侧边栏 Tab 注册失败(" + String(error) + ")");
392
+ log.warn("侧边栏 Tab 注册失败(" + String(error) + ")");
307
393
  if (!fallbackInstalled) {
308
394
  fallbackInstalled = true;
309
395
  try {
310
396
  registerLegacyFallback();
311
397
  } catch (inner) {
312
- console.warn("[dsh-vscode-mode] 回退注册失败(" + String(inner) + ")");
398
+ log.warn("回退注册失败(" + String(inner) + ")");
313
399
  }
314
400
  }
315
401
  return () => {};
@@ -1763,6 +1849,22 @@ window.__ModuleLoader__.load({
1763
1849
  if (idx === 0 && record.callHunk && typeof record.callHunk.newText === "string") return record.callHunk;
1764
1850
  return null;
1765
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
+ }
1766
1868
  /** splitLines 的空文本语义:真实空文件没有一行变更内容。 */
1767
1869
  function splitLines(text) {
1768
1870
  return text.length ? text.split("\n") : [];
@@ -1893,7 +1995,7 @@ window.__ModuleLoader__.load({
1893
1995
  }
1894
1996
  /** 记录是否仍有待处理差异(superseded / 全部已决策 = false)。 */
1895
1997
  function isRecPending(rec) {
1896
- if (!rec || rec.superseded === true || rec.conflict === true) return false;
1998
+ if (!rec || rec.superseded === true) return false;
1897
1999
  const perHunk = Array.isArray(rec.decisions?.perHunk) ? rec.decisions.perHunk : [];
1898
2000
  if (perHunk.length) {
1899
2001
  for (let i = 0; i < perHunk.length; i++) if (perHunk[i] !== "accepted" && perHunk[i] !== "rejected" && !noopHunk(rec, (rec.hunks || [])[i])) return true;
@@ -1964,11 +2066,18 @@ window.__ModuleLoader__.load({
1964
2066
  shift: prefix
1965
2067
  };
1966
2068
  }
1967
- /** 计算文件内各差异区域(行范围 + 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
+ */
1968
2076
  function diffRegions(records, content) {
1969
2077
  const regions = [];
1970
2078
  if (content === null) return regions;
1971
- const lines = splitLines(content);
2079
+ const normalized = normalizeForCompare(content);
2080
+ const lines = splitLines(normalized);
1972
2081
  for (const rec of records) {
1973
2082
  if (rec.create) {
1974
2083
  for (let i = 0; i < rec.hunks.length; i++) {
@@ -1995,10 +2104,10 @@ window.__ModuleLoader__.load({
1995
2104
  const hunk = preciseHunk(rec, i);
1996
2105
  if (hunk && !noopHunk(rec, hunk)) entries.push({
1997
2106
  idx: i,
1998
- hunk
2107
+ hunk: normalizeHunk(hunk)
1999
2108
  });
2000
2109
  }
2001
- const locations = locateHunks(content, entries.map((entry) => entry.hunk));
2110
+ const locations = locateHunks(normalized, entries.map((entry) => entry.hunk));
2002
2111
  for (let i = 0; i < entries.length; i++) {
2003
2112
  const entry = entries[i];
2004
2113
  const location = locations[i];
@@ -2017,7 +2126,7 @@ window.__ModuleLoader__.load({
2017
2126
  });
2018
2127
  continue;
2019
2128
  }
2020
- const start = countLinesBefore(content, location.start) + 1;
2129
+ const start = countLinesBefore(normalized, location.start) + 1;
2021
2130
  const trimmed = trimCommonLines(entry.hunk.oldText === null ? [] : entry.hunk.oldText.split("\n"), entry.hunk.newText.split("\n"));
2022
2131
  const regionStart = start + trimmed.shift;
2023
2132
  regions.push({
@@ -3684,7 +3793,7 @@ window.__ModuleLoader__.load({
3684
3793
  tokenTypes: [...LSP_SEMANTIC_TOKEN_TYPES],
3685
3794
  tokenModifiers: [...LSP_SEMANTIC_TOKEN_MODIFIERS]
3686
3795
  };
3687
- let registered = false;
3796
+ let registered$1 = false;
3688
3797
  const disposables = [];
3689
3798
  /** 目标文件打开并定位(复用现有 openFileAt 的 edrv:open-editor 事件通道)。 */
3690
3799
  function openAt(path, line, column) {
@@ -3701,8 +3810,8 @@ window.__ModuleLoader__.load({
3701
3810
  }
3702
3811
  /** 注册全部 Monaco LSP provider 与文档跟踪(幂等)。 */
3703
3812
  function registerLspProviders(monaco) {
3704
- if (registered) return;
3705
- registered = true;
3813
+ if (registered$1) return;
3814
+ registered$1 = true;
3706
3815
  const attachModel = (model) => {
3707
3816
  if (!model || model.uri.scheme !== "edrv") return;
3708
3817
  const path = pathOfModel(model);
@@ -3947,7 +4056,7 @@ window.__ModuleLoader__.load({
3947
4056
  * 数据来源于语义 token(每 model+version 缓存 60s),不做逐词 LSP 查询。
3948
4057
  * 作者 ddj 2026-09-02
3949
4058
  */
3950
- const CACHE_TTL_MS = 6e4;
4059
+ const CACHE_TTL_MS$1 = 6e4;
3951
4060
  /**
3952
4061
  * 给一个 Monaco 编辑器绑定 Ctrl+hover 下划线提示(幂等)。
3953
4062
  * @author ddj 2026年09月02号
@@ -3977,7 +4086,7 @@ window.__ModuleLoader__.load({
3977
4086
  const rangesOf = async (model) => {
3978
4087
  const path = pathOfModel(model);
3979
4088
  const version = model.getVersionId?.() ?? 0;
3980
- 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;
3981
4090
  const res = await fetchSemanticTokens(path, model.getValue()).catch(() => null);
3982
4091
  const ranges = res && Array.isArray(res.data) ? decodeSemanticTokens(res.data) : [];
3983
4092
  cache = {
@@ -4054,6 +4163,237 @@ window.__ModuleLoader__.load({
4054
4163
  refreshStatus(true);
4055
4164
  }
4056
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
4057
4397
  //#region src/client/ui/EditorView.ts
4058
4398
  /**
4059
4399
  * dsh-vscode-mode client — EditorView:中央 VSCode 式文件编辑器(编排层)。
@@ -4093,6 +4433,12 @@ window.__ModuleLoader__.load({
4093
4433
  const pdfHostRef = react.default.useRef(null);
4094
4434
  const [status, setStatus] = react.default.useState("");
4095
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);
4096
4442
  const [error, setError] = react.default.useState(null);
4097
4443
  const [loadError, setLoadError] = react.default.useState(null);
4098
4444
  const [loadStage, setLoadStage] = react.default.useState({
@@ -4564,6 +4910,39 @@ window.__ModuleLoader__.load({
4564
4910
  clearInterval(timer);
4565
4911
  };
4566
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
+ }, []);
4567
4946
  react.default.useEffect(() => {
4568
4947
  const onOpen = (e) => {
4569
4948
  const p = e?.detail?.path;
@@ -4848,7 +5227,9 @@ window.__ModuleLoader__.load({
4848
5227
  }));
4849
5228
  };
4850
5229
  loadMonaco(onProgress).then((m) => {
4851
- if (alive) setMonaco(m);
5230
+ if (!alive) return;
5231
+ setMonaco(m);
5232
+ setupAiInline(m);
4852
5233
  }).catch((e) => {
4853
5234
  if (alive) {
4854
5235
  setMonacoErr(String(e?.message ?? e));
@@ -5048,7 +5429,8 @@ window.__ModuleLoader__.load({
5048
5429
  renderWhitespace: "selection",
5049
5430
  smoothScrolling: true,
5050
5431
  cursorBlinking: "smooth",
5051
- padding: { top: side ? 6 : 8 }
5432
+ padding: { top: side ? 6 : 8 },
5433
+ inlineSuggest: { enabled: true }
5052
5434
  });
5053
5435
  ed.onDidChangeModelContent(() => {
5054
5436
  if (!ed.getModel() || programmaticRef.current) return;
@@ -5134,6 +5516,10 @@ window.__ModuleLoader__.load({
5134
5516
  });
5135
5517
  bindLspEditor(ed);
5136
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
+ });
5137
5523
  const hideSoon = () => {
5138
5524
  if (hoverEditorRef.current || hoverPanelRef.current || hideTimerRef.current) return;
5139
5525
  hideTimerRef.current = setTimeout(() => {
@@ -5670,10 +6056,35 @@ window.__ModuleLoader__.load({
5670
6056
  none: "未配置"
5671
6057
  }[lspServer.source] ?? lspServer.source) : "LSP " + lspLanguage + " · 未启动";
5672
6058
  const lspProgress = typeof lspServer?.progress === "number" ? Math.round(lspServer.progress) : null;
5673
- const lspFooter = lspLanguage === "lua" || lspLanguage === "csharp" ? react.default.createElement("div", {
5674
- 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
+ },
5675
6068
  title: lspServer?.progressMessage || lspLabel
5676
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;
5677
6088
  const navBackTarget = navRef.current.peekBack();
5678
6089
  const navForwardTarget = navRef.current.peekForward();
5679
6090
  const navNameOf = (entry) => entry && entry.path ? String(entry.path).split(/[\\/]/).pop() : "";
@@ -5952,7 +6363,7 @@ window.__ModuleLoader__.load({
5952
6363
  flexDirection: "column",
5953
6364
  overflow: "hidden"
5954
6365
  }
5955
- }, pathBar, tabRow, sideHintEl, editorArea, lspFooter);
6366
+ }, pathBar, tabRow, sideHintEl, editorArea, statusBar);
5956
6367
  const editorRow = react.default.createElement("div", { className: "edrv-editor-row" }, sidebarPanels ? react.default.createElement(SidebarView, {
5957
6368
  registry: sidebarPanels,
5958
6369
  ctx: sidebarCtx,
@@ -6413,14 +6824,14 @@ window.__ModuleLoader__.load({
6413
6824
  function registerSlotSafely(ctx, spec, render) {
6414
6825
  const slots = ctx?.slots;
6415
6826
  if (!slots || typeof slots.inject !== "function" || typeof slots.register !== "function") {
6416
- console.warn("[dsh-vscode-mode] slots 服务不可用,跳过 slot " + spec.name);
6827
+ log.warn("slots 服务不可用,跳过 slot " + spec.name);
6417
6828
  return null;
6418
6829
  }
6419
6830
  try {
6420
6831
  const disposer = slots.inject(spec.name, () => slots.register(spec, render));
6421
6832
  return typeof disposer === "function" ? disposer : null;
6422
6833
  } catch (error) {
6423
- console.warn("[dsh-vscode-mode] slot " + spec.name + " 注册失败(" + String(error) + "),已跳过");
6834
+ log.warn("slot " + spec.name + " 注册失败(" + String(error) + "),已跳过");
6424
6835
  return null;
6425
6836
  }
6426
6837
  }
@@ -7861,6 +8272,111 @@ window.__ModuleLoader__.load({
7861
8272
  }, busy === "add" ? "校验中…" : "添加")), react.default.createElement("small", null, "安装 = 复制到 <项目>/Packages/com.dsh.editor(内嵌包);更新 = 整目录替换;卸载 = 删除该目录。Unity 打开时切回窗口自动刷新生效。插件卸载/重载时会自动清理并恢复右键菜单注册(「移除注册」后不再恢复)。")));
7862
8273
  }
7863
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
7864
8380
  //#region src/client/ui/McpSettings.ts
7865
8381
  /**
7866
8382
  * dsh-vscode-mode client — VSCodeMode 设置区:通用 / 快捷键 / MCP 管理 / 语言服务器 / 性能优化 / 兼容性。
@@ -8372,6 +8888,7 @@ window.__ModuleLoader__.load({
8372
8888
  });
8373
8889
  else if (tab === "compat") body = react.default.createElement(CompatSection, { getSummary: compatSummary });
8374
8890
  else if (tab === "lsp") body = react.default.createElement(LspSettings, null);
8891
+ else if (tab === "ai") body = react.default.createElement(AiSettings, null);
8375
8892
  else body = react.default.createElement(PerfSettings, null);
8376
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", {
8377
8894
  className: tab === "general" ? "active" : "",
@@ -8386,6 +8903,9 @@ window.__ModuleLoader__.load({
8386
8903
  className: tab === "lsp" ? "active" : "",
8387
8904
  onClick: () => setTab("lsp")
8388
8905
  }, "语言服务器"), react.default.createElement("button", {
8906
+ className: tab === "ai" ? "active" : "",
8907
+ onClick: () => setTab("ai")
8908
+ }, "AI 补全"), react.default.createElement("button", {
8389
8909
  className: tab === "perf" ? "active" : "",
8390
8910
  onClick: () => setTab("perf")
8391
8911
  }, "性能优化"), react.default.createElement("button", {
@@ -8914,12 +9434,12 @@ window.__ModuleLoader__.load({
8914
9434
  if (!params) return;
8915
9435
  const referrer = options.referrer ?? (typeof document !== "undefined" ? document.referrer : "");
8916
9436
  if (!referrerAllowed(referrer, options.origin ?? (typeof location !== "undefined" ? location.origin : ""))) {
8917
- console.warn("[dsh-vscode-mode] 已忽略跨源深链请求(referrer=" + referrer + ")");
9437
+ log.warn("已忽略跨源深链请求(referrer=" + referrer + ")");
8918
9438
  return;
8919
9439
  }
8920
9440
  stripCurrentUrl();
8921
9441
  openDeepLink(ctx, params).catch((error) => {
8922
- console.warn("[dsh-vscode-mode] 深链打开失败:" + String(error));
9442
+ log.warn("深链打开失败:" + String(error));
8923
9443
  toastDom("深链打开失败:" + String(error?.message ?? error));
8924
9444
  });
8925
9445
  }
@@ -11776,8 +12296,8 @@ window.__ModuleLoader__.load({
11776
12296
  let selected = autoValue("auto");
11777
12297
  /** 0.1.3+ 会话文件链接路由(remote.session.openWorkspacePath)是否已安装(compatSummary 展示用)。 */
11778
12298
  let remoteOpenInstalled = false;
11779
- /** 两条文件链接路由共用的路由日志(openPathRouter / remoteOpenRouter)。 */
11780
- const routeLogger = (message) => console.warn("[dsh-vscode-mode] " + message);
12299
+ /** 两条文件链接路由共用的路由日志(openPathRouter / remoteOpenRouter;统一走插件日志器)。 */
12300
+ const routeLogger = (message) => log.warn(message);
11781
12301
  /** 两条路由共用的 FileOpenContext:当前会话 id 与工作区 cwd。 */
11782
12302
  const openContext = () => {
11783
12303
  const current = sessions?.list?.getSnapshot?.();
@@ -11877,7 +12397,7 @@ window.__ModuleLoader__.load({
11877
12397
  let remoteRetries = 0;
11878
12398
  const retryRemoteOpen = () => {
11879
12399
  if (remoteOpenInstalled || remoteRetries >= 15) {
11880
- if (!remoteOpenInstalled) console.warn("[dsh-vscode-mode] 未探测到 remote.session.openWorkspacePath,0.1.3+ 会话文件链接路由未安装");
12400
+ if (!remoteOpenInstalled) log.warn("未探测到 remote.session.openWorkspacePath,0.1.3+ 会话文件链接路由未安装");
11881
12401
  return;
11882
12402
  }
11883
12403
  remoteRetries += 1;
@@ -11895,12 +12415,12 @@ window.__ModuleLoader__.load({
11895
12415
  logger: routeLogger
11896
12416
  });
11897
12417
  if (!disposer) {
11898
- console.warn("[dsh-vscode-mode] remote.session 存在但 openWorkspacePath 不可补丁,会话文件链接路由未安装");
12418
+ log.warn("remote.session 存在但 openWorkspacePath 不可补丁,会话文件链接路由未安装");
11899
12419
  return;
11900
12420
  }
11901
12421
  remoteOpenInstalled = true;
11902
12422
  ctx.effect(() => disposer, "vscode-mode: remote file link routing");
11903
- console.info("[dsh-vscode-mode] 已安装 0.1.3+ 会话文件链接路由(remote.session.openWorkspacePath)");
12423
+ log.info("已安装 0.1.3+ 会话文件链接路由(remote.session.openWorkspacePath)");
11904
12424
  }, 2e3);
11905
12425
  };
11906
12426
  retryRemoteOpen();
@@ -11971,7 +12491,7 @@ window.__ModuleLoader__.load({
11971
12491
  if (sideService !== void 0) return;
11972
12492
  sideService = detectSidebarService(ctx);
11973
12493
  if (sideService !== void 0) {
11974
- console.info("[dsh-vscode-mode] 检测到 dsh-better-sidebar,切换侧边栏编辑形态");
12494
+ log.info("检测到 dsh-better-sidebar,切换侧边栏编辑形态");
11975
12495
  applySideForm();
11976
12496
  } else retrySideService();
11977
12497
  }, 2e3);