dsh-skill-hub 0.3.13 → 0.3.15
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 +6 -4
- package/README.md +9 -5
- package/README.zh.md +9 -5
- package/lib/client.js +55 -58
- package/lib/client.js.map +1 -1
- package/lib/index.js +529 -315
- package/lib/types/client/SkillHubSettingsCard.d.ts +5 -3
- package/lib/types/client/icons.d.ts +4 -5
- package/lib/types/client/index.d.ts +13 -12
- package/lib/types/client/locales/market.d.ts +1 -0
- package/lib/types/client/locales.d.ts +1 -0
- package/lib/types/client/settings-form.d.ts +9 -3
- package/lib/types/client/slash-dots.d.ts +20 -11
- package/lib/types/index.d.ts +59 -24
- package/lib/types/protocol/api.d.ts +22 -2
- package/lib/types/protocol/config.d.ts +30 -4
- package/lib/types/protocol/market.d.ts +1 -1
- package/lib/types/protocol/repo.d.ts +18 -1
- package/lib/types/protocol.d.ts +3 -3
- package/lib/types/repo/discovery.d.ts +59 -3
- package/lib/types/repo/github-client.d.ts +16 -0
- package/lib/types/repo.d.ts +6 -3
- package/lib/types/routes/helpers.d.ts +8 -1
- package/lib/types/routes/market.d.ts +2 -2
- package/lib/types/routes.d.ts +1 -1
- package/lib/types/store/store.d.ts +4 -0
- package/package.json +31 -41
- package/src/client/SkillHubSettingsCard.tsx +5 -3
- package/src/client/icons.tsx +4 -5
- package/src/client/index.tsx +33 -32
- package/src/client/locales/market.ts +2 -0
- package/src/client/panel/RepoScanCard.tsx +6 -3
- package/src/client/panel/SkillHubPanel.tsx +1 -1
- package/src/client/panel/SourcesView.tsx +1 -1
- package/src/client/panel/hooks/useGroupFlow.ts +1 -8
- package/src/client/settings-form.ts +9 -4
- package/src/client/slash-dots.test.ts +19 -36
- package/src/client/slash-dots.tsx +30 -60
- package/src/index.ts +145 -82
- package/src/protocol/api.ts +24 -2
- package/src/protocol/config.ts +35 -4
- package/src/protocol/market.ts +1 -1
- package/src/protocol/repo.ts +22 -1
- package/src/protocol.ts +3 -3
- package/src/repo/discovery.ts +120 -33
- package/src/repo/github-client.ts +18 -1
- package/src/repo/install.ts +8 -7
- package/src/repo.test.ts +158 -4
- package/src/repo.ts +10 -2
- package/src/routes/config.ts +19 -2
- package/src/routes/helpers.ts +9 -2
- package/src/routes/market.ts +49 -37
- package/src/routes/repo-import.ts +3 -2
- package/src/routes/sources.ts +2 -1
- package/src/routes.test.ts +107 -1
- package/src/routes.ts +21 -3
- package/src/store/migrate.ts +3 -2
- package/src/store/store.ts +7 -2
- package/src/store.test.ts +50 -0
- package/src/update.ts +4 -2
|
@@ -97,6 +97,10 @@ export declare class SkillHubStore {
|
|
|
97
97
|
* When `dir` is given, every baseline path under that skill directory is
|
|
98
98
|
* dropped first, so files the upstream removed never linger in the
|
|
99
99
|
* baseline and skew later update diffs.
|
|
100
|
+
*
|
|
101
|
+
* `dir` may be '' — a skill whose SKILL.md sits at the repo root. Its prefix
|
|
102
|
+
* is empty, so the whole baseline is replaced: that skill owns the tree, and
|
|
103
|
+
* keeping stale paths would make every later diff report "changed" forever.
|
|
100
104
|
*/
|
|
101
105
|
mergeSourceManifest(repo: string, manifest: Record<string, number>, dir?: string): Promise<void>;
|
|
102
106
|
/** skillName → collection name for every recorded origin (derived from sources). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-skill-hub",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
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": {
|
|
@@ -61,19 +61,7 @@
|
|
|
61
61
|
],
|
|
62
62
|
"compatibility": {
|
|
63
63
|
"dshVersions": [
|
|
64
|
-
"0.1.
|
|
65
|
-
"0.1.1-rc.2",
|
|
66
|
-
"0.1.2-alpha.2",
|
|
67
|
-
"0.1.2-alpha.3",
|
|
68
|
-
"0.1.2-alpha.4",
|
|
69
|
-
"0.1.2-alpha.5",
|
|
70
|
-
"0.1.2-rc.1",
|
|
71
|
-
"0.1.3-alpha.2",
|
|
72
|
-
"0.1.5-alpha.1",
|
|
73
|
-
"0.1.5-alpha.2",
|
|
74
|
-
"0.1.5-rc.1",
|
|
75
|
-
"0.1.5-rc.2",
|
|
76
|
-
"0.1.6-alpha.1"
|
|
64
|
+
"0.1.7-alpha.1"
|
|
77
65
|
]
|
|
78
66
|
},
|
|
79
67
|
"capability": {
|
|
@@ -95,19 +83,20 @@
|
|
|
95
83
|
},
|
|
96
84
|
"peerDependencies": {
|
|
97
85
|
"@deepseek-ai/cordis": "^4.0.1 || ^4.0.2",
|
|
98
|
-
"@deepseek-ai/dsh-client-connection": ">=0.1.
|
|
99
|
-
"@deepseek-ai/dsh-client-locale": ">=0.1.
|
|
100
|
-
"@deepseek-ai/dsh-client-runtime": ">=0.1.
|
|
101
|
-
"@deepseek-ai/dsh-client-store": ">=0.1.
|
|
102
|
-
"@deepseek-ai/dsh-client-ui-input-trigger": ">=0.1.
|
|
103
|
-
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.
|
|
104
|
-
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.
|
|
105
|
-
"@deepseek-ai/dsh-host-webserver": ">=0.1.
|
|
106
|
-
"@deepseek-ai/dsh-session": ">=0.1.
|
|
107
|
-
"@deepseek-ai/dsh-session-query": ">=0.1.
|
|
108
|
-
"@deepseek-ai/dsh-settings": ">=0.1.
|
|
109
|
-
"@deepseek-ai/dsh-skill": ">=0.1.
|
|
110
|
-
"@deepseek-ai/dsh-system-prompt": ">=0.1.
|
|
86
|
+
"@deepseek-ai/dsh-client-connection": ">=0.1.7-alpha.1 <0.2.0-0",
|
|
87
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.7-alpha.1 <0.2.0-0",
|
|
88
|
+
"@deepseek-ai/dsh-client-runtime": ">=0.1.7-alpha.1 <0.2.0-0",
|
|
89
|
+
"@deepseek-ai/dsh-client-store": ">=0.1.7-alpha.1 <0.2.0-0",
|
|
90
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": ">=0.1.7-alpha.1 <0.2.0-0",
|
|
91
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-alpha.1 <0.2.0-0",
|
|
92
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.7-alpha.1 <0.2.0-0",
|
|
93
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.7-alpha.1 <0.2.0-0",
|
|
94
|
+
"@deepseek-ai/dsh-session": ">=0.1.7-alpha.1 <0.2.0-0",
|
|
95
|
+
"@deepseek-ai/dsh-session-query": ">=0.1.7-alpha.1 <0.2.0-0",
|
|
96
|
+
"@deepseek-ai/dsh-settings": ">=0.1.7-alpha.1 <0.2.0-0",
|
|
97
|
+
"@deepseek-ai/dsh-skill": ">=0.1.7-alpha.1 <0.2.0-0",
|
|
98
|
+
"@deepseek-ai/dsh-system-prompt": ">=0.1.7-alpha.1 <0.2.0-0",
|
|
99
|
+
"@deepseek-ai/schemastery": "^3.18.3",
|
|
111
100
|
"react": "^18.2.0",
|
|
112
101
|
"react-dom": "^18.2.0"
|
|
113
102
|
},
|
|
@@ -121,19 +110,21 @@
|
|
|
121
110
|
},
|
|
122
111
|
"devDependencies": {
|
|
123
112
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
124
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.
|
|
125
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.
|
|
126
|
-
"@deepseek-ai/dsh-client-store": "^0.1.
|
|
127
|
-
"@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.
|
|
128
|
-
"@deepseek-ai/dsh-client-ui-
|
|
129
|
-
"@deepseek-ai/dsh-client-ui-settings
|
|
130
|
-
"@deepseek-ai/dsh-client-ui-
|
|
131
|
-
"@deepseek-ai/dsh-
|
|
132
|
-
"@deepseek-ai/dsh-
|
|
133
|
-
"@deepseek-ai/dsh-session
|
|
134
|
-
"@deepseek-ai/dsh-
|
|
135
|
-
"@deepseek-ai/dsh-
|
|
136
|
-
"@deepseek-ai/dsh-
|
|
113
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.7-alpha.1",
|
|
114
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.7-alpha.1",
|
|
115
|
+
"@deepseek-ai/dsh-client-store": "^0.1.7-alpha.1",
|
|
116
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.7-alpha.1",
|
|
117
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "^0.1.7-alpha.1",
|
|
118
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.7-alpha.1",
|
|
119
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.7-alpha.1",
|
|
120
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.7-alpha.1",
|
|
121
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.7-alpha.1",
|
|
122
|
+
"@deepseek-ai/dsh-session": "^0.1.7-alpha.1",
|
|
123
|
+
"@deepseek-ai/dsh-session-query": "^0.1.7-alpha.1",
|
|
124
|
+
"@deepseek-ai/dsh-settings": "^0.1.7-alpha.1",
|
|
125
|
+
"@deepseek-ai/dsh-skill": "^0.1.7-alpha.1",
|
|
126
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.7-alpha.1",
|
|
127
|
+
"@deepseek-ai/schemastery": "^3.18.3",
|
|
137
128
|
"@types/js-yaml": "^4.0.9",
|
|
138
129
|
"@types/node": "^22.20.0",
|
|
139
130
|
"@types/react": "~18.3.1",
|
|
@@ -142,7 +133,6 @@
|
|
|
142
133
|
"lightningcss": "1.32.0",
|
|
143
134
|
"react": "^18.3.1",
|
|
144
135
|
"react-dom": "^18.3.1",
|
|
145
|
-
"schemastery": "^3.18.0",
|
|
146
136
|
"tsdown": "0.22.2",
|
|
147
137
|
"typescript": "~5.7.2",
|
|
148
138
|
"vitest": "^3.0.0",
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* The dsh-skill-hub plugin settings card: bridges the hub's settings
|
|
3
3
|
* namespace (bound through the official settings transport) onto the
|
|
4
4
|
* family-style staged card form (enabled master switch + agent announcement).
|
|
5
|
-
* Registered into
|
|
6
|
-
*
|
|
5
|
+
* Registered into `plugins.bundle.config` under the bundle's package name, so
|
|
6
|
+
* the Plugins manager renders it on dsh-skill-hub's own page. That slot is
|
|
7
|
+
* dispatched with `view: 'page'` only, so the card always renders its full
|
|
8
|
+
* form (no `summary` one-liner variant).
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import { useEffect, useState, type ReactElement } from 'react'
|
|
@@ -54,7 +56,7 @@ export interface SkillHubSettingsCardFace {
|
|
|
54
56
|
|
|
55
57
|
/** Props the slot renderer binds (locale copy + injected form actions). */
|
|
56
58
|
export type SkillHubSettingsCardProps =
|
|
57
|
-
PropsRuntime<'
|
|
59
|
+
PropsRuntime<'plugins.bundle.config'> & PropsLocale<'dsh-skill-hub'> & InjectFace<SkillHubSettingsCardFace>
|
|
58
60
|
|
|
59
61
|
/** Bridges the hub's config scope onto the card's staged form. */
|
|
60
62
|
export class SkillHubSettingsCardController {
|
package/src/client/icons.tsx
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
* Vendored UI icons for dsh-skill-hub.
|
|
3
3
|
*
|
|
4
4
|
* These were originally imported from `@deepseek-ai/dsh-client-ui-primitives`.
|
|
5
|
-
* That package is
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* older and newer dsh hosts.
|
|
5
|
+
* That package is not exposed as a standalone plugin module by the dsh web
|
|
6
|
+
* build this plugin targets — dsh keeps a static compatibility module instead.
|
|
7
|
+
* Vendoring the few tiny outline icons the hub uses keeps the browser half
|
|
8
|
+
* self-contained and independent of how that module is packaged.
|
|
10
9
|
*
|
|
11
10
|
* SVG paths are copied verbatim from dsh-client-ui-primitives (MIT licensed)
|
|
12
11
|
* so the visuals remain pixel-identical to the dsh icon family.
|
package/src/client/index.tsx
CHANGED
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
* Browser-half entry for the dsh-skill-hub plugin — runs inside the dsh
|
|
3
3
|
* web GUI.
|
|
4
4
|
*
|
|
5
|
-
* Registers the dsh-skill-hub locale dictionaries and mounts
|
|
6
|
-
* surfaces, both through official slots (no DOM injection):
|
|
7
|
-
* - a plugin-management card in the `settings.plugin.item` slot (Settings →
|
|
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);
|
|
5
|
+
* Registers the dsh-skill-hub locale dictionaries and mounts:
|
|
13
6
|
* - a top-level Settings section (Settings → 技能) hosting the skill hub
|
|
14
|
-
* panel: catalog, search, enable/disable, diagnostics, new-skill form
|
|
7
|
+
* panel: catalog, search, enable/disable, diagnostics, new-skill form;
|
|
8
|
+
* - the chat "/" menu skill dots, colored from the same config the panel reads.
|
|
9
|
+
*
|
|
10
|
+
* - a configuration card in the `plugins.bundle.config` slot, keyed by the
|
|
11
|
+
* BUNDLE PACKAGE NAME, rendered on the plugin's own page in the Plugins
|
|
12
|
+
* manager (sidebar → 插件 → dsh-skill-hub). dsh 0.1.7 has no auto-generated
|
|
13
|
+
* config page: the manager only renders that section for bundles that
|
|
14
|
+
* register this slot, and the card writes through the shared config form
|
|
15
|
+
* (`ctx.configForms.get(skill-hub)`), the same form the host routes read.
|
|
15
16
|
*
|
|
16
17
|
* Failure policy: mounting problems are logged, never thrown — the web
|
|
17
18
|
* shell fails the whole boot when a plugin apply throws, and an external
|
|
@@ -25,24 +26,25 @@
|
|
|
25
26
|
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
26
27
|
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
|
27
28
|
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
28
|
-
// Type-only: pulls the
|
|
29
|
+
// Type-only: pulls the configForms service merge and the settings.section slot.
|
|
29
30
|
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
30
31
|
// Type-only: pulls the LocaleNamespaceMap merge table.
|
|
31
32
|
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
32
|
-
// Type-only: pulls the settings-plugins SlotMap merge (settings.plugin.item).
|
|
33
|
-
import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
|
|
34
33
|
// Type-only: pulls the Context merge for ctx.inputTriggers (slash-dots wiring).
|
|
35
34
|
import type {} from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
|
35
|
+
// Type-only: pulls the plugin-manager SlotMap merge (plugins.bundle.config).
|
|
36
|
+
import type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'
|
|
36
37
|
// Type-only: pulls the connection/reset event.
|
|
37
38
|
import type {} from '@deepseek-ai/dsh-client-connection/client'
|
|
38
39
|
|
|
39
|
-
//
|
|
40
|
+
// The slots service is owned by ui-renderer, which this build does not depend
|
|
41
|
+
// on; augment the Context locally so the browser half typechecks.
|
|
40
42
|
declare module '@deepseek-ai/cordis' {
|
|
41
43
|
interface Context {
|
|
42
44
|
slots: any
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
|
-
import type
|
|
47
|
+
import { HUB_ENTRY_ID, type HubSettingsValue } from '../protocol.ts'
|
|
46
48
|
import { SkillHubApi } from './api.ts'
|
|
47
49
|
import { en, zh, type HubKey } from './locales.ts'
|
|
48
50
|
import { applySettingsNavIcon } from './settings-nav-icon.ts'
|
|
@@ -63,11 +65,11 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
63
65
|
/**
|
|
64
66
|
* Required services (fiber inject waiting — the runtime must be up first).
|
|
65
67
|
* `connection`/`remote` are the settings transport's own prerequisites
|
|
66
|
-
* (`ctx.
|
|
67
|
-
* `
|
|
68
|
-
* settings-plugins inject list.
|
|
68
|
+
* (`ctx.configForms.get` resolves them on the caller's fiber), and
|
|
69
|
+
* `configForms` is the shared settings-form service itself; mirror the
|
|
70
|
+
* official settings-plugins inject list.
|
|
69
71
|
*/
|
|
70
|
-
export const inject = ['slots', 'locale', 'connection', 'remote', '
|
|
72
|
+
export const inject = ['slots', 'locale', 'connection', 'remote', 'configForms', 'inputTriggers']
|
|
71
73
|
|
|
72
74
|
/** Type-only surface (export discipline: no value exports beyond the plugin contract). */
|
|
73
75
|
export type { SkillHubPanelProps } from './panel/SkillHubPanel.tsx'
|
|
@@ -83,30 +85,29 @@ export function apply(ctx: ClientContext): void {
|
|
|
83
85
|
const t = ctx.locale.bind(NS)
|
|
84
86
|
const api = new SkillHubApi()
|
|
85
87
|
|
|
86
|
-
// The
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
// Single scope instance reused for both the card and slash-dots to avoid
|
|
91
|
-
// duplicate subscriptions (review #3).
|
|
92
|
-
const scope = ctx.settingsScope.bind<HubSettingsValue>({ namespace: NS })
|
|
88
|
+
// The hub's config form is the same Loader entry the host writes and the
|
|
89
|
+
// Plugins manager renders; the browser half reads it only to color the dots
|
|
90
|
+
// (single form instance, so slash-dots keeps one subscription).
|
|
91
|
+
const scope = ctx.configForms.get<HubSettingsValue>(HUB_ENTRY_ID)
|
|
93
92
|
const settingsCard = new SkillHubSettingsCardController(scope)
|
|
94
93
|
|
|
95
94
|
// Chat `/` 菜单技能圆点:为每个候选行加可调用性圆点(蓝=模型可调,绿=仅用户),颜色与面板图例同步;仅装饰,不自动预填 "/"
|
|
96
95
|
// inject 含 inputTriggers 保证 fiber 就绪后再 wrap,slash-dots 内的 undefined 防御仅用于单元测试 mock
|
|
97
96
|
ctx.effect(() => setupSkillSlashDots(ctx, api, scope), 'dsh-skill-hub: slash dots')
|
|
98
97
|
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
// (
|
|
98
|
+
// The plugin's own configuration page. It must be registered here: the
|
|
99
|
+
// Plugins manager only renders the config section for bundles that appear in
|
|
100
|
+
// `plugins.bundle.config` (`ledger.bundles.has(openPkg.name)`), and its key is
|
|
101
|
+
// the BUNDLE PACKAGE NAME — not the settings entry id (`skill-hub`). There is
|
|
102
|
+
// no auto-generated fallback page in dsh 0.1.7.
|
|
102
103
|
ctx.effect(
|
|
103
|
-
() => ctx.slots.inject('
|
|
104
|
-
name: '
|
|
105
|
-
key:
|
|
104
|
+
() => ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
105
|
+
name: 'plugins.bundle.config',
|
|
106
|
+
key: 'dsh-skill-hub',
|
|
106
107
|
locale: NS,
|
|
107
108
|
inject: () => settingsCard.inject(),
|
|
108
109
|
}, SkillHubSettingsCard)),
|
|
109
|
-
'dsh-skill-hub:
|
|
110
|
+
'dsh-skill-hub: plugin config',
|
|
110
111
|
)
|
|
111
112
|
|
|
112
113
|
// Top-level Settings section: the skill management page.
|
|
@@ -74,6 +74,7 @@ export const zhMarket = {
|
|
|
74
74
|
'repo.cancel': '取消',
|
|
75
75
|
'repo.cancelled': '已取消 · 已导入 {imported}/{total} 已保留,临时文件已清理',
|
|
76
76
|
'repo.importingCurrent': '正在导入: {name} ({done}/{total})',
|
|
77
|
+
'repo.root.self': '仓库根',
|
|
77
78
|
'repo.root.skills': 'skills',
|
|
78
79
|
'repo.root.designTemplates': 'design-templates',
|
|
79
80
|
'repo.root.generic': '{root}',
|
|
@@ -154,6 +155,7 @@ export const enMarket: Record<keyof typeof zhMarket, string> = {
|
|
|
154
155
|
'repo.cancel': 'Cancel',
|
|
155
156
|
'repo.cancelled': 'Cancelled · {imported}/{total} kept, temp files cleaned',
|
|
156
157
|
'repo.importingCurrent': 'Importing: {name} ({done}/{total})',
|
|
158
|
+
'repo.root.self': 'Repo root',
|
|
157
159
|
'repo.root.skills': 'Skills',
|
|
158
160
|
'repo.root.designTemplates': 'Templates',
|
|
159
161
|
'repo.root.generic': '{root}',
|
|
@@ -14,8 +14,11 @@ import css from './panel.module.css'
|
|
|
14
14
|
/**
|
|
15
15
|
* 已知技能根的显示名键;未知根直接显示目录名。
|
|
16
16
|
* zh 字典里这两项就是目录名本身(与接线前的硬编码文本一致)。
|
|
17
|
+
* 空串是「技能就在仓库根」的哨兵根(host 侧 REPO_ROOT),目录名无从显示,
|
|
18
|
+
* 给它一个可读标签,否则筛选按钮会是一片空白。
|
|
17
19
|
*/
|
|
18
|
-
function rootLabelKey(root: string): 'repo.root.skills' | 'repo.root.designTemplates' | 'repo.root.generic' {
|
|
20
|
+
function rootLabelKey(root: string): 'repo.root.self' | 'repo.root.skills' | 'repo.root.designTemplates' | 'repo.root.generic' {
|
|
21
|
+
if (root === '') return 'repo.root.self'
|
|
19
22
|
if (root === 'skills') return 'repo.root.skills'
|
|
20
23
|
if (root === 'design-templates') return 'repo.root.designTemplates'
|
|
21
24
|
return 'repo.root.generic'
|
|
@@ -94,7 +97,7 @@ export function RepoScanCard(props: { hub: SkillHubState }): JSX.Element | null
|
|
|
94
97
|
/>
|
|
95
98
|
<button type='button' className={css.button + (repoFilter === 'all' ? ' ' + css.primary : '')} style={{ padding:'6px 10px', fontSize:12 }} onClick={() => setRepoFilter('all')}>{tt('repo.allRoots', { count: entries.length })}</button>
|
|
96
99
|
{roots.map((root) => (
|
|
97
|
-
<button key={root} type='button' className={css.button + (repoFilter === root ? ' ' + css.primary : '')} style={{ padding:'6px 10px', fontSize:12 }} onClick={() => setRepoFilter(root)}>{tt(rootLabelKey(root), { root })} {counts.get(root) ?? 0}</button>
|
|
100
|
+
<button key={root === '' ? 'repo-root' : root} type='button' className={css.button + (repoFilter === root ? ' ' + css.primary : '')} style={{ padding:'6px 10px', fontSize:12 }} onClick={() => setRepoFilter(root)}>{tt(rootLabelKey(root), { root })} {counts.get(root) ?? 0}</button>
|
|
98
101
|
))}
|
|
99
102
|
</div>
|
|
100
103
|
)
|
|
@@ -122,7 +125,7 @@ export function RepoScanCard(props: { hub: SkillHubState }): JSX.Element | null
|
|
|
122
125
|
/>
|
|
123
126
|
<div className={css.rowMain}>
|
|
124
127
|
<div className={css.rowName}>{entry.name}</div>
|
|
125
|
-
<div className={css.rowDesc}>{entry.dir} · {tt('repo.files', { count: entry.fileCount, size: formatBytes(entry.totalBytes) })}</div>
|
|
128
|
+
<div className={css.rowDesc}>{entry.dir === '' ? tt('repo.root.self') : entry.dir} · {tt('repo.files', { count: entry.fileCount, size: formatBytes(entry.totalBytes) })}</div>
|
|
126
129
|
</div>
|
|
127
130
|
{existing
|
|
128
131
|
? <span className={css.badge + ' ' + css.badgeReadonly}>{tt('repo.existing')}</span>
|
|
@@ -43,7 +43,7 @@ export function SkillHubPanel(props: SkillHubPanelProps): React.JSX.Element {
|
|
|
43
43
|
catalog, loading, loadError, successBanner, updateState, detail, detailLoading, showForm, formName, formDesc,
|
|
44
44
|
formRoot, formBusy, formMessage, hubConfig, tab, skillView, sourceFilter, sortKey, search,
|
|
45
45
|
workspace, setWorkspace,
|
|
46
|
-
sourcesState, tagBusy, batchBusy,
|
|
46
|
+
sourcesState, tagBusy, batchBusy, sourceOptions, filtered,
|
|
47
47
|
conflictDialog, confirmDialog, deleteSkillDialog, deleteGroupDialog, confirmClearTrash, branchChoice, branchBusy, marketSyncDialog,
|
|
48
48
|
syncBusy, editingTag, editName, membersDraft, editSearch, uses, groupsState, sourceCheck, checkingSource, syncingSource,
|
|
49
49
|
showLegend, editMode,
|
|
@@ -38,7 +38,7 @@ export function SourcesView(props: { hub: SkillHubState }): JSX.Element {
|
|
|
38
38
|
const uncategorized = sourceFiltered.filter((skill) => origins[skill.name] === undefined && !isProjectSource(skill.source))
|
|
39
39
|
const personalDisabled = (catalog?.disabled ?? []).filter((record) => origins[record.name] === undefined)
|
|
40
40
|
.filter((record) => normalized.length === 0 || record.name.toLocaleLowerCase().includes(normalized) || record.description.toLocaleLowerCase().includes(normalized))
|
|
41
|
-
.filter((
|
|
41
|
+
.filter(() => sourceFilter === 'all' || sourceFilter === PRIVATE_SOURCE)
|
|
42
42
|
const allPersonalNames = [...uncategorized.map((s) => s.name), ...personalDisabled.map((r) => r.name)]
|
|
43
43
|
const hasPersonal = allPersonalNames.length > 0
|
|
44
44
|
const defaultTopKeys: string[] = [
|
|
@@ -5,14 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useCallback, useState, type FormEvent } from 'react'
|
|
8
|
-
import type {
|
|
9
|
-
GroupsResponse,
|
|
10
|
-
SkillTag,
|
|
11
|
-
SourceGroupReorderRequest,
|
|
12
|
-
TagDeleteRequest,
|
|
13
|
-
TagMembersRequest,
|
|
14
|
-
TagReorderRequest,
|
|
15
|
-
} from '../../../protocol.ts'
|
|
8
|
+
import type { GroupsResponse, SkillTag } from '../../../protocol.ts'
|
|
16
9
|
import type { SkillHubApi } from '../../api.ts'
|
|
17
10
|
import { errorMessage } from '../../helpers.ts'
|
|
18
11
|
import { conflictsOnClose, type GroupSwitchState } from '../../grouping.ts'
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
|
|
13
|
-
import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
14
13
|
|
|
15
14
|
/** One staged field: its draft text plus the clear/reset marker. */
|
|
16
15
|
interface StagedEdit {
|
|
@@ -107,12 +106,18 @@ export interface FieldState {
|
|
|
107
106
|
invalid: boolean
|
|
108
107
|
}
|
|
109
108
|
|
|
110
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* The settings-form face this card consumes. Declared structurally rather than
|
|
111
|
+
* imported so the card stays independent of the settings package's generics:
|
|
112
|
+
* the browser half passes it `ctx.configForms.get<HubSettingsValue>(HUB_ENTRY_ID)`,
|
|
113
|
+
* which satisfies this shape (ConfigForm<T>).
|
|
114
|
+
*/
|
|
111
115
|
export interface FormScope {
|
|
112
116
|
subscribe(listener: () => void): () => void
|
|
113
117
|
getSnapshot(): { status: string; writable: boolean; value?: Record<string, unknown>; base?: unknown; user?: unknown }
|
|
114
|
-
|
|
115
|
-
|
|
118
|
+
/** Both write paths report host acceptance (ConfigForm.set/unset contract). */
|
|
119
|
+
set(field: string, value: unknown): Promise<boolean>
|
|
120
|
+
unset(field: string): Promise<boolean>
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
/** Stages one card's edits and writes them through the settings transport. */
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Slash-menu skill dots: source lookup + candidates wrapping. The wrapper's
|
|
3
3
|
* contract is pure and injectable — fake api (catalog) and fake settings scope
|
|
4
|
-
* (getSnapshot)
|
|
5
|
-
*
|
|
4
|
+
* (getSnapshot) — so it tests cleanly in the node vitest environment. The dot
|
|
5
|
+
* itself is injected into the rendered DOM, which does not exist here, so these
|
|
6
|
+
* tests assert that candidate rows pass through untouched. The module caches
|
|
7
|
+
* the catalog map; resetModelCache keeps tests independent.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import { beforeEach, describe, expect, it } from 'vitest'
|
|
9
|
-
import type {
|
|
11
|
+
import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
10
12
|
import type { InputTriggerCandidate, InputTriggerServiceContract, InputTriggerSource } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
|
11
13
|
import type { HubSettingsValue } from '../protocol.ts'
|
|
12
14
|
import type { SkillHubApi } from './api.ts'
|
|
@@ -33,14 +35,14 @@ function registry(...sources: InputTriggerSource[]): InputTriggerServiceContract
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/** Fake settings scope snapshotting a fixed HubSettingsValue. */
|
|
36
|
-
function scopeWith(value: HubSettingsValue):
|
|
38
|
+
function scopeWith(value: HubSettingsValue): ConfigForm<HubSettingsValue> {
|
|
37
39
|
return {
|
|
38
40
|
getSnapshot: () => ({ status: 'ready', value, base: undefined, user: undefined, revision: 1, writable: true, mode: 'host' }),
|
|
39
41
|
subscribe: () => () => {},
|
|
40
42
|
set: async () => {},
|
|
41
43
|
unset: async () => {},
|
|
42
44
|
mutate: async () => {},
|
|
43
|
-
} as unknown as
|
|
45
|
+
} as unknown as ConfigForm<HubSettingsValue>
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/** Fake hub api whose catalog lists the given skills. */
|
|
@@ -85,11 +87,10 @@ describe('findSkillSource', () => {
|
|
|
85
87
|
})
|
|
86
88
|
|
|
87
89
|
describe('wrapSkillSource', () => {
|
|
88
|
-
it('
|
|
90
|
+
it('leaves candidate rows untouched — the dot goes to the DOM, not the icon slot', async () => {
|
|
89
91
|
const source = skillSource(async () => [
|
|
90
92
|
{ name: 'code-review', description: 'review code' },
|
|
91
93
|
{ name: 'personal-note', description: 'only me' },
|
|
92
|
-
{ name: 'known-default', description: 'fallback unknown' },
|
|
93
94
|
])
|
|
94
95
|
const api = apiWith([
|
|
95
96
|
{ name: 'code-review', modelInvocable: true },
|
|
@@ -99,16 +100,13 @@ describe('wrapSkillSource', () => {
|
|
|
99
100
|
|
|
100
101
|
const restore = wrapSkillSource(source, api, scope)
|
|
101
102
|
try {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
expect(
|
|
108
|
-
|
|
109
|
-
expect((rows[1].icon as unknown as { props: { style: { background: string } } }).props.style.background).toBe('#445566')
|
|
110
|
-
// Unknown name → model default (still a dot, safer than no dot).
|
|
111
|
-
expect(rows[2].icon).toBeDefined()
|
|
103
|
+
const rows = await source.candidates({ sessionId: 's1' as never }, { query: '', position: 'leading', drilled: false, signal: new AbortController().signal })
|
|
104
|
+
|
|
105
|
+
expect(rows).toHaveLength(2)
|
|
106
|
+
// MenuView renders `icon` through ReferenceIcon (enum-narrowed), so a
|
|
107
|
+
// custom element would never show — rows must carry no icon at all.
|
|
108
|
+
expect(rows[0].icon).toBeUndefined()
|
|
109
|
+
expect(rows[1].icon).toBeUndefined()
|
|
112
110
|
// Original fields pass through untouched.
|
|
113
111
|
expect(rows[0].name).toBe('code-review')
|
|
114
112
|
expect(rows[0].description).toBe('review code')
|
|
@@ -117,33 +115,18 @@ describe('wrapSkillSource', () => {
|
|
|
117
115
|
}
|
|
118
116
|
})
|
|
119
117
|
|
|
120
|
-
it('
|
|
118
|
+
it('still returns the rows when the catalog route is down', async () => {
|
|
121
119
|
const source = skillSource(async () => [{ name: 'lonely', description: '' }])
|
|
122
120
|
const api = { catalog: async () => { throw new Error('route down') } } as unknown as SkillHubApi
|
|
123
121
|
const scope = scopeWith({ enabled: true, announceToAgent: true, showUseCount: true, showUseTime: true, showGroupSummary: true })
|
|
124
122
|
|
|
125
|
-
const restore = wrapSkillSource(source, api, scope)
|
|
126
|
-
try {
|
|
127
|
-
const rows = await source.candidates({ sessionId: 's1' as never }, { query: '', position: 'leading', signal: new AbortController().signal } as unknown as import('@deepseek-ai/dsh-client-ui-input-trigger/client').CandidateRequest)
|
|
128
|
-
// Catalog failed → unknown name → model default dot still renders.
|
|
129
|
-
expect(rows[0].icon).toBeDefined()
|
|
130
|
-
expect((rows[0].icon as unknown as { props: { style: { background: string } } }).props.style.background).toBe('#2f81f7')
|
|
131
|
-
} finally {
|
|
132
|
-
restore()
|
|
133
|
-
}
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('skips dot injection on new host (alpha.2, drilled present) for ReferenceIcon compat', async () => {
|
|
137
|
-
const source = skillSource(async () => [{ name: 'code-review', description: 'review code' }])
|
|
138
|
-
const api = apiWith([{ name: 'code-review', modelInvocable: true }])
|
|
139
|
-
const scope = scopeWith({ enabled: true, announceToAgent: true, showUseCount: true, showUseTime: true, showGroupSummary: true, dotModelColor: '#112233', dotUserColor: '#445566' })
|
|
140
|
-
|
|
141
123
|
const restore = wrapSkillSource(source, api, scope)
|
|
142
124
|
try {
|
|
143
125
|
const rows = await source.candidates({ sessionId: 's1' as never }, { query: '', position: 'leading', drilled: false, signal: new AbortController().signal })
|
|
126
|
+
// A failed catalog only means no dots (unknown names are skipped); the
|
|
127
|
+
// menu itself must keep working.
|
|
144
128
|
expect(rows).toHaveLength(1)
|
|
145
|
-
expect(rows[0].
|
|
146
|
-
expect(rows[0].name).toBe('code-review')
|
|
129
|
+
expect(rows[0].name).toBe('lonely')
|
|
147
130
|
} finally {
|
|
148
131
|
restore()
|
|
149
132
|
}
|