prompt-skill-armory 0.4.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/LICENSE +21 -0
- package/README.md +112 -0
- package/cli.cjs +110 -0
- package/package.json +27 -0
- package/packages/client-ui-switchblade/src/client/SwitchbladeSection.tsx +614 -0
- package/packages/client-ui-switchblade/src/client/index.ts +75 -0
- package/packages/client-ui-switchblade/src/client/locales.ts +106 -0
- package/packages/client-ui-switchblade/src/client/store.ts +350 -0
- package/packages/client-ui-switchblade/src/css-modules.d.ts +5 -0
- package/packages/client-ui-switchblade/src/index.ts +12 -0
- package/packages/client-ui-switchblade/src/invariant.ts +10 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt-SkillArmory management page.
|
|
3
|
+
*
|
|
4
|
+
* Three tabs within the settings dialog's fixed width: Prompts / Skills /
|
|
5
|
+
* Agent Presets. The Skills tab is the single home for skills — both the ones
|
|
6
|
+
* installed through this panel and the ones scanned from the local skill
|
|
7
|
+
* roots — merged into one list with full management (add / edit / toggle /
|
|
8
|
+
* remove / invoke hint). A CLI entry box offers direct command installation.
|
|
9
|
+
* @module @deepseek-ai/dsh-client-ui-switchblade
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useEffect, useState } from 'react'
|
|
13
|
+
import type { CSSProperties, JSX } from 'react'
|
|
14
|
+
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
15
|
+
import type { SwitchbladeKey } from './locales.ts'
|
|
16
|
+
import type { SwitchbladeSectionInjected, SwitchbladeSectionState } from './store.ts'
|
|
17
|
+
|
|
18
|
+
export type { SwitchbladeSectionInjected } from './store.ts'
|
|
19
|
+
|
|
20
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
21
|
+
interface LocaleNamespaceMap {
|
|
22
|
+
/** Prompt-SkillArmory page copy. */
|
|
23
|
+
'settings.switchblade': SwitchbladeKey
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Full component props: settings-section runtime + locale + injected face. */
|
|
28
|
+
export type SwitchbladeSectionProps =
|
|
29
|
+
PropsRuntime<'settings.section'>
|
|
30
|
+
& PropsLocale<'settings.switchblade'>
|
|
31
|
+
& InjectFace<SwitchbladeSectionInjected>
|
|
32
|
+
|
|
33
|
+
const PHOSPHOR = '#00ff9c'
|
|
34
|
+
const DAMNED = '#ff2b4b'
|
|
35
|
+
const AMBER = '#ffb000'
|
|
36
|
+
const GRAY = '#0f3d2c'
|
|
37
|
+
|
|
38
|
+
const CSS: Record<string, CSSProperties> = {
|
|
39
|
+
root: {
|
|
40
|
+
fontFamily: "'JetBrains Mono','IBM Plex Mono',ui-monospace,monospace",
|
|
41
|
+
background: '#04070a',
|
|
42
|
+
color: PHOSPHOR,
|
|
43
|
+
padding: '16px',
|
|
44
|
+
border: `1px solid ${GRAY}`,
|
|
45
|
+
boxShadow: 'inset 0 0 40px rgba(0,255,156,.06), 0 0 18px rgba(0,255,156,.15)',
|
|
46
|
+
borderRadius: '2px',
|
|
47
|
+
width: '100%',
|
|
48
|
+
boxSizing: 'border-box' as const,
|
|
49
|
+
},
|
|
50
|
+
head: {
|
|
51
|
+
display: 'flex',
|
|
52
|
+
alignItems: 'center',
|
|
53
|
+
justifyContent: 'space-between',
|
|
54
|
+
borderBottom: `1px solid ${GRAY}`,
|
|
55
|
+
paddingBottom: '8px',
|
|
56
|
+
marginBottom: '8px',
|
|
57
|
+
},
|
|
58
|
+
title: {
|
|
59
|
+
fontSize: '14px',
|
|
60
|
+
fontWeight: 700,
|
|
61
|
+
letterSpacing: '1px',
|
|
62
|
+
textShadow: `0 0 8px ${PHOSPHOR}`,
|
|
63
|
+
display: 'flex',
|
|
64
|
+
alignItems: 'center',
|
|
65
|
+
gap: '6px',
|
|
66
|
+
},
|
|
67
|
+
titleAccent: {
|
|
68
|
+
color: DAMNED,
|
|
69
|
+
textShadow: `0 0 8px ${DAMNED}`,
|
|
70
|
+
},
|
|
71
|
+
tabs: {
|
|
72
|
+
display: 'flex',
|
|
73
|
+
gap: '4px',
|
|
74
|
+
borderBottom: `1px solid ${GRAY}`,
|
|
75
|
+
marginBottom: '12px',
|
|
76
|
+
flexWrap: 'wrap' as const,
|
|
77
|
+
},
|
|
78
|
+
tab: {
|
|
79
|
+
background: 'transparent',
|
|
80
|
+
border: `1px solid transparent`,
|
|
81
|
+
borderBottom: 'none',
|
|
82
|
+
color: '#5fb08c',
|
|
83
|
+
font: 'inherit',
|
|
84
|
+
fontSize: '12px',
|
|
85
|
+
letterSpacing: '0.5px',
|
|
86
|
+
padding: '6px 10px',
|
|
87
|
+
cursor: 'pointer',
|
|
88
|
+
},
|
|
89
|
+
tabActive: {
|
|
90
|
+
color: PHOSPHOR,
|
|
91
|
+
borderColor: GRAY,
|
|
92
|
+
background: 'rgba(0,40,24,.1)',
|
|
93
|
+
textShadow: `0 0 6px ${PHOSPHOR}`,
|
|
94
|
+
},
|
|
95
|
+
columns: {
|
|
96
|
+
display: 'grid',
|
|
97
|
+
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
|
|
98
|
+
gap: '12px',
|
|
99
|
+
alignItems: 'start',
|
|
100
|
+
},
|
|
101
|
+
column: {
|
|
102
|
+
display: 'flex',
|
|
103
|
+
flexDirection: 'column' as const,
|
|
104
|
+
gap: '10px',
|
|
105
|
+
minWidth: '0',
|
|
106
|
+
},
|
|
107
|
+
colHeader: {
|
|
108
|
+
fontSize: '12px',
|
|
109
|
+
fontWeight: 700,
|
|
110
|
+
letterSpacing: '1px',
|
|
111
|
+
color: AMBER,
|
|
112
|
+
borderBottom: `1px solid ${GRAY}`,
|
|
113
|
+
paddingBottom: '6px',
|
|
114
|
+
marginBottom: '2px',
|
|
115
|
+
},
|
|
116
|
+
card: {
|
|
117
|
+
border: `1px solid ${GRAY}`,
|
|
118
|
+
background: 'rgba(0,40,24,.08)',
|
|
119
|
+
padding: '8px 10px',
|
|
120
|
+
},
|
|
121
|
+
cardTop: {
|
|
122
|
+
display: 'flex',
|
|
123
|
+
alignItems: 'center',
|
|
124
|
+
justifyContent: 'space-between',
|
|
125
|
+
gap: '8px',
|
|
126
|
+
},
|
|
127
|
+
name: {
|
|
128
|
+
fontSize: '12px',
|
|
129
|
+
fontWeight: 700,
|
|
130
|
+
wordBreak: 'break-all' as const,
|
|
131
|
+
},
|
|
132
|
+
badge: {
|
|
133
|
+
fontSize: '9px',
|
|
134
|
+
letterSpacing: '1px',
|
|
135
|
+
padding: '2px 6px',
|
|
136
|
+
border: '1px solid currentColor',
|
|
137
|
+
flex: 'none',
|
|
138
|
+
},
|
|
139
|
+
badgeEnabled: { color: PHOSPHOR },
|
|
140
|
+
badgeDisabled: { color: DAMNED },
|
|
141
|
+
badgeInstalled: { color: AMBER },
|
|
142
|
+
desc: {
|
|
143
|
+
fontSize: '11px',
|
|
144
|
+
color: '#5fb08c',
|
|
145
|
+
marginTop: '4px',
|
|
146
|
+
},
|
|
147
|
+
invokeHint: {
|
|
148
|
+
fontSize: '10px',
|
|
149
|
+
color: '#3f8f6a',
|
|
150
|
+
marginTop: '4px',
|
|
151
|
+
fontStyle: 'italic',
|
|
152
|
+
},
|
|
153
|
+
empty: {
|
|
154
|
+
fontSize: '11px',
|
|
155
|
+
color: GRAY,
|
|
156
|
+
padding: '8px 0',
|
|
157
|
+
},
|
|
158
|
+
error: {
|
|
159
|
+
color: DAMNED,
|
|
160
|
+
fontSize: '11px',
|
|
161
|
+
padding: '8px 0',
|
|
162
|
+
},
|
|
163
|
+
refreshBtn: {
|
|
164
|
+
background: 'transparent',
|
|
165
|
+
border: `1px solid ${GRAY}`,
|
|
166
|
+
color: PHOSPHOR,
|
|
167
|
+
font: 'inherit',
|
|
168
|
+
fontSize: '11px',
|
|
169
|
+
letterSpacing: '2px',
|
|
170
|
+
textTransform: 'uppercase' as const,
|
|
171
|
+
padding: '4px 10px',
|
|
172
|
+
cursor: 'pointer',
|
|
173
|
+
},
|
|
174
|
+
actionBtn: {
|
|
175
|
+
background: 'transparent',
|
|
176
|
+
border: `1px solid ${GRAY}`,
|
|
177
|
+
color: PHOSPHOR,
|
|
178
|
+
font: 'inherit',
|
|
179
|
+
fontSize: '10px',
|
|
180
|
+
letterSpacing: '1px',
|
|
181
|
+
padding: '2px 8px',
|
|
182
|
+
cursor: 'pointer',
|
|
183
|
+
marginTop: '6px',
|
|
184
|
+
},
|
|
185
|
+
dangerBtn: {
|
|
186
|
+
borderColor: DAMNED,
|
|
187
|
+
color: DAMNED,
|
|
188
|
+
},
|
|
189
|
+
form: {
|
|
190
|
+
display: 'flex',
|
|
191
|
+
flexDirection: 'column' as const,
|
|
192
|
+
gap: '6px',
|
|
193
|
+
marginBottom: '10px',
|
|
194
|
+
padding: '10px',
|
|
195
|
+
border: `1px solid ${GRAY}`,
|
|
196
|
+
background: 'rgba(0,40,24,.05)',
|
|
197
|
+
},
|
|
198
|
+
input: {
|
|
199
|
+
background: '#04070a',
|
|
200
|
+
border: `1px solid ${GRAY}`,
|
|
201
|
+
color: PHOSPHOR,
|
|
202
|
+
font: 'inherit',
|
|
203
|
+
fontSize: '11px',
|
|
204
|
+
padding: '6px 8px',
|
|
205
|
+
},
|
|
206
|
+
textarea: {
|
|
207
|
+
background: '#04070a',
|
|
208
|
+
border: `1px solid ${GRAY}`,
|
|
209
|
+
color: PHOSPHOR,
|
|
210
|
+
font: 'inherit',
|
|
211
|
+
fontSize: '11px',
|
|
212
|
+
padding: '6px 8px',
|
|
213
|
+
minHeight: '80px',
|
|
214
|
+
resize: 'vertical' as const,
|
|
215
|
+
},
|
|
216
|
+
actions: {
|
|
217
|
+
display: 'flex',
|
|
218
|
+
gap: '6px',
|
|
219
|
+
flexWrap: 'wrap' as const,
|
|
220
|
+
},
|
|
221
|
+
hint: {
|
|
222
|
+
color: GRAY,
|
|
223
|
+
fontSize: '9px',
|
|
224
|
+
letterSpacing: '1px',
|
|
225
|
+
},
|
|
226
|
+
scrollBox: {
|
|
227
|
+
maxHeight: '520px',
|
|
228
|
+
overflowY: 'auto' as const,
|
|
229
|
+
display: 'flex',
|
|
230
|
+
flexDirection: 'column' as const,
|
|
231
|
+
gap: '8px',
|
|
232
|
+
paddingRight: '4px',
|
|
233
|
+
},
|
|
234
|
+
searchInput: {
|
|
235
|
+
background: '#04070a',
|
|
236
|
+
border: `1px solid ${GRAY}`,
|
|
237
|
+
color: '#5fb08c',
|
|
238
|
+
font: 'inherit',
|
|
239
|
+
fontSize: '10px',
|
|
240
|
+
padding: '5px 8px',
|
|
241
|
+
width: '100%',
|
|
242
|
+
boxSizing: 'border-box' as const,
|
|
243
|
+
},
|
|
244
|
+
fileBtn: {
|
|
245
|
+
background: 'transparent',
|
|
246
|
+
border: `1px dashed ${GRAY}`,
|
|
247
|
+
color: '#5fb08c',
|
|
248
|
+
font: 'inherit',
|
|
249
|
+
fontSize: '10px',
|
|
250
|
+
letterSpacing: '1px',
|
|
251
|
+
padding: '8px',
|
|
252
|
+
cursor: 'pointer',
|
|
253
|
+
textAlign: 'center' as const,
|
|
254
|
+
},
|
|
255
|
+
cliBox: {
|
|
256
|
+
border: `1px dashed ${AMBER}`,
|
|
257
|
+
padding: '8px 10px',
|
|
258
|
+
fontSize: '10px',
|
|
259
|
+
color: '#5fb08c',
|
|
260
|
+
background: 'rgba(255,176,0,.04)',
|
|
261
|
+
},
|
|
262
|
+
versionBadge: {
|
|
263
|
+
fontSize: '10px',
|
|
264
|
+
fontWeight: 700,
|
|
265
|
+
letterSpacing: '1px',
|
|
266
|
+
color: PHOSPHOR,
|
|
267
|
+
border: `1px solid ${PHOSPHOR}`,
|
|
268
|
+
borderRadius: '3px',
|
|
269
|
+
padding: '1px 6px',
|
|
270
|
+
marginLeft: '6px',
|
|
271
|
+
background: 'rgba(0,255,156,.08)',
|
|
272
|
+
textShadow: `0 0 6px ${PHOSPHOR}`,
|
|
273
|
+
flex: 'none',
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Open-book glyph. */
|
|
278
|
+
function BookIcon({ size = 16 }: { size?: number }): JSX.Element {
|
|
279
|
+
return (
|
|
280
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" style={{ flex: 'none' }} aria-hidden="true">
|
|
281
|
+
<path d="M7.5 3.2C6.2 2.4 4.6 2.2 2.8 2.5c-.5.08-.8.5-.8 1v7.6c0 .4.3.7.7.7 1.7-.2 3.2.1 4.8 1V3.2z" fill="currentColor" opacity="0.55" />
|
|
282
|
+
<path d="M8.5 3.2c1.3-.8 2.9-1 4.7-.7.5.08.8.5.8 1v7.6c0 .4-.3.7-.7.7-1.7-.2-3.2.1-4.8 1V3.2z" fill="currentColor" opacity="0.85" />
|
|
283
|
+
<path d="M8 3.2v10.3" stroke="currentColor" strokeWidth="0.7" />
|
|
284
|
+
</svg>
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
type TabKey = 'prompts' | 'skills' | 'presets'
|
|
289
|
+
|
|
290
|
+
/** Bump with every release; keep in sync with package.json version + CHANGELOG. */
|
|
291
|
+
const ARMORY_VERSION = '0.4.1'
|
|
292
|
+
|
|
293
|
+
/** Render the Prompt-SkillArmory management page. */
|
|
294
|
+
export function SwitchbladeSection(props: SwitchbladeSectionProps): JSX.Element {
|
|
295
|
+
const {
|
|
296
|
+
useSwitchblade, t, load,
|
|
297
|
+
setDefaultPreset, addPrompt, updatePrompt, setPromptEnabled, setDefaultPrompt, deletePrompt,
|
|
298
|
+
installSkill, updateSkill, setSkillEnabled, uninstallSkill,
|
|
299
|
+
} = props
|
|
300
|
+
const state = useSwitchblade((snapshot: SwitchbladeSectionState) => snapshot)
|
|
301
|
+
const [promptName, setPromptName] = useState('')
|
|
302
|
+
const [promptDesc, setPromptDesc] = useState('')
|
|
303
|
+
const [promptContent, setPromptContent] = useState('')
|
|
304
|
+
const [skillName, setSkillName] = useState('')
|
|
305
|
+
const [skillDesc, setSkillDesc] = useState('')
|
|
306
|
+
const [skillContent, setSkillContent] = useState('')
|
|
307
|
+
const [busy, setBusy] = useState(false)
|
|
308
|
+
const [pickedFile, setPickedFile] = useState('')
|
|
309
|
+
const [promptQuery, setPromptQuery] = useState('')
|
|
310
|
+
const [skillQuery, setSkillQuery] = useState('')
|
|
311
|
+
const [presetQuery, setPresetQuery] = useState('')
|
|
312
|
+
const [activeTab, setActiveTab] = useState<TabKey>('prompts')
|
|
313
|
+
const [editingPromptId, setEditingPromptId] = useState<string | undefined>()
|
|
314
|
+
const [editingSkillName, setEditingSkillName] = useState<string | undefined>()
|
|
315
|
+
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
void load()
|
|
318
|
+
}, [load])
|
|
319
|
+
|
|
320
|
+
const refresh = (): void => {
|
|
321
|
+
void load()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const setDefault = (id: string): void => {
|
|
325
|
+
void setDefaultPreset(id)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Read a local skill .md file into the import form. */
|
|
329
|
+
const onSkillFile = (file: File | undefined): void => {
|
|
330
|
+
if (file === undefined) return
|
|
331
|
+
setPickedFile(file.name)
|
|
332
|
+
void file.text().then((text) => {
|
|
333
|
+
const first = text.split('\n')[0]?.trim() ?? ''
|
|
334
|
+
const nameMatch = /^#\s+([a-z0-9][a-z0-9-]*)$/i.exec(first)
|
|
335
|
+
if (nameMatch !== null) setSkillName(nameMatch[1]?.toLowerCase() ?? '')
|
|
336
|
+
setSkillContent(text.trim())
|
|
337
|
+
const descLine = text.split('\n').find((l) => l.startsWith('> '))
|
|
338
|
+
if (descLine !== undefined) setSkillDesc(descLine.slice(2).trim())
|
|
339
|
+
}).catch((error: unknown) => console.error('[switchblade] read skill file failed', error))
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const submitPrompt = (): void => {
|
|
343
|
+
if (promptName.trim() === '' || promptContent.trim() === '') return
|
|
344
|
+
setBusy(true)
|
|
345
|
+
const action = editingPromptId !== undefined
|
|
346
|
+
? updatePrompt(editingPromptId, { name: promptName, description: promptDesc, content: promptContent })
|
|
347
|
+
: addPrompt({ name: promptName, description: promptDesc, content: promptContent })
|
|
348
|
+
void action
|
|
349
|
+
.catch((error: unknown) => console.error('[switchblade] prompt save failed', error))
|
|
350
|
+
.finally(() => {
|
|
351
|
+
setBusy(false)
|
|
352
|
+
setPromptName(''); setPromptDesc(''); setPromptContent('')
|
|
353
|
+
setEditingPromptId(undefined)
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const startEditPrompt = (row: { promptId: string; name: string; desc: string; content?: string }): void => {
|
|
358
|
+
setEditingPromptId(row.promptId)
|
|
359
|
+
setPromptName(row.name)
|
|
360
|
+
setPromptDesc(row.desc)
|
|
361
|
+
setPromptContent(row.content ?? '')
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const togglePrompt = (id: string, enabled: boolean): void => {
|
|
365
|
+
void setPromptEnabled(id, enabled).catch((error: unknown) => console.error('[switchblade] toggle failed', error))
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const markDefault = (id: string): void => {
|
|
369
|
+
void setDefaultPrompt(id).catch((error: unknown) => console.error('[switchblade] setDefault failed', error))
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const removePrompt = (id: string): void => {
|
|
373
|
+
void deletePrompt(id).catch((error: unknown) => console.error('[switchblade] delete failed', error))
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const submitSkill = (): void => {
|
|
377
|
+
if (skillName.trim() === '' || skillContent.trim() === '') return
|
|
378
|
+
setBusy(true)
|
|
379
|
+
const action = editingSkillName !== undefined
|
|
380
|
+
? updateSkill(editingSkillName, { name: skillName, description: skillDesc, content: skillContent })
|
|
381
|
+
: installSkill({ name: skillName, description: skillDesc, content: skillContent })
|
|
382
|
+
void action
|
|
383
|
+
.catch((error: unknown) => console.error('[switchblade] skill save failed', error))
|
|
384
|
+
.finally(() => {
|
|
385
|
+
setBusy(false)
|
|
386
|
+
setSkillName(''); setSkillDesc(''); setSkillContent('')
|
|
387
|
+
setEditingSkillName(undefined)
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const startEditSkill = (row: { installedName: string; name: string; desc: string }): void => {
|
|
392
|
+
const found = state.installedSkills.find((s) => s.name === row.installedName)
|
|
393
|
+
setEditingSkillName(row.installedName)
|
|
394
|
+
setSkillName(row.name)
|
|
395
|
+
setSkillDesc(row.desc)
|
|
396
|
+
setSkillContent(found?.content ?? '')
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const toggleSkill = (name: string, enabled: boolean): void => {
|
|
400
|
+
void setSkillEnabled(name, enabled).catch((error: unknown) => console.error('[switchblade] toggle skill failed', error))
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const removeSkill = (name: string): void => {
|
|
404
|
+
void uninstallSkill(name).catch((error: unknown) => console.error('[switchblade] uninstall failed', error))
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Adopt a scanned (local) skill into the managed list. */
|
|
408
|
+
const adoptSkill = (name: string): void => {
|
|
409
|
+
const found = state.skills.find((s) => s.name === name)
|
|
410
|
+
if (found === undefined) return
|
|
411
|
+
void installSkill({ name: found.name, description: found.description, content: `# ${found.name}\n\n${found.description}` })
|
|
412
|
+
.catch((error: unknown) => console.error('[switchblade] adopt skill failed', error))
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const promptRows = state.prompts.map((p) => ({
|
|
416
|
+
id: p.id, name: p.name, desc: p.description, state: p.enabled ? ('enabled' as const) : ('disabled' as const),
|
|
417
|
+
promptId: p.id, isDefault: p.isDefault, content: p.content, promptEnabled: p.enabled,
|
|
418
|
+
}))
|
|
419
|
+
const presetRows = state.presets.map((p) => ({
|
|
420
|
+
id: p.id, name: p.name ?? p.id, desc: p.description ?? p.trust,
|
|
421
|
+
state: p.isDefault ? ('enabled' as const) : ('installed' as const),
|
|
422
|
+
presetId: p.id, isDefault: p.isDefault,
|
|
423
|
+
}))
|
|
424
|
+
|
|
425
|
+
// Merge managed + scanned skills into ONE list, managed first.
|
|
426
|
+
const managedNames = new Set(state.installedSkills.map((s) => s.name))
|
|
427
|
+
const managedRows = state.installedSkills.map((s) => ({
|
|
428
|
+
key: `m-${s.name}`, name: s.name, desc: s.description,
|
|
429
|
+
state: s.enabled ? ('enabled' as const) : ('disabled' as const),
|
|
430
|
+
installedName: s.name, skillEnabled: s.enabled, source: 'managed' as const,
|
|
431
|
+
}))
|
|
432
|
+
const scannedRows = state.skills
|
|
433
|
+
.filter((s) => !managedNames.has(s.name))
|
|
434
|
+
.map((s) => ({
|
|
435
|
+
key: `s-${s.name}`, name: s.name, desc: s.description,
|
|
436
|
+
state: ('installed' as const), installedName: s.name, skillEnabled: false, source: 'scanned' as const,
|
|
437
|
+
}))
|
|
438
|
+
const allSkillRows = [...managedRows, ...scannedRows]
|
|
439
|
+
|
|
440
|
+
const match = (row: { name: string; desc: string }, q: string): boolean => {
|
|
441
|
+
const query = q.trim().toLowerCase()
|
|
442
|
+
if (query === '') return true
|
|
443
|
+
return row.name.toLowerCase().includes(query) || row.desc.toLowerCase().includes(query)
|
|
444
|
+
}
|
|
445
|
+
const filteredPrompts = promptRows.filter((r) => match(r, promptQuery))
|
|
446
|
+
const filteredPresets = presetRows.filter((r) => match(r, presetQuery))
|
|
447
|
+
const filteredSkills = allSkillRows.filter((r) => match(r, skillQuery))
|
|
448
|
+
|
|
449
|
+
const badge = (state: 'enabled' | 'disabled' | 'installed'): CSSProperties => (
|
|
450
|
+
state === 'enabled' ? CSS.badgeEnabled! : state === 'disabled' ? CSS.badgeDisabled! : CSS.badgeInstalled!
|
|
451
|
+
)
|
|
452
|
+
const label = (state: 'enabled' | 'disabled' | 'installed'): string => (
|
|
453
|
+
state === 'enabled' ? t('enabled') : state === 'disabled' ? t('disabled') : t('installed')
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
return (
|
|
457
|
+
<div style={CSS.root}>
|
|
458
|
+
<div style={CSS.head}>
|
|
459
|
+
<div style={CSS.title}>
|
|
460
|
+
<BookIcon size={16} /> <span style={CSS.titleAccent}>Prompt</span>-SkillArmory
|
|
461
|
+
<span style={CSS.versionBadge}>v{ARMORY_VERSION}</span>
|
|
462
|
+
</div>
|
|
463
|
+
<button style={CSS.refreshBtn} onClick={refresh}>{t('refresh')}</button>
|
|
464
|
+
</div>
|
|
465
|
+
|
|
466
|
+
{state.status === 'error' && <div style={CSS.error}>✖ {t('loadFailed')}: {state.message}</div>}
|
|
467
|
+
|
|
468
|
+
{/* Tab bar */}
|
|
469
|
+
<div style={CSS.tabs}>
|
|
470
|
+
<button style={{ ...CSS.tab, ...(activeTab === 'prompts' ? CSS.tabActive : {}) }} onClick={() => setActiveTab('prompts')}>
|
|
471
|
+
{t('promptsTitle')} ({state.status === 'loading' ? '…' : promptRows.length})
|
|
472
|
+
</button>
|
|
473
|
+
<button style={{ ...CSS.tab, ...(activeTab === 'skills' ? CSS.tabActive : {}) }} onClick={() => setActiveTab('skills')}>
|
|
474
|
+
{t('installSkill')} ({state.status === 'loading' ? '…' : allSkillRows.length})
|
|
475
|
+
</button>
|
|
476
|
+
<button style={{ ...CSS.tab, ...(activeTab === 'presets' ? CSS.tabActive : {}) }} onClick={() => setActiveTab('presets')}>
|
|
477
|
+
{t('agentPresetsTitle')} ({state.status === 'loading' ? '…' : presetRows.length})
|
|
478
|
+
</button>
|
|
479
|
+
</div>
|
|
480
|
+
|
|
481
|
+
{/* ── Tab: prompts ─────────────────────────────────────── */}
|
|
482
|
+
{activeTab === 'prompts' && (
|
|
483
|
+
<div style={CSS.columns}>
|
|
484
|
+
<div style={CSS.column}>
|
|
485
|
+
<div style={CSS.form}>
|
|
486
|
+
<input style={CSS.input} placeholder={t('promptNamePlaceholder')} value={promptName} onChange={(e) => setPromptName(e.target.value)} />
|
|
487
|
+
<input style={CSS.input} placeholder={t('promptDescPlaceholder')} value={promptDesc} onChange={(e) => setPromptDesc(e.target.value)} />
|
|
488
|
+
<textarea style={CSS.textarea} placeholder={t('promptContentPlaceholder')} value={promptContent} onChange={(e) => setPromptContent(e.target.value)} />
|
|
489
|
+
<div style={CSS.actions}>
|
|
490
|
+
<button style={CSS.actionBtn} disabled={busy} onClick={submitPrompt}>
|
|
491
|
+
{editingPromptId !== undefined ? t('save') : t('addPrompt')}
|
|
492
|
+
</button>
|
|
493
|
+
{editingPromptId !== undefined && (
|
|
494
|
+
<button style={CSS.actionBtn} onClick={() => { setEditingPromptId(undefined); setPromptName(''); setPromptDesc(''); setPromptContent('') }}>{t('cancel')}</button>
|
|
495
|
+
)}
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
<div style={CSS.column}>
|
|
500
|
+
<input style={CSS.searchInput} placeholder={t('searchPlaceholder')} value={promptQuery} onChange={(e) => setPromptQuery(e.target.value)} />
|
|
501
|
+
<div style={CSS.scrollBox}>
|
|
502
|
+
{filteredPrompts.length === 0
|
|
503
|
+
? <div style={CSS.empty}>{t('empty')}</div>
|
|
504
|
+
: filteredPrompts.map((row) => (
|
|
505
|
+
<div key={row.id} style={CSS.card}>
|
|
506
|
+
<div style={CSS.cardTop}>
|
|
507
|
+
<div style={CSS.name}>{row.isDefault ? '★ ' : ''}{row.name}</div>
|
|
508
|
+
<span style={{ ...CSS.badge, ...row.state === 'enabled' ? CSS.badgeEnabled : CSS.badgeDisabled }}>
|
|
509
|
+
{row.state === 'enabled' ? t('enabled') : t('disabled')}
|
|
510
|
+
</span>
|
|
511
|
+
</div>
|
|
512
|
+
<div style={CSS.desc}>{row.desc || row.content?.slice(0, 80)}</div>
|
|
513
|
+
<div style={CSS.actions}>
|
|
514
|
+
{!row.isDefault && <button style={CSS.actionBtn} onClick={() => markDefault(row.promptId!)}>{t('setDefault')}</button>}
|
|
515
|
+
<button style={CSS.actionBtn} onClick={() => togglePrompt(row.promptId!, !row.promptEnabled)}>
|
|
516
|
+
{row.state === 'enabled' ? t('disable') : t('enable')}
|
|
517
|
+
</button>
|
|
518
|
+
<button style={CSS.actionBtn} onClick={() => startEditPrompt(row)}>{t('edit')}</button>
|
|
519
|
+
<button style={{ ...CSS.actionBtn, ...CSS.dangerBtn }} onClick={() => removePrompt(row.promptId!)}>{t('delete')}</button>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
))}
|
|
523
|
+
</div>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
)}
|
|
527
|
+
|
|
528
|
+
{/* ── Tab: skills (merged managed + scanned) ───────────── */}
|
|
529
|
+
{activeTab === 'skills' && (
|
|
530
|
+
<div style={CSS.columns}>
|
|
531
|
+
<div style={CSS.column}>
|
|
532
|
+
<div style={CSS.form}>
|
|
533
|
+
<label style={CSS.fileBtn}>
|
|
534
|
+
{pickedFile !== '' ? `📄 ${pickedFile}` : t('pickSkillFile')}
|
|
535
|
+
<input type="file" accept=".md,.markdown,text/markdown,text/plain" style={{ display: 'none' }} onChange={(e) => onSkillFile(e.target.files?.[0])} />
|
|
536
|
+
</label>
|
|
537
|
+
<input style={CSS.input} placeholder={t('skillNamePlaceholder')} value={skillName} onChange={(e) => setSkillName(e.target.value)} />
|
|
538
|
+
<input style={CSS.input} placeholder={t('skillDescPlaceholder')} value={skillDesc} onChange={(e) => setSkillDesc(e.target.value)} />
|
|
539
|
+
<textarea style={CSS.textarea} placeholder={t('skillContentPlaceholder')} value={skillContent} onChange={(e) => setSkillContent(e.target.value)} />
|
|
540
|
+
<div style={CSS.actions}>
|
|
541
|
+
<button style={CSS.actionBtn} disabled={busy} onClick={submitSkill}>
|
|
542
|
+
{editingSkillName !== undefined ? t('save') : t('addSkill')}
|
|
543
|
+
</button>
|
|
544
|
+
{editingSkillName !== undefined && (
|
|
545
|
+
<button style={CSS.actionBtn} onClick={() => { setEditingSkillName(undefined); setSkillName(''); setSkillDesc(''); setSkillContent('') }}>{t('cancel')}</button>
|
|
546
|
+
)}
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
{/* CLI entry */}
|
|
550
|
+
<div style={CSS.cliBox}>
|
|
551
|
+
<div style={{ marginBottom: '4px' }}>{t('cliHint')}</div>
|
|
552
|
+
<code style={{ fontSize: '10px', color: PHOSPHOR }}>/armory-skill-dir <目录></code><br />
|
|
553
|
+
<code style={{ fontSize: '10px', color: PHOSPHOR }}>/armory-install-zip <zip路径></code>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
<div style={CSS.column}>
|
|
557
|
+
<input style={CSS.searchInput} placeholder={t('searchPlaceholder')} value={skillQuery} onChange={(e) => setSkillQuery(e.target.value)} />
|
|
558
|
+
<div style={CSS.scrollBox}>
|
|
559
|
+
{filteredSkills.length === 0
|
|
560
|
+
? <div style={CSS.empty}>{t('empty')}</div>
|
|
561
|
+
: filteredSkills.map((row) => (
|
|
562
|
+
<div key={row.key} style={CSS.card}>
|
|
563
|
+
<div style={CSS.cardTop}>
|
|
564
|
+
<div style={CSS.name}>{row.name}</div>
|
|
565
|
+
<span style={{ ...CSS.badge, ...badge(row.state) }}>{label(row.state)}</span>
|
|
566
|
+
</div>
|
|
567
|
+
<div style={CSS.desc}>{row.desc}</div>
|
|
568
|
+
<div style={CSS.invokeHint}>/ {row.name}</div>
|
|
569
|
+
<div style={CSS.actions}>
|
|
570
|
+
{row.source === 'scanned' ? (
|
|
571
|
+
<button style={CSS.actionBtn} onClick={() => adoptSkill(row.name)}>{t('manage')}</button>
|
|
572
|
+
) : (
|
|
573
|
+
<>
|
|
574
|
+
<button style={CSS.actionBtn} onClick={() => toggleSkill(row.installedName!, !row.skillEnabled)}>
|
|
575
|
+
{row.state === 'enabled' ? t('disable') : t('enable')}
|
|
576
|
+
</button>
|
|
577
|
+
<button style={CSS.actionBtn} onClick={() => startEditSkill(row)}>{t('edit')}</button>
|
|
578
|
+
<button style={{ ...CSS.actionBtn, ...CSS.dangerBtn }} onClick={() => removeSkill(row.installedName!)}>{t('uninstall')}</button>
|
|
579
|
+
</>
|
|
580
|
+
)}
|
|
581
|
+
</div>
|
|
582
|
+
</div>
|
|
583
|
+
))}
|
|
584
|
+
</div>
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
)}
|
|
588
|
+
|
|
589
|
+
{/* ── Tab: presets ──────────────────────────────────────── */}
|
|
590
|
+
{activeTab === 'presets' && (
|
|
591
|
+
<div style={CSS.columns}>
|
|
592
|
+
<div style={CSS.column}>
|
|
593
|
+
<div style={CSS.colHeader}>{t('agentPresetsTitle')} ({presetRows.length})</div>
|
|
594
|
+
<input style={CSS.searchInput} placeholder={t('searchPlaceholder')} value={presetQuery} onChange={(e) => setPresetQuery(e.target.value)} />
|
|
595
|
+
<div style={CSS.scrollBox}>
|
|
596
|
+
{filteredPresets.length === 0
|
|
597
|
+
? <div style={CSS.empty}>{t('empty')}</div>
|
|
598
|
+
: filteredPresets.map((row) => (
|
|
599
|
+
<div key={row.id} style={CSS.card}>
|
|
600
|
+
<div style={CSS.cardTop}>
|
|
601
|
+
<div style={CSS.name}>{row.isDefault ? '★ ' : ''}{row.name}</div>
|
|
602
|
+
<span style={{ ...CSS.badge, ...badge(row.state) }}>{label(row.state)}</span>
|
|
603
|
+
</div>
|
|
604
|
+
<div style={CSS.desc}>{row.desc}</div>
|
|
605
|
+
{!row.isDefault && <button style={CSS.actionBtn} onClick={() => setDefault(row.presetId!)}>{t('setDefault')}</button>}
|
|
606
|
+
</div>
|
|
607
|
+
))}
|
|
608
|
+
</div>
|
|
609
|
+
</div>
|
|
610
|
+
</div>
|
|
611
|
+
)}
|
|
612
|
+
</div>
|
|
613
|
+
)
|
|
614
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Switchblade management page, browser half: registers the `settings.section`
|
|
3
|
+
* navigation entry and renders the edgelord panel from the connection RPC
|
|
4
|
+
* state. Global scope (root) — one management seat for every session.
|
|
5
|
+
* @module @deepseek-ai/dsh-client-ui-switchblade
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client'
|
|
9
|
+
import type { ClientContext, ISessions } from '@deepseek-ai/dsh-client-runtime/client'
|
|
10
|
+
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
|
11
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
12
|
+
// Type-only: pulls the settings shell's SlotMap merge (the 'settings.section' entry).
|
|
13
|
+
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
14
|
+
// Type-only: pulls the LocaleNamespaceMap merge slot (the 'settings.switchblade' entry).
|
|
15
|
+
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
16
|
+
import { en, NS, zh, type SwitchbladeKey } from './locales.ts'
|
|
17
|
+
import { SwitchbladeSection } from './SwitchbladeSection.tsx'
|
|
18
|
+
import type { SwitchbladeSectionInjected } from './SwitchbladeSection.tsx'
|
|
19
|
+
import { SwitchbladeSectionController } from './store.ts'
|
|
20
|
+
|
|
21
|
+
export { SwitchbladeSection } from './SwitchbladeSection.tsx'
|
|
22
|
+
export type { SwitchbladeSectionInjected, SwitchbladeSectionProps } from './SwitchbladeSection.tsx'
|
|
23
|
+
export { SwitchbladeSectionController } from './store.ts'
|
|
24
|
+
export type { SwitchbladeSectionState, SkillRow, PresetRow } from './store.ts'
|
|
25
|
+
|
|
26
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
27
|
+
interface LocaleNamespaceMap {
|
|
28
|
+
/** Switchblade management page copy. */
|
|
29
|
+
'settings.switchblade': SwitchbladeKey
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Required services (cordis fiber inject). */
|
|
34
|
+
export const inject = ['slots', 'locale', 'connection', 'sessions']
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Mount the Switchblade settings section.
|
|
38
|
+
* @param ctx - the browser plugin context.
|
|
39
|
+
*/
|
|
40
|
+
export function apply(ctx: ClientContext): void {
|
|
41
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-switchblade: dictionaries')
|
|
42
|
+
|
|
43
|
+
const api = (ctx.get('connection') as ConnectionHandle).api
|
|
44
|
+
const sessions = ctx.get('sessions') as ISessions
|
|
45
|
+
const controller = new SwitchbladeSectionController(api, () => {
|
|
46
|
+
const state = sessions.list.getSnapshot()
|
|
47
|
+
return state.current === undefined ? undefined : state.current
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
51
|
+
name: 'settings.section',
|
|
52
|
+
id: 'switchblade',
|
|
53
|
+
order: 30,
|
|
54
|
+
label: () => ctx.locale.bind(NS)('nav'),
|
|
55
|
+
locale: NS,
|
|
56
|
+
inject: (): SwitchbladeSectionInjected => ({
|
|
57
|
+
hooks: { switchblade: controller.store },
|
|
58
|
+
load: () => controller.load(),
|
|
59
|
+
setDefaultPreset: (id: string) => controller.setDefaultPreset(id),
|
|
60
|
+
addPrompt: (input) => controller.addPrompt(input),
|
|
61
|
+
updatePrompt: (id, patch) => controller.updatePrompt(id, patch),
|
|
62
|
+
setPromptEnabled: (id, enabled) => controller.setPromptEnabled(id, enabled),
|
|
63
|
+
setDefaultPrompt: (id) => controller.setDefaultPrompt(id),
|
|
64
|
+
deletePrompt: (id) => controller.deletePrompt(id),
|
|
65
|
+
installSkill: (input) => controller.installSkill(input),
|
|
66
|
+
updateSkill: (name, patch) => controller.updateSkill(name, patch),
|
|
67
|
+
setSkillEnabled: (name, enabled) => controller.setSkillEnabled(name, enabled),
|
|
68
|
+
uninstallSkill: (name) => controller.uninstallSkill(name),
|
|
69
|
+
installSkillFromZip: (name, dataBase64) => controller.installSkillFromZip(name, dataBase64),
|
|
70
|
+
}),
|
|
71
|
+
}, SwitchbladeSection))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Cordis plugin identity. */
|
|
75
|
+
export const name = 'ui-switchblade'
|