dsh-skill-hub 0.2.2 → 0.2.5

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.
Files changed (38) hide show
  1. package/README.md +10 -14
  2. package/README.zh.md +9 -12
  3. package/lib/client.js +493 -177
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +213 -36
  6. package/lib/types/client/SkillHubSettingsCard.d.ts +4 -6
  7. package/lib/types/client/icons.d.ts +25 -0
  8. package/lib/types/client/locales.d.ts +7 -3
  9. package/lib/types/client/panel/format.d.ts +10 -0
  10. package/lib/types/client/settings-card.d.ts +18 -0
  11. package/lib/types/client/settings-form.d.ts +9 -0
  12. package/lib/types/client/slash-dots.d.ts +57 -0
  13. package/lib/types/index.d.ts +4 -0
  14. package/lib/types/protocol.d.ts +42 -1
  15. package/lib/types/stats.d.ts +53 -4
  16. package/lib/types/store.d.ts +7 -2
  17. package/package.json +68 -26
  18. package/src/client/SkillHubSettingsCard.tsx +35 -10
  19. package/src/client/icons.tsx +61 -0
  20. package/src/client/index.tsx +13 -1
  21. package/src/client/locales.ts +14 -6
  22. package/src/client/panel/SkillDetailView.tsx +6 -2
  23. package/src/client/panel/SkillHubPanel.tsx +2 -2
  24. package/src/client/panel/SkillRow.tsx +8 -3
  25. package/src/client/panel/format.ts +11 -0
  26. package/src/client/panel/panel.module.css +1 -0
  27. package/src/client/settings-card.tsx +49 -1
  28. package/src/client/settings-form.ts +21 -0
  29. package/src/client/slash-dots.test.ts +145 -0
  30. package/src/client/slash-dots.tsx +190 -0
  31. package/src/index.ts +34 -3
  32. package/src/protocol.ts +47 -1
  33. package/src/routes.test.ts +3 -1
  34. package/src/routes.ts +2 -2
  35. package/src/stats.test.ts +273 -2
  36. package/src/stats.ts +166 -23
  37. package/src/store.test.ts +32 -0
  38. package/src/store.ts +58 -3
