dsh-skill-hub 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -33
- package/lib/client.js +3477 -436
- package/lib/client.js.map +1 -1
- package/lib/index.js +2408 -366
- package/lib/types/client/SkillHubSettingsCard.d.ts +5 -0
- package/lib/types/client/api-config-scope.d.ts +6 -0
- package/lib/types/client/api.d.ts +44 -2
- package/lib/types/client/grouping.d.ts +64 -0
- package/lib/types/client/locales.d.ts +155 -18
- package/lib/types/client/market-catalog.d.ts +14 -0
- package/lib/types/client/panel/DisabledRow.d.ts +12 -0
- package/lib/types/client/panel/GroupSummary.d.ts +10 -0
- package/lib/types/client/panel/MarketView.d.ts +11 -0
- package/lib/types/client/panel/ScenesView.d.ts +11 -0
- package/lib/types/client/panel/SkillDetailView.d.ts +31 -0
- package/lib/types/client/panel/SkillHubPanel.d.ts +11 -3
- package/lib/types/client/panel/SkillRow.d.ts +11 -0
- package/lib/types/client/panel/SourceStatusBadge.d.ts +18 -0
- package/lib/types/client/panel/SourcesView.d.ts +10 -0
- package/lib/types/client/panel/TagEditorView.d.ts +29 -0
- package/lib/types/client/panel/dialogs.d.ts +73 -0
- package/lib/types/client/panel/format.d.ts +14 -0
- package/lib/types/client/panel/useSkillHub.d.ts +183 -0
- package/lib/types/client/settings-card.d.ts +21 -4
- package/lib/types/client/settings-form.d.ts +2 -0
- package/lib/types/client/settings-nav-icon.d.ts +8 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/protocol.d.ts +428 -7
- package/lib/types/provider.d.ts +6 -1
- package/lib/types/repo.d.ts +133 -0
- package/lib/types/routes.d.ts +9 -4
- package/lib/types/skillfs.d.ts +19 -14
- package/lib/types/stats.d.ts +17 -4
- package/lib/types/store.d.ts +112 -6
- package/lib/types/update.d.ts +20 -0
- package/package.json +5 -3
- package/src/client/SkillHubSettingsCard.tsx +61 -8
- package/src/client/api-config-scope.test.ts +79 -0
- package/src/client/api-config-scope.ts +28 -4
- package/src/client/api.ts +239 -9
- package/src/client/grouping.test.ts +109 -0
- package/src/client/grouping.ts +120 -0
- package/src/client/index.tsx +5 -2
- package/src/client/locales.ts +314 -40
- package/src/client/market-catalog.ts +22 -0
- package/src/client/panel/DisabledRow.tsx +32 -0
- package/src/client/panel/GroupSummary.tsx +27 -0
- package/src/client/panel/MarketView.tsx +173 -0
- package/src/client/panel/ScenesView.tsx +75 -0
- package/src/client/panel/SkillDetailView.tsx +95 -0
- package/src/client/panel/SkillHubPanel.tsx +251 -257
- package/src/client/panel/SkillRow.tsx +54 -0
- package/src/client/panel/SourceStatusBadge.tsx +61 -0
- package/src/client/panel/SourcesView.tsx +104 -0
- package/src/client/panel/TagEditorView.tsx +89 -0
- package/src/client/panel/dialogs.tsx +185 -0
- package/src/client/panel/format.ts +31 -0
- package/src/client/panel/panel.module.css +209 -45
- package/src/client/panel/useSkillHub.ts +797 -0
- package/src/client/settings-card.module.css +44 -28
- package/src/client/settings-card.tsx +64 -8
- package/src/client/settings-form.ts +12 -0
- package/src/client/settings-nav-icon.ts +42 -0
- package/src/index.ts +19 -15
- package/src/protocol.ts +460 -7
- package/src/provider.test.ts +2 -4
- package/src/provider.ts +18 -14
- package/src/repo.test.ts +212 -0
- package/src/repo.ts +528 -0
- package/src/routes.test.ts +564 -52
- package/src/routes.ts +999 -153
- package/src/skillfs.test.ts +98 -16
- package/src/skillfs.ts +68 -33
- package/src/stats.test.ts +52 -17
- package/src/stats.ts +55 -18
- package/src/store.test.ts +205 -1
- package/src/store.ts +527 -24
- package/src/update.test.ts +73 -0
- package/src/update.ts +136 -0
package/lib/types/skillfs.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* userInvocable defaults true), legacy keys rejected, and the returned
|
|
12
12
|
* content is the body after the frontmatter block, trimmed.
|
|
13
13
|
*/
|
|
14
|
-
import type { DiagnosticEntry, WritableRoot } from './protocol.ts';
|
|
14
|
+
import type { DiagnosticEntry, TrashEntry, WritableRoot } from './protocol.ts';
|
|
15
15
|
/** Root ids this module may write to. */
|
|
16
16
|
export declare const WRITABLE_ROOTS: readonly WritableRoot[];
|
|
17
17
|
/** Resolve the absolute directory of one writable root. */
|
|
@@ -32,13 +32,26 @@ export declare function createSkill(root: WritableRoot, name: string, descriptio
|
|
|
32
32
|
export declare function disableSkill(path: string): Promise<string>;
|
|
33
33
|
/** Re-enable a previously disabled file (reverse of disableSkill). */
|
|
34
34
|
export declare function enableSkill(disabledPath: string): Promise<string>;
|
|
35
|
+
/**
|
|
36
|
+
* Move a skill (directory bundle or flat .md file) into the root's trash.
|
|
37
|
+
* The skill stays recoverable: the source is renamed into
|
|
38
|
+
* <root>/.trash/<name>-<timestamp>. A directory bundle is addressed by its
|
|
39
|
+
* SKILL.md path and moved as a whole directory.
|
|
40
|
+
* @returns the trashed path plus the original path that was moved.
|
|
41
|
+
*/
|
|
42
|
+
export declare function trashSkill(sourcePath: string): Promise<{
|
|
43
|
+
path: string;
|
|
44
|
+
source: string;
|
|
45
|
+
}>;
|
|
46
|
+
/** Restore a trashed skill (directory or flat file) to its original location. */
|
|
47
|
+
export declare function restoreSkill(entry: TrashEntry, home?: string): Promise<string>;
|
|
48
|
+
/** Permanently delete one trashed skill (directory or flat file). */
|
|
49
|
+
export declare function clearTrash(entry: TrashEntry, home?: string): Promise<string>;
|
|
35
50
|
/** One parsed frontmatter outcome (official provider semantics). */
|
|
36
51
|
export interface FrontmatterValue {
|
|
37
52
|
name: string;
|
|
38
53
|
description: string;
|
|
39
54
|
whenToUse?: string;
|
|
40
|
-
/** Optional group names the skill declares (frontmatter `sets`). */
|
|
41
|
-
sets?: string[];
|
|
42
55
|
invocation: {
|
|
43
56
|
modelInvocable: boolean;
|
|
44
57
|
userInvocable: boolean;
|
|
@@ -46,13 +59,6 @@ export interface FrontmatterValue {
|
|
|
46
59
|
/** Instruction body after the frontmatter block, trimmed. */
|
|
47
60
|
content: string;
|
|
48
61
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Normalize an optional frontmatter `sets` value into a clean string list.
|
|
51
|
-
* Accepts a single string or a string array; trims, drops empties, and
|
|
52
|
-
* ignores non-string entries. Returns undefined when absent or empty, so a
|
|
53
|
-
* skill without sets stays uncategorized.
|
|
54
|
-
*/
|
|
55
|
-
export declare function normalizeSets(raw: unknown): string[] | undefined;
|
|
56
62
|
/** Parse a SKILL.md the way the official filesystem provider does. */
|
|
57
63
|
export declare function parseFrontmatter(text: string): {
|
|
58
64
|
value: FrontmatterValue;
|
|
@@ -70,11 +76,10 @@ export interface SkillEntry {
|
|
|
70
76
|
/**
|
|
71
77
|
* Scan one skills root for discovery files: directory bundles (SKILL.md)
|
|
72
78
|
* and flat <name>.md files. Hub-disabled files (.disabled) are excluded;
|
|
73
|
-
*
|
|
74
|
-
* official provider's user-dsh behavior).
|
|
79
|
+
* dot-prefixed entries (including .trash and .system) are always skipped.
|
|
75
80
|
*/
|
|
76
|
-
export declare function scanRoot(base: string
|
|
77
|
-
/** Scan one writable root
|
|
81
|
+
export declare function scanRoot(base: string): Promise<SkillEntry[]>;
|
|
82
|
+
/** Scan one writable root. */
|
|
78
83
|
export declare function listSkillEntries(root: WritableRoot, home?: string): Promise<SkillEntry[]>;
|
|
79
84
|
/**
|
|
80
85
|
* Walk up from cwd (max 32 levels) for a project marker (.dsh or .git);
|
package/lib/types/stats.d.ts
CHANGED
|
@@ -26,11 +26,24 @@ export interface SessionQueryLike {
|
|
|
26
26
|
events: SessionEvent[];
|
|
27
27
|
}>;
|
|
28
28
|
}
|
|
29
|
-
/**
|
|
30
|
-
export
|
|
31
|
-
|
|
29
|
+
/** Per-skill invocation stats in one session's event log. */
|
|
30
|
+
export interface InvocationStat {
|
|
31
|
+
count: number;
|
|
32
|
+
/** Unix epoch ms of the most recent invocation in this batch. */
|
|
33
|
+
lastUsed: number;
|
|
34
|
+
}
|
|
35
|
+
/** Collect per-skill invocation counts and last-used times from one session. */
|
|
36
|
+
export declare function countSkillInvocations(events: readonly SessionEvent[]): Map<string, InvocationStat>;
|
|
37
|
+
/** Scan every session and total per-skill counts, keeping the latest lastUsed. */
|
|
32
38
|
export declare function readSkillStats(query: SessionQueryLike): Promise<SkillStat[]>;
|
|
33
39
|
/** A memoized stats reader (the panel polls, but logs change slowly). */
|
|
34
40
|
export type SkillStatsReader = () => Promise<SkillStat[]>;
|
|
35
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Wrap a query in a stale-while-revalidate cache: responses never wait for a
|
|
43
|
+
* full session-log scan. While the TTL is fresh the cached totals are
|
|
44
|
+
* returned; after expiry the stale totals are returned immediately and a
|
|
45
|
+
* single background rescan refreshes them — the panel's next poll picks the
|
|
46
|
+
* fresh numbers. A full scan decompresses every session log and can take
|
|
47
|
+
* seconds, so it must never sit on the request path.
|
|
48
|
+
*/
|
|
36
49
|
export declare function createSkillStatsReader(query: SessionQueryLike, ttlMs?: number): SkillStatsReader;
|
package/lib/types/store.d.ts
CHANGED
|
@@ -1,25 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hub sidecar store: remembers which skills the hub toggled off
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Hub sidecar store: remembers which skills the hub toggled off and the
|
|
3
|
+
* user's organization/tracking records. Disabling renames the skill's
|
|
4
|
+
* SKILL.md (or flat .md) out of the filesystem provider's discovery shapes,
|
|
5
|
+
* so the provider catalog alone cannot see disabled skills; this store keeps
|
|
6
|
+
* name/path/root so the GUI can list them and re-enable. It also persists
|
|
7
|
+
* user tag groups, upstream source records (repo + commit snapshot for
|
|
8
|
+
* update checks), the market source list, and the trash (skills removed
|
|
9
|
+
* after upstream deletion).
|
|
6
10
|
*
|
|
7
11
|
* State file: $DSH_HOME/dsh-skill-hub.json — a small JSON document written
|
|
8
12
|
* atomically (tmp file + rename).
|
|
9
13
|
*/
|
|
10
|
-
import type { DisabledSkill, HubConfig } from './protocol.ts';
|
|
14
|
+
import type { DisabledSkill, HubConfig, MarketSourceRecord, RepoRoot, SkillTag, SourceRecord, TrashEntry } from './protocol.ts';
|
|
15
|
+
/** 默认场景名(系统预置的兜底场景,新技能自动归入)。 */
|
|
16
|
+
export declare const DEFAULT_SCENE_NAME = "\u901A\u7528";
|
|
11
17
|
/** Resolve the DSH home directory (the filesystem provider's user-dsh root base). */
|
|
12
18
|
export declare function dshHome(): string;
|
|
13
19
|
/** Resolve the sidecar state path (injectable in tests). */
|
|
14
20
|
export declare function statePath(home?: string): string;
|
|
21
|
+
/** Current sidecar schema version. Bump on breaking shape changes and add a migration below. */
|
|
22
|
+
export declare const STORE_VERSION = 3;
|
|
23
|
+
/**
|
|
24
|
+
* Business-rule failure the routes layer can map onto a 4xx status instead
|
|
25
|
+
* of a blanket 500: user input is invalid (validation → 400), the target
|
|
26
|
+
* does not exist (not-found → 404), or the operation conflicts with an
|
|
27
|
+
* invariant (conflict → 409).
|
|
28
|
+
*/
|
|
29
|
+
export declare class StoreError extends Error {
|
|
30
|
+
readonly kind: 'validation' | 'not-found' | 'conflict';
|
|
31
|
+
constructor(kind: StoreError['kind'], message: string);
|
|
32
|
+
}
|
|
15
33
|
/** Sidecar state owner. */
|
|
16
34
|
export declare class SkillHubStore {
|
|
17
35
|
private readonly file;
|
|
18
36
|
private entries;
|
|
19
37
|
private config;
|
|
38
|
+
private tagsById;
|
|
39
|
+
private sourcesByRepo;
|
|
40
|
+
private marketSources;
|
|
41
|
+
private trashByName;
|
|
20
42
|
private loaded;
|
|
43
|
+
/** Serializes persist runs: concurrent mutators must not let an earlier
|
|
44
|
+
* snapshot overwrite a later one (rename is atomic, ordering is not). */
|
|
45
|
+
private writeChain;
|
|
21
46
|
constructor(file?: string);
|
|
22
47
|
private ensureLoaded;
|
|
48
|
+
/**
|
|
49
|
+
* 保证存在默认场景:没有任何 default 标记的 tag 时创建「通用」。
|
|
50
|
+
* 新技能创建后自动归入它;用户可改名,但默认场景不可删除。
|
|
51
|
+
*/
|
|
52
|
+
private ensureDefaultTag;
|
|
23
53
|
listDisabled(): Promise<DisabledSkill[]>;
|
|
24
54
|
getDisabled(name: string): Promise<DisabledSkill | undefined>;
|
|
25
55
|
addDisabled(entry: DisabledSkill): Promise<void>;
|
|
@@ -28,9 +58,85 @@ export declare class SkillHubStore {
|
|
|
28
58
|
getConfig(): Promise<Partial<HubConfig>>;
|
|
29
59
|
/**
|
|
30
60
|
* Persist a runtime-config patch, merged over the saved values. A field
|
|
31
|
-
* whose patch value is
|
|
61
|
+
* whose patch value is undefined is removed from the saved layer, so the
|
|
32
62
|
* setting re-inherits its default (the web card's "reset" path).
|
|
33
63
|
*/
|
|
34
64
|
setConfig(config: Partial<HubConfig>): Promise<void>;
|
|
65
|
+
/** All user-defined tag groups, in creation order. */
|
|
66
|
+
listTags(): Promise<SkillTag[]>;
|
|
67
|
+
/** One tag by id (undefined when absent). */
|
|
68
|
+
getTag(id: string): Promise<SkillTag | undefined>;
|
|
69
|
+
/**
|
|
70
|
+
* Create (no id) or rename (with id) a tag. Returns the saved tag.
|
|
71
|
+
* Creating assigns a fresh UUID; renaming keeps members.
|
|
72
|
+
*/
|
|
73
|
+
saveTag(input: {
|
|
74
|
+
id?: string;
|
|
75
|
+
name: string;
|
|
76
|
+
}): Promise<SkillTag>;
|
|
77
|
+
/** Delete a tag by id (no-op when absent). The default scene cannot be deleted. */
|
|
78
|
+
deleteTag(id: string): Promise<void>;
|
|
79
|
+
/** The default scene (「通用」), guaranteed to exist after ensureLoaded. */
|
|
80
|
+
getDefaultTag(): Promise<SkillTag | undefined>;
|
|
81
|
+
/** Append one skill name to a tag (deduplicated; no-op when already a member). */
|
|
82
|
+
addSkillToTag(id: string, name: string): Promise<SkillTag | undefined>;
|
|
83
|
+
/**
|
|
84
|
+
* Replace a tag's member list wholesale (idempotent). Deduplicates and
|
|
85
|
+
* drops blank names; unknown skill names are kept (they may arrive later),
|
|
86
|
+
* the routes layer filters against the live catalog before persisting.
|
|
87
|
+
*/
|
|
88
|
+
setTagMembers(id: string, skillNames: readonly string[]): Promise<SkillTag | undefined>;
|
|
89
|
+
/** Remove one skill from every tag group (used when the skill is deleted). */
|
|
90
|
+
removeSkillFromTags(name: string): Promise<void>;
|
|
91
|
+
/** All source records, sorted by repo. */
|
|
92
|
+
listSources(): Promise<SourceRecord[]>;
|
|
93
|
+
/** One source record by repo (undefined when absent). */
|
|
94
|
+
getSource(repo: string): Promise<SourceRecord | undefined>;
|
|
95
|
+
/** The source record that tracks a skill name (undefined when untracked). */
|
|
96
|
+
getSourceForSkill(name: string): Promise<SourceRecord | undefined>;
|
|
97
|
+
/** Remove a source record entirely (no-op when absent). */
|
|
98
|
+
deleteSource(repo: string): Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Upsert one skill into a source record. When the repo has no record yet a
|
|
101
|
+
* new one is created (root + commit snapshot from the caller).
|
|
102
|
+
*/
|
|
103
|
+
addSourceSkill(repo: string, root: RepoRoot, commitSha: string, ref: string | undefined, skillName: string): Promise<void>;
|
|
104
|
+
/** Replace a source's skill list (used after sync/confirm-delete). */
|
|
105
|
+
setSourceSkills(repo: string, skills: readonly string[]): Promise<SourceRecord | undefined>;
|
|
106
|
+
/** Remove one skill from every source record (used when the skill is deleted). */
|
|
107
|
+
removeSkillFromSources(name: string): Promise<void>;
|
|
108
|
+
/** Update a source's commit snapshot. */
|
|
109
|
+
setSourceCommit(repo: string, commitSha: string): Promise<void>;
|
|
110
|
+
/** Update a source's pinned ref (release tag / branch) when the market syncs. */
|
|
111
|
+
setSourceRef(repo: string, ref: string): Promise<void>;
|
|
112
|
+
/**
|
|
113
|
+
* Merge per-path manifest entries into a source (incremental imports).
|
|
114
|
+
* When `dir` is given, every baseline path under that skill directory is
|
|
115
|
+
* dropped first, so files the upstream removed never linger in the
|
|
116
|
+
* baseline and skew later update diffs.
|
|
117
|
+
*/
|
|
118
|
+
mergeSourceManifest(repo: string, manifest: Record<string, number>, dir?: string): Promise<void>;
|
|
119
|
+
/** skillName → collection name for every recorded origin (derived from sources). */
|
|
120
|
+
listOrigins(): Promise<Record<string, string>>;
|
|
121
|
+
/** The user's market sources, in addition order. */
|
|
122
|
+
listMarketSources(): Promise<MarketSourceRecord[]>;
|
|
123
|
+
/** One market source by repo (undefined when absent). */
|
|
124
|
+
getMarketSource(repo: string): Promise<MarketSourceRecord | undefined>;
|
|
125
|
+
/** Add a repo (deduplicated), optionally with a pinned ref. Returns the fresh list. */
|
|
126
|
+
addMarketSource(repo: string, ref?: string): Promise<MarketSourceRecord[]>;
|
|
127
|
+
/** Remove a repo (no-op when absent). Returns the fresh list. */
|
|
128
|
+
removeMarketSource(repo: string): Promise<MarketSourceRecord[]>;
|
|
129
|
+
/** Pin a market source to an explicit ref (branch/tag). Returns the record. */
|
|
130
|
+
setMarketSourceRef(repo: string, ref: string): Promise<MarketSourceRecord | undefined>;
|
|
131
|
+
/** Record the commit a market source's pinned ref resolved to (update baseline). */
|
|
132
|
+
setMarketSourceCommit(repo: string, commitSha: string): Promise<void>;
|
|
133
|
+
/** All trashed skills, newest first. */
|
|
134
|
+
listTrash(): Promise<TrashEntry[]>;
|
|
135
|
+
/** One trash entry by skill name (undefined when absent). */
|
|
136
|
+
getTrash(name: string): Promise<TrashEntry | undefined>;
|
|
137
|
+
/** Record a trashed skill. */
|
|
138
|
+
addTrash(entry: TrashEntry): Promise<void>;
|
|
139
|
+
/** Remove a trash record (after restore). */
|
|
140
|
+
removeTrash(name: string): Promise<void>;
|
|
35
141
|
private persist;
|
|
36
142
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-update check for dsh-skill-hub, mirroring cc-switch's behavior:
|
|
3
|
+
* query GitHub's latest release and compare it to the installed version.
|
|
4
|
+
*
|
|
5
|
+
* This stays dependency-free: Node's global fetch is used and the version
|
|
6
|
+
* comparison is a small semver-subset parser (the plugin only publishes
|
|
7
|
+
* plain x.y.z tags, optionally prefixed with `v`).
|
|
8
|
+
*/
|
|
9
|
+
import type { UpdateCheckResponse } from './protocol.ts';
|
|
10
|
+
/** Repository checked for releases. Keep in sync with package.json. */
|
|
11
|
+
export declare const UPDATE_REPO = "cheshireez/dsh-skill-hub";
|
|
12
|
+
export declare const CURRENT_VERSION: string;
|
|
13
|
+
/** Normalize a release tag (`v1.2.3` → `1.2.3`) and parse it numerically. */
|
|
14
|
+
export declare function parseVersion(tag: string): [number, number, number] | null;
|
|
15
|
+
/** Compare two dotted version strings: negative when a < b, 0 equal, positive when a > b. */
|
|
16
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
17
|
+
/** True when latest is strictly newer than current. */
|
|
18
|
+
export declare function isUpdateAvailable(currentVersion: string, latestVersion: string): boolean;
|
|
19
|
+
/** Best-effort GitHub latest-release lookup. Always resolves with ok:true. */
|
|
20
|
+
export declare function checkLatestRelease(repo?: string, fetchImpl?: typeof fetch): Promise<UpdateCheckResponse>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-skill-hub",
|
|
3
|
-
"version": "0.
|
|
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 —
|
|
3
|
+
"version": "0.2.0",
|
|
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": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
|
|
47
47
|
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
48
48
|
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
49
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
|
|
49
50
|
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
|
|
50
51
|
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
51
52
|
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
|
|
63
64
|
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
64
65
|
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
|
|
65
67
|
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
|
|
66
68
|
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.0-rc.6",
|
|
67
69
|
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
@@ -122,4 +124,4 @@
|
|
|
122
124
|
"url": "https://github.com/cheshireez/dsh-skill-hub/issues"
|
|
123
125
|
},
|
|
124
126
|
"homepage": "https://github.com/cheshireez/dsh-skill-hub"
|
|
125
|
-
}
|
|
127
|
+
}
|
|
@@ -9,13 +9,19 @@
|
|
|
9
9
|
|
|
10
10
|
import type { ReactElement } from 'react'
|
|
11
11
|
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
|
12
|
-
import {
|
|
13
|
-
import { booleanField, CardForm, type CardShell, type FieldState, type FormScope } from './settings-form.ts'
|
|
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
|
+
import { DEFAULT_DOT_MODEL_COLOR, DEFAULT_DOT_USER_COLOR } from './api-config-scope.ts'
|
|
14
15
|
|
|
15
16
|
/** The card's projected state. */
|
|
16
17
|
export interface SkillHubSettingsState extends CardShell {
|
|
17
18
|
enabled: FieldState
|
|
18
19
|
announceToAgent: FieldState
|
|
20
|
+
dotModelColor: FieldState
|
|
21
|
+
dotUserColor: FieldState
|
|
22
|
+
showUseCount: FieldState
|
|
23
|
+
showUseTime: FieldState
|
|
24
|
+
showGroupSummary: FieldState
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
/** Props the slot renderer receives (locale copy + injected form actions). */
|
|
@@ -35,7 +41,7 @@ export class SkillHubSettingsCardController {
|
|
|
35
41
|
|
|
36
42
|
/** @param scope - the hub config scope the card edits (ApiConfigScope). */
|
|
37
43
|
constructor(scope: FormScope) {
|
|
38
|
-
this.form = new CardForm(scope, [booleanField('enabled'), booleanField('announceToAgent')])
|
|
44
|
+
this.form = new CardForm(scope, [booleanField('enabled'), booleanField('announceToAgent'), colorField('dotModelColor'), colorField('dotUserColor'), booleanField('showUseCount'), booleanField('showUseTime'), booleanField('showGroupSummary')])
|
|
39
45
|
this.store = this.form.bind(() => this.projection())
|
|
40
46
|
}
|
|
41
47
|
|
|
@@ -44,6 +50,11 @@ export class SkillHubSettingsCardController {
|
|
|
44
50
|
...this.form.shell(),
|
|
45
51
|
enabled: this.form.field('enabled'),
|
|
46
52
|
announceToAgent: this.form.field('announceToAgent'),
|
|
53
|
+
dotModelColor: this.form.field('dotModelColor'),
|
|
54
|
+
dotUserColor: this.form.field('dotUserColor'),
|
|
55
|
+
showUseCount: this.form.field('showUseCount'),
|
|
56
|
+
showUseTime: this.form.field('showUseTime'),
|
|
57
|
+
showGroupSummary: this.form.field('showGroupSummary'),
|
|
47
58
|
}
|
|
48
59
|
}
|
|
49
60
|
|
|
@@ -90,18 +101,60 @@ export function SkillHubSettingsCard(props: SkillHubSettingsCardProps): ReactEle
|
|
|
90
101
|
onEdit={(text) => { props.edit('enabled', text) }}
|
|
91
102
|
onReset={() => { props.resetField('enabled') }}
|
|
92
103
|
/>
|
|
93
|
-
<
|
|
94
|
-
id='settings-skill-hub-announce'
|
|
104
|
+
<SwitchField
|
|
95
105
|
label={t('settings.announceToAgent')}
|
|
96
106
|
hint={t('settings.announceToAgentHint')}
|
|
97
|
-
inheritLabel={t('settings.inherit')}
|
|
98
|
-
onLabel={t('settings.on')}
|
|
99
|
-
offLabel={t('settings.off')}
|
|
100
107
|
{...fieldProps}
|
|
101
108
|
{...state.announceToAgent}
|
|
102
109
|
onEdit={(text) => { props.edit('announceToAgent', text) }}
|
|
103
110
|
onReset={() => { props.resetField('announceToAgent') }}
|
|
104
111
|
/>
|
|
112
|
+
<ColorField
|
|
113
|
+
id='skill-hub-dot-model-color'
|
|
114
|
+
label={t('settings.dotModelColor')}
|
|
115
|
+
hint={t('settings.dotModelColorHint')}
|
|
116
|
+
inheritLabel={t('settings.inherit')}
|
|
117
|
+
defaultColor={DEFAULT_DOT_MODEL_COLOR}
|
|
118
|
+
{...fieldProps}
|
|
119
|
+
{...state.dotModelColor}
|
|
120
|
+
onEdit={(text) => { props.edit('dotModelColor', text) }}
|
|
121
|
+
onReset={() => { props.resetField('dotModelColor') }}
|
|
122
|
+
/>
|
|
123
|
+
<ColorField
|
|
124
|
+
id='skill-hub-dot-user-color'
|
|
125
|
+
label={t('settings.dotUserColor')}
|
|
126
|
+
hint={t('settings.dotUserColorHint')}
|
|
127
|
+
inheritLabel={t('settings.inherit')}
|
|
128
|
+
defaultColor={DEFAULT_DOT_USER_COLOR}
|
|
129
|
+
{...fieldProps}
|
|
130
|
+
{...state.dotUserColor}
|
|
131
|
+
onEdit={(text) => { props.edit('dotUserColor', text) }}
|
|
132
|
+
onReset={() => { props.resetField('dotUserColor') }}
|
|
133
|
+
/>
|
|
134
|
+
<SwitchField
|
|
135
|
+
label={t('settings.showUseCount')}
|
|
136
|
+
hint={t('settings.showUseCountHint')}
|
|
137
|
+
{...fieldProps}
|
|
138
|
+
{...state.showUseCount}
|
|
139
|
+
onEdit={(text) => { props.edit('showUseCount', text) }}
|
|
140
|
+
onReset={() => { props.resetField('showUseCount') }}
|
|
141
|
+
/>
|
|
142
|
+
<SwitchField
|
|
143
|
+
label={t('settings.showUseTime')}
|
|
144
|
+
hint={t('settings.showUseTimeHint')}
|
|
145
|
+
{...fieldProps}
|
|
146
|
+
{...state.showUseTime}
|
|
147
|
+
onEdit={(text) => { props.edit('showUseTime', text) }}
|
|
148
|
+
onReset={() => { props.resetField('showUseTime') }}
|
|
149
|
+
/>
|
|
150
|
+
<SwitchField
|
|
151
|
+
label={t('settings.showGroupSummary')}
|
|
152
|
+
hint={t('settings.showGroupSummaryHint')}
|
|
153
|
+
{...fieldProps}
|
|
154
|
+
{...state.showGroupSummary}
|
|
155
|
+
onEdit={(text) => { props.edit('showGroupSummary', text) }}
|
|
156
|
+
onReset={() => { props.resetField('showGroupSummary') }}
|
|
157
|
+
/>
|
|
105
158
|
</PluginSettingsCard>
|
|
106
159
|
)
|
|
107
160
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { ConfigRequest, ConfigResponse, HubConfig } from '../protocol.ts'
|
|
3
|
+
import type { SkillHubApi } from './api.ts'
|
|
4
|
+
import { ApiConfigScope } from './api-config-scope.ts'
|
|
5
|
+
|
|
6
|
+
function deferredReady(scope: ApiConfigScope): Promise<void> {
|
|
7
|
+
return new Promise((resolve) => setTimeout(resolve, 0))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('ApiConfigScope', () => {
|
|
11
|
+
it('persists the panel display toggles (showUseCount etc.)', async () => {
|
|
12
|
+
const saved: Partial<HubConfig> = {}
|
|
13
|
+
const writes: ConfigRequest[] = []
|
|
14
|
+
const api = {
|
|
15
|
+
config: async (): Promise<ConfigResponse> => ({
|
|
16
|
+
ok: true,
|
|
17
|
+
config: {
|
|
18
|
+
enabled: true,
|
|
19
|
+
announceToAgent: true,
|
|
20
|
+
showUseCount: true,
|
|
21
|
+
showUseTime: true,
|
|
22
|
+
showGroupSummary: true,
|
|
23
|
+
},
|
|
24
|
+
saved,
|
|
25
|
+
}),
|
|
26
|
+
saveConfig: async (patch: ConfigRequest): Promise<ConfigResponse> => {
|
|
27
|
+
writes.push(patch)
|
|
28
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
29
|
+
if (value === null) delete saved[key as keyof HubConfig]
|
|
30
|
+
else saved[key as keyof HubConfig] = value as never
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
ok: true,
|
|
34
|
+
config: {
|
|
35
|
+
enabled: true,
|
|
36
|
+
announceToAgent: true,
|
|
37
|
+
showUseCount: saved.showUseCount !== false,
|
|
38
|
+
showUseTime: saved.showUseTime !== false,
|
|
39
|
+
showGroupSummary: saved.showGroupSummary !== false,
|
|
40
|
+
},
|
|
41
|
+
saved,
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
} as unknown as SkillHubApi
|
|
45
|
+
|
|
46
|
+
const scope = new ApiConfigScope(api)
|
|
47
|
+
await deferredReady(scope)
|
|
48
|
+
|
|
49
|
+
await scope.set('showUseCount', false)
|
|
50
|
+
expect(writes).toContainEqual({ showUseCount: false })
|
|
51
|
+
expect(scope.getSnapshot().value).toMatchObject({ showUseCount: false })
|
|
52
|
+
expect(scope.getSnapshot().user).toMatchObject({ showUseCount: false })
|
|
53
|
+
|
|
54
|
+
await scope.unset('showUseCount')
|
|
55
|
+
expect(writes).toContainEqual({ showUseCount: null })
|
|
56
|
+
expect(scope.getSnapshot().user).not.toHaveProperty('showUseCount')
|
|
57
|
+
expect(scope.getSnapshot().value).toMatchObject({ showUseCount: true })
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('ignores unknown fields', async () => {
|
|
61
|
+
const writes: ConfigRequest[] = []
|
|
62
|
+
const api = {
|
|
63
|
+
config: async (): Promise<ConfigResponse> => ({
|
|
64
|
+
ok: true,
|
|
65
|
+
config: { enabled: true, announceToAgent: true },
|
|
66
|
+
saved: {},
|
|
67
|
+
}),
|
|
68
|
+
saveConfig: async (patch: ConfigRequest): Promise<ConfigResponse> => {
|
|
69
|
+
writes.push(patch)
|
|
70
|
+
return { ok: true, config: { enabled: true, announceToAgent: true }, saved: {} }
|
|
71
|
+
},
|
|
72
|
+
} as unknown as SkillHubApi
|
|
73
|
+
|
|
74
|
+
const scope = new ApiConfigScope(api)
|
|
75
|
+
await deferredReady(scope)
|
|
76
|
+
await scope.set('not-a-field', true)
|
|
77
|
+
expect(writes).toHaveLength(0)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -17,11 +17,32 @@ import type { HubConfig } from '../protocol.ts'
|
|
|
17
17
|
import type { SkillHubApi } from './api.ts'
|
|
18
18
|
import type { FormScope } from './settings-form.ts'
|
|
19
19
|
|
|
20
|
+
/** Model-invocable dot color default. Single source for the TS side; the
|
|
21
|
+
* panel's CSS mirrors it via --hub-model (panel.module.css). */
|
|
22
|
+
export const DEFAULT_DOT_MODEL_COLOR = '#2f81f7'
|
|
23
|
+
/** User-invocable dot color default. Single source for the TS side; the
|
|
24
|
+
* panel's CSS mirrors it via --hub-user (panel.module.css). */
|
|
25
|
+
export const DEFAULT_DOT_USER_COLOR = '#3fb950'
|
|
26
|
+
|
|
20
27
|
/** Built-in defaults every field inherits until the user overrides it. */
|
|
21
|
-
const DEFAULTS: HubConfig = {
|
|
28
|
+
const DEFAULTS: HubConfig = {
|
|
29
|
+
enabled: true,
|
|
30
|
+
announceToAgent: true,
|
|
31
|
+
showUseCount: true,
|
|
32
|
+
showUseTime: true,
|
|
33
|
+
showGroupSummary: true,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Boolean config fields (color fields are strings). */
|
|
37
|
+
const BOOLEAN_FIELDS = new Set<keyof HubConfig>([
|
|
38
|
+
'enabled', 'announceToAgent', 'showUseCount', 'showUseTime', 'showGroupSummary',
|
|
39
|
+
])
|
|
22
40
|
|
|
23
41
|
/** Config fields the card edits (used to guard set/unset field names). */
|
|
24
|
-
const FIELDS = new Set<keyof HubConfig>([
|
|
42
|
+
const FIELDS = new Set<keyof HubConfig>([
|
|
43
|
+
'enabled', 'announceToAgent', 'dotModelColor', 'dotUserColor',
|
|
44
|
+
'showUseCount', 'showUseTime', 'showGroupSummary',
|
|
45
|
+
])
|
|
25
46
|
|
|
26
47
|
/** FormScope-compatible reactive config handle (see module doc). */
|
|
27
48
|
export class ApiConfigScope implements FormScope {
|
|
@@ -67,7 +88,10 @@ export class ApiConfigScope implements FormScope {
|
|
|
67
88
|
/** Write one field through the config route and adopt the persisted state. */
|
|
68
89
|
async set(field: string, value: unknown): Promise<void> {
|
|
69
90
|
if (!FIELDS.has(field as keyof HubConfig)) return
|
|
70
|
-
|
|
91
|
+
const patch = BOOLEAN_FIELDS.has(field as keyof HubConfig)
|
|
92
|
+
? { [field]: Boolean(value) }
|
|
93
|
+
: { [field]: typeof value === 'string' ? value : undefined }
|
|
94
|
+
await this.apply(patch as Partial<HubConfig>)
|
|
71
95
|
}
|
|
72
96
|
|
|
73
97
|
/** Clear one field's override so it re-inherits the default. */
|
|
@@ -79,7 +103,7 @@ export class ApiConfigScope implements FormScope {
|
|
|
79
103
|
/** Persist a patch, then re-seed from the route's fresh response. */
|
|
80
104
|
private async apply(patch: Partial<HubConfig>): Promise<void> {
|
|
81
105
|
// null on the wire clears a saved override (see ConfigRequest).
|
|
82
|
-
const request: Record<string, boolean | null> = {}
|
|
106
|
+
const request: Record<string, boolean | string | null> = {}
|
|
83
107
|
for (const [key, value] of Object.entries(patch)) {
|
|
84
108
|
request[key] = value === undefined ? null : value
|
|
85
109
|
}
|