dsh-vscode-mode 0.5.0 → 0.5.1

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.
@@ -23,6 +23,16 @@ let pending = null
23
23
  let registered = false
24
24
  let disposer = null
25
25
 
26
+ /**
27
+ * 片段 provider 注销器的全局挂点名。
28
+ *
29
+ * 为什么挂 window:`window.monaco` 由 loader 注入后**跨插件重载存活**,而模块级
30
+ * registered/disposer 会随 bundle 重新求值复位 —— 只判 registered 会在每次重载后
31
+ * 重复注册补全 provider(同一编辑器出现重复候补)。故注销器落 window 供跨代认领与清理。
32
+ * @author ddj 2026年09月18号
33
+ */
34
+ const SNIPPET_GLOBAL = '__edrvSnippetsDisposer__'
35
+
26
36
  /** 当前会话 id(补全按会话工作区叠加项目片段;EditorView 装配时注入)。 */
27
37
  let sessionId = null
28
38
 
@@ -102,6 +112,12 @@ export function entriesForLanguage(entries, languageId) {
102
112
  */
103
113
  export function registerSnippetProvider(monaco) {
104
114
  if (registered || !monaco?.languages?.registerCompletionItemProvider) return
115
+ // 跨重载守卫:上一代 bundle 注册的 provider 仍在存活 window.monaco 上(注销器已落 window)
116
+ const host = /* @__PURE__ */ (typeof window === 'undefined' ? undefined : window)
117
+ if (host && host[SNIPPET_GLOBAL]) {
118
+ registered = true
119
+ return
120
+ }
105
121
  const snippetKind = monaco.languages.CompletionItemKind?.Snippet
106
122
  const asSnippet = monaco.languages.CompletionItemInsertTextRule?.InsertAsSnippet
107
123
  // 缺少片段枚举(精简版 Monaco)时不注册:否则候选项会退化为纯文本插入,误导用户
@@ -135,6 +151,8 @@ export function registerSnippetProvider(monaco) {
135
151
  }
136
152
  },
137
153
  })
154
+ // 注销器落 window:跨重载认领(见 SNIPPET_GLOBAL 说明)
155
+ if (host) host[SNIPPET_GLOBAL] = disposer
138
156
  }
139
157
 
140
158
  /**
@@ -147,13 +165,17 @@ export function setupSnippets(monaco) {
147
165
  }
148
166
 
149
167
  /**
150
- * 卸载:注销 provider(插件热重载/卸载时调用)。
151
- * @author ddj 2026年09月10号
168
+ * 卸载:注销 provider 并复位状态(插件热重载/卸载时调用)。
169
+ * 兼容上一代 bundle 遗留的 window 注销器(模块级 disposer 为空时仍能清干净)。
170
+ * @author ddj 2026年09月10号 / 2026年09月18号
152
171
  */
