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.
@@ -1,48 +1,266 @@
1
- /** Tools tab: hide tools from the model catalog (blacklist only — check = hide). */
2
- import { createElement as h, type ReactElement } from 'react'
3
- import type { Config, Inventory, SettingsScope } from './types.ts'
1
+ /** 工具 Tab:四个部分 —— 引导期 / 常驻期 / 压缩受控期(三个阶段恒定显示,
2
+ * 预设没有某个阶段时该部分只是空的)+ 本系统全部工具(只读池)。
3
+ * - 上三段每行 = 该阶段进入过滤的目录 ∪ 用户加回的工具(预过滤视图,隐藏后
4
+ * 仍在列表可反选),勾选 = 该阶段可见 / 隐藏。三份名单互不继承。
5
+ * - 「加回」:被该阶段默认裁掉、但仍在当前预设注册表里的工具,可以拖进来重新
6
+ * 出现(写该阶段的 add 名单,装配时从注册表查回 schema 追加)。注册表里根本
7
+ * 没有的(别的预设独有)加不进来,界面明确说明。
8
+ * - 全部工具 = 跨预设累积的注册表并集(只读池),每行标出该工具在三个阶段的
9
+ * 状态;它按定义比任何单一预设都大,所以里面的工具完全可能不在当前预设注册表。
10
+ * - 拖动:全部 → 某阶段 = 让该工具在这一阶段出现;阶段 → 阶段 = 搬移(源阶段
11
+ * 隐藏 + 目标阶段显示);上三段 → 全部 = 在该阶段隐藏它。
12
+ * - 只有黑名单,没有白名单:每个动作只作用于被拖 / 被点的那一个工具,绝不
13
+ * 因为「拖进了一个工具」而把该阶段其它工具一起关掉。 */
14
+ import { createElement as h, useState, type CSSProperties, type ReactElement, type DragEvent } from 'react'
15
+ import type { Config, Inventory, PhaseViewKey, Preview } from './types.ts'
4
16
  import type { Translate } from './locales.ts'
17
+ import { PART_ORDER, isToolHidden, withPhaseAdd, withPhaseExclude } from './presets.ts'
5
18
  import { s } from './styles.ts'
6
19
 
