dsh-vscode-mode 0.1.57 → 0.1.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import React from 'react'
10
+ import { log } from './log.js'
10
11
 
11
12
  /** betterSidebar 服务名(ctx.get 用;不要加进 inject——缺失会让插件停靠等待)。 */
12
13
  export const BETTER_SIDEBAR_SERVICE = 'betterSidebar'
@@ -137,7 +138,7 @@ export function routeSideEditor(path: string | null, focusDiff: boolean): boolea
137
138
  try {
138
139
  return ensureSideEditor(path, focusDiff) === true
139
140
  } catch (error) {
140
- console.warn('[dsh-vscode-mode] 侧栏编辑器打开失败(' + String(error) + '),已回退')
141
+ log.warn('侧栏编辑器打开失败(' + String(error) + '),已回退')
141
142
  return false
142
143
  }
143
144
  }
@@ -177,7 +178,7 @@ export function installSideEditor(options: {
177
178
  if (typeof service.isTabEnabled === 'function' && !service.isTabEnabled(SIDE_TAB_ID)) {
178
179
  if (!fallbackInstalled) {
179
180
  fallbackInstalled = true
180
- try { registerLegacyFallback() } catch (error) { console.warn('[dsh-vscode-mode] 回退注册失败(' + String(error) + ')') }
181
+ try { registerLegacyFallback() } catch (error) { log.warn('回退注册失败(' + String(error) + ')') }
181
182
  }
182
183
  return false
183
184
  }
@@ -192,7 +193,7 @@ export function installSideEditor(options: {
192
193
  meta: { openPath: path || undefined, focusDiff },
193
194
  }, { sessionId, cwd: scope?.cwd })
194
195
  } catch (error) {
195
- console.warn('[dsh-vscode-mode] openTab 失败(' + String(error) + ')')
196
+ log.warn('openTab 失败(' + String(error) + ')')
196
197
  return false
197
198
  }
198
199
  if (sideEditorMounted) {
@@ -215,11 +216,11 @@ export function installSideEditor(options: {
215
216
  component: renderTab,
216
217
  })
217
218
  } catch (error) {
218
- console.warn('[dsh-vscode-mode] 侧边栏 Tab 注册失败(' + String(error) + ')')
219
+ log.warn('侧边栏 Tab 注册失败(' + String(error) + ')')
219
220
  // 注册失败 = 侧栏形态不可用:立即回退中央页签,不接管打开路由
220
221
  if (!fallbackInstalled) {
221
222
  fallbackInstalled = true
222
- try { registerLegacyFallback() } catch (inner) { console.warn('[dsh-vscode-mode] 回退注册失败(' + String(inner) + ')') }
223
+ try { registerLegacyFallback() } catch (inner) { log.warn('回退注册失败(' + String(inner) + ')') }
223
224
  }
224
225
  return () => {}
225
226
  }
@@ -47,7 +47,7 @@ export function noopHunk(rec: RecordView | undefined, h: Hunk | CallHunk | null
47
47
 
48
48
  /** 记录是否仍有待处理差异(superseded / 全部已决策 = false)。 */
49
49
  export function isRecPending(rec: RecordView): boolean {
50
- if (!rec || rec.superseded === true || rec.conflict === true) return false
50
+ if (!rec || rec.superseded === true) return false
51
51
  const perHunk = Array.isArray(rec.decisions?.perHunk) ? rec.decisions.perHunk : []
52
52
  if (perHunk.length) {
53
53
  // 只认存在至少一个"非空差异且未决策"的 hunk
@@ -4,7 +4,7 @@
4
4
  * 作者 ddj 2026-08-20
5
5
  */
6
6
  import type { Hunk, RecordView } from '../../shared/types.js'
7
- import { locateHunks, lineBefore, preciseHunk, splitLines } from '../../shared/diff.js'
7
+ import { locateHunks, lineBefore, normalizeForCompare, normalizeHunk, preciseHunk, splitLines } from '../../shared/diff.js'
8
8
  import { ST, noopHunk, statusAt } from './records.js'
9
9
  import type { Status } from './records.js'
10
10
 
@@ -46,11 +46,18 @@ export function trimCommonLines(oldLines: string[], newLines: string[]): { oldLi
46
46
  return { oldLines: oldLines.slice(prefix, oldLines.length - suffix), newLines: newLines.slice(prefix, newLines.length - suffix), shift: prefix }
47
47
  }
48
48
 
49
- /** 计算文件内各差异区域(行范围 + old/new + 状态),用于行内绿标注与 DiffBox。 */
49
+ /**
50
+ * 计算文件内各差异区域(行范围 + old/new + 状态),用于行内绿标注与 DiffBox。
51
+ * 定位统一基于归一化文本(剥 BOM、CRLF→LF):外部工具可能改变行尾/BOM,
52
+ * 与 edit 工具的 LF hunk 口径不一致会导致定位失败(差异被误标 stale)。
53
+ * 行号按 \n 计数,归一化不改变行号,展示语义不变。
54
+ * @author ddj 2026年09月09号
55
+ */
50
56
  export function diffRegions(records: RecordView[], content: string | null): Region[] {
51
57
  const regions: Region[] = []
52
58
  if (content === null) return regions
53
- const lines = splitLines(content)
59
+ const normalized = normalizeForCompare(content)
60
+ const lines = splitLines(normalized)
54
61
  for (const rec of records) {
55
62
  if (rec.create) {
56
63
  for (let i = 0; i < rec.hunks.length; i++) {
@@ -63,9 +70,9 @@ export function diffRegions(records: RecordView[], content: string | null): Regi
63
70
  const entries: Array<{ idx: number; hunk: Hunk }> = []
64
71
  for (let i = 0; i < rec.hunks.length; i++) {
65
72
  const hunk = preciseHunk(rec, i)
66
- if (hunk && !noopHunk(rec, hunk)) entries.push({ idx: i, hunk })
73
+ if (hunk && !noopHunk(rec, hunk)) entries.push({ idx: i, hunk: normalizeHunk(hunk) })
67
74
  }
68
- const locations = locateHunks(content, entries.map((entry) => entry.hunk))
75
+ const locations = locateHunks(normalized, entries.map((entry) => entry.hunk))
69
76
  for (let i = 0; i < entries.length; i++) {
70
77
  const entry = entries[i]
71
78
  const location = locations[i]
@@ -74,7 +81,7 @@ export function diffRegions(records: RecordView[], content: string | null): Regi
74
81
  regions.push({ callId: rec.callId, idx: entry.idx, stale: true, status, create: false, oldLines: entry.hunk.oldText === null ? [] : entry.hunk.oldText.split('\n'), newLines: entry.hunk.newText ? entry.hunk.newText.split('\n') : [], rec, superseded: rec.superseded === true })
75
82
  continue
76
83
  }
77
- const start = countLinesBefore(content, location.start) + 1
84
+ const start = countLinesBefore(normalized, location.start) + 1
78
85
  const oldLines = entry.hunk.oldText === null ? [] : entry.hunk.oldText.split('\n')
79
86
  const newLines = entry.hunk.newText.split('\n')
80
87
  const trimmed = trimCommonLines(oldLines, newLines)
@@ -0,0 +1,104 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — AI 补全设置卡片。
4
+ * 开关 + 模型下拉(edrv.ai.models 目录,按 provider 分组,默认「自动」)+
5
+ * 思考强度下拉(随所选模型的 efforts 元数据动态渲染:无档位隐藏;含「跟随默认」)。
6
+ * 保存走 edrv.ai.configUpdate;保存后广播 edrv:ai-config 事件让 provider 即时生效。
7
+ * 作者 ddj
8
+ */
9
+ import React from 'react'
10
+ import { rpc } from '../rpc.js'
11
+ import { setAiInlineEnabled } from '../ai/inlineProvider.js'
12
+
13
+ /**
14
+ * AI 补全设置卡片(设置页「AI 补全」Tab 主体)。
15
+ * @author ddj
16
+ */
17
+ export function AiSettings() {
18
+ const [cfg, setCfg] = React.useState(null)
19
+ const [dir, setDir] = React.useState(null)
20
+ const [note, setNote] = React.useState('')
21
+ const [error, setError] = React.useState('')
22
+ const [busy, setBusy] = React.useState(false)
23
+
24
+ React.useEffect(() => {
25
+ Promise.all([rpc('edrv.ai.configGet', {}), rpc('edrv.ai.models', {})])
26
+ .then(([c, d]) => {
27
+ if (c.ok) setCfg({ enabled: c.enabled, provider: c.provider, model: c.model, effort: c.effort })
28
+ else setError(c.error || '配置读取失败')
29
+ if (d.ok) setDir(d)
30
+ else setError(d.error || '模型目录读取失败')
31
+ })
32
+ .catch((e) => setError(String(e)))
33
+ }, [])
34
+
35
+ /** 所选 provider 下的模型条目(自动路由时取首组供档位参考)。 */
36
+ const modelsOf = (provider) => {
37
+ if (!dir?.providers?.length) return []
38
+ if (!provider) return dir.providers[0]?.models ?? []
39
+ return dir.providers.find((p) => p.id === provider)?.models ?? []
40
+ }
41
+
42
+ /** 档位可选项:模型无 reasoning 元数据 → 空数组(选择器隐藏)。 */
43
+ const efforts = (() => {
44
+ const models = modelsOf(cfg?.provider)
45
+ return models.find((m) => m.model === cfg?.model)?.efforts ?? models[0]?.efforts ?? []
46
+ })()
47
+
48
+ const save = async (patch) => {
49
+ setBusy(true)
50
+ setError('')
51
+ try {
52
+ const res = await rpc('edrv.ai.configUpdate', patch)
53
+ if (!res.ok) throw new Error(res.error || '保存失败')
54
+ setCfg({ enabled: res.enabled, provider: res.provider, model: res.model, effort: res.effort })
55
+ setAiInlineEnabled(res.enabled)
56
+ setNote('已保存')
57
+ window.dispatchEvent(new CustomEvent('edrv:ai-config', { detail: { enabled: res.enabled } }))
58
+ } catch (e) {
59
+ setError(String(e))
60
+ } finally {
61
+ setBusy(false)
62
+ }
63
+ }
64
+
65
+ const toggle = () => save({ enabled: !cfg.enabled })
66
+
67
+ const pickModel = (value) => {
68
+ // 「自动」:provider/model 清空(host 自动取目录首个);档位保留
69
+ if (!value) return save({ provider: '', model: '' })
70
+ const idx = value.indexOf('/')
71
+ const provider = value.slice(0, idx)
72
+ const model = value.slice(idx + 1)
73
+ const efforts = modelsOf(provider).find((m) => m.model === model)?.efforts ?? []
74
+ // 换模型后原档位可能不属于新模型:档位一并清空(跟随默认)
75
+ const keep = efforts.some((e) => e.id === cfg.effort)
76
+ save(keep ? { provider, model } : { provider, model, effort: '' })
77
+ }
78
+
79
+ if (!cfg) return React.createElement('div', { className: 'vsm-mcp-empty' }, error || '正在读取 AI 补全配置…')
80
+
81
+ const modelValue = cfg.provider && cfg.model ? cfg.provider + '/' + cfg.model : ''
82
+ return React.createElement('section', { className: 'vsm-lsp-card' },
83
+ React.createElement('h3', null, 'AI 自动补全(实验)'),
84
+ React.createElement('p', { className: 'vsm-lsp-note' }, '编辑停顿后由模型生成内联建议(ghost text),Tab 接受,Alt+\\ 手动触发。每次补全是一次模型调用;建议选择非推理模型,且思考强度选「跟随默认」或最低档以获得更快响应。'),
85
+ React.createElement('div', { className: 'vsm-lsp-row' },
86
+ React.createElement('button', { className: cfg.enabled ? 'vsm-primary' : '', disabled: busy, onClick: toggle },
87
+ cfg.enabled ? '已开启(点击关闭)' : '已关闭(点击开启)'),
88
+ ),
89
+ React.createElement('div', { className: 'vsm-lsp-row' },
90
+ React.createElement('label', null, '模型',
91
+ React.createElement('select', { value: modelValue, disabled: busy || !dir?.providers?.length, onChange: (e) => pickModel(e.target.value) },
92
+ React.createElement('option', { value: '' }, dir?.providers?.length ? '自动(取第一个可用模型)' : '无可用模型'),
93
+ (dir?.providers ?? []).flatMap((p) => p.models.map((m) => React.createElement('option', { key: p.id + '/' + m.model, value: p.id + '/' + m.model }, p.name + ' · ' + m.name)))),
94
+ ),
95
+ efforts.length ? React.createElement('label', null, '思考强度',
96
+ React.createElement('select', { value: cfg.effort, disabled: busy, onChange: (e) => save({ effort: e.target.value }) },
97
+ React.createElement('option', { value: '' }, '跟随默认'),
98
+ efforts.map((e) => React.createElement('option', { key: e.id, value: e.id }, e.name))),
99
+ ) : null,
100
+ ),
101
+ note && React.createElement('div', { className: 'vsm-compat-item ok' }, React.createElement('span', { className: 'vsm-compat-name' }, note)),
102
+ error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
103
+ )
104
+ }
@@ -33,6 +33,7 @@ import { CACHE_KEY } from '../paths.js'
33
33
  import { bindLspEditor, runGoToDefinition, runFindReferences, hideReferencesOverlay } from '../monaco/lsp/providers.js'
34
34
  import { bindLspUnderline } from '../monaco/lsp/underline.js'
35
35
  import { onLspProgress, refreshStatus } from '../monaco/lsp/index.js'
36
+ import { setupAiInline, trackAiEditor, aiInlineEnabled } from '../ai/inlineProvider.js'
36
37
 
37
38
  /**
38
39
  * 中央编辑区:文件页签(脏点/关闭/打开路径)+ Ctrl+P 搜索 + Monaco 编辑器 +
@@ -66,6 +67,9 @@ export function EditorView(props) {
66
67
  const pdfHostRef = React.useRef(null) // PDF 面板外壳 div(命令式控制器接管)
67
68
  const [status, setStatus] = React.useState('')
68
69
  const [lspServers, setLspServers] = React.useState([])
70
+ // AI 补全状态(底部状态栏段):{ state: idle|busy|ok|error, detail, enabled }
71
+ const [aiStatus, setAiStatus] = React.useState({ state: 'idle', detail: null, enabled: false })
72
+ const aiStatusTimer = React.useRef(null)
69
73
  const [error, setError] = React.useState(null)
70
74
  const [loadError, setLoadError] = React.useState(null)
71
75
  const [loadStage, setLoadStage] = React.useState({ progress: 0, message: '准备加载编辑器…' })
@@ -496,6 +500,26 @@ export function EditorView(props) {
496
500
  return () => { unsubscribe(); clearInterval(timer) }
497
501
  }, [sessionId])
498
502
 
503
+ // AI 补全状态事件:busy/ok/error/idle 即时上栏;ok 3s、error 10s 后回落就绪
504
+ React.useEffect(() => {
505
+ const apply = (state, detail) => {
506
+ if (aiStatusTimer.current) { clearTimeout(aiStatusTimer.current); aiStatusTimer.current = null }
507
+ setAiStatus((prev) => ({ state, detail: detail ?? null, enabled: state === 'config' ? detail?.enabled === true : prev.enabled }))
508
+ if (state === 'ok') aiStatusTimer.current = setTimeout(() => setAiStatus((p) => (p.state === 'ok' ? { ...p, state: 'idle' } : p)), 3000)
509
+ else if (state === 'error') aiStatusTimer.current = setTimeout(() => setAiStatus((p) => (p.state === 'error' ? { ...p, state: 'idle' } : p)), 10000)
510
+ }
511
+ const onAiStatus = (e) => { if (e?.detail?.state) apply(e.detail.state, e.detail.detail) }
512
+ const onAiConfig = (e) => apply('config', { enabled: e?.detail?.enabled === true })
513
+ window.addEventListener('edrv:ai-status', onAiStatus)
514
+ window.addEventListener('edrv:ai-config', onAiConfig)
515
+ apply('config', { enabled: aiInlineEnabled() })
516
+ return () => {
517
+ window.removeEventListener('edrv:ai-status', onAiStatus)
518
+ window.removeEventListener('edrv:ai-config', onAiConfig)
519
+ if (aiStatusTimer.current) clearTimeout(aiStatusTimer.current)
520
+ }
521
+ }, [])
522
+
499
523
  React.useEffect(() => {
500
524
  const onOpen = (e) => {
501
525
  const p = e?.detail?.path
@@ -776,7 +800,12 @@ export function EditorView(props) {
776
800
  const progress = stage.phase === 'ready' ? 70 : Math.round(10 + stage.progress * 0.6)
777
801
  setLoadStage((prev) => ({ progress: Math.max(prev.progress, progress), message: stage.message }))
778
802
  }
779
- loadMonaco(onProgress).then((m) => { if (alive) setMonaco(m) }).catch((e) => {
803
+ loadMonaco(onProgress).then((m) => {
804
+ if (!alive) return
805
+ setMonaco(m)
806
+ // AI 内联补全 provider 注册 + 开关初始化(幂等)
807
+ setupAiInline(m)
808
+ }).catch((e) => {
780
809
  if (alive) {
781
810
  setMonacoErr(String(e?.message ?? e))
782
811
  setStatus('Monaco 不可用')
@@ -943,6 +972,8 @@ export function EditorView(props) {
943
972
  smoothScrolling: true,
944
973
  cursorBlinking: 'smooth',
945
974
  padding: { top: side ? 6 : 8 },
975
+ // AI 内联补全(ghost text):开关经 edrv.ai.configGet 拉取;provider 见 client/ai/inlineProvider
976
+ inlineSuggest: { enabled: true },
946
977
  })
947
978
  // 保存改由窗口级快捷键监听执行(键位可配置;见上方 edrv.save 监听)
948
979
  ed.onDidChangeModelContent(() => {
@@ -1014,6 +1045,11 @@ export function EditorView(props) {
1014
1045
  ed.addCommand(m.KeyCode.F12, () => { void runGoToDefinition(ed) })
1015
1046
  bindLspEditor(ed)
1016
1047
  bindLspUnderline(ed, m)
1048
+ // AI 补全:编辑器实例登记(差异静默判定用)+ Alt+\ 手动触发 ghost text
1049
+ trackAiEditor(ed)
1050
+ ed.addCommand(m.KeyMod.Alt | m.KeyCode.Backslash, () => {
1051
+ ed.trigger('edrv-ai', 'editor.action.inlineSuggest.trigger', null)
1052
+ })
1017
1053
  // hover 差异块 → 浮出 Keep/Undo(req:鼠标移到编辑区差异块时显示)
1018
1054
  // 防闪烁:① 区域不变不 setState(浮窗锚定差异块起始行,不跟随鼠标);② 延迟隐藏;
1019
1055
  // ③ 浮窗自身 onMouseEnter 取消隐藏计时(鼠标在浮窗与编辑器间移动不闪)。
@@ -1437,14 +1473,39 @@ export function EditorView(props) {
1437
1473
  ? 'LSP ' + lspLanguage + ' · ' + (lspPhaseText[lspServer.phase] ?? lspServer.phase) + ' · ' + (lspSourceText[lspServer.source] ?? lspServer.source)
1438
1474
  : 'LSP ' + lspLanguage + ' · 未启动'
1439
1475
  const lspProgress = typeof lspServer?.progress === 'number' ? Math.round(lspServer.progress) : null
1440
- const lspFooter = (lspLanguage === 'lua' || lspLanguage === 'csharp')
1441
- ? React.createElement('div', { className: 'edrv-statusbar', title: lspServer?.progressMessage || lspLabel },
1476
+ // 状态段行内样式:单行状态栏内的一段;LSP 段弹性占满空闲宽度,把后续段推到行尾
1477
+ const lspSegStyle = { display: 'inline-flex', alignItems: 'center', gap: '10px', minWidth: 0, flex: '1 1 auto' }
1478
+ const lspSeg = (lspLanguage === 'lua' || lspLanguage === 'csharp')
1479
+ ? React.createElement('span', { className: 'edrv-status-seg', style: lspSegStyle, title: lspServer?.progressMessage || lspLabel },
1442
1480
  React.createElement('span', { className: 'edrv-lsp-status-dot ' + (lspServer?.phase === 'ready' ? 'ready' : lspServer?.phase === 'indexing' || lspServer?.phase === 'starting' ? 'busy' : 'idle') }),
1443
1481
  React.createElement('span', { className: 'edrv-sp-lsp' }, lspLabel),
1444
1482
  lspServer?.progressMessage ? React.createElement('span', { className: 'edrv-sp-progress-message' }, lspServer.progressMessage) : null,
1445
1483
  lspProgress !== null ? React.createElement('span', { className: 'edrv-sp-progress' }, lspProgress + '%') : null)
1446
1484
  : null
1447
1485
 
1486
+ // AI 补全状态段(全语言可用,不随 LSP 语言过滤):就绪/请求中/完成·耗时/失败原因
1487
+ const aiEnabled = aiStatus.enabled
1488
+ const aiState = aiStatus.state
1489
+ const aiDotCls = aiState === 'busy' ? 'busy' : aiState === 'ok' ? 'ready' : aiState === 'error' ? 'idle' : 'ready'
1490
+ const aiMsText = aiStatus.detail && aiStatus.detail.ms != null ? (aiStatus.detail.ms / 1000).toFixed(1) + 's' : ''
1491
+ const aiNote = aiStatus.detail && aiStatus.detail.note ? String(aiStatus.detail.note) : ''
1492
+ const aiLabel = !aiEnabled ? 'AI 补全 · 关'
1493
+ : aiState === 'busy' ? 'AI 补全请求中…'
1494
+ : aiState === 'ok' ? 'AI 补全完成' + (aiMsText ? ' · ' + aiMsText : '')
1495
+ : aiState === 'error' ? 'AI 补全失败 · ' + aiNote.slice(0, 48)
1496
+ : 'AI 补全就绪'
1497
+ const aiSegStyle = { display: 'inline-flex', alignItems: 'center', gap: '10px', minWidth: 0, flex: '0 0 auto' }
1498
+ const aiSeg = (active && aiEnabled !== null)
1499
+ ? React.createElement('span', { className: 'edrv-status-seg', style: aiSegStyle, title: aiState === 'error' && aiNote ? aiNote : 'AI 自动补全(设置页「AI 补全」可配置)' },
1500
+ React.createElement('span', { className: 'edrv-lsp-status-dot ' + (!aiEnabled ? 'idle' : aiDotCls) }),
1501
+ React.createElement('span', { className: 'edrv-sp-lsp' }, aiLabel))
1502
+ : null
1503
+
1504
+ // 底部状态栏:单行多段——LSP 段居左(弹性吸收空闲宽度),AI 段固定右对齐
1505
+ const statusBar = (lspSeg || aiSeg)
1506
+ ? React.createElement('div', { className: 'edrv-statusbar' }, lspSeg, aiSeg)
1507
+ : null
1508
+
1448
1509
  // 导航历史按钮:目标条目名(tooltip 提示下一步会回到哪个文件)
1449
1510
  const navBackTarget = navRef.current.peekBack()
1450
1511
  const navForwardTarget = navRef.current.peekForward()
@@ -1662,7 +1723,7 @@ export function EditorView(props) {
1662
1723
  tabRow,
1663
1724
  sideHintEl,
1664
1725
  editorArea,
1665
- lspFooter)
1726
+ statusBar)
1666
1727
  // 编辑器根节点按 composer 顶部边界动态限高,底部对话区域继续由 DSH 原生渲染。
1667
1728
  // 侧栏形态由面板容器给高(100%),不做 composer 几何同步。
1668
1729
  const editorRow = React.createElement('div', { className: 'edrv-editor-row' },
@@ -15,6 +15,7 @@ import { KeybindingsPanel } from './KeybindingsPanel.js'
15
15
  import { LspSettings } from './LspSettings.js'
16
16
  import { PerfSettings } from './PerfSettings.js'
17
17
  import { IntegrationSettings } from './IntegrationSettings.js'
18
+ import { AiSettings } from './AiSettings.js'
18
19
 
19
20
  const EMPTY = { serverName: '', transport: 'stdio', command: '', args: '', cwd: '', url: '', headers: '' }
20
21
 
@@ -429,10 +430,11 @@ export function McpSettings(props) {
429
430
  refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject })
430
431
  else if (tab === 'compat') body = React.createElement(CompatSection, { getSummary: compatSummary })
431
432
  else if (tab === 'lsp') body = React.createElement(LspSettings, null)
433
+ else if (tab === 'ai') body = React.createElement(AiSettings, null)
432
434
  else body = React.createElement(PerfSettings, null)
433
435
  return React.createElement('section', { className: 'vsm-mcp-page' },
434
436
  React.createElement('header', { className: 'vsm-mcp-header' }, React.createElement('div', null, React.createElement('h2', null, 'VSCodeMode'), React.createElement('p', null, '管理当前 profile 与各项目的 Model Context Protocol 服务。'))),
435
- React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'general' ? 'active' : '', onClick: () => setTab('general') }, '通用'), React.createElement('button', { className: tab === 'keys' ? 'active' : '', onClick: () => setTab('keys') }, '快捷键'), React.createElement('button', { className: tab === 'mcp' ? 'active' : '', onClick: () => setTab('mcp') }, 'MCP 管理'), React.createElement('button', { className: tab === 'lsp' ? 'active' : '', onClick: () => setTab('lsp') }, '语言服务器'), React.createElement('button', { className: tab === 'perf' ? 'active' : '', onClick: () => setTab('perf') }, '性能优化'), React.createElement('button', { className: tab === 'compat' ? 'active' : '', onClick: () => setTab('compat') }, '兼容性')),
437
+ React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'general' ? 'active' : '', onClick: () => setTab('general') }, '通用'), React.createElement('button', { className: tab === 'keys' ? 'active' : '', onClick: () => setTab('keys') }, '快捷键'), React.createElement('button', { className: tab === 'mcp' ? 'active' : '', onClick: () => setTab('mcp') }, 'MCP 管理'), React.createElement('button', { className: tab === 'lsp' ? 'active' : '', onClick: () => setTab('lsp') }, '语言服务器'), React.createElement('button', { className: tab === 'ai' ? 'active' : '', onClick: () => setTab('ai') }, 'AI 补全'), React.createElement('button', { className: tab === 'perf' ? 'active' : '', onClick: () => setTab('perf') }, '性能优化'), React.createElement('button', { className: tab === 'compat' ? 'active' : '', onClick: () => setTab('compat') }, '兼容性')),
436
438
  error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
437
439
  body,
438
440
  )
@@ -0,0 +1,84 @@
1
+ /**
2
+ * dsh-vscode-mode host — 诊断文件通道(client 上报 → 缓冲批量落盘)。
3
+ * 通道:edrv.debug RPC → 内存缓冲(满/空闲触发)→ 串行读改写
4
+ * `~/.dsh/dsh-vscode-mode/logs/debug.<cwdHash>.log`(512KB 上限截断保留尾部)。
5
+ * 设计:console 不一定落盘,文件可靠;写失败静默忽略(调试日志不阻塞业务)。
6
+ * 迁移自 src/rpc.ts(enqueueDebug/flushDebug),逻辑一字不改,仅终端回显改走统一日志器。
7
+ * 作者 ddj 2026-09-08
8
+ */
9
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
10
+ import { join } from 'node:path'
11
+ import { DEBUG_LOG, debugLogFile, pluginLogRoot } from './paths.js'
12
+ import { log } from './log.js'
13
+ import type { Ctx } from './store.js'
14
+
15
+ /** cwd → 内存缓冲:行数组 + 累计字节数 + 待触发 flush 定时器(攒批落盘,避免每条日志全文件读改写)。 */
16
+ const debugBuffers = new Map<string, { lines: string[]; len: number; timer: ReturnType<typeof setTimeout> | null }>()
17
+ /** cwd → Promise 链:串行化 debug 日志落盘(fs read+write 非原子,避免并发丢行)。 */
18
+ const debugWriteQueues = new Map<string, Promise<void>>()
19
+ /** debug 日志单文件上限:超限截断保留尾部,防文件无限增长拖慢每次追加。 */
20
+ const DEBUG_LOG_CAP = 512 * 1024
21
+ /** debug 日志批量缓冲上限:攒满即落盘(一次读改写),上限内不逐条写文件。 */
22
+ const DEBUG_BUF_CAP = 32 * 1024
23
+ /** debug 日志空闲 flush 延迟:缓冲未满时,静默一段时间后落盘一次。 */
24
+ const DEBUG_FLUSH_IDLE_MS = 1000
25
+ /** cwd → 已清理旧工作区 debug 日志标记(一次性)。 */
26
+ const debugLegacyCleaned = new Set<string>()
27
+ /** 诊断回显日志器(终端回显统一带 `[dsh-vscode-mode:debug]` 前缀)。 */
28
+ const debugLog = log.child('debug')
29
+
30
+ /**
31
+ * 批量落盘一条 debug 日志缓冲:读旧文件 → 追加 → 超上限截断保留尾部 → 一次写回。
32
+ * 串行链保证同一 cwd 的读改写不交错丢行;写失败静默忽略(调试日志不阻塞业务)。
33
+ * @author ddj 2026年09月08号
34
+ * @param cwd 工作区(日志落 ~/.dsh/dsh-vscode-mode/logs/debug.<cwdHash>.log)
35
+ * @param batch 本批日志行
36
+ */
37
+ function flushDebug(cwd: string, batch: string[]): Promise<void> {
38
+ const prev = debugWriteQueues.get(cwd) ?? Promise.resolve()
39
+ const task = prev.then(async () => {
40
+ try {
41
+ await mkdir(pluginLogRoot(), { recursive: true })
42
+ const target = debugLogFile(cwd)
43
+ const old = await readFile(target, 'utf8').catch(() => '')
44
+ const appended = old + batch.map((l) => new Date().toISOString() + ' ' + l + '\n').join('')
45
+ const next = appended.length > DEBUG_LOG_CAP ? appended.slice(-Math.floor(DEBUG_LOG_CAP / 2)) : appended
46
+ await writeFile(target, next, 'utf8')
47
+ // 迁移后一次性清理旧工作区 debug 日志(best-effort)
48
+ if (!debugLegacyCleaned.has(cwd)) {
49
+ debugLegacyCleaned.add(cwd)
50
+ await rm(join(cwd, DEBUG_LOG), { force: true }).catch(() => {})
51
+ }
52
+ } catch (e) { /* 写日志失败忽略 */ }
53
+ })
54
+ debugWriteQueues.set(cwd, task)
55
+ return task
56
+ }
57
+
58
+ /**
59
+ * 记录一条 client 上报的诊断日志:入队缓冲批量落盘 + 终端回显(`[dsh-vscode-mode:debug]` 前缀)。
60
+ * @author ddj 2026年09月08号
61
+ * @param _ctx DSH 上下文(入参保留:诊断通道按 cwd 隔离,当前不需要 ctx,未来过滤用)
62
+ * @param cwd 工作区(日志文件按 cwd hash 隔离)
63
+ * @param text 单条日志文本(不含换行)
64
+ */
65
+ export function debugRecord(_ctx: Ctx, cwd: string, text: string): void {
66
+ const st = debugBuffers.get(cwd) ?? { lines: [], len: 0, timer: null }
67
+ st.lines.push(text)
68
+ st.len += text.length
69
+ const flush = () => {
70
+ st.timer = null
71
+ const batch = st.lines
72
+ st.lines = []
73
+ st.len = 0
74
+ void flushDebug(cwd, batch)
75
+ }
76
+ if (st.len >= DEBUG_BUF_CAP) {
77
+ if (st.timer) clearTimeout(st.timer)
78
+ flush()
79
+ } else if (!st.timer) {
80
+ st.timer = setTimeout(flush, DEBUG_FLUSH_IDLE_MS)
81
+ }
82
+ debugBuffers.set(cwd, st)
83
+ debugLog.error(text)
84
+ }
@@ -10,12 +10,22 @@
10
10
  import type { Ctx } from './store.js'
11
11
  import { KEYBINDING_DEFAULTS } from './shared/keybindings.js'
12
12
  import { INTEGRATION_BASE_DEFAULT } from './shared/integration.js'
13
+ import type { AiConfigPatch, AiConfigView } from './shared/ai.js'
14
+ import { log } from './log.js'
13
15
 
14
16
  export const FILE_OPEN_SETTINGS_NS = 'dsh-vscode-mode'
15
17
  export const FILE_OPEN_DEFAULT = 'auto'
16
18
  export interface FileOpenSettings { fileOpenTool: string; integrationBaseUrl: string }
17
19
  export interface FileOpenSettingsState { value: string; revision?: number; update: (value: string, expectedRevision?: number) => Promise<void> }
18
20
 
21
+ /** AI 内联补全配置默认值(默认关闭;路由空 = 自动;档位空 = 跟随模型默认)。 */
22
+ export const AI_CONFIG_DEFAULT: { enabled: boolean; provider: string; model: string; effort: string } = {
23
+ enabled: false,
24
+ provider: '',
25
+ model: '',
26
+ effort: '',
27
+ }
28
+
19
29
  type SettingsProvider = {
20
30
  update?: (ns: string, patch: object, expectedRevision?: number) => Promise<void>
21
31
  describe?: (options?: unknown) => Array<{ ns?: string; value?: unknown; revision?: number }>
@@ -121,7 +131,7 @@ export async function runSettingsInstall(
121
131
  /* 装载失败按缺失处理 */
122
132
  }
123
133
  if (!deps) {
124
- console.warn('[dsh-vscode-mode] 设置依赖不可用,section ' + ns + ' 未安装(配置回退)')
134
+ log.warn('设置依赖不可用,section ' + ns + ' 未安装(配置回退)')
125
135
  return recordInstall('none', INSTALL_NONE)
126
136
  }
127
137
  const legacy = deps.installSettingsSection
@@ -130,7 +140,7 @@ export async function runSettingsInstall(
130
140
  legacy(ctx, ns, schema, entry, hooks)
131
141
  return recordInstall('legacy', INSTALL_LEGACY)
132
142
  } catch (error) {
133
- console.warn('[dsh-vscode-mode] legacy 设置安装失败,尝试服务路由:' + String(error))
143
+ log.warn('legacy 设置安装失败,尝试服务路由:' + String(error))
134
144
  }
135
145
  }
136
146
  const ctxInject = (ctx as unknown as { inject?: (services: string[], callback: (sctx: unknown) => void) => unknown }).inject
@@ -141,7 +151,7 @@ export async function runSettingsInstall(
141
151
  const provider = typeof sc.get === 'function' ? sc.get('settings') : sc.settings
142
152
  const install = (provider as { installSection?: unknown } | undefined)?.installSection
143
153
  if (typeof install !== 'function') {
144
- console.warn('[dsh-vscode-mode] settings 服务无 installSection(DSH 版本 API 变化),section ' + ns + ' 降级为配置值')
154
+ log.warn('settings 服务无 installSection(DSH 版本 API 变化),section ' + ns + ' 降级为配置值')
145
155
  recordInstall('none', INSTALL_NONE)
146
156
  return
147
157
  }
@@ -149,12 +159,12 @@ export async function runSettingsInstall(
149
159
  install.call(provider, ctx, ns, schema, entry, hooks)
150
160
  recordInstall('service', INSTALL_SERVICE)
151
161
  } catch (error) {
152
- console.warn('[dsh-vscode-mode] settings.installSection 安装失败(' + String(error) + '),section ' + ns + ' 降级为配置值')
162
+ log.warn('settings.installSection 安装失败(' + String(error) + '),section ' + ns + ' 降级为配置值')
153
163
  recordInstall('none', INSTALL_NONE)
154
164
  }
155
165
  })
156
166
  } catch (error) {
157
- console.warn('[dsh-vscode-mode] settings 服务路由不可用(' + String(error) + ')')
167
+ log.warn('settings 服务路由不可用(' + String(error) + ')')
158
168
  return recordInstall('none', INSTALL_NONE)
159
169
  }
160
170
  // inject 回调若同步执行(settings 已就绪)会覆写观测值;仍未执行(unknown)时乐观按 service 记录
@@ -218,6 +228,11 @@ export async function installOpenSettingsSection(
218
228
  keybindings: deps.z.object(keybindingsShape(deps.z)).default({ ...KEYBINDING_DEFAULTS }),
219
229
  sidebarMinWidth: deps.z.number().default(300),
220
230
  integrationBaseUrl: deps.z.string().default(INTEGRATION_BASE_DEFAULT),
231
+ // AI 内联补全(默认关;provider/model 空 = 自动路由;effort 空 = 跟随模型默认)
232
+ aiInline: deps.z.boolean().default(AI_CONFIG_DEFAULT.enabled),
233
+ aiProvider: deps.z.string().default(AI_CONFIG_DEFAULT.provider),
234
+ aiModel: deps.z.string().default(AI_CONFIG_DEFAULT.model),
235
+ aiEffort: deps.z.string().default(AI_CONFIG_DEFAULT.effort),
221
236
  })
222
237
  const strategy = await runSettingsInstall(ctx, ns, schema, entry, {
223
238
  setSource: (source) => hooks.setSource(source as () => FileOpenSettings),
@@ -226,15 +241,26 @@ export async function installOpenSettingsSection(
226
241
  return strategy === 'legacy' || strategy === 'service'
227
242
  }
228
243
 
244
+ /** AI 配置脏值读取(settings 未就绪时回退默认)。 */
245
+ function aiValueOf(stored: unknown): { enabled: boolean; provider: string; model: string; effort: string } {
246
+ const raw = (stored ?? {}) as Record<string, unknown>
247
+ return {
248
+ enabled: raw.aiInline === true,
249
+ provider: typeof raw.aiProvider === 'string' ? raw.aiProvider : AI_CONFIG_DEFAULT.provider,
250
+ model: typeof raw.aiModel === 'string' ? raw.aiModel : AI_CONFIG_DEFAULT.model,
251
+ effort: typeof raw.aiEffort === 'string' ? raw.aiEffort : AI_CONFIG_DEFAULT.effort,
252
+ }
253
+ }
254
+
229
255
  /**
230
256
  * 注册设置命名空间,并提供 host 侧的读取与更新状态。
231
257
  * @author ddj 2026年08月24号
232
258
  * @param ctx DSH host 上下文
233
259
  * @param config 插件组合配置
234
260
  * @param onChange 设置变化回调
235
- * @returns 设置状态
261
+ * @returns 设置状态(fileOpenTool 值 + AI 补全配置读写)
236
262
  */
237
- export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: string) => void): FileOpenSettingsState {
263
+ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: string) => void): FileOpenSettingsState & { ai: () => AiConfigView; aiUpdate: (patch: AiConfigPatch, expectedRevision?: number) => Promise<AiConfigView> } {
238
264
  let current = configValue(config)
239
265
  let revision: number | undefined
240
266
  let provider: SettingsProvider | undefined
@@ -246,9 +272,25 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
246
272
  const setSource = (source: () => FileOpenSettings): void => notify(source().fileOpenTool)
247
273
  const settingsChange = (): void => syncRevision()
248
274
 
249
- void installOpenSettingsSection(ctx, FILE_OPEN_SETTINGS_NS, { fileOpenTool: current, integrationBaseUrl: baseValueOf(config) }, { setSource, onChange: settingsChange })
275
+ /** AI 配置当前值(settings 未就绪回退默认)。 */
276
+ let aiCurrent: AiConfigView = { ...AI_CONFIG_DEFAULT }
277
+ const aiSync = (): void => {
278
+ const descriptor = provider?.describe?.({ redactSecrets: true })?.find((item: { ns?: string }) => item.ns === FILE_OPEN_SETTINGS_NS)
279
+ const stored = descriptor?.value
280
+ aiCurrent = stored !== undefined ? aiValueOf(stored) : aiCurrent
281
+ }
282
+
283
+ void installOpenSettingsSection(ctx, FILE_OPEN_SETTINGS_NS, { fileOpenTool: current, integrationBaseUrl: baseValueOf(config) }, {
284
+ setSource: (source) => {
285
+ notify(source().fileOpenTool)
286
+ aiCurrent = aiValueOf(source())
287
+ syncRevision()
288
+ },
289
+ onChange: settingsChange,
290
+ })
250
291
  ctx.inject?.(['settings'], (settingsCtx: Ctx) => {
251
292
  provider = settingsCtx.get('settings')
293
+ aiSync()
252
294
  syncRevision()
253
295
  })
254
296
 
@@ -264,6 +306,26 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
264
306
  notify(stored?.fileOpenTool ?? next)
265
307
  syncRevision()
266
308
  },
309
+ ai: () => aiCurrent,
310
+ aiUpdate: async (patch: AiConfigPatch, expectedRevision?: number): Promise<AiConfigView> => {
311
+ if (!provider?.update) {
312
+ // settings 不可用:内存态生效(重启回落默认),保持与 fileOpenTool 的降级语义一致
313
+ if (patch.enabled !== undefined) aiCurrent.enabled = patch.enabled
314
+ if (patch.provider !== undefined) aiCurrent.provider = patch.provider
315
+ if (patch.model !== undefined) aiCurrent.model = patch.model
316
+ if (patch.effort !== undefined) aiCurrent.effort = patch.effort
317
+ return aiCurrent
318
+ }
319
+ const stored = { ...aiCurrent }
320
+ const body: Record<string, unknown> = {}
321
+ if (patch.enabled !== undefined) { stored.enabled = patch.enabled; body.aiInline = patch.enabled }
322
+ if (patch.provider !== undefined) { stored.provider = patch.provider; body.aiProvider = patch.provider }
323
+ if (patch.model !== undefined) { stored.model = patch.model; body.aiModel = patch.model }
324
+ if (patch.effort !== undefined) { stored.effort = patch.effort; body.aiEffort = patch.effort }
325
+ await provider.update(FILE_OPEN_SETTINGS_NS, body, expectedRevision)
326
+ aiSync()
327
+ return aiCurrent
328
+ },
267
329
  }
268
330
  }
269
331
 
@@ -12,3 +12,6 @@ declare module '@deepseek-ai/schemastery' {
12
12
  // settingsNamespace,0.1.2-alpha 起移除(改由 settings 服务 installSection 方法承载)。
13
13
  // fileOpenSettings.ts 以动态导入 + 属性探测方式访问,见 SettingsDeps.installSettingsSection。
14
14
 
15
+ // @deepseek-ai/dsh-llm 不在此声明(ambient 模块无法被 import 且与 Ctx=any 惯例冲突):
16
+ // AI 补全消费的最小类型面见 src/ai/llmTypes.ts,运行时值经 ctx.get('llm') 获取。
17
+