dsh-vscode-mode 0.1.58 → 0.1.60

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() : "";
@@ -6018,7 +6343,7 @@ window.__ModuleLoader__.load({
6018
6343
  const copyInstallCmd = () => {
6019
6344
  if (navigator.clipboard?.writeText) navigator.clipboard.writeText(SIDEBAR_INSTALL_CMD).catch(() => {});
6020
6345
  };
6021
- const sideHintEl = layout === "tab" && sideHint && !hintDismissed ? react.default.createElement("div", { className: "edrv-side-hint" }, react.default.createElement("span", { className: "edrv-side-hint-text" }, "安装 dsh-better-sidebar 后可启用侧边栏编辑(对话+编辑同屏):"), react.default.createElement("code", { className: "edrv-side-hint-cmd" }, SIDEBAR_INSTALL_CMD), react.default.createElement("button", {
6346
+ const sideHintEl = layout === "tab" && sideHint && !hintDismissed ? react.default.createElement("div", { className: "edrv-side-hint" }, react.default.createElement("span", { className: "edrv-side-hint-text" }, "DSH 0.1.5+ 自带官方右侧侧边栏(升级即用);旧版可安装 dsh-better-sidebar 启用侧边栏编辑(对话+编辑同屏):"), react.default.createElement("code", { className: "edrv-side-hint-cmd" }, SIDEBAR_INSTALL_CMD), react.default.createElement("button", {
6022
6347
  className: "edrv-pill edrv-pill-ghost",
6023
6348
  title: "复制安装命令",
6024
6349
  onClick: copyInstallCmd
@@ -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,
@@ -6558,14 +6883,254 @@ window.__ModuleLoader__.load({
6558
6883
  };
6559
6884
  }
6560
6885
  //#endregion
6886
+ //#region src/client/officialSidebar.ts
6887
+ /**
6888
+ * dsh-vscode-mode client — 官方右侧 Sidebar 桥:探测 DSH 0.1.5-alpha.1+ 的
6889
+ * ctx.sidebarRightTabs / ctx.sidebarRight 服务,两段式注册「文件编辑」Tab
6890
+ * (类型定义进注册表,正文进 keyed slot sidebar.right.pane.tab)。
6891
+ * 可选依赖模式:服务缺失时调用方回退 better-sidebar / 中央页签;本模块不注册任何东西。
6892
+ * 维护约定:官方侧边栏(DSH 0.1.5+)是侧边栏编辑形态的唯一维护面;
6893
+ * dsh-better-sidebar 桥(sidebarBridge.ts)已归档,仅作旧版 DSH 回退,不再新增能力。
6894
+ * 纯逻辑(探测/导航参数解析)可单测,注册装配仅存在于 install 路径。
6895
+ * 作者 ddj 2026年09月09号
6896
+ */
6897
+ /** 官方 Tab 类型注册表服务名(ctx.get 用;不进 inject——旧版 DSH 无此包,inject 会停等)。 */
6898
+ const OFFICIAL_TABS_SERVICE = "sidebarRightTabs";
6899
+ /** 官方导航控制器服务名(ctx.get 用)。 */
6900
+ const OFFICIAL_SERVICE = "sidebarRight";
6901
+ /** 官方 keyed slot 名(Tab 正文挂点,由官方侧边栏的 rightbar seat 声明)。 */
6902
+ const OFFICIAL_SLOT_NAME = "sidebar.right.pane.tab";
6903
+ /** 本插件 Tab 类型定义 id(官方约定用包名;正文 slot 的 key 与之相同)。 */
6904
+ const OFFICIAL_TAB_ID = "dsh-vscode-mode";
6905
+ /** 本插件 Tab 的类型判别符(ctx.sidebarRight.openTab 的 kind)。 */
6906
+ const OFFICIAL_TAB_KIND = "edrvEditor";
6907
+ /** Tab 标题(与 better-sidebar 形态一致)。 */
6908
+ const OFFICIAL_TAB_TITLE = "文件编辑";
6909
+ /** file 地址认领的 patterns(extension 档接管官方 textpreview 的 fallback 认领,注销自动回落)。 */
6910
+ const OFFICIAL_FILE_PATTERN = "dsh-resource://file/**";
6911
+ /** file 资源类型的定义 id(正文 slot 的 key)。 */
6912
+ const OFFICIAL_FILE_TAB_ID = "dsh-vscode-mode/file";
6913
+ /** file 资源类型的 kind(与页类型 edrvEditor 区分:同 kind 只允许一份 extension)。 */
6914
+ const OFFICIAL_FILE_KIND = "edrvEditorFile";
6915
+ /**
6916
+ * 结构化探测官方右侧 Sidebar 服务(可选依赖:缺失/降级返回 undefined)。
6917
+ * 服务仅存在于 DSH 0.1.5-alpha.1+,探测命中 ≡ 版本判定成立。
6918
+ * @author ddj 2026年09月09号
6919
+ * @param ctx 客户端服务上下文
6920
+ * @returns 注册表与控制器对,或 undefined
6921
+ */
6922
+ function detectOfficial(ctx) {
6923
+ try {
6924
+ const tabs = ctx.get(OFFICIAL_TABS_SERVICE);
6925
+ const service = ctx.get(OFFICIAL_SERVICE);
6926
+ if (!tabs || typeof tabs.register !== "function") return void 0;
6927
+ if (!service || typeof service.openTab !== "function") return void 0;
6928
+ return {
6929
+ tabs,
6930
+ service
6931
+ };
6932
+ } catch {
6933
+ return;
6934
+ }
6935
+ }
6936
+ /**
6937
+ * 从官方导航参数安全解析打开请求(缺字段/坏类型一律降级为空请求,不抛错)。
6938
+ * @author ddj 2026年09月09号
6939
+ * @param params openTab 传入的 params(正文经 navigation.params 读回)
6940
+ * @returns 待打开路径与是否聚焦首个差异
6941
+ */
6942
+ function resolveNavOpen(params) {
6943
+ const value = params ?? {};
6944
+ return {
6945
+ path: typeof value?.openPath === "string" && value.openPath ? value.openPath : null,
6946
+ focusDiff: value?.focusDiff === true
6947
+ };
6948
+ }
6949
+ /** 逐段 component 解码(失败返回 null,对齐官方 parseFileAddress 的容错语义)。 */
6950
+ function decodeSegment(raw) {
6951
+ if (raw === void 0) return null;
6952
+ try {
6953
+ return decodeURIComponent(raw);
6954
+ } catch {
6955
+ return null;
6956
+ }
6957
+ }
6958
+ /**
6959
+ * 解析官方 file 资源地址(镜像 dsh-util-workspace-path 的 parseFileAddress 语法,不 import 官方包)。
6960
+ * session 域:`dsh-resource://file/session/<sessionId>/<逐段编码的相对路径>`;
6961
+ * absolute 域:`dsh-resource://file/absolute/<去前导 / 的绝对路径>`(POSIX 补回前导 /,
6962
+ * Windows 盘符 `C:/` 原样,UNC 保留空首段 `//server/share`)。
6963
+ * @author ddj 2026年09月09号
6964
+ * @param address 候选地址
6965
+ * @returns 文件路径(session 域附带 sessionId);非 file 地址/畸形/解码失败返回 null
6966
+ */
6967
+ function parseOfficialFileAddress(address) {
6968
+ if (typeof address !== "string" || !address.startsWith("dsh-resource://file/")) return null;
6969
+ const [scope, ...tail] = address.slice(20).split("/");
6970
+ const kind = decodeSegment(scope);
6971
+ if (kind === "session") {
6972
+ const sessionId = decodeSegment(tail[0]);
6973
+ const segments = tail.slice(1).map(decodeSegment);
6974
+ if (!sessionId || segments.length === 0 || segments.some((s) => s === null)) return null;
6975
+ const path = segments.join("/");
6976
+ return path ? {
6977
+ path,
6978
+ sessionId
6979
+ } : null;
6980
+ }
6981
+ if (kind === "absolute") {
6982
+ const unc = tail[0] === "" && tail.length > 1;
6983
+ const segments = (unc ? tail.slice(1) : tail).map(decodeSegment);
6984
+ if (segments.length === 0 || segments[0] === "" || segments.some((s) => s === null)) return null;
6985
+ const list = segments;
6986
+ if (unc) return { path: "//" + list.join("/") };
6987
+ if (/^[A-Za-z]:$/.test(list[0])) return { path: list.join("/") };
6988
+ return { path: "/" + list.join("/") };
6989
+ }
6990
+ return null;
6991
+ }
6992
+ /**
6993
+ * file 地址的 Tab chip 标题:解码后的 basename(解码失败回退原始串切片)。
6994
+ * @author ddj 2026年09月09号
6995
+ * @param address file 资源地址
6996
+ * @returns 标题文本
6997
+ */
6998
+ function officialFileTitle(address) {
6999
+ const source = parseOfficialFileAddress(address)?.path ?? (typeof address === "string" ? address : "");
7000
+ const normalized = source.replace(/\\/g, "/");
7001
+ return normalized.slice(normalized.lastIndexOf("/") + 1) || source;
7002
+ }
7003
+ /** component 编码一段(对齐官方 encodeSegment:盘符冒号保持字面)。 */
7004
+ function encodeSegment(segment) {
7005
+ return encodeURIComponent(segment).replace(/%3A/gi, ":");
7006
+ }
7007
+ /**
7008
+ * 构造官方 file 资源地址(镜像 fileAddressFor 的分派:绝对路径→absolute 域,相对→session 域)。
7009
+ * @author ddj 2026年09月09号
7010
+ * @param path 绝对或工作区相对路径(两种分隔符均可)
7011
+ * @param sessionId 会话 id(相对路径必需;绝对路径可省)
7012
+ * @returns `dsh-resource://file/…` 地址
7013
+ * @throws 路径为空,或相对路径缺 sessionId
7014
+ */
7015
+ function buildFileAddress(path, sessionId) {
7016
+ const posix = (typeof path === "string" ? path : "").replace(/\\/g, "/");
7017
+ if (!posix.trim()) throw new Error("buildFileAddress:路径为空");
7018
+ if (posix.startsWith("/") || posix.startsWith("//") || /^[A-Za-z]:[/\\]/.test(posix)) {
7019
+ const unc = posix.startsWith("//");
7020
+ const body = posix.replace(/^\/+/, "");
7021
+ return "dsh-resource://file/absolute/" + (unc ? "/" : "") + body.split("/").map(encodeSegment).join("/");
7022
+ }
7023
+ if (!sessionId) throw new Error("buildFileAddress:相对路径需要会话 id");
7024
+ const relative = posix.replace(/^(?:\.\/)+/, "").replace(/^\/+/, "");
7025
+ return "dsh-resource://file/session/" + encodeSegment(sessionId) + "/" + relative.split("/").map(encodeSegment).join("/");
7026
+ }
7027
+ /**
7028
+ * 注册官方 Tab 正文(keyed slot,key=定义 id;slots.inject 等待 rightbar seat 声明)。
7029
+ * @author ddj 2026年09月09号
7030
+ * @param slots slots 服务
7031
+ * @param key 正文注册键(= 类型定义 id)
7032
+ * @param renderTab 正文组件(包装后的 OfficialSideTab)
7033
+ * @returns 注销函数;注册抛错返回 null
7034
+ */
7035
+ function registerBodySlot(slots, key, renderTab) {
7036
+ try {
7037
+ const disposer = slots.inject(OFFICIAL_SLOT_NAME, () => slots.register({
7038
+ name: OFFICIAL_SLOT_NAME,
7039
+ key
7040
+ }, renderTab));
7041
+ return typeof disposer === "function" ? disposer : null;
7042
+ } catch (error) {
7043
+ log.warn("官方侧边栏正文 slot 注册失败(" + String(error) + ")");
7044
+ return null;
7045
+ }
7046
+ }
7047
+ /**
7048
+ * 注册官方右侧 Sidebar「文件编辑」Tab 并接管打开路由(两段式 + 路由)。
7049
+ * 调用方须把返回值挂进 ctx.effect;类型注册抛错时返回 null,由调用方回退旧形态。
7050
+ * @author ddj 2026年09月09号
7051
+ * @param options 装配参数(探测命中的一对服务 + slots + 正文组件 + guide 文案)
7052
+ * @returns 卸载器(反注册类型与正文 + 撤销路由);类型注册失败返回 null
7053
+ */
7054
+ function installOfficial(options) {
7055
+ const { tabs, service, slots, renderTab, guide } = options;
7056
+ const open = (path, focusDiff) => {
7057
+ try {
7058
+ service.openTab(OFFICIAL_TAB_KIND, { params: {
7059
+ openPath: path || void 0,
7060
+ focusDiff
7061
+ } });
7062
+ } catch (error) {
7063
+ log.warn("官方侧边栏打开失败(" + String(error) + ")");
7064
+ return false;
7065
+ }
7066
+ return true;
7067
+ };
7068
+ let typeDisposer = null;
7069
+ try {
7070
+ typeDisposer = tabs.register({
7071
+ id: OFFICIAL_TAB_ID,
7072
+ kind: OFFICIAL_TAB_KIND,
7073
+ priority: "extension",
7074
+ title: () => OFFICIAL_TAB_TITLE,
7075
+ guide: [{
7076
+ order: 20,
7077
+ title: guide.title,
7078
+ description: guide.description
7079
+ }]
7080
+ });
7081
+ } catch (error) {
7082
+ log.warn("官方侧边栏 Tab 类型注册失败(" + String(error) + "),回退旧形态");
7083
+ return null;
7084
+ }
7085
+ setEnsureSideEditor(open);
7086
+ const bodyDisposer = registerBodySlot(slots, OFFICIAL_TAB_ID, renderTab);
7087
+ return () => {
7088
+ if (typeof typeDisposer === "function") typeDisposer();
7089
+ if (typeof bodyDisposer === "function") bodyDisposer();
7090
+ setEnsureSideEditor(null);
7091
+ };
7092
+ }
7093
+ /**
7094
+ * 注册官方 file 资源地址认领(extension 档接管 `dsh-resource://file/**`,
7095
+ * 聊天文件链接/文件树打开改落本插件编辑器;注销后官方 textpreview 自动恢复)。
7096
+ * 认领与否由「文件链接使用工具」设置驱动(见 fileOpeners.shouldClaimFiles)。
7097
+ * @author ddj 2026年09月09号
7098
+ * @param options 装配参数(探测命中的注册表 + slots + 正文组件)
7099
+ * @returns 卸载器(反注册类型与正文);类型注册抛错返回 null(回落官方查看器)
7100
+ */
7101
+ function registerOfficialFileClaim(options) {
7102
+ const { tabs, slots, renderTab } = options;
7103
+ let typeDisposer = null;
7104
+ try {
7105
+ typeDisposer = tabs.register({
7106
+ id: OFFICIAL_FILE_TAB_ID,
7107
+ kind: OFFICIAL_FILE_KIND,
7108
+ patterns: [OFFICIAL_FILE_PATTERN],
7109
+ priority: "extension",
7110
+ canOpen: (address) => parseOfficialFileAddress(address) !== null,
7111
+ title: (address) => officialFileTitle(address)
7112
+ });
7113
+ } catch (error) {
7114
+ log.warn("官方 file 地址认领注册失败(" + String(error) + "),链接走官方查看器");
7115
+ return null;
7116
+ }
7117
+ const bodyDisposer = registerBodySlot(slots, OFFICIAL_FILE_TAB_ID, renderTab);
7118
+ return () => {
7119
+ if (typeof typeDisposer === "function") typeDisposer();
7120
+ if (typeof bodyDisposer === "function") bodyDisposer();
7121
+ };
7122
+ }
7123
+ //#endregion
6561
7124
  //#region src/client/fileOpeners.ts
6562
7125
  /**
6563
7126
  * dsh-vscode-mode client — 文件打开器注册表与自动选择。
6564
- * @author ddj 2026年08月24号
7127
+ * @author ddj 2026年08月24号 / 2026年09月09号
6565
7128
  */
6566
7129
  const AUTO_OPEN_TOOL = "auto";
6567
7130
  const VSCODE_OPEN_TOOL = "dsh-vscode-mode";
6568
7131
  const SIDEBAR_OPEN_TOOL = SIDEBAR_PLUGIN;
7132
+ /** 官方右侧侧边栏打开器 id(「文件链接使用工具」下拉的官方选项;DSH 0.1.5+)。 */
7133
+ const OFFICIAL_OPEN_TOOL = "dsh-official-sidebar";
6569
7134
  /**
6570
7135
  * 写入外部打开器并触发注册表通知。
6571
7136
  * @author ddj 2026年08月24号
@@ -6691,6 +7256,36 @@ window.__ModuleLoader__.load({
6691
7256
  const normalized = path.replace(/\\/g, "/");
6692
7257
  return normalized.slice(normalized.lastIndexOf("/") + 1) || path;
6693
7258
  }
7259
+ /**
7260
+ * 文件链接是否由本插件认领官方 file 地址:自动/VSCodeMode 档接管(链接进本插件编辑器),
7261
+ * 官方侧边栏/system/better-sidebar 档不认领(官方查看器或旧版路由生效)。
7262
+ * @author ddj 2026年09月09号
7263
+ * @param selected 「文件链接使用工具」当前值
7264
+ * @returns 是否认领
7265
+ */
7266
+ function shouldClaimFiles(selected) {
7267
+ return selected === "auto" || selected === "dsh-vscode-mode";
7268
+ }
7269
+ /**
7270
+ * 创建「官方侧边栏」文件打开器(DSH 0.1.5+,经 ctx.sidebarRight.openResource 打开官方查看器)。
7271
+ * 仅官方服务探测命中时注册;下拉选项随注册动态出现(McpSettings 动态渲染)。
7272
+ * @author ddj 2026年09月09号
7273
+ * @param service 官方导航控制器
7274
+ * @returns 文件打开器
7275
+ */
7276
+ function officialSidebarOpener(service) {
7277
+ return {
7278
+ id: OFFICIAL_OPEN_TOOL,
7279
+ label: "官方侧边栏",
7280
+ description: "在 DSH 官方右侧侧边栏查看器中打开(0.1.5+)",
7281
+ priority: 90,
7282
+ isAvailable: () => typeof service.openResource === "function",
7283
+ open: (path, context) => {
7284
+ if (typeof service.openResource !== "function") throw new Error("官方侧边栏 openResource 能力不可用");
7285
+ service.openResource(buildFileAddress(path, context.sessionId));
7286
+ }
7287
+ };
7288
+ }
6694
7289
  //#endregion
6695
7290
  //#region src/client/settingsContext.ts
6696
7291
  /**
@@ -7947,6 +8542,111 @@ window.__ModuleLoader__.load({
7947
8542
  }, busy === "add" ? "校验中…" : "添加")), react.default.createElement("small", null, "安装 = 复制到 <项目>/Packages/com.dsh.editor(内嵌包);更新 = 整目录替换;卸载 = 删除该目录。Unity 打开时切回窗口自动刷新生效。插件卸载/重载时会自动清理并恢复右键菜单注册(「移除注册」后不再恢复)。")));
7948
8543
  }
7949
8544
  //#endregion
8545
+ //#region src/client/ui/AiSettings.ts
8546
+ /**
8547
+ * dsh-vscode-mode client — AI 补全设置卡片。
8548
+ * 开关 + 模型下拉(edrv.ai.models 目录,按 provider 分组,默认「自动」)+
8549
+ * 思考强度下拉(随所选模型的 efforts 元数据动态渲染:无档位隐藏;含「跟随默认」)。
8550
+ * 保存走 edrv.ai.configUpdate;保存后广播 edrv:ai-config 事件让 provider 即时生效。
8551
+ * 作者 ddj
8552
+ */
8553
+ /**
8554
+ * AI 补全设置卡片(设置页「AI 补全」Tab 主体)。
8555
+ * @author ddj
8556
+ */
8557
+ function AiSettings() {
8558
+ const [cfg, setCfg] = react.default.useState(null);
8559
+ const [dir, setDir] = react.default.useState(null);
8560
+ const [note, setNote] = react.default.useState("");
8561
+ const [error, setError] = react.default.useState("");
8562
+ const [busy, setBusy] = react.default.useState(false);
8563
+ react.default.useEffect(() => {
8564
+ Promise.all([rpc("edrv.ai.configGet", {}), rpc("edrv.ai.models", {})]).then(([c, d]) => {
8565
+ if (c.ok) setCfg({
8566
+ enabled: c.enabled,
8567
+ provider: c.provider,
8568
+ model: c.model,
8569
+ effort: c.effort
8570
+ });
8571
+ else setError(c.error || "配置读取失败");
8572
+ if (d.ok) setDir(d);
8573
+ else setError(d.error || "模型目录读取失败");
8574
+ }).catch((e) => setError(String(e)));
8575
+ }, []);
8576
+ /** 所选 provider 下的模型条目(自动路由时取首组供档位参考)。 */
8577
+ const modelsOf = (provider) => {
8578
+ if (!dir?.providers?.length) return [];
8579
+ if (!provider) return dir.providers[0]?.models ?? [];
8580
+ return dir.providers.find((p) => p.id === provider)?.models ?? [];
8581
+ };
8582
+ /** 档位可选项:模型无 reasoning 元数据 → 空数组(选择器隐藏)。 */
8583
+ const efforts = (() => {
8584
+ const models = modelsOf(cfg?.provider);
8585
+ return models.find((m) => m.model === cfg?.model)?.efforts ?? models[0]?.efforts ?? [];
8586
+ })();
8587
+ const save = async (patch) => {
8588
+ setBusy(true);
8589
+ setError("");
8590
+ try {
8591
+ const res = await rpc("edrv.ai.configUpdate", patch);
8592
+ if (!res.ok) throw new Error(res.error || "保存失败");
8593
+ setCfg({
8594
+ enabled: res.enabled,
8595
+ provider: res.provider,
8596
+ model: res.model,
8597
+ effort: res.effort
8598
+ });
8599
+ setAiInlineEnabled(res.enabled);
8600
+ setNote("已保存");
8601
+ window.dispatchEvent(new CustomEvent("edrv:ai-config", { detail: { enabled: res.enabled } }));
8602
+ } catch (e) {
8603
+ setError(String(e));
8604
+ } finally {
8605
+ setBusy(false);
8606
+ }
8607
+ };
8608
+ const toggle = () => save({ enabled: !cfg.enabled });
8609
+ const pickModel = (value) => {
8610
+ if (!value) return save({
8611
+ provider: "",
8612
+ model: ""
8613
+ });
8614
+ const idx = value.indexOf("/");
8615
+ const provider = value.slice(0, idx);
8616
+ const model = value.slice(idx + 1);
8617
+ const keep = (modelsOf(provider).find((m) => m.model === model)?.efforts ?? []).some((e) => e.id === cfg.effort);
8618
+ save(keep ? {
8619
+ provider,
8620
+ model
8621
+ } : {
8622
+ provider,
8623
+ model,
8624
+ effort: ""
8625
+ });
8626
+ };
8627
+ if (!cfg) return react.default.createElement("div", { className: "vsm-mcp-empty" }, error || "正在读取 AI 补全配置…");
8628
+ const modelValue = cfg.provider && cfg.model ? cfg.provider + "/" + cfg.model : "";
8629
+ 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", {
8630
+ className: cfg.enabled ? "vsm-primary" : "",
8631
+ disabled: busy,
8632
+ onClick: toggle
8633
+ }, cfg.enabled ? "已开启(点击关闭)" : "已关闭(点击开启)")), react.default.createElement("div", { className: "vsm-lsp-row" }, react.default.createElement("label", null, "模型", react.default.createElement("select", {
8634
+ value: modelValue,
8635
+ disabled: busy || !dir?.providers?.length,
8636
+ onChange: (e) => pickModel(e.target.value)
8637
+ }, react.default.createElement("option", { value: "" }, dir?.providers?.length ? "自动(取第一个可用模型)" : "无可用模型"), (dir?.providers ?? []).flatMap((p) => p.models.map((m) => react.default.createElement("option", {
8638
+ key: p.id + "/" + m.model,
8639
+ value: p.id + "/" + m.model
8640
+ }, p.name + " · " + m.name))))), efforts.length ? react.default.createElement("label", null, "思考强度", react.default.createElement("select", {
8641
+ value: cfg.effort,
8642
+ disabled: busy,
8643
+ onChange: (e) => save({ effort: e.target.value })
8644
+ }, react.default.createElement("option", { value: "" }, "跟随默认"), efforts.map((e) => react.default.createElement("option", {
8645
+ key: e.id,
8646
+ value: e.id
8647
+ }, 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));
8648
+ }
8649
+ //#endregion
7950
8650
  //#region src/client/ui/McpSettings.ts
7951
8651
  /**
7952
8652
  * dsh-vscode-mode client — VSCodeMode 设置区:通用 / 快捷键 / MCP 管理 / 语言服务器 / 性能优化 / 兼容性。
@@ -8458,6 +9158,7 @@ window.__ModuleLoader__.load({
8458
9158
  });
8459
9159
  else if (tab === "compat") body = react.default.createElement(CompatSection, { getSummary: compatSummary });
8460
9160
  else if (tab === "lsp") body = react.default.createElement(LspSettings, null);
9161
+ else if (tab === "ai") body = react.default.createElement(AiSettings, null);
8461
9162
  else body = react.default.createElement(PerfSettings, null);
8462
9163
  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
9164
  className: tab === "general" ? "active" : "",
@@ -8472,6 +9173,9 @@ window.__ModuleLoader__.load({
8472
9173
  className: tab === "lsp" ? "active" : "",
8473
9174
  onClick: () => setTab("lsp")
8474
9175
  }, "语言服务器"), react.default.createElement("button", {
9176
+ className: tab === "ai" ? "active" : "",
9177
+ onClick: () => setTab("ai")
9178
+ }, "AI 补全"), react.default.createElement("button", {
8475
9179
  className: tab === "perf" ? "active" : "",
8476
9180
  onClick: () => setTab("perf")
8477
9181
  }, "性能优化"), react.default.createElement("button", {
@@ -9144,6 +9848,68 @@ window.__ModuleLoader__.load({
9144
9848
  });
9145
9849
  }
9146
9850
  //#endregion
9851
+ //#region src/client/ui/OfficialSideTab.ts
9852
+ /**
9853
+ * dsh-vscode-mode client — OfficialSideTab:DSH 官方右侧 Sidebar「文件编辑」Tab 正文包装。
9854
+ * 双用途:页类型 Tab(edrvEditor)经 navigation.params(openPath/focusDiff)打开;
9855
+ * file 资源类型 Tab(edrvEditorFile,认领 dsh-resource://file/**)经 navigation.address
9856
+ * 解码出真实路径并透传 params.line 行号定位。派发既有 edrv:open-editor 事件复用
9857
+ * EditorView 打开流;按会话 key 保证切会话干净重建。
9858
+ * Tab 正文切换后重挂载时按最新导航恢复打开(对齐 better-sidebar 形态的种子行为)。
9859
+ * 作者 ddj 2026年09月09号
9860
+ */
9861
+ /**
9862
+ * 官方侧边栏 Tab 正文组件(keyed slot sidebar.right.pane.tab 装配)。
9863
+ * @param props slot 框架注入(useTabInfo/sessionId)+ 装配依赖(schedule 等)
9864
+ * @returns EditorView 侧栏形态
9865
+ */
9866
+ function OfficialSideTab(props) {
9867
+ const sessionId = props?.sessionId;
9868
+ const useTabInfo = props?.useTabInfo;
9869
+ let navigation;
9870
+ if (typeof useTabInfo === "function") try {
9871
+ navigation = useTabInfo()?.tab?.navigation;
9872
+ } catch {
9873
+ navigation = void 0;
9874
+ }
9875
+ const revision = typeof navigation?.revision === "number" ? navigation.revision : -1;
9876
+ const revisionRef = react.default.useRef(-1);
9877
+ react.default.useEffect(() => {
9878
+ setSideEditorMounted(true);
9879
+ return () => setSideEditorMounted(false);
9880
+ }, []);
9881
+ react.default.useEffect(() => {
9882
+ if (revision === revisionRef.current) return;
9883
+ revisionRef.current = revision;
9884
+ if (revision <= 0) return;
9885
+ const fileHit = parseOfficialFileAddress(navigation?.address);
9886
+ if (fileHit) {
9887
+ const line = navigation?.params?.line;
9888
+ window.dispatchEvent(new CustomEvent("edrv:open-editor", { detail: {
9889
+ path: fileHit.path,
9890
+ line: typeof line === "number" ? line : void 0
9891
+ } }));
9892
+ return;
9893
+ }
9894
+ const request = resolveNavOpen(navigation?.params);
9895
+ window.dispatchEvent(new CustomEvent("edrv:open-editor", { detail: {
9896
+ path: request.path,
9897
+ focusDiff: request.focusDiff
9898
+ } }));
9899
+ });
9900
+ return react.default.createElement(EditorView, {
9901
+ key: sessionId,
9902
+ sessionId,
9903
+ layout: "side",
9904
+ schedule: props?.schedule,
9905
+ addToConversation: props?.addToConversation,
9906
+ sidebarPanels: props?.sidebarPanels,
9907
+ outlineSources: props?.outlineSources,
9908
+ fileMenuItems: props?.fileMenuItems,
9909
+ sessions: props?.sessions
9910
+ });
9911
+ }
9912
+ //#endregion
9147
9913
  //#region src/client/sidebar/registry.ts
9148
9914
  /** 校验面板定义并写入注册表(缺 id/render 抛 TypeError)。 */
9149
9915
  function panelRegister(entries, notify, panel) {
@@ -11798,8 +12564,8 @@ window.__ModuleLoader__.load({
11798
12564
  //#region src/client/index.ts
11799
12565
  /**
11800
12566
  * dsh-vscode-mode client — 浏览器半入口:slot 注册 + 装配。
11801
- * 挂点:betterSidebar「文件编辑」Tab(侧边栏形态,对话+编辑同屏;未装 dsh-better-sidebar 时
11802
- * 回退 conversation.view 中央页签)+ conversation.input.dock 差异条 + header 差异角标。
12567
+ * 挂点:官方右侧 Sidebar「文件编辑」Tab(DSH 0.1.5+,对话+编辑同屏)> betterSidebar Tab
12568
+ * (归档,仅旧版 DSH 回退)> conversation.view 中央页签 + conversation.input.dock 差异条 + header 差异角标。
11803
12569
  * 与 Host 通信:同源 fetch('/edrv/rpc')(shared/rpc 契约)。
11804
12570
  *
11805
12571
  * ⚠️ 跨版本 slot 装配(2026-08-21):新版 DSH 的 slots 系统要求 slot 必须由父 entry
@@ -11875,6 +12641,10 @@ window.__ModuleLoader__.load({
11875
12641
  };
11876
12642
  /** 可选探测 betterSidebar 服务(不进 inject:缺失会让插件停靠等待,杀死回退路径)。 */
11877
12643
  let sideService = detectSidebarService(ctx);
12644
+ /** 可选探测官方右侧 Sidebar(DSH 0.1.5-alpha.1+;探测命中 ≡ 版本判定,不进 inject 同上)。 */
12645
+ let officialService = detectOfficial(ctx);
12646
+ /** 官方 file 地址认领的当前注销器(null=未认领;由 syncFileClaim 依设置值切换)。 */
12647
+ let claimDisposer = null;
11878
12648
  /** 「添加到对话」动作集:编辑区/Tab 右键菜单注入文件引用与代码块(conversation 服务缺失时各动作安全降级)。 */
11879
12649
  const addToConversation = createAddToConversation(ctx);
11880
12650
  /** 客户端侧兼容摘要(与 host 报告合并展示;惰性求值保证 HMR 后仍新鲜)。 */
@@ -11900,9 +12670,19 @@ window.__ModuleLoader__.load({
11900
12670
  note: registry.get("dsh-better-sidebar") !== void 0 ? "已注册(优先级 80)" : "未检测到侧边栏打开能力"
11901
12671
  },
11902
12672
  {
11903
- name: "侧边栏编辑区(dsh-better-sidebar)",
12673
+ name: "侧边栏编辑区(官方 Sidebar)",
12674
+ active: officialService !== void 0,
12675
+ note: officialService !== void 0 ? "编辑区=官方右侧 Sidebar Tab(对话+编辑同屏,DSH 0.1.5+)" : "官方侧边栏服务未探测到(DSH < 0.1.5-alpha.1 时属预期)"
12676
+ },
12677
+ {
12678
+ name: "侧边栏编辑区(dsh-better-sidebar,归档)",
11904
12679
  active: sideService !== void 0,
11905
- note: sideService !== void 0 ? "编辑区=侧边栏 Tab(对话+编辑同屏)" : "未检测到;安装 dsh-better-sidebar 后刷新启用侧边栏形态(" + SIDEBAR_INSTALL_CMD + ")"
12680
+ note: sideService !== void 0 ? "编辑区=侧边栏 Tab(旧版 DSH 回退形态)" : "未检测到;DSH ≥ 0.1.5 优先官方侧边栏,旧版可安装(" + SIDEBAR_INSTALL_CMD + ")"
12681
+ },
12682
+ {
12683
+ name: "文件链接官方认领(dsh-resource://file)",
12684
+ active: claimDisposer !== null,
12685
+ note: claimDisposer !== null ? "聊天文件链接由本插件编辑器接管(官方侧边栏内打开)" : "链接走官方查看器或旧版路由(未认领)"
11906
12686
  }
11907
12687
  ];
11908
12688
  ctx.provide("fileOpeners", registry);
@@ -11930,12 +12710,39 @@ window.__ModuleLoader__.load({
11930
12710
  const sidebar = scanSidebar(ctx);
11931
12711
  return sidebar ? registry.register(sidebar) : void 0;
11932
12712
  }, "vscode-mode: sidebar file opener");
12713
+ const officialRenderTab = (props) => react.default.createElement(OfficialSideTab, Object.assign({}, props, {
12714
+ schedule,
12715
+ addToConversation,
12716
+ sidebarPanels,
12717
+ outlineSources,
12718
+ fileMenuItems,
12719
+ sessions
12720
+ }));
12721
+ /** 官方 file 地址认领同步:自动/VSCodeMode 档认领(链接进本插件编辑器),其余交官方查看器。 */
12722
+ const syncFileClaim = () => {
12723
+ const official = officialService;
12724
+ const want = official !== void 0 && shouldClaimFiles(selected);
12725
+ if (want && claimDisposer === null && official) {
12726
+ const disposer = registerOfficialFileClaim({
12727
+ tabs: official.tabs,
12728
+ slots: ctx.slots,
12729
+ renderTab: officialRenderTab
12730
+ });
12731
+ if (disposer !== null) claimDisposer = ctx.effect(() => disposer, "vscode-mode: official file claim");
12732
+ return;
12733
+ }
12734
+ if (!want && claimDisposer !== null) {
12735
+ claimDisposer();
12736
+ claimDisposer = null;
12737
+ }
12738
+ };
11933
12739
  ctx.effect(() => {
11934
12740
  if (!settings) return void 0;
11935
12741
  const sync = () => {
11936
12742
  const snapshot = settings.getSnapshot();
11937
12743
  if (snapshot.status === "loading") return;
11938
12744
  selected = autoValue(snapshot.value?.fileOpenTool);
12745
+ syncFileClaim();
11939
12746
  window.dispatchEvent(new CustomEvent("edrv:file-open-tool-change", { detail: { value: selected } }));
11940
12747
  };
11941
12748
  sync();
@@ -12008,11 +12815,50 @@ window.__ModuleLoader__.load({
12008
12815
  sessions
12009
12816
  })));
12010
12817
  };
12818
+ let officialDisposer = null;
12011
12819
  let sideDisposer = null;
12012
12820
  let legacyDisposer = null;
12821
+ const installOfficialForm = () => {
12822
+ const official = officialService;
12823
+ if (officialDisposer !== null || !official) return;
12824
+ if (sideDisposer !== null) {
12825
+ sideDisposer();
12826
+ sideDisposer = null;
12827
+ }
12828
+ if (legacyDisposer !== null) {
12829
+ legacyDisposer();
12830
+ legacyDisposer = null;
12831
+ }
12832
+ const outcome = installOfficial({
12833
+ tabs: official.tabs,
12834
+ service: official.service,
12835
+ slots: ctx.slots,
12836
+ renderTab: officialRenderTab,
12837
+ guide: {
12838
+ title: () => OFFICIAL_TAB_TITLE,
12839
+ description: () => "Monaco 文件编辑器:对话与编辑同屏(差异审查/跳转/大纲)"
12840
+ }
12841
+ });
12842
+ if (outcome === null) {
12843
+ officialService = void 0;
12844
+ applySideForm();
12845
+ return;
12846
+ }
12847
+ officialDisposer = ctx.effect(() => outcome, "vscode-mode: official sidebar editor tab");
12848
+ if (typeof official.service.openResource === "function") ctx.effect(() => registry.register(officialSidebarOpener(official.service)), "vscode-mode: official sidebar opener");
12849
+ syncFileClaim();
12850
+ };
12013
12851
  const applySideForm = () => {
12852
+ if (officialService) {
12853
+ installOfficialForm();
12854
+ return;
12855
+ }
12014
12856
  if (sideService) {
12015
12857
  if (sideDisposer !== null) return;
12858
+ if (officialDisposer !== null) {
12859
+ officialDisposer();
12860
+ officialDisposer = null;
12861
+ }
12016
12862
  if (legacyDisposer !== null) {
12017
12863
  legacyDisposer();
12018
12864
  legacyDisposer = null;
@@ -12038,28 +12884,41 @@ window.__ModuleLoader__.load({
12038
12884
  },
12039
12885
  registerLegacyFallback: registerLegacyTab
12040
12886
  }), "vscode-mode: sidebar editor tab");
12041
- } else {
12042
- if (legacyDisposer !== null) return;
12043
- if (sideDisposer !== null) {
12044
- sideDisposer();
12045
- sideDisposer = null;
12046
- }
12047
- setEnsureSideEditor(null);
12048
- legacyDisposer = registerLegacyTab();
12887
+ return;
12888
+ }
12889
+ if (legacyDisposer !== null) return;
12890
+ if (officialDisposer !== null) {
12891
+ officialDisposer();
12892
+ officialDisposer = null;
12049
12893
  }
12894
+ if (sideDisposer !== null) {
12895
+ sideDisposer();
12896
+ sideDisposer = null;
12897
+ }
12898
+ setEnsureSideEditor(null);
12899
+ legacyDisposer = registerLegacyTab();
12050
12900
  };
12051
12901
  applySideForm();
12052
12902
  let sideRetries = 0;
12053
12903
  const retrySideService = () => {
12054
- if (sideService !== void 0 || sideRetries >= 15) return;
12904
+ if (officialService !== void 0 || sideRetries >= 15) return;
12055
12905
  sideRetries += 1;
12056
12906
  schedule(() => {
12057
- if (sideService !== void 0) return;
12058
- sideService = detectSidebarService(ctx);
12059
- if (sideService !== void 0) {
12060
- log.info("检测到 dsh-better-sidebar,切换侧边栏编辑形态");
12907
+ if (officialService !== void 0) return;
12908
+ officialService = detectOfficial(ctx);
12909
+ if (officialService !== void 0) {
12910
+ log.info("检测到官方右侧 Sidebar(DSH 0.1.5+),切换官方侧边栏编辑形态");
12061
12911
  applySideForm();
12062
- } else retrySideService();
12912
+ return;
12913
+ }
12914
+ if (sideService === void 0) {
12915
+ sideService = detectSidebarService(ctx);
12916
+ if (sideService !== void 0) {
12917
+ log.info("检测到 dsh-better-sidebar,切换侧边栏编辑形态(官方未探测到,后续仍优先官方)");
12918
+ applySideForm();
12919
+ }
12920
+ }
12921
+ retrySideService();
12063
12922
  }, 2e3);
12064
12923
  };
12065
12924
  retrySideService();