dsh-vscode-mode 0.8.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.
@@ -0,0 +1,293 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — AI 智能整理弹窗(11-ai-changelist-triage)。
4
+ *
5
+ * 双阶段单弹窗:
6
+ * - 预览(run = null):按**整理功能**划分三个分组卡(建议分组 changelist / 建议还原 revert /
7
+ * 建议忽略 svn:ignore),每段段级全选 + 逐项勾选确认;默认「分组勾选(可逆无副作用)、
8
+ * 还原/忽略不勾(有副作用)」,未勾选项一律不动。
9
+ * - 执行(run 非空):弹窗不关闭,切换为进度视图——阶段徽标 + 步骤计数 + 总进度条 +
10
+ * 逐步日志(✓/✗,失败红字);「暂停」在当前 RPC 返回后的步骤边界生效(svn 子进程不可
11
+ * 安全中断,如实标注「等待当前步骤完成…」),「继续」从断点续跑,「取消剩余」终止未执行
12
+ * 步骤(已执行不回滚)。
13
+ * Esc/遮罩仅在预览阶段生效(= 取消零副作用);执行阶段防误关。
14
+ * 作者 ddj 2026年09月23号
15
+ */
16
+ import React from 'react'
17
+ import { ModalShell } from './ModalShell.js'
18
+
19
+ /**
20
+ * 预览勾选初值:分组默认全勾(changelist 可逆),还原/忽略默认不勾(有副作用)。
21
+ * @author ddj 2026年09月23号
22
+ * @param plan 三段方案
23
+ * @returns 勾选态(组级 + 逐项)
24
+ */
25
+ function initPlanSel(plan) {
26
+ return {
27
+ groups: (plan.groups || []).map((group) => ({
28
+ name: group.name,
29
+ reason: group.reason || '',
30
+ checked: true,
31
+ paths: (group.paths || []).map((path) => ({ path, checked: true })),
32
+ })),
33
+ reverts: (plan.reverts || []).map((item) => ({ path: item.path, reason: item.reason || '', checked: false })),
34
+ ignores: (plan.ignores || []).map((item) => ({ path: item.path, reason: item.reason || '', checked: false })),
35
+ }
36
+ }
37
+
38
+ /**
39
+ * 勾选态 → 执行载荷(空段/空组剔除;保持勾选顺序)。
40
+ * @author ddj 2026年09月23号
41
+ * @param sel 勾选态
42
+ * @returns 执行载荷 { groups, reverts, ignores }
43
+ */
44
+ function selectedOf(sel) {
45
+ const groups = []
46
+ for (const group of sel.groups) {
47
+ const paths = group.paths.filter((p) => p.checked).map((p) => p.path)
48
+ if (group.checked && paths.length) groups.push({ name: group.name, paths })
49
+ }
50
+ return {
51
+ groups,
52
+ reverts: sel.reverts.filter((item) => item.checked).map((item) => item.path),
53
+ ignores: sel.ignores.filter((item) => item.checked).map((item) => item.path),
54
+ }
55
+ }
56
+
57
+ /** 执行载荷 → 已选动作总数(「执行所选」禁用判定)。 */
58
+ function selectedCount(selected) {
59
+ let n = selected.reverts.length + selected.ignores.length
60
+ for (const group of selected.groups) n += group.paths.length
61
+ return n
62
+ }
63
+
64
+ /** 复选框元素(原生 input;仓库 UI 原语无 Checkbox,此处沿用补丁对话框的原生控件口径)。 */
65
+ function checkBoxEl(checked, onToggle, title) {
66
+ return React.createElement('input', {
67
+ type: 'checkbox',
68
+ checked,
69
+ title,
70
+ onChange: () => onToggle(),
71
+ onClick: (event) => event.stopPropagation(),
72
+ })
73
+ }
74
+
75
+ /** 段头(全选框 + 标题 + 计数 + 副标题)。 */
76
+ function sectionHeadEl(title, note, allChecked, onToggleAll, countText) {
77
+ return React.createElement('div', { className: 'edrv-svnplan-sechead' },
78
+ checkBoxEl(allChecked, onToggleAll, '全选/全不选'),
79
+ React.createElement('span', { className: 'edrv-svnplan-sectitle' }, title),
80
+ React.createElement('span', { className: 'edrv-svn-count' }, countText),
81
+ (note ? React.createElement('span', { className: 'edrv-svnplan-note' }, note) : null))
82
+ }
83
+
84
+ /**
85
+ * 阶段一 · 预览确认(按整理功能三分组)。
86
+ * @author ddj 2026年09月23号
87
+ * @param sel 勾选态
88
+ * @param setSel 勾选态更新
89
+ * @param open 组卡展开集合(组名)
90
+ * @param toggleOpen 组卡展开切换
91
+ * @returns 预览主体元素
92
+ */
93
+ function previewBodyEl(sel, setSel, open, toggleOpen) {
94
+ /** 通用段更新(reverts/ignores 共用)。 */
95
+ const patchItems = (key, index, checked) => setSel((old) => ({
96
+ ...old,
97
+ [key]: old[key].map((item, i) => (i === index ? { ...item, checked } : item)),
98
+ }))
99
+ const toggleItemsAll = (key, checked) => setSel((old) => ({
100
+ ...old,
101
+ [key]: old[key].map((item) => ({ ...item, checked })),
102
+ }))
103
+ /** 组级/组内更新。 */
104
+ const patchGroup = (index, patch) => setSel((old) => ({
105
+ ...old,
106
+ groups: old.groups.map((group, i) => (i === index ? { ...group, ...patch } : group)),
107
+ }))
108
+ const toggleGroupAll = (index, checked) => setSel((old) => ({
109
+ ...old,
110
+ groups: old.groups.map((group, i) => (i === index
111
+ ? { ...group, checked, paths: group.paths.map((p) => ({ ...p, checked })) }
112
+ : group)),
113
+ }))
114
+
115
+ const groupCards = sel.groups.map((group, index) => {
116
+ const done = group.paths.filter((p) => p.checked).length
117
+ const isOpen = open.has(group.name)
118
+ const fileRows = isOpen
119
+ ? group.paths.map((item, pi) => React.createElement('div', { key: item.path, className: 'edrv-svnplan-item' },
120
+ checkBoxEl(item.checked, () => patchGroup(index, {
121
+ paths: group.paths.map((p, j) => (j === pi ? { ...p, checked: !p.checked } : p)),
122
+ }), item.path),
123
+ React.createElement('span', { className: 'edrv-svnplan-path', title: item.path }, item.path)))
124
+ : null
125
+ return React.createElement('div', { key: group.name, className: 'edrv-svnplan-card' },
126
+ React.createElement('div', { className: 'edrv-svnplan-row' },
127
+ checkBoxEl(group.checked, () => toggleGroupAll(index, !group.checked), '全选该组'),
128
+ React.createElement('span', {
129
+ className: 'edrv-svnplan-groupname',
130
+ title: '点击展开/收起成员',
131
+ onClick: () => toggleOpen(group.name),
132
+ }, (isOpen ? '▾ ' : '▸ ') + group.name),
133
+ React.createElement('span', { className: 'edrv-svn-count' }, done + '/' + group.paths.length + ' 项'),
134
+ (group.reason ? React.createElement('span', { className: 'edrv-svnplan-reason', title: group.reason }, group.reason) : null)),
135
+ fileRows)
136
+ })
137
+
138
+ const itemRowsOf = (key) => sel[key].map((item, index) => React.createElement('div', { key: item.path, className: 'edrv-svnplan-item' },
139
+ checkBoxEl(item.checked, () => patchItems(key, index, !item.checked), item.path),
140
+ React.createElement('span', { className: 'edrv-svnplan-path', title: item.path }, item.path),
141
+ (item.reason ? React.createElement('span', { className: 'edrv-svnplan-reason', title: item.reason }, item.reason) : null)))
142
+
143
+ // 段级全选:取反当前全选态(checkBoxEl 的 onToggle 不回传状态,须闭包捕获)
144
+ const groupsAll = sel.groups.length > 0 && sel.groups.every((g) => g.checked && g.paths.every((p) => p.checked))
145
+ const revertsAll = sel.reverts.length > 0 && sel.reverts.every((i) => i.checked)
146
+ const ignoresAll = sel.ignores.length > 0 && sel.ignores.every((i) => i.checked)
147
+
148
+ return React.createElement(React.Fragment, null,
149
+ React.createElement('div', { className: 'edrv-svnplan-sec' },
150
+ sectionHeadEl('建议分组(changelist)', '无副作用、可逆', groupsAll,
151
+ () => setSel((old) => ({
152
+ ...old,
153
+ groups: old.groups.map((group) => ({ ...group, checked: !groupsAll, paths: group.paths.map((p) => ({ ...p, checked: !groupsAll })) })),
154
+ })), sel.groups.length + ' 组'),
155
+ React.createElement('div', { className: 'edrv-svnplan-list' }, groupCards)),
156
+ React.createElement('div', { className: 'edrv-svnplan-sec' },
157
+ sectionHeadEl('建议还原(revert)', '放弃本地改动,默认不勾', revertsAll,
158
+ () => toggleItemsAll('reverts', !revertsAll), sel.reverts.length + ' 项'),
159
+ React.createElement('div', { className: 'edrv-svnplan-list' }, itemRowsOf('reverts'))),
160
+ React.createElement('div', { className: 'edrv-svnplan-sec' },
161
+ sectionHeadEl('建议忽略(svn:ignore)', '将写入 svn:ignore 属性,随提交进仓库,默认不勾', ignoresAll,
162
+ () => toggleItemsAll('ignores', !ignoresAll), sel.ignores.length + ' 项'),
163
+ React.createElement('div', { className: 'edrv-svnplan-list' }, itemRowsOf('ignores'))))
164
+ }
165
+
166
+ /**
167
+ * 阶段二 · 执行进度(阶段徽标 + 计数 + 进度条 + 逐步日志)。
168
+ * @author ddj 2026年09月23号
169
+ * @param run 执行状态
170
+ * @returns 进度主体元素
171
+ */
172
+ function progressBodyEl(run) {
173
+ const pct = run.total > 0 ? Math.floor((run.index / run.total) * 100) : 0
174
+ const statusText = run.status === 'pausing' ? '等待当前步骤完成…(暂停在步骤边界生效)'
175
+ : run.status === 'paused' ? '已暂停'
176
+ : run.status === 'cancelling' ? '正在取消剩余步骤…'
177
+ : run.status === 'done' ? (run.cancelled ? '已取消剩余步骤' : '执行完毕')
178
+ : '执行中…'
179
+ const logRows = run.log.map((entry, index) => React.createElement('div', {
180
+ key: index,
181
+ className: 'edrv-svnplan-log' + (entry.ok ? '' : ' edrv-svnplan-log-bad'),
182
+ }, (entry.ok ? '✓ ' : '✗ ') + entry.text))
183
+ return React.createElement(React.Fragment, null,
184
+ React.createElement('div', { className: 'edrv-svnplan-stage' },
185
+ React.createElement('span', { className: 'edrv-svnplan-badge' }, statusText),
186
+ (run.cur ? React.createElement('span', { className: 'edrv-svnplan-badge edrv-svnplan-badge-cur' }, run.cur) : null),
187
+ React.createElement('span', { className: 'edrv-svn-count' }, run.index + '/' + run.total + ' 步')),
188
+ React.createElement('div', { className: 'edrv-svnplan-bar' },
189
+ React.createElement('div', { className: 'edrv-svnplan-bar-in', style: { width: pct + '%' } })),
190
+ React.createElement('div', { className: 'edrv-svnplan-logwrap' }, logRows))
191
+ }
192
+
193
+ /**
194
+ * AI 智能整理弹窗(预览 + 进度双阶段)。
195
+ * @author ddj 2026年09月23号
196
+ * @param props.plan 三段方案(host 已归一)
197
+ * @param props.meta 分析元信息 { entriesCount, diffIncluded, dropped, model, source? }(source='agent' = 混合通道)
198
+ * @param props.run 执行状态;null = 预览阶段(shape 见 SvnPanel runPlanSteps)
199
+ * @param props.onExecute 预览确认(选中载荷 → 启动执行引擎)
200
+ * @param props.onPause 暂停(步骤边界生效)
201
+ * @param props.onResume 继续
202
+ * @param props.onCancelRest 取消剩余
203
+ * @param props.onClose 关闭(预览 = 取消零副作用;执行阶段按钮仅在结束后可用)
204
+ * @returns 弹窗元素
205
+ */
206
+ export function SvnAiPlanDialog(props) {
207
+ const { plan, meta, run, onExecute, onPause, onResume, onCancelRest, onClose } = props
208
+ const [sel, setSel] = React.useState(() => initPlanSel(plan))
209
+ const [open, setOpen] = React.useState(() => new Set())
210
+ const selected = selectedOf(sel)
211
+ const count = selectedCount(selected)
212
+
213
+ /** 组卡展开切换(纯 UI 态)。 */
214
+ const toggleOpen = (name) => {
215
+ const next = new Set(open)
216
+ if (next.has(name)) next.delete(name)
217
+ else next.add(name)
218
+ setOpen(next)
219
+ }
220
+
221
+ const running = run !== null && run.status !== 'done'
222
+ const header = React.createElement('div', { className: 'edrv-svnplan-head' },
223
+ React.createElement('h3', { className: 'edrv-svnlog-title' }, 'AI 智能整理 SVN 变更'),
224
+ // 混合通道来源标注(agent 深度分析 vs 快速通道直调)
225
+ (meta?.source === 'agent'
226
+ ? React.createElement('span', { className: 'edrv-svnplan-badge', title: '会话 AI 助手深度分析(codegraph/读文件)产出' }, 'AI 助手深度分析')
227
+ : null),
228
+ (meta?.model ? React.createElement('span', { className: 'edrv-svn-count', title: '分析模型' }, meta.model) : null),
229
+ React.createElement('span', { style: { flex: 1 } }),
230
+ React.createElement('button', {
231
+ className: 'edrv-svn-act',
232
+ title: running ? '执行中不可关闭(可取消剩余)' : '关闭',
233
+ disabled: running,
234
+ onClick: onClose,
235
+ }, '✕'))
236
+
237
+ const metaEl = React.createElement('div', { className: 'edrv-svnplan-meta' },
238
+ React.createElement('span', { className: 'edrv-svn-count' }, '分析条目 ' + (meta?.entriesCount ?? 0) + ' 项'),
239
+ ((meta?.dropped ?? 0) > 0 ? React.createElement('span', { className: 'edrv-svn-count' }, '已丢弃 AI 无效建议 ' + meta.dropped + ' 项') : null),
240
+ (meta?.diffIncluded === false
241
+ ? React.createElement('span', { className: 'edrv-svnplan-note' }, '未含 diff 上下文,分析精度有限')
242
+ : null))
243
+
244
+ let body = null
245
+ let footer = null
246
+ if (run === null) {
247
+ body = previewBodyEl(sel, setSel, open, toggleOpen)
248
+ footer = React.createElement('div', { className: 'edrv-svnplan-foot' },
249
+ React.createElement('span', { className: 'edrv-svn-count' }, '已选 ' + count + ' 项'),
250
+ React.createElement('span', { style: { flex: 1 } }),
251
+ React.createElement('button', { className: 'edrv-svn-act', title: '不执行任何动作', onClick: onClose }, '取消'),
252
+ React.createElement('button', {
253
+ className: 'edrv-svn-act edrv-svnplan-primary',
254
+ title: '执行全部勾选项(未勾选不动)',
255
+ disabled: count === 0,
256
+ onClick: () => onExecute(selected),
257
+ }, '执行所选(' + count + ' 项)'))
258
+ } else {
259
+ body = progressBodyEl(run)
260
+ footer = React.createElement('div', { className: 'edrv-svnplan-foot' },
261
+ React.createElement('span', { style: { flex: 1 } }),
262
+ (run.status === 'done'
263
+ ? React.createElement('button', {
264
+ className: 'edrv-svn-act edrv-svnplan-primary',
265
+ title: '关闭弹窗并刷新变更列表',
266
+ onClick: onClose,
267
+ }, '关闭并刷新')
268
+ : React.createElement(React.Fragment, null,
269
+ React.createElement('button', {
270
+ className: 'edrv-svn-act',
271
+ title: run.status === 'running' || run.status === 'pausing'
272
+ ? '暂停(当前步骤完成后生效)'
273
+ : '从断点继续执行',
274
+ disabled: run.status === 'pausing' || run.status === 'cancelling',
275
+ onClick: run.status === 'paused' ? onResume : onPause,
276
+ }, run.status === 'paused' ? '继续' : '暂停'),
277
+ React.createElement('button', {
278
+ className: 'edrv-svn-act edrv-svn-act-danger',
279
+ title: '终止未执行步骤(已执行部分不回滚)',
280
+ disabled: run.status === 'cancelling',
281
+ onClick: onCancelRest,
282
+ }, '取消剩余'))))
283
+ }
284
+
285
+ return React.createElement(ModalShell, {
286
+ key: 'edrv-svn-plan',
287
+ dialogClass: 'edrv-svnplan-dialog',
288
+ width: 'min(680px, calc(100vw - 48px))',
289
+ closeOnMask: !running,
290
+ closeOnEsc: !running,
291
+ onClose,
292
+ }, header, metaEl, body, footer)
293
+ }
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import React from 'react'
12
12
  import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