153
172
  export function disposeSnippets() {
154
- if (typeof disposer === 'function') {
155
- try { disposer.dispose?.() } catch { /* 已注销 */ }
173
+ const host = /* @__PURE__ */ (typeof window === 'undefined' ? undefined : window)
174
+ const target = (typeof disposer === 'function' ? disposer : null) ?? (host ? host[SNIPPET_GLOBAL] : null)
175
+ if (target && typeof target.dispose === 'function') {
176
+ try { target.dispose() } catch { /* 已注销 */ }
156
177
  }
178
+ if (host) delete host[SNIPPET_GLOBAL]
157
179
  disposer = null
158
180
  registered = false
159
181
  invalidateSnippets()
@@ -184,23 +184,48 @@ function isPinned(tab: TabLike): boolean {
184
184
  /**
185
185
  * 归一化持久化的页签数据:兼容旧版 `string[]`、去重、固定分区。
186
186
  * 损坏项(非字符串/非对象/无 path)直接丢弃。
187
- * @author ddj 2026年09月11号
187
+ *
188
+ * G9:传入 cwd 时把每个路径收敛为**页签规范形态**(工作区相对路径),
189
+ * 以迁移历史持久化数据里残留的绝对路径(见 {@link tabPathOf})。
190
+ * @author ddj 2026年09月11号 / 2026年09月18号
188
191
  * @param raw localStorage 解析结果(任意形状)
192
+ * @param cwd 会话工作区目录(可选;给出时同步归一化路径形态)
189
193
  * @returns 归一化后的页签数组
190
194
  */
191
- export function normalizeTabs(raw: unknown): TabLike[] {
195
+ export function normalizeTabs(raw: unknown, cwd?: string | null): TabLike[] {
192
196
  if (!Array.isArray(raw)) return []
193
197
  const seen = new Set<string>()
194
198
  const out: TabLike[] = []
195
199
  for (const item of raw) {
196
200
  const tab = tabOf(item)
197
- if (!tab || seen.has(tab.path)) continue
198
- seen.add(tab.path)
199
- out.push(tab)
201
+ if (!tab) continue
202
+ const path = tabPathOf(tab.path, cwd)
203
+ if (!path || seen.has(path)) continue
204
+ seen.add(path)
205
+ out.push(tab.pinned === true ? { path, pinned: true } : { path })
200
206
  }
201
207
  return pinnedFirst(out)
202
208
  }
203
209
 
210
+ /**
211
+ * 页签规范形态 = 工作区相对路径(G9)。
212
+ *
213
+ * 为什么需要:差异记录路径取自工具结果 `target.displayPath`(官方恒为绝对拼写),
214
+ * 而资源管理器树给出的是工作区相对路径。两者进页签后,地址栏形态不一致,且
215
+ * `insertTab` 按原串去重 → 同一文件可能出两个页签;绝对路径还会被
216
+ * `isTreeRevealable` 判为不可定位,「在资源管理器视图中显示」对差异入口失效。
217
+ * 统一经 {@link relativeOf} 收敛后,页签/地址栏/去重/持久化/`sameFile` 口径一致。
218
+ *
219
+ * 工作区外文件(全局片段/规则)由 relativeOf 自然回退为原绝对路径,语义不变。
220
+ * @author ddj 2026年09月18号
221
+ * @param path 原始路径(绝对或相对,两种分隔符均可)
222
+ * @param cwd 会话工作区目录(可空;无 cwd 时仅做分隔符归一)
223
+ * @returns 页签规范路径
224
+ */
225
+ export function tabPathOf(path: string, cwd?: string | null): string {
226
+ return relativeOf(path, cwd)
227
+ }
228
+
204
229
  /**
205
230
  * 解释单条持久化页签:字符串 = 路径(旧版),对象取 path/pinned。
206
231
  * @author ddj 2026年09月11号
@@ -9,6 +9,7 @@
9
9
  import React from 'react'
10
10
  import { rpc } from '../rpc.js'
11
11
  import { setAiInlineEnabled } from '../ai/inlineProvider.js'
12
+ import { isDelistedModel } from '../../shared/ai.js'
12
13
 
13
14
  /**
14
15
  * AI 补全设置卡片(设置页「AI 补全」Tab 主体)。
@@ -79,9 +80,16 @@ export function AiSettings() {
79
80
  if (!cfg) return React.createElement('div', { className: 'vsm-mcp-empty' }, error || '正在读取 AI 补全配置…')
80
81
 
81
82
  const modelValue = cfg.provider && cfg.model ? cfg.provider + '/' + cfg.model : ''
83
+ // G7:配置指向已从官方默认列表下架的模型(DSH 0.1.6-alpha.2 移除 V4 Flash 系列)。
84
+ // 仅提示,不擅自改写用户已保存的配置(避免覆盖其意图);清空即回到自动路由。
85
+ const delisted = isDelistedModel(cfg.model)
82
86
  return React.createElement('section', { className: 'vsm-lsp-card' },
83
87
  React.createElement('h3', null, 'AI 自动补全(实验)'),
84
88
  React.createElement('p', { className: 'vsm-lsp-note' }, '编辑停顿后由模型生成内联建议(ghost text),Tab 接受,Alt+\\ 手动触发。每次补全是一次模型调用;建议选择非推理模型,且思考强度选「跟随默认」或最低档以获得更快响应。'),
89
+ (delisted
90
+ ? React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' },
91
+ '当前模型「' + cfg.model + '」已从 DSH 默认模型列表移除(0.1.6-alpha.2 起)。该模型可能不再可用;将「模型」改回「自动」即可使用第一个可用模型(现有配置不会被自动改动)。')
92
+ : null),
85
93
  React.createElement('div', { className: 'vsm-lsp-row' },
86
94
  React.createElement('button', { className: cfg.enabled ? 'vsm-primary' : '', disabled: busy, onClick: toggle },
87
95
  cfg.enabled ? '已开启(点击关闭)' : '已关闭(点击开启)'),
@@ -56,7 +56,7 @@ import { SnippetsPicker } from './SnippetsPicker.js'
56
56
  import { invalidateSnippets, setSnippetsSession, setupSnippets } from '../snippets/provider.js'
57
57
  import {
58
58
  absoluteOf, ancestorDirsOf, applyClose, baseNameOf, closeAll, closeOthers, closeRight, closeSaved,
59
- insertTab, isTreeRevealable, normalizeTabs, pickActive, relativeOf, togglePin,
59
+ insertTab, isTreeRevealable, normalizeTabs, pickActive, relativeOf, tabPathOf, togglePin,
60
60
  } from '../tabActions.js'
61
61
  import { buildTabMenu } from '../tabMenu.js'
62
62
  import { ensureSvnChanges, ensureSvnStatus, getSvnChanges, getSvnStatus, refreshSvnChanges, svnAdd, svnChangeMapOf, svnDiffBase, svnEditorActions, svnRevert, svnTortoise, svnUpdate } from '../svnStatus.js'
@@ -307,6 +307,33 @@ export function EditorView(props) {
307
307
  if (select) setActive(path)
308
308
  }
309
309
 
310
+ /**
311
+ * 页签规范路径(G9):统一收敛为工作区相对路径后再进页签。
312
+ *
313
+ * 差异记录路径取自工具结果 `target.displayPath`(官方恒绝对),而资源管理器树给相对路径;
314
+ * 不归一会导致地址栏形态不一致、同文件出现两个页签(insertTab 按原串去重)、
315
+ * 且绝对路径被 isTreeRevealable 判为不可定位(「在资源管理器视图中显示」失效)。
316
+ * 工作区外文件由 relativeOf 回退原绝对路径,语义不变。
317
+ * @author ddj 2026年09月18号
318
+ * @param path 原始路径(绝对或相对)
319
+ * @returns 页签规范路径
320
+ */
321
+ const tabPath = (path) => tabPathOf(path, cwd)
322
+ /**
323
+ * 打开文件入口的统一收敛(G9):所有「进页签」路径都经此归一,避免逐点打补丁。
324
+ * @author ddj 2026年09月18号
325
+ * @param path 原始路径
326
+ * @param select 是否设为活动页签
327
+ * @returns 归一后的路径(空值返回 null)
328
+ */
329
+ const addTabNorm = (path, select) => {
330
+ if (!path) return null
331
+ const normalized = tabPath(path)
332
+ if (!normalized) return null
333
+ addTab(normalized, select)
334
+ return normalized
335
+ }
336
+
310
337
  /**
311
338
  * 保存当前活动文件的视图状态(光标/滚动/折叠)到工作区作用域缓存。
312
339
  * @author ddj 2026年08月28号
@@ -388,7 +415,7 @@ export function EditorView(props) {
388
415
  flushSave()
389
416
  saveViewState(active)
390
417
  navPendingRef.current = entry
391
- addTab(entry.path, true)
418
+ addTabNorm(entry.path, true)
392
419
  setFocusRequest((value) => value + 1) // 目标已是活动文件时也触发恢复
393
420
  }
394
421
 
@@ -832,20 +859,23 @@ export function EditorView(props) {
832
859
  const onOpen = (e) => {
833
860
  const p = e?.detail?.path
834
861
  if (!p) return
862
+ // G9:入口统一归一为工作区相对路径(差异栏/对话链接/LSP 跳转等都经此事件)
863
+ const normalized = tabPath(p)
864
+ if (!normalized) return
835
865
  if (e?.detail?.focusDiff === true) {
836
866
  recordNav()
837
- pendingFocusRef.current = { path: p, region: null }
867
+ pendingFocusRef.current = { path: normalized, region: null }
838
868
  setFocusRequest((value) => value + 1)
839
- addTab(p, true)
869
+ addTab(normalized, true)
840
870
  return
841
871
  }
842
872
  if (e?.detail?.line != null) {
843
873
  // LSP/搜索跳转:打开并定位到行列(endLine/endColumn 为目标区间,供落地高亮)
844
- openFileAt(p, e?.detail?.line, e?.detail?.column, e?.detail?.endLine, e?.detail?.endColumn)
874
+ openFileAt(normalized, e?.detail?.line, e?.detail?.column, e?.detail?.endLine, e?.detail?.endColumn)
845
875
  return
846
876
  }
847
877
  recordNav()
848
- addTab(p, true)
878
+ addTab(normalized, true)
849
879
  }
850
880
  const onShowLauncher = (event) => {
851
881
  const tab = event?.detail?.tab
@@ -939,10 +969,13 @@ export function EditorView(props) {
939
969
  ?? localStorage.getItem(CACHE_KEY.editorLegacy + String(scope))
940
970
  if (raw) {
941
971
  const saved = JSON.parse(raw)
942
- const restored = normalizeTabs(saved?.tabs)
972
+ // G9:迁移历史持久化里的绝对路径页签(旧版差异入口写入),并顺带按新形态去重
973
+ const restored = normalizeTabs(saved?.tabs, cwd)
943
974
  if (restored.length) {
944
975
  setTabs(restored)
945
- setActive(pickActive(restored, saved?.active))
976
+ // 活动路径同形态归一,否则恢复后匹配不到任何页签(pickActive 回退首个)
977
+ const wanted = typeof saved?.active === 'string' ? tabPathOf(saved.active, cwd) : saved?.active
978
+ setActive(pickActive(restored, wanted))
946
979
  }
947
980
  }
948
981
  } catch (e) { /* 损坏忽略 */ }
@@ -2495,8 +2528,9 @@ export function EditorView(props) {
2495
2528
  // 打开文件即离开基线差异审阅态(差异视图是「当前文件」的临时视图)
2496
2529
  setSvnDiff(null)
2497
2530
  recordNav()
2498
- addTab(path, true)
2499
- if (focusDiff) pendingFocusRef.current = { path, region: null }
2531
+ // G9:统一归一为页签规范形态(差异栏/启动器/树/命令栏等入口一致)
2532
+ const normalized = addTabNorm(path, true)
2533
+ if (focusDiff && normalized) pendingFocusRef.current = { path: normalized, region: null }
2500
2534
  }
2501
2535
 
2502
2536
  /**
@@ -2512,8 +2546,10 @@ export function EditorView(props) {
2512
2546
  const openFileAt = (path, line, column, endLine, endColumn) => {
2513
2547
  if (!path) return
2514
2548
  recordNav()
2515
- addTab(path, true)
2516
- pendingFocusRef.current = { path, region: null, line: line ?? null, column: column ?? 1, endLine: endLine ?? null, endColumn: endColumn ?? null }
2549
+ // G9:同 openFile,先归一再进页签,保证待跳转路径与 active 同形态
2550
+ const normalized = addTabNorm(path, true)
2551
+ if (!normalized) return
2552
+ pendingFocusRef.current = { path: normalized, region: null, line: line ?? null, column: column ?? 1, endLine: endLine ?? null, endColumn: endColumn ?? null }
2517
2553
  setFocusRequest((value) => value + 1)
2518
2554
  }
2519
2555
 
@@ -2683,7 +2719,9 @@ export function EditorView(props) {
2683
2719
  }, '保留本地')))
2684
2720
  }
2685
2721
 
2686
- const otherFiles = sum.pendingFiles.filter((f) => f.path !== active)
2722
+ // G9:记录路径为绝对、active 为页签规范形态(相对),必须经 sameFile 归一比较,
2723
+ // 否则当前活动文件会被误列入「其他差异文件」。
2724
+ const otherFiles = sum.pendingFiles.filter((f) => !sameFile(f.path, active))
2687
2725
 
2688
2726
  /**
2689
2727
  * 渲染编辑器/文件加载进度面板。
package/src/compat.ts CHANGED
@@ -133,7 +133,7 @@ export function detectGuards(ctx: Ctx): CompatAdapter[] {
133
133
  }
134
134
 
135
135
  /** 已实测覆盖的最高 DSH 版本(适配矩阵上界,超过则提示,见 buildReport)。 */
136
- const TESTED_DSH_MAX = '0.1.6-alpha.1'
136
+ const TESTED_DSH_MAX = '0.1.6-alpha.2'
137
137
 
138
138
  /** 版本适配机制状态行:DSH 版本探测 + 设置 section 安装策略。 */
139
139
  export function versionAdapters(dshVersion: string): CompatAdapter[] {
package/src/dshVersion.ts CHANGED
@@ -82,10 +82,17 @@ export function inDshRange(version: DshVersion | null | undefined, range: DshRan
82
82
  return true
83
83
  }
84
84
 
85
- /** 版本线标签:报告与文档展示版本归属;不可解析返回 '未知'。 */
85
+ /**
86
+ * 版本线标签:报告与文档展示版本归属;不可解析返回 '未知'。
87
+ * 逐线自新到旧匹配,先命中先返回(区间无上界,故顺序即优先级)。
88
+ * @author ddj 2026年09月02号 / 2026年09月18号
89
+ * @param input 版本串(如 '0.1.6-alpha.2')
90
+ * @returns 版本线标签
91
+ */
86
92
  export function familyLabel(input: string): string {
87
93
  const version = parseDshVersion(input)
88
94
  if (!version) return '未知'
95
+ if (inDshRange(version, { from: '0.1.6-alpha.2' })) return '0.1.6-alpha.2 及更新(会话多实例共存 + 回合改动卡片 + Office 侧栏预览 + 侧栏浏览器)'
89
96
  if (inDshRange(version, { from: '0.1.6-alpha.1' })) return '0.1.6-alpha 及更新(MCP SDK v2 + Web 侧边栏终端 + 文件链接默认侧栏预览)'
90
97
  if (inDshRange(version, { from: '0.1.5-alpha.1' })) return '0.1.5-alpha 及更新(官方右侧 Sidebar 编辑区 + sidebar.panellist)'
91
98
  if (inDshRange(version, { from: '0.1.3-alpha.1' })) return '0.1.3-alpha 及更新(对话文件链接=remote.session.openWorkspacePath)'
package/src/shared/ai.ts CHANGED
@@ -77,6 +77,31 @@ export interface AiConfigPatch {
77
77
  effort?: string
78
78
  }
79
79
 
80
+ /**
81
+ * 已从官方默认模型列表下架的模型标识(依据 DSH 0.1.6-alpha.2 release notes:
82
+ * 「默认模型列表移除 V4 Flash 和 V4 Flash Vision Exp」)。
83
+ *
84
+ * 用途:用户此前保存过这些模型时,配置仍指向它们,而新版模型目录不再列出 →
85
+ * 需给出可读降级提示(而非静默失败)。按模型名小写子串匹配,
86
+ * 因为 provider 前缀与具体版本后缀(如 -exp)在不同部署下写法不一。
87
+ */
88
+ export const DELISTED_MODEL_MARKERS: readonly string[] = ['v4-flash']
89
+
90
+ /**
91
+ * 配置指向的模型是否已被官方下架(G7)。
92
+ *
93
+ * 语义:仅当确实指定了 model(非空 = 非自动路由)且命中下架标识时返回 true。
94
+ * 空 model 表示「自动取首个可用模型」,不构成问题,故不提示。
95
+ * @author ddj 2026年09月18号
96
+ * @param model 配置中的模型标识(可空)
97
+ * @returns 是否已下架
98
+ */
99
+ export function isDelistedModel(model: unknown): boolean {
100
+ const text = typeof model === 'string' ? model.trim().toLowerCase() : ''
101
+ if (!text) return false
102
+ return DELISTED_MODEL_MARKERS.some((marker) => text.includes(marker))
103
+ }
104
+
80
105
  /**
81
106
  * 按 AI_PREFIX_MAX/AI_SUFFIX_MAX 裁剪前后缀(client 侧发请求前调用)。
82
107
  * @author ddj