dsh-vscode-mode 0.1.57 → 0.1.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +551 -31
- package/lib/client.js.map +1 -1
- package/lib/index.js +561 -98
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/ai/inline.ts +192 -0
- package/src/ai/llmTypes.ts +83 -0
- package/src/ai/rpc.ts +35 -0
- package/src/capture.ts +2 -1
- package/src/client/ai/inlineProvider.ts +190 -0
- package/src/client/compat.ts +3 -2
- package/src/client/externalOpen.ts +3 -2
- package/src/client/index.ts +7 -6
- package/src/client/log.ts +10 -0
- package/src/client/sidebarBridge.ts +6 -5
- package/src/client/state/records.ts +1 -1
- package/src/client/state/regions.ts +13 -6
- package/src/client/ui/AiSettings.ts +104 -0
- package/src/client/ui/EditorView.ts +65 -4
- package/src/client/ui/McpSettings.ts +3 -1
- package/src/debugLog.ts +84 -0
- package/src/fileOpenSettings.ts +70 -8
- package/src/host-deps.d.ts +3 -0
- package/src/index.ts +21 -14
- package/src/log.ts +19 -0
- package/src/mcpIsolation.ts +2 -1
- package/src/rpc.ts +10 -76
- package/src/shared/ai.ts +158 -0
- package/src/shared/diff.ts +18 -0
- package/src/shared/logger.ts +114 -0
- package/src/shared/rpc.ts +9 -0
- package/src/store.ts +8 -4
- package/src/workspace.ts +2 -1
|
@@ -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
|
-
|
|
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) {
|
|
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
|
-
|
|
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
|
-
|
|
219
|
+
log.warn('侧边栏 Tab 注册失败(' + String(error) + ')')
|
|
219
220
|
// 注册失败 = 侧栏形态不可用:立即回退中央页签,不接管打开路由
|
|
220
221
|
if (!fallbackInstalled) {
|
|
221
222
|
fallbackInstalled = true
|
|
222
|
-
try { registerLegacyFallback() } catch (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
|
|
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
|
-
/**
|
|
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
|
|
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(
|
|
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(
|
|
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) => {
|
|
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
|
-
|
|
1441
|
-
|
|
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
|
-
|
|
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
|
)
|
package/src/debugLog.ts
ADDED
|
@@ -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
|
+
}
|
package/src/fileOpenSettings.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
+
log.warn('settings.installSection 安装失败(' + String(error) + '),section ' + ns + ' 降级为配置值')
|
|
153
163
|
recordInstall('none', INSTALL_NONE)
|
|
154
164
|
}
|
|
155
165
|
})
|
|
156
166
|
} catch (error) {
|
|
157
|
-
|
|
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
|
-
|
|
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
|
|
package/src/host-deps.d.ts
CHANGED
|
@@ -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
|
+
|