13
+ import { classifyFileType, FileTypeIcon } from '@deepseek-ai/dsh-client-ui-primitives'
13
14
  import type { EdrvIconProps } from '@deepseek-ai/dsh-client-ui-primitives'
14
15
 
15
16
  /** 图标组件形状(与垫片 EdrvIconProps 兼容)。 */
@@ -70,3 +71,21 @@ export const IconBranchOutline16 = iconOf('IconBranchOutline16', 'IconBranchOutl
70
71
 
71
72
  // 两代同名存活的原语:直通再导出(0.1.6/0.1.7 实证均在)
72
73
  export { FileTypeIcon, classifyFileType } from '@deepseek-ai/dsh-client-ui-primitives'
74
+
75
+ /**
76
+ * 文件图标元素(官方 FileTypeIcon + classifyFileType 分类,未知类型为 other)。
77
+ * 文件树与 SVN 变更行共用(原为 FileExplorer 私有副本,上移共享防两处漂移)。
78
+ * @author ddj 2026年09月23号
79
+ * @param name 文件名(含扩展名;目录由调用方走目录图标)
80
+ * @returns 图标元素;原语缺失或分类异常返回 null
81
+ */
82
+ export function fileIconEl(name: string): React.ReactElement | null {
83
+ if (typeof FileTypeIcon !== 'function' || typeof classifyFileType !== 'function') return null
84
+ try {
85
+ // FileTypeIcon 签名为 (props) => ReactNode(非 ReactElement),createElement 重载不认——
86
+ // 纯函数组件直接调用并收窄返回类型(与 FileExplorer 原 createElement 等效)。
87
+ return FileTypeIcon({ kind: classifyFileType(String(name ?? '')), size: 16, className: 'edrv-tree-icon' }) as React.ReactElement
88
+ } catch (error) {
89
+ return null
90
+ }
91
+ }
package/src/compat.ts CHANGED
@@ -10,7 +10,7 @@ import { readFileSync } from 'node:fs'
10
10
  import { dirname, join } from 'node:path'
11
11
  import { fileURLToPath } from 'node:url'
12
12
  import { entriesOf } from './mcp.js'
13
- import { loadSettingsDeps, schemaLibName, settingsInstallNote, settingsInstallStrategy } from './fileOpenSettings.js'
13
+ import { loadSettingsDeps, schemaLibName, settingsInstallNote, settingsInstallStrategy, configVolatileState } from './fileOpenSettings.js'
14
14
  import { readDevForm } from './devForm.js'
15
15
  import { compareDshVersions, detectDshVersion, familyLabel, parseDshVersion } from './dshVersion.js'
16
16
  import { SKILL_PREFIXES, skillGroupState } from './skills.js'
@@ -133,9 +133,12 @@ export function detectGuards(ctx: Ctx): CompatAdapter[] {
133
133
  }
134
134
 
135
135
  /** 已实测覆盖的最高 DSH 版本(适配矩阵上界,超过则提示,见 buildReport)。 */
136
- const TESTED_DSH_MAX = '0.1.7-alpha.1'
136
+ const TESTED_DSH_MAX = '0.1.7-alpha.2'
137
137
 
138
- /** 版本适配机制状态行:DSH 版本探测 + 设置 section 安装策略。 */
138
+ /** Config volatile 标记起效的版本线(此前设置走 section 安装,与 volatile 无关)。 */
139
+ const CONFIG_VOLATILE_MIN = '0.1.7-alpha.1'
140
+
141
+ /** 版本适配机制状态行:DSH 版本探测 + 设置 section 安装策略 + Config volatile 标记。 */
139
142
  export function versionAdapters(dshVersion: string): CompatAdapter[] {
140
143
  const adapters: CompatAdapter[] = []
141
144
  const parsed = parseDshVersion(dshVersion)
@@ -152,12 +155,24 @@ export function versionAdapters(dshVersion: string): CompatAdapter[] {
152
155
  active: strategy === 'legacy' || strategy === 'service' || strategy === 'forms',
153
156
  note: settingsInstallNote(),
154
157
  })
158
+ // 追加在末尾(既有下标有断言);仅 Config 按 volatile 语义构建过才展示该行
159
+ const volatileState = configVolatileState()
160
+ if (volatileState.requested) {
161
+ const volatileOk = volatileState.total > 0 && volatileState.marked === volatileState.total
162
+ adapters.push({
163
+ name: 'Config volatile 字段(0.1.7 设置页)',
164
+ active: volatileOk,
165
+ note: volatileOk
166
+ ? volatileState.marked + '/' + volatileState.total + ' 已标记(SettingsForms 可下发本插件 ns)'
167
+ : volatileState.marked + '/' + volatileState.total + ' 已标记:schemastery 依赖过旧无 .volatile(),0.1.7 设置页不可用',
168
+ })
169
+ }
155
170
  return adapters
156
171
  }