7
- export function ToolsTab({ cfg, inv, scope, t }: { cfg: Config; inv: Inventory | null; scope: SettingsScope; t: Translate }): ReactElement {
8
- const tools = inv?.tools ?? []
9
- const exclude = cfg.tools?.exclude ?? []
20
+ /** Panel 并行拉取的三阶段装配(与 PreviewTab 同一形状)。 */
21
+ type PhaseViews = Record<PhaseViewKey, Preview | null>
10
22
 
11
- // Blacklist-only: a tool is hidden iff it is in the exclude list. Any stale
12
- // whitelist (include) is discarded on write so mode switching is gone.
13
- const hidden = (name: string): boolean => exclude.includes(name)
23
+ /** 拖拽来源:全部池,或某个阶段部分。 */
24
+ type DragSource = { kind: 'all' } | { kind: 'part'; key: PhaseViewKey }
14
25
 
15
- const toggle = (name: string, currentlyHidden: boolean): void => {
16
- const next = exclude.slice()
17
- if (currentlyHidden) { const i = next.indexOf(name); if (i >= 0) next.splice(i, 1) }
18
- else next.push(name)
19
- scope.set('tools', { exclude: next, include: [] })
26
+ /** 一次拖放的反馈(ok = 已写入,warn = 什么都没改并说明原因)。 */
27
+ type Notice = { kind: 'ok' | 'warn'; text: string }
28
+
29
+ export function ToolsTab({ cfg, inv, phases, t, write }: {
30
+ cfg: Config
31
+ inv: Inventory | null
32
+ phases: PhaseViews | null
33
+ t: Translate
34
+ write: (field: 'sections' | 'replace' | 'inject' | 'tools', value: unknown) => void
35
+ }): ReactElement {
36
+ const [dragFrom, setDragFrom] = useState<DragSource | null>(null)
37
+ const [draggedName, setDraggedName] = useState<string | null>(null)
38
+ const [dropHint, setDropHint] = useState<string | null>(null)
39
+ const [notice, setNotice] = useState<Notice | null>(null)
40
+
41
+ // 某阶段自己的黑名单 / 加回名单(三份互不继承)。
42
+ const excludeOf = (key: PhaseViewKey): string[] => {
43
+ const tools = cfg.tools ?? {}
44
+ const list = key === 'bootstrap' ? tools.bootstrap?.exclude : key === 'compaction' ? tools.compaction?.exclude : tools.exclude
45
+ return list ?? []
46
+ }
47
+ const addOf = (key: PhaseViewKey): string[] => {
48
+ const tools = cfg.tools ?? {}
49
+ const list = key === 'bootstrap' ? tools.bootstrap?.add : key === 'compaction' ? tools.compaction?.add : tools.add
50
+ return list ?? []
51
+ }
52
+ // 一次写入同时落该阶段的 exclude 与 add(在同一份 tools 上叠完再写一次,
53
+ // 分两次写会各自基于旧 cfg 计算而互相覆盖)。
54
+ const writeLists = (key: PhaseViewKey, exclude: string[], add: string[]): void => {
55
+ write('tools', withPhaseAdd(withPhaseExclude(cfg.tools ?? {}, key, exclude), key, add))
56
+ }
57
+
58
+ // 该阶段的装配目录(进入本插件过滤的工具原文)与该预设注册表(能加回的来源)。
59
+ const catalogOf = (key: PhaseViewKey): string[] => (phases?.[key]?.baseTools ?? []).map((tool) => tool.name)
60
+ const isInCatalog = (key: PhaseViewKey, name: string): boolean => catalogOf(key).includes(name)
61
+ // 注册表三个阶段共享同一 scope,取第一个非空的即可。
62
+ const registry: Set<string> = (() => {
63
+ const list = PART_ORDER.map((key) => phases?.[key]?.registryTools).find((x) => Array.isArray(x) && x.length > 0) ?? []
64
+ return new Set(list)
65
+ })()
66
+ const isInRegistry = (name: string): boolean => registry.has(name)
67
+
68
+ // 该工具在某阶段是否「在场」(在目录里且没被隐藏,或已加回)。
69
+ const presentIn = (key: PhaseViewKey, name: string): boolean =>
70
+ addOf(key).includes(name) || (isInCatalog(key, name) && !excludeOf(key).includes(name))
71
+
72
+ // 把一个工具放进某阶段(复制语义,绝不改动其它阶段):不在该阶段目录里就加进
73
+ // add 名单;`hidden` 决定它在目标阶段是可见还是隐藏(拖拽时继承源阶段的状态)。
74
+ const addToPhase = (key: PhaseViewKey, name: string, hidden: boolean): void => {
75
+ let exclude = excludeOf(key)
76
+ let add = addOf(key)
77
+ if (!isInCatalog(key, name) && !add.includes(name)) add = [...add, name]
78
+ exclude = hidden
79
+ ? (exclude.includes(name) ? exclude : [...exclude, name])
80
+ : exclude.filter((x) => x !== name)
81
+ writeLists(key, exclude, add)
82
+ }
83
+
84
+ // 阶段部分里的行 = 该阶段进入过滤的目录 ∪ 已加回(但尚未出现在装配里)的工具。
85
+ const rowsOf = (key: PhaseViewKey): Array<{ name: string; description: string; hidden: boolean; added: boolean }> => {
86
+ const filter = { exclude: excludeOf(key) }
87
+ const catalog = (phases?.[key]?.baseTools ?? []).map((tool) => ({
88
+ name: tool.name,
89
+ description: tool.description ?? '',
90
+ hidden: isToolHidden(tool.name, filter),
91
+ added: false,
92
+ }))
93
+ const names = new Set(catalog.map((row) => row.name))
94
+ const added = addOf(key)
95
+ .filter((name) => !names.has(name))
96
+ .map((name) => ({ name, description: '', hidden: false, added: true }))
97
+ return [...catalog, ...added]
98
+ }
99
+
100
+ // 勾选 / 取消勾选:只动这一个工具在本阶段的可见性。
101
+ const toggleHide = (key: PhaseViewKey, name: string, currentlyHidden: boolean): void => {
102
+ if (currentlyHidden) {
103
+ addToPhase(key, name, false)
104
+ } else {
105
+ let exclude = excludeOf(key)
106
+ let add = addOf(key)
107
+ if (add.includes(name)) add = add.filter((x) => x !== name)
108
+ else if (!exclude.includes(name)) exclude = [...exclude, name]
109
+ writeLists(key, exclude, add)
110
+ }
111
+ setNotice(null)
112
+ }
113
+
114
+ const phaseNoun = (key: PhaseViewKey): string =>
115
+ key === 'bootstrap' ? t('phaseStageGuide') : key === 'compaction' ? t('phaseStageControlled') : t('phaseStageResident')
116
+
117
+ // 拖到阶段部分 = 复制:目标阶段多一份,源阶段原样不动;隐藏状态跟着走
118
+ //(源阶段是禁用的,复制过去也是禁用,启用同理)。
119
+ const dropOnPart = (dest: PhaseViewKey, name: string, from: DragSource): void => {
120
+ if (!isInCatalog(dest, name) && !isInRegistry(name)) {
121
+ setNotice({ kind: 'warn', text: t('toolNotInRegistry', { name }) })
122
+ return
123
+ }
124
+ const copied = from.kind === 'part' && from.key !== dest
125
+ const hidden = copied ? excludeOf(from.key).includes(name) : false
126
+ addToPhase(dest, name, hidden)
127
+ setNotice({
128
+ kind: 'ok',
129
+ text: copied
130
+ ? t('toolCopied', { name, from: phaseNoun(from.key), to: phaseNoun(dest) })
131
+ : isInCatalog(dest, name)
132
+ ? t('toolShown', { name, to: phaseNoun(dest) })
133
+ : t('toolAdded', { name, phase: phaseNoun(dest) }),
134
+ })
135
+ }
136
+
137
+ // 拖回「全部」池 = 把它从来源那个阶段拿掉:加回的工具撤销加回,原生工具隐藏。
138
+ const dropOnAll = (name: string, from: DragSource): void => {
139
+ if (from.kind !== 'part') return
140
+ const key = from.key
141
+ let exclude = excludeOf(key)
142
+ let add = addOf(key)
143
+ if (add.includes(name)) {
144
+ add = add.filter((x) => x !== name)
145
+ } else if (!exclude.includes(name)) {
146
+ exclude = [...exclude, name]
147
+ }
148
+ writeLists(key, exclude, add)
149
+ setNotice({ kind: 'ok', text: t('toolHiddenIn', { name, to: phaseNoun(key) }) })
150
+ }
151
+
152
+ // 某阶段的「启用 / 总数」:本部分列出的行里有多少对模型可见。
153
+ // 模型视角的真实目录数看预览 Tab —— 装配本身没给出、又没加回的工具不会进模型。
154
+ const visibleCount = (key: PhaseViewKey): string => {
155
+ const rows = rowsOf(key)
156
+ return `${rows.filter((row) => !row.hidden).length} / ${rows.length}`
157
+ }
158
+
159
+ const partNoun = (key: PhaseViewKey): string =>
160
+ key === 'bootstrap' ? t('toolsPartGuide') : key === 'compaction' ? t('toolsPartControlled') : t('toolsPartResident')
161
+
162
+ // ── 拖拽 ──────────────────────────────────────────────────────────────────
163
+ const startDrag = (e: DragEvent, from: DragSource, name: string): void => {
164
+ setDragFrom(from)
165
+ setDraggedName(name)
166
+ e.dataTransfer.effectAllowed = 'copy'
167
+ try { e.dataTransfer.setData('text/plain', name) } catch { /* ignore */ }
168
+ }
169
+ const zoneDragOver = (e: DragEvent, zone: string): void => {
170
+ if (!dragFrom || !draggedName) return
171
+ const same = zone === 'all' ? dragFrom.kind === 'all' : (dragFrom.kind === 'part' && dragFrom.key === zone.replace('part:', ''))
172
+ if (zone === 'all' && dragFrom.kind === 'all') { e.preventDefault(); return }
173
+ if (same) return
174
+ e.preventDefault()
175
+ e.dataTransfer.dropEffect = 'copy'
176
+ setDropHint(zone)
20
177
  }
178
+ const zoneDrop = (e: DragEvent, zone: string): void => {
179
+ e.preventDefault()
180
+ if (dragFrom && draggedName) {
181
+ if (zone === 'all') dropOnAll(draggedName, dragFrom)
182
+ else {
183
+ const dest = zone.replace('part:', '') as PhaseViewKey
184
+ if (PART_ORDER.includes(dest)) dropOnPart(dest, draggedName, dragFrom)
185
+ }
186
+ }
187
+ setDragFrom(null)
188
+ setDraggedName(null)
189
+ setDropHint(null)
190
+ }
191
+ const endDrag = (): void => { setDragFrom(null); setDraggedName(null); setDropHint(null) }
192
+ const zoneStyle = (zone: string): CSSProperties | undefined =>
193
+ dropHint === zone ? { outline: '2px dashed #7aa7ff', outlineOffset: 2 } : undefined
194
+
195
+ const allTools = (inv?.tools ?? []).map((tool) => ({
196
+ name: tool.name,
197
+ description: typeof tool === 'string' ? '' : (tool.description ?? ''),
198
+ }))
21
199
 
22
- const setAll = (hideAll: boolean): void => {
23
- scope.set('tools', { exclude: hideAll ? tools.map((tool) => tool.name) : [], include: [] })
200
+ // 池行上的三阶段状态标记(引 / 常 / 压):绿 = 可见或已加回,灰 = 已隐藏,暗 = 不在。
201
+ const markStyle = (key: PhaseViewKey, name: string): CSSProperties => {
202
+ if (presentIn(key, name)) return { ...s.badgeOk, padding: '0 3px' }
203
+ if (isInCatalog(key, name)) return { ...s.badgeBlocked, padding: '0 3px' }
204
+ return { ...s.muted, opacity: 0.45, padding: '0 3px' }
205
+ }
206
+ const markKey = (key: PhaseViewKey): string =>
207
+ key === 'bootstrap' ? 'phaseShortGuide' : key === 'active' ? 'phaseShortResident' : 'phaseShortControlled'
208
+
209
+ const renderPhasePart = (key: PhaseViewKey): ReactElement => {
210
+ const rows = rowsOf(key)
211
+ const zoneKey = `part:${key}`
212
+ return h('div', {
213
+ key: zoneKey,
214
+ style: s.injectBox,
215
+ onDragOver: (e: DragEvent) => zoneDragOver(e, zoneKey),
216
+ onDrop: (e: DragEvent) => zoneDrop(e, zoneKey),
217
+ ...(zoneStyle(zoneKey) ?? {}),
218
+ }, [
219
+ h('div', { style: s.rowTitle }, [
220
+ h('span', null, partNoun(key)),
221
+ h('span', { style: s.orderTag }, `${t('phaseVisible')} ${visibleCount(key)}`),
222
+ rows.length === 0 ? h('span', { style: s.muted }, t('dragHint')) : null,
223
+ ]),
224
+ h('div', { style: s.toolWrap }, rows.map((row) => h('span', {
225
+ key: row.name,
226
+ style: { ...s.toolChip, cursor: 'move', display: 'inline-flex', alignItems: 'center', gap: 4 },
227
+ title: row.description.slice(0, 120),
228
+ draggable: true,
229
+ onDragStart: (e: DragEvent) => startDrag(e, { kind: 'part', key }, row.name),
230
+ onDragEnd: endDrag,
231
+ }, [
232
+ h('input', {
233
+ type: 'checkbox',
234
+ checked: !row.hidden,
235
+ onChange: () => toggleHide(key, row.name, row.hidden),
236
+ style: { margin: 0, cursor: 'pointer' },
237
+ }),
238
+ h('span', { style: row.hidden ? s.badgeBlocked : s.badgeOk }, row.name),
239
+ ]))),
240
+ ])
24
241
  }
25
242
 
26
243
  return h('div', {}, [
27
- h('div', { style: s.bar }, [
28
- h('button', { style: s.mini, onClick: () => setAll(true) }, t('selectAll')),
29
- h('button', { style: s.mini, onClick: () => setAll(false) }, t('selectNone')),
30
- ]),
31
- h('div', { style: s.list }, [
32
- tools.length === 0 ? h('div', { style: s.muted }, t('empty')) : null,
33
- tools.map((tool) => {
34
- const isHidden = hidden(tool.name)
35
- return h('div', { key: tool.name, style: s.row }, [
36
- h('label', { style: s.switchWrap }, [
37
- h('input', { type: 'checkbox', checked: isHidden, onChange: () => toggle(tool.name, isHidden) }),
38
- h('span', { style: isHidden ? s.badgeBlocked : s.badgeOk }, isHidden ? t('hiddenOn') : t('hiddenOff')),
39
- ]),
40
- h('div', { style: s.rowBody }, [
41
- h('div', { style: s.rowTitle }, h('span', { style: s.code }, tool.name)),
42
- h('div', { style: s.preview }, String(tool.description ?? '').slice(0, 140)),
43
- ]),
44
- ])
45
- }),
244
+ h('div', { style: s.muted }, t('toolsFourHint')),
245
+ notice ? h('div', { style: notice.kind === 'ok' ? s.noticeOk : s.noticeWarn }, notice.text) : null,
246
+ PART_ORDER.map(renderPhasePart),
247
+ // 全部工具:本系统注册表的完整目录,只读池(不可勾选,供拖入三个阶段)。
248
+ h('div', {
249
+ style: s.injectBox,
250
+ onDragOver: (e: DragEvent) => zoneDragOver(e, 'all'),
251
+ onDrop: (e: DragEvent) => zoneDrop(e, 'all'),
252
+ ...(zoneStyle('all') ?? {}),
253
+ }, [
254
+ h('div', { style: s.rowTitle }, [t('allToolsTitle'), h('span', { style: s.orderTag }, `${allTools.length}`)]),
255
+ allTools.length === 0 ? h('div', { style: s.muted }, t('empty')) : null,
256
+ h('div', { style: s.toolWrap }, allTools.map((tool) => h('span', {
257
+ key: tool.name,
258
+ style: { ...s.toolChip, cursor: 'copy' },
259
+ title: tool.description.slice(0, 120),
260
+ draggable: true,
261
+ onDragStart: (e: DragEvent) => startDrag(e, { kind: 'all' }, tool.name),
262
+ onDragEnd: endDrag,
263
+ }, tool.name))),
46
264
  ]),
47
265
  ])
48
266
  }