dsh-plugin-capabilities 0.1.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.
@@ -0,0 +1,301 @@
1
+ /** Settings → Plugins “技能/Skills” tab: view the discovered catalog, edit the
2
+ * user-owned root. Pure presentation-layer — data arrives through props. */
3
+
4
+ import { useEffect, useState } from 'react'
5
+ import type { ReactElement } from 'react'
6
+ import { Button, IconRefreshOutline14, IconSkillOutline16, Modal, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
7
+ import { CSS } from './css.ts'
8
+ import type { Translate } from './index.ts'
9
+
10
+ /** One skill as the host reports it (plus the editable flag the route adds). */
11
+ export interface SkillRowView {
12
+ name: string
13
+ description: string
14
+ whenToUse?: string
15
+ invocation: { modelInvocable: boolean; userInvocable: boolean }
16
+ source: string
17
+ provider: string
18
+ editable: boolean
19
+ }
20
+
21
+ export interface SkillsInjected {
22
+ list(): Promise<{ skills: SkillRowView[] }>
23
+ get(name: string): Promise<{ content: string }>
24
+ save(input: { name: string; description: string; whenToUse?: string; modelInvocable: boolean; userInvocable: boolean; content: string }): Promise<{ ok: boolean }>
25
+ remove(name: string): Promise<{ ok: boolean }>
26
+ }
27
+
28
+ /** Editor dialog state; null when closed. */
29
+ interface EditorState {
30
+ mode: 'create' | 'edit' | 'view'
31
+ name: string
32
+ description: string
33
+ whenToUse: string
34
+ modelInvocable: boolean
35
+ userInvocable: boolean
36
+ content: string
37
+ }
38
+
39
+ const SOURCE_KEYS: Record<string, string> = {
40
+ 'project-dsh': 'sourceProjectDsh',
41
+ 'project-agents': 'sourceProjectAgents',
42
+ 'user-dsh': 'sourceUserDsh',
43
+ 'user-agents': 'sourceUserAgents',
44
+ runtime: 'sourceRuntime',
45
+ bundled: 'sourceBundled',
46
+ custom: 'sourceCustom',
47
+ }
48
+
49
+ export function SkillsTab(props: { t: Translate; injected: SkillsInjected }): ReactElement {
50
+ const { t, injected } = props
51
+ const [skills, setSkills] = useState<SkillRowView[] | null>(null)
52
+ const [editor, setEditor] = useState<EditorState | null>(null)
53
+ const [confirmName, setConfirmName] = useState<string | null>(null)
54
+ const [busy, setBusy] = useState(false)
55
+ const [outcome, setOutcome] = useState<{ ok: boolean; text: string } | null>(null)
56
+ const [formError, setFormError] = useState<string | null>(null)
57
+ const [reload, setReload] = useState(0)
58
+
59
+ useEffect(() => {
60
+ let current = true
61
+ void injected.list().then(
62
+ (body) => { if (current) setSkills(body.skills) },
63
+ (error: Error) => { if (current) { setSkills([]); setOutcome({ ok: false, text: `${t('failed')}: ${String(error.message ?? error)}` }) } },
64
+ )
65
+ return () => { current = false }
66
+ }, [injected, reload, t])
67
+
68
+ const openCreate = (): void => {
69
+ setFormError(null)
70
+ setEditor({ mode: 'create', name: '', description: '', whenToUse: '', modelInvocable: true, userInvocable: true, content: '' })
71
+ }
72
+
73
+ const openExisting = async (skill: SkillRowView): Promise<void> => {
74
+ setBusy(true)
75
+ setFormError(null)
76
+ try {
77
+ const body = await injected.get(skill.name)
78
+ setEditor({
79
+ mode: skill.editable ? 'edit' : 'view',
80
+ name: skill.name,
81
+ description: skill.description,
82
+ whenToUse: skill.whenToUse ?? '',
83
+ modelInvocable: skill.invocation.modelInvocable,
84
+ userInvocable: skill.invocation.userInvocable,
85
+ content: body.content,
86
+ })
87
+ } catch (error) {
88
+ setOutcome({ ok: false, text: `${t('failed')}: ${String(error instanceof Error ? error.message : error)}` })
89
+ } finally {
90
+ setBusy(false)
91
+ }
92
+ }
93
+
94
+ /**
95
+ * After a write/delete, the watched directory takes a moment to invalidate
96
+ * the catalog — poll until the change is visible (bounded), then stop.
97
+ */
98
+ const refreshUntil = (predicate: (skills: SkillRowView[]) => boolean, timeoutMs = 10_000): void => {
99
+ const deadline = Date.now() + timeoutMs
100
+ const tick = (): void => {
101
+ if (Date.now() > deadline) return
102
+ setTimeout(() => {
103
+ void injected.list().then(
104
+ (body) => {
105
+ setSkills(body.skills)
106
+ if (!predicate(body.skills)) tick()
107
+ },
108
+ () => tick(),
109
+ )
110
+ }, 1000)
111
+ }
112
+ tick()
113
+ }
114
+
115
+ const doSave = async (): Promise<void> => {
116
+ if (editor === null) return
117
+ setBusy(true)
118
+ setFormError(null)
119
+ try {
120
+ const name = editor.name.trim()
121
+ await injected.save({
122
+ name,
123
+ description: editor.description,
124
+ whenToUse: editor.whenToUse.trim() === '' ? undefined : editor.whenToUse,
125
+ modelInvocable: editor.modelInvocable,
126
+ userInvocable: editor.userInvocable,
127
+ content: editor.content,
128
+ })
129
+ setOutcome({ ok: true, text: t('saved') })
130
+ setEditor(null)
131
+ refreshUntil(skills => skills.some(skill => skill.name === name))
132
+ } catch (error) {
133
+ setFormError(String(error instanceof Error ? error.message : error))
134
+ } finally {
135
+ setBusy(false)
136
+ }
137
+ }
138
+
139
+ const doDelete = async (): Promise<void> => {
140
+ if (confirmName === null) return
141
+ const name = confirmName
142
+ setBusy(true)
143
+ try {
144
+ await injected.remove(name)
145
+ setOutcome({ ok: true, text: t('saved') })
146
+ refreshUntil(skills => !skills.some(skill => skill.name === name))
147
+ } catch (error) {
148
+ setOutcome({ ok: false, text: `${t('failed')}: ${String(error instanceof Error ? error.message : error)}` })
149
+ } finally {
150
+ setBusy(false)
151
+ setConfirmName(null)
152
+ }
153
+ }
154
+
155
+ const readOnly = editor?.mode === 'view'
156
+
157
+ return (
158
+ <div className="dpc-section">
159
+ <style>{CSS}</style>
160
+
161
+ <div className="dpc-head">
162
+ <IconSkillOutline16 aria-hidden="true" />
163
+ <h3>{t('skillsTitle')}</h3>
164
+ <span className="dpc-spacer" />
165
+ <Button variant="primary" size="sm" onClick={openCreate}>{t('newSkill')}</Button>
166
+ </div>
167
+ <p className="dpc-intro">{t('skillsIntro')}</p>
168
+
169
+ {outcome !== null && (
170
+ <div className="dpc-banner" data-kind={outcome.ok ? 'ok' : 'error'} role="status">
171
+ <StateDot state={outcome.ok ? 'done' : 'error'} size={10} />
172
+ <div className="dpc-bannerBody"><span>{outcome.text}</span></div>
173
+ </div>
174
+ )}
175
+
176
+ <div className="dpc-listHead">
177
+ <h3>{t('skillsTab')}</h3>
178
+ {skills !== null && <span className="dpc-count">{skills.length}</span>}
179
+ <span className="dpc-spacer" />
180
+ <button type="button" className="dpc-refresh" aria-label={t('view')} title={t('view')} disabled={busy} onClick={() => setReload((value) => value + 1)}>
181
+ <IconRefreshOutline14 size={14} aria-hidden="true" />
182
+ </button>
183
+ </div>
184
+
185
+ {skills === null && <p className="dpc-empty">{t('loading')}</p>}
186
+ {skills !== null && skills.length === 0 && <p className="dpc-empty">{t('emptySkills')}</p>}
187
+ {skills !== null && skills.length > 0 && (
188
+ <ul className="dpc-cards">
189
+ {skills.map((skill) => (
190
+ <li className="dpc-card" key={`${skill.source}/${skill.name}`}>
191
+ <div className="dpc-cardTop">
192
+ <strong className="dpc-cardTitle" title={skill.name}>{skill.name}</strong>
193
+ <span className="dpc-tag" data-kind="source">{t(SOURCE_KEYS[skill.source] ?? 'sourceCustom')}</span>
194
+ </div>
195
+ <p className="dpc-cardDesc" title={skill.description}>{skill.description}</p>
196
+ <div className="dpc-cardRow">
197
+ {!skill.invocation.modelInvocable && <span className="dpc-tag">⚙</span>}
198
+ {!skill.invocation.userInvocable && <span className="dpc-tag">/</span>}
199
+ <span className="dpc-spacer" />
200
+ <Button variant="ghost" size="sm" disabled={busy} onClick={() => void openExisting(skill)}>
201
+ {skill.editable ? t('edit') : t('view')}
202
+ </Button>
203
+ {skill.editable && (
204
+ <Button variant="ghost" size="sm" disabled={busy} onClick={() => setConfirmName(skill.name)}>{t('delete')}</Button>
205
+ )}
206
+ </div>
207
+ </li>
208
+ ))}
209
+ </ul>
210
+ )}
211
+
212
+ <Modal
213
+ open={editor !== null}
214
+ onClose={() => setEditor(null)}
215
+ title={editor === null ? '' : editor.mode === 'create' ? t('newSkill') : editor.mode === 'edit' ? t('editSkill') : t('viewSkill')}
216
+ >
217
+ {editor !== null && (
218
+ <div className="dpc-form">
219
+ <label className="dpc-label">
220
+ <span>{t('skillName')}</span>
221
+ <input
222
+ className="dpc-input"
223
+ value={editor.name}
224
+ disabled={readOnly || editor.mode === 'edit'}
225
+ onChange={(event) => setEditor({ ...editor, name: event.target.value })}
226
+ />
227
+ </label>
228
+ <label className="dpc-label">
229
+ <span>{t('skillDescription')}</span>
230
+ <input
231
+ className="dpc-input"
232
+ value={editor.description}
233
+ disabled={readOnly}
234
+ onChange={(event) => setEditor({ ...editor, description: event.target.value })}
235
+ />
236
+ </label>
237
+ <label className="dpc-label">
238
+ <span>{t('skillWhenToUse')}</span>
239
+ <input
240
+ className="dpc-input"
241
+ value={editor.whenToUse}
242
+ disabled={readOnly}
243
+ onChange={(event) => setEditor({ ...editor, whenToUse: event.target.value })}
244
+ />
245
+ </label>
246
+ <div className="dpc-checks">
247
+ <label>
248
+ <input
249
+ type="checkbox"
250
+ checked={editor.modelInvocable}
251
+ disabled={readOnly}
252
+ onChange={(event) => setEditor({ ...editor, modelInvocable: event.target.checked })}
253
+ />
254
+ {t('modelInvocable')}
255
+ </label>
256
+ <label>
257
+ <input
258
+ type="checkbox"
259
+ checked={editor.userInvocable}
260
+ disabled={readOnly}
261
+ onChange={(event) => setEditor({ ...editor, userInvocable: event.target.checked })}
262
+ />
263
+ {t('userInvocable')}
264
+ </label>
265
+ </div>
266
+ <label className="dpc-label">
267
+ <span>{t('skillContent')}</span>
268
+ <textarea
269
+ className="dpc-textarea"
270
+ value={editor.content}
271
+ readOnly={readOnly}
272
+ onChange={(event) => setEditor({ ...editor, content: event.target.value })}
273
+ />
274
+ </label>
275
+ {formError !== null && <p className="dpc-formError">{formError}</p>}
276
+ <div className="dpc-cardRow">
277
+ <span className="dpc-spacer" />
278
+ <Button variant="ghost" onClick={() => setEditor(null)}>{readOnly ? t('close') : t('cancel')}</Button>
279
+ {!readOnly && <Button variant="primary" disabled={busy} onClick={() => void doSave()}>{t('save')}</Button>}
280
+ </div>
281
+ </div>
282
+ )}
283
+ </Modal>
284
+
285
+ <Modal
286
+ open={confirmName !== null}
287
+ onClose={() => setConfirmName(null)}
288
+ title={t('confirmDelete')}
289
+ description={confirmName ?? undefined}
290
+ footer={
291
+ <>
292
+ <Button variant="ghost" onClick={() => setConfirmName(null)}>{t('cancel')}</Button>
293
+ <Button variant="primary" disabled={busy} onClick={() => void doDelete()}>{t('delete')}</Button>
294
+ </>
295
+ }
296
+ >
297
+ <p>{t('deleteWarn')}</p>
298
+ </Modal>
299
+ </div>
300
+ )
301
+ }
@@ -0,0 +1,47 @@
1
+ /** Shared scoped stylesheet for the capabilities tabs — rides the host's
2
+ * --dsw-* tokens (same design language as the plugin-inventory and install
3
+ * tabs) so light and dark themes both stay correct. Prefix: dpc-. */
4
+
5
+ export const CSS = `
6
+ .dpc-section{display:flex;flex-direction:column;gap:14px;width:100%;max-width:760px;color:var(--dsw-alias-label-primary)}
7
+ .dpc-head{display:flex;align-items:center;gap:8px}
8
+ .dpc-head h3{margin:0;font-size:13px;line-height:20px;font-weight:600}
9
+ .dpc-head>svg{flex:none;color:var(--dsw-alias-label-tertiary)}
10
+ .dpc-intro{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary)}
11
+ .dpc-listHead{display:flex;align-items:baseline;gap:7px;padding:0 2px;margin-top:2px}
12
+ .dpc-listHead h3{margin:0;font-size:13px;line-height:20px;font-weight:600}
13
+ .dpc-count{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}
14
+ .dpc-spacer{flex:1}
15
+ .dpc-refresh{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer}
16
+ .dpc-refresh:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
17
+ .dpc-refresh:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
18
+ .dpc-empty{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary)}
19
+ .dpc-cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:stretch;gap:10px;margin:0;padding:0;list-style:none}
20
+ .dpc-card{display:flex;flex-direction:column;gap:8px;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-3);padding:12px 14px}
21
+ .dpc-card:hover{background:var(--dsw-alias-interactive-bg-hover)}
22
+ .dpc-cardTop{display:flex;align-items:center;gap:8px}
23
+ .dpc-cardTitle{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;line-height:20px;font-weight:600;font-family:var(--ds-font-family-code)}
24
+ .dpc-cardDesc{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
25
+ .dpc-cardRow{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
26
+ .dpc-tag{display:inline-flex;align-items:center;min-height:20px;border-radius:5px;padding:1px 6px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px;white-space:nowrap}
27
+ .dpc-tag[data-kind='source']{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent);color:var(--dsw-alias-state-business-primary)}
28
+ .dpc-tag[data-kind='off']{background:color-mix(in srgb,var(--dsw-alias-state-warning-primary,var(--dsw-alias-label-tertiary)) 12%,transparent);color:var(--dsw-alias-label-secondary)}
29
+ .dpc-cardActions{display:flex;align-items:center;gap:6px;margin-left:auto}
30
+ .dpc-banner{display:flex;align-items:flex-start;gap:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px 12px;background:var(--dsw-alias-bg-layer-3);font-size:13px;line-height:20px}
31
+ .dpc-banner[data-kind='ok']{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 35%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 8%,transparent)}
32
+ .dpc-banner[data-kind='error']{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 35%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent)}
33
+ .dpc-banner[data-kind='info']{border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 35%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 8%,transparent)}
34
+ .dpc-bannerBody{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
35
+ .dpc-bannerHint{display:flex;align-items:center;gap:8px;flex-wrap:wrap;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
36
+ .dpc-form{display:flex;flex-direction:column;gap:10px}
37
+ .dpc-label{display:flex;flex-direction:column;gap:4px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
38
+ .dpc-label>span:first-child{color:var(--dsw-alias-label-tertiary)}
39
+ .dpc-input,.dpc-textarea,.dpc-select{width:100%;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 10px;outline:none;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}
40
+ .dpc-textarea{min-height:120px;resize:vertical;font-family:var(--ds-font-family-code);line-height:1.5}
41
+ .dpc-textarea[data-short='true']{min-height:64px}
42
+ .dpc-input:focus-visible,.dpc-textarea:focus-visible,.dpc-select:focus-visible{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 2px color-mix(in srgb,var(--dsw-alias-state-business-primary) 18%,transparent)}
43
+ .dpc-checks{display:flex;gap:16px;font-size:13px;line-height:20px}
44
+ .dpc-checks label{display:inline-flex;align-items:center;gap:6px;cursor:pointer}
45
+ .dpc-formError{margin:0;color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}
46
+ @media(max-width:680px){.dpc-cards{grid-template-columns:minmax(0,1fr)}}
47
+ `
@@ -0,0 +1,12 @@
1
+ /** The desktop shell's preload bridge (present only inside DSH Desktop). */
2
+ declare global {
3
+ interface Window {
4
+ dshDesktop?: {
5
+ retry(): void
6
+ openLogs(): void
7
+ restartSidecar(): void
8
+ }
9
+ }
10
+ }
11
+
12
+ export {}
@@ -0,0 +1,108 @@
1
+ /** dsh-plugin-capabilities client entry: contributes the “技能/Skills” and
2
+ * “MCP” tabs into Settings → Plugins. Calls the host routes with fetch. */
3
+
4
+ import { createElement as h } from 'react'
5
+ import { McpTab, type McpInjected, type McpRow } from './McpTab.tsx'
6
+ import { SkillsTab, type SkillsInjected, type SkillRowView } from './SkillsTab.tsx'
7
+ import { zh, en } from './locales.ts'
8
+
9
+ /** Locale dictionary namespace owned by this plugin. */
10
+ export const NS = 'settings.pluginCapabilities'
11
+
12
+ /** The `t` function bound by the locale service. */
13
+ export interface Translate {
14
+ (key: string): string
15
+ }
16
+
17
+ /** Minimal structural subset of the slots service. */
18
+ interface SlotsService {
19
+ inject(slot: string, register: () => unknown): void
20
+ register(meta: Record<string, unknown>, component: () => unknown): unknown
21
+ }
22
+
23
+ /** Minimal structural subset of the locale service. */
24
+ interface LocaleService {
25
+ register(namespace: string, dicts: { zh: Record<string, string>; en: Record<string, string> }): unknown
26
+ bind(namespace: string): Translate
27
+ }
28
+
29
+ /** The client cordis context this plugin relies on (structural). */
30
+ interface CapabilitiesClientContext {
31
+ effect(callback: () => unknown, label?: string): void
32
+ locale: LocaleService
33
+ slots: SlotsService
34
+ }
35
+
36
+ /** Same-origin fetch of the manager's host routes. */
37
+ async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
38
+ const response = await fetch(path, init)
39
+ const body = (await response.json()) as T & { error?: string }
40
+ if (!response.ok) throw new Error(body.error ?? `HTTP ${response.status}`)
41
+ return body
42
+ }
43
+
44
+ const post = (path: string, body: unknown): Promise<unknown> =>
45
+ fetchJson(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
46
+
47
+ export const name = 'dsh-plugin-capabilities'
48
+ export const inject = ['slots', 'locale']
49
+
50
+ export interface SkillBody { content: string }
51
+
52
+ /** One foreign-agent server from the import scan. */
53
+ export interface ImportedServerView {
54
+ agent: 'claude-code' | 'codex'
55
+ name: string
56
+ transport: 'stdio' | 'streamable-http'
57
+ command?: string
58
+ args?: string[]
59
+ env?: Record<string, string>
60
+ url?: string
61
+ headers?: Record<string, string>
62
+ }
63
+
64
+ export function apply(ctx: CapabilitiesClientContext): void {
65
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-plugin-capabilities: dictionaries')
66
+ const t = ctx.locale.bind(NS)
67
+
68
+ const skillsInjected: SkillsInjected = {
69
+ list: () => fetchJson<{ skills: SkillRowView[] }>('/dsh-plugin-capabilities/skills'),
70
+ get: (name: string) => fetchJson<SkillBody>(`/dsh-plugin-capabilities/skill?name=${encodeURIComponent(name)}`),
71
+ save: (input: unknown) => post('/dsh-plugin-capabilities/skill/save', input) as Promise<{ ok: boolean }>,
72
+ remove: (name: string) => post('/dsh-plugin-capabilities/skill/delete', { name }) as Promise<{ ok: boolean }>,
73
+ }
74
+
75
+ const mcpInjected: McpInjected = {
76
+ list: () => fetchJson<{ servers: McpRow[] }>('/dsh-plugin-capabilities/mcp'),
77
+ save: (input: unknown) => post('/dsh-plugin-capabilities/mcp/save', input) as Promise<{ ok: boolean; id: string }>,
78
+ toggle: (id: string, disabled: boolean) => post('/dsh-plugin-capabilities/mcp/toggle', { id, disabled }) as Promise<{ ok: boolean }>,
79
+ remove: (id: string) => post('/dsh-plugin-capabilities/mcp/remove', { id }) as Promise<{ ok: boolean }>,
80
+ scanImport: () => fetchJson<{ servers: ImportedServerView[]; existing: string[] }>('/dsh-plugin-capabilities/import/scan'),
81
+ applyImport: (items: Array<{ agent: string; name: string }>) =>
82
+ post('/dsh-plugin-capabilities/import/apply', { items }) as Promise<{ ok: boolean; results: Array<{ name: string; ok: boolean; error?: string }> }>,
83
+ restart: (): void => { window.dshDesktop?.restartSidecar?.() },
84
+ desktop: window.dshDesktop !== undefined,
85
+ }
86
+
87
+ ctx.slots.inject('settings.plugins.tab', () => {
88
+ return ctx.slots.register({
89
+ name: 'settings.plugins.tab',
90
+ id: 'capabilities-skills',
91
+ order: 30,
92
+ label: () => t('skillsTab'),
93
+ locale: NS,
94
+ inject: () => skillsInjected,
95
+ }, () => h(SkillsTab, { t, injected: skillsInjected }))
96
+ })
97
+
98
+ ctx.slots.inject('settings.plugins.tab', () => {
99
+ return ctx.slots.register({
100
+ name: 'settings.plugins.tab',
101
+ id: 'capabilities-mcp',
102
+ order: 40,
103
+ label: () => t('mcpTab'),
104
+ locale: NS,
105
+ inject: () => mcpInjected,
106
+ }, () => h(McpTab, { t, injected: mcpInjected }))
107
+ })
108
+ }
@@ -0,0 +1,131 @@
1
+ /** zh/en dictionaries for the Settings capabilities tabs. */
2
+
3
+ export const zh = {
4
+ skillsTab: '技能',
5
+ mcpTab: 'MCP',
6
+ skillsTitle: '技能管理',
7
+ skillsIntro: '查看与编辑 dsh 发现的技能;用户级技能(DSH_HOME/skills)可在此新建、修改、删除,文件被监听,保存即生效。若存在 ~/.claude/skills 或 ~/.codex/skills,会自动纳入扫描(零拷贝、实时同步)。',
8
+ newSkill: '新建技能',
9
+ editSkill: '编辑技能',
10
+ viewSkill: '查看技能',
11
+ skillName: '名称(kebab-case)',
12
+ skillDescription: '描述',
13
+ skillWhenToUse: '使用时机(可选)',
14
+ skillContent: '正文(Markdown)',
15
+ modelInvocable: '允许模型调用',
16
+ userInvocable: '允许用户 / 调用',
17
+ save: '保存',
18
+ cancel: '取消',
19
+ delete: '删除',
20
+ edit: '编辑',
21
+ view: '查看',
22
+ close: '关闭',
23
+ emptySkills: '还没有发现任何技能',
24
+ loading: '载入中…',
25
+ source: '来源',
26
+ sourceProjectDsh: '项目',
27
+ sourceProjectAgents: '项目',
28
+ sourceUserDsh: '用户',
29
+ sourceUserAgents: '用户',
30
+ sourceRuntime: '运行时',
31
+ sourceBundled: '内置',
32
+ sourceCustom: '自定义',
33
+ readOnly: '只读',
34
+ confirmDelete: '确认删除技能?',
35
+ deleteWarn: '将删除 DSH_HOME/skills 下的整个技能目录,此操作不可撤销。',
36
+ saved: '已保存,技能目录被监听,稍候即可在会话中使用。',
37
+ mcpTitle: 'MCP 服务器',
38
+ mcpIntro: '管理 profile 中的 MCP 服务器行(@deepseek-ai/dsh-mcp-client)。新增、修改或删除后需要重启 dsh 才生效。',
39
+ addServer: '添加服务器',
40
+ editServer: '编辑服务器',
41
+ serverName: '服务器名(工具名前缀)',
42
+ transport: '传输方式',
43
+ transportStdio: 'stdio(本地命令)',
44
+ transportHttp: 'streamable-http(URL)',
45
+ command: '命令',
46
+ args: '参数(每行一个)',
47
+ envPairs: '环境变量(每行 KEY=VALUE)',
48
+ url: 'URL',
49
+ headersPairs: '请求头(每行 KEY: VALUE)',
50
+ disabled: '已停用',
51
+ enabled: '启用中',
52
+ toggle: '停用/启用',
53
+ confirmRemove: '确认移除该服务器?',
54
+ removeWarn: '将从 profile 配置中移除这一行,重启 dsh 后其工具不再出现。',
55
+ emptyMcp: '还没有配置 MCP 服务器',
56
+ importServers: '从其他 Agent 导入',
57
+ importIntro: '扫描 Claude Code(~/.claude.json)与 Codex(~/.codex/config.toml)的 MCP 服务器配置,勾选后导入为本 profile 的服务器行。',
58
+ importEmpty: '没有发现可导入的 MCP 服务器',
59
+ importExisting: '已存在',
60
+ importSelected: '导入选中项',
61
+ restartNeeded: '配置已写入,重启 dsh 后生效。',
62
+ restartDesktopHint: '重启由桌面应用负责:托盘菜单「重启服务」。',
63
+ restartOtherHint: '重启方式:关闭当前 dsh 进程后重新运行。',
64
+ restartNow: '重启服务',
65
+ failed: '操作失败',
66
+ }
67
+
68
+ export const en = {
69
+ skillsTab: 'Skills',
70
+ mcpTab: 'MCP',
71
+ skillsTitle: 'Skills',
72
+ skillsIntro: 'View and edit the skills dsh discovers; user-level skills (DSH_HOME/skills) can be created, edited, and deleted here — the directory is watched, saves apply without a restart. ~/.claude/skills and ~/.codex/skills are scanned too when present (zero-copy, live-synced).',
73
+ newSkill: 'New skill',
74
+ editSkill: 'Edit skill',
75
+ viewSkill: 'View skill',
76
+ skillName: 'Name (kebab-case)',
77
+ skillDescription: 'Description',
78
+ skillWhenToUse: 'When to use (optional)',
79
+ skillContent: 'Body (Markdown)',
80
+ modelInvocable: 'Model-invocable',
81
+ userInvocable: 'User-invocable (/)',
82
+ save: 'Save',
83
+ cancel: 'Cancel',
84
+ delete: 'Delete',
85
+ edit: 'Edit',
86
+ view: 'View',
87
+ close: 'Close',
88
+ emptySkills: 'No skills discovered yet',
89
+ loading: 'Loading…',
90
+ source: 'Source',
91
+ sourceProjectDsh: 'project',
92
+ sourceProjectAgents: 'project',
93
+ sourceUserDsh: 'user',
94
+ sourceUserAgents: 'user',
95
+ sourceRuntime: 'runtime',
96
+ sourceBundled: 'bundled',
97
+ sourceCustom: 'custom',
98
+ readOnly: 'read-only',
99
+ confirmDelete: 'Delete this skill?',
100
+ deleteWarn: 'Removes the whole skill directory under DSH_HOME/skills. This cannot be undone.',
101
+ saved: 'Saved. The skills directory is watched; the skill is usable in sessions shortly.',
102
+ mcpTitle: 'MCP servers',
103
+ mcpIntro: 'Manage the MCP server rows (@deepseek-ai/dsh-mcp-client) in this profile. Additions, edits, and removals take effect after a dsh restart.',
104
+ addServer: 'Add server',
105
+ editServer: 'Edit server',
106
+ serverName: 'Server name (tool name prefix)',
107
+ transport: 'Transport',
108
+ transportStdio: 'stdio (local command)',
109
+ transportHttp: 'streamable-http (URL)',
110
+ command: 'Command',
111
+ args: 'Arguments (one per line)',
112
+ envPairs: 'Environment (KEY=VALUE per line)',
113
+ url: 'URL',
114
+ headersPairs: 'Headers (KEY: VALUE per line)',
115
+ disabled: 'Disabled',
116
+ enabled: 'Enabled',
117
+ toggle: 'Enable/disable',
118
+ confirmRemove: 'Remove this server?',
119
+ removeWarn: 'Removes the row from the profile configuration; its tools disappear after the next dsh restart.',
120
+ emptyMcp: 'No MCP servers configured yet',
121
+ importServers: 'Import from other agents',
122
+ importIntro: 'Scans Claude Code (~/.claude.json) and Codex (~/.codex/config.toml) MCP server configs; selected entries become server rows in this profile.',
123
+ importEmpty: 'No importable MCP servers found',
124
+ importExisting: 'already here',
125
+ importSelected: 'Import selected',
126
+ restartNeeded: 'Configuration written — restart dsh to apply.',
127
+ restartDesktopHint: 'The desktop app owns restarts: use the tray “Restart service”.',
128
+ restartOtherHint: 'Restart by closing this dsh process and running it again.',
129
+ restartNow: 'Restart service',
130
+ failed: 'Operation failed',
131
+ }
@@ -0,0 +1,57 @@
1
+ /** Minimal ambient types for the primitives the browser half uses, matching
2
+ * @deepseek-ai/dsh-client-ui-primitives (provided by the host's frozen
3
+ * platform module table; never bundled). Only members used here are
4
+ * declared — keep in sync with the host package. */
5
+
6
+ declare module '@deepseek-ai/dsh-client-ui-primitives' {
7
+ import type { ButtonHTMLAttributes, InputHTMLAttributes, ReactElement, ReactNode, SVGProps } from 'react'
8
+
9
+ export type ButtonVariant = 'primary' | 'ghost' | 'outline' | 'toolbar'
10
+ export function Button(props: {
11
+ variant?: ButtonVariant
12
+ size?: 'md' | 'sm'
13
+ icon?: ReactNode
14
+ className?: string | undefined
15
+ children?: ReactNode
16
+ } & ButtonHTMLAttributes<HTMLButtonElement>): ReactElement
17
+
18
+ export function Input(props: {
19
+ icon?: ReactNode
20
+ className?: string
21
+ } & InputHTMLAttributes<HTMLInputElement>): ReactElement
22
+
23
+ export function Modal(props: {
24
+ open: boolean
25
+ onClose: () => void
26
+ title: string
27
+ closeLabel?: string
28
+ description?: string
29
+ children?: ReactNode
30
+ footer?: ReactNode
31
+ className?: string
32
+ contentClassName?: string
33
+ headless?: boolean
34
+ }): ReactElement | null
35
+
36
+ export type StateDotState = 'done' | 'warning' | 'ongoing' | 'error'
37
+ export function StateDot(props: {
38
+ state: StateDotState
39
+ size?: number | undefined
40
+ className?: string | undefined
41
+ }): ReactElement
42
+
43
+ export function IconRefreshOutline14(props: {
44
+ className?: string | undefined
45
+ size?: number | undefined
46
+ } & SVGProps<SVGSVGElement>): ReactElement
47
+
48
+ export function IconSkillOutline16(props: {
49
+ className?: string | undefined
50
+ size?: number | undefined
51
+ } & SVGProps<SVGSVGElement>): ReactElement
52
+
53
+ export function IconApiOutline14(props: {
54
+ className?: string | undefined
55
+ size?: number | undefined
56
+ } & SVGProps<SVGSVGElement>): ReactElement
57
+ }