157
172
 
158
173
  /**
159
174
  * 构建完整兼容性报告(RPC 与启动日志共用)。
160
- * @author ddj 2026年08月24号 / 2026年09月02号
175
+ * @author ddj 2026年08月24号 / 2026年09月02号 / 2026年09月23号
161
176
  * @param ctx DSH host 上下文
162
177
  * @param options 测试注入:depsAvailable 跳过动态导入、version 固定插件版本、dshVersion 固定 DSH 版本
163
178
  * @returns 兼容性报告
@@ -180,6 +195,12 @@ export async function buildReport(
180
195
  if (parsed && testedMax && compareDshVersions(parsed, testedMax) > 0) {
181
196
  warnings.push('DSH ' + dshVersion + ' 高于已实测版本(' + TESTED_DSH_MAX + '):设置 API 按能力探测运行,异常时请回报适配矩阵')
182
197
  }
198
+ // 0.1.7 线上 Config 未完成 volatile 标记 = 设置页不可用(ns 不下发),显式告警而非静默
199
+ const volatileState = configVolatileState()
200
+ const volatileMin = parseDshVersion(CONFIG_VOLATILE_MIN)
201
+ if (volatileState.requested && volatileState.marked < volatileState.total && parsed && volatileMin && compareDshVersions(parsed, volatileMin) >= 0) {
202
+ warnings.push('插件 Config volatile 标记不完整(' + volatileState.marked + '/' + volatileState.total + '):@deepseek-ai/schemastery 依赖过旧,0.1.7 设置页不可用(重装插件依赖后重启 DSH)')
203
+ }
183
204
  for (const guard of guards) {
184
205
  if (!guard.active && guard.note) warnings.push(guard.note)
185
206
  }
@@ -6,7 +6,7 @@
6
6
  * 插件 Config schema,见 runSettingsInstall 四策略:legacy/service/forms/none)。
7
7
  * 缺失/任一策略失败时插件仍可加载(fileOpenTool 降级为配置值,compat 报告可见),
8
8
  * 全程 try/catch,不产生未捕获 rejection。
9
- * 作者 ddj 2026年08月24号 / 2026年08月26号 / 2026年09月02号 / 2026年09月22号
9
+ * 作者 ddj 2026年08月24号 / 2026年08月26号 / 2026年09月02号 / 2026年09月22号 / 2026年09月23号
10
10
  */
