dsh-skill-hub 0.3.11 → 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 +132 -2
- package/lib/types/client/grouping.d.ts +18 -1
- package/lib/types/error-text.d.ts +4 -1
- package/lib/types/reconcile.d.ts +24 -0
- package/lib/types/skillfs/scan.d.ts +7 -0
- package/package.json +35 -29
- 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/error-text.test.ts +45 -0
- package/src/error-text.ts +44 -2
- 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
|
@@ -108,10 +108,45 @@ function isProjectSource(source) {
|
|
|
108
108
|
* 错误文案收敛:宿主各处把 unknown 异常转成一行可读文字。原先这条表达式
|
|
109
109
|
* 在宿主侧内联了 22 次(`error instanceof Error ? error.message : String(error)`),
|
|
110
110
|
* 现在统一走这里。浏览器半有自己的 `helpers.errorMessage`,两半互不引用。
|
|
111
|
+
*
|
|
112
|
+
* Error 的 `cause` 链(undici 的 `fetch failed`、AggregateError 的多地址
|
|
113
|
+
* 失败、TLS 证书错误等)以括号附录带出,避免只剩笼统的顶层 message。
|
|
111
114
|
*/
|
|
112
|
-
/**
|
|
115
|
+
/** cause 链最大展开深度,防自引用/超深链。 */
|
|
116
|
+
const MAX_CAUSE_DEPTH = 4;
|
|
117
|
+
/** 合成一条 cause 细节;code 已在 message 里则不重复。 */
|
|
118
|
+
function causeDetail(message, code) {
|
|
119
|
+
const text = message.trim();
|
|
120
|
+
if (code === "") return text;
|
|
121
|
+
if (text === "") return "[" + code + "]";
|
|
122
|
+
return text.includes(code) ? text : text + " [" + code + "]";
|
|
123
|
+
}
|
|
124
|
+
/** 深度受限地收集 cause 链细节(AggregateError 展开其 errors)。 */
|
|
125
|
+
function collectCauseDetails(cause, depth, seen, out) {
|
|
126
|
+
if (depth > MAX_CAUSE_DEPTH || cause === null || cause === void 0 || seen.has(cause)) return;
|
|
127
|
+
seen.add(cause);
|
|
128
|
+
if (cause instanceof Error) {
|
|
129
|
+
const errors = cause.errors;
|
|
130
|
+
if (Array.isArray(errors)) for (const item of errors) collectCauseDetails(item, depth + 1, seen, out);
|
|
131
|
+
const rawCode = cause.code;
|
|
132
|
+
const code = typeof rawCode === "string" ? rawCode : "";
|
|
133
|
+
const detail = causeDetail(cause.message, code);
|
|
134
|
+
if (detail !== "") out.push(detail);
|
|
135
|
+
collectCauseDetails(cause.cause, depth + 1, seen, out);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const detail = causeDetail(String(cause), "");
|
|
139
|
+
if (detail !== "") out.push(detail);
|
|
140
|
+
}
|
|
141
|
+
/** 从 unknown 异常取出可读文案;非 Error 值用 String 兜底,cause 附在括号里。 */
|
|
113
142
|
function errorText(error) {
|
|
114
|
-
|
|
143
|
+
if (!(error instanceof Error)) return String(error);
|
|
144
|
+
const details = [];
|
|
145
|
+
collectCauseDetails(error.cause, 1, /* @__PURE__ */ new Set(), details);
|
|
146
|
+
for (let index = details.length - 1; index >= 0; index -= 1) if (details[index] === error.message || details.indexOf(details[index]) !== index) details.splice(index, 1);
|
|
147
|
+
if (details.length === 0) return error.message;
|
|
148
|
+
const suffix = details.join("; ");
|
|
149
|
+
return error.message === "" ? suffix : error.message + " (" + suffix + ")";
|
|
115
150
|
}
|
|
116
151
|
//#endregion
|
|
117
152
|
//#region src/store/paths.ts
|
|
@@ -1088,6 +1123,39 @@ async function scanRoot(base) {
|
|
|
1088
1123
|
function listSkillEntries(root, home = dshHome()) {
|
|
1089
1124
|
return scanRoot(rootPath(root, home));
|
|
1090
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
|
+
}
|
|
1091
1159
|
/** Read UI metadata from `agents/openai.yaml` beside a directory skill (mirrors codex SkillInterface). */
|
|
1092
1160
|
async function readSkillInterface(directory) {
|
|
1093
1161
|
const yamlPath = join(directory, "agents", "openai.yaml");
|
|
@@ -4366,6 +4434,62 @@ function createSkillStatsReader(query, ttlMs = 3e5, options = {}) {
|
|
|
4366
4434
|
return reader;
|
|
4367
4435
|
}
|
|
4368
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
|
|
4369
4493
|
//#region src/index.ts
|
|
4370
4494
|
/** Stable cordis plugin name (matches cordis.patch.yml insert id). */
|
|
4371
4495
|
const name = "skill-hub";
|
|
@@ -4492,6 +4616,12 @@ function apply(ctx, config) {
|
|
|
4492
4616
|
} catch (error) {
|
|
4493
4617
|
ctx.logger.warn("[dsh-skill-hub] startup cleanup failed", error);
|
|
4494
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
|
+
}
|
|
4495
4625
|
})();
|
|
4496
4626
|
(async () => {
|
|
4497
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
|
/**
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* 错误文案收敛:宿主各处把 unknown 异常转成一行可读文字。原先这条表达式
|
|
3
3
|
* 在宿主侧内联了 22 次(`error instanceof Error ? error.message : String(error)`),
|
|
4
4
|
* 现在统一走这里。浏览器半有自己的 `helpers.errorMessage`,两半互不引用。
|
|
5
|
+
*
|
|
6
|
+
* Error 的 `cause` 链(undici 的 `fetch failed`、AggregateError 的多地址
|
|
7
|
+
* 失败、TLS 证书错误等)以括号附录带出,避免只剩笼统的顶层 message。
|
|
5
8
|
*/
|
|
6
|
-
/** 从 unknown 异常取出可读文案;非 Error 值用 String
|
|
9
|
+
/** 从 unknown 异常取出可读文案;非 Error 值用 String 兜底,cause 附在括号里。 */
|
|
7
10
|
export declare function errorText(error: unknown): string;
|
|
@@ -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": {
|
|
@@ -69,7 +69,11 @@
|
|
|
69
69
|
"0.1.2-alpha.5",
|
|
70
70
|
"0.1.2-rc.1",
|
|
71
71
|
"0.1.3-alpha.2",
|
|
72
|
-
"0.1.5-alpha.1"
|
|
72
|
+
"0.1.5-alpha.1",
|
|
73
|
+
"0.1.5-alpha.2",
|
|
74
|
+
"0.1.5-rc.1",
|
|
75
|
+
"0.1.5-rc.2",
|
|
76
|
+
"0.1.6-alpha.1"
|
|
73
77
|
]
|
|
74
78
|
},
|
|
75
79
|
"capability": {
|
|
@@ -91,19 +95,19 @@
|
|
|
91
95
|
},
|
|
92
96
|
"peerDependencies": {
|
|
93
97
|
"@deepseek-ai/cordis": "^4.0.1 || ^4.0.2",
|
|
94
|
-
"@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",
|
|
95
|
-
"@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",
|
|
96
|
-
"@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",
|
|
97
|
-
"@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",
|
|
98
|
-
"@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",
|
|
99
|
-
"@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",
|
|
100
|
-
"@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",
|
|
101
|
-
"@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",
|
|
102
|
-
"@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",
|
|
103
|
-
"@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",
|
|
104
|
-
"@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",
|
|
105
|
-
"@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",
|
|
106
|
-
"@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",
|
|
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",
|
|
107
111
|
"react": "^18.2.0",
|
|
108
112
|
"react-dom": "^18.2.0"
|
|
109
113
|
},
|
|
@@ -117,30 +121,32 @@
|
|
|
117
121
|
},
|
|
118
122
|
"devDependencies": {
|
|
119
123
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
120
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.
|
|
121
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.
|
|
122
|
-
"@deepseek-ai/dsh-client-store": "^0.1.
|
|
123
|
-
"@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.
|
|
124
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.
|
|
125
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.
|
|
126
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.
|
|
127
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.
|
|
128
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
129
|
-
"@deepseek-ai/dsh-session-query": "^0.1.
|
|
130
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
131
|
-
"@deepseek-ai/dsh-skill": "^0.1.
|
|
132
|
-
"@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",
|
|
133
137
|
"@types/js-yaml": "^4.0.9",
|
|
134
138
|
"@types/node": "^22.20.0",
|
|
135
139
|
"@types/react": "~18.3.1",
|
|
136
140
|
"@types/react-dom": "^18.3.5",
|
|
141
|
+
"immer": "^10.2.0",
|
|
137
142
|
"lightningcss": "1.32.0",
|
|
138
143
|
"react": "^18.3.1",
|
|
139
144
|
"react-dom": "^18.3.1",
|
|
140
145
|
"schemastery": "^3.18.0",
|
|
141
146
|
"tsdown": "0.22.2",
|
|
142
147
|
"typescript": "~5.7.2",
|
|
143
|
-
"vitest": "^3.0.0"
|
|
148
|
+
"vitest": "^3.0.0",
|
|
149
|
+
"zustand": "~4.4.7"
|
|
144
150
|
},
|
|
145
151
|
"files": [
|
|
146
152
|
"lib/**/*.js",
|
|
@@ -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}
|