dsh-skill-hub 0.3.12 → 0.3.13
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 +4 -2
- package/README.md +2 -1
- package/README.zh.md +2 -1
- package/lib/client.js +85 -41
- package/lib/client.js.map +1 -1
- package/lib/index.js +95 -0
- package/lib/types/client/grouping.d.ts +18 -1
- package/lib/types/reconcile.d.ts +24 -0
- package/lib/types/skillfs/scan.d.ts +7 -0
- package/package.json +29 -28
- package/src/client/grouping.test.ts +35 -2
- package/src/client/grouping.ts +38 -1
- package/src/client/panel/PanelDialogs.tsx +8 -2
- package/src/client/panel/SourcesView.tsx +12 -12
- package/src/client/panel/dialogs.tsx +45 -18
- package/src/index.ts +11 -0
- package/src/reconcile.test.ts +76 -0
- package/src/reconcile.ts +62 -0
- package/src/skillfs/scan.ts +38 -0
package/lib/index.js
CHANGED
|
@@ -1123,6 +1123,39 @@ async function scanRoot(base) {
|
|
|
1123
1123
|
function listSkillEntries(root, home = dshHome()) {
|
|
1124
1124
|
return scanRoot(rootPath(root, home));
|
|
1125
1125
|
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Scan one skills root for hub-disabled discovery files: directory bundles
|
|
1128
|
+
* renamed to SKILL.md.disabled and flat <name>.md.disabled files. Used by
|
|
1129
|
+
* the startup reconcile to rebuild sidecar records that were lost, which
|
|
1130
|
+
* would otherwise leave the skill invisible in every view.
|
|
1131
|
+
*/
|
|
1132
|
+
async function scanDisabledRoot(base) {
|
|
1133
|
+
const paths = [];
|
|
1134
|
+
let names;
|
|
1135
|
+
try {
|
|
1136
|
+
names = await readdir(base);
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
if (error.code === "ENOENT") return paths;
|
|
1139
|
+
throw error;
|
|
1140
|
+
}
|
|
1141
|
+
for (const name of names) {
|
|
1142
|
+
if (name.startsWith(".")) continue;
|
|
1143
|
+
const absolute = join(base, name);
|
|
1144
|
+
let stats;
|
|
1145
|
+
try {
|
|
1146
|
+
stats = await stat(absolute);
|
|
1147
|
+
} catch {
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
if (stats.isDirectory()) {
|
|
1151
|
+
const candidate = join(absolute, "SKILL.md.disabled");
|
|
1152
|
+
try {
|
|
1153
|
+
if ((await stat(candidate)).isFile()) paths.push(candidate);
|
|
1154
|
+
} catch {}
|
|
1155
|
+
} else if (name.endsWith(".md.disabled") && name !== "SKILL.md.disabled") paths.push(absolute);
|
|
1156
|
+
}
|
|
1157
|
+
return paths.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
1158
|
+
}
|
|
1126
1159
|
/** Read UI metadata from `agents/openai.yaml` beside a directory skill (mirrors codex SkillInterface). */
|
|
1127
1160
|
async function readSkillInterface(directory) {
|
|
1128
1161
|
const yamlPath = join(directory, "agents", "openai.yaml");
|
|
@@ -4401,6 +4434,62 @@ function createSkillStatsReader(query, ttlMs = 3e5, options = {}) {
|
|
|
4401
4434
|
return reader;
|
|
4402
4435
|
}
|
|
4403
4436
|
//#endregion
|
|
4437
|
+
//#region src/reconcile.ts
|
|
4438
|
+
/**
|
|
4439
|
+
* Sidecar reconcile for hub-disabled skills.
|
|
4440
|
+
*
|
|
4441
|
+
* The catalog merges two sources: enabled skills come from the provider
|
|
4442
|
+
* (SKILL.md discovery), disabled skills come from the sidecar's `disabled`
|
|
4443
|
+
* records. The two can drift — a sidecar restored from a backup, a hand
|
|
4444
|
+
* edit, or an older build can leave a `SKILL.md.disabled` file on disk with
|
|
4445
|
+
* no record, which makes the skill invisible in every view (it is neither
|
|
4446
|
+
* enabled nor disabled) and leaves its origin collection rendering as an
|
|
4447
|
+
* empty shell. This walk rebuilds the missing records from disk at startup.
|
|
4448
|
+
*/
|
|
4449
|
+
/**
|
|
4450
|
+
* Add sidecar disabled records for every `.disabled` discovery file that has
|
|
4451
|
+
* none yet. Existing records (matched by path or by name) win, so this never
|
|
4452
|
+
* rewrites user data; unreadable or invalid files are skipped (they surface
|
|
4453
|
+
* in the diagnostics scan instead). Returns the records added.
|
|
4454
|
+
*/
|
|
4455
|
+
async function reconcileDisabledSkills(store, home = dshHome()) {
|
|
4456
|
+
const known = await store.listDisabled();
|
|
4457
|
+
const knownNames = new Set(known.map((entry) => entry.name));
|
|
4458
|
+
const knownPaths = new Set(known.map((entry) => entry.path));
|
|
4459
|
+
const added = [];
|
|
4460
|
+
for (const root of WRITABLE_ROOTS) for (const path of await scanDisabledRoot(rootPath(root, home))) {
|
|
4461
|
+
if (knownPaths.has(path)) continue;
|
|
4462
|
+
let text;
|
|
4463
|
+
try {
|
|
4464
|
+
text = await readFile(path, "utf8");
|
|
4465
|
+
} catch {
|
|
4466
|
+
continue;
|
|
4467
|
+
}
|
|
4468
|
+
const parsed = parseFrontmatter(text);
|
|
4469
|
+
if ("error" in parsed) continue;
|
|
4470
|
+
const { name, description } = parsed.value;
|
|
4471
|
+
if (knownNames.has(name)) continue;
|
|
4472
|
+
let disabledAt = 0;
|
|
4473
|
+
try {
|
|
4474
|
+
disabledAt = (await stat(path)).mtimeMs;
|
|
4475
|
+
} catch {
|
|
4476
|
+
continue;
|
|
4477
|
+
}
|
|
4478
|
+
const record = {
|
|
4479
|
+
name,
|
|
4480
|
+
description,
|
|
4481
|
+
path,
|
|
4482
|
+
root,
|
|
4483
|
+
disabledAt
|
|
4484
|
+
};
|
|
4485
|
+
await store.addDisabled(record);
|
|
4486
|
+
knownNames.add(name);
|
|
4487
|
+
knownPaths.add(path);
|
|
4488
|
+
added.push(record);
|
|
4489
|
+
}
|
|
4490
|
+
return added;
|
|
4491
|
+
}
|
|
4492
|
+
//#endregion
|
|
4404
4493
|
//#region src/index.ts
|
|
4405
4494
|
/** Stable cordis plugin name (matches cordis.patch.yml insert id). */
|
|
4406
4495
|
const name = "skill-hub";
|
|
@@ -4527,6 +4616,12 @@ function apply(ctx, config) {
|
|
|
4527
4616
|
} catch (error) {
|
|
4528
4617
|
ctx.logger.warn("[dsh-skill-hub] startup cleanup failed", error);
|
|
4529
4618
|
}
|
|
4619
|
+
try {
|
|
4620
|
+
const reconciled = await reconcileDisabledSkills(store, home);
|
|
4621
|
+
if (reconciled.length > 0) ctx.logger.info(`[dsh-skill-hub] startup reconciled ${reconciled.length} disabled skill record(s): ${reconciled.map((entry) => entry.name).join(", ")}`);
|
|
4622
|
+
} catch (error) {
|
|
4623
|
+
ctx.logger.warn("[dsh-skill-hub] startup disabled-skill reconcile failed", error);
|
|
4624
|
+
}
|
|
4530
4625
|
})();
|
|
4531
4626
|
(async () => {
|
|
4532
4627
|
try {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Closing a group whose member is enabled in another group is a conflict the
|
|
13
13
|
* GUI resolves with a dialog; the helpers below compute both sides.
|
|
14
14
|
*/
|
|
15
|
-
import type { CatalogSkill, CollectionGroup, SkillTag } from '../protocol.ts';
|
|
15
|
+
import type { CatalogSkill, CollectionGroup, DisabledSkill, SkillTag } from '../protocol.ts';
|
|
16
16
|
import { isProjectSource } from '../protocol.ts';
|
|
17
17
|
export { isProjectSource };
|
|
18
18
|
/** Grouped switch state derived from member enablement. */
|
|
@@ -50,6 +50,23 @@ export declare const PRIVATE_SOURCE = "private";
|
|
|
50
50
|
* 项目级技能(有 workspace 归属)永远不算「个人」。
|
|
51
51
|
*/
|
|
52
52
|
export declare function filterBySource(skills: readonly CatalogSkill[], source: string, origins: Readonly<Record<string, string>>): CatalogSkill[];
|
|
53
|
+
/** One origin collection with the members visible under the current filters. */
|
|
54
|
+
export interface VisibleCollection {
|
|
55
|
+
collection: CollectionGroup;
|
|
56
|
+
/** Enabled, currently visible members. */
|
|
57
|
+
skills: CatalogSkill[];
|
|
58
|
+
/** Disabled records passing the current name/description filter. */
|
|
59
|
+
disabledMembers: DisabledSkill[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Match origin collections against the currently visible enabled skills and
|
|
63
|
+
* disabled records, dropping collections with no visible member. Without
|
|
64
|
+
* this, a stale origin (skill deleted on disk, or a `.disabled` file whose
|
|
65
|
+
* sidecar record was lost) renders a group header with zero rows — an empty
|
|
66
|
+
* shell the sources tab otherwise never shows (project/personal groups
|
|
67
|
+
* already disappear when they have nothing to display).
|
|
68
|
+
*/
|
|
69
|
+
export declare function visibleCollections(collections: readonly CollectionGroup[], visibleSkills: readonly CatalogSkill[], disabledRecords: readonly DisabledSkill[], normalized: string, sourceFilter: string, origins: Readonly<Record<string, string>>): VisibleCollection[];
|
|
53
70
|
/** Catalog sort keys offered by the filter bar. */
|
|
54
71
|
export type SortKey = 'name' | 'added' | 'uses';
|
|
55
72
|
/**
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidecar reconcile for hub-disabled skills.
|
|
3
|
+
*
|
|
4
|
+
* The catalog merges two sources: enabled skills come from the provider
|
|
5
|
+
* (SKILL.md discovery), disabled skills come from the sidecar's `disabled`
|
|
6
|
+
* records. The two can drift — a sidecar restored from a backup, a hand
|
|
7
|
+
* edit, or an older build can leave a `SKILL.md.disabled` file on disk with
|
|
8
|
+
* no record, which makes the skill invisible in every view (it is neither
|
|
9
|
+
* enabled nor disabled) and leaves its origin collection rendering as an
|
|
10
|
+
* empty shell. This walk rebuilds the missing records from disk at startup.
|
|
11
|
+
*/
|
|
12
|
+
import type { DisabledSkill } from './protocol.ts';
|
|
13
|
+
/** Narrow store view used by the reconcile (SkillHubStore satisfies it). */
|
|
14
|
+
export interface DisabledReconcileStore {
|
|
15
|
+
listDisabled(): Promise<DisabledSkill[]>;
|
|
16
|
+
addDisabled(entry: DisabledSkill): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Add sidecar disabled records for every `.disabled` discovery file that has
|
|
20
|
+
* none yet. Existing records (matched by path or by name) win, so this never
|
|
21
|
+
* rewrites user data; unreadable or invalid files are skipped (they surface
|
|
22
|
+
* in the diagnostics scan instead). Returns the records added.
|
|
23
|
+
*/
|
|
24
|
+
export declare function reconcileDisabledSkills(store: DisabledReconcileStore, home?: string): Promise<DisabledSkill[]>;
|
|
@@ -15,6 +15,13 @@ export interface SkillEntry {
|
|
|
15
15
|
export declare function scanRoot(base: string): Promise<SkillEntry[]>;
|
|
16
16
|
/** Scan one writable root. */
|
|
17
17
|
export declare function listSkillEntries(root: WritableRoot, home?: string): Promise<SkillEntry[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Scan one skills root for hub-disabled discovery files: directory bundles
|
|
20
|
+
* renamed to SKILL.md.disabled and flat <name>.md.disabled files. Used by
|
|
21
|
+
* the startup reconcile to rebuild sidecar records that were lost, which
|
|
22
|
+
* would otherwise leave the skill invisible in every view.
|
|
23
|
+
*/
|
|
24
|
+
export declare function scanDisabledRoot(base: string): Promise<string[]>;
|
|
18
25
|
/** UI metadata from `agents/openai.yaml` beside a directory skill (mirrors codex SkillInterface). */
|
|
19
26
|
export interface SkillInterface {
|
|
20
27
|
displayName?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-skill-hub",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
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": {
|
|
@@ -72,7 +72,8 @@
|
|
|
72
72
|
"0.1.5-alpha.1",
|
|
73
73
|
"0.1.5-alpha.2",
|
|
74
74
|
"0.1.5-rc.1",
|
|
75
|
-
"0.1.5-rc.2"
|
|
75
|
+
"0.1.5-rc.2",
|
|
76
|
+
"0.1.6-alpha.1"
|
|
76
77
|
]
|
|
77
78
|
},
|
|
78
79
|
"capability": {
|
|
@@ -94,19 +95,19 @@
|
|
|
94
95
|
},
|
|
95
96
|
"peerDependencies": {
|
|
96
97
|
"@deepseek-ai/cordis": "^4.0.1 || ^4.0.2",
|
|
97
|
-
"@deepseek-ai/dsh-client-connection": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0",
|
|
98
|
-
"@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0",
|
|
99
|
-
"@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0",
|
|
100
|
-
"@deepseek-ai/dsh-client-store": ">=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0",
|
|
101
|
-
"@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 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0",
|
|
102
|
-
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0",
|
|
103
|
-
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0",
|
|
104
|
-
"@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0",
|
|
105
|
-
"@deepseek-ai/dsh-session": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0",
|
|
106
|
-
"@deepseek-ai/dsh-session-query": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0",
|
|
107
|
-
"@deepseek-ai/dsh-settings": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0",
|
|
108
|
-
"@deepseek-ai/dsh-skill": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0",
|
|
109
|
-
"@deepseek-ai/dsh-system-prompt": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0",
|
|
98
|
+
"@deepseek-ai/dsh-client-connection": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0 || >=0.1.6-alpha.1 <0.2.0-0",
|
|
99
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0 || >=0.1.6-alpha.1 <0.2.0-0",
|
|
100
|
+
"@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0 || >=0.1.6-alpha.1 <0.2.0-0",
|
|
101
|
+
"@deepseek-ai/dsh-client-store": ">=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0 || >=0.1.6-alpha.1 <0.2.0-0",
|
|
102
|
+
"@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 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0 || >=0.1.6-alpha.1 <0.2.0-0",
|
|
103
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0 || >=0.1.6-alpha.1 <0.2.0-0",
|
|
104
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0 || >=0.1.6-alpha.1 <0.2.0-0",
|
|
105
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0 || >=0.1.6-alpha.1 <0.2.0-0",
|
|
106
|
+
"@deepseek-ai/dsh-session": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0 || >=0.1.6-alpha.1 <0.2.0-0",
|
|
107
|
+
"@deepseek-ai/dsh-session-query": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0 || >=0.1.6-alpha.1 <0.2.0-0",
|
|
108
|
+
"@deepseek-ai/dsh-settings": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0 || >=0.1.6-alpha.1 <0.2.0-0",
|
|
109
|
+
"@deepseek-ai/dsh-skill": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0 || >=0.1.6-alpha.1 <0.2.0-0",
|
|
110
|
+
"@deepseek-ai/dsh-system-prompt": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.2 <0.2.0-0 || >=0.1.2-alpha.2 <0.2.0-0 || >=0.1.2-alpha.3 <0.2.0-0 || >=0.1.2-alpha.4 <0.2.0-0 || >=0.1.2-alpha.5 <0.2.0-0 || >=0.1.2-rc.1 <0.2.0-0 || >=0.1.3-alpha.2 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0 || >=0.1.5-alpha.2 <0.2.0-0 || >=0.1.5-rc.1 <0.2.0-0 || >=0.1.5-rc.2 <0.2.0-0 || >=0.1.6-alpha.1 <0.2.0-0",
|
|
110
111
|
"react": "^18.2.0",
|
|
111
112
|
"react-dom": "^18.2.0"
|
|
112
113
|
},
|
|
@@ -120,19 +121,19 @@
|
|
|
120
121
|
},
|
|
121
122
|
"devDependencies": {
|
|
122
123
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
123
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.
|
|
124
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.
|
|
125
|
-
"@deepseek-ai/dsh-client-store": "^0.1.
|
|
126
|
-
"@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.
|
|
127
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.
|
|
128
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.
|
|
129
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.
|
|
130
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.
|
|
131
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
132
|
-
"@deepseek-ai/dsh-session-query": "^0.1.
|
|
133
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
134
|
-
"@deepseek-ai/dsh-skill": "^0.1.
|
|
135
|
-
"@deepseek-ai/dsh-system-prompt": "^0.1.
|
|
124
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.6-alpha.1",
|
|
125
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.6-alpha.1",
|
|
126
|
+
"@deepseek-ai/dsh-client-store": "^0.1.6-alpha.1",
|
|
127
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.6-alpha.1",
|
|
128
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.6-alpha.1",
|
|
129
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.6-alpha.1",
|
|
130
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.6-alpha.1",
|
|
131
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.6-alpha.1",
|
|
132
|
+
"@deepseek-ai/dsh-session": "^0.1.6-alpha.1",
|
|
133
|
+
"@deepseek-ai/dsh-session-query": "^0.1.6-alpha.1",
|
|
134
|
+
"@deepseek-ai/dsh-settings": "^0.1.6-alpha.1",
|
|
135
|
+
"@deepseek-ai/dsh-skill": "^0.1.6-alpha.1",
|
|
136
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.6-alpha.1",
|
|
136
137
|
"@types/js-yaml": "^4.0.9",
|
|
137
138
|
"@types/node": "^22.20.0",
|
|
138
139
|
"@types/react": "~18.3.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import type { CatalogSkill, SkillTag } from '../protocol.ts'
|
|
3
|
-
import { conflictsOnClose, filterBySource, formatRelativeTime, groupNamesOf, groupSwitchView, PRIVATE_SOURCE, sortSkills } from './grouping.ts'
|
|
2
|
+
import type { CatalogSkill, CollectionGroup, DisabledSkill, SkillTag } from '../protocol.ts'
|
|
3
|
+
import { conflictsOnClose, filterBySource, formatRelativeTime, groupNamesOf, groupSwitchView, PRIVATE_SOURCE, sortSkills, visibleCollections } from './grouping.ts'
|
|
4
4
|
|
|
5
5
|
function skill(name: string, writable = true): CatalogSkill {
|
|
6
6
|
return {
|
|
@@ -75,6 +75,39 @@ describe('filterBySource', () => {
|
|
|
75
75
|
})
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
+
describe('visibleCollections', () => {
|
|
79
|
+
const collections: CollectionGroup[] = [
|
|
80
|
+
{ name: 'repo/x', skillNames: ['enabled-one', 'disabled-one'] },
|
|
81
|
+
{ name: 'repo/ghost', skillNames: ['gone'] },
|
|
82
|
+
]
|
|
83
|
+
const disabledOne: DisabledSkill = { name: 'disabled-one', description: 'Paused skill', path: '/x/disabled-one/SKILL.md.disabled', root: 'user-dsh', disabledAt: 1 }
|
|
84
|
+
const origins = { 'enabled-one': 'repo/x', 'disabled-one': 'repo/x' }
|
|
85
|
+
|
|
86
|
+
it('keeps collections with visible members and drops empty shells', () => {
|
|
87
|
+
const visible = visibleCollections(collections, [skill('enabled-one')], [disabledOne], '', 'all', origins)
|
|
88
|
+
expect(visible.map((entry) => entry.collection.name)).toEqual(['repo/x'])
|
|
89
|
+
expect(visible[0].skills.map((s) => s.name)).toEqual(['enabled-one'])
|
|
90
|
+
expect(visible[0].disabledMembers.map((d) => d.name)).toEqual(['disabled-one'])
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('drops a collection when neither side passes the search filter', () => {
|
|
94
|
+
expect(visibleCollections(collections, [], [disabledOne], 'no-match', 'all', origins)).toEqual([])
|
|
95
|
+
expect(visibleCollections(collections, [], [disabledOne], 'paused', 'all', origins).map((e) => e.collection.name)).toEqual(['repo/x'])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('applies the source filter to disabled-only collections', () => {
|
|
99
|
+
expect(visibleCollections(collections, [], [disabledOne], '', 'repo/other', origins)).toEqual([])
|
|
100
|
+
expect(visibleCollections(collections, [], [disabledOne], '', 'private', origins)).toEqual([])
|
|
101
|
+
expect(visibleCollections(collections, [], [disabledOne], '', 'repo/x', origins).map((e) => e.collection.name)).toEqual(['repo/x'])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('treats a disabled record with no origin as private', () => {
|
|
105
|
+
const privateRecord = { ...disabledOne, name: 'private-one' }
|
|
106
|
+
const privateCollection: CollectionGroup = { name: 'repo/y', skillNames: ['private-one'] }
|
|
107
|
+
expect(visibleCollections([privateCollection], [], [privateRecord], '', PRIVATE_SOURCE, {}).map((e) => e.collection.name)).toEqual(['repo/y'])
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
78
111
|
describe('sortSkills', () => {
|
|
79
112
|
it('sorts by name ascending', () => {
|
|
80
113
|
const skills = [skill('zeta'), skill('alpha'), skill('beta')]
|
package/src/client/grouping.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* GUI resolves with a dialog; the helpers below compute both sides.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import type { CatalogSkill, CollectionGroup, SkillTag } from '../protocol.ts'
|
|
16
|
+
import type { CatalogSkill, CollectionGroup, DisabledSkill, SkillTag } from '../protocol.ts'
|
|
17
17
|
import { isProjectSource } from '../protocol.ts'
|
|
18
18
|
|
|
19
19
|
export { isProjectSource }
|
|
@@ -87,6 +87,43 @@ export function filterBySource(skills: readonly CatalogSkill[], source: string,
|
|
|
87
87
|
})
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/** One origin collection with the members visible under the current filters. */
|
|
91
|
+
export interface VisibleCollection {
|
|
92
|
+
collection: CollectionGroup
|
|
93
|
+
/** Enabled, currently visible members. */
|
|
94
|
+
skills: CatalogSkill[]
|
|
95
|
+
/** Disabled records passing the current name/description filter. */
|
|
96
|
+
disabledMembers: DisabledSkill[]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Match origin collections against the currently visible enabled skills and
|
|
101
|
+
* disabled records, dropping collections with no visible member. Without
|
|
102
|
+
* this, a stale origin (skill deleted on disk, or a `.disabled` file whose
|
|
103
|
+
* sidecar record was lost) renders a group header with zero rows — an empty
|
|
104
|
+
* shell the sources tab otherwise never shows (project/personal groups
|
|
105
|
+
* already disappear when they have nothing to display).
|
|
106
|
+
*/
|
|
107
|
+
export function visibleCollections(
|
|
108
|
+
collections: readonly CollectionGroup[],
|
|
109
|
+
visibleSkills: readonly CatalogSkill[],
|
|
110
|
+
disabledRecords: readonly DisabledSkill[],
|
|
111
|
+
normalized: string,
|
|
112
|
+
sourceFilter: string,
|
|
113
|
+
origins: Readonly<Record<string, string>>,
|
|
114
|
+
): VisibleCollection[] {
|
|
115
|
+
const visible: VisibleCollection[] = []
|
|
116
|
+
for (const collection of collections) {
|
|
117
|
+
const skills = visibleSkills.filter((skill) => collection.skillNames.includes(skill.name))
|
|
118
|
+
const disabledMembers = disabledRecords.filter((record) =>
|
|
119
|
+
collection.skillNames.includes(record.name)
|
|
120
|
+
&& (normalized.length === 0 || record.name.toLocaleLowerCase().includes(normalized) || record.description.toLocaleLowerCase().includes(normalized))
|
|
121
|
+
&& (sourceFilter === 'all' || (origins[record.name] ?? PRIVATE_SOURCE) === sourceFilter))
|
|
122
|
+
if (skills.length > 0 || disabledMembers.length > 0) visible.push({ collection, skills, disabledMembers })
|
|
123
|
+
}
|
|
124
|
+
return visible
|
|
125
|
+
}
|
|
126
|
+
|
|
90
127
|
/** Catalog sort keys offered by the filter bar. */
|
|
91
128
|
export type SortKey = 'name' | 'added' | 'uses'
|
|
92
129
|
|
|
@@ -101,8 +101,14 @@ export function PanelDialogs(props: PanelDialogsProps): JSX.Element {
|
|
|
101
101
|
<VersionChoiceDialog
|
|
102
102
|
choice={versionDialog}
|
|
103
103
|
busy={versionBusy}
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
// Keep the selected ref and custom override in one functional
|
|
105
|
+
// update, so a ref selection cannot be lost to a stale snapshot.
|
|
106
|
+
onSelect={(selected) => {
|
|
107
|
+
setVersionDialog((previous) => previous === null ? previous : { ...previous, selected, custom: '' })
|
|
108
|
+
}}
|
|
109
|
+
onCustom={(custom) => {
|
|
110
|
+
setVersionDialog((previous) => previous === null ? previous : { ...previous, custom })
|
|
111
|
+
}}
|
|
106
112
|
onCancel={() => { setVersionDialog(null) }}
|
|
107
113
|
onConfirm={() => { void confirmVersionDialog() }}
|
|
108
114
|
/>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { useMemo, useState, type JSX } from 'react'
|
|
10
10
|
import { tt } from '../helpers.ts'
|
|
11
|
-
import { filterBySource, groupSwitchView, isProjectSource, PRIVATE_SOURCE } from '../grouping.ts'
|
|
11
|
+
import { filterBySource, groupSwitchView, isProjectSource, PRIVATE_SOURCE, visibleCollections } from '../grouping.ts'
|
|
12
12
|
import { SkillRow } from './SkillRow.tsx'
|
|
13
13
|
import { DisabledRow } from './DisabledRow.tsx'
|
|
14
14
|
import { GroupSummary } from './GroupSummary.tsx'
|
|
@@ -28,10 +28,14 @@ export function SourcesView(props: { hub: SkillHubState }): JSX.Element {
|
|
|
28
28
|
const duplicateNames = useMemo(() => new Set(catalog?.duplicateNames ?? []), [catalog])
|
|
29
29
|
|
|
30
30
|
// ----- 顶层分组统一拖拽(project / col:xxx / personal 全部可拖) -----
|
|
31
|
-
const
|
|
31
|
+
const sourceFiltered = filterBySource(sorted, sourceFilter, origins)
|
|
32
|
+
const projectSkillsAll = sourceFiltered.filter((skill) => isProjectSource(skill.source))
|
|
32
33
|
const hasProject = projectSkillsAll.length > 0
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
// 无可见成员的来源组不渲染:来源记录指向的技能可能已被删除,或禁用记录
|
|
35
|
+
// 丢失导致技能既非启用也非禁用,留下一个组头有数字、展开 0 行的空壳。
|
|
36
|
+
const visible = visibleCollections(groupsState?.collections ?? [], sourceFiltered, catalog?.disabled ?? [], normalized, sourceFilter, origins)
|
|
37
|
+
const collections = visible.map((entry) => entry.collection)
|
|
38
|
+
const uncategorized = sourceFiltered.filter((skill) => origins[skill.name] === undefined && !isProjectSource(skill.source))
|
|
35
39
|
const personalDisabled = (catalog?.disabled ?? []).filter((record) => origins[record.name] === undefined)
|
|
36
40
|
.filter((record) => normalized.length === 0 || record.name.toLocaleLowerCase().includes(normalized) || record.description.toLocaleLowerCase().includes(normalized))
|
|
37
41
|
.filter((record) => sourceFilter === 'all' || sourceFilter === PRIVATE_SOURCE)
|
|
@@ -79,7 +83,7 @@ export function SourcesView(props: { hub: SkillHubState }): JSX.Element {
|
|
|
79
83
|
const rowProps = { uses: hub.uses, hubConfig: hub.hubConfig, busyNames, editMode: hub.editMode, tagBusy: hub.tagBusy, duplicateNames, toggle: hub.toggle, openDetail: hub.openDetail, requestDeleteSkill: hub.requestDeleteSkill }
|
|
80
84
|
|
|
81
85
|
if (skillView === 'flat') {
|
|
82
|
-
return <>{
|
|
86
|
+
return <>{sourceFiltered.map((skill) => <SkillRow key={skill.name} skill={skill} {...rowProps} />)}</>
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
// 空状态:没有任何分组时提示
|
|
@@ -117,13 +121,9 @@ export function SourcesView(props: { hub: SkillHubState }): JSX.Element {
|
|
|
117
121
|
// Collection 卡片(可拖,归属顶层排序)
|
|
118
122
|
if (topKey.startsWith('col:')) {
|
|
119
123
|
const colName = topKey.slice(4)
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
const
|
|
123
|
-
const disabledMembers = (catalog?.disabled ?? []).filter((record) =>
|
|
124
|
-
collection.skillNames.includes(record.name)
|
|
125
|
-
&& (normalized.length === 0 || record.name.toLocaleLowerCase().includes(normalized) || record.description.toLocaleLowerCase().includes(normalized))
|
|
126
|
-
&& (sourceFilter === 'all' || (origins[record.name] ?? PRIVATE_SOURCE) === sourceFilter))
|
|
124
|
+
const entry = visible.find((item) => item.collection.name === colName)
|
|
125
|
+
if (entry === undefined) return null
|
|
126
|
+
const { collection, skills, disabledMembers } = entry
|
|
127
127
|
const collapsed = collapsedGroups.has('col:' + collection.name)
|
|
128
128
|
const view = groupSwitchView(collection.skillNames, viewNames)
|
|
129
129
|
const check = sourceCheck[collection.name]
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* the same role/aria shell; the panel owns all dialog state.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useEffect, type JSX, type ReactNode } from 'react'
|
|
8
|
+
import { useEffect, useState, type JSX, type ReactNode } from 'react'
|
|
9
9
|
import type { CollectionGroup, SkillTag } from '../../protocol.ts'
|
|
10
10
|
import { tt } from '../helpers.ts'
|
|
11
11
|
import { groupNamesOf } from '../grouping.ts'
|
|
@@ -170,29 +170,56 @@ export function VersionChoiceDialog(props: {
|
|
|
170
170
|
}): JSX.Element {
|
|
171
171
|
const { choice, busy, onSelect, onCustom, onCancel, onConfirm } = props
|
|
172
172
|
const effective = choice.custom.trim() !== '' ? choice.custom.trim() : choice.selected
|
|
173
|
+
type RefGroup = 'releases' | 'branches'
|
|
174
|
+
const branchOptions = choice.branches.filter((branch) => !choice.releases.includes(branch))
|
|
175
|
+
const [refGroup, setRefGroup] = useState<RefGroup>(
|
|
176
|
+
choice.releases.includes(choice.selected) || choice.branches.length === 0 ? 'releases' : 'branches',
|
|
177
|
+
)
|
|
178
|
+
// The dialog starts in a loading state and receives the release/branch
|
|
179
|
+
// lists in a later render. Align the group once that data arrives, without
|
|
180
|
+
// resetting it after the user selects another ref.
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (choice.loading) return
|
|
183
|
+
if (choice.releases.includes(choice.selected)) setRefGroup('releases')
|
|
184
|
+
else if (branchOptions.includes(choice.selected)) setRefGroup('branches')
|
|
185
|
+
}, [choice.loading, choice.repo])
|
|
186
|
+
const activeRefs = refGroup === 'releases' ? choice.releases : branchOptions
|
|
187
|
+
const listedRefs = [...choice.releases, ...branchOptions]
|
|
188
|
+
const currentUnlistedRef = choice.selected !== '' && !listedRefs.includes(choice.selected) ? choice.selected : undefined
|
|
189
|
+
const selectRefs = currentUnlistedRef !== undefined ? [currentUnlistedRef, ...activeRefs] : activeRefs
|
|
190
|
+
const selectValue = selectRefs.includes(choice.selected) ? choice.selected : selectRefs[0] ?? ''
|
|
191
|
+
const selectGroup = (next: RefGroup): void => {
|
|
192
|
+
setRefGroup(next)
|
|
193
|
+
const nextRefs = next === 'releases' ? choice.releases : branchOptions
|
|
194
|
+
if (nextRefs.length > 0 && !nextRefs.includes(choice.selected)) onSelect(nextRefs[0])
|
|
195
|
+
}
|
|
173
196
|
return (
|
|
174
197
|
<DialogShell onClose={onCancel}>
|
|
175
198
|
<h3 className={css.dialogTitle}>{tt('market.versionTitle')}</h3>
|
|
176
199
|
<p className={css.dialogText}>{tt('market.versionText', { repo: choice.repo })}{choice.current !== undefined ? ` (${tt('market.versionCurrent', { ref: choice.current })})` : ''}</p>
|
|
177
200
|
{choice.loading ? <p className={css.dialogText}>{tt('market.scanning')}</p> : (
|
|
178
201
|
<>
|
|
179
|
-
{
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
202
|
+
<div className={css.segmented} role='group' aria-label={tt('market.versionTitle')} style={{ marginBottom: 8 }}>
|
|
203
|
+
<button
|
|
204
|
+
type='button'
|
|
205
|
+
className={css.segBtn + (refGroup === 'releases' ? ' ' + css.segBtnActive : '')}
|
|
206
|
+
disabled={choice.releases.length === 0}
|
|
207
|
+
aria-pressed={refGroup === 'releases'}
|
|
208
|
+
onClick={() => { selectGroup('releases') }}
|
|
209
|
+
>{tt('market.versionReleases')}</button>
|
|
210
|
+
<button
|
|
211
|
+
type='button'
|
|
212
|
+
className={css.segBtn + (refGroup === 'branches' ? ' ' + css.segBtnActive : '')}
|
|
213
|
+
disabled={branchOptions.length === 0}
|
|
214
|
+
aria-pressed={refGroup === 'branches'}
|
|
215
|
+
onClick={() => { selectGroup('branches') }}
|
|
216
|
+
>{tt('market.versionBranches')}</button>
|
|
217
|
+
</div>
|
|
218
|
+
{selectValue !== '' ? (
|
|
219
|
+
<select className={css.select + ' ' + css.dialogSelect} value={selectValue}
|
|
220
|
+
onChange={(event) => { onSelect(event.target.value) }}>
|
|
221
|
+
{selectRefs.map((ref) => <option key={ref} value={ref}>{ref}</option>)}
|
|
222
|
+
</select>
|
|
196
223
|
) : null}
|
|
197
224
|
<p className={css.dialogText} style={{ marginBottom: 4 }}>{tt('market.versionCustom')}</p>
|
|
198
225
|
<input className={css.input + ' ' + css.dialogSelect} value={choice.custom}
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { SkillHubProvider } from './provider.ts'
|
|
|
21
21
|
import { makeRoutes } from './routes.ts'
|
|
22
22
|
import { createSkillStatsReader, asPersistenceSeam, type SessionPersistenceLike, type SessionQueryLike, type SkillStatsReader } from './stats.ts'
|
|
23
23
|
import { SkillHubStore } from './store.ts'
|
|
24
|
+
import { reconcileDisabledSkills } from './reconcile.ts'
|
|
24
25
|
import { cleanupLeftoverImportDirs, setGithubToken } from './repo.ts'
|
|
25
26
|
import { dshHome } from './store.ts'
|
|
26
27
|
import { join } from 'node:path'
|
|
@@ -234,6 +235,16 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
234
235
|
ctx.logger.warn('[dsh-skill-hub] startup cleanup failed', error)
|
|
235
236
|
}
|
|
236
237
|
}
|
|
238
|
+
// 对账:磁盘上已有 .disabled、sidecar 却无记录(状态文件被恢复/手改、旧版本
|
|
239
|
+
// 遗留)时补记录,否则这些技能在面板里既不算启用也不算禁用,来源组空壳。
|
|
240
|
+
try {
|
|
241
|
+
const reconciled = await reconcileDisabledSkills(store, home)
|
|
242
|
+
if (reconciled.length > 0) {
|
|
243
|
+
ctx.logger.info(`[dsh-skill-hub] startup reconciled ${reconciled.length} disabled skill record(s): ${reconciled.map((entry) => entry.name).join(', ')}`)
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
ctx.logger.warn('[dsh-skill-hub] startup disabled-skill reconcile failed', error)
|
|
247
|
+
}
|
|
237
248
|
})()
|
|
238
249
|
|
|
239
250
|
void (async () => {
|