11
11
  import { createRequire } from 'node:module'
12
12
  import { pathToFileURL } from 'node:url'
@@ -24,12 +24,15 @@ export const FILE_OPEN_DEFAULT = 'auto'
24
24
  export interface FileOpenSettings { fileOpenTool: string; integrationBaseUrl: string; svnPath: string; tortoisePath: string }
25
25
  export interface FileOpenSettingsState { value: string; revision?: number; update: (value: string, expectedRevision?: number) => Promise<void> }
26
26
 
27
- /** AI 内联补全配置默认值(默认关闭;路由空 = 自动;档位空 = 跟随模型默认)。 */
28
- 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 } = {
29
29
  enabled: false,
30
30
  provider: '',
31
31
  model: '',
32
32
  effort: '',
33
+ taskProvider: '',
34
+ taskModel: '',
35
+ taskEffort: '',
33
36
  }
34
37
 
35
38
  type SettingsProvider = {
@@ -405,29 +408,118 @@ export async function updateSection(provider: SettingsProvider | undefined, ns:
405
408
  * 字段集 = fileOpenTool/keybindings/sidebarMinWidth/maxOpenEditors/integrationBaseUrl/
406
409
  * aiInline/aiProvider/aiModel/aiEffort/svnPath/tortoisePath/nativeOpenExts,全部带默认值
407
410
  * (Config 启动校验在 undefined/空配置下自动填充,rc/alpha 两代 cordis 均通过)。
408
- * @author ddj 2026年09月22号
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 与观测)
409
419
  * @param z schemastery 命名空间(静态 import 或动态加载均可)
420
+ * @param options volatile:是否按 0.1.7 Config 语义标记字段
410
421
  * @returns schemastery object schema
411
422
  */
412
- export function buildSettingsSchema(z: SettingsDeps['z']): unknown {
413
- return z.object({
414
- fileOpenTool: z.string().default(FILE_OPEN_DEFAULT),
415
- keybindings: z.object(keybindingsShape(z)).default({ ...KEYBINDING_DEFAULTS }),
416
- sidebarMinWidth: z.number().default(300),
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)),
417
436
  // 页签数量上限(0 = 不限制;超限时淘汰最久未使用的页签,固定页签除外)
418
- maxOpenEditors: z.number().default(EDITOR_LIMIT_DEFAULT),
419
- integrationBaseUrl: z.string().default(INTEGRATION_BASE_DEFAULT),
437
+ maxOpenEditors: withVol(z.number().default(EDITOR_LIMIT_DEFAULT)),
438
+ integrationBaseUrl: withVol(z.string().default(INTEGRATION_BASE_DEFAULT)),
420
439
  // AI 内联补全(默认关;provider/model 空 = 自动路由;effort 空 = 跟随模型默认)
421
- aiInline: z.boolean().default(AI_CONFIG_DEFAULT.enabled),
422
- aiProvider: z.string().default(AI_CONFIG_DEFAULT.provider),
423
- aiModel: z.string().default(AI_CONFIG_DEFAULT.model),
424
- aiEffort: z.string().default(AI_CONFIG_DEFAULT.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)),
425
448
  // SVN 能力:svn CLI 覆盖(空 = PATH)与 TortoiseSVN 目录(Windows 过渡增强)
426
- svnPath: z.string().default(''),
427
- tortoisePath: z.string().default(TORTOISE_DIR_DEFAULT),
449
+ svnPath: withVol(z.string().default('')),
450
+ tortoisePath: withVol(z.string().default(TORTOISE_DIR_DEFAULT)),
428
451
  // 原生打开范围(逗号分隔后缀;默认 = 让位清单并集,csv/tsv 决策见 shared/nativeOpen.ts)
429
- nativeOpenExts: z.string().default(DEFAULT_NATIVE_CSV),
430
- })
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])
431
523
  }
