dsh-prompt-customizer 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,16 @@
1
- /** 配置 Tab:两种「预设」分开 ——
2
- * - 保存当前配置:本插件内的配置快照库(config.presets),可应用 / 导出 / 导入,
3
- * 不会变成可选的 agent 预设;
4
- * - 存为 agent 预设:整体 fork 当前编辑目标的预设目录,并把当前定制写进它的
5
- * 覆盖项,新预设随即出现在顶部的编辑目标选择器里。
6
- * 导入导出经 preset-io 双端适配:Tauri 2 桌面走原生对话框,Web 走
7
- * 下载 / 文件选择;成功 / 失败 / 取消都有面板内消息条提示。 */
1
+ /**
2
+ * PresetsPane — 独立面板的预设模式(左右双栏)。
3
+ *
4
+ * 两种「预设」分开,各占一栏:
5
+ * - 左栏(PresetsPane):配置快照库 —— 保存当前配置 / 导入 / 应用 / 导出 /
6
+ * 删除。快照只改本插件内的 presets 库,不会变成可选的 agent 预设;
7
+ * - 右栏(SettingsPane):存为 agent 预设(整体 fork 当前编辑目标的预设目录,
8
+ * 并把当前定制写进它的覆盖项)+ 全局设置(强制覆盖开关、环境变量黑名单、
9
+ * 提示词变量参考、恢复初始状态)。
10
+ *
11
+ * 导入导出经 preset-io 双端适配:Tauri 2 桌面走原生对话框,Web 走下载 /
12
+ * 文件选择;成功 / 失败 / 取消都有面板内消息条提示。
13
+ */
8
14
  import { createElement as h, useEffect, useRef, useState, type ReactElement, type ChangeEvent } from 'react'
9
15
  import type { Config, Inventory, PhaseViewKey, Preview, Preset } from './types.ts'
10
16
  import type { Translate } from './locales.ts'
@@ -18,7 +24,22 @@ interface Notice {
18
24
  text: string
19
25
  }
20
26
 
