dsh-skill-hub 0.2.0 → 0.2.2
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/CONTRIBUTING.md +1 -1
- package/README.md +181 -249
- package/README.zh.md +275 -0
- package/lib/client.js +395 -257
- package/lib/client.js.map +1 -1
- package/lib/index.js +126 -26
- package/lib/types/client/SkillHubSettingsCard.d.ts +23 -17
- package/lib/types/client/api.d.ts +9 -3
- package/lib/types/client/grouping.d.ts +3 -0
- package/lib/types/client/index.d.ts +12 -6
- package/lib/types/client/locales.d.ts +8 -4
- package/lib/types/client/market-catalog.d.ts +1 -1
- package/lib/types/client/panel/MarketView.d.ts +5 -5
- package/lib/types/client/panel/SkillHubPanel.d.ts +1 -1
- package/lib/types/client/panel/SourcesView.d.ts +5 -3
- package/lib/types/client/panel/useSkillHub.d.ts +4 -0
- package/lib/types/client/settings-card.d.ts +5 -4
- package/lib/types/index.d.ts +12 -1
- package/lib/types/protocol.d.ts +23 -0
- package/lib/types/skillfs.d.ts +1 -1
- package/lib/types/update.d.ts +1 -1
- package/package.json +28 -27
- package/src/client/SkillHubSettingsCard.tsx +25 -14
- package/src/client/api.ts +9 -6
- package/src/client/grouping.test.ts +12 -0
- package/src/client/grouping.ts +10 -1
- package/src/client/index.tsx +25 -19
- package/src/client/locales.ts +16 -8
- package/src/client/market-catalog.ts +1 -1
- package/src/client/panel/MarketView.tsx +77 -64
- package/src/client/panel/SkillHubPanel.tsx +18 -1
- package/src/client/panel/SkillRow.tsx +17 -2
- package/src/client/panel/SourcesView.tsx +92 -5
- package/src/client/panel/panel.module.css +9 -1
- package/src/client/panel/useSkillHub.ts +25 -11
- package/src/client/settings-card.tsx +5 -4
- package/src/index.ts +78 -31
- package/src/protocol.ts +24 -0
- package/src/routes.test.ts +55 -0
- package/src/routes.ts +90 -10
- package/src/skillfs.ts +1 -1
- package/src/update.ts +1 -1
- package/lib/types/client/api-config-scope.d.ts +0 -48
- package/src/client/api-config-scope.test.ts +0 -79
- package/src/client/api-config-scope.ts +0 -119
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-skill-hub",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "In-GUI skill hub for DeepSeek Harness (dsh): browse the full local skill catalog from the official ctx.skills registry (every root + third-party providers), toggle skills on/off, inspect bodies, surface frontmatter diagnostics, and scaffold new skills — plus a codex-style skill market (built-in catalog, upstream update checks, one-click update-all) with tracked source sync. The full manager beyond the read-only dsh-skill-manager browser.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -43,36 +43,36 @@
|
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
46
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.
|
|
47
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.
|
|
48
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.
|
|
49
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.
|
|
50
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.
|
|
51
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.
|
|
52
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.
|
|
53
|
-
"@deepseek-ai/dsh-session": "^0.1.0-rc.
|
|
54
|
-
"@deepseek-ai/dsh-session-query": "^0.1.0-rc.
|
|
55
|
-
"@deepseek-ai/dsh-settings": "^0.1.0-rc.
|
|
56
|
-
"@deepseek-ai/dsh-skill": "^0.1.0-rc.
|
|
57
|
-
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.
|
|
46
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.7",
|
|
47
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
|
|
48
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
|
|
49
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
|
|
50
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.7",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
|
|
52
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.7",
|
|
53
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.7",
|
|
54
|
+
"@deepseek-ai/dsh-session-query": "^0.1.0-rc.7",
|
|
55
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
|
|
56
|
+
"@deepseek-ai/dsh-skill": "^0.1.0-rc.7",
|
|
57
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.7",
|
|
58
58
|
"react": "^18.2.0",
|
|
59
59
|
"react-dom": "^18.2.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
63
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.
|
|
64
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.
|
|
65
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.
|
|
66
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.
|
|
67
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.
|
|
68
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.0-rc.
|
|
69
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.
|
|
70
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.
|
|
71
|
-
"@deepseek-ai/dsh-session": "^0.1.0-rc.
|
|
72
|
-
"@deepseek-ai/dsh-session-query": "^0.1.0-rc.
|
|
73
|
-
"@deepseek-ai/dsh-settings": "^0.1.0-rc.
|
|
74
|
-
"@deepseek-ai/dsh-skill": "^0.1.0-rc.
|
|
75
|
-
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.
|
|
63
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.7",
|
|
64
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
|
|
65
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.7",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.0-rc.7",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
|
|
70
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.7",
|
|
71
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.7",
|
|
72
|
+
"@deepseek-ai/dsh-session-query": "^0.1.0-rc.7",
|
|
73
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
|
|
74
|
+
"@deepseek-ai/dsh-skill": "^0.1.0-rc.7",
|
|
75
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.7",
|
|
76
76
|
"@types/js-yaml": "^4.0.9",
|
|
77
77
|
"@types/node": "^22.20.0",
|
|
78
78
|
"@types/react": "~18.3.1",
|
|
@@ -93,6 +93,7 @@
|
|
|
93
93
|
"src",
|
|
94
94
|
"cordis.patch.yml",
|
|
95
95
|
"README.md",
|
|
96
|
+
"README.zh.md",
|
|
96
97
|
"LICENSE",
|
|
97
98
|
"CONTRIBUTING.md"
|
|
98
99
|
],
|
|
@@ -124,4 +125,4 @@
|
|
|
124
125
|
"url": "https://github.com/cheshireez/dsh-skill-hub/issues"
|
|
125
126
|
},
|
|
126
127
|
"homepage": "https://github.com/cheshireez/dsh-skill-hub"
|
|
127
|
-
}
|
|
128
|
+
}
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The dsh-skill-hub plugin settings card: bridges the hub's
|
|
3
|
-
* (
|
|
4
|
-
* form (enabled master switch + agent announcement).
|
|
5
|
-
* official `settings.plugin.item` slot
|
|
6
|
-
*
|
|
7
|
-
* service (the host refuses third-party namespaces).
|
|
2
|
+
* The dsh-skill-hub plugin settings card: bridges the hub's settings
|
|
3
|
+
* namespace (bound through the official settings transport) onto the
|
|
4
|
+
* family-style staged card form (enabled master switch + agent announcement).
|
|
5
|
+
* Registered into the official `settings.plugin.item` slot keyed by that
|
|
6
|
+
* namespace, so the plugin shows up in Settings → 插件 on dsh rc.7+.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
import type { ReactElement } from 'react'
|
|
11
10
|
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
|
11
|
+
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
12
12
|
import { ColorField, PluginSettingsCard, SwitchField } from './settings-card.tsx'
|
|
13
13
|
import { booleanField, CardForm, colorField, type CardShell, type FieldState, type FormScope } from './settings-form.ts'
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
/** Model-invocable dot color default. Single source for the TS side; the
|
|
16
|
+
* panel's CSS mirrors it via --hub-model (panel.module.css). */
|
|
17
|
+
export const DEFAULT_DOT_MODEL_COLOR = '#2f81f7'
|
|
18
|
+
/** User-invocable dot color default. Single source for the TS side; the
|
|
19
|
+
* panel's CSS mirrors it via --hub-user (panel.module.css). */
|
|
20
|
+
export const DEFAULT_DOT_USER_COLOR = '#3fb950'
|
|
15
21
|
|
|
16
22
|
/** The card's projected state. */
|
|
17
23
|
export interface SkillHubSettingsState extends CardShell {
|
|
@@ -24,22 +30,28 @@ export interface SkillHubSettingsState extends CardShell {
|
|
|
24
30
|
showGroupSummary: FieldState
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
/**
|
|
28
|
-
export interface
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
/** The business face the card's slot registration injects. */
|
|
34
|
+
export interface SkillHubSettingsCardFace {
|
|
35
|
+
hooks: {
|
|
36
|
+
/** The card's snapshot store (projected form state). */
|
|
37
|
+
skillHubSettingsCard: SnapshotStore<SkillHubSettingsState>
|
|
38
|
+
}
|
|
31
39
|
save: () => void
|
|
32
40
|
discard: () => void
|
|
33
41
|
edit: (field: string, text: string) => void
|
|
34
42
|
resetField: (field: string) => void
|
|
35
43
|
}
|
|
36
44
|
|
|
45
|
+
/** Props the slot renderer binds (locale copy + injected form actions). */
|
|
46
|
+
export type SkillHubSettingsCardProps =
|
|
47
|
+
PropsRuntime<'settings.plugin.item'> & PropsLocale<'dsh-skill-hub'> & InjectFace<SkillHubSettingsCardFace>
|
|
48
|
+
|
|
37
49
|
/** Bridges the hub's config scope onto the card's staged form. */
|
|
38
50
|
export class SkillHubSettingsCardController {
|
|
39
51
|
private readonly form: CardForm
|
|
40
52
|
private readonly store: SnapshotStore<SkillHubSettingsState>
|
|
41
53
|
|
|
42
|
-
/** @param scope - the hub
|
|
54
|
+
/** @param scope - the hub settings scope the card edits (FormScope-compatible). */
|
|
43
55
|
constructor(scope: FormScope) {
|
|
44
56
|
this.form = new CardForm(scope, [booleanField('enabled'), booleanField('announceToAgent'), colorField('dotModelColor'), colorField('dotUserColor'), booleanField('showUseCount'), booleanField('showUseTime'), booleanField('showGroupSummary')])
|
|
45
57
|
this.store = this.form.bind(() => this.projection())
|
|
@@ -62,7 +74,7 @@ export class SkillHubSettingsCardController {
|
|
|
62
74
|
* Build the face the card's slot registration injects.
|
|
63
75
|
* @returns the card's snapshot hook and its form actions.
|
|
64
76
|
*/
|
|
65
|
-
inject():
|
|
77
|
+
inject(): SkillHubSettingsCardFace {
|
|
66
78
|
return {
|
|
67
79
|
hooks: { skillHubSettingsCard: this.store },
|
|
68
80
|
...this.form.actions(),
|
|
@@ -158,4 +170,3 @@ export function SkillHubSettingsCard(props: SkillHubSettingsCardProps): ReactEle
|
|
|
158
170
|
</PluginSettingsCard>
|
|
159
171
|
)
|
|
160
172
|
}
|
|
161
|
-
|
package/src/client/api.ts
CHANGED
|
@@ -80,13 +80,16 @@ async function fetchWithTimeout(input: RequestInfo | URL, init?: RequestInit, ms
|
|
|
80
80
|
|
|
81
81
|
/** The browser half's only data entry point. */
|
|
82
82
|
export class SkillHubApi {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
/** Catalog lookup options (cwd selects a workspace's project skills). */
|
|
84
|
+
catalog(options?: { cwd?: string }): Promise<CatalogResponse> {
|
|
85
|
+
const url = options?.cwd !== undefined && options.cwd !== '' ? SKILL_HUB_API.catalog + '?cwd=' + encodeURIComponent(options.cwd) : SKILL_HUB_API.catalog
|
|
86
|
+
return fetchWithTimeout(url).then((response) => readJson<CatalogResponse>(response))
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
/** One skill's detail (cwd selects a workspace's project skills). */
|
|
90
|
+
async skill(name: string, options?: { cwd?: string }): Promise<SkillDetail> {
|
|
91
|
+
const cwd = options?.cwd !== undefined && options.cwd !== '' ? '&cwd=' + encodeURIComponent(options.cwd) : ''
|
|
92
|
+
const url = SKILL_HUB_API.skill + '?name=' + encodeURIComponent(name) + cwd
|
|
90
93
|
const response = await fetchWithTimeout(url)
|
|
91
94
|
const body = await readJson<SkillDetailResponse>(response)
|
|
92
95
|
return body.skill
|
|
@@ -138,7 +141,7 @@ export class SkillHubApi {
|
|
|
138
141
|
return readJson<ToggleBatchResponse>(response)
|
|
139
142
|
}
|
|
140
143
|
|
|
141
|
-
/** The user's added market sources
|
|
144
|
+
/** The user's added market sources. */
|
|
142
145
|
async market(): Promise<MarketSourcesResponse> {
|
|
143
146
|
const response = await fetchWithTimeout(SKILL_HUB_API.market)
|
|
144
147
|
return readJson<MarketSourcesResponse>(response)
|
|
@@ -8,6 +8,7 @@ function skill(name: string, writable = true): CatalogSkill {
|
|
|
8
8
|
description: '',
|
|
9
9
|
invocation: { modelInvocable: true, userInvocable: true },
|
|
10
10
|
provider: 'filesystem',
|
|
11
|
+
source: 'user-dsh',
|
|
11
12
|
writable,
|
|
12
13
|
}
|
|
13
14
|
}
|
|
@@ -61,6 +62,17 @@ describe('filterBySource', () => {
|
|
|
61
62
|
expect(filterBySource(skills, PRIVATE_SOURCE, origins).map((s) => s.name)).toEqual(['c'])
|
|
62
63
|
expect(filterBySource(skills, 'all', origins)).toHaveLength(3)
|
|
63
64
|
})
|
|
65
|
+
|
|
66
|
+
it('never buckets project skills as private (they belong to the project tree)', () => {
|
|
67
|
+
const origins = {}
|
|
68
|
+
const proj = skill('proj')
|
|
69
|
+
proj.source = 'project-dsh'
|
|
70
|
+
const projAgents = skill('proj-agents')
|
|
71
|
+
projAgents.source = 'project-agents'
|
|
72
|
+
const skills = [skill('personal'), proj, projAgents]
|
|
73
|
+
expect(filterBySource(skills, PRIVATE_SOURCE, origins).map((s) => s.name)).toEqual(['personal'])
|
|
74
|
+
expect(filterBySource(skills, 'all', origins)).toHaveLength(3)
|
|
75
|
+
})
|
|
64
76
|
})
|
|
65
77
|
|
|
66
78
|
describe('sortSkills', () => {
|
package/src/client/grouping.ts
CHANGED
|
@@ -69,15 +69,24 @@ export function conflictsOnClose(
|
|
|
69
69
|
/** Origin-repo filter value: skills with no source record (private skills). */
|
|
70
70
|
export const PRIVATE_SOURCE = 'private'
|
|
71
71
|
|
|
72
|
+
/** 项目级技能来源(它们有 workspace 归属,不属于「个人」组)。 */
|
|
73
|
+
export function isProjectSource(source: string): boolean {
|
|
74
|
+
return source === 'project-dsh' || source === 'project-agents'
|
|
75
|
+
}
|
|
76
|
+
|
|
72
77
|
/**
|
|
73
78
|
* Apply the origin filter ('all' or a specific origin repo; skills without a
|
|
74
79
|
* source record count as PRIVATE_SOURCE). The origins map is the store's
|
|
75
80
|
* skillName → repo derivation, so filtering follows the tracked source
|
|
76
81
|
* records instead of the filesystem root a skill happens to live under.
|
|
82
|
+
* 项目级技能(有 workspace 归属)永远不算「个人」。
|
|
77
83
|
*/
|
|
78
84
|
export function filterBySource(skills: readonly CatalogSkill[], source: string, origins: Readonly<Record<string, string>>): CatalogSkill[] {
|
|
79
85
|
if (source === 'all') return [...skills]
|
|
80
|
-
return skills.filter((skill) =>
|
|
86
|
+
return skills.filter((skill) => {
|
|
87
|
+
if (isProjectSource(skill.source)) return false
|
|
88
|
+
return (origins[skill.name] ?? PRIVATE_SOURCE) === source
|
|
89
|
+
})
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
/** Catalog sort keys offered by the filter bar. */
|
package/src/client/index.tsx
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* Registers the dsh-skill-hub locale dictionaries and mounts two Settings
|
|
6
6
|
* surfaces, both through official slots (no DOM injection):
|
|
7
7
|
* - a plugin-management card in the `settings.plugin.item` slot (Settings →
|
|
8
|
-
* 插件 → 可配置插件列表),
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* 插件 → 可配置插件列表), keyed by the hub's settings namespace and bound
|
|
9
|
+
* through the official settings transport (dsh rc.7 serves every
|
|
10
|
+
* registered namespace to the web client, and the tab dispatches cards by
|
|
11
|
+
* namespace) — the family-bucket card pattern (PluginSettingsCard +
|
|
12
|
+
* CardForm vendored from dsh-task-board);
|
|
13
13
|
* - a top-level Settings section (Settings → 技能) hosting the skill hub
|
|
14
14
|
* panel: catalog, search, enable/disable, diagnostics, new-skill form.
|
|
15
15
|
*
|
|
@@ -31,8 +31,8 @@ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
|
31
31
|
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
32
32
|
// Type-only: pulls the settings-plugins SlotMap merge (settings.plugin.item).
|
|
33
33
|
import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
|
|
34
|
+
import type { HubSettingsValue } from '../protocol.ts'
|
|
34
35
|
import { SkillHubApi } from './api.ts'
|
|
35
|
-
import { ApiConfigScope } from './api-config-scope.ts'
|
|
36
36
|
import { en, zh, type HubKey } from './locales.ts'
|
|
37
37
|
import { applySettingsNavIcon } from './settings-nav-icon.ts'
|
|
38
38
|
import { SkillHubSettingsCard, SkillHubSettingsCardController } from './SkillHubSettingsCard.tsx'
|
|
@@ -48,8 +48,14 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Required services (fiber inject waiting — the runtime must be up first).
|
|
53
|
+
* `connection`/`remote` are the settings transport's own prerequisites
|
|
54
|
+
* (`ctx.settingsScope.bind` resolves them on the caller's fiber), and
|
|
55
|
+
* `settingsScope` is the namespace-scope binder itself; mirror the official
|
|
56
|
+
* settings-plugins inject list.
|
|
57
|
+
*/
|
|
58
|
+
export const inject = ['slots', 'locale', 'connection', 'remote', 'settingsScope']
|
|
53
59
|
|
|
54
60
|
/** Type-only surface (export discipline: no value exports beyond the plugin contract). */
|
|
55
61
|
export type { SkillHubPanelProps } from './panel/SkillHubPanel.tsx'
|
|
@@ -65,24 +71,24 @@ export function apply(ctx: ClientContext): void {
|
|
|
65
71
|
const t = ctx.locale.bind(NS)
|
|
66
72
|
const api = new SkillHubApi()
|
|
67
73
|
|
|
68
|
-
// The card edits the hub's
|
|
69
|
-
// settings
|
|
70
|
-
//
|
|
71
|
-
|
|
74
|
+
// The card edits the hub's settings namespace through the official
|
|
75
|
+
// settings transport — the configurable-plugins tab only dispatches cards
|
|
76
|
+
// whose key the Host serves, and the Host serves every registered namespace
|
|
77
|
+
// since rc.7, so this is what makes the card appear (and stay in sync).
|
|
78
|
+
const settingsCard = new SkillHubSettingsCardController(
|
|
79
|
+
ctx.settingsScope.bind<HubSettingsValue>({ namespace: NS }),
|
|
80
|
+
)
|
|
72
81
|
|
|
73
82
|
// Plugin-management card: Settings → 插件 → 可配置插件列表.
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
// declare it, so the typed register overload rejects the option. Cast the
|
|
77
|
-
// call (runtime-identical to the official cards) and keep the card fully typed.
|
|
83
|
+
// rc.7's slot contract declares this keyed slot with options `key`
|
|
84
|
+
// (the settings namespace the card edits), so registration is fully typed.
|
|
78
85
|
ctx.effect(
|
|
79
86
|
() => ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
|
|
80
87
|
name: 'settings.plugin.item',
|
|
81
|
-
|
|
82
|
-
order: 115,
|
|
88
|
+
key: NS,
|
|
83
89
|
locale: NS,
|
|
84
90
|
inject: () => settingsCard.inject(),
|
|
85
|
-
}
|
|
91
|
+
}, SkillHubSettingsCard)),
|
|
86
92
|
'dsh-skill-hub: settings card',
|
|
87
93
|
)
|
|
88
94
|
|
package/src/client/locales.ts
CHANGED
|
@@ -27,19 +27,19 @@ export const zh = {
|
|
|
27
27
|
'panel.emptyAll': '本地还没有技能。点击「新建技能」创建第一个。',
|
|
28
28
|
'panel.loading': '读取中…',
|
|
29
29
|
'panel.incomplete': '(目录可能不完整)',
|
|
30
|
-
'
|
|
30
|
+
'panel.workspacePlaceholder': '工作区路径(回车应用)',
|
|
31
|
+
'panel.workspaceHint': '填写项目路径后,面板同时显示该项目 .dsh/skills 与 .agents/skills 下的技能(只读);留空回车恢复用户级视图。',
|
|
32
|
+
'panel.workspaceClear': '清除工作区',
|
|
31
33
|
'market.addPlaceholder': 'owner/repo 或 https://github.com/…',
|
|
32
34
|
'market.addSource': '添加源',
|
|
33
35
|
'market.noSources': '还没有市场源。从下方内置市场添加,或输入一个 GitHub 仓库。',
|
|
34
|
-
'market.
|
|
36
|
+
'market.title': '市场',
|
|
35
37
|
'market.catalogHint': '精选技能仓库,添加后即可扫描安装,并持续检查更新。',
|
|
36
38
|
'market.catalog.anthropics': 'Anthropic 官方技能合集(Claude Skills)',
|
|
37
39
|
'market.catalog.superpowers': 'Superpowers 社区技能合集',
|
|
38
40
|
'market.catalog.mattpocock': 'Matt Pocock 技能集',
|
|
39
41
|
'market.catalog.openDesign': '设计技能合集(design-templates)',
|
|
40
|
-
'market.added': '已添加',
|
|
41
42
|
'market.add': '添加',
|
|
42
|
-
'market.mySources': '我的市场源',
|
|
43
43
|
'market.installed': '已装 {count}',
|
|
44
44
|
'market.updatable': '可更新 {count}',
|
|
45
45
|
'market.deletedUpstream': '上游已删 {count}',
|
|
@@ -104,6 +104,9 @@ export const zh = {
|
|
|
104
104
|
'groups.empty': '还没有场景。先新建一个,再把技能归类。',
|
|
105
105
|
'groups.noCollections': '暂无来源组。从市场导入技能后自动按来源聚合。',
|
|
106
106
|
'groups.personal': '个人',
|
|
107
|
+
'groups.project': '项目级',
|
|
108
|
+
'groups.subdivide': '细分',
|
|
109
|
+
'groups.merge': '合并',
|
|
107
110
|
'groups.noWritable': '该组没有可写技能(只读技能无法由中枢开关)',
|
|
108
111
|
'groups.closeAll': '全部关闭',
|
|
109
112
|
'groups.keepOn': '保留开启',
|
|
@@ -130,6 +133,7 @@ export const zh = {
|
|
|
130
133
|
'row.disable': '禁用',
|
|
131
134
|
'row.enable': '启用',
|
|
132
135
|
'row.delete': '移入回收站',
|
|
136
|
+
'row.open': '查看 {name} 详情(回车)',
|
|
133
137
|
'delete.confirmTitle': '移入回收站?',
|
|
134
138
|
'delete.confirmText': '确定把「{name}」移入回收站吗?可随时恢复。',
|
|
135
139
|
'delete.confirm': '移入回收站',
|
|
@@ -232,19 +236,19 @@ export const en: Record<HubKey, string> = {
|
|
|
232
236
|
'panel.emptyAll': 'No skills installed yet. Create the first one.',
|
|
233
237
|
'panel.loading': 'Loading…',
|
|
234
238
|
'panel.incomplete': '(catalog may be incomplete)',
|
|
235
|
-
'
|
|
239
|
+
'panel.workspacePlaceholder': 'Workspace path (Enter to apply)',
|
|
240
|
+
'panel.workspaceHint': 'Enter a project path to also show that workspace\'s .dsh/skills and .agents/skills skills (read-only); clear + Enter to return to the user-level view.',
|
|
241
|
+
'panel.workspaceClear': 'Clear workspace',
|
|
236
242
|
'market.addPlaceholder': 'owner/repo or https://github.com/…',
|
|
237
243
|
'market.addSource': 'Add source',
|
|
238
244
|
'market.noSources': 'No market sources yet. Add one from the built-in catalog below, or enter a GitHub repo.',
|
|
239
|
-
'market.
|
|
245
|
+
'market.title': 'Market',
|
|
240
246
|
'market.catalogHint': 'Curated skill repos — add one to scan and install, with ongoing update checks.',
|
|
241
247
|
'market.catalog.anthropics': 'Official Anthropic skill collection (Claude Skills)',
|
|
242
248
|
'market.catalog.superpowers': 'Superpowers community skill collection',
|
|
243
249
|
'market.catalog.mattpocock': 'Matt Pocock skill collection',
|
|
244
250
|
'market.catalog.openDesign': 'Design skill collection (design-templates)',
|
|
245
|
-
'market.added': 'Added',
|
|
246
251
|
'market.add': 'Add',
|
|
247
|
-
'market.mySources': 'My sources',
|
|
248
252
|
'market.installed': '{count} installed',
|
|
249
253
|
'market.updatable': '{count} updatable',
|
|
250
254
|
'market.deletedUpstream': '{count} deleted upstream',
|
|
@@ -309,6 +313,9 @@ export const en: Record<HubKey, string> = {
|
|
|
309
313
|
'groups.empty': 'No scenes yet. Create one and start categorizing skills.',
|
|
310
314
|
'groups.noCollections': 'No source groups yet. Import skills from the market to group them by source.',
|
|
311
315
|
'groups.personal': 'Personal',
|
|
316
|
+
'groups.project': 'Project',
|
|
317
|
+
'groups.subdivide': 'Split',
|
|
318
|
+
'groups.merge': 'Merge',
|
|
312
319
|
'groups.noWritable': 'No writable skills in this group (read-only skills cannot be switched from the hub)',
|
|
313
320
|
'groups.closeAll': 'Close all',
|
|
314
321
|
'groups.keepOn': 'Keep on',
|
|
@@ -335,6 +342,7 @@ export const en: Record<HubKey, string> = {
|
|
|
335
342
|
'row.disable': 'Disable',
|
|
336
343
|
'row.enable': 'Enable',
|
|
337
344
|
'row.delete': 'Move to trash',
|
|
345
|
+
'row.open': 'View {name} details (Enter)',
|
|
338
346
|
'delete.confirmTitle': 'Move to trash?',
|
|
339
347
|
'delete.confirmText': 'Move "{name}" to trash? It can be restored anytime.',
|
|
340
348
|
'delete.confirm': 'Move to trash',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in market catalog: well-known skill repos shown on the market tab so
|
|
3
|
-
* users can browse and install without knowing repo URLs
|
|
3
|
+
* users can browse and install without knowing repo URLs.
|
|
4
4
|
* Only the repo slug lives here; adding goes through the normal
|
|
5
5
|
* market-source route and records upstream tracking like any manual source.
|
|
6
6
|
*/
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Market tab:
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* the importable
|
|
2
|
+
* Market tab: one unified market list — built-in catalog entries (add
|
|
3
|
+
* button while not yet added) and the user's market sources (full state
|
|
4
|
+
* badges + actions once added, plus custom sources), with check-all and
|
|
5
|
+
* update-all actions and the repo scan result with the importable checklist.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type
|
|
8
|
+
import { Fragment, type JSX } from 'react'
|
|
9
9
|
import { tt } from '../helpers.ts'
|
|
10
10
|
import { MARKET_CATALOG } from '../market-catalog.ts'
|
|
11
|
+
import type { MarketSourceRecord } from '../../protocol.ts'
|
|
11
12
|
import { ConfirmDialog } from './dialogs.tsx'
|
|
12
13
|
import type { SkillHubState } from './useSkillHub.ts'
|
|
13
14
|
import css from './panel.module.css'
|
|
@@ -24,33 +25,79 @@ export function MarketView(props: { hub: SkillHubState }): JSX.Element {
|
|
|
24
25
|
const { marketState, marketCheck, sourceCheck, sourcesState, repoDiscoverState, scanningRepo, repoSelected, setRepoSelected, repoImporting, repoResult, newSourceName, setNewSourceName, syncingMarket, tagBusy, addSource, addMarketSource, removeMarketSource, scanRepo, checkMarket, checkSources, syncMarketSource, toggleRepoSelected, importRepo, updateAllDialog, setUpdateAllDialog, updateAll } = hub
|
|
25
26
|
/** 有可更新技能的来源(全部更新按钮与确认列表共用)。 */
|
|
26
27
|
const updatableRepos = Object.entries(sourceCheck).filter(([, check]) => check.changed && check.updated.length > 0)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
28
|
+
|
|
29
|
+
/** 一个已添加来源的完整行:状态徽章 + 操作按钮。 */
|
|
30
|
+
const sourceRow = (record: MarketSourceRecord): JSX.Element => {
|
|
31
|
+
const releaseCheck = marketCheck[record.repo]
|
|
32
|
+
const skillCheck = sourceCheck[record.repo]
|
|
33
|
+
const installedCount = sourcesState?.sources.find((source) => source.repo === record.repo)?.skills.length ?? 0
|
|
34
|
+
const scanning = scanningRepo === record.repo
|
|
35
|
+
return (
|
|
36
|
+
<div className={css.row + ' ' + css.rowStatic}>
|
|
37
|
+
<div className={css.rowMain}>
|
|
38
|
+
<div className={css.rowName}>
|
|
39
|
+
<a className={css.sourceLink} href={'https://github.com/' + record.repo} target='_blank' rel='noreferrer'>{record.repo}</a>
|
|
40
|
+
{record.ref !== undefined ? <span className={css.badge + ' ' + css.badgeSource}>{record.ref}</span> : null}
|
|
41
|
+
{installedCount > 0 ? <span className={css.badge + ' ' + css.badgeCount}>{tt('market.installed', { count: installedCount })}</span> : null}
|
|
42
|
+
{skillCheck?.changed === true && skillCheck.updated.length > 0
|
|
43
|
+
? <span className={css.badge + ' ' + css.statusUpdated}>{tt('market.updatable', { count: skillCheck.updated.length })}</span>
|
|
44
|
+
: null}
|
|
45
|
+
{skillCheck !== undefined && skillCheck.deleted.length > 0
|
|
46
|
+
? <span className={css.badge + ' ' + css.statusError}>{tt('market.deletedUpstream', { count: skillCheck.deleted.length })}</span>
|
|
47
|
+
: null}
|
|
48
|
+
{releaseCheck?.updateAvailable === true
|
|
49
|
+
? <span className={css.badge + ' ' + css.statusUpdated}>{releaseCheck.latestTag !== undefined ? tt('market.newRelease', { version: releaseCheck.latestTag }) : tt('market.updated')}</span>
|
|
50
|
+
: null}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<button type='button' className={css.opBtn} disabled={scanning} onClick={() => { void checkMarket() }}>{tt('market.check')}</button>
|
|
54
|
+
<button type='button' className={css.opBtn + (releaseCheck?.updateAvailable === true ? ' ' + css.opDanger : '')} disabled={syncingMarket === record.repo} onClick={() => { void syncMarketSource(record.repo) }}>
|
|
55
|
+
{syncingMarket === record.repo ? tt('market.syncing') : tt('market.sync')}
|
|
56
|
+
</button>
|
|
57
|
+
<button type='button' className={css.opBtn} disabled={scanning} onClick={() => { void scanRepo(record.repo) }}>
|
|
58
|
+
{scanning ? tt('market.scanning') : tt('market.scan')}
|
|
59
|
+
</button>
|
|
60
|
+
<button type='button' className={css.opBtn} disabled={tagBusy} title={tt('market.removeHint')} onClick={() => { void removeMarketSource(record.repo) }}>{tt('market.deleteSource')}</button>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 统一列表:内置目录(未添加 → 描述+添加按钮;已添加 → 完整行)在前,
|
|
66
|
+
// 自定义市场源(不在内置目录里)随后。同一个仓库不会出现两次。
|
|
67
|
+
const rows: Array<{ key: string; element: JSX.Element }> = []
|
|
68
|
+
for (const entry of MARKET_CATALOG) {
|
|
69
|
+
const record = marketState.repos.find((item) => item.repo === entry.repo)
|
|
70
|
+
if (record !== undefined) {
|
|
71
|
+
rows.push({ key: entry.repo, element: sourceRow(record) })
|
|
72
|
+
} else {
|
|
73
|
+
const busy = scanningRepo !== null || tagBusy
|
|
74
|
+
rows.push({
|
|
75
|
+
key: entry.repo,
|
|
76
|
+
element: (
|
|
77
|
+
<div className={css.row + ' ' + css.rowStatic}>
|
|
78
|
+
<div className={css.rowMain}>
|
|
79
|
+
<div className={css.rowName}>
|
|
80
|
+
<a className={css.sourceLink} href={'https://github.com/' + entry.repo} target='_blank' rel='noreferrer'>{entry.repo}</a>
|
|
42
81
|
</div>
|
|
43
|
-
{
|
|
44
|
-
? <span className={css.badge + ' ' + css.badgeReadonly}>{tt('market.added')}</span>
|
|
45
|
-
: <button type='button' className={css.opBtn} disabled={busy} onClick={() => { void addSource(entry.repo) }}>{tt('market.add')}</button>}
|
|
82
|
+
<div className={css.rowDesc}>{tt(entry.descriptionKey)}</div>
|
|
46
83
|
</div>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
84
|
+
<button type='button' className={css.opBtn} disabled={busy} onClick={() => { void addSource(entry.repo) }}>{tt('market.add')}</button>
|
|
85
|
+
</div>
|
|
86
|
+
),
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const record of marketState.repos) {
|
|
91
|
+
if (MARKET_CATALOG.some((entry) => entry.repo === record.repo)) continue
|
|
92
|
+
rows.push({ key: record.repo, element: sourceRow(record) })
|
|
93
|
+
}
|
|
94
|
+
const unaddedCatalog = MARKET_CATALOG.filter((entry) => !marketState.repos.some((record) => record.repo === entry.repo))
|
|
50
95
|
|
|
96
|
+
return (
|
|
97
|
+
<>
|
|
51
98
|
<section className={css.section}>
|
|
52
99
|
<div className={css.sectionTitle + ' ' + css.sectionHeadRow}>
|
|
53
|
-
<span className={css.sectionTitleFill}>{tt('market.
|
|
100
|
+
<span className={css.sectionTitleFill}>{tt('market.title')}</span>
|
|
54
101
|
<button type='button' className={css.opBtn} onClick={() => { void checkSources(); void checkMarket() }}>{tt('market.checkAll')}</button>
|
|
55
102
|
<button
|
|
56
103
|
type='button'
|
|
@@ -59,7 +106,6 @@ export function MarketView(props: { hub: SkillHubState }): JSX.Element {
|
|
|
59
106
|
onClick={() => { setUpdateAllDialog(true) }}
|
|
60
107
|
>{tt('market.updateAll')}</button>
|
|
61
108
|
</div>
|
|
62
|
-
<p className={css.hintLine + ' ' + css.hintPadded}>{tt('market.addHint')}</p>
|
|
63
109
|
<div className={css.buttons + ' ' + css.actionsPadded}>
|
|
64
110
|
<input
|
|
65
111
|
className={css.input + ' ' + css.grow}
|
|
@@ -70,44 +116,11 @@ export function MarketView(props: { hub: SkillHubState }): JSX.Element {
|
|
|
70
116
|
/>
|
|
71
117
|
<button type='button' className={css.button + ' ' + css.primary} disabled={newSourceName.trim() === ''} onClick={() => { void addMarketSource() }}>{tt('market.addSource')}</button>
|
|
72
118
|
</div>
|
|
119
|
+
{unaddedCatalog.length > 0 ? <p className={css.hintLine + ' ' + css.hintPadded}>{tt('market.catalogHint')}</p> : null}
|
|
73
120
|
|
|
74
121
|
{marketState.status === 'loading' ? <div className={css.empty}>{tt('panel.loading')}</div> : null}
|
|
75
|
-
|
|
76
|
-
{marketState.status
|
|
77
|
-
{marketState.repos.map((record) => {
|
|
78
|
-
const releaseCheck = marketCheck[record.repo]
|
|
79
|
-
const skillCheck = sourceCheck[record.repo]
|
|
80
|
-
const installedCount = sourcesState?.sources.find((source) => source.repo === record.repo)?.skills.length ?? 0
|
|
81
|
-
const scanning = scanningRepo === record.repo
|
|
82
|
-
return (
|
|
83
|
-
<div key={record.repo} className={css.row + ' ' + css.rowStatic}>
|
|
84
|
-
<div className={css.rowMain}>
|
|
85
|
-
<div className={css.rowName}>
|
|
86
|
-
<a className={css.sourceLink} href={'https://github.com/' + record.repo} target='_blank' rel='noreferrer'>{record.repo}</a>
|
|
87
|
-
{record.ref !== undefined ? <span className={css.badge + ' ' + css.badgeSource}>{record.ref}</span> : null}
|
|
88
|
-
{installedCount > 0 ? <span className={css.badge + ' ' + css.badgeCount}>{tt('market.installed', { count: installedCount })}</span> : null}
|
|
89
|
-
{skillCheck?.changed === true && skillCheck.updated.length > 0
|
|
90
|
-
? <span className={css.badge + ' ' + css.statusUpdated}>{tt('market.updatable', { count: skillCheck.updated.length })}</span>
|
|
91
|
-
: null}
|
|
92
|
-
{skillCheck !== undefined && skillCheck.deleted.length > 0
|
|
93
|
-
? <span className={css.badge + ' ' + css.statusError}>{tt('market.deletedUpstream', { count: skillCheck.deleted.length })}</span>
|
|
94
|
-
: null}
|
|
95
|
-
{releaseCheck?.updateAvailable === true
|
|
96
|
-
? <span className={css.badge + ' ' + css.statusUpdated}>{releaseCheck.latestTag !== undefined ? tt('market.newRelease', { version: releaseCheck.latestTag }) : tt('market.updated')}</span>
|
|
97
|
-
: null}
|
|
98
|
-
</div>
|
|
99
|
-
</div>
|
|
100
|
-
<button type='button' className={css.opBtn} disabled={scanning} onClick={() => { void checkMarket() }}>{tt('market.check')}</button>
|
|
101
|
-
<button type='button' className={css.opBtn + (releaseCheck?.updateAvailable === true ? ' ' + css.opDanger : '')} disabled={syncingMarket === record.repo} onClick={() => { void syncMarketSource(record.repo) }}>
|
|
102
|
-
{syncingMarket === record.repo ? tt('market.syncing') : tt('market.sync')}
|
|
103
|
-
</button>
|
|
104
|
-
<button type='button' className={css.opBtn} disabled={scanning} onClick={() => { void scanRepo(record.repo) }}>
|
|
105
|
-
{scanning ? tt('market.scanning') : tt('market.scan')}
|
|
106
|
-
</button>
|
|
107
|
-
<button type='button' className={css.opBtn} disabled={tagBusy} title={tt('market.removeHint')} onClick={() => { void removeMarketSource(record.repo) }}>{tt('market.deleteSource')}</button>
|
|
108
|
-
</div>
|
|
109
|
-
)
|
|
110
|
-
})}
|
|
122
|
+
{marketState.status !== 'loading' && rows.length === 0 ? <div className={css.empty}>{tt('market.noSources')}</div> : null}
|
|
123
|
+
{marketState.status !== 'loading' ? rows.map((row) => <Fragment key={row.key}>{row.element}</Fragment>) : null}
|
|
111
124
|
</section>
|
|
112
125
|
{repoDiscoverState.status === 'scanning' ? <div className={css.empty}>{tt('market.scanning')}</div> : null}
|
|
113
126
|
{repoDiscoverState.status === 'error' ? <div className={css.errorBanner}>{repoDiscoverState.message}</div> : null}
|