432
524
 
433
525
  function normalizeValue(value: unknown): string {
@@ -436,24 +528,24 @@ function normalizeValue(value: unknown): string {
436
528
  }
437
529
 
438
530
  function configValue(config: unknown): string {
439
- return normalizeValue((config as { fileOpenTool?: unknown } | undefined)?.fileOpenTool)
531
+ return normalizeValue(configField(config, 'fileOpenTool'))
440
532
  }
441
533
 
442
534
  /** 配置/设置里的深链基址(缺省/非法回退默认值)。 */
443
535
  function baseValueOf(config: unknown): string {
444
- const raw = (config as { integrationBaseUrl?: unknown } | undefined)?.integrationBaseUrl
536
+ const raw = configField(config, 'integrationBaseUrl')
445
537
  return typeof raw === 'string' && raw.trim() ? raw.trim() : INTEGRATION_BASE_DEFAULT
446
538
  }
447
539
 
448
540
  /** 配置/设置里的 svn CLI 覆盖(空 = 从 PATH 解析 'svn')。 */
449
541
  function svnPathValueOf(config: unknown): string {
450
- const raw = (config as { svnPath?: unknown } | undefined)?.svnPath
542
+ const raw = configField(config, 'svnPath')
451
543
  return typeof raw === 'string' ? raw.trim() : ''
452
544
  }
453
545
 
454
546
  /** 配置/设置里的 TortoiseSVN 目录(空 = 默认安装目录)。 */
455
547
  function tortoiseDirValueOf(config: unknown): string {
456
- const raw = (config as { tortoisePath?: unknown } | undefined)?.tortoisePath
548
+ const raw = configField(config, 'tortoisePath')
457
549
  return typeof raw === 'string' && raw.trim() ? raw.trim() : TORTOISE_DIR_DEFAULT
458
550
  }
459
551
 
@@ -490,13 +582,16 @@ export async function installOpenSettingsSection(
490
582
  }
491
583
 
492
584
  /** AI 配置脏值读取(settings 未就绪时回退默认)。 */
493
- 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 } {
494
586
  const raw = (stored ?? {}) as Record<string, unknown>
495
587
  return {
496
588
  enabled: raw.aiInline === true,
497
589
  provider: typeof raw.aiProvider === 'string' ? raw.aiProvider : AI_CONFIG_DEFAULT.provider,
498
590
  model: typeof raw.aiModel === 'string' ? raw.aiModel : AI_CONFIG_DEFAULT.model,
499
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,
500
595
  }
501
596
  }
