dsh-vscode-mode 0.7.0 → 0.9.0

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.
Files changed (60) hide show
  1. package/README.md +37 -6
  2. package/assets/icon.svg +7 -0
  3. package/cordis.patch.yml +16 -0
  4. package/lib/client.js +2804 -300
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +1975 -191
  7. package/lib/index.js.map +1 -1
  8. package/locale/en.json +4 -0
  9. package/locale/zh.json +4 -0
  10. package/package.json +4 -2
  11. package/src/ai/inline.ts +19 -2
  12. package/src/ai/svnTriage.ts +150 -0
  13. package/src/capture.ts +225 -51
  14. package/src/client/addToConversation.ts +203 -1
  15. package/src/client/compat.ts +134 -4
  16. package/src/client/imagePreview.ts +76 -2
  17. package/src/client/index.ts +65 -36
  18. package/src/client/monaco/lsp/lspClient.ts +46 -0
  19. package/src/client/monaco/lsp/providers.ts +245 -1
  20. package/src/client/nativeOpenStore.ts +91 -0
  21. package/src/client/officialSidebar.ts +11 -45
  22. package/src/client/outline/index.ts +1 -1
  23. package/src/client/pdf/pdfPanel.ts +18 -0
  24. package/src/client/pendingNav.ts +141 -0
  25. package/src/client/settingsContext.ts +6 -4
  26. package/src/client/sidebar/panels/FileExplorer.ts +2 -15
  27. package/src/client/sidebar/panels/SvnPanel.ts +682 -72
  28. package/src/client/sidebar/panels/index.ts +1 -1
  29. package/src/client/sidebar/panels/svnListBudget.ts +58 -0
  30. package/src/client/state/records.ts +7 -1
  31. package/src/client/styles/editor.css +47 -5
  32. package/src/client/svnStatus.ts +206 -1
  33. package/src/client/svnStore.ts +6 -1
  34. package/src/client/ui/AiSettings.ts +47 -14
  35. package/src/client/ui/DiffBox.ts +1 -1
  36. package/src/client/ui/DiffLauncher.ts +6 -4
  37. package/src/client/ui/EditorView.ts +149 -22
  38. package/src/client/ui/McpSettings.ts +30 -0
  39. package/src/client/ui/PerfSettings.ts +4 -1
  40. package/src/client/ui/SvnAiPlanDialog.ts +293 -0
  41. package/src/client/ui/icons.ts +91 -0
  42. package/src/compat.ts +26 -5
  43. package/src/dshVersion.ts +2 -1
  44. package/src/fileOpenSettings.ts +274 -49
  45. package/src/index.ts +18 -1
  46. package/src/lsp/client.ts +24 -0
  47. package/src/lsp/rpc.ts +57 -0
  48. package/src/lsp/server.ts +272 -2
  49. package/src/model.ts +2 -1
  50. package/src/perf.ts +90 -4
  51. package/src/remoteWorkspace.ts +177 -0
  52. package/src/routes.ts +13 -1
  53. package/src/rpc.ts +115 -39
  54. package/src/shared/ai.ts +12 -0
  55. package/src/shared/lsp.ts +79 -0
  56. package/src/shared/nativeOpen.ts +91 -0
  57. package/src/shared/rpc.ts +162 -2
  58. package/src/shared/svn.ts +296 -10
  59. package/src/shared/types.ts +5 -1
  60. package/src/svn.ts +235 -13
@@ -2,10 +2,11 @@
2
2
  * dsh-vscode-mode host — 文件链接打开工具 + 快捷键的持久化设置。
3
3
  * 依赖守卫:schemastery 动态加载;@deepseek-ai/dsh-settings 仅作 legacy 探测——
4
4
  * rc 线导出 installSettingsSection free function,0.1.2-alpha 起移除(改由 settings
5
- * 服务的 installSection 方法承载,见 runSettingsInstall 三策略)。
5
+ * 服务的 installSection 方法承载),0.1.7 再移除 installSection(设置并入 profile
6
+ * 插件 Config schema,见 runSettingsInstall 四策略:legacy/service/forms/none)。
6
7
  * 缺失/任一策略失败时插件仍可加载(fileOpenTool 降级为配置值,compat 报告可见),
7
8
  * 全程 try/catch,不产生未捕获 rejection。
8
- * 作者 ddj 2026年08月24号 / 2026年08月26号 / 2026年09月02号
9
+ * 作者 ddj 2026年08月24号 / 2026年08月26号 / 2026年09月02号 / 2026年09月22号 / 2026年09月23号
9
10
  */
10
11
  import { createRequire } from 'node:module'
11
12
  import { pathToFileURL } from 'node:url'
@@ -15,6 +16,7 @@ import { EDITOR_LIMIT_DEFAULT } from './shared/editorLimit.js'
15
16
  import { INTEGRATION_BASE_DEFAULT } from './shared/integration.js'
16
17
  import { TORTOISE_DIR_DEFAULT } from './shared/svn.js'
17
18
  import type { AiConfigPatch, AiConfigView } from './shared/ai.js'
19
+ import { DEFAULT_NATIVE_CSV } from './shared/nativeOpen.js'
18
20
  import { log } from './log.js'
19
21
 
20
22
  export const FILE_OPEN_SETTINGS_NS = 'dsh-vscode-mode'
@@ -22,12 +24,15 @@ export const FILE_OPEN_DEFAULT = 'auto'
22
24
  export interface FileOpenSettings { fileOpenTool: string; integrationBaseUrl: string; svnPath: string; tortoisePath: string }