21
- export function PresetsTab({ cfg, inv, phases, t, writePatch, writeGlobal, saveAsPreset, forkSource, onReset, envBlocklist }: {
27
+ /** 4 秒自动消失的面板内消息条(左右两栏共用行为)。 */
28
+ function useNotice(): { notice: Notice | null; show: (kind: Notice['kind'], text: string) => void } {
29
+ const [notice, setNotice] = useState<Notice | null>(null)
30
+ const noticeTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
31
+ useEffect(() => () => {
32
+ if (noticeTimer.current !== null) clearTimeout(noticeTimer.current)
33
+ }, [])
34
+ const show = (kind: Notice['kind'], text: string): void => {
35
+ if (noticeTimer.current !== null) clearTimeout(noticeTimer.current)
36
+ setNotice({ kind, text })
37
+ noticeTimer.current = setTimeout(() => setNotice(null), 4000)
38
+ }
39
+ return { notice, show }
40
+ }
41
+
42
+ export function PresetsPane({ cfg, inv, phases, t, writePatch, writeGlobal, envBlocklist }: {
22
43
  cfg: Config
23
44
  inv: Inventory | null
24
45
  /** Panel 并行拉取的三阶段装配:快照只捕获当前装配里真实存在的段。 */
@@ -28,38 +49,14 @@ export function PresetsTab({ cfg, inv, phases, t, writePatch, writeGlobal, saveA
28
49
  writePatch: (patch: ConfigPatch) => void
29
50
  /** 预设库专用:永远写在全局字段(presets / activePreset 不分作用域)。 */
30
51
  writeGlobal: (field: string, value: unknown) => void
31
- /** 存为 agent 预设:fork 当前编辑目标的预设目录并写入 overrides[新名]。 */
32
- saveAsPreset: (name: string) => Promise<boolean>
33
- /** fork 来源的显示名(当前编辑目标预设);undefined = 全局目标,由服务端回落默认预设。 */
34
- forkSource: string | undefined
35
- /** 恢复初始状态:清空全部定制并关闭 forceSections(与不装插件等效)。 */
36
- onReset: () => void
37
52
  /** 全局环境变量黑名单(永远顶层字段,不分作用域);导出随行、导入并集。 */
38
53
  envBlocklist: string[]
39
54
  }): ReactElement {
40
55
  const presets = cfg.presets ?? []
41
56
  const [name, setName] = useState('')
42
- // 黑名单「添加」输入框的暂存值。
43
- const [blockInput, setBlockInput] = useState('')
44
- // 「存为 agent 预设」的名字与在途标记(与服务端往返期间禁用按钮)。
45
- const [agentName, setAgentName] = useState('')
46
- const [creating, setCreating] = useState(false)
47
- const [notice, setNotice] = useState<Notice | null>(null)
57
+ const { notice, show } = useNotice()
48
58
  // Web 回退用的隐藏文件输入(tauri 不可用时才点它)。
49
59
  const fileRef = useRef<HTMLInputElement | null>(null)
50
- // 消息条自动消失的定时器;新消息会重置旧定时器。
51
- const noticeTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
52
-
53
- useEffect(() => () => {
54
- if (noticeTimer.current !== null) clearTimeout(noticeTimer.current)
55
- }, [])
56
-
57
- // 显示提示;ok 与 error 分别配色,4 秒后自动消失。
58
- const show = (kind: Notice['kind'], text: string): void => {
59
- if (noticeTimer.current !== null) clearTimeout(noticeTimer.current)
60
- setNotice({ kind, text })
61
- noticeTimer.current = setTimeout(() => setNotice(null), 4000)
62
- }
63
60
 
64
61
  const blockedNames = new Set(cfg.sections ?? [])
65
62
  // 快照的捕获宇宙 = 当前预设三个阶段装配里真实存在的段名。「本系统全部提示词」
@@ -76,7 +73,7 @@ export function PresetsTab({ cfg, inv, phases, t, writePatch, writeGlobal, saveA
76
73
  const currentNames = new Set(merged.map((sec) => sec.name))
77
74
 
78
75
  // 把当前定制捕获为新预设(完整快照)。绝对顺序会被转成相对形式
79
- // (每段记录它应跟随的前一段),因此预设可以跨不同提示词移植。
76
+ //(每段记录它应跟随的前一段),因此预设可以跨不同提示词移植。
80
77
  const saveCurrent = (): void => {
81
78
  const presetName = name.trim() || `${t('preset')} ${presets.length + 1}`
82
79
  const data = buildPresetData(cfg, merged)
@@ -84,13 +81,9 @@ export function PresetsTab({ cfg, inv, phases, t, writePatch, writeGlobal, saveA
84
81
  setName('')
85
82
  }
86
83
 
87
- // 应用预设。补丁由纯函数助手计算:
88
- // - 同名段被覆盖(内容 + 顺序)
89
- // - 预设中有、当前系统里匹配不上的段默认跳过(跨系统导入不凭空建段)
90
- // - 当前有、但不在预设列表中的段被屏蔽
91
- // - 只有预设的「激活段」被解除屏蔽(预设自己的屏蔽名单被保留)
92
- // 四个定制字段以一个补丁写入当前编辑目标(全局顶层或 overrides[id]);
93
- // 快照库(presets / activePreset)永远保持在全局。
84
+ // 应用预设。补丁由纯函数助手计算(同名段覆盖、匹配不上的跳过、不在预设
85
+ // 列表中的当前段被屏蔽、只有预设的「激活段」被解除屏蔽)。四个定制字段以
86
+ // 一个补丁写入当前编辑目标;快照库(presets / activePreset)永远保持在全局。
94
87
  const applyPreset = (preset: Preset): void => {
95
88
  writePatch(applyPresetData(preset.data, cfg, currentNames))
96
89
  writeGlobal('activePreset', preset.id)
@@ -116,8 +109,7 @@ export function PresetsTab({ cfg, inv, phases, t, writePatch, writeGlobal, saveA
116
109
  }
117
110
 
118
111
  // 解析导入文本 → 去重追加 → 提示新增数量。解析失败提示错误。
119
- // 单对象格式的文件还可能随行携带全局环境变量黑名单:与当前名单并集合并
120
- // (只增不减),新增条数一并报进提示。
112
+ // 单对象格式的文件还可能随行携带全局环境变量黑名单:与当前名单并集合并。
121
113
  const importParsed = (text: string): void => {
122
114
  try {
123
115
  const parsed = decodePresetExport(text)
@@ -164,9 +156,61 @@ export function PresetsTab({ cfg, inv, phases, t, writePatch, writeGlobal, saveA
164
156
  reader.readAsText(file)
165
157
  }
166
158
 
167
- // 存为 agent 预设:fork 当前编辑目标的预设目录 + 把当前定制写进它的覆盖项。
168
- // 成败由服务端返回,提示统一走面板顶部的消息条(Panel 的 flash),这里只在
169
- // 成功时清空输入框,避免同一条消息显示两遍。
159
+ return h('div', { style: s.colLeft }, [
160
+ h('div', { style: s.colScroll }, [
161
+ notice ? h('div', { style: notice.kind === 'ok' ? s.noticeOk : s.error }, notice.text) : null,
162
+ h('div', { style: s.groupHead }, t('libraryTitle')),
163
+ h('div', { style: s.injectBox }, [
164
+ h('div', { style: s.injectRow }, [
165
+ h('input', { style: { ...s.input, flex: 1 }, placeholder: t('presetName'), value: name, onChange: (e: ChangeEvent<HTMLInputElement>) => setName(e.target.value) }),
166
+ h('button', { style: s.mini, onClick: saveCurrent }, t('save')),
167
+ ]),
168
+ h('div', { style: s.injectRow }, [
169
+ h('button', { style: s.mini, onClick: () => { void importPreset() } }, t('import')),
170
+ h('input', { ref: fileRef, type: 'file', accept: '.json,application/json', style: { display: 'none' }, onChange: onImportFile }),
171
+ ]),
172
+ ]),
173
+ presets.length === 0 ? h('div', { style: s.muted }, t('empty')) : null,
174
+ presets.map((preset) => {
175
+ const active = cfg.activePreset === preset.id
176
+ return h('div', { key: preset.id, style: s.row }, [
177
+ h('div', { style: s.rowBody }, [
178
+ h('div', { style: s.rowTitle }, [
179
+ h('span', { style: s.code }, preset.name),
180
+ active ? h('span', { style: s.badgeOk }, t('active')) : null,
181
+ ]),
182
+ ]),
183
+ h('button', { style: s.mini, onClick: () => applyPreset(preset) }, t('apply')),
184
+ h('button', { style: s.mini, onClick: () => { void exportPreset(preset) } }, t('export')),
185
+ h('button', { style: s.mini, onClick: () => deletePreset(preset.id) }, t('delete')),
186
+ ])
187
+ }),
188
+ ]),
189
+ ])
190
+ }
191
+
192
+ export function SettingsPane({ cfg, inv, t, writeGlobal, saveAsPreset, forkSource, onReset, envBlocklist }: {
193
+ cfg: Config
194
+ inv: Inventory | null
195
+ t: Translate
196
+ /** 全局字段专用写通道(forceSections / envBlocklist,不经草稿)。 */
197
+ writeGlobal: (field: string, value: unknown) => void
198
+ /** 存为 agent 预设:fork 当前编辑目标的预设目录并写入 overrides[新名]。 */
199
+ saveAsPreset: (name: string) => Promise<boolean>
200
+ /** fork 来源的显示名(当前编辑目标预设);undefined = 全局目标,由服务端回落默认预设。 */
201
+ forkSource: string | undefined
202
+ /** 恢复初始状态:清空全部定制并关闭 forceSections(与不装插件等效)。 */
203
+ onReset: () => void
204
+ envBlocklist: string[]
205
+ }): ReactElement {
206
+ // 「存为 agent 预设」的名字与在途标记(与服务端往返期间禁用按钮)。
207
+ const [agentName, setAgentName] = useState('')
208
+ const [creating, setCreating] = useState(false)
209
+ // 黑名单「添加」输入框的暂存值。
210
+ const [blockInput, setBlockInput] = useState('')
211
+
212
+ // 存为 agent 预设:成败由服务端返回,提示统一走面板顶部的消息条(Panel
213
+ // 的 flash),这里只在成功时清空输入框,避免同一条消息显示两遍。
170
214
  const createAgentPreset = async (): Promise<void> => {
171
215
  const trimmed = agentName.trim()
172
216
  if (trimmed.length === 0 || creating) return
@@ -186,96 +230,69 @@ export function PresetsTab({ cfg, inv, phases, t, writePatch, writeGlobal, saveA
186
230
  writeGlobal('envBlocklist', [...envBlocklist, entry])
187
231
  }
188
232
 
189
- return h('div', { style: s.list }, [
190
- notice ? h('div', { style: notice.kind === 'ok' ? s.noticeOk : s.error }, notice.text) : null,
191
- h('div', { style: s.injectBox }, [
192
- h('div', { style: s.rowTitle }, t('savePreset')),
193
- h('div', { style: s.injectRow }, [
194
- h('input', { style: { ...s.input, flex: 1 }, placeholder: t('presetName'), value: name, onChange: (e: ChangeEvent<HTMLInputElement>) => setName(e.target.value) }),
195
- h('button', { style: s.mini, onClick: saveCurrent }, t('save')),
196
- ]),
197
- ]),
198
- h('div', { style: s.injectBox }, [
199
- h('div', { style: s.rowTitle }, t('saveAsPresetCard')),
200
- h('div', { style: s.muted }, t('saveAsPresetHint', { name: forkSource ?? t('forkSourceDefault') })),
201
- h('div', { style: s.injectRow }, [
202
- h('input', { style: { ...s.input, flex: 1 }, placeholder: t('agentPresetName'), value: agentName, onChange: (e: ChangeEvent<HTMLInputElement>) => setAgentName(e.target.value) }),
203
- h('button', { style: s.mini, disabled: creating || agentName.trim().length === 0, onClick: () => { void createAgentPreset() } }, t('saveAsPreset')),
233
+ return h('div', { style: s.colRight }, [
234
+ h('div', { style: s.colRightScroll }, [
235
+ h('div', { style: s.injectBox }, [
236
+ h('div', { style: s.rowTitle }, t('saveAsPresetCard')),
237
+ h('div', { style: s.muted }, t('saveAsPresetHint', { name: forkSource ?? t('forkSourceDefault') })),
238
+ h('div', { style: s.injectRow }, [
239
+ h('input', { style: { ...s.input, flex: 1 }, placeholder: t('agentPresetName'), value: agentName, onChange: (e: ChangeEvent<HTMLInputElement>) => setAgentName(e.target.value) }),
240
+ h('button', { style: s.mini, disabled: creating || agentName.trim().length === 0, onClick: () => { void createAgentPreset() } }, t('saveAsPreset')),
241
+ ]),
204
242
  ]),
205
- ]),
206
- h('div', { style: s.injectBox }, [
207
- h('div', { style: s.rowTitle }, t('importPreset')),
208
- h('div', { style: s.injectRow }, [
209
- h('button', { style: s.mini, onClick: () => { void importPreset() } }, t('import')),
210
- h('input', { ref: fileRef, type: 'file', accept: '.json,application/json', style: { display: 'none' }, onChange: onImportFile }),
243
+ h('div', { style: s.groupHead }, t('settingsTitle')),
244
+ h('div', { style: s.injectBox }, [
245
+ h('div', { style: s.rowTitle }, t('forceTitle')),
246
+ h('div', { style: s.muted }, t('forceHint')),
247
+ h('div', { style: s.injectRow }, [
248
+ h('label', { style: s.switchWrap }, [
249
+ h('input', {
250
+ type: 'checkbox',
251
+ checked: cfg.forceSections !== false,
252
+ // forceSections 是全局字段,与 presets/activePreset 一样永远写顶层。
253
+ onChange: (e: ChangeEvent<HTMLInputElement>) => writeGlobal('forceSections', e.target.checked),
254
+ }),
255
+ h('span', { style: cfg.forceSections !== false ? s.badgeOk : s.badgeBlocked },
256
+ cfg.forceSections !== false ? t('forceOn') : t('forceOff')),
257
+ ]),
258
+ ]),
211
259
  ]),
212
- ]),
213
- h('div', { style: s.injectBox }, [
214
- h('div', { style: s.rowTitle }, t('forceTitle')),
215
- h('div', { style: s.muted }, t('forceHint')),
216
- h('div', { style: s.injectRow }, [
217
- h('label', { style: s.switchWrap }, [
260
+ h('div', { style: s.injectBox }, [
261
+ h('div', { style: s.rowTitle }, t('envBlockTitle')),
262
+ h('div', { style: s.muted }, t('envBlockHint')),
263
+ h('div', { style: { ...s.toolWrap, marginTop: 6 } }, envBlocklist.map((entry) => h('span', {
264
+ key: entry,
265
+ style: { ...s.toolChip, display: 'inline-flex', alignItems: 'center', gap: 4 },
266
+ }, [
267
+ entry,
268
+ h('span', { style: { cursor: 'pointer' }, title: t('delete'), onClick: () => removeBlockEntry(entry) }, '×'),
269
+ ]))),
270
+ envBlocklist.length === 0 ? h('div', { style: s.muted }, t('envBlockEmpty')) : null,
271
+ h('div', { style: { ...s.injectRow, marginTop: 6 } }, [
218
272
  h('input', {
219
- type: 'checkbox',
220
- checked: cfg.forceSections !== false,
221
- // forceSections 是全局字段,与 presets/activePreset 一样永远写顶层。
222
- onChange: (e: ChangeEvent<HTMLInputElement>) => writeGlobal('forceSections', e.target.checked),
273
+ style: { ...s.input, flex: 1 },
274
+ placeholder: t('envBlockAdd'),
275
+ value: blockInput,
276
+ onChange: (e: ChangeEvent<HTMLInputElement>) => setBlockInput(e.target.value),
223
277
  }),
224
- h('span', { style: cfg.forceSections !== false ? s.badgeOk : s.badgeBlocked },
225
- cfg.forceSections !== false ? t('forceOn') : t('forceOff')),
278
+ h('button', { style: s.mini, disabled: blockInput.trim() === '', onClick: addBlockEntry }, t('envBlockAddAction')),
279
+ ]),
280
+ h('details', { style: { marginTop: 6 } }, [
281
+ h('summary', { style: { ...s.muted, cursor: 'pointer' } }, `${t('envVarsTitle')} (${inv?.variables?.length ?? 0})`),
282
+ h('div', { style: { ...s.muted, marginTop: 4 } }, t('envVarsHint')),
283
+ h('div', { style: { ...s.toolWrap, marginTop: 4 } }, (inv?.variables ?? []).map((varName) => h('span', { key: varName, style: s.toolChip }, `{{${varName}}}`))),
226
284
  ]),
227
285
  ]),
228
- ]),
229
- h('div', { style: s.injectBox }, [
230
- h('div', { style: s.rowTitle }, t('envBlockTitle')),
231
- h('div', { style: s.muted }, t('envBlockHint')),
232
- h('div', { style: { ...s.toolWrap, marginTop: 6 } }, envBlocklist.map((entry) => h('span', {
233
- key: entry,
234
- style: { ...s.toolChip, display: 'inline-flex', alignItems: 'center', gap: 4 },
235
- }, [
236
- entry,
237
- h('span', { style: { cursor: 'pointer' }, title: t('delete'), onClick: () => removeBlockEntry(entry) }, '×'),
238
- ]))),
239
- envBlocklist.length === 0 ? h('div', { style: s.muted }, t('envBlockEmpty')) : null,
240
- h('div', { style: { ...s.injectRow, marginTop: 6 } }, [
241
- h('input', {
242
- style: { ...s.input, flex: 1 },
243
- placeholder: t('envBlockAdd'),
244
- value: blockInput,
245
- onChange: (e: ChangeEvent<HTMLInputElement>) => setBlockInput(e.target.value),
246
- }),
247
- h('button', { style: s.mini, disabled: blockInput.trim() === '', onClick: addBlockEntry }, t('envBlockAddAction')),
248
- ]),
249
- h('details', { style: { marginTop: 6 } }, [
250
- h('summary', { style: { ...s.muted, cursor: 'pointer' } }, `${t('envVarsTitle')} (${inv?.variables?.length ?? 0})`),
251
- h('div', { style: { ...s.muted, marginTop: 4 } }, t('envVarsHint')),
252
- h('div', { style: { ...s.toolWrap, marginTop: 4 } }, (inv?.variables ?? []).map((name) => h('span', { key: name, style: s.toolChip }, `{{${name}}}`))),
253
- ]),
254
- ]),
255
- h('div', { style: s.injectBox }, [
256
- h('div', { style: s.rowTitle }, t('resetTitle')),
257
- h('div', { style: s.muted }, t('resetHint')),
258
- h('div', { style: s.injectRow }, [
259
- h('button', { style: s.mini, onClick: () => {
260
- // 二次确认:清空不可撤销,必须明确点头才动手。
261
- if (window.confirm(t('resetConfirm'))) onReset()
262
- } }, t('resetAction')),
286
+ h('div', { style: s.injectBox }, [
287
+ h('div', { style: s.rowTitle }, t('resetTitle')),
288
+ h('div', { style: s.muted }, t('resetHint')),
289
+ h('div', { style: s.injectRow }, [
290
+ h('button', { style: s.mini, onClick: () => {
291
+ // 二次确认:清空不可撤销,必须明确点头才动手。
292
+ if (window.confirm(t('resetConfirm'))) onReset()
293
+ } }, t('resetAction')),
294
+ ]),
263
295
  ]),
264
296
  ]),
265
- presets.length === 0 ? h('div', { style: s.muted }, t('empty')) : null,
266
- presets.map((preset) => {
267
- const active = cfg.activePreset === preset.id
268
- return h('div', { key: preset.id, style: s.row }, [
269
- h('div', { style: s.rowBody }, [
270
- h('div', { style: s.rowTitle }, [
271
- h('span', { style: s.code }, preset.name),
272
- active ? h('span', { style: s.badgeOk }, t('active')) : null,
273
- ]),
274
- ]),
275
- h('button', { style: s.mini, onClick: () => applyPreset(preset) }, t('apply')),
276
- h('button', { style: s.mini, onClick: () => { void exportPreset(preset) } }, t('export')),
277
- h('button', { style: s.mini, onClick: () => deletePreset(preset.id) }, t('delete')),
278
- ])
279
- }),
280
297
  ])
281
298
  }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * PreviewPane — 独立面板的右栏(提示词 / 工具模式常驻)。
3
+ *
4
+ * 只读预览,没有任何切换控件:看提示词还是工具由顶部模式决定(选「提示词」
5
+ * = 提示词编辑 + 提示词预览;选「工具」= 工具编辑 + 工具预览),阶段由头部
6
+ * 的阶段按钮统一控制(与左栏联动)。数据来自 Panel 并行拉取的三阶段装配,
7
+ * 编辑草稿后由 Panel 防抖叠加同步 —— 所见即模型所见。本视图绝不修改配置。
8
+ */
9
+ import { createElement as h, type ReactElement } from 'react'
10
+ import type { PhaseViewKey, Preview } from './types.ts'
11
+ import type { Translate } from './locales.ts'
12
+ import { s } from './styles.ts'
13
+ import { PreviewTools } from './PreviewTools.tsx'
14
+
15
+ /** Panel 并行拉取的三阶段装配。 */
16
+ type PhaseViews = Record<PhaseViewKey, Preview | null>
17
+
18
+ export function PreviewPane({ t, phases, phase, sub }: {
19
+ t: Translate
20
+ phases: PhaseViews | null
21
+ /** 当前阶段(状态在 Panel 持有,头部阶段按钮统一切换)。 */
22
+ phase: PhaseViewKey
23
+ /** 提示词 / 工具子视图:跟随顶部模式(无手动切换)。 */
24
+ sub: 'prompt' | 'tools'
25
+ }): ReactElement {
26
+ const data = phases?.[phase] ?? null
27
+ // 按当前所选阶段判定「段级定制没进最终提示词」:有 complete 段整段接管就说
28
+ // 接管,否则探测本插件产出的段是否被下游丢弃(丢弃规则不止一种)。
29
+ const lossNote = data === null || data === undefined ? null
30
+ : data.takenOverBy !== undefined
31
+ ? t('sectionsTakenOver', { name: data.takenOverBy })
32
+ : data.lostSections !== undefined
33
+ ? t('sectionsLost', { emitted: data.lostSections.emitted, survived: data.lostSections.survived })
34
+ : null
35
+
36
+ return h('div', { style: s.colRight }, [
37
+ h('div', { style: s.colRightScroll }, [
38
+ // 该阶段的本插件段级产出没进最终提示词(整段接管或被下游丢弃)。
39
+ lossNote !== null ? h('div', { style: s.noticeWarn }, lossNote) : null,
40
+ // scope 挂载失败回退全局层时明确警示:这不是该预设的原生装配。
41
+ data !== null && data.scopeResolved === false
42
+ ? h('div', { style: s.noticeWarn }, t('scopeFallback'))
43
+ : null,
44
+ phases === null ? h('div', { style: s.muted }, t('loading')) : null,
45
+ phases !== null && data === null
46
+ ? h('div', { style: s.error }, t('previewFail'))
47
+ : null,
48
+ sub === 'prompt'
49
+ ? data
50
+ ? [
51
+ h('div', { style: s.rowTitle }, [
52
+ h('span', { style: s.muted }, t('previewHint')),
53
+ h('span', { style: s.orderTag }, `${data.sections.length} ${t('previewSections')}`),
54
+ ]),
55
+ h('pre', { style: s.previewText }, data.text || t('empty')),
56
+ ]
57
+ : null
58
+ : data
59
+ ? [
60
+ // 模型视角 vs 注册表视角的对照:预览按所选阶段运行全部装配规则
61
+ //(含预设原生的阶段裁剪),某些预设(如 PTC / Code Mode)会把
62
+ // 完整目录包装成单一工具,注册表原始目录仍列在工具子视图。
63
+ h('div', { style: s.rowTitle }, [
64
+ h('span', { style: s.muted }, t('previewToolsHint')),
65
+ h('span', { style: s.orderTag },
66
+ `${data.tools.length} / ${data.registryTotal ?? '?'} ${t('previewToolsCount')}`),
67
+ ]),
68
+ h(PreviewTools, { tools: data.tools, t }),
69
+ ]
70
+ : null,
71
+ ]),
72
+ ])
73
+ }