dsh-prompt-customizer 0.2.7 → 0.3.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.
- package/README.en.md +171 -72
- package/README.md +170 -72
- package/client/client.js +1753 -410
- package/client/client.js.map +1 -1
- package/lib/catalog.js +131 -0
- package/lib/effective.js +109 -0
- package/lib/index.js +712 -184
- package/lib/promotion.js +80 -0
- package/lib/schema.js +78 -33
- package/lib/sectionOps.d.mts +32 -0
- package/lib/sectionOps.mjs +134 -0
- package/lib/store.js +124 -0
- package/package.json +7 -5
- package/src/client/PresetsTab.tsx +141 -48
- package/src/client/PreviewTab.tsx +67 -41
- package/src/client/PreviewTools.tsx +6 -4
- package/src/client/SectionsTab.tsx +508 -156
- package/src/client/ToolsTab.tsx +253 -35
- package/src/client/index.ts +331 -46
- package/src/client/locales.ts +135 -7
- package/src/client/preset-io.ts +225 -0
- package/src/client/presets.ts +256 -116
- package/src/client/styles.ts +9 -1
- package/src/client/types.ts +89 -44
- package/lib/client.js +0 -344
|
@@ -1,78 +1,167 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/** 配置 Tab:两种「预设」分开 ——
|
|
2
|
+
* - 保存当前配置:本插件内的配置快照库(config.presets),可应用 / 导出 / 导入,
|
|
3
|
+
* 不会变成可选的 agent 预设;
|
|
4
|
+
* - 存为 agent 预设:整体 fork 当前编辑目标的预设目录,并把当前定制写进它的
|
|
5
|
+
* 覆盖项,新预设随即出现在顶部的编辑目标选择器里。
|
|
6
|
+
* 导入导出经 preset-io 双端适配:Tauri 2 桌面走原生对话框,Web 走
|
|
7
|
+
* 下载 / 文件选择;成功 / 失败 / 取消都有面板内消息条提示。 */
|
|
8
|
+
import { createElement as h, useEffect, useRef, useState, type ReactElement, type ChangeEvent } from 'react'
|
|
9
|
+
import type { Config, Inventory, PhaseViewKey, Preview, Preset } from './types.ts'
|
|
4
10
|
import type { Translate } from './locales.ts'
|
|
5
|
-
import { addImportedPresets, applyPresetData, buildPresetData, mergeSections, removePreset,
|
|
11
|
+
import { addImportedPresets, applyPresetData, buildPresetData, genId, mergeSections, PART_ORDER, removePreset, type ConfigPatch } from './presets.ts'
|
|
12
|
+
import { decodePresetExport, exportPresetFile, importPresetFile } from './preset-io.ts'
|
|
6
13
|
import { s } from './styles.ts'
|
|
7
14
|
|
|
8
|
-
|
|
15
|
+
/** 一次性操作提示:kind 决定配色,几秒后自动消失。 */
|
|
16
|
+
interface Notice {
|
|
17
|
+
kind: 'ok' | 'error'
|
|
18
|
+
text: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function PresetsTab({ cfg, inv, phases, t, writePatch, writeGlobal, saveAsPreset, forkSource }: {
|
|
22
|
+
cfg: Config
|
|
23
|
+
inv: Inventory | null
|
|
24
|
+
/** Panel 并行拉取的三阶段装配:快照只捕获当前装配里真实存在的段。 */
|
|
25
|
+
phases: Record<PhaseViewKey, Preview | null> | null
|
|
26
|
+
t: Translate
|
|
27
|
+
/** 应用预设:完整补丁一次落盘(显式意图,不经草稿)。 */
|
|
28
|
+
writePatch: (patch: ConfigPatch) => void
|
|
29
|
+
/** 预设库专用:永远写在全局字段(presets / activePreset 不分作用域)。 */
|
|
30
|
+
writeGlobal: (field: string, value: unknown) => void
|
|
31
|
+
/** 存为 agent 预设:fork 当前编辑目标的预设目录并写入 overrides[新名]。 */
|
|
32
|
+
saveAsPreset: (name: string) => Promise<boolean>
|
|
33
|
+
/** fork 来源的显示名(当前编辑目标预设);undefined = 全局目标,由服务端回落默认预设。 */
|
|
34
|
+
forkSource: string | undefined
|
|
35
|
+
}): ReactElement {
|
|
9
36
|
const presets = cfg.presets ?? []
|
|
10
37
|
const [name, setName] = useState('')
|
|
38
|
+
// 「存为 agent 预设」的名字与在途标记(与服务端往返期间禁用按钮)。
|
|
39
|
+
const [agentName, setAgentName] = useState('')
|
|
40
|
+
const [creating, setCreating] = useState(false)
|
|
41
|
+
const [notice, setNotice] = useState<Notice | null>(null)
|
|
42
|
+
// Web 回退用的隐藏文件输入(tauri 不可用时才点它)。
|
|
11
43
|
const fileRef = useRef<HTMLInputElement | null>(null)
|
|
44
|
+
// 消息条自动消失的定时器;新消息会重置旧定时器。
|
|
45
|
+
const noticeTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
46
|
+
|
|
47
|
+
useEffect(() => () => {
|
|
48
|
+
if (noticeTimer.current !== null) clearTimeout(noticeTimer.current)
|
|
49
|
+
}, [])
|
|
50
|
+
|
|
51
|
+
// 显示提示;ok 与 error 分别配色,4 秒后自动消失。
|
|
52
|
+
const show = (kind: Notice['kind'], text: string): void => {
|
|
53
|
+
if (noticeTimer.current !== null) clearTimeout(noticeTimer.current)
|
|
54
|
+
setNotice({ kind, text })
|
|
55
|
+
noticeTimer.current = setTimeout(() => setNotice(null), 4000)
|
|
56
|
+
}
|
|
12
57
|
|
|
13
58
|
const blockedNames = new Set(cfg.sections ?? [])
|
|
14
|
-
|
|
59
|
+
// 快照的捕获宇宙 = 当前预设三个阶段装配里真实存在的段名。「本系统全部提示词」
|
|
60
|
+
// 是跨预设只增不减的并集池,直接拿它当捕获集合会把别的预设独有段名灌进快照,
|
|
61
|
+
// 应用回来就是一条条空白幽灵段。装配数据未就绪(宇宙为空)时不过滤,避免捕获成空。
|
|
62
|
+
const assemblyNames = new Set<string>()
|
|
63
|
+
for (const key of PART_ORDER) {
|
|
64
|
+
for (const sec of phases?.[key]?.baseSections ?? []) assemblyNames.add(sec.name)
|
|
65
|
+
}
|
|
66
|
+
const mergedAll = mergeSections(inv, cfg, blockedNames)
|
|
67
|
+
const merged = assemblyNames.size === 0
|
|
68
|
+
? mergedAll
|
|
69
|
+
: mergedAll.filter((sec) => sec.source === 'custom' || assemblyNames.has(sec.name))
|
|
15
70
|
const currentNames = new Set(merged.map((sec) => sec.name))
|
|
16
71
|
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// section it should follow) so the preset stays portable across prompts.
|
|
72
|
+
// 把当前定制捕获为新预设(完整快照)。绝对顺序会被转成相对形式
|
|
73
|
+
// (每段记录它应跟随的前一段),因此预设可以跨不同提示词移植。
|
|
20
74
|
const saveCurrent = (): void => {
|
|
21
75
|
const presetName = name.trim() || `${t('preset')} ${presets.length + 1}`
|
|
22
76
|
const data = buildPresetData(cfg, merged)
|
|
23
|
-
|
|
77
|
+
writeGlobal('presets', [...presets, { id: genId(), name: presetName, data }])
|
|
24
78
|
setName('')
|
|
25
79
|
}
|
|
26
80
|
|
|
27
|
-
//
|
|
28
|
-
// -
|
|
29
|
-
// -
|
|
30
|
-
// -
|
|
31
|
-
// -
|
|
32
|
-
//
|
|
81
|
+
// 应用预设。补丁由纯函数助手计算:
|
|
82
|
+
// - 同名段被覆盖(内容 + 顺序)
|
|
83
|
+
// - 预设中有、当前提示词中没有的段被添加
|
|
84
|
+
// - 当前有、但不在预设列表中的段被屏蔽
|
|
85
|
+
// - 只有预设的「激活段」被解除屏蔽(预设自己的屏蔽名单被保留)
|
|
86
|
+
// 四个定制字段以一个补丁写入当前编辑目标(全局顶层或 overrides[id]);
|
|
87
|
+
// 快照库(presets / activePreset)永远保持在全局。
|
|
33
88
|
const applyPreset = (preset: Preset): void => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
scope.set('replace', patch.replace)
|
|
37
|
-
scope.set('sections', patch.sections)
|
|
38
|
-
scope.set('tools', patch.tools)
|
|
39
|
-
scope.set('activePreset', preset.id)
|
|
89
|
+
writePatch(applyPresetData(preset.data, cfg, currentNames))
|
|
90
|
+
writeGlobal('activePreset', preset.id)
|
|
40
91
|
}
|
|
41
92
|
|
|
42
93
|
const deletePreset = (id: string): void => {
|
|
43
94
|
const next = removePreset(presets, id, cfg.activePreset)
|
|
44
|
-
|
|
45
|
-
if (next.activeId === undefined && cfg.activePreset === id)
|
|
95
|
+
writeGlobal('presets', next.presets)
|
|
96
|
+
if (next.activeId === undefined && cfg.activePreset === id) writeGlobal('activePreset', undefined)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 导出一个预设:Tauri 桌面走原生保存对话框,Web(或 tauri 插件不可用
|
|
100
|
+
// 时)回退为下载。saved/downloaded = 成功;用户取消单独提示,不算失败。
|
|
101
|
+
const exportPreset = async (preset: Preset): Promise<void> => {
|
|
102
|
+
try {
|
|
103
|
+
const res = await exportPresetFile(preset)
|
|
104
|
+
if (res.ok) show('ok', t('exportOk'))
|
|
105
|
+
else if (res.cancelled) show('ok', t('exportCancel'))
|
|
106
|
+
else show('error', `${t('exportFail')}${res.message ? ': ' + res.message : ''}`)
|
|
107
|
+
} catch (e) {
|
|
108
|
+
show('error', `${t('exportFail')}: ${e instanceof Error ? e.message : String(e)}`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 解析导入文本 → 去重追加 → 提示新增数量。解析失败提示错误。
|
|
113
|
+
const importParsed = (text: string): void => {
|
|
114
|
+
try {
|
|
115
|
+
const parsed = decodePresetExport(text)
|
|
116
|
+
const next = addImportedPresets(presets, parsed, genId)
|
|
117
|
+
const added = next.length - presets.length
|
|
118
|
+
if (added <= 0) show('ok', t('importNone'))
|
|
119
|
+
else {
|
|
120
|
+
writeGlobal('presets', next)
|
|
121
|
+
show('ok', `${t('importOk')} (+${added})`)
|
|
122
|
+
}
|
|
123
|
+
} catch (e) {
|
|
124
|
+
show('error', `${t('importFail')}: ${e instanceof Error ? e.message : String(e)}`)
|
|
125
|
+
}
|
|
46
126
|
}
|
|
47
127
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
128
|
+
// 导入预设:优先 Tauri 原生打开对话框;unavailable(Web 或桌面端缺
|
|
129
|
+
// dialog/fs 插件)时点隐藏 file input 回退到文件选择器。取消单独提示。
|
|
130
|
+
const importPreset = async (): Promise<void> => {
|
|
131
|
+
try {
|
|
132
|
+
const res = await importPresetFile()
|
|
133
|
+
if (res.kind === 'text') { importParsed(res.text); return }
|
|
134
|
+
if (res.kind === 'cancelled') { show('ok', t('importCancel')); return }
|
|
135
|
+
fileRef.current?.click()
|
|
136
|
+
} catch (e) {
|
|
137
|
+
show('error', `${t('importFail')}: ${e instanceof Error ? e.message : String(e)}`)
|
|
138
|
+
}
|
|
57
139
|
}
|
|
58
140
|
|
|
59
|
-
//
|
|
60
|
-
// presets with new names are added to the local list.
|
|
141
|
+
// Web 回退路径:文件选择器的 onChange。用户取消(无文件)静默返回。
|
|
61
142
|
const onImportFile = (e: ChangeEvent<HTMLInputElement>): void => {
|
|
62
143
|
const file = e.target.files?.[0]
|
|
144
|
+
e.target.value = ''
|
|
63
145
|
if (!file) return
|
|
64
146
|
const reader = new FileReader()
|
|
65
|
-
reader.onload = () =>
|
|
66
|
-
|
|
67
|
-
const parsed = JSON.parse(String(reader.result))
|
|
68
|
-
scope.set('presets', addImportedPresets(presets, parsed, genId))
|
|
69
|
-
} catch { /* ignore invalid json */ }
|
|
70
|
-
}
|
|
147
|
+
reader.onload = () => importParsed(String(reader.result))
|
|
148
|
+
reader.onerror = () => show('error', t('importFail'))
|
|
71
149
|
reader.readAsText(file)
|
|
72
|
-
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 存为 agent 预设:fork 当前编辑目标的预设目录 + 把当前定制写进它的覆盖项。
|
|
153
|
+
// 成败由服务端返回,提示统一走面板顶部的消息条(Panel 的 flash),这里只在
|
|
154
|
+
// 成功时清空输入框,避免同一条消息显示两遍。
|
|
155
|
+
const createAgentPreset = async (): Promise<void> => {
|
|
156
|
+
const trimmed = agentName.trim()
|
|
157
|
+
if (trimmed.length === 0 || creating) return
|
|
158
|
+
setCreating(true)
|
|
159
|
+
if (await saveAsPreset(trimmed)) setAgentName('')
|
|
160
|
+
setCreating(false)
|
|
73
161
|
}
|
|
74
162
|
|
|
75
163
|
return h('div', { style: s.list }, [
|
|
164
|
+
notice ? h('div', { style: notice.kind === 'ok' ? s.noticeOk : s.error }, notice.text) : null,
|
|
76
165
|
h('div', { style: s.injectBox }, [
|
|
77
166
|
h('div', { style: s.rowTitle }, t('savePreset')),
|
|
78
167
|
h('div', { style: s.injectRow }, [
|
|
@@ -80,10 +169,18 @@ export function PresetsTab({ cfg, inv, scope, t }: { cfg: Config; inv: Inventory
|
|
|
80
169
|
h('button', { style: s.mini, onClick: saveCurrent }, t('save')),
|
|
81
170
|
]),
|
|
82
171
|
]),
|
|
172
|
+
h('div', { style: s.injectBox }, [
|
|
173
|
+
h('div', { style: s.rowTitle }, t('saveAsPresetCard')),
|
|
174
|
+
h('div', { style: s.muted }, t('saveAsPresetHint', { name: forkSource ?? t('forkSourceDefault') })),
|
|
175
|
+
h('div', { style: s.injectRow }, [
|
|
176
|
+
h('input', { style: { ...s.input, flex: 1 }, placeholder: t('agentPresetName'), value: agentName, onChange: (e: ChangeEvent<HTMLInputElement>) => setAgentName(e.target.value) }),
|
|
177
|
+
h('button', { style: s.mini, disabled: creating || agentName.trim().length === 0, onClick: () => { void createAgentPreset() } }, t('saveAsPreset')),
|
|
178
|
+
]),
|
|
179
|
+
]),
|
|
83
180
|
h('div', { style: s.injectBox }, [
|
|
84
181
|
h('div', { style: s.rowTitle }, t('importPreset')),
|
|
85
182
|
h('div', { style: s.injectRow }, [
|
|
86
|
-
h('button', { style: s.mini, onClick: () =>
|
|
183
|
+
h('button', { style: s.mini, onClick: () => { void importPreset() } }, t('import')),
|
|
87
184
|
h('input', { ref: fileRef, type: 'file', accept: '.json,application/json', style: { display: 'none' }, onChange: onImportFile }),
|
|
88
185
|
]),
|
|
89
186
|
]),
|
|
@@ -98,13 +195,9 @@ export function PresetsTab({ cfg, inv, scope, t }: { cfg: Config; inv: Inventory
|
|
|
98
195
|
]),
|
|
99
196
|
]),
|
|
100
197
|
h('button', { style: s.mini, onClick: () => applyPreset(preset) }, t('apply')),
|
|
101
|
-
h('button', { style: s.mini, onClick: () => exportPreset(preset) }, t('export')),
|
|
198
|
+
h('button', { style: s.mini, onClick: () => { void exportPreset(preset) } }, t('export')),
|
|
102
199
|
h('button', { style: s.mini, onClick: () => deletePreset(preset.id) }, t('delete')),
|
|
103
200
|
])
|
|
104
201
|
}),
|
|
105
202
|
])
|
|
106
203
|
}
|
|
107
|
-
|
|
108
|
-
function genId(): string {
|
|
109
|
-
return 'p_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7)
|
|
110
|
-
}
|
|
@@ -1,54 +1,70 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/** 预览 Tab:两个只读子视图 —— 所有插件过滤后的最终装配系统提示词,
|
|
2
|
+
* 以及当前定制(黑名单过滤之后)将注入的工具目录视图。
|
|
3
|
+
* 两个视图都不修改配置。预览目标跟随面板顶部的编辑目标(`target`);
|
|
4
|
+
* 数据直接来自 Panel 并行拉取的三阶段装配(`phases`)—— 与提示词/
|
|
5
|
+
* 工具 Tab 同源(同一份 base + post 视图),杜绝三个视图数据不一致。
|
|
6
|
+
* 阶段按钮恒为引导期 / 常驻期 / 压缩受控期三个(与另两个 Tab 同源同形)。 */
|
|
7
|
+
import { createElement as h, useState, type ReactElement } from 'react'
|
|
8
|
+
import type { PhaseViewKey, Preview } from './types.ts'
|
|
6
9
|
import type { Translate } from './locales.ts'
|
|
10
|
+
import { PART_ORDER } from './presets.ts'
|
|
7
11
|
import { s } from './styles.ts'
|
|
8
12
|
import { PreviewTools } from './PreviewTools.tsx'
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
export function PreviewTab({ t, active, refreshId, target, version, phases }: {
|
|
15
|
+
t: Translate
|
|
16
|
+
active: boolean
|
|
17
|
+
refreshId: number
|
|
18
|
+
/** 顶部编辑目标:undefined = 全局(standard scope);字符串 = agent 预设 id。 */
|
|
19
|
+
target: string | undefined
|
|
20
|
+
/** 配置写入计数:Panel 每次成功写配置后 +1(驱动统一重载)。
|
|
21
|
+
* ponytail: refreshId/version 由 Panel 的统一重载消费,本组件内未直接使用。 */
|
|
22
|
+
version: number
|
|
23
|
+
/** Panel 并行拉取的三阶段装配(模型视角统一数据源)。 */
|
|
24
|
+
phases: Record<PhaseViewKey, Preview | null> | null
|
|
25
|
+
}): ReactElement {
|
|
13
26
|
const [sub, setSub] = useState<'prompt' | 'tools'>('prompt')
|
|
14
|
-
const [
|
|
15
|
-
|
|
27
|
+
const [phase, setPhase] = useState<PhaseViewKey>('bootstrap')
|
|
28
|
+
|
|
29
|
+
const data = phases?.[phase] ?? null
|
|
30
|
+
// 按当前所选阶段判定「段级定制没进最终提示词」:有 complete 段整段接管就说
|
|
31
|
+
// 接管,否则探测本插件产出的段是否被下游丢弃(丢弃规则不止一种)。
|
|
32
|
+
const lossNote = data === null || data === undefined ? null
|
|
33
|
+
: data.takenOverBy !== undefined
|
|
34
|
+
? t('sectionsTakenOver', { name: data.takenOverBy })
|
|
35
|
+
: data.lostSections !== undefined
|
|
36
|
+
? t('sectionsLost', { emitted: data.lostSections.emitted, survived: data.lostSections.survived })
|
|
37
|
+
: null
|
|
16
38
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// never dropped: it schedules one trailing reload, so the preview converges
|
|
21
|
-
// to the latest config even when the in-flight response was served before the
|
|
22
|
-
// write committed on the host.
|
|
23
|
-
const inFlight = useRef(false)
|
|
24
|
-
const pending = useRef(false)
|
|
25
|
-
const load = (): void => {
|
|
26
|
-
if (inFlight.current) { pending.current = true; return }
|
|
27
|
-
inFlight.current = true
|
|
28
|
-
setData(null)
|
|
29
|
-
fetch(PREVIEW_URL, { cache: 'no-store' })
|
|
30
|
-
.then((r) => r.json())
|
|
31
|
-
.then((body: Preview) => {
|
|
32
|
-
if (body && body.ok === false) throw new Error((body as { error?: string }).error ?? 'preview failed')
|
|
33
|
-
setData(body)
|
|
34
|
-
setError(null)
|
|
35
|
-
})
|
|
36
|
-
.catch((e: unknown) => setError(String(e instanceof Error ? e.message : e)))
|
|
37
|
-
.finally(() => {
|
|
38
|
-
inFlight.current = false
|
|
39
|
-
if (pending.current) { pending.current = false; load() }
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
useEffect(load, [])
|
|
43
|
-
useEffect(() => scope.subscribe(load), [scope])
|
|
39
|
+
// 阶段按钮命名:引导期 / 常驻期 / 压缩受控期。
|
|
40
|
+
const stageLabel = (p: PhaseViewKey): string =>
|
|
41
|
+
p === 'bootstrap' ? t('phaseStageGuide') : p === 'compaction' ? t('phaseStageControlled') : t('phaseStageResident')
|
|
44
42
|
|
|
45
43
|
return h('div', { style: s.list }, [
|
|
46
44
|
h('div', { style: s.bar }, [
|
|
47
45
|
h('button', { style: sub === 'prompt' ? s.tabActive : s.tab, onClick: () => setSub('prompt') }, t('previewPrompt')),
|
|
48
46
|
h('button', { style: sub === 'tools' ? s.tabActive : s.tab, onClick: () => setSub('tools') }, t('previewTools')),
|
|
47
|
+
h('div', { style: { marginLeft: 'auto', display: 'flex', gap: 4 } },
|
|
48
|
+
PART_ORDER.map((key) => h('button', {
|
|
49
|
+
key,
|
|
50
|
+
style: phase === key ? s.tabActive : s.tab,
|
|
51
|
+
onClick: () => setPhase(key),
|
|
52
|
+
}, stageLabel(key)))),
|
|
49
53
|
]),
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
// 该阶段的本插件段级产出没进最终提示词(整段接管或被下游丢弃)。
|
|
55
|
+
lossNote !== null ? h('div', { style: s.noticeWarn }, lossNote) : null,
|
|
56
|
+
// scope 挂载失败回退全局层时明确警示:这不是该预设的原生装配。
|
|
57
|
+
data !== null && data.scopeResolved === false
|
|
58
|
+
? h('div', { style: s.noticeWarn }, t('scopeFallback'))
|
|
59
|
+
: null,
|
|
60
|
+
// 伪会话触发预设插件异常时本次预览降级为无会话装配(原生阶段规则未运行)。
|
|
61
|
+
data !== null && data.degraded === true
|
|
62
|
+
? h('div', { style: s.noticeWarn }, t('previewDegraded'))
|
|
63
|
+
: null,
|
|
64
|
+
phases === null ? h('div', { style: s.muted }, t('loading')) : null,
|
|
65
|
+
phases !== null && data === null
|
|
66
|
+
? h('div', { style: s.error }, t('previewFail'))
|
|
67
|
+
: null,
|
|
52
68
|
sub === 'prompt'
|
|
53
69
|
? data
|
|
54
70
|
? [
|
|
@@ -60,7 +76,17 @@ export function PreviewTab({ scope, t }: { scope: SettingsScope; t: Translate })
|
|
|
60
76
|
]
|
|
61
77
|
: null
|
|
62
78
|
: data
|
|
63
|
-
?
|
|
79
|
+
? [
|
|
80
|
+
// 模型视角 vs 注册表视角的对照:预览按所选阶段运行全部装配规则
|
|
81
|
+
// (含预设原生的阶段裁剪),某些预设(如 PTC / Code Mode)会把
|
|
82
|
+
// 完整目录包装成单一工具,注册表原始目录仍列在工具 Tab。
|
|
83
|
+
h('div', { style: s.rowTitle }, [
|
|
84
|
+
h('span', { style: s.muted }, t('previewToolsHint')),
|
|
85
|
+
h('span', { style: s.orderTag },
|
|
86
|
+
`${data.tools.length} / ${data.registryTotal ?? '?'} ${t('previewToolsCount')}`),
|
|
87
|
+
]),
|
|
88
|
+
h(PreviewTools, { tools: data.tools, t }),
|
|
89
|
+
]
|
|
64
90
|
: null,
|
|
65
91
|
])
|
|
66
|
-
}
|
|
92
|
+
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* This view never edits the blacklist/whitelist config. */
|
|
1
|
+
/** 工具预览:当前定制(黑名单过滤之后)最终对模型可见的工具目录,
|
|
2
|
+
* 只读、清单式呈现:列出即代表模型看得见 —— 这里没有「已屏蔽」状态。
|
|
3
|
+
* 本视图绝不修改黑名单配置。 */
|
|
5
4
|
import { createElement as h, type ReactElement } from 'react'
|
|
6
5
|
import type { Translate } from './locales.ts'
|
|
7
6
|
import { s } from './styles.ts'
|
|
8
7
|
|
|
8
|
+
/** 预览工具条目:对象形态或纯字符串工具名(统一归一化为对象)。 */
|
|
9
9
|
export type PreviewTool = { name: string; description: string } | string
|
|
10
10
|
|
|
11
|
+
/** 把字符串形态的条目补全为 { name, description: '' }。 */
|
|
12
|
+
|
|
11
13
|
function norm(tool: PreviewTool): { name: string; description: string } {
|
|
12
14
|
return typeof tool === 'string' ? { name: tool, description: '' } : tool
|
|
13
15
|
}
|