502
597
 
@@ -555,8 +650,12 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
555
650
  })
556
651
  ctx.inject?.(['settings'], (settingsCtx: Ctx) => {
557
652
  provider = settingsCtx.get('settings')
558
- aiSync()
559
- 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)
560
659
  syncRevision()
561
660
  })
562
661
 
@@ -580,6 +679,9 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
580
679
  if (patch.provider !== undefined) { stored.provider = patch.provider; body.aiProvider = patch.provider }
581
680
  if (patch.model !== undefined) { stored.model = patch.model; body.aiModel = patch.model }
582
681
  if (patch.effort !== undefined) { stored.effort = patch.effort; body.aiEffort = patch.effort }
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 }
583
685
  const result = await updateSection(provider, FILE_OPEN_SETTINGS_NS, body, expectedRevision)
584
686
  if (!result.ok) {
585
687
  // settings 不可用/写入失败:内存态生效(重启回落默认),与 fileOpenTool 降级语义一致
package/src/index.ts CHANGED
@@ -44,11 +44,14 @@ export const inject = ['sessions', 'fs', 'webServer', 'loader', 'tools', 'worksp
44
44
  * 字段集与 installOpenSettingsSection 的 section schema 同源(buildSettingsSchema),
45
45
  * 全字段带默认值——undefined/空配置经 schemastery 校验自动填充(rc/alpha 两代
46
46
  * cordis 均读该导出,实测 4.0.0-rc.8 与 0.1.7 行为一致,校验必过)。
47
- * 运行时解析:@deepseek-ai/schemastery 经插件自身 node_modules(link/dev/npm 安装
48
- * 三形态均在 peer/dev 依赖面内,先于宿主树命中)。
49
- * @author ddj 2026年09月22号
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 标记)
50
53
  */
51
- export const Config = buildSettingsSchema(z as unknown as SettingsDeps['z'])
54
+ export const Config = buildSettingsSchema(z as unknown as SettingsDeps['z'], { volatile: true })
52
55
 
53
56
  /**
54
57
  * 装配插件:挂事件监听、注册路由、安装兼容层。
package/src/model.ts CHANGED
@@ -32,7 +32,8 @@ export function normalizeRecord(raw: unknown): DiffRecord | null {
32
32
  }
33
33
  })
34
34
  : []
35
- const toolName: DiffRecord['toolName'] = r.toolName === 'write' ? 'write' : 'edit'
35
+ // 白名单保留已知工具名,未知/缺失回退 'edit'(旧 sidecar 语义不变)
36
+ const toolName: DiffRecord['toolName'] = r.toolName === 'write' || r.toolName === 'remote_ssh_write' ? r.toolName : 'edit'
36
37
  const rawDecisions = r.decisions && typeof r.decisions === 'object' ? r.decisions as Record<string, unknown> : {}
37
38
  const call = rawDecisions.call === 'accepted' || rawDecisions.call === 'rejected' ? rawDecisions.call : 'pending'
38
39
  const rawPerHunk = Array.isArray(rawDecisions.perHunk) ? rawDecisions.perHunk : []