@@ -81,6 +81,8 @@ export interface DiagnosticEntry {
81
81
  /** GET /api/skill-hub/catalog */
82
82
  export interface CatalogResponse {
83
83
  ok: true;
84
+ /** 已安装插件自身的版本号(package.json version),面板标题旁显示。 */
85
+ pluginVersion: string;
84
86
  /** Whether discovery completed within a stable catalog revision. */
85
87
  complete: boolean;
86
88
  /** Sorted winning summaries of every enabled skill (all roots + providers). */
@@ -179,6 +181,30 @@ export interface StatsResponse {
179
181
  /** Sorted per-skill invocation counts. */
180
182
  stats: SkillStat[];
181
183
  }
184
+ /**
185
+ * Persisted incremental-scan checkpoint for the usage statistics (sidecar
186
+ * `skillStats` field). Sessions created before `frozenBefore` are treated as
187
+ * finalized: their per-session counts live in `frozenSessions` (only sessions
188
+ * with at least one invocation are kept) and they are not re-read on
189
+ * incremental scans. A daily full reconciliation rebuilds the cache and
190
+ * advances the watermark, so a resumed old session is eventually re-counted.
191
+ */
192
+ export interface SkillStatsCheckpoint {
193
+ /** The rolling-window configuration this checkpoint was built for (0 = all history). */
194
+ windowDays: number;
195
+ /** Watermark: every session with header.createdAt < this value is frozen. */
196
+ frozenBefore: number;
197
+ /** Per-session counts of finalized sessions, keyed by session id. */
198
+ frozenSessions: Record<string, {
199
+ createdAt: number;
200
+ counts: Record<string, {
201
+ count: number;
202
+ lastUsed: number;
203
+ }>;
204
+ }>;
205
+ /** Epoch ms of the last full reconciliation (drives the daily cadence). */
206
+ lastFullReconcile: number;
207
+ }
182
208
  /** JSON error body shared by every route. */
183
209
  export interface ErrorResponse {
184
210
  error: string;
@@ -199,6 +225,10 @@ export interface HubConfig {
199
225
  showUseTime?: boolean;
200
226
  /** Show group-header usage summaries (count + last used). Default true. */
201
227
  showGroupSummary?: boolean;
228
+ /** 统计滚动窗口天数:只统计最近 N 天的使用;0 = 全部历史。默认 0。 */
229
+ statsWindowDays?: number;
230
+ /** 自动统计扫描间隔(分钟,最小 1)。默认 5。 */
231
+ statsScanMinutes?: number;
202
232
  }
203
233
  /**
204
234
  * The resolved shape of the hub's settings namespace (schema defaults, then
@@ -216,6 +246,10 @@ export type HubSettingsValue = {
216
246
  dotModelColor?: string;
217
247
  /** User-invocable dot color (#rrggbb); absent means the panel default. */
218
248
  dotUserColor?: string;
249
+ /** 统计滚动窗口天数(0 = 全部历史)。 */
250
+ statsWindowDays?: number;
251
+ /** 自动统计扫描间隔(分钟)。 */
252
+ statsScanMinutes?: number;
219
253
  };
220
254
  /**
221
255
  * Hub config defaults — the single source every layer reads: the cordis
@@ -228,12 +262,15 @@ export declare const HUB_CONFIG_DEFAULTS: {
228
262
  readonly showUseCount: true;
229
263
  readonly showUseTime: true;
230
264
  readonly showGroupSummary: true;
265
+ readonly statsWindowDays: 14;
266
+ readonly statsScanMinutes: 5;
231
267
  };
232
268
  /**
233
269
  * Resolve the effective hub config: saved sidecar overrides win over the
234
270
  * cordis composition entry (the web card owns runtime config), missing
235
271
  * booleans fall back to HUB_CONFIG_DEFAULTS, and dot colors pass through
236
- * (saved first, then base) only when set.
272
+ * (saved first, then base) only when set. Numeric stats knobs are clamped to
273
+ * their sane ranges (window ≥ 0, scan interval ≥ 1 minute).
237
274
  */
238
275
  export declare function resolveHubConfig(saved: Partial<HubConfig>, base?: Partial<HubConfig>): HubConfig;
239
276
  /** HEX color validation shared by host routes and the settings card. */
@@ -262,6 +299,10 @@ export interface ConfigRequest {
262
299
  dotModelColor?: string | null;
263
300
  /** Set the dot color; null clears the saved override so it re-inherits the default. */
264
301
  dotUserColor?: string | null;
302
+ /** 统计滚动窗口天数(0 = 全部历史);null 清除覆盖回默认。 */
303
+ statsWindowDays?: number | null;
304
+ /** 自动统计扫描间隔(分钟);null 清除覆盖回默认。 */
305
+ statsScanMinutes?: number | null;
265
306
  }
266
307
  /** One market source: a tracked upstream repo plus its pinned version. */
267
308
  export interface MarketSourceRecord {
@@ -12,14 +12,40 @@
12
12
  *
13
13
  * Counting is per-skill-name, not per-source: a name may resolve to different
14
14
  * files across projects, but the model-facing identity is the kebab-case name.
15
+ *
16
+ * Scaling (per-session checkpoint + incremental scans): a full scan
17
+ * decompresses every session log, which grows linearly with total history.
18
+ * Sessions older than the effective watermark are therefore treated as
19
+ * finalized — their per-session counts live in the checkpoint (persisted by
20
+ * the host via the sidecar) and are skipped on incremental scans; only the
21
+ * recent window is re-read. A daily full reconciliation rebuilds the cache
22
+ * and advances the watermark, so a resumed old session is eventually
23
+ * re-counted. On top of that, the reader's TTL adapts to the measured scan
24
+ * duration (STATS_TTL_SCAN_FACTOR), so a heavy scan also lowers its own
25
+ * frequency.
26
+ *
27
+ * Rolling window (statsWindowDays > 0): totals only include sessions created
28
+ * within the last N days. The watermark then equals the window edge, so
29
+ * sessions outside the window are neither re-read nor counted, and the
30
+ * reconciliation prunes their cache entries. Changing the configured window
31
+ * forces one full reconciliation immediately (the checkpoint records the
32
+ * window it was built for), so the new semantics take effect on the next scan
33
+ * instead of up to a day later.
15
34
  */
16
35
  import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session';
17
- import type { SkillStat } from './protocol.ts';
36
+ import type { SkillStat, SkillStatsCheckpoint } from './protocol.ts';
37
+ /** Fallback freeze horizon when no rolling window is configured (14 days). */
38
+ export declare const STATS_FREEZE_AFTER_MS: number;
39
+ /** Cadence of the full reconciliation that rebuilds the checkpoint (24 h). */
40
+ export declare const STATS_FULL_RECONCILE_MS: number;
41
+ /** Adaptive TTL factor: effective TTL ≥ this multiple of the last scan duration. */
42
+ export declare const STATS_TTL_SCAN_FACTOR = 3;
18
43
  /** Narrow structural view of the session-query service (kept loose for tests). */
19
44
  export interface SessionQueryLike {
20
45
  listSessions(signal?: AbortSignal): Promise<Array<{
21
46
  header: {
22
47
  id: SessionId;
48
+ createdAt?: number;
23
49
  };
24
50
  }>>;
25
51
  readSession(id: SessionId): Promise<{
@@ -34,10 +60,27 @@ export interface InvocationStat {
34
60
  }
35
61
  /** Collect per-skill invocation counts and last-used times from one session. */
36
62
  export declare function countSkillInvocations(events: readonly SessionEvent[]): Map<string, InvocationStat>;
37
- /** Scan every session and total per-skill counts, keeping the latest lastUsed. */
38
- export declare function readSkillStats(query: SessionQueryLike): Promise<SkillStat[]>;
63
+ /**
64
+ * Full-corpus totals in one shot (no checkpoint reuse). Kept as the
65
+ * reference implementation for tests and one-off callers.
66
+ */
67
+ export declare function readSkillStats(query: SessionQueryLike, windowDays?: number): Promise<SkillStat[]>;
39
68
  /** A memoized stats reader (the panel polls, but logs change slowly). */
40
69
  export type SkillStatsReader = () => Promise<SkillStat[]>;
70
+ /** Optional wiring for {@link createSkillStatsReader}. */
71
+ export interface SkillStatsReaderOptions {
72
+ /** Checkpoint restored from the sidecar; absent means "start from zero". */
73
+ checkpoint?: SkillStatsCheckpoint;
74
+ /** Injectable clock (epoch ms); defaults to Date.now. Tests drive time with it. */
75
+ now?: () => number;
76
+ /** Base rescan interval in ms; a getter reads the live config each check. */
77
+ ttlMs?: number | (() => number);
78
+ /** Rolling window in days; a getter reads the live config each scan. 0 = all history. */
79
+ windowDays?: () => number;
80
+ /** Called after a full reconciliation mutated the checkpoint (never after an
81
+ * incremental scan) so the host can persist it to the sidecar. */
82
+ onCheckpoint?: (checkpoint: SkillStatsCheckpoint) => void;
83
+ }
41
84
  /**
42
85
  * Wrap a query in a stale-while-revalidate cache: responses never wait for a
43
86
  * full session-log scan. While the TTL is fresh the cached totals are
@@ -45,5 +88,11 @@ export type SkillStatsReader = () => Promise<SkillStat[]>;
45
88
  * single background rescan refreshes them — the panel's next poll picks the
46
89
  * fresh numbers. A full scan decompresses every session log and can take
47
90
  * seconds, so it must never sit on the request path.
91
+ *
92
+ * Two scaling mechanisms keep this sane as history grows:
93
+ * - the rescan is incremental (per-session checkpoint, see module doc);
94
+ * - the effective TTL adapts to the measured scan duration, so a heavier
95
+ * corpus automatically lowers the rescan cadence instead of burning CPU
96
+ * on every poll interval.
48
97
  */
49
- export declare function createSkillStatsReader(query: SessionQueryLike, ttlMs?: number): SkillStatsReader;
98
+ export declare function createSkillStatsReader(query: SessionQueryLike, ttlMs?: number | (() => number), options?: SkillStatsReaderOptions): SkillStatsReader;
@@ -11,7 +11,7 @@
11
11
  * State file: $DSH_HOME/dsh-skill-hub.json — a small JSON document written
12
12
  * atomically (tmp file + rename).
13
13
  */
14
- import type { DisabledSkill, HubConfig, MarketSourceRecord, RepoRoot, SkillTag, SourceRecord, TrashEntry } from './protocol.ts';
14
+ import type { DisabledSkill, HubConfig, MarketSourceRecord, RepoRoot, SkillStatsCheckpoint, SkillTag, SourceRecord, TrashEntry } from './protocol.ts';
15
15
  /** 默认场景名(系统预置的兜底场景,新技能自动归入)。 */
16
16
  export declare const DEFAULT_SCENE_NAME = "\u901A\u7528";
17
17
  /** Resolve the DSH home directory (the filesystem provider's user-dsh root base). */
@@ -19,7 +19,7 @@ export declare function dshHome(): string;
19
19
  /** Resolve the sidecar state path (injectable in tests). */
20
20
  export declare function statePath(home?: string): string;
21
21
  /** Current sidecar schema version. Bump on breaking shape changes and add a migration below. */
22
- export declare const STORE_VERSION = 3;
22
+ export declare const STORE_VERSION = 4;
23
23
  /**
24
24
  * Business-rule failure the routes layer can map onto a 4xx status instead
25
25
  * of a blanket 500: user input is invalid (validation → 400), the target
@@ -39,6 +39,7 @@ export declare class SkillHubStore {
39
39
  private sourcesByRepo;
40
40
  private marketSources;
41
41
  private trashByName;
42
+ private skillStats;
42
43
  private loaded;
43
44
  /** Serializes persist runs: concurrent mutators must not let an earlier
44
45
  * snapshot overwrite a later one (rename is atomic, ordering is not). */
@@ -138,5 +139,9 @@ export declare class SkillHubStore {
138
139
  addTrash(entry: TrashEntry): Promise<void>;
139
140
  /** Remove a trash record (after restore). */
140
141
  removeTrash(name: string): Promise<void>;
142
+ /** The persisted usage-statistics checkpoint (undefined until first saved). */
143
+ getSkillStatsState(): Promise<SkillStatsCheckpoint | undefined>;
144
+ /** Persist a usage-statistics checkpoint (written at most ~once a day, on full reconciliations). */
145
+ saveSkillStatsState(state: SkillStatsCheckpoint): Promise<void>;
141
146
  private persist;
142
147
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-skill-hub",
3
- "version": "0.2.2",
3
+ "version": "0.2.5",
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": {
@@ -37,42 +37,84 @@
37
37
  "platform": "web"
38
38
  }
39
39
  },
40
+ "dshWorkshop": {
41
+ "schema": "omdsh-workshop-package/v1",
42
+ "type": "plugin",
43
+ "integration": {
44
+ "protocol": "harness-cordis",
45
+ "artifact": "cordis.patch.yml"
46
+ },
47
+ "install": {
48
+ "mode": "guided",
49
+ "adapter": "third-party",
50
+ "failurePolicy": "manual",
51
+ "touchesCurrentBeforeActivation": false
52
+ },
53
+ "lifecycle": {
54
+ "activation": "hot-reload",
55
+ "dispose": "supported"
56
+ },
57
+ "permissions": [
58
+ "fs:read",
59
+ "fs:write",
60
+ "net:https",
61
+ "net:loopback"
62
+ ],
63
+ "compatibility": {
64
+ "dshVersions": [
65
+ "0.1.0-rc.7",
66
+ "0.1.1-rc.2"
67
+ ]
68
+ },
69
+ "capability": {
70
+ "id": "skill-hub-settings-panel",
71
+ "kind": "ui",
72
+ "invocation": "Open Settings → Skills panel; GET /api/skill-hub/catalog",
73
+ "expected": "The full local skill catalog (all roots) renders as groups and toggles persist an enabled/disabled row without a restart."
74
+ },
75
+ "evidence": {
76
+ "install": null,
77
+ "failureIsolation": null,
78
+ "hotReload": null,
79
+ "remove": null
80
+ }
81
+ },
40
82
  "dependencies": {
41
83
  "js-yaml": "^4.1.0",
42
84
  "schemastery": "^3.18.0"
43
85
  },
44
86
  "peerDependencies": {
45
87
  "@deepseek-ai/cordis": "^4.0.1",
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",
88
+ "@deepseek-ai/dsh-client-connection": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
89
+ "@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
90
+ "@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
91
+ "@deepseek-ai/dsh-client-ui-input-trigger": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
92
+ "@deepseek-ai/dsh-client-ui-settings": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
93
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
94
+ "@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
95
+ "@deepseek-ai/dsh-session": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
96
+ "@deepseek-ai/dsh-session-query": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
97
+ "@deepseek-ai/dsh-settings": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
98
+ "@deepseek-ai/dsh-skill": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
99
+ "@deepseek-ai/dsh-system-prompt": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
58
100
  "react": "^18.2.0",
59
101
  "react-dom": "^18.2.0"
60
102
  },
61
103
  "devDependencies": {
62
104
  "@deepseek-ai/cordis": "^4.0.1",
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",
105
+ "@deepseek-ai/dsh-client-connection": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
106
+ "@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
107
+ "@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
108
+ "@deepseek-ai/dsh-client-ui-input-trigger": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
109
+ "@deepseek-ai/dsh-client-ui-settings": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
110
+ "@deepseek-ai/dsh-client-ui-settings-plugins": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
111
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
112
+ "@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
113
+ "@deepseek-ai/dsh-session": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
114
+ "@deepseek-ai/dsh-session-query": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
115
+ "@deepseek-ai/dsh-settings": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
116
+ "@deepseek-ai/dsh-skill": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
117
+ "@deepseek-ai/dsh-system-prompt": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0",
76
118
  "@types/js-yaml": "^4.0.9",
77
119
  "@types/node": "^22.20.0",
78
120
  "@types/react": "~18.3.1",
@@ -9,15 +9,12 @@
9
9
  import type { ReactElement } from 'react'
10
10
  import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
11
11
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
12
- import { ColorField, PluginSettingsCard, SwitchField } from './settings-card.tsx'
13
- import { booleanField, CardForm, colorField, type CardShell, type FieldState, type FormScope } from './settings-form.ts'
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'
12
+ import { ColorField, NumberField, PluginSettingsCard, SwitchField } from './settings-card.tsx'
13
+ import { booleanField, CardForm, colorField, numberField, type CardShell, type FieldState, type FormScope } from './settings-form.ts'
14
+ // Single source for the TS-side dot defaults (the panel CSS mirrors these).
15
+ import { DEFAULT_DOT_MODEL_COLOR, DEFAULT_DOT_USER_COLOR } from './panel/format.ts'
16
+ // Re-export for any consumer that imported them from the card before the move.
17
+ export { DEFAULT_DOT_MODEL_COLOR, DEFAULT_DOT_USER_COLOR }
21
18
 
22
19
  /** The card's projected state. */
23
20
  export interface SkillHubSettingsState extends CardShell {
@@ -28,6 +25,8 @@ export interface SkillHubSettingsState extends CardShell {
28
25
  showUseCount: FieldState
29
26
  showUseTime: FieldState
30
27
  showGroupSummary: FieldState
28
+ statsWindowDays: FieldState
29
+ statsScanMinutes: FieldState
31
30
  }
32
31
 
33
32
  /** The business face the card's slot registration injects. */
@@ -53,7 +52,7 @@ export class SkillHubSettingsCardController {
53
52
 
54
53
  /** @param scope - the hub settings scope the card edits (FormScope-compatible). */
55
54
  constructor(scope: FormScope) {
56
- this.form = new CardForm(scope, [booleanField('enabled'), booleanField('announceToAgent'), colorField('dotModelColor'), colorField('dotUserColor'), booleanField('showUseCount'), booleanField('showUseTime'), booleanField('showGroupSummary')])
55
+ this.form = new CardForm(scope, [booleanField('enabled'), booleanField('announceToAgent'), colorField('dotModelColor'), colorField('dotUserColor'), booleanField('showUseCount'), booleanField('showUseTime'), booleanField('showGroupSummary'), numberField('statsWindowDays', { min: 0, max: 3650 }), numberField('statsScanMinutes', { min: 1, max: 1440 })])
57
56
  this.store = this.form.bind(() => this.projection())
58
57
  }
59
58
 
@@ -67,6 +66,8 @@ export class SkillHubSettingsCardController {
67
66
  showUseCount: this.form.field('showUseCount'),
68
67
  showUseTime: this.form.field('showUseTime'),
69
68
  showGroupSummary: this.form.field('showGroupSummary'),
69
+ statsWindowDays: this.form.field('statsWindowDays'),
70
+ statsScanMinutes: this.form.field('statsScanMinutes'),
70
71
  }
71
72
  }
72
73
 
@@ -167,6 +168,30 @@ export function SkillHubSettingsCard(props: SkillHubSettingsCardProps): ReactEle
167
168
  onEdit={(text) => { props.edit('showGroupSummary', text) }}
168
169
  onReset={() => { props.resetField('showGroupSummary') }}
169
170
  />
171
+ <NumberField
172
+ id='skill-hub-stats-window-days'
173
+ label={t('settings.statsWindowDays')}
174
+ hint={t('settings.statsWindowDaysHint')}
175
+ inheritLabel={t('settings.inherit')}
176
+ {...fieldProps}
177
+ text={state.statsWindowDays.text}
178
+ overridden={state.statsWindowDays.overridden}
179
+ invalid={state.statsWindowDays.invalid}
180
+ onEdit={(text) => { props.edit('statsWindowDays', text) }}
181
+ onReset={() => { props.resetField('statsWindowDays') }}
182
+ />
183
+ <NumberField
184
+ id='skill-hub-stats-scan-minutes'
185
+ label={t('settings.statsScanMinutes')}
186
+ hint={t('settings.statsScanMinutesHint')}
187
+ inheritLabel={t('settings.inherit')}
188
+ {...fieldProps}
189
+ text={state.statsScanMinutes.text}
190
+ overridden={state.statsScanMinutes.overridden}
191
+ invalid={state.statsScanMinutes.invalid}
192
+ onEdit={(text) => { props.edit('statsScanMinutes', text) }}
193
+ onReset={() => { props.resetField('statsScanMinutes') }}
194
+ />
170
195
  </PluginSettingsCard>
171
196
  )
172
197
  }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Vendored UI icons for dsh-skill-hub.
3
+ *
4
+ * These were originally imported from `@deepseek-ai/dsh-client-ui-primitives`.
5
+ * That package is still published for the rc.7/rc.2 SDK families, but newer dsh
6
+ * web builds no longer expose it as a standalone plugin module — they keep a
7
+ * static compatibility module instead. Vendoring the few tiny outline icons the
8
+ * hub uses makes the browser half self-contained and equally compatible with
9
+ * older and newer dsh hosts.
10
+ *
11
+ * SVG paths are copied verbatim from dsh-client-ui-primitives (MIT licensed)
12
+ * so the visuals remain pixel-identical to the dsh icon family.
13
+ */
14
+
15
+ import type { JSX } from 'react'
16
+
17
+ /** Props understood by the dsh icon family: size + optional className. */
18
+ export interface IconProps {
19
+ size?: number
20
+ className?: string
21
+ }
22
+
23
+ /** ic_ds_chevron_down_outline_14 */
24
+ export function IconChevronDownOutline14({ size = 14, className }: IconProps): JSX.Element {
25
+ return (
26
+ <svg width={size} height={size} className={className} viewBox='0 0 14 14' fill='none' xmlns='http://www.w3.org/2000/svg'>
27
+ <path
28
+ d='M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z'
29
+ fill='currentColor'
30
+ />
31
+ </svg>
32
+ )
33
+ }
34
+
35
+ /** ic_ds_trash_outline_16 */
36
+ export function IconTrashOutline16({ size = 16, className }: IconProps): JSX.Element {
37
+ return (
38
+ <svg width={size} height={size} className={className} viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
39
+ <path
40
+ d='M14.4782 4.84067L14.2138 10.1152C14.1102 12.1872 14.067 13.0115 13.3866 13.9607C13.1044 14.3546 12.7498 14.6912 12.3424 14.9535C11.8239 15.2872 11.2415 15.4316 10.5585 15.4998C9.88727 15.5668 9.04946 15.5656 7.99998 15.5656C6.95051 15.5656 6.1127 15.5668 5.44142 15.4998C4.75851 15.4316 4.17602 15.2872 3.65753 14.9535C3.25012 14.6912 2.89559 14.3546 2.61332 13.9607C1.93296 13.0115 1.88979 12.1872 1.78619 10.1152L1.52179 4.84067L2.89006 4.77277L3.15343 10.0463C3.26221 12.2218 3.32452 12.6015 3.72646 13.1624C3.90825 13.4161 4.13686 13.6334 4.39927 13.8023C4.66204 13.9714 5.00263 14.0792 5.57825 14.1367C6.16562 14.1953 6.92298 14.1963 7.99998 14.1963C9.07699 14.1963 9.83434 14.1953 10.4217 14.1367C10.9973 14.0792 11.3379 13.9714 11.6007 13.8023C11.8631 13.6334 12.0917 13.4161 12.2735 13.1624C12.6755 12.6015 12.7378 12.2218 12.8465 10.0463L13.1099 4.77277L14.4782 4.84067ZM5.43011 6.22849H6.7994V11.3909H5.43011V6.22849ZM9.20056 6.22849H10.5699V11.3909H9.20056V6.22849ZM8.53597 0.434431C9.17976 0.434431 9.6522 0.426926 10.0966 0.571258C10.2357 0.616451 10.3717 0.672554 10.502 0.738948C10.9182 0.951107 11.2464 1.29099 11.7015 1.74612L12.4978 2.54136H15.3742V3.91169H0.625732V2.54136H3.50218L4.29845 1.74612C4.75358 1.29099 5.08174 0.951107 5.49801 0.738948C5.62831 0.672554 5.76425 0.616451 5.90334 0.571258C6.34776 0.426926 6.82021 0.434431 7.46399 0.434431H8.53597ZM7.46399 1.80476C6.73208 1.80476 6.51641 1.81187 6.32617 1.87369C6.25545 1.89667 6.18668 1.92533 6.12041 1.95907C5.96398 2.03878 5.82348 2.16253 5.44142 2.54136H10.5585C10.1765 2.16253 10.036 2.03878 9.87955 1.95907C9.81329 1.92533 9.74452 1.89667 9.6738 1.87369C9.48356 1.81187 9.26789 1.80476 8.53597 1.80476H7.46399Z'
41
+ fill='currentColor'
42
+ />
43
+ </svg>
44
+ )
45
+ }
46
+
47
+ /** ic_ds_skill_outline_16 */
48
+ export function IconSkillOutline16({ size = 16, className }: IconProps): JSX.Element {
49
+ return (
50
+ <svg width={size} height={size} className={className} viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
51
+ <path
52
+ d='M12.5113 15.4067C12.4395 15.6249 12.1308 15.6249 12.059 15.4067L11.643 14.1416C11.454 13.567 11.0033 13.1164 10.4288 12.9274L9.16369 12.5113C8.94544 12.4395 8.94544 12.1308 9.16369 12.059L10.4288 11.643C11.0033 11.454 11.454 11.0033 11.643 10.4288L12.059 9.16369C12.1308 8.94544 12.4395 8.94544 12.5113 9.16369L12.9274 10.4288C13.1164 11.0033 13.567 11.454 14.1416 11.643L15.4067 12.059C15.6249 12.1308 15.6249 12.4395 15.4067 12.5113L14.1416 12.9274C13.567 13.1164 13.1164 13.567 12.9274 14.1416L12.5113 15.4067Z'
53
+ fill='currentColor'
54
+ />
55
+ <path
56
+ d='M9.02246 0.546878C9.9822 0.546878 10.7564 0.545403 11.374 0.612307C12.0042 0.680586 12.5515 0.826244 13.0273 1.17188C13.3052 1.37376 13.5501 1.61868 13.752 1.89649C14.0975 2.37225 14.2432 2.91984 14.3115 3.54981C14.3784 4.16727 14.377 4.94206 14.377 5.90137V8.51367C13.9611 8.29533 13.5071 8.13985 13.0273 8.06055V5.90137C13.0273 4.9121 13.0259 4.22322 12.9688 3.69532C12.9129 3.18044 12.8098 2.89782 12.6592 2.69043C12.5406 2.52724 12.3966 2.38326 12.2334 2.26465C12.026 2.11404 11.7437 2.0109 11.2285 1.95508C10.7005 1.89789 10.0122 1.89649 9.02246 1.89649H6.55371C5.56395 1.89649 4.87569 1.89787 4.34766 1.95508C3.83242 2.01092 3.55022 2.11398 3.34278 2.26465C3.17953 2.38329 3.03564 2.52719 2.91699 2.69043C2.76642 2.89782 2.66325 3.18042 2.60742 3.69532C2.55027 4.22322 2.54883 4.9121 2.54883 5.90137V10.0986C2.54883 11.0878 2.55031 11.7768 2.60742 12.3047C2.66326 12.8196 2.76642 13.1032 2.91699 13.3105C3.03558 13.4736 3.17966 13.6178 3.34278 13.7363C3.5502 13.8869 3.83265 13.9901 4.34766 14.0459C4.87568 14.1031 5.56398 14.1035 6.55371 14.1035H8.08399C8.27443 14.6025 8.55077 15.0585 8.89551 15.4541H6.55371C5.59402 15.4541 4.81976 15.4546 4.20215 15.3877C3.57204 15.3194 3.02468 15.1738 2.54883 14.8281C2.27111 14.6263 2.02606 14.3813 1.82422 14.1035C1.47883 13.6278 1.33293 13.08 1.26465 12.4502C1.19783 11.8327 1.19922 11.0579 1.19922 10.0986V5.90137C1.19922 4.94206 1.1978 4.16727 1.26465 3.54981C1.33295 2.91984 1.47867 2.37225 1.82422 1.89649C2.02613 1.61864 2.27098 1.37379 2.54883 1.17188C3.02472 0.826181 3.57197 0.6806 4.20215 0.612307C4.81976 0.545393 5.594 0.546877 6.55371 0.546878H9.02246ZM9.19629 9.14649H4.5459V7.84571H9.19629V9.14649ZM11.0303 6.10645H4.5459V4.80567H11.0303V6.10645Z'
57
+ fill='currentColor'
58
+ />
59
+ </svg>
60
+ )
61
+ }
@@ -31,10 +31,13 @@ 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
+ // Type-only: pulls the Context merge for ctx.inputTriggers (slash-dots wiring).
35
+ import type {} from '@deepseek-ai/dsh-client-ui-input-trigger/client'
34
36
  import type { HubSettingsValue } from '../protocol.ts'
35
37
  import { SkillHubApi } from './api.ts'
36
38
  import { en, zh, type HubKey } from './locales.ts'
37
39
  import { applySettingsNavIcon } from './settings-nav-icon.ts'
40
+ import { setupSkillSlashDots } from './slash-dots.tsx'
38
41
  import { SkillHubSettingsCard, SkillHubSettingsCardController } from './SkillHubSettingsCard.tsx'
39
42
  import { SkillHubPanel } from './panel/SkillHubPanel.tsx'
40
43
 
@@ -55,7 +58,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
55
58
  * `settingsScope` is the namespace-scope binder itself; mirror the official
56
59
  * settings-plugins inject list.
57
60
  */
58
- export const inject = ['slots', 'locale', 'connection', 'remote', 'settingsScope']
61
+ export const inject = ['slots', 'locale', 'connection', 'remote', 'settingsScope', 'inputTriggers']
59
62
 
60
63
  /** Type-only surface (export discipline: no value exports beyond the plugin contract). */
61
64
  export type { SkillHubPanelProps } from './panel/SkillHubPanel.tsx'
@@ -79,6 +82,15 @@ export function apply(ctx: ClientContext): void {
79
82
  ctx.settingsScope.bind<HubSettingsValue>({ namespace: NS }),
80
83
  )
81
84
 
85
+ // Chat `/` menu skill dots: wrap the core skill source so every candidate
86
+ // row carries the invocation dot, colored from the same settings the panel
87
+ // legend uses (dotModelColor / dotUserColor). Own scope = independent reader;
88
+ // fails silent, never takes the GUI down.
89
+ ctx.effect(
90
+ () => setupSkillSlashDots(ctx, api, ctx.settingsScope.bind<HubSettingsValue>({ namespace: NS })),
91
+ 'dsh-skill-hub: slash dots',
92
+ )
93
+
82
94
  // Plugin-management card: Settings → 插件 → 可配置插件列表.
83
95
  // rc.7's slot contract declares this keyed slot with options `key`
84
96
  // (the settings namespace the card edits), so registration is fully typed.
@@ -19,7 +19,7 @@ export const zh = {
19
19
  'panel.search': '搜索技能…',
20
20
  'legend.model': '模型可调 — Agent 可自动调用',
21
21
  'legend.user': '用户可调 — 仅显式点名时触发',
22
- 'legend.hint': '圆点标在技能名旁;无圆点表示该技能不可被调用。',
22
+ 'legend.hint': '每个技能最多一个圆点;无圆点 = 不可被调用。',
23
23
  'panel.enabled': '已启用',
24
24
  'panel.disabled': '已禁用',
25
25
  'panel.diagnostics': '发现诊断',
@@ -188,15 +188,19 @@ export const zh = {
188
188
  'settings.announceToAgent': '向 Agent 公告',
189
189
  'settings.announceToAgentHint': '在系统提示中加入本插件说明,用户提到技能管理时 Agent 知道如何协作。',
190
190
  'settings.dotModelColor': '模型可调圆点颜色',
191
- 'settings.dotModelColorHint': '技能名旁蓝色圆点的颜色(#rrggbb)。',
191
+ 'settings.dotModelColorHint': '技能行/聊天 / 菜单中「模型可调」蓝色圆点的颜色(#rrggbb)。',
192
192
  'settings.dotUserColor': '用户可调圆点颜色',
193
- 'settings.dotUserColorHint': '技能名旁绿色圆点的颜色(#rrggbb)。',
193
+ 'settings.dotUserColorHint': '技能行/聊天 / 菜单中「仅用户可调」绿色圆点的颜色(#rrggbb)。',
194
194
  'settings.showUseCount': '显示调用次数',
195
195
  'settings.showUseCountHint': '在技能名旁显示琥珀色调用次数。',
196
196
  'settings.showUseTime': '显示最近调用时间',
197
197
  'settings.showUseTimeHint': '在技能名行右侧显示相对时间(如「3 天前」)。',
198
198
  'settings.showGroupSummary': '显示分组汇总',
199
199
  'settings.showGroupSummaryHint': '在分组标题后汇总调用次数与最近调用时间。',
200
+ 'settings.statsWindowDays': '统计窗口(天)',
201
+ 'settings.statsWindowDaysHint': '只统计最近 N 天的使用次数,默认 14 天;0 = 全部历史。改动立即生效。',
202
+ 'settings.statsScanMinutes': '自动统计间隔(分钟)',
203
+ 'settings.statsScanMinutesHint': '后台扫描会话日志的间隔,最小 1 分钟;扫描耗时会自动拉长间隔。',
200
204
  'settings.inherit': '继承',
201
205
  'settings.on': '开',
202
206
  'settings.off': '关',
@@ -228,7 +232,7 @@ export const en: Record<HubKey, string> = {
228
232
  'panel.search': 'Search skills…',
229
233
  'legend.model': 'Model-invocable — agents may call automatically',
230
234
  'legend.user': 'User-invocable — explicit invocation only',
231
- 'legend.hint': 'Dots sit beside the skill name; no dot means the skill is not invocable.',
235
+ 'legend.hint': 'At most one dot per skill; no dot = not invocable.',
232
236
  'panel.enabled': 'Enabled',
233
237
  'panel.disabled': 'Disabled',
234
238
  'panel.diagnostics': 'Discovery diagnostics',
@@ -397,15 +401,19 @@ export const en: Record<HubKey, string> = {
397
401
  'settings.announceToAgent': 'Announce to agent',
398
402
  'settings.announceToAgentHint': 'Adds a system-prompt section so agents know how to collaborate when users mention skill management.',
399
403
  'settings.dotModelColor': 'Model-invocable dot color',
400
- 'settings.dotModelColorHint': 'Color of the blue dot beside skill names (#rrggbb).',
404
+ 'settings.dotModelColorHint': 'Color of the blue "model-callable" dot in the skill panel and the chat / menu (#rrggbb).',
401
405
  'settings.dotUserColor': 'User-invocable dot color',
402
- 'settings.dotUserColorHint': 'Color of the green dot beside skill names (#rrggbb).',
406
+ 'settings.dotUserColorHint': 'Color of the green "user-only" dot in the skill panel and the chat / menu (#rrggbb).',
403
407
  'settings.showUseCount': 'Show invocation count',
404
408
  'settings.showUseCountHint': 'Show the amber invocation count beside each skill name.',
405
409
  'settings.showUseTime': 'Show last-used time',
406
410
  'settings.showUseTimeHint': 'Show relative last-used time on the name row (e.g. "3 d ago").',
407
411
  'settings.showGroupSummary': 'Show group summaries',
408
412
  'settings.showGroupSummaryHint': 'Summarize invocation count and last-used time after group titles.',
413
+ 'settings.statsWindowDays': 'Stats window (days)',
414
+ 'settings.statsWindowDaysHint': 'Count only usage within the last N days (default 14); 0 = full history. Changes apply immediately.',
415
+ 'settings.statsScanMinutes': 'Auto stats interval (minutes)',
416
+ 'settings.statsScanMinutesHint': 'How often the background session-log scan runs (min 1); heavy scans automatically stretch it.',
409
417
  'settings.inherit': 'Inherit',
410
418
  'settings.on': 'On',
411
419
  'settings.off': 'Off',
@@ -42,8 +42,12 @@ export function SkillDetailView(props: SkillDetailViewProps): JSX.Element {
42
42
  <button type='button' className={css.back} onClick={onBack}>{tt('detail.back')}</button>
43
43
  <span className={css.detailName}>
44
44
  {detail.name}
45
- {detail.invocation.modelInvocable ? <span className={css.dot + ' ' + css.dotModel} style={dotStyle(hubConfig?.dotModelColor)} title={tt('legend.model')} /> : null}
46
- {detail.invocation.userInvocable ? <span className={css.dot + ' ' + css.dotUser} style={dotStyle(hubConfig?.dotUserColor)} title={tt('legend.user')} /> : null}
45
+ {/* 单一状态圆点:模型可调 → 蓝;否则用户可调 → 绿。与聊天 / 菜单同规则。 */}
46
+ {detail.invocation.modelInvocable
47
+ ? <span className={css.dot + ' ' + css.dotModel} style={dotStyle(hubConfig?.dotModelColor)} title={tt('legend.model')} />
48
+ : detail.invocation.userInvocable
49
+ ? <span className={css.dot + ' ' + css.dotUser} style={dotStyle(hubConfig?.dotUserColor)} title={tt('legend.user')} />
50
+ : null}
47
51
  </span>
48
52
  </div>
49
53
  <div className={css.detailMeta}>
@@ -14,7 +14,7 @@
14
14
 
15
15
  import { useState } from 'react'
16
16
  import type { WritableRoot } from '../../protocol.ts'
17
- import { IconSkillOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
17
+ import { IconSkillOutline16 } from '../icons.tsx'
18
18
  import type { SkillHubApi } from '../api.ts'
19
19
  import { tt } from '../helpers.ts'
20
20
  import { PRIVATE_SOURCE, type SortKey } from '../grouping.ts'
@@ -119,7 +119,7 @@ export function SkillHubPanel(props: SkillHubPanelProps): React.JSX.Element {
119
119
  return (
120
120
  <div className={css.panel}>
121
121
  <div className={css.header}>
122
- <h2 className={css.title}><IconSkillOutline16 size={16} className={css.titleIcon} /> {tt('panel.title')}</h2>
122
+ <h2 className={css.title}><IconSkillOutline16 size={16} className={css.titleIcon} /> {tt('panel.title')}{catalog !== null ? <span className={css.pluginVersion}>v{catalog.pluginVersion}</span> : null}</h2>
123
123
  {catalog !== null
124
124
  ? <span className={css.headerCount}>
125
125
  {tt('panel.count', { count: catalog.skills.length + catalog.disabled.length })}