dsh-prompt-customizer 0.2.6 → 0.3.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.
- package/LICENSE +21 -21
- package/README.en.md +13 -32
- package/README.i18n.yaml +2 -2
- package/README.md +45 -38
- package/client/client.js +41 -16
- package/client/client.js.map +1 -1
- package/cordis.patch.yml +8 -8
- package/lib/index.js +270 -254
- package/lib/schema.js +51 -51
- package/package.json +61 -61
- package/src/client/PresetsTab.tsx +110 -110
- package/src/client/PreviewTab.tsx +61 -59
- package/src/client/PreviewTools.tsx +31 -31
- package/src/client/SectionsTab.tsx +202 -201
- package/src/client/ToolsTab.tsx +48 -48
- package/src/client/index.ts +116 -115
- package/src/client/locales.ts +101 -97
- package/src/client/presets.ts +268 -268
- package/src/client/styles.ts +41 -41
- package/src/client/types.ts +50 -50
- package/lib/client.js +0 -344
|
@@ -1,201 +1,202 @@
|
|
|
1
|
-
/** Prompt-sections tab: block / replace / inject / reorder prompt sections. */
|
|
2
|
-
import { createElement as h, useState, type ReactElement, type DragEvent, type ChangeEvent } from 'react'
|
|
3
|
-
import type { Config, Inventory, SettingsScope } from './types.ts'
|
|
4
|
-
import type { Translate } from './locales.ts'
|
|
5
|
-
import { mergeSections, removeSection, type Section } from './presets.ts'
|
|
6
|
-
import { s } from './styles.ts'
|
|
7
|
-
|
|
8
|
-
export function SectionsTab({ cfg, inv, scope, t }: { cfg: Config; inv: Inventory | null; scope: SettingsScope; t: Translate }): ReactElement {
|
|
9
|
-
const replace = cfg.replace ?? {}
|
|
10
|
-
const blockedNames = new Set(cfg.sections ?? [])
|
|
11
|
-
const [editing, setEditing] = useState<string | null>(null)
|
|
12
|
-
const [draft, setDraft] = useState('')
|
|
13
|
-
const [dragName, setDragName] = useState<string | null>(null)
|
|
14
|
-
const [dropTarget, setDropTarget] = useState<{ name: string; pos: 'above' | 'below' } | null>(null)
|
|
15
|
-
|
|
16
|
-
// Merge inventory sections with user-injected sections, sorted by order.
|
|
17
|
-
// Injected sections that don't exist in the original prompt appear here too,
|
|
18
|
-
// positioned by their order value.
|
|
19
|
-
const merged: Section[] = mergeSections(inv, cfg, blockedNames)
|
|
20
|
-
|
|
21
|
-
const toggleBlock = (name: string, currentlyBlocked: boolean): void => {
|
|
22
|
-
const list = (cfg.sections ?? []).slice()
|
|
23
|
-
if (currentlyBlocked) { const i = list.indexOf(name); if (i >= 0) list.splice(i, 1) }
|
|
24
|
-
else list.push(name)
|
|
25
|
-
scope.set('sections', list)
|
|
26
|
-
}
|
|
27
|
-
const startReplace = (name: string, original: string): void => {
|
|
28
|
-
// Echo the ORIGINAL prompt text so the user edits from the current value.
|
|
29
|
-
setEditing(name)
|
|
30
|
-
setDraft(replace[name] ?? original)
|
|
31
|
-
}
|
|
32
|
-
const commitReplace = (name: string): void => {
|
|
33
|
-
const next = { ...replace }
|
|
34
|
-
if (draft.trim() === '') delete next[name]
|
|
35
|
-
else next[name] = draft
|
|
36
|
-
scope.set('replace', next)
|
|
37
|
-
setEditing(null)
|
|
38
|
-
}
|
|
39
|
-
const
|
|
40
|
-
const next = { ...replace }
|
|
41
|
-
delete next[name]
|
|
42
|
-
scope.set('replace', next)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Persist the full ordered list
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
scope.set('
|
|
65
|
-
scope.set('
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
next
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
e.dataTransfer.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
e.
|
|
87
|
-
e.
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
next
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
merged.
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
...s.
|
|
132
|
-
...(
|
|
133
|
-
...(isDropTarget && dropTarget.pos === '
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
h('
|
|
146
|
-
|
|
147
|
-
h('
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
h('span', { style: s.
|
|
153
|
-
h('span', { style: s.
|
|
154
|
-
h('span', { style:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
h('
|
|
160
|
-
|
|
161
|
-
h('button', { style: s.mini, onClick: () =>
|
|
162
|
-
h('button', { style: s.mini, onClick: () =>
|
|
163
|
-
]),
|
|
164
|
-
])
|
|
165
|
-
: h('div', { style: s.preview }, String(sec.text ?? '').slice(0, 140) || (sec.source === 'custom' ? t('empty') : t('dynamic'))),
|
|
166
|
-
]),
|
|
167
|
-
h('div', { style: s.arrowCol }, [
|
|
168
|
-
h('button', { style: s.arrow, disabled: index === 0, onClick: () => moveOrder(index, -1), title: t('moveUp') }, '↑'),
|
|
169
|
-
h('button', { style: s.arrow, disabled: index === merged.length - 1, onClick: () => moveOrder(index, 1), title: t('moveDown') }, '↓'),
|
|
170
|
-
]),
|
|
171
|
-
h('button', { style: s.mini, onClick: () =>
|
|
172
|
-
sec.source
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
h(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const [
|
|
185
|
-
const [
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
h('input', { style: { ...s.input, width: '
|
|
197
|
-
h('input', { style: { ...s.input,
|
|
198
|
-
h('
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
1
|
+
/** Prompt-sections tab: block / replace / inject / reorder prompt sections. */
|
|
2
|
+
import { createElement as h, useState, type ReactElement, type DragEvent, type ChangeEvent } from 'react'
|
|
3
|
+
import type { Config, Inventory, SettingsScope } from './types.ts'
|
|
4
|
+
import type { Translate } from './locales.ts'
|
|
5
|
+
import { mergeSections, removeSection, type Section } from './presets.ts'
|
|
6
|
+
import { s } from './styles.ts'
|
|
7
|
+
|
|
8
|
+
export function SectionsTab({ cfg, inv, scope, t }: { cfg: Config; inv: Inventory | null; scope: SettingsScope; t: Translate }): ReactElement {
|
|
9
|
+
const replace = cfg.replace ?? {}
|
|
10
|
+
const blockedNames = new Set(cfg.sections ?? [])
|
|
11
|
+
const [editing, setEditing] = useState<string | null>(null)
|
|
12
|
+
const [draft, setDraft] = useState('')
|
|
13
|
+
const [dragName, setDragName] = useState<string | null>(null)
|
|
14
|
+
const [dropTarget, setDropTarget] = useState<{ name: string; pos: 'above' | 'below' } | null>(null)
|
|
15
|
+
|
|
16
|
+
// Merge inventory sections with user-injected sections, sorted by order.
|
|
17
|
+
// Injected sections that don't exist in the original prompt appear here too,
|
|
18
|
+
// positioned by their order value.
|
|
19
|
+
const merged: Section[] = mergeSections(inv, cfg, blockedNames)
|
|
20
|
+
|
|
21
|
+
const toggleBlock = (name: string, currentlyBlocked: boolean): void => {
|
|
22
|
+
const list = (cfg.sections ?? []).slice()
|
|
23
|
+
if (currentlyBlocked) { const i = list.indexOf(name); if (i >= 0) list.splice(i, 1) }
|
|
24
|
+
else list.push(name)
|
|
25
|
+
scope.set('sections', list)
|
|
26
|
+
}
|
|
27
|
+
const startReplace = (name: string, original: string): void => {
|
|
28
|
+
// Echo the ORIGINAL prompt text so the user edits from the current value.
|
|
29
|
+
setEditing(name)
|
|
30
|
+
setDraft(replace[name] ?? original)
|
|
31
|
+
}
|
|
32
|
+
const commitReplace = (name: string): void => {
|
|
33
|
+
const next = { ...replace }
|
|
34
|
+
if (draft.trim() === '') delete next[name]
|
|
35
|
+
else next[name] = draft
|
|
36
|
+
scope.set('replace', next)
|
|
37
|
+
setEditing(null)
|
|
38
|
+
}
|
|
39
|
+
const restoreReplace = (name: string): void => {
|
|
40
|
+
const next = { ...replace }
|
|
41
|
+
delete next[name]
|
|
42
|
+
scope.set('replace', next)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Persist the full ordered list (system + custom). We re-index every section
|
|
46
|
+
// with a sequential integer order (0, 1, 2, …) so the plugin panel order is
|
|
47
|
+
// the authoritative sort order. System sections keep empty text (so the
|
|
48
|
+
// server never freezes their dynamically generated content); custom sections
|
|
49
|
+
// keep their own text.
|
|
50
|
+
const persistOrder = (ordered: Section[]): void => {
|
|
51
|
+
const list = ordered.map((sec, i) => {
|
|
52
|
+
const existing = (cfg.inject ?? []).find((x) => x.name === sec.name)
|
|
53
|
+
const isCustom = sec.source === 'custom'
|
|
54
|
+
return { name: sec.name, order: i, text: isCustom ? (sec.text ?? '') : (existing?.text ?? ''), custom: isCustom }
|
|
55
|
+
})
|
|
56
|
+
scope.set('inject', list)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Delete a custom (plugin-added) section: remove it from inject / sections /
|
|
60
|
+
// replace so it no longer appears in the panel at all.
|
|
61
|
+
const removeCustom = (name: string): void => {
|
|
62
|
+
const patch = removeSection(name, cfg)
|
|
63
|
+
scope.set('sections', patch.sections)
|
|
64
|
+
scope.set('inject', patch.inject)
|
|
65
|
+
scope.set('replace', patch.replace)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Move a section up/down one slot (array-index swap), then re-index.
|
|
69
|
+
const moveOrder = (index: number, dir: -1 | 1): void => {
|
|
70
|
+
const target = index + dir
|
|
71
|
+
if (target < 0 || target >= merged.length) return
|
|
72
|
+
const next = merged.slice()
|
|
73
|
+
const [item] = next.splice(index, 1)
|
|
74
|
+
next.splice(target, 0, item)
|
|
75
|
+
persistOrder(next)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const onDragStart = (e: DragEvent, name: string): void => {
|
|
79
|
+
setDragName(name)
|
|
80
|
+
e.dataTransfer.effectAllowed = 'move'
|
|
81
|
+
try { e.dataTransfer.setData('text/plain', name) } catch { /* ignore */ }
|
|
82
|
+
}
|
|
83
|
+
const onDragOver = (e: DragEvent, name: string): void => {
|
|
84
|
+
if (!dragName || dragName === name) return
|
|
85
|
+
e.preventDefault()
|
|
86
|
+
e.dataTransfer.dropEffect = 'move'
|
|
87
|
+
const rect = e.currentTarget.getBoundingClientRect()
|
|
88
|
+
const pos = e.clientY < rect.top + rect.height / 2 ? 'above' : 'below'
|
|
89
|
+
setDropTarget({ name, pos })
|
|
90
|
+
}
|
|
91
|
+
const onDrop = (e: DragEvent, target: Section): void => {
|
|
92
|
+
e.preventDefault()
|
|
93
|
+
if (!dragName || dragName === target.name) { setDragName(null); setDropTarget(null); return }
|
|
94
|
+
const dragged = merged.find((x) => x.name === dragName)
|
|
95
|
+
if (!dragged) { setDragName(null); setDropTarget(null); return }
|
|
96
|
+
const next = merged.filter((x) => x.name !== dragName)
|
|
97
|
+
const idx = next.findIndex((x) => x.name === target.name)
|
|
98
|
+
const pos = dropTarget?.name === target.name ? dropTarget.pos : 'below'
|
|
99
|
+
next.splice(pos === 'above' ? idx : idx + 1, 0, dragged)
|
|
100
|
+
persistOrder(next)
|
|
101
|
+
setDragName(null)
|
|
102
|
+
setDropTarget(null)
|
|
103
|
+
}
|
|
104
|
+
const onDragEnd = (): void => { setDragName(null); setDropTarget(null) }
|
|
105
|
+
|
|
106
|
+
// Add a new injected section, placed by its order hint, then re-index. The
|
|
107
|
+
// section is marked `custom` (the hidden marker) so it is identified as
|
|
108
|
+
// plugin-generated and thus deletable, and its own text is preserved.
|
|
109
|
+
const addSection = (name: string, order: number, text: string): void => {
|
|
110
|
+
const next = merged.slice()
|
|
111
|
+
const entry: Section = { name, order, text, active: true, replaced: false, source: 'custom' }
|
|
112
|
+
const existing = next.findIndex((x) => x.name === name)
|
|
113
|
+
if (existing >= 0) next[existing] = entry
|
|
114
|
+
else {
|
|
115
|
+
const idx = next.findIndex((x) => x.order > order)
|
|
116
|
+
if (idx < 0) next.push(entry)
|
|
117
|
+
else next.splice(idx, 0, entry)
|
|
118
|
+
}
|
|
119
|
+
persistOrder(next)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return h('div', { style: s.list }, [
|
|
123
|
+
merged.length === 0 ? h('div', { style: s.muted }, t('empty')) : null,
|
|
124
|
+
merged.map((sec, index) => {
|
|
125
|
+
const blocked = blockedNames.has(sec.name)
|
|
126
|
+
const isEditing = editing === sec.name
|
|
127
|
+
const isDragging = dragName === sec.name
|
|
128
|
+
const isDropTarget = dropTarget?.name === sec.name
|
|
129
|
+
const rowStyle = {
|
|
130
|
+
...s.row,
|
|
131
|
+
...(isDragging ? s.rowDragging : {}),
|
|
132
|
+
...(isDropTarget && dropTarget.pos === 'above' ? s.rowDropAbove : {}),
|
|
133
|
+
...(isDropTarget && dropTarget.pos === 'below' ? s.rowDropBelow : {}),
|
|
134
|
+
}
|
|
135
|
+
return h('div', {
|
|
136
|
+
key: sec.name,
|
|
137
|
+
style: rowStyle,
|
|
138
|
+
draggable: true,
|
|
139
|
+
onDragStart: (e: DragEvent) => onDragStart(e, sec.name),
|
|
140
|
+
onDragOver: (e: DragEvent) => onDragOver(e, sec.name),
|
|
141
|
+
onDrop: (e: DragEvent) => onDrop(e, sec),
|
|
142
|
+
onDragEnd,
|
|
143
|
+
}, [
|
|
144
|
+
h('span', { style: s.dragHandle, title: t('drag') }, '⠿'),
|
|
145
|
+
h('label', { style: s.switchWrap }, [
|
|
146
|
+
h('input', { type: 'checkbox', checked: blocked, onChange: () => toggleBlock(sec.name, blocked) }),
|
|
147
|
+
h('span', { style: blocked ? s.badgeBlocked : s.badgeOk }, blocked ? t('blockedOn') : t('blockedOff')),
|
|
148
|
+
]),
|
|
149
|
+
h('div', { style: s.rowBody }, [
|
|
150
|
+
h('div', { style: s.rowTitle }, [
|
|
151
|
+
h('span', { style: s.code }, sec.name),
|
|
152
|
+
h('span', { style: s.orderTag }, '#' + index),
|
|
153
|
+
h('span', { style: sec.source === 'custom' ? s.badgeCustom : s.badgeSystem }, sec.source === 'custom' ? t('manual') : t('system')),
|
|
154
|
+
replace[sec.name] ? h('span', { style: s.badgeReplaced }, t('replaced')) : null,
|
|
155
|
+
]),
|
|
156
|
+
isEditing
|
|
157
|
+
? h('div', { style: s.editBox }, [
|
|
158
|
+
h('textarea', { style: s.editInput, value: draft, onChange: (e: ChangeEvent<HTMLTextAreaElement>) => setDraft(e.target.value), rows: 3 }),
|
|
159
|
+
h('div', { style: s.injectRow }, [
|
|
160
|
+
h('button', { style: s.mini, onClick: () => commitReplace(sec.name) }, t('save')),
|
|
161
|
+
h('button', { style: s.mini, onClick: () => setEditing(null) }, t('clearInput')),
|
|
162
|
+
sec.source !== 'custom' && replace[sec.name] ? h('button', { style: s.mini, onClick: () => restoreReplace(sec.name) }, t('restore')) : null,
|
|
163
|
+
]),
|
|
164
|
+
])
|
|
165
|
+
: h('div', { style: s.preview }, String(replace[sec.name] ?? sec.text ?? '').slice(0, 140) || (sec.source === 'custom' ? t('empty') : t('dynamic'))),
|
|
166
|
+
]),
|
|
167
|
+
h('div', { style: s.arrowCol }, [
|
|
168
|
+
h('button', { style: s.arrow, disabled: index === 0, onClick: () => moveOrder(index, -1), title: t('moveUp') }, '↑'),
|
|
169
|
+
h('button', { style: s.arrow, disabled: index === merged.length - 1, onClick: () => moveOrder(index, 1), title: t('moveDown') }, '↓'),
|
|
170
|
+
]),
|
|
171
|
+
isEditing ? null : h('button', { style: s.mini, onClick: () => startReplace(sec.name, String(sec.text ?? '')) }, t('replace')),
|
|
172
|
+
!isEditing && sec.source !== 'custom' && replace[sec.name] ? h('button', { style: s.mini, onClick: () => restoreReplace(sec.name) }, t('restore')) : null,
|
|
173
|
+
sec.source === 'custom' ? h('button', { style: s.mini, onClick: () => removeCustom(sec.name), title: t('delete') }, t('delete')) : null,
|
|
174
|
+
])
|
|
175
|
+
}),
|
|
176
|
+
h('div', { style: s.injectBox }, [
|
|
177
|
+
h('div', { style: s.rowTitle }, t('injectNew')),
|
|
178
|
+
h(InjectForm, { onAdd: addSection, t }),
|
|
179
|
+
]),
|
|
180
|
+
])
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function InjectForm({ onAdd, t }: { onAdd: (name: string, order: number, text: string) => void; t: Translate }): ReactElement {
|
|
184
|
+
const [name, setName] = useState('')
|
|
185
|
+
const [order, setOrder] = useState('120')
|
|
186
|
+
const [text, setText] = useState('')
|
|
187
|
+
|
|
188
|
+
const submit = (): void => {
|
|
189
|
+
if (!name.trim()) return
|
|
190
|
+
onAdd(name.trim(), Number(order) || 120, text)
|
|
191
|
+
setName(''); setText(''); setOrder('120')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return h('div', {}, [
|
|
195
|
+
h('div', { style: s.injectRow }, [
|
|
196
|
+
h('input', { style: { ...s.input, width: '22%' }, placeholder: t('name'), value: name, onChange: (e) => setName(e.target.value) }),
|
|
197
|
+
h('input', { style: { ...s.input, width: '12%' }, type: 'number', placeholder: t('order'), value: order, onChange: (e) => setOrder(e.target.value) }),
|
|
198
|
+
h('input', { style: { ...s.input, flex: 1 }, placeholder: t('text'), value: text, onChange: (e) => setText(e.target.value) }),
|
|
199
|
+
h('button', { style: s.mini, onClick: submit }, t('add')),
|
|
200
|
+
]),
|
|
201
|
+
])
|
|
202
|
+
}
|
package/src/client/ToolsTab.tsx
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
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'
|
|
4
|
-
import type { Translate } from './locales.ts'
|
|
5
|
-
import { s } from './styles.ts'
|
|
6
|
-
|
|
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 ?? []
|
|
10
|
-
|
|
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)
|
|
14
|
-
|
|
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: [] })
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const setAll = (hideAll: boolean): void => {
|
|
23
|
-
scope.set('tools', { exclude: hideAll ? tools.map((tool) => tool.name) : [], include: [] })
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
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
|
-
}),
|
|
46
|
-
]),
|
|
47
|
-
])
|
|
48
|
-
}
|
|
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'
|
|
4
|
+
import type { Translate } from './locales.ts'
|
|
5
|
+
import { s } from './styles.ts'
|
|
6
|
+
|
|
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 ?? []
|
|
10
|
+
|
|
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)
|
|
14
|
+
|
|
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: [] })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const setAll = (hideAll: boolean): void => {
|
|
23
|
+
scope.set('tools', { exclude: hideAll ? tools.map((tool) => tool.name) : [], include: [] })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
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
|
+
}),
|
|
46
|
+
]),
|
|
47
|
+
])
|
|
48
|
+
}
|