23
25
  export interface FileOpenSettingsState { value: string; revision?: number; update: (value: string, expectedRevision?: number) => Promise<void> }
24
26
 
25
- /** AI 内联补全配置默认值(默认关闭;路由空 = 自动;档位空 = 跟随模型默认)。 */
26
- export const AI_CONFIG_DEFAULT: { enabled: boolean; provider: string; model: string; effort: string } = {
27
+ /** AI 配置默认值(补全默认关;路由空 = 自动;档位空 = 跟随模型默认;任务模型空 = 跟随补全配置)。 */
28
+ export const AI_CONFIG_DEFAULT: { enabled: boolean; provider: string; model: string; effort: string; taskProvider: string; taskModel: string; taskEffort: string } = {
27
29
  enabled: false,
28
30
  provider: '',
29
31
  model: '',
30
32
  effort: '',
33
+ taskProvider: '',
34
+ taskModel: '',
35
+ taskEffort: '',
31
36
  }
32
37
 
33
38
  type SettingsProvider = {
@@ -170,11 +175,12 @@ export function loadSettingsDeps(importFn: (specifier: string) => Promise<unknow
170
175
  }
171
176
 
172
177
  /** 设置 section 安装策略(版本适配机制的观测值)。 */
173
- export type SettingsInstallStrategy = 'legacy' | 'service' | 'none' | 'unknown'
178
+ export type SettingsInstallStrategy = 'legacy' | 'service' | 'forms' | 'none' | 'unknown'
174
179
 
175
180
  const INSTALL_UNMOUNTED = '设置 section 尚未装配'
176
181
  const INSTALL_LEGACY = 'rc 线:dsh-settings.installSettingsSection'
177
182
  const INSTALL_SERVICE = '0.1.2-alpha 线:settings 服务 installSection'
183
+ const INSTALL_FORMS = '0.1.7 线:设置并入插件 Config schema(SettingsForms)'
178
184
  const INSTALL_NONE = '两路均不可用:设置持久化降级为配置值'
179
185
 
180
186
  interface ObservedInstall { strategy: SettingsInstallStrategy; note: string }
@@ -201,15 +207,54 @@ export function resetSettingsInstallObserved(): void {
201
207
  observedInstall = { strategy: 'unknown', note: INSTALL_UNMOUNTED }
202
208
  }
203
209
 
210
+ /**
211
+ * 判定设置写冲突错误(0.1.7 SettingsConflictError:code 常量 / 构造器名双通道)。
212
+ * @author ddj 2026年09月22号
213
+ * @param error 捕获到的错误
214
+ * @returns 是否为 revision 冲突
215
+ */
216
+ function isConflictError(error: unknown): boolean {
217
+ if (!error || typeof error !== 'object') return false
218
+ const e = error as { code?: unknown; name?: unknown }
219
+ return e.code === 'SETTINGS_CONFLICT' || e.name === 'SettingsConflictError'
220
+ }
221
+
222
+ /**
223
+ * 0.1.7 forms 策略:订阅 settings/document-updated 事件,目标 namespace 变化时
224
+ * 把 describe 最新值推给 hooks(setSource + onChange),替代旧 installSection 的
225
+ * 变更回调;disposer 经 ctx.effect 随插件 fiber 卸载(热重载不泄漏)。
226
+ * @author ddj 2026年09月22号
227
+ * @param ctx DSH host 上下文
228
+ * @param provider settings 服务(SettingsForms)
229
+ * @param ns 目标设置命名空间
230
+ * @param hooks 设置源绑定与变更回调
231
+ */
232
+ function watchDocUpdates(ctx: Ctx, provider: SettingsProvider, ns: string, hooks: { setSource: (source: () => unknown) => void; onChange: () => void }): void {
233
+ try {
234
+ const bus = ctx as { on?: (name: string, handler: (updatedNs: unknown, revision: unknown) => void) => unknown; effect?: (fn: () => unknown) => unknown }
235
+ if (typeof bus.on !== 'function') return
236
+ const off = bus.on('settings/document-updated', (updatedNs: unknown) => {
237
+ if (updatedNs !== ns) return
238
+ hooks.setSource(() => readSectionValue(provider, ns))
239
+ hooks.onChange()
240
+ })
241
+ if (typeof bus.effect === 'function' && typeof off === 'function') bus.effect(() => off)
242
+ } catch (error) {
243
+ log.warn('settings/document-updated 订阅失败:' + String(error))
244
+ }
245
+ }
246
+
204
247
  /**
205
248
  * 设置 section 版本自适应安装(核心策略分派)。
206
249
  * legacy:dsh-settings 仍导出 installSettingsSection(rc 线)→ 原样调用,行为与旧版一致。
207
250
  * service:该导出已移除(0.1.2-alpha 起)→ 经 ctx.inject(['settings']) 走服务方法
208
251
  * provider.installSection(owner, ns, schema, entry, hooks)(等义封装,含
209
- * base 层与 fiber 卸载回退)。回调内方法缺失再降级记录 none。
252
+ * base 层与 fiber 卸载回退)。
253
+ * forms:0.1.7 起 installSection 移除,设置并入 profile 插件 Config(SettingsForms
254
+ * 的 describe+update 在场即成立)→ 不装 section,改订阅 document-updated。
210
255
  * none:两条路由都不存在 → 仅记录并告警,调用方按配置值运行。
211
256
  * 全程不抛错、不产生未捕获 rejection。
212
- * @author ddj 2026年09月02号
257
+ * @author ddj 2026年09月02号(2026年09月22号 增补 forms 策略)
213
258
  * @param ctx DSH host 上下文(inject 可选探测)
214
259
  * @param ns 设置命名空间
215
260
  * @param schema schemastery schema
@@ -253,6 +298,13 @@ export async function runSettingsInstall(
253
298
  const provider = typeof sc.get === 'function' ? sc.get('settings') : sc.settings
254
299
  const install = (provider as { installSection?: unknown } | undefined)?.installSection
255
300
  if (typeof install !== 'function') {
301
+ // 0.1.7:installSection 已移除;SettingsForms(describe+update)在场走 forms 策略
302
+ const forms = provider as SettingsProvider | undefined
303
+ if (typeof forms?.describe === 'function' && typeof forms?.update === 'function') {
304
+ recordInstall('forms', INSTALL_FORMS)
305
+ watchDocUpdates(ctx, forms, ns, hooks)
306
+ return
307
+ }
256
308
  log.warn('settings 服务无 installSection(DSH 版本 API 变化),section ' + ns + ' 降级为配置值')
257
309
  recordInstall('none', INSTALL_NONE)
258
310
  return
@@ -286,30 +338,214 @@ function keybindingsShape(z: SettingsDeps['z']): Record<string, unknown> {
286
338
  return shape
287
339
  }
288
340
 
341
+ /**
342
+ * 设置节 describe 项的形状校验:value 须为普通对象。
343
+ * ns 在两代语义不同(旧=自装 section 名,0.1.7=profile entry id),同名撞车时
344
+ * 以形状兜底防误命中非设置数据;0.1.7 下本插件 entry 的 Config 值与旧 section
345
+ * 值同为设置键对象,两形状天然兼容。
346
+ * @author ddj 2026年09月22号
347
+ * @param value describe 项的 value 字段
348
+ * @returns 是否具备设置节形状
349
+ */
350
+ function isSectionShaped(value: unknown): boolean {
351
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
352
+ }
353
+
354
+ /** describe 项最小形状(读写工具共用)。 */
355
+ export interface SectionDescriptor { ns?: string; value?: unknown; revision?: number }
356
+
357
+ /**
358
+ * 按 ns + 形状校验查找设置节(host 读路径统一入口)。
359
+ * @author ddj 2026年09月22号
360
+ * @param provider settings 服务(可空)
361
+ * @param ns 设置命名空间
362
+ * @returns 命中的 describe 项;未命中返回 undefined
363
+ */
364
+ export function sectionOf(provider: SettingsProvider | undefined, ns: string): SectionDescriptor | undefined {
365
+ const items = provider?.describe?.({ redactSecrets: true })
366
+ if (!items) return undefined
367
+ return items.find((item) => item.ns === ns && isSectionShaped(item.value))
368
+ }
369
+
370
+ /** 读设置节存储值(未就绪/形状不符返回 undefined)。 */
371
+ function readSectionValue(provider: SettingsProvider | undefined, ns: string): unknown {
372
+ return sectionOf(provider, ns)?.value
373
+ }
374
+
375
+ /** 设置写入结果(ok=false 时带原因;conflict=true 表示经冲突重试后成功)。 */
376
+ export interface SectionWriteResult { ok: boolean; conflict?: boolean; error?: string }
377
+
378
+ /**
379
+ * 带冲突自愈的设置写入:0.1.7 SettingsConflictError(revision 拒写)时重读
380
+ * revision 重试一次;其余错误与服务缺失一律结构化返回,绝不抛未捕获异常。
381
+ * @author ddj 2026年09月22号
382
+ * @param provider settings 服务(可空)
383
+ * @param ns 设置命名空间
384
+ * @param patch 写入字段
385
+ * @param expectedRevision 读侧携带的 revision(缺省用 describe 最新值)
386
+ * @returns 写入结果
387
+ */
388
+ export async function updateSection(provider: SettingsProvider | undefined, ns: string, patch: object, expectedRevision?: number): Promise<SectionWriteResult> {
389
+ if (!provider?.update) return { ok: false, error: 'settings 服务无 update' }
390
+ const first = expectedRevision ?? sectionOf(provider, ns)?.revision
391
+ try {
392
+ await provider.update(ns, patch, first)
393
+ return { ok: true }
394
+ } catch (error) {
395
+ if (!isConflictError(error)) return { ok: false, error: String(error) }
396
+ }
397
+ const fresh = sectionOf(provider, ns)?.revision
398
+ try {
399
+ await provider.update(ns, patch, fresh)
400
+ return { ok: true, conflict: true }
401
+ } catch (error) {
402
+ return { ok: false, conflict: true, error: String(error) }
403
+ }
404
+ }
405
+
406
+ /**
407
+ * 构建设置 section schema(install 路径与插件 Config 声明共用,保证两代形状同源)。
408
+ * 字段集 = fileOpenTool/keybindings/sidebarMinWidth/maxOpenEditors/integrationBaseUrl/
409
+ * aiInline/aiProvider/aiModel/aiEffort/svnPath/tortoisePath/nativeOpenExts,全部带默认值
410
+ * (Config 启动校验在 undefined/空配置下自动填充,rc/alpha 两代 cordis 均通过)。
411
+ * options.volatile:DSH 0.1.7 线的 Config 导出专用——SettingsForms.describe 只下发
412
+ * 含 volatile 字段的 entry(volatileForm 门槛),不标记则 ns 永不出现在 describe、
413
+ * client configForms 恒 unavailable;且字段须位于固定 object 路径(keybindings 整个
414
+ * object 标记、子键不标)。section 安装路径(legacy/service)**不标**:rc/0.1.6 的
415
+ * register→resolve 无该门槛,标了反而让 scope.get() 返回引用污染读取。
416
+ * 标记经 markVolatile 能力守卫:z 无 .volatile()(旧 schemastery)或调用抛错时保持
417
+ * 原字段并记录观测态,模块加载绝不抛错(降级=维持现状,兼容性报告可见)。
418
+ * @author ddj 2026年09月22号(2026年09月23号 增补 options.volatile 与观测)
419
+ * @param z schemastery 命名空间(静态 import 或动态加载均可)
420
+ * @param options volatile:是否按 0.1.7 Config 语义标记字段
421
+ * @returns schemastery object schema
422
+ */
423
+ export function buildSettingsSchema(z: SettingsDeps['z'], options?: { volatile?: boolean }): unknown {
424
+ const wantVolatile = options?.volatile === true
425
+ let marked = 0
426
+ const withVol = (field: unknown): unknown => {
427
+ if (!wantVolatile) return field
428
+ const result = markVolatile(field)
429
+ if (result.marked) marked += 1
430
+ return result.field
431
+ }
432
+ const shape: Record<string, unknown> = {
433
+ fileOpenTool: withVol(z.string().default(FILE_OPEN_DEFAULT)),
434
+ keybindings: withVol(z.object(keybindingsShape(z)).default({ ...KEYBINDING_DEFAULTS })),
435
+ sidebarMinWidth: withVol(z.number().default(300)),
436
+ // 页签数量上限(0 = 不限制;超限时淘汰最久未使用的页签,固定页签除外)
437
+ maxOpenEditors: withVol(z.number().default(EDITOR_LIMIT_DEFAULT)),
438
+ integrationBaseUrl: withVol(z.string().default(INTEGRATION_BASE_DEFAULT)),
439
+ // AI 内联补全(默认关;provider/model 空 = 自动路由;effort 空 = 跟随模型默认)
440
+ aiInline: withVol(z.boolean().default(AI_CONFIG_DEFAULT.enabled)),
441
+ aiProvider: withVol(z.string().default(AI_CONFIG_DEFAULT.provider)),
442
+ aiModel: withVol(z.string().default(AI_CONFIG_DEFAULT.model)),
443
+ aiEffort: withVol(z.string().default(AI_CONFIG_DEFAULT.effort)),
444
+ // AI 任务模型(非补全场景如 AI 智能整理;空 = 跟随补全配置)
445
+ aiTaskProvider: withVol(z.string().default(AI_CONFIG_DEFAULT.taskProvider)),
446
+ aiTaskModel: withVol(z.string().default(AI_CONFIG_DEFAULT.taskModel)),
447
+ aiTaskEffort: withVol(z.string().default(AI_CONFIG_DEFAULT.taskEffort)),
448
+ // SVN 能力:svn CLI 覆盖(空 = PATH)与 TortoiseSVN 目录(Windows 过渡增强)
449
+ svnPath: withVol(z.string().default('')),
450
+ tortoisePath: withVol(z.string().default(TORTOISE_DIR_DEFAULT)),
451
+ // 原生打开范围(逗号分隔后缀;默认 = 让位清单并集,csv/tsv 决策见 shared/nativeOpen.ts)
452
+ nativeOpenExts: withVol(z.string().default(DEFAULT_NATIVE_CSV)),
453
+ }
454
+ if (wantVolatile) configVolatile = { requested: true, marked, total: Object.keys(shape).length }
455
+ return z.object(shape)
456
+ }
457
+
458
+ /** Config volatile 标记观测(构建期记录;兼容报告读取,测试可复位)。 */
459
+ export interface ConfigVolatileState { requested: boolean; marked: number; total: number }
460
+
461
+ let configVolatile: ConfigVolatileState = { requested: false, marked: 0, total: 0 }
462
+
463
+ /**
464
+ * 读取 Config volatile 标记观测(0.1.7 设置页可用性的构建期判据)。
465
+ * @author ddj 2026年09月23号
466
+ * @returns 观测态(requested=是否按 Config 语义构建;marked/total=成功标记字段数)
467
+ */
468
+ export function configVolatileState(): ConfigVolatileState {
469
+ return configVolatile
470
+ }
471
+
472
+ /**
473
+ * 复位 volatile 标记观测(测试隔离用)。
474
+ * @author ddj 2026年09月23号
475
+ */
476
+ export function resetConfigVolatileState(): void {
477
+ configVolatile = { requested: false, marked: 0, total: 0 }
478
+ }
479
+
480
+ /**
481
+ * 为已完成 default 链的 schema 字段追加 volatile 标记(能力守卫)。
482
+ * @author ddj 2026年09月23号
483
+ * @param field schema 字段(两代 schemastery 兼容面,可能没有 volatile 方法)
484
+ * @returns 处理后字段与是否成功标记
485
+ */
486
+ function markVolatile(field: unknown): { field: unknown; marked: boolean } {
487
+ const target = field as { volatile?: () => unknown } | null | undefined
488
+ if (!target || typeof target.volatile !== 'function') return { field, marked: false }
489
+ try {
490
+ return { field: target.volatile(), marked: true }
491
+ } catch {
492
+ return { field, marked: false }
493
+ }
494
+ }
495
+
496
+ /** cosmokit volatile 引用写协议符号(Symbol.for 跨拷贝一致,同 cosmokit.isVolatile 判据)。 */
497
+ const VOLATILE_WRITE = Symbol.for('cosmokit.volatile.write')
498
+
499
+ /**
500
+ * 解引用 0.1.7 volatile 配置引用(普通值直传)。
501
+ * cordis resolveConfig 会把 .volatile() 字段解析成稳定引用,直接读会得到引用对象;
502
+ * 不解引用则配置回退值全部落入默认值分支。
503
+ * @author ddj 2026年09月23号
504
+ * @param value 配置字段原始值
505
+ * @returns 引用的当前快照,或原值
506
+ */
507
+ export function unref(value: unknown): unknown {
508
+ if (value !== null && typeof value === 'object' && VOLATILE_WRITE in (value as object)) {
509
+ return (value as { get: () => unknown }).get()
510
+ }
511
+ return value
512
+ }
513
+
514
+ /**
515
+ * 读插件组合配置字段并解引用(配置读取唯一入口)。
516
+ * @author ddj 2026年09月23号
517
+ * @param config 插件组合配置(apply 收到的 Config 值)
518
+ * @param key 字段名
519
+ * @returns 字段值(引用已解包;缺失为 undefined)
520
+ */
521
+ export function configField(config: unknown, key: string): unknown {
522
+ return unref((config as Record<string, unknown> | null | undefined)?.[key])
523
+ }
524
+
289
525
  function normalizeValue(value: unknown): string {
290
526
  if (typeof value !== 'string' || value.trim() === '') return FILE_OPEN_DEFAULT
291
527
  return value.trim()
292
528
  }
293
529
 
294
530
  function configValue(config: unknown): string {
295
- return normalizeValue((config as { fileOpenTool?: unknown } | undefined)?.fileOpenTool)
531
+ return normalizeValue(configField(config, 'fileOpenTool'))
296
532
  }
297
533
 
298
534
  /** 配置/设置里的深链基址(缺省/非法回退默认值)。 */
299
535
  function baseValueOf(config: unknown): string {
300
- const raw = (config as { integrationBaseUrl?: unknown } | undefined)?.integrationBaseUrl
536
+ const raw = configField(config, 'integrationBaseUrl')
301
537
  return typeof raw === 'string' && raw.trim() ? raw.trim() : INTEGRATION_BASE_DEFAULT
302
538
  }
303
539
 
304
540
  /** 配置/设置里的 svn CLI 覆盖(空 = 从 PATH 解析 'svn')。 */
305
541
  function svnPathValueOf(config: unknown): string {
306
- const raw = (config as { svnPath?: unknown } | undefined)?.svnPath
542
+ const raw = configField(config, 'svnPath')
307
543
  return typeof raw === 'string' ? raw.trim() : ''
308
544
  }
309
545
 
310
546
  /** 配置/设置里的 TortoiseSVN 目录(空 = 默认安装目录)。 */
311
547
  function tortoiseDirValueOf(config: unknown): string {
312
- const raw = (config as { tortoisePath?: unknown } | undefined)?.tortoisePath
548
+ const raw = configField(config, 'tortoisePath')
313
549
  return typeof raw === 'string' && raw.trim() ? raw.trim() : TORTOISE_DIR_DEFAULT
314
550
  }
315
551
 
@@ -337,37 +573,25 @@ export async function installOpenSettingsSection(
337
573
  /* 装载失败按缺失处理 */
338
574
  }
339
575
  if (!deps) return false
340
- const schema = deps.z.object({
341
- fileOpenTool: deps.z.string().default(FILE_OPEN_DEFAULT),
342
- keybindings: deps.z.object(keybindingsShape(deps.z)).default({ ...KEYBINDING_DEFAULTS }),
343
- sidebarMinWidth: deps.z.number().default(300),
344
- // 页签数量上限(0 = 不限制;超限时淘汰最久未使用的页签,固定页签除外)
345
- maxOpenEditors: deps.z.number().default(EDITOR_LIMIT_DEFAULT),
346
- integrationBaseUrl: deps.z.string().default(INTEGRATION_BASE_DEFAULT),
347
- // AI 内联补全(默认关;provider/model 空 = 自动路由;effort 空 = 跟随模型默认)
348
- aiInline: deps.z.boolean().default(AI_CONFIG_DEFAULT.enabled),
349
- aiProvider: deps.z.string().default(AI_CONFIG_DEFAULT.provider),
350
- aiModel: deps.z.string().default(AI_CONFIG_DEFAULT.model),
351
- aiEffort: deps.z.string().default(AI_CONFIG_DEFAULT.effort),
352
- // SVN 能力:svn CLI 覆盖(空 = PATH)与 TortoiseSVN 目录(Windows 过渡增强)
353
- svnPath: deps.z.string().default(''),
354
- tortoisePath: deps.z.string().default(TORTOISE_DIR_DEFAULT),
355
- })
576
+ const schema = buildSettingsSchema(deps.z)
356
577
  const strategy = await runSettingsInstall(ctx, ns, schema, entry, {
357
578
  setSource: (source) => hooks.setSource(source as () => FileOpenSettings),
358
579
  onChange: hooks.onChange,
359
580
  }, loader)
360
- return strategy === 'legacy' || strategy === 'service'
581
+ return strategy === 'legacy' || strategy === 'service' || strategy === 'forms'
361
582
  }
362
583
 
363
584
  /** AI 配置脏值读取(settings 未就绪时回退默认)。 */
364
- function aiValueOf(stored: unknown): { enabled: boolean; provider: string; model: string; effort: string } {
585
+ function aiValueOf(stored: unknown): { enabled: boolean; provider: string; model: string; effort: string; taskProvider: string; taskModel: string; taskEffort: string } {
365
586
  const raw = (stored ?? {}) as Record<string, unknown>
366
587
  return {
367
588
  enabled: raw.aiInline === true,
368
589
  provider: typeof raw.aiProvider === 'string' ? raw.aiProvider : AI_CONFIG_DEFAULT.provider,
369
590
  model: typeof raw.aiModel === 'string' ? raw.aiModel : AI_CONFIG_DEFAULT.model,
370
591
  effort: typeof raw.aiEffort === 'string' ? raw.aiEffort : AI_CONFIG_DEFAULT.effort,
592
+ taskProvider: typeof raw.aiTaskProvider === 'string' ? raw.aiTaskProvider : AI_CONFIG_DEFAULT.taskProvider,
593
+ taskModel: typeof raw.aiTaskModel === 'string' ? raw.aiTaskModel : AI_CONFIG_DEFAULT.taskModel,
594
+ taskEffort: typeof raw.aiTaskEffort === 'string' ? raw.aiTaskEffort : AI_CONFIG_DEFAULT.taskEffort,
371
595
  }
372
596
  }
373
597
 
@@ -385,8 +609,7 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
385
609
  let provider: SettingsProvider | undefined
386
610
  const notify = (value: unknown): void => { current = normalizeValue(value); onChange(current) }
387
611
  const syncRevision = (): void => {
388
- const descriptor = provider?.describe?.({ redactSecrets: true })?.find((item: { ns?: string }) => item.ns === FILE_OPEN_SETTINGS_NS)
389
- revision = descriptor?.revision
612
+ revision = sectionOf(provider, FILE_OPEN_SETTINGS_NS)?.revision
390
613
  }
391
614
  const setSource = (source: () => FileOpenSettings): void => notify(source().fileOpenTool)
392
615
  const settingsChange = (): void => syncRevision()
@@ -394,8 +617,7 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
394
617
  /** AI 配置当前值(settings 未就绪回退默认)。 */
395
618
  let aiCurrent: AiConfigView = { ...AI_CONFIG_DEFAULT }
396
619
  const aiSync = (): void => {
397
- const descriptor = provider?.describe?.({ redactSecrets: true })?.find((item: { ns?: string }) => item.ns === FILE_OPEN_SETTINGS_NS)
398
- const stored = descriptor?.value
620
+ const stored = readSectionValue(provider, FILE_OPEN_SETTINGS_NS)
399
621
  aiCurrent = stored !== undefined ? aiValueOf(stored) : aiCurrent
400
622
  }
401
623
 
@@ -409,7 +631,7 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
409
631
  }
410
632
  /** 读取 settings 存储值(describe 未就绪返回 undefined)。 */
411
633
  const storedValue = (): unknown => {
412
- return provider?.describe?.({ redactSecrets: true })?.find((item: { ns?: string }) => item.ns === FILE_OPEN_SETTINGS_NS)?.value
634
+ return readSectionValue(provider, FILE_OPEN_SETTINGS_NS)
413
635
  }
414
636
 
415
637
  void installOpenSettingsSection(ctx, FILE_OPEN_SETTINGS_NS, {
@@ -428,8 +650,12 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
428
650
  })
429
651
  ctx.inject?.(['settings'], (settingsCtx: Ctx) => {
430
652
  provider = settingsCtx.get('settings')
431
- aiSync()
432
- svnSync(storedValue())
653
+ // 初值直读:settings/document-updated 首事件可能先于本订阅建立,只靠事件会漏初值
654
+ const stored = storedValue()
655
+ const section = stored as { fileOpenTool?: unknown } | undefined
656
+ if (section?.fileOpenTool !== undefined) notify(section.fileOpenTool)
657
+ aiCurrent = stored !== undefined ? aiValueOf(stored) : aiCurrent
658
+ svnSync(stored)
433
659
  syncRevision()
434
660
  })
435
661
 
@@ -438,31 +664,30 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
438
664
  get revision() { return revision },
439
665
  update: async (value: string, expectedRevision?: number): Promise<void> => {
440
666
  const next = normalizeValue(value)
441
- if (!provider?.update) { notify(next); return }
442
- await provider.update(FILE_OPEN_SETTINGS_NS, { fileOpenTool: next }, expectedRevision)
443
- const descriptor = provider.describe?.({ redactSecrets: true })?.find((item: { ns?: string; value?: unknown }) => item.ns === FILE_OPEN_SETTINGS_NS)
444
- const stored = descriptor?.value as { fileOpenTool?: unknown } | undefined
667
+ const result = await updateSection(provider, FILE_OPEN_SETTINGS_NS, { fileOpenTool: next }, expectedRevision)
668
+ if (!result.ok) log.warn('fileOpenTool 设置写入失败:' + (result.error ?? '未知原因'))
669
+ const stored = readSectionValue(provider, FILE_OPEN_SETTINGS_NS) as { fileOpenTool?: unknown } | undefined
445
670
  notify(stored?.fileOpenTool ?? next)
446
671
  syncRevision()
447
672
  },
448
673
  ai: () => aiCurrent,
449
674
  svn: () => svnCurrent,
450
675
  aiUpdate: async (patch: AiConfigPatch, expectedRevision?: number): Promise<AiConfigView> => {
451
- if (!provider?.update) {
452
- // settings 不可用:内存态生效(重启回落默认),保持与 fileOpenTool 的降级语义一致
453
- if (patch.enabled !== undefined) aiCurrent.enabled = patch.enabled
454
- if (patch.provider !== undefined) aiCurrent.provider = patch.provider
455
- if (patch.model !== undefined) aiCurrent.model = patch.model
456
- if (patch.effort !== undefined) aiCurrent.effort = patch.effort
457
- return aiCurrent
458
- }
459
676
  const stored = { ...aiCurrent }
460
677
  const body: Record<string, unknown> = {}
461
678
  if (patch.enabled !== undefined) { stored.enabled = patch.enabled; body.aiInline = patch.enabled }
462
679
  if (patch.provider !== undefined) { stored.provider = patch.provider; body.aiProvider = patch.provider }
463
680
  if (patch.model !== undefined) { stored.model = patch.model; body.aiModel = patch.model }
464
681
  if (patch.effort !== undefined) { stored.effort = patch.effort; body.aiEffort = patch.effort }
465
- await provider.update(FILE_OPEN_SETTINGS_NS, body, expectedRevision)
682
+ if (patch.taskProvider !== undefined) { stored.taskProvider = patch.taskProvider; body.aiTaskProvider = patch.taskProvider }
683
+ if (patch.taskModel !== undefined) { stored.taskModel = patch.taskModel; body.aiTaskModel = patch.taskModel }
684
+ if (patch.taskEffort !== undefined) { stored.taskEffort = patch.taskEffort; body.aiTaskEffort = patch.taskEffort }
685
+ const result = await updateSection(provider, FILE_OPEN_SETTINGS_NS, body, expectedRevision)
686
+ if (!result.ok) {
687
+ // settings 不可用/写入失败:内存态生效(重启回落默认),与 fileOpenTool 降级语义一致
688
+ aiCurrent = stored
689
+ return aiCurrent
690
+ }
466
691
  aiSync()
467
692
  return aiCurrent
468
693
  },
package/src/index.ts CHANGED
@@ -14,7 +14,9 @@ import { newSearcher } from './search/orchestrator.js'
14
14
  import { newContentSearcher } from './search/content.js'
15
15
  import { installIsolation } from './mcpIsolation.js'
16
16
  import { cwdOf } from './registry.js'
17
- import { setupOpenSettings } from './fileOpenSettings.js'
17
+ import { setupOpenSettings, buildSettingsSchema } from './fileOpenSettings.js'
18
+ import type { SettingsDeps } from './fileOpenSettings.js'
19
+ import z from '@deepseek-ai/schemastery'
18
20
  import { shellMenuLifecycle } from './integrate.js'
19
21
  import { disposeIndex } from './treeIndex.js'
20
22
  import { sweepTreeCache } from './paths.js'
@@ -36,6 +38,21 @@ import type { Ctx } from './store.js'
36
38
  export const name = "dsh-vscode-mode"
37
39
  export const inject = ['sessions', 'fs', 'webServer', 'loader', 'tools', 'workspaceRegistry', 'agents']
38
40
 
41
+ /**
42
+ * 插件 Config schema(cordis 从 module 导出读 `plugin.Config` 做启动校验;
43
+ * DSH 0.1.7 起设置页按此 schema 自动生成表单,设置值 = profile 插件配置)。
44
+ * 字段集与 installOpenSettingsSection 的 section schema 同源(buildSettingsSchema),
45
+ * 全字段带默认值——undefined/空配置经 schemastery 校验自动填充(rc/alpha 两代
46
+ * cordis 均读该导出,实测 4.0.0-rc.8 与 0.1.7 行为一致,校验必过)。
47
+ * volatile: true —— 0.1.7 的 SettingsForms.describe 只下发含 volatile 字段的 entry
48
+ * (volatileForm 门槛),漏标则 ns 不出现 → client configForms 恒 unavailable →
49
+ * 设置页「设置服务暂不可用」;同时 volatile 字段由 Loader 就地提交免重载。
50
+ * 运行时解析:@deepseek-ai/schemastery 经插件自身 dependencies(link/dev/npm 安装
51
+ * 三形态一致,钉 ~3.18.4 —— 3.18.1 起才有 .volatile(),低版本经 markVolatile 守卫降级)。
52
+ * @author ddj 2026年09月22号(2026年09月23号 增补 volatile 标记)
53
+ */
54
+ export const Config = buildSettingsSchema(z as unknown as SettingsDeps['z'], { volatile: true })
55
+
39
56
  /**
40
57
  * 装配插件:挂事件监听、注册路由、安装兼容层。
41
58
  * @author ddj 2026年08月20号
package/src/lsp/client.ts CHANGED
@@ -62,6 +62,9 @@ export function createLspClient(
62
62
  workspaceSymbol: false,
63
63
  hover: false,
64
64
  semanticTokens: false,
65
+ completion: false,
66
+ completionResolve: false,
67
+ signatureHelp: false,
65
68
  semanticTokenTypes: [],
66
69
  semanticTokenModifiers: [],
67
70
  }
@@ -164,6 +167,8 @@ export function createLspClient(
164
167
  documentSymbolProvider?: unknown
165
168
  workspaceSymbolProvider?: unknown
166
169
  hoverProvider?: unknown
170
+ completionProvider?: { resolveProvider?: unknown; triggerCharacters?: unknown }
171
+ signatureHelpProvider?: { triggerCharacters?: unknown; retriggerCharacters?: unknown }
167
172
  semanticTokensProvider?: {
168
173
  legend?: { tokenTypes?: unknown; tokenModifiers?: unknown }
169
174
  }
@@ -185,6 +190,22 @@ export function createLspClient(
185
190
  references: { dynamicRegistration: false },
186
191
  documentSymbol: { dynamicRegistration: false },
187
192
  hover: { dynamicRegistration: false },
193
+ // 补全能力声明:snippetSupport 让服务器回 InsertAsSnippet 片段;
194
+ // resolveSupport.properties 声明我方支持 completionItem/resolve 惰性补全
195
+ // (EmmyLua 据此把 documentation 挪到 resolve 阶段,列表载荷显著变小)。
196
+ completion: {
197
+ dynamicRegistration: false,
198
+ contextSupport: true,
199
+ completionItem: {
200
+ snippetSupport: true,
201
+ documentationFormat: ['markdown', 'plaintext'],
202
+ resolveSupport: { properties: ['documentation', 'detail', 'additionalTextEdits'] },
203
+ },
204
+ },
205
+ signatureHelp: {
206
+ dynamicRegistration: false,
207
+ signatureInformation: { documentationFormat: ['markdown', 'plaintext'] },
208
+ },
188
209
  semanticTokens: {
189
210
  dynamicRegistration: false,
190
211
  requests: { range: false, full: { delta: false } },
@@ -205,6 +226,9 @@ export function createLspClient(
205
226
  workspaceSymbol: Boolean(caps.workspaceSymbolProvider),
206
227
  hover: Boolean(caps.hoverProvider),
207
228
  semanticTokens: Boolean(caps.semanticTokensProvider),
229
+ completion: Boolean(caps.completionProvider),
230
+ completionResolve: Boolean(caps.completionProvider?.resolveProvider),
231
+ signatureHelp: Boolean(caps.signatureHelpProvider),
208
232
  semanticTokenTypes: Array.isArray(legend?.tokenTypes)
209
233
  ? legend.tokenTypes.filter((item): item is string => typeof item === 'string')
210
234
  : [...LSP_SEMANTIC_TOKEN_TYPES],
package/src/lsp/rpc.ts CHANGED
@@ -436,6 +436,63 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
436
436
  }
437
437
  },
438
438
 
439
+ 'edrv.lsp.completion': async (args) => {
440
+ const lang = langOfPath(args.path)
441
+ if (!lang) return { ok: true, completions: undefined }
442
+ const sc = await rootOf(args.sessionId)
443
+ if ('err' in sc) return { ok: false, error: sc.err }
444
+ try {
445
+ const server = serverOf(sc.root, lang)
446
+ if (!server) return { ok: true, completions: undefined }
447
+ const completions = await server.completion(
448
+ wsPath(sc.root, args.path),
449
+ args.position.line,
450
+ args.position.character,
451
+ args.context,
452
+ )
453
+ if (!completions) return { ok: true, completions: undefined }
454
+ const truncated = completions.items.length > 500
455
+ return {
456
+ ok: true,
457
+ completions: truncated ? { ...completions, items: completions.items.slice(0, 500), truncated: true } : completions,
458
+ }
459
+ } catch (error) {
460
+ return { ok: false, error: 'LSP 补全查询失败:' + String(error) }
461
+ }
462
+ },
463
+
464
+ 'edrv.lsp.resolveCompletion': async (args) => {
465
+ const lang = langOfPath(args.path)
466
+ if (!lang) return { ok: true, item: undefined }
467
+ const sc = await rootOf(args.sessionId)
468
+ if ('err' in sc) return { ok: false, error: sc.err }
469
+ try {
470
+ const server = serverOf(sc.root, lang)
471
+ if (!server) return { ok: true, item: args.item }
472
+ const item = await server.resolveCompletion(wsPath(sc.root, args.path), args.item)
473
+ return { ok: true, item: item ?? undefined }
474
+ } catch (error) {
475
+ return { ok: false, error: 'LSP 补全解析失败:' + String(error) }
476
+ }
477
+ },
478
+
479
+ 'edrv.lsp.signatureHelp': async (args) => {
480
+ const lang = langOfPath(args.path)
481
+ if (!lang) return { ok: true, signatureHelp: undefined }
482
+ const sc = await rootOf(args.sessionId)
483
+ if ('err' in sc) return { ok: false, error: sc.err }
484
+ try {
485
+ const server = serverOf(sc.root, lang)
486
+ if (!server) return { ok: true, signatureHelp: undefined }
487
+ return {
488
+ ok: true,
489
+ signatureHelp: (await server.signatureHelp(wsPath(sc.root, args.path), args.position.line, args.position.character)) ?? undefined,
490
+ }
491
+ } catch (error) {
492
+ return { ok: false, error: 'LSP 签名帮助查询失败:' + String(error) }
493
+ }
494
+ },
495
+
439
496
  'edrv.lsp.semanticTokens': async (args) => {
440
497
  const lang = langOfPath(args.path)
441
498
  if (!lang) return { ok: true, tokens: undefined }