dsh-skill-hub 0.0.1 → 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/CONTRIBUTING.md +56 -0
- package/README.md +355 -20
- package/cordis.patch.yml +12 -0
- package/lib/client.js +4514 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +3119 -0
- package/lib/invariant.js +6 -0
- package/lib/types/client/SkillHubSettingsCard.d.ts +53 -0
- package/lib/types/client/api-config-scope.d.ts +48 -0
- package/lib/types/client/api.d.ts +64 -0
- package/lib/types/client/grouping.d.ts +64 -0
- package/lib/types/client/helpers.d.ts +11 -0
- package/lib/types/client/index.d.ts +42 -0
- package/lib/types/client/locales.d.ts +216 -0
- 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 +18 -0
- 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 +81 -0
- package/lib/types/client/settings-form.d.ts +95 -0
- package/lib/types/client/settings-nav-icon.d.ts +8 -0
- package/lib/types/index.d.ts +36 -0
- package/lib/types/invariant.d.ts +3 -0
- package/lib/types/protocol.d.ts +571 -0
- package/lib/types/provider.d.ts +47 -0
- package/lib/types/repo.d.ts +133 -0
- package/lib/types/routes.d.ts +51 -0
- package/lib/types/skillfs.d.ts +95 -0
- package/lib/types/stats.d.ts +49 -0
- package/lib/types/store.d.ts +142 -0
- package/lib/types/update.d.ts +20 -0
- package/package.json +115 -11
- package/src/client/SkillHubSettingsCard.tsx +161 -0
- package/src/client/api-config-scope.test.ts +79 -0
- package/src/client/api-config-scope.ts +119 -0
- package/src/client/api.ts +327 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/grouping.test.ts +109 -0
- package/src/client/grouping.ts +120 -0
- package/src/client/helpers.ts +23 -0
- package/src/client/index.tsx +104 -0
- package/src/client/locales.ts +432 -0
- 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 +347 -0
- 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 +219 -0
- package/src/client/panel/useSkillHub.ts +797 -0
- package/src/client/settings-card.module.css +52 -0
- package/src/client/settings-card.tsx +237 -0
- package/src/client/settings-form.ts +227 -0
- package/src/client/settings-nav-icon.ts +42 -0
- package/src/index.ts +181 -0
- package/src/invariant.ts +4 -0
- package/src/protocol.ts +622 -0
- package/src/provider.test.ts +112 -0
- package/src/provider.ts +168 -0
- package/src/repo.test.ts +212 -0
- package/src/repo.ts +528 -0
- package/src/routes.test.ts +786 -0
- package/src/routes.ts +1252 -0
- package/src/skillfs.test.ts +290 -0
- package/src/skillfs.ts +306 -0
- package/src/stats.test.ts +168 -0
- package/src/stats.ts +124 -0
- package/src/store.test.ts +253 -0
- package/src/store.ts +628 -0
- package/src/update.test.ts +73 -0
- package/src/update.ts +136 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,4514 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-skill-hub",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
11
|
+
//#region src/protocol.ts
|
|
12
|
+
/**
|
|
13
|
+
* Shared API contract for dsh-skill-hub: the route paths and payload shapes
|
|
14
|
+
* both the host half and the browser half import. The browser half must never
|
|
15
|
+
* depend on host SDK packages, so registry types are re-spelled here as
|
|
16
|
+
* plain JSON-safe interfaces.
|
|
17
|
+
*/
|
|
18
|
+
/** Browser-facing base paths of the skill-hub API family. */
|
|
19
|
+
const SKILL_HUB_API = {
|
|
20
|
+
catalog: "/api/skill-hub/catalog",
|
|
21
|
+
skill: "/api/skill-hub/skill",
|
|
22
|
+
skillDelete: "/api/skill-hub/skill/delete",
|
|
23
|
+
toggle: "/api/skill-hub/toggle",
|
|
24
|
+
toggleBatch: "/api/skill-hub/toggle-batch",
|
|
25
|
+
create: "/api/skill-hub/create",
|
|
26
|
+
stats: "/api/skill-hub/stats",
|
|
27
|
+
config: "/api/skill-hub/config",
|
|
28
|
+
market: "/api/skill-hub/market",
|
|
29
|
+
marketSource: "/api/skill-hub/market/source",
|
|
30
|
+
marketSourceDelete: "/api/skill-hub/market/source/delete",
|
|
31
|
+
marketSourceRef: "/api/skill-hub/market/source/ref",
|
|
32
|
+
marketCheck: "/api/skill-hub/market/check",
|
|
33
|
+
marketSync: "/api/skill-hub/market/source/sync",
|
|
34
|
+
repo: "/api/skill-hub/repo",
|
|
35
|
+
repoImport: "/api/skill-hub/repo/import",
|
|
36
|
+
update: "/api/skill-hub/update",
|
|
37
|
+
groups: "/api/skill-hub/groups",
|
|
38
|
+
tag: "/api/skill-hub/tag",
|
|
39
|
+
tagDelete: "/api/skill-hub/tag/delete",
|
|
40
|
+
tagMembers: "/api/skill-hub/tag/members",
|
|
41
|
+
sources: "/api/skill-hub/sources",
|
|
42
|
+
sourceCheck: "/api/skill-hub/sources/check",
|
|
43
|
+
sourceSync: "/api/skill-hub/sources/sync",
|
|
44
|
+
sourceDelete: "/api/skill-hub/sources/delete",
|
|
45
|
+
sourceRestore: "/api/skill-hub/sources/restore",
|
|
46
|
+
sourceTrashClear: "/api/skill-hub/sources/trash/clear"
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/client/api.ts
|
|
50
|
+
/**
|
|
51
|
+
* Browser-side API client for the /api/skill-hub route family. The only
|
|
52
|
+
* data access path the panel uses — plain fetch, same origin.
|
|
53
|
+
*/
|
|
54
|
+
/** Error carrying the route's JSON error message. */
|
|
55
|
+
var SkillHubApiError = class extends Error {
|
|
56
|
+
constructor(message) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "SkillHubApiError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/** Parse a JSON response or throw a SkillHubApiError. */
|
|
62
|
+
async function readJson(response) {
|
|
63
|
+
let body;
|
|
64
|
+
try {
|
|
65
|
+
body = await response.json();
|
|
66
|
+
} catch {
|
|
67
|
+
throw new SkillHubApiError("HTTP " + response.status + ": invalid JSON response");
|
|
68
|
+
}
|
|
69
|
+
if (!response.ok) throw new SkillHubApiError(typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : "HTTP " + response.status);
|
|
70
|
+
return body;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Fetch with a hard timeout: a hung connection (proxy, stalled keep-alive,
|
|
74
|
+
* host wedged) must never leave the panel stuck on "loading" forever — it
|
|
75
|
+
* fails fast and surfaces the error banner instead.
|
|
76
|
+
*/
|
|
77
|
+
async function fetchWithTimeout(input, init, ms = 1e4) {
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
80
|
+
try {
|
|
81
|
+
return await fetch(input, {
|
|
82
|
+
...init,
|
|
83
|
+
signal: controller.signal
|
|
84
|
+
});
|
|
85
|
+
} finally {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** The browser half's only data entry point. */
|
|
90
|
+
var SkillHubApi = class {
|
|
91
|
+
async catalog() {
|
|
92
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.catalog));
|
|
93
|
+
}
|
|
94
|
+
async skill(name) {
|
|
95
|
+
return (await readJson(await fetchWithTimeout(SKILL_HUB_API.skill + "?name=" + encodeURIComponent(name)))).skill;
|
|
96
|
+
}
|
|
97
|
+
/** Move one writable skill into the restorable trash. */
|
|
98
|
+
async deleteSkill(name) {
|
|
99
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.skillDelete, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { "content-type": "application/json" },
|
|
102
|
+
body: JSON.stringify({ name })
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
/** Toggle one skill; resolves with the fresh catalog from the route. */
|
|
106
|
+
async toggle(name, enabled) {
|
|
107
|
+
return (await readJson(await fetchWithTimeout(SKILL_HUB_API.toggle, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: { "content-type": "application/json" },
|
|
110
|
+
body: JSON.stringify({
|
|
111
|
+
name,
|
|
112
|
+
enabled
|
|
113
|
+
})
|
|
114
|
+
}))).catalog;
|
|
115
|
+
}
|
|
116
|
+
async create(payload) {
|
|
117
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.create, {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: { "content-type": "application/json" },
|
|
120
|
+
body: JSON.stringify(payload)
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
async stats() {
|
|
124
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.stats));
|
|
125
|
+
}
|
|
126
|
+
/** Toggle a whole group in one write; resolves with the fresh catalog + failures. */
|
|
127
|
+
async toggleBatch(names, enabled) {
|
|
128
|
+
const payload = {
|
|
129
|
+
names,
|
|
130
|
+
enabled
|
|
131
|
+
};
|
|
132
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.toggleBatch, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: { "content-type": "application/json" },
|
|
135
|
+
body: JSON.stringify(payload)
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
/** The user's added market sources (codex-style repo slugs). */
|
|
139
|
+
async market() {
|
|
140
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.market));
|
|
141
|
+
}
|
|
142
|
+
/** Add a market source repo; resolves with the fresh list. */
|
|
143
|
+
async addMarketSource(repo) {
|
|
144
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.marketSource, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: { "content-type": "application/json" },
|
|
147
|
+
body: JSON.stringify({ repo })
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
/** Remove a market source repo; resolves with the fresh list. */
|
|
151
|
+
async removeMarketSource(repo) {
|
|
152
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.marketSourceDelete, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: { "content-type": "application/json" },
|
|
155
|
+
body: JSON.stringify({ repo })
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
/** Check every market source for updates (throttled server-side). */
|
|
159
|
+
async marketCheck() {
|
|
160
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.marketCheck));
|
|
161
|
+
}
|
|
162
|
+
/** Align a market source to its pinned ref; resolves with tracked skills. */
|
|
163
|
+
async marketSync(repo) {
|
|
164
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.marketSync, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: { "content-type": "application/json" },
|
|
167
|
+
body: JSON.stringify({ repo })
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
/** Pin a market source to an explicit ref (branch picker). */
|
|
171
|
+
async setMarketSourceRef(repo, ref) {
|
|
172
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.marketSourceRef, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: { "content-type": "application/json" },
|
|
175
|
+
body: JSON.stringify({
|
|
176
|
+
repo,
|
|
177
|
+
ref
|
|
178
|
+
})
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
/** Check the hub's own latest GitHub release. */
|
|
182
|
+
async updateCheck() {
|
|
183
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.update));
|
|
184
|
+
}
|
|
185
|
+
/** Discover importable skills in a GitHub repo. */
|
|
186
|
+
async repoDiscover(repo) {
|
|
187
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.repo + "?repo=" + encodeURIComponent(repo)));
|
|
188
|
+
}
|
|
189
|
+
/** Install selected repo skills into the user-dsh root (records the source). */
|
|
190
|
+
async repoImport(repo, paths, ref) {
|
|
191
|
+
const payload = {
|
|
192
|
+
repo,
|
|
193
|
+
paths,
|
|
194
|
+
...ref !== void 0 && ref !== "" ? { ref } : {}
|
|
195
|
+
};
|
|
196
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.repoImport, {
|
|
197
|
+
method: "POST",
|
|
198
|
+
headers: { "content-type": "application/json" },
|
|
199
|
+
body: JSON.stringify(payload)
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
/** Read the hub's runtime config (effective values + saved overrides). */
|
|
203
|
+
async config() {
|
|
204
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.config));
|
|
205
|
+
}
|
|
206
|
+
/** Patch the hub's runtime config (null clears a saved override). */
|
|
207
|
+
async saveConfig(patch) {
|
|
208
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.config, {
|
|
209
|
+
method: "POST",
|
|
210
|
+
headers: { "content-type": "application/json" },
|
|
211
|
+
body: JSON.stringify(patch)
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
/** 用户 tag 分组 + 系统集合组 + origin 映射。 */
|
|
215
|
+
async groups() {
|
|
216
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.groups));
|
|
217
|
+
}
|
|
218
|
+
/** 新建(无 id)或重命名(带 id)一个 tag 分组;返回全部 tag。 */
|
|
219
|
+
async saveTag(payload) {
|
|
220
|
+
return (await readJson(await fetchWithTimeout(SKILL_HUB_API.tag, {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: { "content-type": "application/json" },
|
|
223
|
+
body: JSON.stringify(payload)
|
|
224
|
+
}))).tags;
|
|
225
|
+
}
|
|
226
|
+
/** 删除一个 tag 分组;返回全部 tag。 */
|
|
227
|
+
async deleteTag(id) {
|
|
228
|
+
return (await readJson(await fetchWithTimeout(SKILL_HUB_API.tagDelete, {
|
|
229
|
+
method: "POST",
|
|
230
|
+
headers: { "content-type": "application/json" },
|
|
231
|
+
body: JSON.stringify({ id })
|
|
232
|
+
}))).tags;
|
|
233
|
+
}
|
|
234
|
+
/** 直接设置某 tag 的完整成员列表;返回全部 tag。 */
|
|
235
|
+
async setTagMembers(id, skillNames) {
|
|
236
|
+
return (await readJson(await fetchWithTimeout(SKILL_HUB_API.tagMembers, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: { "content-type": "application/json" },
|
|
239
|
+
body: JSON.stringify({
|
|
240
|
+
id,
|
|
241
|
+
skillNames
|
|
242
|
+
})
|
|
243
|
+
}))).tags;
|
|
244
|
+
}
|
|
245
|
+
/** 来源列表 + 派生 origin 映射 + 集合组 + 回收站。 */
|
|
246
|
+
async sources() {
|
|
247
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.sources));
|
|
248
|
+
}
|
|
249
|
+
/** 检查指定(或全部)来源的上游更新。 */
|
|
250
|
+
async checkSources(repo) {
|
|
251
|
+
const payload = repo !== void 0 ? { repo } : {};
|
|
252
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.sourceCheck, {
|
|
253
|
+
method: "POST",
|
|
254
|
+
headers: { "content-type": "application/json" },
|
|
255
|
+
body: JSON.stringify(payload)
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
/** 同步一个来源的所选(或全部)技能到上游最新版本。 */
|
|
259
|
+
async syncSource(repo, skills) {
|
|
260
|
+
const payload = skills !== void 0 ? {
|
|
261
|
+
repo,
|
|
262
|
+
skills
|
|
263
|
+
} : { repo };
|
|
264
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.sourceSync, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: { "content-type": "application/json" },
|
|
267
|
+
body: JSON.stringify(payload)
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
/** 跟进上游删除:把所选技能移入回收站。 */
|
|
271
|
+
async confirmDeleteSource(repo, skills) {
|
|
272
|
+
const payload = {
|
|
273
|
+
repo,
|
|
274
|
+
skills
|
|
275
|
+
};
|
|
276
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.sourceDelete, {
|
|
277
|
+
method: "POST",
|
|
278
|
+
headers: { "content-type": "application/json" },
|
|
279
|
+
body: JSON.stringify(payload)
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
/** 从回收站恢复一个技能。 */
|
|
283
|
+
async restoreSource(name) {
|
|
284
|
+
const payload = { name };
|
|
285
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.sourceRestore, {
|
|
286
|
+
method: "POST",
|
|
287
|
+
headers: { "content-type": "application/json" },
|
|
288
|
+
body: JSON.stringify(payload)
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
/** Permanently delete every skill currently in the trash. */
|
|
292
|
+
async clearTrash() {
|
|
293
|
+
return readJson(await fetchWithTimeout(SKILL_HUB_API.sourceTrashClear, { method: "POST" }));
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/client/api-config-scope.ts
|
|
298
|
+
/** Model-invocable dot color default. Single source for the TS side; the
|
|
299
|
+
* panel's CSS mirrors it via --hub-model (panel.module.css). */
|
|
300
|
+
const DEFAULT_DOT_MODEL_COLOR = "#2f81f7";
|
|
301
|
+
/** User-invocable dot color default. Single source for the TS side; the
|
|
302
|
+
* panel's CSS mirrors it via --hub-user (panel.module.css). */
|
|
303
|
+
const DEFAULT_DOT_USER_COLOR = "#3fb950";
|
|
304
|
+
/** Built-in defaults every field inherits until the user overrides it. */
|
|
305
|
+
const DEFAULTS = {
|
|
306
|
+
enabled: true,
|
|
307
|
+
announceToAgent: true,
|
|
308
|
+
showUseCount: true,
|
|
309
|
+
showUseTime: true,
|
|
310
|
+
showGroupSummary: true
|
|
311
|
+
};
|
|
312
|
+
/** Boolean config fields (color fields are strings). */
|
|
313
|
+
const BOOLEAN_FIELDS = /* @__PURE__ */ new Set([
|
|
314
|
+
"enabled",
|
|
315
|
+
"announceToAgent",
|
|
316
|
+
"showUseCount",
|
|
317
|
+
"showUseTime",
|
|
318
|
+
"showGroupSummary"
|
|
319
|
+
]);
|
|
320
|
+
/** Config fields the card edits (used to guard set/unset field names). */
|
|
321
|
+
const FIELDS = /* @__PURE__ */ new Set([
|
|
322
|
+
"enabled",
|
|
323
|
+
"announceToAgent",
|
|
324
|
+
"dotModelColor",
|
|
325
|
+
"dotUserColor",
|
|
326
|
+
"showUseCount",
|
|
327
|
+
"showUseTime",
|
|
328
|
+
"showGroupSummary"
|
|
329
|
+
]);
|
|
330
|
+
/** FormScope-compatible reactive config handle (see module doc). */
|
|
331
|
+
var ApiConfigScope = class {
|
|
332
|
+
api;
|
|
333
|
+
status = "loading";
|
|
334
|
+
value = { ...DEFAULTS };
|
|
335
|
+
saved = {};
|
|
336
|
+
listeners = /* @__PURE__ */ new Set();
|
|
337
|
+
/** @param api - the browser-half API client (shared with the skill panel). */
|
|
338
|
+
constructor(api) {
|
|
339
|
+
this.api = api;
|
|
340
|
+
this.load();
|
|
341
|
+
}
|
|
342
|
+
async load() {
|
|
343
|
+
try {
|
|
344
|
+
const response = await this.api.config();
|
|
345
|
+
this.value = response.config;
|
|
346
|
+
this.saved = { ...response.saved };
|
|
347
|
+
this.status = "ready";
|
|
348
|
+
} catch {
|
|
349
|
+
this.status = "unavailable";
|
|
350
|
+
}
|
|
351
|
+
this.publish();
|
|
352
|
+
}
|
|
353
|
+
subscribe(listener) {
|
|
354
|
+
this.listeners.add(listener);
|
|
355
|
+
return () => {
|
|
356
|
+
this.listeners.delete(listener);
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
getSnapshot() {
|
|
360
|
+
return {
|
|
361
|
+
status: this.status,
|
|
362
|
+
writable: true,
|
|
363
|
+
value: { ...this.value },
|
|
364
|
+
base: { ...DEFAULTS },
|
|
365
|
+
user: { ...this.saved }
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/** Write one field through the config route and adopt the persisted state. */
|
|
369
|
+
async set(field, value) {
|
|
370
|
+
if (!FIELDS.has(field)) return;
|
|
371
|
+
const patch = BOOLEAN_FIELDS.has(field) ? { [field]: Boolean(value) } : { [field]: typeof value === "string" ? value : void 0 };
|
|
372
|
+
await this.apply(patch);
|
|
373
|
+
}
|
|
374
|
+
/** Clear one field's override so it re-inherits the default. */
|
|
375
|
+
async unset(field) {
|
|
376
|
+
if (!FIELDS.has(field)) return;
|
|
377
|
+
await this.apply({ [field]: void 0 });
|
|
378
|
+
}
|
|
379
|
+
/** Persist a patch, then re-seed from the route's fresh response. */
|
|
380
|
+
async apply(patch) {
|
|
381
|
+
const request = {};
|
|
382
|
+
for (const [key, value] of Object.entries(patch)) request[key] = value === void 0 ? null : value;
|
|
383
|
+
const response = await this.api.saveConfig(request);
|
|
384
|
+
this.value = response.config;
|
|
385
|
+
this.saved = { ...response.saved };
|
|
386
|
+
this.publish();
|
|
387
|
+
}
|
|
388
|
+
publish() {
|
|
389
|
+
for (const listener of this.listeners) listener();
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
//#endregion
|
|
393
|
+
//#region src/client/locales.ts
|
|
394
|
+
/**
|
|
395
|
+
* dsh-skill-hub surface copy (zh/en). The dictionary type is re-exported
|
|
396
|
+
* so the LocaleNamespaceMap module augmentation in client/index.ts can
|
|
397
|
+
* reference it without a value import.
|
|
398
|
+
*/
|
|
399
|
+
const zh = {
|
|
400
|
+
"entry.label": "技能",
|
|
401
|
+
"entry.tooltip": "技能中枢:浏览、分组与同步管理本地技能",
|
|
402
|
+
"view.sources": "来源",
|
|
403
|
+
"view.scenes": "场景",
|
|
404
|
+
"view.market": "市场",
|
|
405
|
+
"view.flat": "平铺",
|
|
406
|
+
"view.grouped": "分组",
|
|
407
|
+
"panel.title": "技能中枢",
|
|
408
|
+
"panel.count": "共 {count} 个技能",
|
|
409
|
+
"panel.disabledCount": "已禁用 {count}",
|
|
410
|
+
"panel.new": "新建技能",
|
|
411
|
+
"panel.search": "搜索技能…",
|
|
412
|
+
"legend.model": "模型可调 — Agent 可自动调用",
|
|
413
|
+
"legend.user": "用户可调 — 仅显式点名时触发",
|
|
414
|
+
"legend.hint": "圆点标在技能名旁;无圆点表示该技能不可被调用。",
|
|
415
|
+
"panel.enabled": "已启用",
|
|
416
|
+
"panel.disabled": "已禁用",
|
|
417
|
+
"panel.diagnostics": "发现诊断",
|
|
418
|
+
"panel.empty": "没有匹配的技能。",
|
|
419
|
+
"panel.emptyAll": "本地还没有技能。点击「新建技能」创建第一个。",
|
|
420
|
+
"panel.loading": "读取中…",
|
|
421
|
+
"panel.incomplete": "(目录可能不完整)",
|
|
422
|
+
"market.addHint": "像 codex 一样添加仓库源:输入 owner/repo 或 GitHub 链接,扫描其中的技能并一键导入;导入后自动跟踪上游更新。",
|
|
423
|
+
"market.addPlaceholder": "owner/repo 或 https://github.com/…",
|
|
424
|
+
"market.addSource": "添加源",
|
|
425
|
+
"market.noSources": "还没有市场源。从下方内置市场添加,或输入一个 GitHub 仓库。",
|
|
426
|
+
"market.catalogTitle": "内置市场",
|
|
427
|
+
"market.catalogHint": "精选技能仓库,添加后即可扫描安装,并持续检查更新。",
|
|
428
|
+
"market.catalog.anthropics": "Anthropic 官方技能合集(Claude Skills)",
|
|
429
|
+
"market.catalog.superpowers": "Superpowers 社区技能合集",
|
|
430
|
+
"market.catalog.mattpocock": "Matt Pocock 技能集",
|
|
431
|
+
"market.catalog.openDesign": "设计技能合集(design-templates)",
|
|
432
|
+
"market.added": "已添加",
|
|
433
|
+
"market.add": "添加",
|
|
434
|
+
"market.mySources": "我的市场源",
|
|
435
|
+
"market.installed": "已装 {count}",
|
|
436
|
+
"market.updatable": "可更新 {count}",
|
|
437
|
+
"market.deletedUpstream": "上游已删 {count}",
|
|
438
|
+
"market.checkAll": "检查全部",
|
|
439
|
+
"market.updateAll": "全部更新",
|
|
440
|
+
"market.updateAllConfirmTitle": "批量更新全部技能?",
|
|
441
|
+
"market.updateAllConfirmText": "将把以下来源的可更新技能更新到上游最新版本(覆盖本地修改):",
|
|
442
|
+
"market.updateAllItem": "{repo}({count} 个技能)",
|
|
443
|
+
"market.updateAllDone": "已更新 {count} 个来源",
|
|
444
|
+
"market.deleteSource": "移除源",
|
|
445
|
+
"market.scan": "扫描",
|
|
446
|
+
"market.scanning": "扫描中…",
|
|
447
|
+
"market.check": "检查",
|
|
448
|
+
"market.sync": "同步",
|
|
449
|
+
"market.syncing": "同步中…",
|
|
450
|
+
"market.updated": "有更新",
|
|
451
|
+
"market.newRelease": "新版本 v{version}",
|
|
452
|
+
"market.branchTitle": "选择分支",
|
|
453
|
+
"market.branchHint": "该仓库没有正式发布,选择要跟踪的分支(默认为 main):",
|
|
454
|
+
"market.branchConfirm": "确定",
|
|
455
|
+
"market.syncTitle": "同步本地技能",
|
|
456
|
+
"market.syncText": "市场源已对齐到 {ref},要同步更新以下本地技能吗?",
|
|
457
|
+
"market.syncNone": "该来源还没有导入本地技能。",
|
|
458
|
+
"market.syncCancel": "跳过",
|
|
459
|
+
"market.removeHint": "移除源不影响已安装的技能。",
|
|
460
|
+
"repo.ready": "发现 {count} 个可导入技能",
|
|
461
|
+
"repo.empty": "没有找到可导入技能。",
|
|
462
|
+
"repo.selectAll": "全选",
|
|
463
|
+
"repo.clearAll": "清空",
|
|
464
|
+
"repo.import": "导入所选 ({count})",
|
|
465
|
+
"repo.importing": "导入中…",
|
|
466
|
+
"repo.existing": "已存在",
|
|
467
|
+
"repo.files": "{count} 个文件 · {size}",
|
|
468
|
+
"repo.imported": "导入成功 {count} 个",
|
|
469
|
+
"repo.skippedExisting": "跳过已存在 {count} 个",
|
|
470
|
+
"repo.failed": "失败 {count} 个",
|
|
471
|
+
"repo.root.skills": "技能 skills",
|
|
472
|
+
"repo.root.designTemplates": "模板 design-templates",
|
|
473
|
+
"update.check": "检查更新",
|
|
474
|
+
"update.checking": "检查中…",
|
|
475
|
+
"update.upToDate": "已是最新版本 v{version}",
|
|
476
|
+
"update.available": "发现新版本 v{version}(当前 v{current})",
|
|
477
|
+
"update.newVersion": "新版本 v{version}",
|
|
478
|
+
"update.viewRelease": "查看发布",
|
|
479
|
+
"update.error": "检查更新失败:{error}",
|
|
480
|
+
"update.unavailable": "暂无可用的发布版本",
|
|
481
|
+
"group.project-dsh": "项目 .dsh/skills",
|
|
482
|
+
"group.project-agents": "项目 .agents/skills",
|
|
483
|
+
"group.custom": "自定义目录",
|
|
484
|
+
"group.runtime": "运行时",
|
|
485
|
+
"group.user-dsh": "用户 ~/.dsh/skills",
|
|
486
|
+
"group.user-agents": "用户 ~/.agents/skills",
|
|
487
|
+
"group.bundled": "内置",
|
|
488
|
+
"group.other": "其他来源",
|
|
489
|
+
"groups.new": "新建场景",
|
|
490
|
+
"groups.namePlaceholder": "场景名",
|
|
491
|
+
"groups.edit": "编辑",
|
|
492
|
+
"groups.rename": "保存名称",
|
|
493
|
+
"groups.delete": "删除场景",
|
|
494
|
+
"groups.saveMembers": "保存成员",
|
|
495
|
+
"groups.membersHint": "勾选技能加入此场景(含已禁用)。",
|
|
496
|
+
"groups.empty": "还没有场景。先新建一个,再把技能归类。",
|
|
497
|
+
"groups.noCollections": "暂无来源组。从市场导入技能后自动按来源聚合。",
|
|
498
|
+
"groups.personal": "个人",
|
|
499
|
+
"groups.noWritable": "该组没有可写技能(只读技能无法由中枢开关)",
|
|
500
|
+
"groups.closeAll": "全部关闭",
|
|
501
|
+
"groups.keepOn": "保留开启",
|
|
502
|
+
"groups.conflictTitle": "关闭此组?",
|
|
503
|
+
"groups.conflictText": "以下技能也在其他组中开启,关闭它们会影响那些组:",
|
|
504
|
+
"filter.allSources": "全部来源",
|
|
505
|
+
"filter.private": "个人",
|
|
506
|
+
"sort.name": "按名称",
|
|
507
|
+
"sort.added": "按添加时间",
|
|
508
|
+
"sort.uses": "按调用次数",
|
|
509
|
+
"time.justNow": "刚刚",
|
|
510
|
+
"time.minutesAgo": "{value} 分钟前",
|
|
511
|
+
"time.hoursAgo": "{value} 小时前",
|
|
512
|
+
"time.daysAgo": "{value} 天前",
|
|
513
|
+
"time.weeksAgo": "{value} 周前",
|
|
514
|
+
"badge.source.project-dsh": "项目 .dsh",
|
|
515
|
+
"badge.source.project-agents": "项目 .agents",
|
|
516
|
+
"badge.source.custom": "自定义",
|
|
517
|
+
"badge.source.runtime": "运行时",
|
|
518
|
+
"badge.source.user-dsh": "用户 .dsh",
|
|
519
|
+
"badge.source.user-agents": "用户 .agents",
|
|
520
|
+
"badge.source.bundled": "内置",
|
|
521
|
+
"row.readonly": "只读",
|
|
522
|
+
"row.disable": "禁用",
|
|
523
|
+
"row.enable": "启用",
|
|
524
|
+
"row.delete": "移入回收站",
|
|
525
|
+
"delete.confirmTitle": "移入回收站?",
|
|
526
|
+
"delete.confirmText": "确定把「{name}」移入回收站吗?可随时恢复。",
|
|
527
|
+
"delete.confirm": "移入回收站",
|
|
528
|
+
"detail.back": "返回列表",
|
|
529
|
+
"detail.provider": "提供者",
|
|
530
|
+
"detail.addedAt": "添加时间",
|
|
531
|
+
"detail.updatedAt": "更新时间",
|
|
532
|
+
"detail.path": "路径",
|
|
533
|
+
"detail.whenToUse": "适用场景",
|
|
534
|
+
"detail.uses": "调用次数",
|
|
535
|
+
"detail.groups": "所属场景",
|
|
536
|
+
"detail.loading": "读取技能正文…",
|
|
537
|
+
"source.private": "个人技能(未绑定上游,不检查更新)",
|
|
538
|
+
"source.commit": "已同步版本",
|
|
539
|
+
"source.unverified": "尚未核对上游",
|
|
540
|
+
"source.check": "检查更新",
|
|
541
|
+
"source.checkHint": "点击检查上游更新",
|
|
542
|
+
"source.checking": "检查中…",
|
|
543
|
+
"source.upToDate": "已是最新",
|
|
544
|
+
"source.updated": "有更新 {count} 个",
|
|
545
|
+
"source.deleted": "上游已删除 {count} 个",
|
|
546
|
+
"source.error": "检查失败",
|
|
547
|
+
"source.throttled": "刚刚检查过",
|
|
548
|
+
"source.sync": "同步",
|
|
549
|
+
"source.syncing": "同步中…",
|
|
550
|
+
"source.syncConfirmTitle": "同步技能?",
|
|
551
|
+
"source.syncConfirmText": "将用上游最新版本覆盖本地对以下技能的所有修改:",
|
|
552
|
+
"source.followDelete": "跟进删除",
|
|
553
|
+
"source.deleteConfirmTitle": "跟进上游删除?",
|
|
554
|
+
"source.deleteConfirmText": "以下技能已从上游仓库移除,本地目录将移入回收站(可恢复):",
|
|
555
|
+
"source.trash": "回收站",
|
|
556
|
+
"source.restore": "恢复",
|
|
557
|
+
"source.clearTrash": "清空回收站",
|
|
558
|
+
"source.clearTrashConfirmTitle": "清空回收站?",
|
|
559
|
+
"source.clearTrashConfirmText": "回收站里的技能会被永久删除,无法恢复。此操作不可撤销。",
|
|
560
|
+
"source.clearTrashConfirm": "永久删除",
|
|
561
|
+
"form.capabilityHint": "能力怎么选:重复方法 → 技能;外部系统 → MCP;长期资料 → 知识库。",
|
|
562
|
+
"form.descPlaceholder": "一句话说明用途(含触发词),Agent 靠它自动激活技能",
|
|
563
|
+
"form.name": "技能名(kebab-case,如 code-review)",
|
|
564
|
+
"form.desc": "描述(一句话路由说明)",
|
|
565
|
+
"form.root": "目标目录",
|
|
566
|
+
"form.submit": "创建",
|
|
567
|
+
"form.cancel": "取消",
|
|
568
|
+
"form.busy": "创建中…",
|
|
569
|
+
"form.success": "已创建:",
|
|
570
|
+
"form.error": "创建失败:",
|
|
571
|
+
"err.dismiss": "知道了",
|
|
572
|
+
"settings.title": "Skill Hub",
|
|
573
|
+
"settings.description": "管理 dsh 本地技能目录:浏览、分组、来源同步与新建。",
|
|
574
|
+
"settings.enabled": "启用插件",
|
|
575
|
+
"settings.enabledHint": "关闭后技能中枢的路由、入口与公告全部下线。",
|
|
576
|
+
"settings.announceToAgent": "向 Agent 公告",
|
|
577
|
+
"settings.announceToAgentHint": "在系统提示中加入本插件说明,用户提到技能管理时 Agent 知道如何协作。",
|
|
578
|
+
"settings.dotModelColor": "模型可调圆点颜色",
|
|
579
|
+
"settings.dotModelColorHint": "技能名旁蓝色圆点的颜色(#rrggbb)。",
|
|
580
|
+
"settings.dotUserColor": "用户可调圆点颜色",
|
|
581
|
+
"settings.dotUserColorHint": "技能名旁绿色圆点的颜色(#rrggbb)。",
|
|
582
|
+
"settings.showUseCount": "显示调用次数",
|
|
583
|
+
"settings.showUseCountHint": "在技能名旁显示琥珀色调用次数。",
|
|
584
|
+
"settings.showUseTime": "显示最近调用时间",
|
|
585
|
+
"settings.showUseTimeHint": "在技能名行右侧显示相对时间(如「3 天前」)。",
|
|
586
|
+
"settings.showGroupSummary": "显示分组汇总",
|
|
587
|
+
"settings.showGroupSummaryHint": "在分组标题后汇总调用次数与最近调用时间。",
|
|
588
|
+
"settings.inherit": "继承",
|
|
589
|
+
"settings.on": "开",
|
|
590
|
+
"settings.off": "关",
|
|
591
|
+
"settings.overridden": "已覆盖",
|
|
592
|
+
"settings.reset": "恢复默认",
|
|
593
|
+
"settings.unsaved": "未保存",
|
|
594
|
+
"settings.save": "保存",
|
|
595
|
+
"settings.saving": "保存中…",
|
|
596
|
+
"settings.discard": "放弃修改",
|
|
597
|
+
"settings.saveFailed": "本部署没有接受这些值,已保留供你修改。",
|
|
598
|
+
"settings.readOnly": "本部署的设置为只读。",
|
|
599
|
+
"settings.notExposed": "本部署没有挂载该设置命名空间。",
|
|
600
|
+
"settings.collapse": "收起设置",
|
|
601
|
+
"settings.expand": "展开设置"
|
|
602
|
+
};
|
|
603
|
+
const en = {
|
|
604
|
+
"entry.label": "Skills",
|
|
605
|
+
"entry.tooltip": "Skill hub: browse, group, and sync local skills",
|
|
606
|
+
"view.sources": "Sources",
|
|
607
|
+
"view.scenes": "Scenes",
|
|
608
|
+
"view.market": "Market",
|
|
609
|
+
"view.flat": "Flat",
|
|
610
|
+
"view.grouped": "Grouped",
|
|
611
|
+
"panel.title": "Skill Hub",
|
|
612
|
+
"panel.count": "{count} skills total",
|
|
613
|
+
"panel.disabledCount": "{count} disabled",
|
|
614
|
+
"panel.new": "New Skill",
|
|
615
|
+
"panel.search": "Search skills…",
|
|
616
|
+
"legend.model": "Model-invocable — agents may call automatically",
|
|
617
|
+
"legend.user": "User-invocable — explicit invocation only",
|
|
618
|
+
"legend.hint": "Dots sit beside the skill name; no dot means the skill is not invocable.",
|
|
619
|
+
"panel.enabled": "Enabled",
|
|
620
|
+
"panel.disabled": "Disabled",
|
|
621
|
+
"panel.diagnostics": "Discovery diagnostics",
|
|
622
|
+
"panel.empty": "No matching skills.",
|
|
623
|
+
"panel.emptyAll": "No skills installed yet. Create the first one.",
|
|
624
|
+
"panel.loading": "Loading…",
|
|
625
|
+
"panel.incomplete": "(catalog may be incomplete)",
|
|
626
|
+
"market.addHint": "Codex-style market sources: add a repo (owner/repo or GitHub URL), scan its skills and import; imported skills are tracked upstream automatically.",
|
|
627
|
+
"market.addPlaceholder": "owner/repo or https://github.com/…",
|
|
628
|
+
"market.addSource": "Add source",
|
|
629
|
+
"market.noSources": "No market sources yet. Add one from the built-in catalog below, or enter a GitHub repo.",
|
|
630
|
+
"market.catalogTitle": "Market catalog",
|
|
631
|
+
"market.catalogHint": "Curated skill repos — add one to scan and install, with ongoing update checks.",
|
|
632
|
+
"market.catalog.anthropics": "Official Anthropic skill collection (Claude Skills)",
|
|
633
|
+
"market.catalog.superpowers": "Superpowers community skill collection",
|
|
634
|
+
"market.catalog.mattpocock": "Matt Pocock skill collection",
|
|
635
|
+
"market.catalog.openDesign": "Design skill collection (design-templates)",
|
|
636
|
+
"market.added": "Added",
|
|
637
|
+
"market.add": "Add",
|
|
638
|
+
"market.mySources": "My sources",
|
|
639
|
+
"market.installed": "{count} installed",
|
|
640
|
+
"market.updatable": "{count} updatable",
|
|
641
|
+
"market.deletedUpstream": "{count} deleted upstream",
|
|
642
|
+
"market.checkAll": "Check all",
|
|
643
|
+
"market.updateAll": "Update all",
|
|
644
|
+
"market.updateAllConfirmTitle": "Update every skill?",
|
|
645
|
+
"market.updateAllConfirmText": "The updatable skills of these sources will be overwritten with their latest upstream versions:",
|
|
646
|
+
"market.updateAllItem": "{repo} ({count} skills)",
|
|
647
|
+
"market.updateAllDone": "Updated {count} sources",
|
|
648
|
+
"market.deleteSource": "Remove source",
|
|
649
|
+
"market.scan": "Scan",
|
|
650
|
+
"market.scanning": "Scanning…",
|
|
651
|
+
"market.check": "Check",
|
|
652
|
+
"market.sync": "Sync",
|
|
653
|
+
"market.syncing": "Syncing…",
|
|
654
|
+
"market.updated": "Updates available",
|
|
655
|
+
"market.newRelease": "New release v{version}",
|
|
656
|
+
"market.branchTitle": "Choose a branch",
|
|
657
|
+
"market.branchHint": "This repo has no releases. Pick a branch to track (main by default):",
|
|
658
|
+
"market.branchConfirm": "Confirm",
|
|
659
|
+
"market.syncTitle": "Update local skills",
|
|
660
|
+
"market.syncText": "Market source aligned to {ref}. Update these local skills?",
|
|
661
|
+
"market.syncNone": "No local skills tracked from this repo yet.",
|
|
662
|
+
"market.syncCancel": "Skip",
|
|
663
|
+
"market.removeHint": "Removing a source does not affect installed skills.",
|
|
664
|
+
"repo.ready": "{count} importable skills found",
|
|
665
|
+
"repo.empty": "No importable skills found.",
|
|
666
|
+
"repo.selectAll": "Select all",
|
|
667
|
+
"repo.clearAll": "Clear",
|
|
668
|
+
"repo.import": "Import selected ({count})",
|
|
669
|
+
"repo.importing": "Importing…",
|
|
670
|
+
"repo.existing": "Existing",
|
|
671
|
+
"repo.files": "{count} files · {size}",
|
|
672
|
+
"repo.imported": "Imported {count}",
|
|
673
|
+
"repo.skippedExisting": "Skipped existing {count}",
|
|
674
|
+
"repo.failed": "Failed {count}",
|
|
675
|
+
"repo.root.skills": "Skills",
|
|
676
|
+
"repo.root.designTemplates": "Templates design-templates",
|
|
677
|
+
"update.check": "Check for updates",
|
|
678
|
+
"update.checking": "Checking…",
|
|
679
|
+
"update.upToDate": "Up to date: v{version}",
|
|
680
|
+
"update.available": "Update available: v{version} (current v{current})",
|
|
681
|
+
"update.newVersion": "New version v{version}",
|
|
682
|
+
"update.viewRelease": "View release",
|
|
683
|
+
"update.error": "Update check failed: {error}",
|
|
684
|
+
"update.unavailable": "No release available",
|
|
685
|
+
"group.project-dsh": "Project .dsh/skills",
|
|
686
|
+
"group.project-agents": "Project .agents/skills",
|
|
687
|
+
"group.custom": "Custom directories",
|
|
688
|
+
"group.runtime": "Runtime",
|
|
689
|
+
"group.user-dsh": "User ~/.dsh/skills",
|
|
690
|
+
"group.user-agents": "User ~/.agents/skills",
|
|
691
|
+
"group.bundled": "Bundled",
|
|
692
|
+
"group.other": "Other sources",
|
|
693
|
+
"groups.new": "New scene",
|
|
694
|
+
"groups.namePlaceholder": "Scene name",
|
|
695
|
+
"groups.edit": "Edit",
|
|
696
|
+
"groups.rename": "Save name",
|
|
697
|
+
"groups.delete": "Delete scene",
|
|
698
|
+
"groups.saveMembers": "Save members",
|
|
699
|
+
"groups.membersHint": "Check skills to add to this scene (disabled skills included).",
|
|
700
|
+
"groups.empty": "No scenes yet. Create one and start categorizing skills.",
|
|
701
|
+
"groups.noCollections": "No source groups yet. Import skills from the market to group them by source.",
|
|
702
|
+
"groups.personal": "Personal",
|
|
703
|
+
"groups.noWritable": "No writable skills in this group (read-only skills cannot be switched from the hub)",
|
|
704
|
+
"groups.closeAll": "Close all",
|
|
705
|
+
"groups.keepOn": "Keep on",
|
|
706
|
+
"groups.conflictTitle": "Close this group?",
|
|
707
|
+
"groups.conflictText": "These skills are also enabled in other groups; closing them affects those groups:",
|
|
708
|
+
"filter.allSources": "All sources",
|
|
709
|
+
"filter.private": "Personal",
|
|
710
|
+
"sort.name": "By name",
|
|
711
|
+
"sort.added": "By added",
|
|
712
|
+
"sort.uses": "By uses",
|
|
713
|
+
"time.justNow": "just now",
|
|
714
|
+
"time.minutesAgo": "{value} min ago",
|
|
715
|
+
"time.hoursAgo": "{value} h ago",
|
|
716
|
+
"time.daysAgo": "{value} d ago",
|
|
717
|
+
"time.weeksAgo": "{value} w ago",
|
|
718
|
+
"badge.source.project-dsh": "project .dsh",
|
|
719
|
+
"badge.source.project-agents": "project .agents",
|
|
720
|
+
"badge.source.custom": "custom",
|
|
721
|
+
"badge.source.runtime": "runtime",
|
|
722
|
+
"badge.source.user-dsh": "user .dsh",
|
|
723
|
+
"badge.source.user-agents": "user .agents",
|
|
724
|
+
"badge.source.bundled": "bundled",
|
|
725
|
+
"row.readonly": "read-only",
|
|
726
|
+
"row.disable": "Disable",
|
|
727
|
+
"row.enable": "Enable",
|
|
728
|
+
"row.delete": "Move to trash",
|
|
729
|
+
"delete.confirmTitle": "Move to trash?",
|
|
730
|
+
"delete.confirmText": "Move \"{name}\" to trash? It can be restored anytime.",
|
|
731
|
+
"delete.confirm": "Move to trash",
|
|
732
|
+
"detail.back": "Back to list",
|
|
733
|
+
"detail.provider": "Provider",
|
|
734
|
+
"detail.addedAt": "Added",
|
|
735
|
+
"detail.updatedAt": "Updated",
|
|
736
|
+
"detail.path": "Path",
|
|
737
|
+
"detail.whenToUse": "When to use",
|
|
738
|
+
"detail.uses": "Uses",
|
|
739
|
+
"detail.groups": "Groups",
|
|
740
|
+
"detail.loading": "Loading skill body…",
|
|
741
|
+
"source.private": "Personal skill (not bound to an upstream; no update checks)",
|
|
742
|
+
"source.commit": "Synced commit",
|
|
743
|
+
"source.unverified": "upstream not verified yet",
|
|
744
|
+
"source.check": "Check for updates",
|
|
745
|
+
"source.checkHint": "Click to check the upstream for updates",
|
|
746
|
+
"source.checking": "Checking…",
|
|
747
|
+
"source.upToDate": "Up to date",
|
|
748
|
+
"source.updated": "{count} updates",
|
|
749
|
+
"source.deleted": "{count} deleted upstream",
|
|
750
|
+
"source.error": "Check failed",
|
|
751
|
+
"source.throttled": "Checked recently",
|
|
752
|
+
"source.sync": "Sync",
|
|
753
|
+
"source.syncing": "Syncing…",
|
|
754
|
+
"source.syncConfirmTitle": "Sync skills?",
|
|
755
|
+
"source.syncConfirmText": "This overwrites local changes to these skills with the latest upstream version:",
|
|
756
|
+
"source.followDelete": "Follow deletion",
|
|
757
|
+
"source.deleteConfirmTitle": "Follow upstream deletion?",
|
|
758
|
+
"source.deleteConfirmText": "These skills were removed upstream; their local directories move to trash (restorable):",
|
|
759
|
+
"source.trash": "Trash",
|
|
760
|
+
"source.restore": "Restore",
|
|
761
|
+
"source.clearTrash": "Empty trash",
|
|
762
|
+
"source.clearTrashConfirmTitle": "Empty the trash?",
|
|
763
|
+
"source.clearTrashConfirmText": "Skills in the trash will be permanently deleted and cannot be restored.",
|
|
764
|
+
"source.clearTrashConfirm": "Delete permanently",
|
|
765
|
+
"form.capabilityHint": "Choosing a capability: repeatable method → skill; external system → MCP; long-term knowledge → knowledge base.",
|
|
766
|
+
"form.descPlaceholder": "One line on what it does (with trigger words) — agents auto-activate from this",
|
|
767
|
+
"form.name": "Skill name (kebab-case, e.g. code-review)",
|
|
768
|
+
"form.desc": "Description (one routing sentence)",
|
|
769
|
+
"form.root": "Target root",
|
|
770
|
+
"form.submit": "Create",
|
|
771
|
+
"form.cancel": "Cancel",
|
|
772
|
+
"form.busy": "Creating…",
|
|
773
|
+
"form.success": "Created: ",
|
|
774
|
+
"form.error": "Create failed: ",
|
|
775
|
+
"err.dismiss": "Dismiss",
|
|
776
|
+
"settings.title": "Skill Hub",
|
|
777
|
+
"settings.description": "Manage the local dsh skill catalog: browse, group, sync, and scaffold skills.",
|
|
778
|
+
"settings.enabled": "Enable plugin",
|
|
779
|
+
"settings.enabledHint": "Disables the hub routes, entries, and announcement.",
|
|
780
|
+
"settings.announceToAgent": "Announce to agent",
|
|
781
|
+
"settings.announceToAgentHint": "Adds a system-prompt section so agents know how to collaborate when users mention skill management.",
|
|
782
|
+
"settings.dotModelColor": "Model-invocable dot color",
|
|
783
|
+
"settings.dotModelColorHint": "Color of the blue dot beside skill names (#rrggbb).",
|
|
784
|
+
"settings.dotUserColor": "User-invocable dot color",
|
|
785
|
+
"settings.dotUserColorHint": "Color of the green dot beside skill names (#rrggbb).",
|
|
786
|
+
"settings.showUseCount": "Show invocation count",
|
|
787
|
+
"settings.showUseCountHint": "Show the amber invocation count beside each skill name.",
|
|
788
|
+
"settings.showUseTime": "Show last-used time",
|
|
789
|
+
"settings.showUseTimeHint": "Show relative last-used time on the name row (e.g. \"3 d ago\").",
|
|
790
|
+
"settings.showGroupSummary": "Show group summaries",
|
|
791
|
+
"settings.showGroupSummaryHint": "Summarize invocation count and last-used time after group titles.",
|
|
792
|
+
"settings.inherit": "Inherit",
|
|
793
|
+
"settings.on": "On",
|
|
794
|
+
"settings.off": "Off",
|
|
795
|
+
"settings.overridden": "Overridden",
|
|
796
|
+
"settings.reset": "Reset to default",
|
|
797
|
+
"settings.unsaved": "Unsaved",
|
|
798
|
+
"settings.save": "Save",
|
|
799
|
+
"settings.saving": "Saving…",
|
|
800
|
+
"settings.discard": "Discard",
|
|
801
|
+
"settings.saveFailed": "The deployment did not accept these values; they were left for you to correct.",
|
|
802
|
+
"settings.readOnly": "This deployment stores settings read-only.",
|
|
803
|
+
"settings.notExposed": "This deployment does not expose the settings namespace.",
|
|
804
|
+
"settings.collapse": "Hide settings",
|
|
805
|
+
"settings.expand": "Show settings"
|
|
806
|
+
};
|
|
807
|
+
/** Interpolate a {name} template with the given values. */
|
|
808
|
+
function t(dict, key, values) {
|
|
809
|
+
let text = dict[key] ?? key;
|
|
810
|
+
if (values !== void 0) for (const [name, value] of Object.entries(values)) text = text.replaceAll("{" + name + "}", String(value));
|
|
811
|
+
return text;
|
|
812
|
+
}
|
|
813
|
+
//#endregion
|
|
814
|
+
//#region src/client/settings-nav-icon.ts
|
|
815
|
+
/**
|
|
816
|
+
* The settings shell renders section nav icons from a hard-coded id map and
|
|
817
|
+
* exposes no registrant icon option, so the skill-hub section falls back to
|
|
818
|
+
* the generic gear. This scoped DOM patch swaps in the official skill icon
|
|
819
|
+
* whenever the settings dialog re-renders.
|
|
820
|
+
*/
|
|
821
|
+
const SKILL_SECTION_LABELS = ["技能", "Skills"];
|
|
822
|
+
const SKILL_ICON_SVG = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" data-skill-hub-nav-icon="true"><path d="M12.5113 15.4067C12.4395 15.6249 12.1308 15.6249 12.059 15.4067L11.643 14.1416C11.454 13.567 11.0033 13.1164 10.4288 12.9274L9.16369 12.5113C8.94544 12.4395 8.94544 12.1308 9.16369 12.059L10.4288 11.643C11.0033 11.454 11.454 11.0033 11.643 10.4288L12.059 9.16369C12.1308 8.94544 12.4395 8.94544 12.5113 9.16369L12.9274 10.4288C13.1164 11.0033 13.567 11.454 14.1416 11.643L15.4067 12.059C15.6249 12.1308 15.6249 12.4395 15.4067 12.5113L14.1416 12.9274C13.567 13.1164 13.1164 13.567 12.9274 14.1416L12.5113 15.4067Z" fill="currentColor"/><path d="M9.02246 0.546878C9.9822 0.546878 10.7564 0.545403 11.374 0.612307C12.0042 0.680586 12.5515 0.826244 13.0273 1.17188C13.3052 1.37376 13.5501 1.61868 13.752 1.89649C14.0975 2.37225 14.2432 2.91984 14.3115 3.54981C14.3784 4.16727 14.377 4.94206 14.377 5.90137V8.51367C13.9611 8.29533 13.5071 8.13985 13.0273 8.06055V5.90137C13.0273 4.9121 13.0259 4.22322 12.9688 3.69532C12.9129 3.18044 12.8098 2.89782 12.6592 2.69043C12.5406 2.52724 12.3966 2.38326 12.2334 2.26465C12.026 2.11404 11.7437 2.0109 11.2285 1.95508C10.7005 1.89789 10.0122 1.89649 9.02246 1.89649H6.55371C5.56395 1.89649 4.87569 1.89787 4.34766 1.95508C3.83242 2.01092 3.55022 2.11398 3.34278 2.26465C3.17953 2.38329 3.03564 2.52719 2.91699 2.69043C2.76642 2.89782 2.66325 3.18042 2.60742 3.69532C2.55027 4.22322 2.54883 4.9121 2.54883 5.90137V10.0986C2.54883 11.0878 2.55031 11.7768 2.60742 12.3047C2.66326 12.8196 2.76642 13.1032 2.91699 13.3105C3.03558 13.4736 3.17966 13.6178 3.34278 13.7363C3.5502 13.8869 3.83265 13.9901 4.34766 14.0459C4.87568 14.1031 5.56398 14.1035 6.55371 14.1035H8.08399C8.27443 14.6025 8.55077 15.0585 8.89551 15.4541H6.55371C5.59402 15.4541 4.81976 15.4546 4.20215 15.3877C3.57204 15.3194 3.02468 15.1738 2.54883 14.8281C2.27111 14.6263 2.02606 14.3813 1.82422 14.1035C1.47883 13.6278 1.33293 13.08 1.26465 12.4502C1.19783 11.8327 1.19922 11.0579 1.19922 10.0986V5.90137C1.19922 4.94206 1.1978 4.16727 1.26465 3.54981C1.33295 2.91984 1.47867 2.37225 1.82422 1.89649C2.02613 1.61864 2.27098 1.37379 2.54883 1.17188C3.02472 0.826181 3.57197 0.6806 4.20215 0.612307C4.81976 0.545393 5.594 0.546877 6.55371 0.546878H9.02246ZM9.19629 9.14649H4.5459V7.84571H9.19629V9.14649ZM11.0303 6.10645H4.5459V4.80567H11.0303V6.10645Z" fill="currentColor"/></svg>`;
|
|
823
|
+
let iconTemplate = null;
|
|
824
|
+
function skillIcon() {
|
|
825
|
+
if (iconTemplate === null) iconTemplate = new DOMParser().parseFromString(SKILL_ICON_SVG, "image/svg+xml").documentElement;
|
|
826
|
+
return iconTemplate.cloneNode(true);
|
|
827
|
+
}
|
|
828
|
+
function patchNavIcon() {
|
|
829
|
+
const dialog = document.querySelector("[role=\"dialog\"]");
|
|
830
|
+
if (dialog === null) return;
|
|
831
|
+
for (const button of dialog.querySelectorAll("button")) {
|
|
832
|
+
const label = button.querySelector("span");
|
|
833
|
+
if (label === null || !SKILL_SECTION_LABELS.includes(label.textContent?.trim() ?? "")) continue;
|
|
834
|
+
if (button.querySelector("svg[data-skill-hub-nav-icon]") !== null) continue;
|
|
835
|
+
const current = button.querySelector("svg");
|
|
836
|
+
const icon = skillIcon();
|
|
837
|
+
if (current !== null) current.replaceWith(icon);
|
|
838
|
+
else button.insertBefore(icon, button.firstChild);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
/** Swap the settings nav gear for the skill icon and keep it in sync. */
|
|
842
|
+
function applySettingsNavIcon() {
|
|
843
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {};
|
|
844
|
+
patchNavIcon();
|
|
845
|
+
const observer = new MutationObserver(() => {
|
|
846
|
+
patchNavIcon();
|
|
847
|
+
});
|
|
848
|
+
observer.observe(document.body, {
|
|
849
|
+
childList: true,
|
|
850
|
+
subtree: true
|
|
851
|
+
});
|
|
852
|
+
return () => {
|
|
853
|
+
observer.disconnect();
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
//#endregion
|
|
857
|
+
//#region \0dsh-css:/Users/huanyi/Documents/personal/tools/dsh-skill-hub/src/client/settings-card.module.css.mjs
|
|
858
|
+
const css$1 = ".TqD_hG_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.TqD_hG_card:hover{border-color:var(--dsw-alias-label-dimmed)}.TqD_hG_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.TqD_hG_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.TqD_hG_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.TqD_hG_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.TqD_hG_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.TqD_hG_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.TqD_hG_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.TqD_hG_chevron,.TqD_hG_chevronOpen{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.TqD_hG_chevronOpen{transform:rotate(180deg)}.TqD_hG_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.TqD_hG_readOnly,.TqD_hG_notExposed{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.TqD_hG_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.TqD_hG_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}.TqD_hG_discard,.TqD_hG_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.TqD_hG_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.TqD_hG_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.TqD_hG_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.TqD_hG_discard:disabled,.TqD_hG_save:disabled{opacity:.4;cursor:default}.TqD_hG_discard:focus-visible,.TqD_hG_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.TqD_hG_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.TqD_hG_field+.TqD_hG_field{border-top:1px solid var(--dsw-alias-border-l2)}.TqD_hG_head{align-items:center;gap:8px;display:flex}.TqD_hG_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.TqD_hG_badges{align-items:center;gap:8px;display:inline-flex}.TqD_hG_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.TqD_hG_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.TqD_hG_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.TqD_hG_reset:disabled{cursor:default}.TqD_hG_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.TqD_hG_input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.TqD_hG_input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.TqD_hG_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.TqD_hG_colorRow{align-items:center;gap:8px;display:flex}.TqD_hG_colorInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);cursor:pointer;border-radius:8px;width:34px;height:34px;padding:0}.TqD_hG_colorInput:disabled{opacity:.5;cursor:default}.TqD_hG_colorInput::-webkit-color-swatch-wrapper{padding:2px}.TqD_hG_colorInput::-webkit-color-swatch{border:none;border-radius:4px}.TqD_hG_colorText{flex:1;min-width:0}.TqD_hG_switchRow{align-items:center;gap:12px;display:flex}.TqD_hG_switchText{flex:1;min-width:0}.TqD_hG_switch,.TqD_hG_switchOn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);cursor:pointer;border-radius:999px;flex:none;width:38px;height:22px;padding:2px;transition:background .15s,border-color .15s}.TqD_hG_switchOn{border-color:var(--dsw-alias-state-business-primary,#2f81f7);background:var(--dsw-alias-state-business-primary,#2f81f7)}.TqD_hG_switch:disabled,.TqD_hG_switchOn:disabled{opacity:.4;cursor:default}.TqD_hG_switchThumb{background:#fff;border-radius:50%;width:16px;height:16px;transition:transform .15s;display:block;transform:translate(0);box-shadow:0 1px 2px #00000040}.TqD_hG_switchOn .TqD_hG_switchThumb{transform:translate(16px)}";
|
|
859
|
+
const tagId$1 = "dsh-skill-hub/settings-card.module.css";
|
|
860
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
861
|
+
const tag = document.createElement("style");
|
|
862
|
+
tag.dataset.plugin = "dsh-skill-hub";
|
|
863
|
+
tag.dataset.pluginCss = tagId$1;
|
|
864
|
+
tag.textContent = css$1;
|
|
865
|
+
document.head.appendChild(tag);
|
|
866
|
+
}
|
|
867
|
+
var settings_card_module_css_default = {
|
|
868
|
+
"badge": "TqD_hG_badge",
|
|
869
|
+
"badges": "TqD_hG_badges",
|
|
870
|
+
"body": "TqD_hG_body",
|
|
871
|
+
"card": "TqD_hG_card",
|
|
872
|
+
"cardOpen": "TqD_hG_cardOpen",
|
|
873
|
+
"chevron": "TqD_hG_chevron",
|
|
874
|
+
"chevronOpen": "TqD_hG_chevronOpen",
|
|
875
|
+
"colorInput": "TqD_hG_colorInput",
|
|
876
|
+
"colorRow": "TqD_hG_colorRow",
|
|
877
|
+
"colorText": "TqD_hG_colorText",
|
|
878
|
+
"description": "TqD_hG_description",
|
|
879
|
+
"discard": "TqD_hG_discard",
|
|
880
|
+
"failed": "TqD_hG_failed",
|
|
881
|
+
"field": "TqD_hG_field",
|
|
882
|
+
"footer": "TqD_hG_footer",
|
|
883
|
+
"head": "TqD_hG_head",
|
|
884
|
+
"headText": "TqD_hG_headText",
|
|
885
|
+
"header": "TqD_hG_header",
|
|
886
|
+
"hint": "TqD_hG_hint",
|
|
887
|
+
"input": "TqD_hG_input",
|
|
888
|
+
"label": "TqD_hG_label",
|
|
889
|
+
"name": "TqD_hG_name",
|
|
890
|
+
"notExposed": "TqD_hG_notExposed",
|
|
891
|
+
"pending": "TqD_hG_pending",
|
|
892
|
+
"readOnly": "TqD_hG_readOnly",
|
|
893
|
+
"reset": "TqD_hG_reset",
|
|
894
|
+
"save": "TqD_hG_save",
|
|
895
|
+
"switch": "TqD_hG_switch",
|
|
896
|
+
"switchOn": "TqD_hG_switchOn",
|
|
897
|
+
"switchRow": "TqD_hG_switchRow",
|
|
898
|
+
"switchText": "TqD_hG_switchText",
|
|
899
|
+
"switchThumb": "TqD_hG_switchThumb"
|
|
900
|
+
};
|
|
901
|
+
//#endregion
|
|
902
|
+
//#region src/client/settings-card.tsx
|
|
903
|
+
/**
|
|
904
|
+
* Shared chrome for the plugin settings card, aligned with the official
|
|
905
|
+
* dsh-client-ui-settings-plugins PluginCard design language: the same tokens,
|
|
906
|
+
* disclosure header, field rhythm, and copy. Renders nothing while the
|
|
907
|
+
* namespace is unavailable — a deployment that does not compose the owning
|
|
908
|
+
* plugin should show no trace of it.
|
|
909
|
+
*/
|
|
910
|
+
/**
|
|
911
|
+
* Render one plugin settings card.
|
|
912
|
+
* @param props - the plugin's copy keys, its form state, and its controls.
|
|
913
|
+
* @returns the card, or nothing while the namespace is still loading.
|
|
914
|
+
*/
|
|
915
|
+
function PluginSettingsCard(props) {
|
|
916
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
917
|
+
const { state } = props;
|
|
918
|
+
if (!state.available) return null;
|
|
919
|
+
const title = props.t(props.titleKey);
|
|
920
|
+
const blocked = !state.dirty || state.invalid || state.saving;
|
|
921
|
+
const header = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
922
|
+
type: "button",
|
|
923
|
+
className: settings_card_module_css_default.header,
|
|
924
|
+
"aria-expanded": open,
|
|
925
|
+
"aria-label": props.t(open ? "settings.collapse" : "settings.expand") + ": " + title,
|
|
926
|
+
onClick: () => {
|
|
927
|
+
setOpen(!open);
|
|
928
|
+
},
|
|
929
|
+
children: [
|
|
930
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
931
|
+
className: settings_card_module_css_default.headText,
|
|
932
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
933
|
+
className: settings_card_module_css_default.name,
|
|
934
|
+
children: title
|
|
935
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
936
|
+
className: settings_card_module_css_default.description,
|
|
937
|
+
children: props.t(props.descriptionKey)
|
|
938
|
+
})]
|
|
939
|
+
}),
|
|
940
|
+
state.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
941
|
+
className: settings_card_module_css_default.pending,
|
|
942
|
+
children: props.t("settings.unsaved")
|
|
943
|
+
}) : null,
|
|
944
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: open ? settings_card_module_css_default.chevronOpen : settings_card_module_css_default.chevron })
|
|
945
|
+
]
|
|
946
|
+
});
|
|
947
|
+
if (!state.exposed) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
948
|
+
className: settings_card_module_css_default.card + (open ? " " + settings_card_module_css_default.cardOpen : ""),
|
|
949
|
+
children: [header, open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
950
|
+
className: settings_card_module_css_default.body,
|
|
951
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
952
|
+
className: settings_card_module_css_default.notExposed,
|
|
953
|
+
role: "status",
|
|
954
|
+
children: props.t("settings.notExposed")
|
|
955
|
+
})
|
|
956
|
+
}) : null]
|
|
957
|
+
});
|
|
958
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
959
|
+
className: settings_card_module_css_default.card + (open ? " " + settings_card_module_css_default.cardOpen : ""),
|
|
960
|
+
children: [header, open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
961
|
+
className: settings_card_module_css_default.body,
|
|
962
|
+
children: [
|
|
963
|
+
!state.writable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
964
|
+
className: settings_card_module_css_default.readOnly,
|
|
965
|
+
role: "status",
|
|
966
|
+
children: props.t("settings.readOnly")
|
|
967
|
+
}) : null,
|
|
968
|
+
props.children,
|
|
969
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
970
|
+
className: settings_card_module_css_default.footer,
|
|
971
|
+
children: [
|
|
972
|
+
state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
973
|
+
className: settings_card_module_css_default.failed,
|
|
974
|
+
role: "status",
|
|
975
|
+
children: props.t("settings.saveFailed")
|
|
976
|
+
}) : null,
|
|
977
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
978
|
+
type: "button",
|
|
979
|
+
className: settings_card_module_css_default.discard,
|
|
980
|
+
disabled: !state.dirty || state.saving,
|
|
981
|
+
onClick: props.onDiscard,
|
|
982
|
+
children: props.t("settings.discard")
|
|
983
|
+
}),
|
|
984
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
985
|
+
type: "button",
|
|
986
|
+
className: settings_card_module_css_default.save,
|
|
987
|
+
disabled: blocked,
|
|
988
|
+
onClick: props.onSave,
|
|
989
|
+
children: props.t(!state.saving ? "settings.save" : "settings.saving")
|
|
990
|
+
})
|
|
991
|
+
]
|
|
992
|
+
})
|
|
993
|
+
]
|
|
994
|
+
}) : null]
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
function SwitchField(props) {
|
|
998
|
+
const checked = props.text !== "false";
|
|
999
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1000
|
+
className: settings_card_module_css_default.field,
|
|
1001
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1002
|
+
className: settings_card_module_css_default.switchRow,
|
|
1003
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1004
|
+
className: settings_card_module_css_default.switchText,
|
|
1005
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1006
|
+
className: settings_card_module_css_default.head,
|
|
1007
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1008
|
+
className: settings_card_module_css_default.label,
|
|
1009
|
+
children: props.label
|
|
1010
|
+
}), props.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1011
|
+
className: settings_card_module_css_default.badges,
|
|
1012
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1013
|
+
className: settings_card_module_css_default.badge,
|
|
1014
|
+
children: props.overriddenLabel
|
|
1015
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1016
|
+
type: "button",
|
|
1017
|
+
className: settings_card_module_css_default.reset,
|
|
1018
|
+
disabled: props.disabled,
|
|
1019
|
+
onClick: props.onReset,
|
|
1020
|
+
children: props.resetLabel
|
|
1021
|
+
})]
|
|
1022
|
+
}) : null]
|
|
1023
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1024
|
+
className: settings_card_module_css_default.hint,
|
|
1025
|
+
children: props.hint
|
|
1026
|
+
})]
|
|
1027
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1028
|
+
type: "button",
|
|
1029
|
+
className: checked ? settings_card_module_css_default.switchOn : settings_card_module_css_default.switch,
|
|
1030
|
+
role: "switch",
|
|
1031
|
+
"aria-checked": checked,
|
|
1032
|
+
"aria-label": props.label,
|
|
1033
|
+
disabled: props.disabled,
|
|
1034
|
+
onClick: () => {
|
|
1035
|
+
props.onEdit(String(!checked));
|
|
1036
|
+
},
|
|
1037
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: settings_card_module_css_default.switchThumb })
|
|
1038
|
+
})]
|
|
1039
|
+
})
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
function ColorField(props) {
|
|
1043
|
+
const value = /^#[0-9a-f]{6}$/i.test(props.text) ? props.text : props.defaultColor;
|
|
1044
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1045
|
+
className: settings_card_module_css_default.field,
|
|
1046
|
+
children: [
|
|
1047
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1048
|
+
className: settings_card_module_css_default.head,
|
|
1049
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1050
|
+
className: settings_card_module_css_default.label,
|
|
1051
|
+
htmlFor: props.id,
|
|
1052
|
+
children: props.label
|
|
1053
|
+
}), props.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1054
|
+
className: settings_card_module_css_default.badges,
|
|
1055
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1056
|
+
className: settings_card_module_css_default.badge,
|
|
1057
|
+
children: props.overriddenLabel
|
|
1058
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1059
|
+
type: "button",
|
|
1060
|
+
className: settings_card_module_css_default.reset,
|
|
1061
|
+
disabled: props.disabled,
|
|
1062
|
+
onClick: props.onReset,
|
|
1063
|
+
children: props.resetLabel
|
|
1064
|
+
})]
|
|
1065
|
+
}) : null]
|
|
1066
|
+
}),
|
|
1067
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1068
|
+
className: settings_card_module_css_default.colorRow,
|
|
1069
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1070
|
+
id: props.id,
|
|
1071
|
+
type: "color",
|
|
1072
|
+
className: settings_card_module_css_default.colorInput,
|
|
1073
|
+
value,
|
|
1074
|
+
disabled: props.disabled,
|
|
1075
|
+
onChange: (event) => {
|
|
1076
|
+
props.onEdit(event.target.value);
|
|
1077
|
+
}
|
|
1078
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1079
|
+
type: "text",
|
|
1080
|
+
className: settings_card_module_css_default.input + " " + settings_card_module_css_default.colorText,
|
|
1081
|
+
value: props.text,
|
|
1082
|
+
disabled: props.disabled,
|
|
1083
|
+
placeholder: props.inheritLabel,
|
|
1084
|
+
onChange: (event) => {
|
|
1085
|
+
props.onEdit(event.target.value.trim());
|
|
1086
|
+
}
|
|
1087
|
+
})]
|
|
1088
|
+
}),
|
|
1089
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1090
|
+
className: settings_card_module_css_default.hint,
|
|
1091
|
+
children: props.hint
|
|
1092
|
+
})
|
|
1093
|
+
]
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
//#endregion
|
|
1097
|
+
//#region src/client/settings-form.ts
|
|
1098
|
+
/**
|
|
1099
|
+
* Settings-card form machinery, vendored from the dsh-web-ui family bucket
|
|
1100
|
+
* (packages/dsh-task-board/src/client/settings-form.ts) and re-typed. Stages
|
|
1101
|
+
* one card's edits over one settings namespace and writes them on save.
|
|
1102
|
+
*
|
|
1103
|
+
* The Host is the only authority on whether a value was accepted — its
|
|
1104
|
+
* validators own the constraints no schema can express — so the outcome is
|
|
1105
|
+
* read back from the section rather than predicted here. A save that did not
|
|
1106
|
+
* land keeps its drafts, so the user can correct them instead of retyping.
|
|
1107
|
+
*/
|
|
1108
|
+
/** A boolean field, edited through true/false draft text. */
|
|
1109
|
+
function booleanField(field) {
|
|
1110
|
+
return {
|
|
1111
|
+
field,
|
|
1112
|
+
format: (value) => typeof value === "boolean" ? String(value) : "",
|
|
1113
|
+
parse: (text) => {
|
|
1114
|
+
if (text === "true") return {
|
|
1115
|
+
kind: "set",
|
|
1116
|
+
value: true
|
|
1117
|
+
};
|
|
1118
|
+
if (text === "false") return {
|
|
1119
|
+
kind: "set",
|
|
1120
|
+
value: false
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
/** A #rrggbb color field, edited through hex draft text. */
|
|
1126
|
+
function colorField(field) {
|
|
1127
|
+
return {
|
|
1128
|
+
field,
|
|
1129
|
+
format: (value) => typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value) ? value : "",
|
|
1130
|
+
parse: (text) => {
|
|
1131
|
+
if (/^#[0-9a-f]{6}$/i.test(text)) return {
|
|
1132
|
+
kind: "set",
|
|
1133
|
+
value: text
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
/** Stages one card's edits and writes them through the settings transport. */
|
|
1139
|
+
var CardForm = class {
|
|
1140
|
+
scope;
|
|
1141
|
+
specs = /* @__PURE__ */ new Map();
|
|
1142
|
+
staged = /* @__PURE__ */ new Map();
|
|
1143
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1144
|
+
saving = false;
|
|
1145
|
+
failed = false;
|
|
1146
|
+
/** @param scope - the bound settings scope for this card's namespace. */
|
|
1147
|
+
constructor(scope, specs) {
|
|
1148
|
+
this.scope = scope;
|
|
1149
|
+
for (const spec of specs) this.specs.set(spec.field, spec);
|
|
1150
|
+
scope.subscribe(() => {
|
|
1151
|
+
this.publish();
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
/** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
|
|
1155
|
+
bind(project) {
|
|
1156
|
+
const store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(project());
|
|
1157
|
+
this.listeners.add(() => {
|
|
1158
|
+
store.set(project());
|
|
1159
|
+
});
|
|
1160
|
+
return store;
|
|
1161
|
+
}
|
|
1162
|
+
/** Read the card-level state: what the Host serves, and what a save would do. */
|
|
1163
|
+
shell() {
|
|
1164
|
+
const snapshot = this.scope.getSnapshot();
|
|
1165
|
+
const plan = this.plan();
|
|
1166
|
+
return {
|
|
1167
|
+
available: snapshot.status !== "loading",
|
|
1168
|
+
exposed: snapshot.status === "ready",
|
|
1169
|
+
writable: snapshot.writable,
|
|
1170
|
+
dirty: plan.length > 0,
|
|
1171
|
+
invalid: plan.some((item) => item.run === void 0),
|
|
1172
|
+
saving: this.saving,
|
|
1173
|
+
failed: this.failed
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
/** Read one field's state from the effective section and its staged draft. */
|
|
1177
|
+
field(field) {
|
|
1178
|
+
const spec = this.specOf(field);
|
|
1179
|
+
const staged = this.staged.get(field);
|
|
1180
|
+
if (staged === void 0) return {
|
|
1181
|
+
text: spec.format(this.sectionValue(field)),
|
|
1182
|
+
overridden: this.stored(field),
|
|
1183
|
+
invalid: false
|
|
1184
|
+
};
|
|
1185
|
+
const write = staged.clear ? { kind: "clear" } : spec.parse(staged.text);
|
|
1186
|
+
return {
|
|
1187
|
+
text: staged.text,
|
|
1188
|
+
overridden: write?.kind === "set",
|
|
1189
|
+
invalid: write === void 0
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
/** The actions the card's slot registration injects. */
|
|
1193
|
+
actions() {
|
|
1194
|
+
return {
|
|
1195
|
+
edit: (field, text) => {
|
|
1196
|
+
this.stage(field, {
|
|
1197
|
+
text,
|
|
1198
|
+
clear: false
|
|
1199
|
+
});
|
|
1200
|
+
},
|
|
1201
|
+
resetField: (field) => {
|
|
1202
|
+
this.stage(field, {
|
|
1203
|
+
text: this.specOf(field).format(this.baseValue(field)),
|
|
1204
|
+
clear: true
|
|
1205
|
+
});
|
|
1206
|
+
},
|
|
1207
|
+
save: () => {
|
|
1208
|
+
this.save();
|
|
1209
|
+
},
|
|
1210
|
+
discard: () => {
|
|
1211
|
+
if (this.staged.size === 0 && !this.failed) return;
|
|
1212
|
+
this.staged.clear();
|
|
1213
|
+
this.failed = false;
|
|
1214
|
+
this.publish();
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
/** Write every staged edit, then re-seed from what the Host accepted. */
|
|
1219
|
+
async save() {
|
|
1220
|
+
const plan = this.plan();
|
|
1221
|
+
const writes = plan.flatMap((item) => item.run === void 0 ? [] : [item.run]);
|
|
1222
|
+
if (plan.length === 0 || this.saving || writes.length !== plan.length) return;
|
|
1223
|
+
this.saving = true;
|
|
1224
|
+
this.failed = false;
|
|
1225
|
+
this.publish();
|
|
1226
|
+
let landed = true;
|
|
1227
|
+
for (const write of writes) landed = await write() && landed;
|
|
1228
|
+
if (landed) this.staged.clear();
|
|
1229
|
+
this.saving = false;
|
|
1230
|
+
this.failed = !landed;
|
|
1231
|
+
this.publish();
|
|
1232
|
+
}
|
|
1233
|
+
/** Every staged edit a save would write, in staging order. */
|
|
1234
|
+
plan() {
|
|
1235
|
+
const plan = [];
|
|
1236
|
+
for (const [field, staged] of this.staged) {
|
|
1237
|
+
const spec = this.specOf(field);
|
|
1238
|
+
if (staged.clear) {
|
|
1239
|
+
if (this.stored(field)) plan.push({
|
|
1240
|
+
field,
|
|
1241
|
+
run: () => this.clear(field)
|
|
1242
|
+
});
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
if (staged.text === spec.format(this.sectionValue(field))) continue;
|
|
1246
|
+
const write = spec.parse(staged.text);
|
|
1247
|
+
if (write === void 0) plan.push({
|
|
1248
|
+
field,
|
|
1249
|
+
run: void 0
|
|
1250
|
+
});
|
|
1251
|
+
else if (write.kind === "clear") plan.push({
|
|
1252
|
+
field,
|
|
1253
|
+
run: () => this.clear(field)
|
|
1254
|
+
});
|
|
1255
|
+
else plan.push({
|
|
1256
|
+
field,
|
|
1257
|
+
run: () => this.store(field, write.value)
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
return plan;
|
|
1261
|
+
}
|
|
1262
|
+
async clear(field) {
|
|
1263
|
+
await this.scope.unset(field);
|
|
1264
|
+
return !this.stored(field);
|
|
1265
|
+
}
|
|
1266
|
+
async store(field, value) {
|
|
1267
|
+
await this.scope.set(field, value);
|
|
1268
|
+
return this.userLayer()?.[field] === value;
|
|
1269
|
+
}
|
|
1270
|
+
userRecord() {
|
|
1271
|
+
const user = this.scope.getSnapshot().user;
|
|
1272
|
+
return typeof user === "object" && user !== null ? user : void 0;
|
|
1273
|
+
}
|
|
1274
|
+
stage(field, edit) {
|
|
1275
|
+
this.staged.set(field, edit);
|
|
1276
|
+
this.failed = false;
|
|
1277
|
+
this.publish();
|
|
1278
|
+
}
|
|
1279
|
+
specOf(field) {
|
|
1280
|
+
const spec = this.specs.get(field);
|
|
1281
|
+
if (spec === void 0) throw new Error("settings card has no field " + field);
|
|
1282
|
+
return spec;
|
|
1283
|
+
}
|
|
1284
|
+
sectionValue(field) {
|
|
1285
|
+
return this.scope.getSnapshot().value?.[field];
|
|
1286
|
+
}
|
|
1287
|
+
baseValue(field) {
|
|
1288
|
+
const base = this.scope.getSnapshot().base;
|
|
1289
|
+
return typeof base === "object" && base !== null ? base[field] : void 0;
|
|
1290
|
+
}
|
|
1291
|
+
userLayer() {
|
|
1292
|
+
return this.userRecord();
|
|
1293
|
+
}
|
|
1294
|
+
stored(field) {
|
|
1295
|
+
const user = this.userRecord();
|
|
1296
|
+
return user !== void 0 && Object.hasOwn(user, field);
|
|
1297
|
+
}
|
|
1298
|
+
publish() {
|
|
1299
|
+
for (const listener of this.listeners) listener();
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
//#endregion
|
|
1303
|
+
//#region src/client/SkillHubSettingsCard.tsx
|
|
1304
|
+
/** Bridges the hub's config scope onto the card's staged form. */
|
|
1305
|
+
var SkillHubSettingsCardController = class {
|
|
1306
|
+
form;
|
|
1307
|
+
store;
|
|
1308
|
+
/** @param scope - the hub config scope the card edits (ApiConfigScope). */
|
|
1309
|
+
constructor(scope) {
|
|
1310
|
+
this.form = new CardForm(scope, [
|
|
1311
|
+
booleanField("enabled"),
|
|
1312
|
+
booleanField("announceToAgent"),
|
|
1313
|
+
colorField("dotModelColor"),
|
|
1314
|
+
colorField("dotUserColor"),
|
|
1315
|
+
booleanField("showUseCount"),
|
|
1316
|
+
booleanField("showUseTime"),
|
|
1317
|
+
booleanField("showGroupSummary")
|
|
1318
|
+
]);
|
|
1319
|
+
this.store = this.form.bind(() => this.projection());
|
|
1320
|
+
}
|
|
1321
|
+
projection() {
|
|
1322
|
+
return {
|
|
1323
|
+
...this.form.shell(),
|
|
1324
|
+
enabled: this.form.field("enabled"),
|
|
1325
|
+
announceToAgent: this.form.field("announceToAgent"),
|
|
1326
|
+
dotModelColor: this.form.field("dotModelColor"),
|
|
1327
|
+
dotUserColor: this.form.field("dotUserColor"),
|
|
1328
|
+
showUseCount: this.form.field("showUseCount"),
|
|
1329
|
+
showUseTime: this.form.field("showUseTime"),
|
|
1330
|
+
showGroupSummary: this.form.field("showGroupSummary")
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Build the face the card's slot registration injects.
|
|
1335
|
+
* @returns the card's snapshot hook and its form actions.
|
|
1336
|
+
*/
|
|
1337
|
+
inject() {
|
|
1338
|
+
return {
|
|
1339
|
+
hooks: { skillHubSettingsCard: this.store },
|
|
1340
|
+
...this.form.actions()
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
/**
|
|
1345
|
+
* Render the dsh-skill-hub card.
|
|
1346
|
+
* @param props - locale copy, the card snapshot, and its form actions.
|
|
1347
|
+
* @returns the card.
|
|
1348
|
+
*/
|
|
1349
|
+
function SkillHubSettingsCard(props) {
|
|
1350
|
+
const { t } = props;
|
|
1351
|
+
const state = props.useSkillHubSettingsCard((snapshot) => snapshot);
|
|
1352
|
+
const disabled = !state.writable;
|
|
1353
|
+
const fieldProps = {
|
|
1354
|
+
overriddenLabel: t("settings.overridden"),
|
|
1355
|
+
resetLabel: t("settings.reset"),
|
|
1356
|
+
disabled
|
|
1357
|
+
};
|
|
1358
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(PluginSettingsCard, {
|
|
1359
|
+
t,
|
|
1360
|
+
titleKey: "settings.title",
|
|
1361
|
+
descriptionKey: "settings.description",
|
|
1362
|
+
state,
|
|
1363
|
+
onSave: props.save,
|
|
1364
|
+
onDiscard: props.discard,
|
|
1365
|
+
children: [
|
|
1366
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchField, {
|
|
1367
|
+
label: t("settings.enabled"),
|
|
1368
|
+
hint: t("settings.enabledHint"),
|
|
1369
|
+
...fieldProps,
|
|
1370
|
+
...state.enabled,
|
|
1371
|
+
onEdit: (text) => {
|
|
1372
|
+
props.edit("enabled", text);
|
|
1373
|
+
},
|
|
1374
|
+
onReset: () => {
|
|
1375
|
+
props.resetField("enabled");
|
|
1376
|
+
}
|
|
1377
|
+
}),
|
|
1378
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchField, {
|
|
1379
|
+
label: t("settings.announceToAgent"),
|
|
1380
|
+
hint: t("settings.announceToAgentHint"),
|
|
1381
|
+
...fieldProps,
|
|
1382
|
+
...state.announceToAgent,
|
|
1383
|
+
onEdit: (text) => {
|
|
1384
|
+
props.edit("announceToAgent", text);
|
|
1385
|
+
},
|
|
1386
|
+
onReset: () => {
|
|
1387
|
+
props.resetField("announceToAgent");
|
|
1388
|
+
}
|
|
1389
|
+
}),
|
|
1390
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorField, {
|
|
1391
|
+
id: "skill-hub-dot-model-color",
|
|
1392
|
+
label: t("settings.dotModelColor"),
|
|
1393
|
+
hint: t("settings.dotModelColorHint"),
|
|
1394
|
+
inheritLabel: t("settings.inherit"),
|
|
1395
|
+
defaultColor: DEFAULT_DOT_MODEL_COLOR,
|
|
1396
|
+
...fieldProps,
|
|
1397
|
+
...state.dotModelColor,
|
|
1398
|
+
onEdit: (text) => {
|
|
1399
|
+
props.edit("dotModelColor", text);
|
|
1400
|
+
},
|
|
1401
|
+
onReset: () => {
|
|
1402
|
+
props.resetField("dotModelColor");
|
|
1403
|
+
}
|
|
1404
|
+
}),
|
|
1405
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColorField, {
|
|
1406
|
+
id: "skill-hub-dot-user-color",
|
|
1407
|
+
label: t("settings.dotUserColor"),
|
|
1408
|
+
hint: t("settings.dotUserColorHint"),
|
|
1409
|
+
inheritLabel: t("settings.inherit"),
|
|
1410
|
+
defaultColor: DEFAULT_DOT_USER_COLOR,
|
|
1411
|
+
...fieldProps,
|
|
1412
|
+
...state.dotUserColor,
|
|
1413
|
+
onEdit: (text) => {
|
|
1414
|
+
props.edit("dotUserColor", text);
|
|
1415
|
+
},
|
|
1416
|
+
onReset: () => {
|
|
1417
|
+
props.resetField("dotUserColor");
|
|
1418
|
+
}
|
|
1419
|
+
}),
|
|
1420
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchField, {
|
|
1421
|
+
label: t("settings.showUseCount"),
|
|
1422
|
+
hint: t("settings.showUseCountHint"),
|
|
1423
|
+
...fieldProps,
|
|
1424
|
+
...state.showUseCount,
|
|
1425
|
+
onEdit: (text) => {
|
|
1426
|
+
props.edit("showUseCount", text);
|
|
1427
|
+
},
|
|
1428
|
+
onReset: () => {
|
|
1429
|
+
props.resetField("showUseCount");
|
|
1430
|
+
}
|
|
1431
|
+
}),
|
|
1432
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchField, {
|
|
1433
|
+
label: t("settings.showUseTime"),
|
|
1434
|
+
hint: t("settings.showUseTimeHint"),
|
|
1435
|
+
...fieldProps,
|
|
1436
|
+
...state.showUseTime,
|
|
1437
|
+
onEdit: (text) => {
|
|
1438
|
+
props.edit("showUseTime", text);
|
|
1439
|
+
},
|
|
1440
|
+
onReset: () => {
|
|
1441
|
+
props.resetField("showUseTime");
|
|
1442
|
+
}
|
|
1443
|
+
}),
|
|
1444
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchField, {
|
|
1445
|
+
label: t("settings.showGroupSummary"),
|
|
1446
|
+
hint: t("settings.showGroupSummaryHint"),
|
|
1447
|
+
...fieldProps,
|
|
1448
|
+
...state.showGroupSummary,
|
|
1449
|
+
onEdit: (text) => {
|
|
1450
|
+
props.edit("showGroupSummary", text);
|
|
1451
|
+
},
|
|
1452
|
+
onReset: () => {
|
|
1453
|
+
props.resetField("showGroupSummary");
|
|
1454
|
+
}
|
|
1455
|
+
})
|
|
1456
|
+
]
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
//#endregion
|
|
1460
|
+
//#region src/client/helpers.ts
|
|
1461
|
+
/**
|
|
1462
|
+
* Shared panel helpers: the active-dictionary pick (document-language
|
|
1463
|
+
* based, family precedent) plus a small error-message extractor.
|
|
1464
|
+
*/
|
|
1465
|
+
/** Active dictionary, picked by the document language at call time. */
|
|
1466
|
+
function dictionary() {
|
|
1467
|
+
return (typeof document !== "undefined" ? document.documentElement.lang : "zh").toLowerCase().startsWith("en") ? { ...en } : { ...zh };
|
|
1468
|
+
}
|
|
1469
|
+
/** Translate a key with optional {name} template params (current language). */
|
|
1470
|
+
function tt(key, values) {
|
|
1471
|
+
return t(dictionary(), key, values);
|
|
1472
|
+
}
|
|
1473
|
+
/** Human-readable error text from an unknown thrown value. */
|
|
1474
|
+
function errorMessage(error) {
|
|
1475
|
+
if (error instanceof Error) return error.message;
|
|
1476
|
+
return String(error);
|
|
1477
|
+
}
|
|
1478
|
+
//#endregion
|
|
1479
|
+
//#region src/client/grouping.ts
|
|
1480
|
+
/**
|
|
1481
|
+
* Derive a group switch view from its member names and the set of currently
|
|
1482
|
+
* enabled skill names (catalog.skills). Members not in the enabled set count
|
|
1483
|
+
* as disabled (catalog.disabled or absent read-only rows).
|
|
1484
|
+
*/
|
|
1485
|
+
function groupSwitchView(members, enabledNames) {
|
|
1486
|
+
const enabled = [];
|
|
1487
|
+
const disabled = [];
|
|
1488
|
+
for (const name of members) if (enabledNames.has(name)) enabled.push(name);
|
|
1489
|
+
else disabled.push(name);
|
|
1490
|
+
return {
|
|
1491
|
+
state: disabled.length === 0 ? "on" : enabled.length === 0 ? "off" : "mixed",
|
|
1492
|
+
enabled,
|
|
1493
|
+
disabled
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
/** Names of every group a skill belongs to (tags + collections). */
|
|
1497
|
+
function groupNamesOf(name, tags, collections) {
|
|
1498
|
+
const names = [];
|
|
1499
|
+
for (const tag of tags) if (tag.skillNames.includes(name)) names.push(tag.name);
|
|
1500
|
+
for (const collection of collections) if (collection.skillNames.includes(name)) names.push(collection.name);
|
|
1501
|
+
return names;
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Members of a group that are currently enabled AND also belong to at least
|
|
1505
|
+
* one other group — the set a "close" action must ask about.
|
|
1506
|
+
*/
|
|
1507
|
+
function conflictsOnClose(members, enabledNames, otherGroups) {
|
|
1508
|
+
return members.filter((name) => {
|
|
1509
|
+
if (!enabledNames.has(name)) return false;
|
|
1510
|
+
return otherGroups.some((group) => group.members.includes(name));
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
/** Origin-repo filter value: skills with no source record (private skills). */
|
|
1514
|
+
const PRIVATE_SOURCE = "private";
|
|
1515
|
+
/**
|
|
1516
|
+
* Apply the origin filter ('all' or a specific origin repo; skills without a
|
|
1517
|
+
* source record count as PRIVATE_SOURCE). The origins map is the store's
|
|
1518
|
+
* skillName → repo derivation, so filtering follows the tracked source
|
|
1519
|
+
* records instead of the filesystem root a skill happens to live under.
|
|
1520
|
+
*/
|
|
1521
|
+
function filterBySource(skills, source, origins) {
|
|
1522
|
+
if (source === "all") return [...skills];
|
|
1523
|
+
return skills.filter((skill) => (origins[skill.name] ?? "private") === source);
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Sort a skill list in place-safe copy order: name ascending, added
|
|
1527
|
+
* descending (newest first, unknown addedAt last), or uses descending
|
|
1528
|
+
* (most-called first). Unknown values always trail.
|
|
1529
|
+
*/
|
|
1530
|
+
function sortSkills(skills, key, getUses) {
|
|
1531
|
+
const list = [...skills];
|
|
1532
|
+
if (key === "name") list.sort((a, b) => a.name.localeCompare(b.name));
|
|
1533
|
+
else if (key === "added") list.sort((a, b) => (b.addedAt ?? -Infinity) - (a.addedAt ?? -Infinity));
|
|
1534
|
+
else if (key === "uses") list.sort((a, b) => (getUses?.(b.name) ?? 0) - (getUses?.(a.name) ?? 0));
|
|
1535
|
+
return list;
|
|
1536
|
+
}
|
|
1537
|
+
/** Format an epoch-ms timestamp as a short relative time bucket. */
|
|
1538
|
+
function formatRelativeTime(ms, now = Date.now()) {
|
|
1539
|
+
const diff = Math.max(0, now - ms);
|
|
1540
|
+
const minutes = Math.floor(diff / 6e4);
|
|
1541
|
+
if (minutes < 1) return { key: "time.justNow" };
|
|
1542
|
+
if (minutes < 60) return {
|
|
1543
|
+
key: "time.minutesAgo",
|
|
1544
|
+
value: minutes
|
|
1545
|
+
};
|
|
1546
|
+
const hours = Math.floor(minutes / 60);
|
|
1547
|
+
if (hours < 24) return {
|
|
1548
|
+
key: "time.hoursAgo",
|
|
1549
|
+
value: hours
|
|
1550
|
+
};
|
|
1551
|
+
const days = Math.floor(hours / 24);
|
|
1552
|
+
if (days < 7) return {
|
|
1553
|
+
key: "time.daysAgo",
|
|
1554
|
+
value: days
|
|
1555
|
+
};
|
|
1556
|
+
return {
|
|
1557
|
+
key: "time.weeksAgo",
|
|
1558
|
+
value: Math.floor(days / 7)
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
//#endregion
|
|
1562
|
+
//#region src/client/panel/format.ts
|
|
1563
|
+
/** Dot inline style from the user-chosen color (undefined keeps the CSS default). */
|
|
1564
|
+
function dotStyle(color) {
|
|
1565
|
+
if (color === void 0) return void 0;
|
|
1566
|
+
return {
|
|
1567
|
+
background: color,
|
|
1568
|
+
borderColor: color
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
/** Localized relative-time text for a Unix-ms timestamp. */
|
|
1572
|
+
function relativeTimeText(ms) {
|
|
1573
|
+
const rt = formatRelativeTime(ms);
|
|
1574
|
+
return tt(rt.key, rt.value !== void 0 ? { value: rt.value } : void 0);
|
|
1575
|
+
}
|
|
1576
|
+
/** Localized absolute time for the detail meta (e.g. "2026/8/16 11:00:00"). */
|
|
1577
|
+
function formatDateTime(ms) {
|
|
1578
|
+
return new Date(ms).toLocaleString();
|
|
1579
|
+
}
|
|
1580
|
+
/** Short form of a commit SHA for display. */
|
|
1581
|
+
function shortSha(sha) {
|
|
1582
|
+
return sha.length > 7 ? sha.slice(0, 7) : sha;
|
|
1583
|
+
}
|
|
1584
|
+
//#endregion
|
|
1585
|
+
//#region \0dsh-css:/Users/huanyi/Documents/personal/tools/dsh-skill-hub/src/client/panel/panel.module.css.mjs
|
|
1586
|
+
const css = "._6VtqdG_panel{-webkit-font-smoothing:antialiased;color-scheme:light dark;--hub-model:#2f81f7;--hub-user:#3fb950;flex-direction:column;gap:14px;max-width:720px;margin:0 auto;padding:28px 20px 44px;font-family:-apple-system,BlinkMacSystemFont,SF Pro Text,Helvetica Neue,sans-serif;display:flex}._6VtqdG_header{flex-wrap:wrap;align-items:baseline;gap:10px;display:flex}._6VtqdG_title{letter-spacing:-.02em;margin:0;font-size:24px;font-weight:700}._6VtqdG_hint{opacity:.5;font-size:12px}._6VtqdG_actions{gap:8px;margin-left:auto;display:flex}._6VtqdG_segmented{background:#80808024;border-radius:9px;gap:2px;padding:2px;display:flex}._6VtqdG_segBtn{color:inherit;opacity:.62;cursor:pointer;background:0 0;border:none;border-radius:7px;padding:5px 13px;font-size:12px;transition:background .15s,opacity .15s}._6VtqdG_segBtn:hover{opacity:.9}._6VtqdG_segBtnActive{opacity:1;background:#80808047;font-weight:600}._6VtqdG_search{width:100%;color:inherit;background:#8080801f;border:none;border-radius:10px;outline:none;padding:10px 14px;font-size:13px;transition:background .15s}._6VtqdG_search::placeholder{opacity:.45}._6VtqdG_search:focus{background:#8080802e}._6VtqdG_section{background:#8080800f;border-radius:12px;flex-direction:column;margin-top:4px;display:flex;overflow:hidden;box-shadow:0 1px 2px #00000008,0 4px 12px #0000000a}._6VtqdG_sectionTitle{margin:14px 16px 2px;font-size:13px;font-weight:700}._6VtqdG_groupHead{align-items:center;gap:8px;padding:12px 14px 4px;display:flex}._6VtqdG_groupTitle{opacity:.92;flex:1;min-width:0;font-size:13px;font-weight:600}._6VtqdG_groupOps{flex:none;gap:6px;display:inline-flex}._6VtqdG_opBtn{color:inherit;cursor:pointer;background:#80808021;border:none;border-radius:999px;padding:3px 11px;font-size:11px;transition:background .15s}._6VtqdG_opBtn:hover{background:#80808038}._6VtqdG_opBtn:disabled{opacity:.4;cursor:default}._6VtqdG_row{cursor:pointer;background:0 0;align-items:center;gap:12px;padding:10px 14px;transition:background .12s;display:flex}._6VtqdG_row:hover{background:#80808014}._6VtqdG_rowStatic{cursor:default}._6VtqdG_rowStatic:hover{background:0 0}._6VtqdG_row+._6VtqdG_row{border-top:.5px solid #80808021}._6VtqdG_rowMain{flex:1;min-width:0}._6VtqdG_rowName{align-items:center;gap:5px;font-size:13.5px;display:flex}._6VtqdG_rowNameText{font-weight:600}._6VtqdG_rowDesc{opacity:.52;white-space:nowrap;text-overflow:ellipsis;margin-top:1px;font-size:12.5px;overflow:hidden}._6VtqdG_rowMeta{opacity:.45;white-space:nowrap;flex:none;font-size:11.5px}._6VtqdG_badges{flex:none;align-items:center;gap:6px;display:flex}._6VtqdG_badge{opacity:.8;white-space:nowrap;border:.5px solid #8080804d;border-radius:999px;padding:1px 7px;font-size:10.5px}._6VtqdG_badgeModel{color:var(--hub-model);border-color:currentColor}._6VtqdG_badgeUser{color:var(--hub-user);border-color:currentColor}._6VtqdG_badgeUses{color:#d29922;border-color:currentColor}._6VtqdG_badgeReadonly{opacity:.5}._6VtqdG_switch{cursor:pointer;background:#8080804d;border:none;border-radius:999px;flex:none;width:36px;height:21px;padding:2px;transition:background .18s}._6VtqdG_switch:disabled{opacity:.5;cursor:default}._6VtqdG_switchOn{background:#34c759}._6VtqdG_switchThumb{background:#fff;border-radius:50%;width:17px;height:17px;transition:transform .18s;display:block;transform:translate(0);box-shadow:0 1px 2px #00000040}._6VtqdG_switchOn ._6VtqdG_switchThumb{transform:translate(15px)}._6VtqdG_empty{opacity:.5;text-align:center;padding:20px 0;font-size:13px}._6VtqdG_diagRow{background:#d299220f;border-left:3px solid #d29922;padding:9px 14px;font-size:12px}._6VtqdG_diagPath{opacity:.75;word-break:break-all;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}._6VtqdG_diagReason{opacity:.65;margin-top:1px}._6VtqdG_updateLink{color:inherit;font-weight:600;text-decoration:underline}._6VtqdG_errorBanner{color:#d1242f;white-space:pre-wrap;word-break:break-all;background:#d1242f12;border-radius:10px;align-items:flex-start;gap:10px;padding:10px 14px;font-size:12.5px;display:flex}._6VtqdG_successBanner{color:#3fb950;white-space:pre-wrap;word-break:break-all;background:#3fb95014;border-radius:10px;align-items:flex-start;gap:10px;padding:10px 14px;font-size:12.5px;display:flex}._6VtqdG_detailHead{flex-wrap:wrap;align-items:center;gap:10px;display:flex}._6VtqdG_back{color:inherit;cursor:pointer;background:#8080801f;border:none;border-radius:8px;padding:6px 12px;font-size:12.5px}._6VtqdG_back:hover{background:#80808033}._6VtqdG_detailName{letter-spacing:-.01em;font-size:19px;font-weight:700}._6VtqdG_detailMeta{opacity:.6;flex-direction:column;gap:4px;font-size:12px;display:flex}._6VtqdG_detailMetaLine{word-break:break-all}._6VtqdG_detailContent{white-space:pre-wrap;word-break:break-word;background:#8080800d;border-radius:12px;max-height:62vh;padding:14px 16px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.6;overflow:auto}._6VtqdG_form{background:#8080800d;border-radius:12px;flex-direction:column;gap:10px;padding:14px 16px;display:flex;box-shadow:0 1px 2px #00000008,0 4px 12px #0000000a}._6VtqdG_formRow{flex-direction:column;gap:5px;display:flex}._6VtqdG_formLabel{opacity:.6;font-size:12px}._6VtqdG_input,._6VtqdG_select{color:inherit;background:#8080801f;border:none;border-radius:9px;outline:none;padding:9px 12px;font-size:13px}._6VtqdG_input:focus,._6VtqdG_select:focus{background:#8080802e}._6VtqdG_buttons{align-items:center;gap:8px;display:flex}._6VtqdG_button{cursor:pointer;color:inherit;background:#8080801f;border:none;border-radius:8px;padding:6px 13px;font-size:12.5px;transition:background .15s}._6VtqdG_button:hover{background:#80808033}._6VtqdG_primary{background:var(--hub-model);color:#fff}._6VtqdG_primary:hover{opacity:.92;background:#2f81f7}._6VtqdG_primary:disabled{opacity:.45;cursor:default}._6VtqdG_danger{color:#fff;background:#d1242f}._6VtqdG_danger:hover{opacity:.92;background:#d1242f}._6VtqdG_danger:disabled{opacity:.45;cursor:default}._6VtqdG_formError{color:#d1242f}._6VtqdG_formSuccess{color:#3fb950}._6VtqdG_muted{opacity:.55}._6VtqdG_dot{vertical-align:middle;border-radius:50%;width:7px;height:7px;margin-left:5px;display:inline-block}._6VtqdG_dotModel{background:var(--hub-model)}._6VtqdG_dotUser{background:var(--hub-user)}._6VtqdG_legend{opacity:.55;flex-wrap:wrap;align-items:center;gap:12px;padding:2px 2px 0;font-size:11.5px;display:flex}._6VtqdG_legendItem{align-items:center;gap:4px;display:inline-flex}._6VtqdG_legendItem ._6VtqdG_dot{margin-left:0}._6VtqdG_legendHint{opacity:.8}._6VtqdG_badgeSource,._6VtqdG_badgeCount{opacity:.72;background:#8080801a;border:none}._6VtqdG_badgeDisabled{opacity:.58;background:#8080801a;border:none}._6VtqdG_disclosure{min-width:0;color:inherit;cursor:pointer;text-align:left;background:0 0;border:none;flex:1;align-items:center;gap:7px;padding:2px 0;display:flex}._6VtqdG_disclosure:hover ._6VtqdG_groupTitle{opacity:1}._6VtqdG_chevron{opacity:.55;border-bottom:1.5px solid;border-right:1.5px solid;flex:none;width:8px;height:8px;margin-right:2px;transition:transform .15s;transform:rotate(45deg)}._6VtqdG_chevronCollapsed{transform:rotate(-45deg)}._6VtqdG_headerCount{opacity:.5;white-space:nowrap;margin-left:2px;font-size:12px}._6VtqdG_subbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._6VtqdG_legendToggle{width:22px;height:22px;color:inherit;cursor:pointer;opacity:.55;background:#80808024;border:none;border-radius:50%;flex:none;font-size:12px;line-height:1;transition:opacity .15s,background .15s}._6VtqdG_legendToggle:hover{opacity:1}._6VtqdG_legendToggleActive{opacity:1;background:#80808047}._6VtqdG_filterBar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._6VtqdG_filterBar ._6VtqdG_formLabel{margin:0}._6VtqdG_useCount{color:#d29922;vertical-align:1px;background:#d299221f;border-radius:999px;margin-left:6px;padding:0 6px;font-size:11px;font-weight:700;line-height:1.6;display:inline-block}._6VtqdG_useTime{opacity:.45;white-space:nowrap;margin-left:auto;font-size:11px}._6VtqdG_switchMixed{background:#2f81f773}._6VtqdG_switchMixed ._6VtqdG_switchThumb{transform:translate(7.5px);box-shadow:0 1px 2px #00000040}._6VtqdG_groupTitleInner{align-items:baseline;display:inline-flex}._6VtqdG_groupOps{flex-wrap:wrap;flex:none;align-items:center;gap:6px;display:inline-flex}._6VtqdG_sourceLink{color:inherit;opacity:.9;border-bottom:1px dotted;font-weight:600;text-decoration:none}._6VtqdG_sourceLink:hover{opacity:1}._6VtqdG_statusBadges{flex:none;align-items:center;gap:4px;display:inline-flex}._6VtqdG_statusBadge{opacity:.8;white-space:nowrap;background:#8080801a;border:.5px solid #8080804d;border-radius:999px;padding:1px 7px;font-size:10.5px}._6VtqdG_statusOk{color:#3fb950;background:0 0;border-color:currentColor}._6VtqdG_statusUpdated{color:#d29922;background:#d2992214;border-color:currentColor}._6VtqdG_statusError{color:#d1242f;background:#d1242f14;border-color:currentColor}._6VtqdG_statusWrap{cursor:pointer;background:0 0;border:none;align-items:center;gap:4px;padding:0;font-family:inherit;display:inline-flex}._6VtqdG_statusWrap:hover ._6VtqdG_statusBadge{opacity:1}._6VtqdG_statusButton{cursor:pointer;font-family:inherit}._6VtqdG_statusButton:hover{opacity:1}._6VtqdG_opDanger{color:#d1242f;background:#d1242f1a}._6VtqdG_opDanger:hover{background:#d1242f2e}._6VtqdG_iconBtn{border-radius:999px;justify-content:center;align-items:center;width:24px;height:24px;padding:0;display:inline-flex}._6VtqdG_sourceCard{background:#8080800d;border-radius:12px;flex-direction:column;gap:4px;padding:12px 14px;font-size:12px;display:flex}._6VtqdG_sourceCardTitle{flex-wrap:wrap;align-items:center;gap:8px;display:flex}._6VtqdG_dialogOverlay{z-index:50;-webkit-backdrop-filter:blur(2px);background:#00000059;justify-content:center;align-items:center;padding:24px;display:flex;position:fixed;inset:0}._6VtqdG_dialog{background:var(--dsw-surface,#f5f5f7);min-width:min(320px,100%);max-width:380px;color:var(--dsw-text,#1d1d1f);border-radius:14px;padding:18px 18px 14px;box-shadow:0 12px 40px #00000047,0 2px 8px #00000029}@media (prefers-color-scheme:dark){._6VtqdG_dialog{color:#f5f5f7;background:#2c2c2e}}._6VtqdG_dialogTitle{letter-spacing:-.01em;margin:0 0 8px;font-size:15px;font-weight:700}._6VtqdG_dialogText{opacity:.65;margin:0 0 10px;font-size:12.5px;line-height:1.5}._6VtqdG_dialogList{flex-direction:column;gap:5px;max-height:200px;margin:0 0 14px;padding:0;list-style:none;display:flex;overflow:auto}._6VtqdG_dialogList li{opacity:.85;word-break:break-all;font-size:12.5px}._6VtqdG_dialogActions{justify-content:flex-end;gap:8px;display:flex}._6VtqdG_filterBar ._6VtqdG_search{flex:1;min-width:160px}._6VtqdG_filterBar ._6VtqdG_formLabel{flex:none}._6VtqdG_filterBar ._6VtqdG_select{flex:none;max-width:150px}._6VtqdG_filterBar ._6VtqdG_segmented{flex:none}._6VtqdG_titleIcon{vertical-align:-2px;opacity:.8;margin-right:2px;display:inline-block}._6VtqdG_grow{flex:1}._6VtqdG_hintLine{opacity:.55;margin:0;font-size:11.5px}._6VtqdG_hintPadded{margin:6px 12px 2px}._6VtqdG_hintInline{margin:4px 2px}._6VtqdG_rowMuted{cursor:default;opacity:.55}._6VtqdG_actionsPadded{padding:8px 12px}._6VtqdG_actionsTop{margin-top:8px}._6VtqdG_actionsBottom{margin-bottom:8px}._6VtqdG_groupTime{margin-left:6px}._6VtqdG_sectionHeadRow{align-items:center;gap:8px;display:flex}._6VtqdG_sectionTitleFill{flex:1}._6VtqdG_dialogSelect{width:100%;margin-bottom:12px}._6VtqdG_dialogRow{align-items:center;gap:8px;display:flex}";
|
|
1587
|
+
const tagId = "dsh-skill-hub/panel.module.css";
|
|
1588
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
1589
|
+
const tag = document.createElement("style");
|
|
1590
|
+
tag.dataset.plugin = "dsh-skill-hub";
|
|
1591
|
+
tag.dataset.pluginCss = tagId;
|
|
1592
|
+
tag.textContent = css;
|
|
1593
|
+
document.head.appendChild(tag);
|
|
1594
|
+
}
|
|
1595
|
+
var panel_module_css_default = {
|
|
1596
|
+
"actions": "_6VtqdG_actions",
|
|
1597
|
+
"actionsBottom": "_6VtqdG_actionsBottom",
|
|
1598
|
+
"actionsPadded": "_6VtqdG_actionsPadded",
|
|
1599
|
+
"actionsTop": "_6VtqdG_actionsTop",
|
|
1600
|
+
"back": "_6VtqdG_back",
|
|
1601
|
+
"badge": "_6VtqdG_badge",
|
|
1602
|
+
"badgeCount": "_6VtqdG_badgeCount",
|
|
1603
|
+
"badgeDisabled": "_6VtqdG_badgeDisabled",
|
|
1604
|
+
"badgeModel": "_6VtqdG_badgeModel",
|
|
1605
|
+
"badgeReadonly": "_6VtqdG_badgeReadonly",
|
|
1606
|
+
"badgeSource": "_6VtqdG_badgeSource",
|
|
1607
|
+
"badgeUser": "_6VtqdG_badgeUser",
|
|
1608
|
+
"badgeUses": "_6VtqdG_badgeUses",
|
|
1609
|
+
"badges": "_6VtqdG_badges",
|
|
1610
|
+
"button": "_6VtqdG_button",
|
|
1611
|
+
"buttons": "_6VtqdG_buttons",
|
|
1612
|
+
"chevron": "_6VtqdG_chevron",
|
|
1613
|
+
"chevronCollapsed": "_6VtqdG_chevronCollapsed",
|
|
1614
|
+
"danger": "_6VtqdG_danger",
|
|
1615
|
+
"detailContent": "_6VtqdG_detailContent",
|
|
1616
|
+
"detailHead": "_6VtqdG_detailHead",
|
|
1617
|
+
"detailMeta": "_6VtqdG_detailMeta",
|
|
1618
|
+
"detailMetaLine": "_6VtqdG_detailMetaLine",
|
|
1619
|
+
"detailName": "_6VtqdG_detailName",
|
|
1620
|
+
"diagPath": "_6VtqdG_diagPath",
|
|
1621
|
+
"diagReason": "_6VtqdG_diagReason",
|
|
1622
|
+
"diagRow": "_6VtqdG_diagRow",
|
|
1623
|
+
"dialog": "_6VtqdG_dialog",
|
|
1624
|
+
"dialogActions": "_6VtqdG_dialogActions",
|
|
1625
|
+
"dialogList": "_6VtqdG_dialogList",
|
|
1626
|
+
"dialogOverlay": "_6VtqdG_dialogOverlay",
|
|
1627
|
+
"dialogRow": "_6VtqdG_dialogRow",
|
|
1628
|
+
"dialogSelect": "_6VtqdG_dialogSelect",
|
|
1629
|
+
"dialogText": "_6VtqdG_dialogText",
|
|
1630
|
+
"dialogTitle": "_6VtqdG_dialogTitle",
|
|
1631
|
+
"disclosure": "_6VtqdG_disclosure",
|
|
1632
|
+
"dot": "_6VtqdG_dot",
|
|
1633
|
+
"dotModel": "_6VtqdG_dotModel",
|
|
1634
|
+
"dotUser": "_6VtqdG_dotUser",
|
|
1635
|
+
"empty": "_6VtqdG_empty",
|
|
1636
|
+
"errorBanner": "_6VtqdG_errorBanner",
|
|
1637
|
+
"filterBar": "_6VtqdG_filterBar",
|
|
1638
|
+
"form": "_6VtqdG_form",
|
|
1639
|
+
"formError": "_6VtqdG_formError",
|
|
1640
|
+
"formLabel": "_6VtqdG_formLabel",
|
|
1641
|
+
"formRow": "_6VtqdG_formRow",
|
|
1642
|
+
"formSuccess": "_6VtqdG_formSuccess",
|
|
1643
|
+
"groupHead": "_6VtqdG_groupHead",
|
|
1644
|
+
"groupOps": "_6VtqdG_groupOps",
|
|
1645
|
+
"groupTime": "_6VtqdG_groupTime",
|
|
1646
|
+
"groupTitle": "_6VtqdG_groupTitle",
|
|
1647
|
+
"groupTitleInner": "_6VtqdG_groupTitleInner",
|
|
1648
|
+
"grow": "_6VtqdG_grow",
|
|
1649
|
+
"header": "_6VtqdG_header",
|
|
1650
|
+
"headerCount": "_6VtqdG_headerCount",
|
|
1651
|
+
"hint": "_6VtqdG_hint",
|
|
1652
|
+
"hintInline": "_6VtqdG_hintInline",
|
|
1653
|
+
"hintLine": "_6VtqdG_hintLine",
|
|
1654
|
+
"hintPadded": "_6VtqdG_hintPadded",
|
|
1655
|
+
"iconBtn": "_6VtqdG_iconBtn",
|
|
1656
|
+
"input": "_6VtqdG_input",
|
|
1657
|
+
"legend": "_6VtqdG_legend",
|
|
1658
|
+
"legendHint": "_6VtqdG_legendHint",
|
|
1659
|
+
"legendItem": "_6VtqdG_legendItem",
|
|
1660
|
+
"legendToggle": "_6VtqdG_legendToggle",
|
|
1661
|
+
"legendToggleActive": "_6VtqdG_legendToggleActive",
|
|
1662
|
+
"muted": "_6VtqdG_muted",
|
|
1663
|
+
"opBtn": "_6VtqdG_opBtn",
|
|
1664
|
+
"opDanger": "_6VtqdG_opDanger",
|
|
1665
|
+
"panel": "_6VtqdG_panel",
|
|
1666
|
+
"primary": "_6VtqdG_primary",
|
|
1667
|
+
"row": "_6VtqdG_row",
|
|
1668
|
+
"rowDesc": "_6VtqdG_rowDesc",
|
|
1669
|
+
"rowMain": "_6VtqdG_rowMain",
|
|
1670
|
+
"rowMeta": "_6VtqdG_rowMeta",
|
|
1671
|
+
"rowMuted": "_6VtqdG_rowMuted",
|
|
1672
|
+
"rowName": "_6VtqdG_rowName",
|
|
1673
|
+
"rowNameText": "_6VtqdG_rowNameText",
|
|
1674
|
+
"rowStatic": "_6VtqdG_rowStatic",
|
|
1675
|
+
"search": "_6VtqdG_search",
|
|
1676
|
+
"section": "_6VtqdG_section",
|
|
1677
|
+
"sectionHeadRow": "_6VtqdG_sectionHeadRow",
|
|
1678
|
+
"sectionTitle": "_6VtqdG_sectionTitle",
|
|
1679
|
+
"sectionTitleFill": "_6VtqdG_sectionTitleFill",
|
|
1680
|
+
"segBtn": "_6VtqdG_segBtn",
|
|
1681
|
+
"segBtnActive": "_6VtqdG_segBtnActive",
|
|
1682
|
+
"segmented": "_6VtqdG_segmented",
|
|
1683
|
+
"select": "_6VtqdG_select",
|
|
1684
|
+
"sourceCard": "_6VtqdG_sourceCard",
|
|
1685
|
+
"sourceCardTitle": "_6VtqdG_sourceCardTitle",
|
|
1686
|
+
"sourceLink": "_6VtqdG_sourceLink",
|
|
1687
|
+
"statusBadge": "_6VtqdG_statusBadge",
|
|
1688
|
+
"statusBadges": "_6VtqdG_statusBadges",
|
|
1689
|
+
"statusButton": "_6VtqdG_statusButton",
|
|
1690
|
+
"statusError": "_6VtqdG_statusError",
|
|
1691
|
+
"statusOk": "_6VtqdG_statusOk",
|
|
1692
|
+
"statusUpdated": "_6VtqdG_statusUpdated",
|
|
1693
|
+
"statusWrap": "_6VtqdG_statusWrap",
|
|
1694
|
+
"subbar": "_6VtqdG_subbar",
|
|
1695
|
+
"successBanner": "_6VtqdG_successBanner",
|
|
1696
|
+
"switch": "_6VtqdG_switch",
|
|
1697
|
+
"switchMixed": "_6VtqdG_switchMixed",
|
|
1698
|
+
"switchOn": "_6VtqdG_switchOn",
|
|
1699
|
+
"switchThumb": "_6VtqdG_switchThumb",
|
|
1700
|
+
"title": "_6VtqdG_title",
|
|
1701
|
+
"titleIcon": "_6VtqdG_titleIcon",
|
|
1702
|
+
"updateLink": "_6VtqdG_updateLink",
|
|
1703
|
+
"useCount": "_6VtqdG_useCount",
|
|
1704
|
+
"useTime": "_6VtqdG_useTime"
|
|
1705
|
+
};
|
|
1706
|
+
//#endregion
|
|
1707
|
+
//#region src/client/panel/SourceStatusBadge.tsx
|
|
1708
|
+
function SourceStatusBadge(props) {
|
|
1709
|
+
const { check, checking, onCheck } = props;
|
|
1710
|
+
if (checking === true) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1711
|
+
className: panel_module_css_default.statusBadge,
|
|
1712
|
+
children: tt("source.checking")
|
|
1713
|
+
});
|
|
1714
|
+
if (check === void 0) {
|
|
1715
|
+
if (onCheck === void 0) return null;
|
|
1716
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1717
|
+
type: "button",
|
|
1718
|
+
className: panel_module_css_default.statusBadge + " " + panel_module_css_default.statusButton,
|
|
1719
|
+
title: tt("source.checkHint"),
|
|
1720
|
+
onClick: onCheck,
|
|
1721
|
+
children: tt("source.check")
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
const badges = (() => {
|
|
1725
|
+
if (check.error !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1726
|
+
className: panel_module_css_default.statusBadge + " " + panel_module_css_default.statusError,
|
|
1727
|
+
children: tt("source.error")
|
|
1728
|
+
});
|
|
1729
|
+
if (check.throttled === true) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1730
|
+
className: panel_module_css_default.statusBadge,
|
|
1731
|
+
children: tt("source.throttled")
|
|
1732
|
+
});
|
|
1733
|
+
if (check.unverified === true) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1734
|
+
className: panel_module_css_default.statusBadge,
|
|
1735
|
+
children: tt("source.unverified")
|
|
1736
|
+
});
|
|
1737
|
+
if (check.changed) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1738
|
+
className: panel_module_css_default.statusBadges,
|
|
1739
|
+
children: [check.updated.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1740
|
+
className: panel_module_css_default.statusBadge + " " + panel_module_css_default.statusUpdated,
|
|
1741
|
+
children: tt("source.updated", { count: check.updated.length })
|
|
1742
|
+
}) : null, check.deleted.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1743
|
+
className: panel_module_css_default.statusBadge + " " + panel_module_css_default.statusError,
|
|
1744
|
+
children: tt("source.deleted", { count: check.deleted.length })
|
|
1745
|
+
}) : null]
|
|
1746
|
+
});
|
|
1747
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1748
|
+
className: panel_module_css_default.statusBadge + " " + panel_module_css_default.statusOk,
|
|
1749
|
+
children: tt("source.upToDate")
|
|
1750
|
+
});
|
|
1751
|
+
})();
|
|
1752
|
+
if (onCheck === void 0) return badges;
|
|
1753
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1754
|
+
type: "button",
|
|
1755
|
+
className: panel_module_css_default.statusWrap,
|
|
1756
|
+
title: tt("source.checkHint"),
|
|
1757
|
+
onClick: onCheck,
|
|
1758
|
+
children: badges
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
//#endregion
|
|
1762
|
+
//#region src/client/panel/SkillDetailView.tsx
|
|
1763
|
+
function SkillDetailView(props) {
|
|
1764
|
+
const { detail, hubConfig, uses, groupsState, sourcesState, sourceCheck, checkingSource, syncingSource, loading, onBack, onCheck, onSync, onFollowDelete } = props;
|
|
1765
|
+
const detailSource = sourcesState?.sources.find((source) => source.skills.includes(detail.name));
|
|
1766
|
+
const detailCheck = detailSource !== void 0 ? sourceCheck[detailSource.repo] : void 0;
|
|
1767
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1768
|
+
className: panel_module_css_default.panel,
|
|
1769
|
+
children: [
|
|
1770
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1771
|
+
className: panel_module_css_default.detailHead,
|
|
1772
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1773
|
+
type: "button",
|
|
1774
|
+
className: panel_module_css_default.back,
|
|
1775
|
+
onClick: onBack,
|
|
1776
|
+
children: tt("detail.back")
|
|
1777
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1778
|
+
className: panel_module_css_default.detailName,
|
|
1779
|
+
children: [
|
|
1780
|
+
detail.name,
|
|
1781
|
+
detail.invocation.modelInvocable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1782
|
+
className: panel_module_css_default.dot + " " + panel_module_css_default.dotModel,
|
|
1783
|
+
style: dotStyle(hubConfig?.dotModelColor),
|
|
1784
|
+
title: tt("legend.model")
|
|
1785
|
+
}) : null,
|
|
1786
|
+
detail.invocation.userInvocable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1787
|
+
className: panel_module_css_default.dot + " " + panel_module_css_default.dotUser,
|
|
1788
|
+
style: dotStyle(hubConfig?.dotUserColor),
|
|
1789
|
+
title: tt("legend.user")
|
|
1790
|
+
}) : null
|
|
1791
|
+
]
|
|
1792
|
+
})]
|
|
1793
|
+
}),
|
|
1794
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1795
|
+
className: panel_module_css_default.detailMeta,
|
|
1796
|
+
children: [
|
|
1797
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1798
|
+
className: panel_module_css_default.detailMetaLine,
|
|
1799
|
+
children: [
|
|
1800
|
+
tt("detail.provider"),
|
|
1801
|
+
": ",
|
|
1802
|
+
detail.provider
|
|
1803
|
+
]
|
|
1804
|
+
}),
|
|
1805
|
+
detail.addedAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1806
|
+
className: panel_module_css_default.detailMetaLine,
|
|
1807
|
+
children: [
|
|
1808
|
+
tt("detail.addedAt"),
|
|
1809
|
+
": ",
|
|
1810
|
+
formatDateTime(detail.addedAt)
|
|
1811
|
+
]
|
|
1812
|
+
}) : null,
|
|
1813
|
+
detail.updatedAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1814
|
+
className: panel_module_css_default.detailMetaLine,
|
|
1815
|
+
children: [
|
|
1816
|
+
tt("detail.updatedAt"),
|
|
1817
|
+
": ",
|
|
1818
|
+
formatDateTime(detail.updatedAt)
|
|
1819
|
+
]
|
|
1820
|
+
}) : null,
|
|
1821
|
+
detail.path !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1822
|
+
className: panel_module_css_default.detailMetaLine,
|
|
1823
|
+
children: [
|
|
1824
|
+
tt("detail.path"),
|
|
1825
|
+
": ",
|
|
1826
|
+
detail.path
|
|
1827
|
+
]
|
|
1828
|
+
}) : null,
|
|
1829
|
+
detail.whenToUse !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1830
|
+
className: panel_module_css_default.detailMetaLine,
|
|
1831
|
+
children: [
|
|
1832
|
+
tt("detail.whenToUse"),
|
|
1833
|
+
": ",
|
|
1834
|
+
detail.whenToUse
|
|
1835
|
+
]
|
|
1836
|
+
}) : null,
|
|
1837
|
+
(() => {
|
|
1838
|
+
const stat = uses.get(detail.name);
|
|
1839
|
+
if (stat === void 0 || stat.count === 0) return null;
|
|
1840
|
+
const at = stat.lastUsed !== void 0 ? " · " + new Date(stat.lastUsed).toLocaleString() : "";
|
|
1841
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1842
|
+
className: panel_module_css_default.detailMetaLine,
|
|
1843
|
+
children: [
|
|
1844
|
+
tt("detail.uses"),
|
|
1845
|
+
": ",
|
|
1846
|
+
stat.count,
|
|
1847
|
+
at
|
|
1848
|
+
]
|
|
1849
|
+
});
|
|
1850
|
+
})(),
|
|
1851
|
+
(() => {
|
|
1852
|
+
const tags = (groupsState?.tags ?? []).filter((tag) => tag.skillNames.includes(detail.name)).map((tag) => tag.name);
|
|
1853
|
+
return tags.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1854
|
+
className: panel_module_css_default.detailMetaLine,
|
|
1855
|
+
children: [
|
|
1856
|
+
tt("detail.groups"),
|
|
1857
|
+
": ",
|
|
1858
|
+
tags.join(", ")
|
|
1859
|
+
]
|
|
1860
|
+
}) : null;
|
|
1861
|
+
})()
|
|
1862
|
+
]
|
|
1863
|
+
}),
|
|
1864
|
+
detailSource !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1865
|
+
className: panel_module_css_default.sourceCard,
|
|
1866
|
+
children: [
|
|
1867
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1868
|
+
className: panel_module_css_default.sourceCardTitle,
|
|
1869
|
+
children: [
|
|
1870
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
1871
|
+
className: panel_module_css_default.sourceLink,
|
|
1872
|
+
href: "https://github.com/" + detailSource.repo,
|
|
1873
|
+
target: "_blank",
|
|
1874
|
+
rel: "noreferrer",
|
|
1875
|
+
children: detailSource.repo
|
|
1876
|
+
}),
|
|
1877
|
+
detailSource.ref !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1878
|
+
className: panel_module_css_default.badge + " " + panel_module_css_default.badgeSource,
|
|
1879
|
+
children: detailSource.ref
|
|
1880
|
+
}) : null,
|
|
1881
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SourceStatusBadge, { check: detailCheck })
|
|
1882
|
+
]
|
|
1883
|
+
}),
|
|
1884
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1885
|
+
className: panel_module_css_default.detailMetaLine,
|
|
1886
|
+
children: [
|
|
1887
|
+
tt("source.commit"),
|
|
1888
|
+
": ",
|
|
1889
|
+
detailSource.commitSha === "" ? tt("source.unverified") : shortSha(detailSource.commitSha)
|
|
1890
|
+
]
|
|
1891
|
+
}),
|
|
1892
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1893
|
+
className: panel_module_css_default.buttons + " " + panel_module_css_default.actionsTop,
|
|
1894
|
+
children: [
|
|
1895
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1896
|
+
type: "button",
|
|
1897
|
+
className: panel_module_css_default.opBtn,
|
|
1898
|
+
disabled: checkingSource !== null,
|
|
1899
|
+
onClick: () => {
|
|
1900
|
+
onCheck(detailSource.repo);
|
|
1901
|
+
},
|
|
1902
|
+
children: checkingSource === detailSource.repo ? tt("source.checking") : tt("source.check")
|
|
1903
|
+
}),
|
|
1904
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1905
|
+
type: "button",
|
|
1906
|
+
className: panel_module_css_default.opBtn,
|
|
1907
|
+
disabled: syncingSource !== null,
|
|
1908
|
+
onClick: () => {
|
|
1909
|
+
onSync(detailSource.repo, [detail.name]);
|
|
1910
|
+
},
|
|
1911
|
+
children: syncingSource === detailSource.repo ? tt("source.syncing") : tt("source.sync")
|
|
1912
|
+
}),
|
|
1913
|
+
detailCheck?.deleted.includes(detail.name) === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1914
|
+
type: "button",
|
|
1915
|
+
className: panel_module_css_default.opBtn + " " + panel_module_css_default.opDanger,
|
|
1916
|
+
onClick: () => {
|
|
1917
|
+
onFollowDelete(detailSource.repo, [detail.name]);
|
|
1918
|
+
},
|
|
1919
|
+
children: tt("source.followDelete")
|
|
1920
|
+
}) : null
|
|
1921
|
+
]
|
|
1922
|
+
})
|
|
1923
|
+
]
|
|
1924
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1925
|
+
className: panel_module_css_default.hintLine,
|
|
1926
|
+
children: tt("source.private")
|
|
1927
|
+
}),
|
|
1928
|
+
loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1929
|
+
className: panel_module_css_default.muted,
|
|
1930
|
+
children: tt("detail.loading")
|
|
1931
|
+
}) : null,
|
|
1932
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
1933
|
+
className: panel_module_css_default.detailContent,
|
|
1934
|
+
children: detail.content
|
|
1935
|
+
})
|
|
1936
|
+
]
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
//#endregion
|
|
1940
|
+
//#region src/client/panel/TagEditorView.tsx
|
|
1941
|
+
function TagEditorView(props) {
|
|
1942
|
+
const { tag, editName, membersDraft, editSearch, catalog, tagBusy, onBack, onEditName, onEditSearch, onToggleMember, onRename, onDelete, onSaveMembers } = props;
|
|
1943
|
+
const editQuery = editSearch.trim().toLocaleLowerCase();
|
|
1944
|
+
const editSkills = (catalog?.skills ?? []).filter((skill) => editQuery.length === 0 || skill.name.toLocaleLowerCase().includes(editQuery) || skill.description.toLocaleLowerCase().includes(editQuery));
|
|
1945
|
+
const editDisabled = (catalog?.disabled ?? []).filter((record) => editQuery.length === 0 || record.name.toLocaleLowerCase().includes(editQuery) || record.description.toLocaleLowerCase().includes(editQuery));
|
|
1946
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1947
|
+
className: panel_module_css_default.panel,
|
|
1948
|
+
children: [
|
|
1949
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1950
|
+
className: panel_module_css_default.detailHead,
|
|
1951
|
+
children: [
|
|
1952
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1953
|
+
type: "button",
|
|
1954
|
+
className: panel_module_css_default.back,
|
|
1955
|
+
onClick: onBack,
|
|
1956
|
+
children: tt("detail.back")
|
|
1957
|
+
}),
|
|
1958
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1959
|
+
className: panel_module_css_default.input + " " + panel_module_css_default.grow,
|
|
1960
|
+
value: editName,
|
|
1961
|
+
onChange: (event) => {
|
|
1962
|
+
onEditName(event.target.value);
|
|
1963
|
+
},
|
|
1964
|
+
placeholder: tt("groups.namePlaceholder")
|
|
1965
|
+
}),
|
|
1966
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1967
|
+
type: "button",
|
|
1968
|
+
className: panel_module_css_default.opBtn,
|
|
1969
|
+
disabled: tagBusy || editName.trim() === "",
|
|
1970
|
+
onClick: onRename,
|
|
1971
|
+
children: tt("groups.rename")
|
|
1972
|
+
}),
|
|
1973
|
+
tag.default !== true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1974
|
+
type: "button",
|
|
1975
|
+
className: panel_module_css_default.opBtn,
|
|
1976
|
+
disabled: tagBusy,
|
|
1977
|
+
onClick: onDelete,
|
|
1978
|
+
children: tt("groups.delete")
|
|
1979
|
+
}) : null
|
|
1980
|
+
]
|
|
1981
|
+
}),
|
|
1982
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1983
|
+
className: panel_module_css_default.hintLine,
|
|
1984
|
+
children: tt("groups.membersHint")
|
|
1985
|
+
}),
|
|
1986
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1987
|
+
className: panel_module_css_default.search,
|
|
1988
|
+
value: editSearch,
|
|
1989
|
+
onChange: (event) => {
|
|
1990
|
+
onEditSearch(event.target.value);
|
|
1991
|
+
},
|
|
1992
|
+
placeholder: tt("panel.search")
|
|
1993
|
+
}),
|
|
1994
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1995
|
+
className: panel_module_css_default.section,
|
|
1996
|
+
children: [editSkills.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1997
|
+
className: panel_module_css_default.row,
|
|
1998
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1999
|
+
type: "checkbox",
|
|
2000
|
+
checked: membersDraft.has(skill.name),
|
|
2001
|
+
onChange: (event) => {
|
|
2002
|
+
onToggleMember(skill.name, event.target.checked);
|
|
2003
|
+
}
|
|
2004
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2005
|
+
className: panel_module_css_default.rowMain,
|
|
2006
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2007
|
+
className: panel_module_css_default.rowName,
|
|
2008
|
+
children: skill.name
|
|
2009
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2010
|
+
className: panel_module_css_default.rowDesc,
|
|
2011
|
+
children: skill.description
|
|
2012
|
+
})]
|
|
2013
|
+
})]
|
|
2014
|
+
}, skill.name)), editDisabled.map((record) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2015
|
+
className: panel_module_css_default.row,
|
|
2016
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2017
|
+
type: "checkbox",
|
|
2018
|
+
checked: membersDraft.has(record.name),
|
|
2019
|
+
onChange: (event) => {
|
|
2020
|
+
onToggleMember(record.name, event.target.checked);
|
|
2021
|
+
}
|
|
2022
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2023
|
+
className: panel_module_css_default.rowMain,
|
|
2024
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2025
|
+
className: panel_module_css_default.rowName,
|
|
2026
|
+
children: record.name
|
|
2027
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2028
|
+
className: panel_module_css_default.rowDesc,
|
|
2029
|
+
children: [
|
|
2030
|
+
record.description,
|
|
2031
|
+
" · ",
|
|
2032
|
+
tt("panel.disabled")
|
|
2033
|
+
]
|
|
2034
|
+
})]
|
|
2035
|
+
})]
|
|
2036
|
+
}, record.name))]
|
|
2037
|
+
}),
|
|
2038
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2039
|
+
className: panel_module_css_default.buttons,
|
|
2040
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2041
|
+
type: "button",
|
|
2042
|
+
className: panel_module_css_default.button + " " + panel_module_css_default.primary,
|
|
2043
|
+
disabled: tagBusy,
|
|
2044
|
+
onClick: onSaveMembers,
|
|
2045
|
+
children: tt("groups.saveMembers")
|
|
2046
|
+
})
|
|
2047
|
+
})
|
|
2048
|
+
]
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
//#endregion
|
|
2052
|
+
//#region src/client/panel/SkillRow.tsx
|
|
2053
|
+
function SkillRow(props) {
|
|
2054
|
+
const { skill, hub } = props;
|
|
2055
|
+
const stat = hub.uses.get(skill.name);
|
|
2056
|
+
const count = stat?.count ?? 0;
|
|
2057
|
+
const lastUsed = stat?.lastUsed;
|
|
2058
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2059
|
+
className: panel_module_css_default.row,
|
|
2060
|
+
onClick: () => {
|
|
2061
|
+
hub.openDetail(skill.name);
|
|
2062
|
+
},
|
|
2063
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2064
|
+
className: panel_module_css_default.rowMain,
|
|
2065
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2066
|
+
className: panel_module_css_default.rowName,
|
|
2067
|
+
children: [
|
|
2068
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2069
|
+
className: panel_module_css_default.rowNameText,
|
|
2070
|
+
children: skill.name
|
|
2071
|
+
}),
|
|
2072
|
+
count > 0 && hub.hubConfig?.showUseCount !== false ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2073
|
+
className: panel_module_css_default.useCount,
|
|
2074
|
+
children: count
|
|
2075
|
+
}) : null,
|
|
2076
|
+
skill.invocation.modelInvocable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2077
|
+
className: panel_module_css_default.dot + " " + panel_module_css_default.dotModel,
|
|
2078
|
+
style: dotStyle(hub.hubConfig?.dotModelColor),
|
|
2079
|
+
title: tt("legend.model")
|
|
2080
|
+
}) : null,
|
|
2081
|
+
skill.invocation.userInvocable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2082
|
+
className: panel_module_css_default.dot + " " + panel_module_css_default.dotUser,
|
|
2083
|
+
style: dotStyle(hub.hubConfig?.dotUserColor),
|
|
2084
|
+
title: tt("legend.user")
|
|
2085
|
+
}) : null,
|
|
2086
|
+
lastUsed !== void 0 && hub.hubConfig?.showUseTime !== false ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2087
|
+
className: panel_module_css_default.useTime,
|
|
2088
|
+
children: relativeTimeText(lastUsed)
|
|
2089
|
+
}) : null
|
|
2090
|
+
]
|
|
2091
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2092
|
+
className: panel_module_css_default.rowDesc,
|
|
2093
|
+
children: skill.description
|
|
2094
|
+
})]
|
|
2095
|
+
}), skill.writable ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2096
|
+
type: "button",
|
|
2097
|
+
className: panel_module_css_default.opBtn + " " + panel_module_css_default.opDanger + " " + panel_module_css_default.iconBtn,
|
|
2098
|
+
disabled: hub.busyNames.has(skill.name) || hub.tagBusy,
|
|
2099
|
+
title: tt("row.delete"),
|
|
2100
|
+
"aria-label": tt("row.delete"),
|
|
2101
|
+
onClick: (event) => {
|
|
2102
|
+
event.stopPropagation();
|
|
2103
|
+
hub.requestDeleteSkill(skill.name);
|
|
2104
|
+
},
|
|
2105
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, { size: 14 })
|
|
2106
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2107
|
+
type: "button",
|
|
2108
|
+
role: "switch",
|
|
2109
|
+
"aria-checked": true,
|
|
2110
|
+
"aria-label": tt("row.disable"),
|
|
2111
|
+
className: panel_module_css_default.switch + " " + panel_module_css_default.switchOn,
|
|
2112
|
+
disabled: hub.busyNames.has(skill.name),
|
|
2113
|
+
onClick: (event) => {
|
|
2114
|
+
event.stopPropagation();
|
|
2115
|
+
hub.toggle(skill, false);
|
|
2116
|
+
},
|
|
2117
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.switchThumb })
|
|
2118
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2119
|
+
className: panel_module_css_default.badge + " " + panel_module_css_default.badgeReadonly,
|
|
2120
|
+
children: tt("row.readonly")
|
|
2121
|
+
})]
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
//#endregion
|
|
2125
|
+
//#region src/client/panel/DisabledRow.tsx
|
|
2126
|
+
/** Source badge label for a disabled record, derived from its writable root. */
|
|
2127
|
+
function disabledSourceLabel(root) {
|
|
2128
|
+
return tt("badge.source." + root);
|
|
2129
|
+
}
|
|
2130
|
+
function DisabledRow(props) {
|
|
2131
|
+
const { record, busy, onEnable } = props;
|
|
2132
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2133
|
+
className: panel_module_css_default.row + " " + panel_module_css_default.rowStatic,
|
|
2134
|
+
children: [
|
|
2135
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2136
|
+
className: panel_module_css_default.rowMain,
|
|
2137
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2138
|
+
className: panel_module_css_default.rowName,
|
|
2139
|
+
children: record.name
|
|
2140
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2141
|
+
className: panel_module_css_default.rowDesc,
|
|
2142
|
+
children: record.description
|
|
2143
|
+
})]
|
|
2144
|
+
}),
|
|
2145
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2146
|
+
className: panel_module_css_default.badges,
|
|
2147
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2148
|
+
className: panel_module_css_default.badge + " " + panel_module_css_default.badgeSource,
|
|
2149
|
+
children: disabledSourceLabel(record.root)
|
|
2150
|
+
})
|
|
2151
|
+
}),
|
|
2152
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2153
|
+
type: "button",
|
|
2154
|
+
role: "switch",
|
|
2155
|
+
"aria-checked": false,
|
|
2156
|
+
"aria-label": tt("row.enable"),
|
|
2157
|
+
className: panel_module_css_default.switch,
|
|
2158
|
+
disabled: busy,
|
|
2159
|
+
onClick: onEnable,
|
|
2160
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.switchThumb })
|
|
2161
|
+
})
|
|
2162
|
+
]
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
//#endregion
|
|
2166
|
+
//#region src/client/panel/GroupSummary.tsx
|
|
2167
|
+
function GroupSummary(props) {
|
|
2168
|
+
const { members, hub } = props;
|
|
2169
|
+
let total = 0;
|
|
2170
|
+
let latest;
|
|
2171
|
+
for (const name of members) {
|
|
2172
|
+
const stat = hub.uses.get(name);
|
|
2173
|
+
if (stat === void 0) continue;
|
|
2174
|
+
total += stat.count;
|
|
2175
|
+
if (stat.lastUsed !== void 0 && (latest === void 0 || stat.lastUsed > latest)) latest = stat.lastUsed;
|
|
2176
|
+
}
|
|
2177
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2178
|
+
className: panel_module_css_default.groupTitleInner,
|
|
2179
|
+
children: [hub.hubConfig?.showGroupSummary !== false && total > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2180
|
+
className: panel_module_css_default.useCount,
|
|
2181
|
+
children: total
|
|
2182
|
+
}) : null, hub.hubConfig?.showGroupSummary !== false && latest !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2183
|
+
className: panel_module_css_default.useTime + " " + panel_module_css_default.groupTime,
|
|
2184
|
+
children: relativeTimeText(latest)
|
|
2185
|
+
}) : null]
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
//#endregion
|
|
2189
|
+
//#region src/client/panel/SourcesView.tsx
|
|
2190
|
+
function SourcesView(props) {
|
|
2191
|
+
const { hub } = props;
|
|
2192
|
+
const { catalog, groupsState, skillView, sourceFilter, origins, sorted, normalized, collapsedGroups, viewNames, sourceCheck, actionNames, checkingSource, syncingSource, batchBusy, busyNames, toggleGroupCollapse, checkSources, requestSync, requestDelete, toggleGroup, enableDisabled } = hub;
|
|
2193
|
+
if (skillView === "flat") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: filterBySource(sorted, sourceFilter, origins).map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
|
|
2194
|
+
skill,
|
|
2195
|
+
hub
|
|
2196
|
+
}, skill.name)) });
|
|
2197
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2198
|
+
groupsState !== null && groupsState.collections.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2199
|
+
className: panel_module_css_default.empty,
|
|
2200
|
+
children: tt("groups.noCollections")
|
|
2201
|
+
}) : null,
|
|
2202
|
+
groupsState?.collections.map((collection) => {
|
|
2203
|
+
const skills = filterBySource(sorted, sourceFilter, origins).filter((skill) => collection.skillNames.includes(skill.name));
|
|
2204
|
+
const disabledMembers = (catalog?.disabled ?? []).filter((record) => collection.skillNames.includes(record.name) && (normalized.length === 0 || record.name.toLocaleLowerCase().includes(normalized) || record.description.toLocaleLowerCase().includes(normalized)) && (sourceFilter === "all" || (origins[record.name] ?? "private") === sourceFilter));
|
|
2205
|
+
const collapsed = collapsedGroups.has("col:" + collection.name);
|
|
2206
|
+
const view = groupSwitchView(collection.skillNames, viewNames);
|
|
2207
|
+
const check = sourceCheck[collection.name];
|
|
2208
|
+
const hasWritable = collection.skillNames.some((name) => actionNames.has(name));
|
|
2209
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2210
|
+
className: panel_module_css_default.section,
|
|
2211
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2212
|
+
className: panel_module_css_default.groupHead,
|
|
2213
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2214
|
+
type: "button",
|
|
2215
|
+
className: panel_module_css_default.disclosure,
|
|
2216
|
+
"aria-expanded": !collapsed,
|
|
2217
|
+
onClick: () => {
|
|
2218
|
+
toggleGroupCollapse("col:" + collection.name);
|
|
2219
|
+
},
|
|
2220
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.chevron + (collapsed ? " " + panel_module_css_default.chevronCollapsed : "") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2221
|
+
className: panel_module_css_default.groupTitle,
|
|
2222
|
+
children: [
|
|
2223
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2224
|
+
className: panel_module_css_default.sourceLink,
|
|
2225
|
+
href: "https://github.com/" + collection.name,
|
|
2226
|
+
target: "_blank",
|
|
2227
|
+
rel: "noreferrer",
|
|
2228
|
+
onClick: (event) => {
|
|
2229
|
+
event.stopPropagation();
|
|
2230
|
+
},
|
|
2231
|
+
children: collection.name
|
|
2232
|
+
}),
|
|
2233
|
+
" · " + collection.skillNames.length,
|
|
2234
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupSummary, {
|
|
2235
|
+
members: collection.skillNames,
|
|
2236
|
+
hub
|
|
2237
|
+
})
|
|
2238
|
+
]
|
|
2239
|
+
})]
|
|
2240
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2241
|
+
className: panel_module_css_default.groupOps,
|
|
2242
|
+
children: [
|
|
2243
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SourceStatusBadge, {
|
|
2244
|
+
check,
|
|
2245
|
+
checking: checkingSource === collection.name,
|
|
2246
|
+
onCheck: () => {
|
|
2247
|
+
checkSources(collection.name);
|
|
2248
|
+
}
|
|
2249
|
+
}),
|
|
2250
|
+
check !== void 0 && check.changed && check.updated.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2251
|
+
type: "button",
|
|
2252
|
+
className: panel_module_css_default.opBtn,
|
|
2253
|
+
disabled: syncingSource !== null,
|
|
2254
|
+
onClick: (event) => {
|
|
2255
|
+
event.stopPropagation();
|
|
2256
|
+
requestSync(collection.name, check.updated);
|
|
2257
|
+
},
|
|
2258
|
+
children: syncingSource === collection.name ? tt("source.syncing") : tt("source.sync")
|
|
2259
|
+
}) : null,
|
|
2260
|
+
check !== void 0 && check.deleted.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2261
|
+
type: "button",
|
|
2262
|
+
className: panel_module_css_default.opBtn + " " + panel_module_css_default.opDanger,
|
|
2263
|
+
onClick: (event) => {
|
|
2264
|
+
event.stopPropagation();
|
|
2265
|
+
requestDelete(collection.name, check.deleted);
|
|
2266
|
+
},
|
|
2267
|
+
children: tt("source.followDelete")
|
|
2268
|
+
}) : null,
|
|
2269
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2270
|
+
type: "button",
|
|
2271
|
+
role: "switch",
|
|
2272
|
+
"aria-checked": view.state !== "off",
|
|
2273
|
+
"aria-label": collection.name,
|
|
2274
|
+
className: panel_module_css_default.switch + (view.state === "on" ? " " + panel_module_css_default.switchOn : view.state === "mixed" ? " " + panel_module_css_default.switchMixed : ""),
|
|
2275
|
+
disabled: batchBusy || collection.skillNames.length === 0 || view.state !== "off" && !hasWritable,
|
|
2276
|
+
title: view.state !== "off" && !hasWritable ? tt("groups.noWritable") : void 0,
|
|
2277
|
+
onClick: (event) => {
|
|
2278
|
+
event.stopPropagation();
|
|
2279
|
+
toggleGroup("col:" + collection.name, collection.name, view.state);
|
|
2280
|
+
},
|
|
2281
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.switchThumb })
|
|
2282
|
+
})
|
|
2283
|
+
]
|
|
2284
|
+
})]
|
|
2285
|
+
}), !collapsed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [skills.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
|
|
2286
|
+
skill,
|
|
2287
|
+
hub
|
|
2288
|
+
}, skill.name)), disabledMembers.map((record) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DisabledRow, {
|
|
2289
|
+
record,
|
|
2290
|
+
busy: busyNames.has(record.name),
|
|
2291
|
+
onEnable: () => {
|
|
2292
|
+
enableDisabled(record);
|
|
2293
|
+
}
|
|
2294
|
+
}, record.name))] }) : null]
|
|
2295
|
+
}, "col:" + collection.name);
|
|
2296
|
+
}),
|
|
2297
|
+
(() => {
|
|
2298
|
+
const uncategorized = filterBySource(sorted, sourceFilter, origins).filter((skill) => origins[skill.name] === void 0);
|
|
2299
|
+
if (uncategorized.length === 0) return null;
|
|
2300
|
+
const collapsed = collapsedGroups.has("uncategorized-source");
|
|
2301
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2302
|
+
className: panel_module_css_default.section,
|
|
2303
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2304
|
+
className: panel_module_css_default.groupHead,
|
|
2305
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2306
|
+
type: "button",
|
|
2307
|
+
className: panel_module_css_default.disclosure,
|
|
2308
|
+
"aria-expanded": !collapsed,
|
|
2309
|
+
onClick: () => {
|
|
2310
|
+
toggleGroupCollapse("uncategorized-source");
|
|
2311
|
+
},
|
|
2312
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.chevron + (collapsed ? " " + panel_module_css_default.chevronCollapsed : "") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2313
|
+
className: panel_module_css_default.groupTitle,
|
|
2314
|
+
children: [
|
|
2315
|
+
tt("groups.personal"),
|
|
2316
|
+
" · ",
|
|
2317
|
+
uncategorized.length,
|
|
2318
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupSummary, {
|
|
2319
|
+
members: uncategorized.map((skill) => skill.name),
|
|
2320
|
+
hub
|
|
2321
|
+
})
|
|
2322
|
+
]
|
|
2323
|
+
})]
|
|
2324
|
+
})
|
|
2325
|
+
}), !collapsed ? uncategorized.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
|
|
2326
|
+
skill,
|
|
2327
|
+
hub
|
|
2328
|
+
}, skill.name)) : null]
|
|
2329
|
+
});
|
|
2330
|
+
})()
|
|
2331
|
+
] });
|
|
2332
|
+
}
|
|
2333
|
+
//#endregion
|
|
2334
|
+
//#region src/client/panel/ScenesView.tsx
|
|
2335
|
+
function ScenesView(props) {
|
|
2336
|
+
const { hub } = props;
|
|
2337
|
+
const { catalog, groupsState, sorted, normalized, collapsedGroups, viewNames, actionNames, batchBusy, busyNames, newTagName, setNewTagName, tagBusy, createTag, toggleGroupCollapse, toggleGroup, setEditingTag, setEditName, setMembersDraft, setEditSearch, enableDisabled } = hub;
|
|
2338
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2339
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("form", {
|
|
2340
|
+
className: panel_module_css_default.form,
|
|
2341
|
+
onSubmit: (event) => {
|
|
2342
|
+
createTag(event);
|
|
2343
|
+
},
|
|
2344
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2345
|
+
className: panel_module_css_default.buttons,
|
|
2346
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2347
|
+
className: panel_module_css_default.input + " " + panel_module_css_default.grow,
|
|
2348
|
+
value: newTagName,
|
|
2349
|
+
onChange: (event) => {
|
|
2350
|
+
setNewTagName(event.target.value);
|
|
2351
|
+
},
|
|
2352
|
+
placeholder: tt("groups.namePlaceholder")
|
|
2353
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2354
|
+
type: "submit",
|
|
2355
|
+
className: panel_module_css_default.button + " " + panel_module_css_default.primary,
|
|
2356
|
+
disabled: tagBusy || newTagName.trim() === "",
|
|
2357
|
+
children: tt("groups.new")
|
|
2358
|
+
})]
|
|
2359
|
+
})
|
|
2360
|
+
}),
|
|
2361
|
+
groupsState !== null && groupsState.tags.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2362
|
+
className: panel_module_css_default.empty,
|
|
2363
|
+
children: tt("groups.empty")
|
|
2364
|
+
}) : null,
|
|
2365
|
+
groupsState?.tags.map((tag) => {
|
|
2366
|
+
const skills = sorted.filter((skill) => tag.skillNames.includes(skill.name));
|
|
2367
|
+
const disabledMembers = (catalog?.disabled ?? []).filter((record) => tag.skillNames.includes(record.name) && (normalized.length === 0 || record.name.toLocaleLowerCase().includes(normalized) || record.description.toLocaleLowerCase().includes(normalized)));
|
|
2368
|
+
const collapsed = collapsedGroups.has("tag:" + tag.id);
|
|
2369
|
+
const view = groupSwitchView(tag.skillNames, viewNames);
|
|
2370
|
+
const hasWritable = tag.skillNames.some((name) => actionNames.has(name));
|
|
2371
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2372
|
+
className: panel_module_css_default.section,
|
|
2373
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2374
|
+
className: panel_module_css_default.groupHead,
|
|
2375
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2376
|
+
type: "button",
|
|
2377
|
+
className: panel_module_css_default.disclosure,
|
|
2378
|
+
"aria-expanded": !collapsed,
|
|
2379
|
+
onClick: () => {
|
|
2380
|
+
toggleGroupCollapse("tag:" + tag.id);
|
|
2381
|
+
},
|
|
2382
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.chevron + (collapsed ? " " + panel_module_css_default.chevronCollapsed : "") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2383
|
+
className: panel_module_css_default.groupTitle,
|
|
2384
|
+
children: [
|
|
2385
|
+
tag.name,
|
|
2386
|
+
" · ",
|
|
2387
|
+
tag.skillNames.length,
|
|
2388
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupSummary, {
|
|
2389
|
+
members: tag.skillNames,
|
|
2390
|
+
hub
|
|
2391
|
+
})
|
|
2392
|
+
]
|
|
2393
|
+
})]
|
|
2394
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2395
|
+
className: panel_module_css_default.groupOps,
|
|
2396
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2397
|
+
type: "button",
|
|
2398
|
+
role: "switch",
|
|
2399
|
+
"aria-checked": view.state !== "off",
|
|
2400
|
+
"aria-label": tag.name,
|
|
2401
|
+
className: panel_module_css_default.switch + (view.state === "on" ? " " + panel_module_css_default.switchOn : view.state === "mixed" ? " " + panel_module_css_default.switchMixed : ""),
|
|
2402
|
+
disabled: batchBusy || tag.skillNames.length === 0 || view.state !== "off" && !hasWritable,
|
|
2403
|
+
title: view.state !== "off" && !hasWritable ? tt("groups.noWritable") : void 0,
|
|
2404
|
+
onClick: (event) => {
|
|
2405
|
+
event.stopPropagation();
|
|
2406
|
+
toggleGroup("tag:" + tag.id, tag.name, view.state);
|
|
2407
|
+
},
|
|
2408
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: panel_module_css_default.switchThumb })
|
|
2409
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2410
|
+
type: "button",
|
|
2411
|
+
className: panel_module_css_default.opBtn,
|
|
2412
|
+
onClick: () => {
|
|
2413
|
+
setEditingTag(tag);
|
|
2414
|
+
setEditName(tag.name);
|
|
2415
|
+
setMembersDraft(new Set(tag.skillNames));
|
|
2416
|
+
setEditSearch("");
|
|
2417
|
+
},
|
|
2418
|
+
children: tt("groups.edit")
|
|
2419
|
+
})]
|
|
2420
|
+
})]
|
|
2421
|
+
}), !collapsed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [skills.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
|
|
2422
|
+
skill,
|
|
2423
|
+
hub
|
|
2424
|
+
}, skill.name)), disabledMembers.map((record) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DisabledRow, {
|
|
2425
|
+
record,
|
|
2426
|
+
busy: busyNames.has(record.name),
|
|
2427
|
+
onEnable: () => {
|
|
2428
|
+
enableDisabled(record);
|
|
2429
|
+
}
|
|
2430
|
+
}, record.name))] }) : null]
|
|
2431
|
+
}, "tag:" + tag.id);
|
|
2432
|
+
})
|
|
2433
|
+
] });
|
|
2434
|
+
}
|
|
2435
|
+
//#endregion
|
|
2436
|
+
//#region src/client/market-catalog.ts
|
|
2437
|
+
/** 默认市场目录:精选技能仓库(顺序即展示顺序)。 */
|
|
2438
|
+
const MARKET_CATALOG = [
|
|
2439
|
+
{
|
|
2440
|
+
repo: "anthropics/skills",
|
|
2441
|
+
descriptionKey: "market.catalog.anthropics"
|
|
2442
|
+
},
|
|
2443
|
+
{
|
|
2444
|
+
repo: "obra/superpowers",
|
|
2445
|
+
descriptionKey: "market.catalog.superpowers"
|
|
2446
|
+
},
|
|
2447
|
+
{
|
|
2448
|
+
repo: "mattpocock/skills",
|
|
2449
|
+
descriptionKey: "market.catalog.mattpocock"
|
|
2450
|
+
},
|
|
2451
|
+
{
|
|
2452
|
+
repo: "nexu-io/open-design",
|
|
2453
|
+
descriptionKey: "market.catalog.openDesign"
|
|
2454
|
+
}
|
|
2455
|
+
];
|
|
2456
|
+
//#endregion
|
|
2457
|
+
//#region src/client/panel/dialogs.tsx
|
|
2458
|
+
/**
|
|
2459
|
+
* Shared dialog family for the panel: one overlay shell plus the specific
|
|
2460
|
+
* dialogs (group-close conflict, sync/delete confirm, branch picker, and
|
|
2461
|
+
* market sync selection). Every dialog closes on outside click and keeps
|
|
2462
|
+
* the same role/aria shell; the panel owns all dialog state.
|
|
2463
|
+
*/
|
|
2464
|
+
/** Overlay + centered alert-dialog shell; outside click or Escape cancels. */
|
|
2465
|
+
function DialogShell(props) {
|
|
2466
|
+
(0, react.useEffect)(() => {
|
|
2467
|
+
const onKey = (event) => {
|
|
2468
|
+
if (event.key === "Escape") props.onClose();
|
|
2469
|
+
};
|
|
2470
|
+
window.addEventListener("keydown", onKey);
|
|
2471
|
+
return () => {
|
|
2472
|
+
window.removeEventListener("keydown", onKey);
|
|
2473
|
+
};
|
|
2474
|
+
});
|
|
2475
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2476
|
+
className: panel_module_css_default.dialogOverlay,
|
|
2477
|
+
onClick: props.onClose,
|
|
2478
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2479
|
+
className: panel_module_css_default.dialog,
|
|
2480
|
+
role: "alertdialog",
|
|
2481
|
+
"aria-modal": "true",
|
|
2482
|
+
onClick: (event) => {
|
|
2483
|
+
event.stopPropagation();
|
|
2484
|
+
},
|
|
2485
|
+
children: props.children
|
|
2486
|
+
})
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
/** Simple confirm dialog: title + text + optional skill list + cancel/confirm. */
|
|
2490
|
+
function ConfirmDialog(props) {
|
|
2491
|
+
const { title, text, items, confirmLabel, danger, onCancel, onConfirm } = props;
|
|
2492
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogShell, {
|
|
2493
|
+
onClose: onCancel,
|
|
2494
|
+
children: [
|
|
2495
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
2496
|
+
className: panel_module_css_default.dialogTitle,
|
|
2497
|
+
children: title
|
|
2498
|
+
}),
|
|
2499
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2500
|
+
className: panel_module_css_default.dialogText,
|
|
2501
|
+
children: text
|
|
2502
|
+
}),
|
|
2503
|
+
items !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
2504
|
+
className: panel_module_css_default.dialogList,
|
|
2505
|
+
children: items.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2506
|
+
className: panel_module_css_default.rowNameText,
|
|
2507
|
+
children: name
|
|
2508
|
+
}) }, name))
|
|
2509
|
+
}) : null,
|
|
2510
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2511
|
+
className: panel_module_css_default.dialogActions,
|
|
2512
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2513
|
+
type: "button",
|
|
2514
|
+
className: panel_module_css_default.button,
|
|
2515
|
+
onClick: onCancel,
|
|
2516
|
+
children: tt("form.cancel")
|
|
2517
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2518
|
+
type: "button",
|
|
2519
|
+
className: panel_module_css_default.button + (danger === true ? " " + panel_module_css_default.danger : " " + panel_module_css_default.primary),
|
|
2520
|
+
onClick: onConfirm,
|
|
2521
|
+
children: confirmLabel
|
|
2522
|
+
})]
|
|
2523
|
+
})
|
|
2524
|
+
]
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
/** Group-close conflict: which skills stay on, and which other groups also enable them. */
|
|
2528
|
+
function ConflictDialog(props) {
|
|
2529
|
+
const { dialog, tags, collections, onClose, onKeepOn, onCloseAll } = props;
|
|
2530
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogShell, {
|
|
2531
|
+
onClose,
|
|
2532
|
+
children: [
|
|
2533
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
2534
|
+
className: panel_module_css_default.dialogTitle,
|
|
2535
|
+
children: tt("groups.conflictTitle")
|
|
2536
|
+
}),
|
|
2537
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2538
|
+
className: panel_module_css_default.dialogText,
|
|
2539
|
+
children: tt("groups.conflictText")
|
|
2540
|
+
}),
|
|
2541
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
2542
|
+
className: panel_module_css_default.dialogList,
|
|
2543
|
+
children: dialog.conflicts.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2544
|
+
className: panel_module_css_default.rowNameText,
|
|
2545
|
+
children: name
|
|
2546
|
+
}), " — " + groupNamesOf(name, [...tags], [...collections]).join(", ")] }, name))
|
|
2547
|
+
}),
|
|
2548
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2549
|
+
className: panel_module_css_default.dialogActions,
|
|
2550
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2551
|
+
type: "button",
|
|
2552
|
+
className: panel_module_css_default.button,
|
|
2553
|
+
onClick: onKeepOn,
|
|
2554
|
+
children: tt("groups.keepOn")
|
|
2555
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2556
|
+
type: "button",
|
|
2557
|
+
className: panel_module_css_default.button + " " + panel_module_css_default.primary,
|
|
2558
|
+
onClick: onCloseAll,
|
|
2559
|
+
children: tt("groups.closeAll")
|
|
2560
|
+
})]
|
|
2561
|
+
})
|
|
2562
|
+
]
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
/** Branch picker for a repo without releases (pins the ref, then rescans). */
|
|
2566
|
+
function BranchChoiceDialog(props) {
|
|
2567
|
+
const { choice, busy, onSelect, onCancel, onConfirm } = props;
|
|
2568
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogShell, {
|
|
2569
|
+
onClose: onCancel,
|
|
2570
|
+
children: [
|
|
2571
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
2572
|
+
className: panel_module_css_default.dialogTitle,
|
|
2573
|
+
children: tt("market.branchTitle")
|
|
2574
|
+
}),
|
|
2575
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2576
|
+
className: panel_module_css_default.dialogText,
|
|
2577
|
+
children: tt("market.branchHint")
|
|
2578
|
+
}),
|
|
2579
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
2580
|
+
className: panel_module_css_default.select + " " + panel_module_css_default.dialogSelect,
|
|
2581
|
+
value: choice.selected,
|
|
2582
|
+
onChange: (event) => {
|
|
2583
|
+
onSelect(event.target.value);
|
|
2584
|
+
},
|
|
2585
|
+
children: choice.branches.map((branch) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2586
|
+
value: branch,
|
|
2587
|
+
children: branch
|
|
2588
|
+
}, branch))
|
|
2589
|
+
}),
|
|
2590
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2591
|
+
className: panel_module_css_default.dialogActions,
|
|
2592
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2593
|
+
type: "button",
|
|
2594
|
+
className: panel_module_css_default.button,
|
|
2595
|
+
onClick: onCancel,
|
|
2596
|
+
children: tt("form.cancel")
|
|
2597
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2598
|
+
type: "button",
|
|
2599
|
+
className: panel_module_css_default.button + " " + panel_module_css_default.primary,
|
|
2600
|
+
disabled: busy,
|
|
2601
|
+
onClick: onConfirm,
|
|
2602
|
+
children: tt("market.branchConfirm")
|
|
2603
|
+
})]
|
|
2604
|
+
})
|
|
2605
|
+
]
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
/** Post-sync selection: which tracked skills to batch-update to the new version. */
|
|
2609
|
+
function MarketSyncDialog(props) {
|
|
2610
|
+
const { dialog, busy, onToggle, onCancel, onConfirm } = props;
|
|
2611
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogShell, {
|
|
2612
|
+
onClose: onCancel,
|
|
2613
|
+
children: [
|
|
2614
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
2615
|
+
className: panel_module_css_default.dialogTitle,
|
|
2616
|
+
children: tt("market.syncTitle")
|
|
2617
|
+
}),
|
|
2618
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2619
|
+
className: panel_module_css_default.dialogText,
|
|
2620
|
+
children: tt("market.syncText", { ref: dialog.ref })
|
|
2621
|
+
}),
|
|
2622
|
+
dialog.skills.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2623
|
+
className: panel_module_css_default.dialogText,
|
|
2624
|
+
children: tt("market.syncNone")
|
|
2625
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2626
|
+
className: panel_module_css_default.dialogList,
|
|
2627
|
+
children: dialog.skills.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2628
|
+
className: panel_module_css_default.dialogRow,
|
|
2629
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2630
|
+
type: "checkbox",
|
|
2631
|
+
checked: dialog.selected.has(name),
|
|
2632
|
+
onChange: (event) => {
|
|
2633
|
+
onToggle(name, event.target.checked);
|
|
2634
|
+
}
|
|
2635
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2636
|
+
className: panel_module_css_default.rowNameText,
|
|
2637
|
+
children: name
|
|
2638
|
+
})]
|
|
2639
|
+
}, name))
|
|
2640
|
+
}),
|
|
2641
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2642
|
+
className: panel_module_css_default.dialogActions,
|
|
2643
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2644
|
+
type: "button",
|
|
2645
|
+
className: panel_module_css_default.button,
|
|
2646
|
+
onClick: onCancel,
|
|
2647
|
+
children: tt("market.syncCancel")
|
|
2648
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2649
|
+
type: "button",
|
|
2650
|
+
className: panel_module_css_default.button + " " + panel_module_css_default.primary,
|
|
2651
|
+
disabled: busy || dialog.skills.length === 0,
|
|
2652
|
+
onClick: onConfirm,
|
|
2653
|
+
children: busy ? tt("source.syncing") : tt("source.sync")
|
|
2654
|
+
})]
|
|
2655
|
+
})
|
|
2656
|
+
]
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
//#endregion
|
|
2660
|
+
//#region src/client/panel/MarketView.tsx
|
|
2661
|
+
/** Compact byte size for repo preview rows. */
|
|
2662
|
+
function formatBytes(bytes) {
|
|
2663
|
+
if (bytes < 1024) return bytes + " B";
|
|
2664
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
|
2665
|
+
return (bytes / 1024 / 1024).toFixed(1) + " MB";
|
|
2666
|
+
}
|
|
2667
|
+
function MarketView(props) {
|
|
2668
|
+
const { hub } = props;
|
|
2669
|
+
const { marketState, marketCheck, sourceCheck, sourcesState, repoDiscoverState, scanningRepo, repoSelected, setRepoSelected, repoImporting, repoResult, newSourceName, setNewSourceName, syncingMarket, tagBusy, addSource, addMarketSource, removeMarketSource, scanRepo, checkMarket, checkSources, syncMarketSource, toggleRepoSelected, importRepo, updateAllDialog, setUpdateAllDialog, updateAll } = hub;
|
|
2670
|
+
/** 有可更新技能的来源(全部更新按钮与确认列表共用)。 */
|
|
2671
|
+
const updatableRepos = Object.entries(sourceCheck).filter(([, check]) => check.changed && check.updated.length > 0);
|
|
2672
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2673
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2674
|
+
className: panel_module_css_default.section,
|
|
2675
|
+
children: [
|
|
2676
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2677
|
+
className: panel_module_css_default.sectionTitle,
|
|
2678
|
+
children: tt("market.catalogTitle")
|
|
2679
|
+
}),
|
|
2680
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2681
|
+
className: panel_module_css_default.hintLine + " " + panel_module_css_default.hintPadded,
|
|
2682
|
+
children: tt("market.catalogHint")
|
|
2683
|
+
}),
|
|
2684
|
+
MARKET_CATALOG.map((entry) => {
|
|
2685
|
+
const added = marketState.repos.some((record) => record.repo === entry.repo);
|
|
2686
|
+
const busy = scanningRepo !== null || tagBusy;
|
|
2687
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2688
|
+
className: panel_module_css_default.row + " " + panel_module_css_default.rowStatic,
|
|
2689
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2690
|
+
className: panel_module_css_default.rowMain,
|
|
2691
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2692
|
+
className: panel_module_css_default.rowName,
|
|
2693
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2694
|
+
className: panel_module_css_default.sourceLink,
|
|
2695
|
+
href: "https://github.com/" + entry.repo,
|
|
2696
|
+
target: "_blank",
|
|
2697
|
+
rel: "noreferrer",
|
|
2698
|
+
children: entry.repo
|
|
2699
|
+
})
|
|
2700
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2701
|
+
className: panel_module_css_default.rowDesc,
|
|
2702
|
+
children: tt(entry.descriptionKey)
|
|
2703
|
+
})]
|
|
2704
|
+
}), added ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2705
|
+
className: panel_module_css_default.badge + " " + panel_module_css_default.badgeReadonly,
|
|
2706
|
+
children: tt("market.added")
|
|
2707
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2708
|
+
type: "button",
|
|
2709
|
+
className: panel_module_css_default.opBtn,
|
|
2710
|
+
disabled: busy,
|
|
2711
|
+
onClick: () => {
|
|
2712
|
+
addSource(entry.repo);
|
|
2713
|
+
},
|
|
2714
|
+
children: tt("market.add")
|
|
2715
|
+
})]
|
|
2716
|
+
}, entry.repo);
|
|
2717
|
+
})
|
|
2718
|
+
]
|
|
2719
|
+
}),
|
|
2720
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2721
|
+
className: panel_module_css_default.section,
|
|
2722
|
+
children: [
|
|
2723
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2724
|
+
className: panel_module_css_default.sectionTitle + " " + panel_module_css_default.sectionHeadRow,
|
|
2725
|
+
children: [
|
|
2726
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2727
|
+
className: panel_module_css_default.sectionTitleFill,
|
|
2728
|
+
children: tt("market.mySources")
|
|
2729
|
+
}),
|
|
2730
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2731
|
+
type: "button",
|
|
2732
|
+
className: panel_module_css_default.opBtn,
|
|
2733
|
+
onClick: () => {
|
|
2734
|
+
checkSources();
|
|
2735
|
+
checkMarket();
|
|
2736
|
+
},
|
|
2737
|
+
children: tt("market.checkAll")
|
|
2738
|
+
}),
|
|
2739
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2740
|
+
type: "button",
|
|
2741
|
+
className: panel_module_css_default.opBtn + (updatableRepos.length > 0 ? " " + panel_module_css_default.opDanger : ""),
|
|
2742
|
+
disabled: updatableRepos.length === 0 || tagBusy,
|
|
2743
|
+
onClick: () => {
|
|
2744
|
+
setUpdateAllDialog(true);
|
|
2745
|
+
},
|
|
2746
|
+
children: tt("market.updateAll")
|
|
2747
|
+
})
|
|
2748
|
+
]
|
|
2749
|
+
}),
|
|
2750
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2751
|
+
className: panel_module_css_default.hintLine + " " + panel_module_css_default.hintPadded,
|
|
2752
|
+
children: tt("market.addHint")
|
|
2753
|
+
}),
|
|
2754
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2755
|
+
className: panel_module_css_default.buttons + " " + panel_module_css_default.actionsPadded,
|
|
2756
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2757
|
+
className: panel_module_css_default.input + " " + panel_module_css_default.grow,
|
|
2758
|
+
value: newSourceName,
|
|
2759
|
+
onChange: (event) => {
|
|
2760
|
+
setNewSourceName(event.target.value);
|
|
2761
|
+
},
|
|
2762
|
+
onKeyDown: (event) => {
|
|
2763
|
+
if (event.key === "Enter") {
|
|
2764
|
+
event.preventDefault();
|
|
2765
|
+
addMarketSource();
|
|
2766
|
+
}
|
|
2767
|
+
},
|
|
2768
|
+
placeholder: tt("market.addPlaceholder")
|
|
2769
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2770
|
+
type: "button",
|
|
2771
|
+
className: panel_module_css_default.button + " " + panel_module_css_default.primary,
|
|
2772
|
+
disabled: newSourceName.trim() === "",
|
|
2773
|
+
onClick: () => {
|
|
2774
|
+
addMarketSource();
|
|
2775
|
+
},
|
|
2776
|
+
children: tt("market.addSource")
|
|
2777
|
+
})]
|
|
2778
|
+
}),
|
|
2779
|
+
marketState.status === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2780
|
+
className: panel_module_css_default.empty,
|
|
2781
|
+
children: tt("panel.loading")
|
|
2782
|
+
}) : null,
|
|
2783
|
+
marketState.status === "ready" && marketState.repos.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2784
|
+
className: panel_module_css_default.empty,
|
|
2785
|
+
children: tt("market.noSources")
|
|
2786
|
+
}) : null,
|
|
2787
|
+
marketState.repos.map((record) => {
|
|
2788
|
+
const releaseCheck = marketCheck[record.repo];
|
|
2789
|
+
const skillCheck = sourceCheck[record.repo];
|
|
2790
|
+
const installedCount = sourcesState?.sources.find((source) => source.repo === record.repo)?.skills.length ?? 0;
|
|
2791
|
+
const scanning = scanningRepo === record.repo;
|
|
2792
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2793
|
+
className: panel_module_css_default.row + " " + panel_module_css_default.rowStatic,
|
|
2794
|
+
children: [
|
|
2795
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2796
|
+
className: panel_module_css_default.rowMain,
|
|
2797
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2798
|
+
className: panel_module_css_default.rowName,
|
|
2799
|
+
children: [
|
|
2800
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2801
|
+
className: panel_module_css_default.sourceLink,
|
|
2802
|
+
href: "https://github.com/" + record.repo,
|
|
2803
|
+
target: "_blank",
|
|
2804
|
+
rel: "noreferrer",
|
|
2805
|
+
children: record.repo
|
|
2806
|
+
}),
|
|
2807
|
+
record.ref !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2808
|
+
className: panel_module_css_default.badge + " " + panel_module_css_default.badgeSource,
|
|
2809
|
+
children: record.ref
|
|
2810
|
+
}) : null,
|
|
2811
|
+
installedCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2812
|
+
className: panel_module_css_default.badge + " " + panel_module_css_default.badgeCount,
|
|
2813
|
+
children: tt("market.installed", { count: installedCount })
|
|
2814
|
+
}) : null,
|
|
2815
|
+
skillCheck?.changed === true && skillCheck.updated.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2816
|
+
className: panel_module_css_default.badge + " " + panel_module_css_default.statusUpdated,
|
|
2817
|
+
children: tt("market.updatable", { count: skillCheck.updated.length })
|
|
2818
|
+
}) : null,
|
|
2819
|
+
skillCheck !== void 0 && skillCheck.deleted.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2820
|
+
className: panel_module_css_default.badge + " " + panel_module_css_default.statusError,
|
|
2821
|
+
children: tt("market.deletedUpstream", { count: skillCheck.deleted.length })
|
|
2822
|
+
}) : null,
|
|
2823
|
+
releaseCheck?.updateAvailable === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2824
|
+
className: panel_module_css_default.badge + " " + panel_module_css_default.statusUpdated,
|
|
2825
|
+
children: releaseCheck.latestTag !== void 0 ? tt("market.newRelease", { version: releaseCheck.latestTag }) : tt("market.updated")
|
|
2826
|
+
}) : null
|
|
2827
|
+
]
|
|
2828
|
+
})
|
|
2829
|
+
}),
|
|
2830
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2831
|
+
type: "button",
|
|
2832
|
+
className: panel_module_css_default.opBtn,
|
|
2833
|
+
disabled: scanning,
|
|
2834
|
+
onClick: () => {
|
|
2835
|
+
checkMarket();
|
|
2836
|
+
},
|
|
2837
|
+
children: tt("market.check")
|
|
2838
|
+
}),
|
|
2839
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2840
|
+
type: "button",
|
|
2841
|
+
className: panel_module_css_default.opBtn + (releaseCheck?.updateAvailable === true ? " " + panel_module_css_default.opDanger : ""),
|
|
2842
|
+
disabled: syncingMarket === record.repo,
|
|
2843
|
+
onClick: () => {
|
|
2844
|
+
syncMarketSource(record.repo);
|
|
2845
|
+
},
|
|
2846
|
+
children: syncingMarket === record.repo ? tt("market.syncing") : tt("market.sync")
|
|
2847
|
+
}),
|
|
2848
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2849
|
+
type: "button",
|
|
2850
|
+
className: panel_module_css_default.opBtn,
|
|
2851
|
+
disabled: scanning,
|
|
2852
|
+
onClick: () => {
|
|
2853
|
+
scanRepo(record.repo);
|
|
2854
|
+
},
|
|
2855
|
+
children: scanning ? tt("market.scanning") : tt("market.scan")
|
|
2856
|
+
}),
|
|
2857
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2858
|
+
type: "button",
|
|
2859
|
+
className: panel_module_css_default.opBtn,
|
|
2860
|
+
disabled: tagBusy,
|
|
2861
|
+
title: tt("market.removeHint"),
|
|
2862
|
+
onClick: () => {
|
|
2863
|
+
removeMarketSource(record.repo);
|
|
2864
|
+
},
|
|
2865
|
+
children: tt("market.deleteSource")
|
|
2866
|
+
})
|
|
2867
|
+
]
|
|
2868
|
+
}, record.repo);
|
|
2869
|
+
})
|
|
2870
|
+
]
|
|
2871
|
+
}),
|
|
2872
|
+
repoDiscoverState.status === "scanning" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2873
|
+
className: panel_module_css_default.empty,
|
|
2874
|
+
children: tt("market.scanning")
|
|
2875
|
+
}) : null,
|
|
2876
|
+
repoDiscoverState.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2877
|
+
className: panel_module_css_default.errorBanner,
|
|
2878
|
+
children: repoDiscoverState.message
|
|
2879
|
+
}) : null,
|
|
2880
|
+
repoDiscoverState.status === "ready" ? (() => {
|
|
2881
|
+
const entries = repoDiscoverState.data.entries;
|
|
2882
|
+
const selected = entries.filter((entry) => repoSelected.has(entry.path) && !entry.existing);
|
|
2883
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2884
|
+
className: panel_module_css_default.hintLine + " " + panel_module_css_default.hintInline,
|
|
2885
|
+
children: tt("repo.ready", { count: entries.length })
|
|
2886
|
+
}), entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2887
|
+
className: panel_module_css_default.empty,
|
|
2888
|
+
children: tt("repo.empty")
|
|
2889
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2890
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2891
|
+
className: panel_module_css_default.buttons + " " + panel_module_css_default.actionsBottom,
|
|
2892
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2893
|
+
type: "button",
|
|
2894
|
+
className: panel_module_css_default.button,
|
|
2895
|
+
onClick: () => {
|
|
2896
|
+
setRepoSelected(new Set(entries.filter((entry) => !entry.existing).map((entry) => entry.path)));
|
|
2897
|
+
},
|
|
2898
|
+
children: tt("repo.selectAll")
|
|
2899
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2900
|
+
type: "button",
|
|
2901
|
+
className: panel_module_css_default.button,
|
|
2902
|
+
onClick: () => {
|
|
2903
|
+
setRepoSelected(/* @__PURE__ */ new Set());
|
|
2904
|
+
},
|
|
2905
|
+
children: tt("repo.clearAll")
|
|
2906
|
+
})]
|
|
2907
|
+
}),
|
|
2908
|
+
["skills", "design-templates"].map((root) => {
|
|
2909
|
+
const rootEntries = entries.filter((entry) => entry.root === root);
|
|
2910
|
+
if (rootEntries.length === 0) return null;
|
|
2911
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2912
|
+
className: panel_module_css_default.section,
|
|
2913
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2914
|
+
className: panel_module_css_default.sectionTitle,
|
|
2915
|
+
children: root === "skills" ? tt("repo.root.skills") : tt("repo.root.designTemplates")
|
|
2916
|
+
}), rootEntries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2917
|
+
className: panel_module_css_default.row + (entry.existing ? " " + panel_module_css_default.rowMuted : ""),
|
|
2918
|
+
children: [
|
|
2919
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2920
|
+
type: "checkbox",
|
|
2921
|
+
checked: repoSelected.has(entry.path),
|
|
2922
|
+
disabled: entry.existing,
|
|
2923
|
+
onChange: (event) => {
|
|
2924
|
+
toggleRepoSelected(entry.path, event.target.checked);
|
|
2925
|
+
}
|
|
2926
|
+
}),
|
|
2927
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2928
|
+
className: panel_module_css_default.rowMain,
|
|
2929
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2930
|
+
className: panel_module_css_default.rowName,
|
|
2931
|
+
children: entry.name
|
|
2932
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2933
|
+
className: panel_module_css_default.rowDesc,
|
|
2934
|
+
children: [
|
|
2935
|
+
entry.dir,
|
|
2936
|
+
" · ",
|
|
2937
|
+
tt("repo.files", {
|
|
2938
|
+
count: entry.fileCount,
|
|
2939
|
+
size: formatBytes(entry.totalBytes)
|
|
2940
|
+
})
|
|
2941
|
+
]
|
|
2942
|
+
})]
|
|
2943
|
+
}),
|
|
2944
|
+
entry.existing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2945
|
+
className: panel_module_css_default.badge + " " + panel_module_css_default.badgeReadonly,
|
|
2946
|
+
children: tt("repo.existing")
|
|
2947
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2948
|
+
className: panel_module_css_default.badge + " " + panel_module_css_default.badgeSource,
|
|
2949
|
+
children: entry.origin
|
|
2950
|
+
})
|
|
2951
|
+
]
|
|
2952
|
+
}, entry.path))]
|
|
2953
|
+
}, root);
|
|
2954
|
+
}),
|
|
2955
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2956
|
+
type: "button",
|
|
2957
|
+
className: panel_module_css_default.button + " " + panel_module_css_default.primary,
|
|
2958
|
+
disabled: selected.length === 0 || repoImporting,
|
|
2959
|
+
onClick: () => {
|
|
2960
|
+
importRepo();
|
|
2961
|
+
},
|
|
2962
|
+
children: repoImporting ? tt("repo.importing") : tt("repo.import", { count: selected.length })
|
|
2963
|
+
})
|
|
2964
|
+
] })] });
|
|
2965
|
+
})() : null,
|
|
2966
|
+
repoResult !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2967
|
+
className: panel_module_css_default.formSuccess + " " + panel_module_css_default.actionsTop,
|
|
2968
|
+
children: [
|
|
2969
|
+
tt("repo.imported", { count: repoResult.imported.length }),
|
|
2970
|
+
" · ",
|
|
2971
|
+
tt("repo.skippedExisting", { count: repoResult.skipped.length }),
|
|
2972
|
+
" · ",
|
|
2973
|
+
tt("repo.failed", { count: repoResult.failed.length })
|
|
2974
|
+
]
|
|
2975
|
+
}) : null,
|
|
2976
|
+
updateAllDialog ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
|
|
2977
|
+
title: tt("market.updateAllConfirmTitle"),
|
|
2978
|
+
text: tt("market.updateAllConfirmText"),
|
|
2979
|
+
items: updatableRepos.map(([repo, check]) => tt("market.updateAllItem", {
|
|
2980
|
+
repo,
|
|
2981
|
+
count: check.updated.length
|
|
2982
|
+
})),
|
|
2983
|
+
confirmLabel: tt("market.updateAll"),
|
|
2984
|
+
danger: true,
|
|
2985
|
+
onCancel: () => {
|
|
2986
|
+
setUpdateAllDialog(false);
|
|
2987
|
+
},
|
|
2988
|
+
onConfirm: () => {
|
|
2989
|
+
updateAll();
|
|
2990
|
+
}
|
|
2991
|
+
}) : null
|
|
2992
|
+
] });
|
|
2993
|
+
}
|
|
2994
|
+
//#endregion
|
|
2995
|
+
//#region src/client/panel/useSkillHub.ts
|
|
2996
|
+
/**
|
|
2997
|
+
* useSkillHub — the panel's state owner. Everything the skill hub panel
|
|
2998
|
+
* knows (catalog, groups, sources, market, dialogs, drafts) plus every
|
|
2999
|
+
* async flow lives here; the panel itself only renders. All hooks run
|
|
3000
|
+
* unconditionally at the top, so the panel may early-return for the detail
|
|
3001
|
+
* and tag-editor views without violating the rules of hooks.
|
|
3002
|
+
*/
|
|
3003
|
+
/** Catalog poll interval while the panel is mounted (the provider watcher feeds this). */
|
|
3004
|
+
const POLL_MS = 5e3;
|
|
3005
|
+
/** Relaxed poll for slower-changing data (stats, groups, sources, config). */
|
|
3006
|
+
const SLOW_POLL_MS = 6e4;
|
|
3007
|
+
/** localStorage key of the last daily auto-check timestamp (survives sessions). */
|
|
3008
|
+
const AUTO_CHECK_KEY = "skill-hub.last-auto-check";
|
|
3009
|
+
/** Daily auto-check interval: at most one network round per day. */
|
|
3010
|
+
const AUTO_CHECK_INTERVAL_MS = 1440 * 6e4;
|
|
3011
|
+
/** Whether the daily auto-check should run now (first visit or ≥24h since the last). */
|
|
3012
|
+
function shouldAutoCheck() {
|
|
3013
|
+
try {
|
|
3014
|
+
const raw = localStorage.getItem(AUTO_CHECK_KEY);
|
|
3015
|
+
const last = raw === null ? NaN : Number(raw);
|
|
3016
|
+
if (!Number.isFinite(last)) return true;
|
|
3017
|
+
return Date.now() - last >= AUTO_CHECK_INTERVAL_MS;
|
|
3018
|
+
} catch {
|
|
3019
|
+
return true;
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
/** Record that the daily auto-check ran. */
|
|
3023
|
+
function markAutoChecked() {
|
|
3024
|
+
try {
|
|
3025
|
+
localStorage.setItem(AUTO_CHECK_KEY, String(Date.now()));
|
|
3026
|
+
} catch {}
|
|
3027
|
+
}
|
|
3028
|
+
function useSkillHub(api) {
|
|
3029
|
+
const [catalog, setCatalog] = (0, react.useState)(null);
|
|
3030
|
+
const [loading, setLoading] = (0, react.useState)(true);
|
|
3031
|
+
const [loadError, setLoadError] = (0, react.useState)(null);
|
|
3032
|
+
/** Green success banner (create finished); shown outside the closing form. */
|
|
3033
|
+
const [successBanner, setSuccessBanner] = (0, react.useState)(null);
|
|
3034
|
+
const [updateState, setUpdateState] = (0, react.useState)({ status: "idle" });
|
|
3035
|
+
const [repoDiscoverState, setRepoDiscoverState] = (0, react.useState)({ status: "idle" });
|
|
3036
|
+
/** Market repo currently being scanned (null when idle); per-row busy state. */
|
|
3037
|
+
const [scanningRepo, setScanningRepo] = (0, react.useState)(null);
|
|
3038
|
+
const [repoSelected, setRepoSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
3039
|
+
const [repoImporting, setRepoImporting] = (0, react.useState)(false);
|
|
3040
|
+
const [repoResult, setRepoResult] = (0, react.useState)(null);
|
|
3041
|
+
const [search, setSearch] = (0, react.useState)("");
|
|
3042
|
+
const [detail, setDetail] = (0, react.useState)(null);
|
|
3043
|
+
const [detailLoading, setDetailLoading] = (0, react.useState)(false);
|
|
3044
|
+
const [busyNames, setBusyNames] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
3045
|
+
const [batchBusy, setBatchBusy] = (0, react.useState)(false);
|
|
3046
|
+
const [showForm, setShowForm] = (0, react.useState)(false);
|
|
3047
|
+
const [formName, setFormName] = (0, react.useState)("");
|
|
3048
|
+
const [formDesc, setFormDesc] = (0, react.useState)("");
|
|
3049
|
+
const [formRoot, setFormRoot] = (0, react.useState)("user-dsh");
|
|
3050
|
+
const [formBusy, setFormBusy] = (0, react.useState)(false);
|
|
3051
|
+
const [formMessage, setFormMessage] = (0, react.useState)(null);
|
|
3052
|
+
const [uses, setUses] = (0, react.useState)(/* @__PURE__ */ new Map());
|
|
3053
|
+
const [hubConfig, setHubConfig] = (0, react.useState)(null);
|
|
3054
|
+
const [tab, setTab] = (0, react.useState)("sources");
|
|
3055
|
+
const [skillView, setSkillView] = (0, react.useState)("groups");
|
|
3056
|
+
const [sourceFilter, setSourceFilter] = (0, react.useState)("all");
|
|
3057
|
+
const [sortKey, setSortKey] = (0, react.useState)("name");
|
|
3058
|
+
const [marketState, setMarketState] = (0, react.useState)({
|
|
3059
|
+
status: "loading",
|
|
3060
|
+
repos: []
|
|
3061
|
+
});
|
|
3062
|
+
const [marketCheck, setMarketCheck] = (0, react.useState)({});
|
|
3063
|
+
const [branchChoice, setBranchChoice] = (0, react.useState)(null);
|
|
3064
|
+
const [branchBusy, setBranchBusy] = (0, react.useState)(false);
|
|
3065
|
+
const [marketSyncDialog, setMarketSyncDialog] = (0, react.useState)(null);
|
|
3066
|
+
const [syncingMarket, setSyncingMarket] = (0, react.useState)(null);
|
|
3067
|
+
const [syncBusy, setSyncBusy] = (0, react.useState)(false);
|
|
3068
|
+
const [newSourceName, setNewSourceName] = (0, react.useState)("");
|
|
3069
|
+
const [groupsState, setGroupsState] = (0, react.useState)(null);
|
|
3070
|
+
const [sourcesState, setSourcesState] = (0, react.useState)(null);
|
|
3071
|
+
const [sourceCheck, setSourceCheck] = (0, react.useState)({});
|
|
3072
|
+
const [checkingSource, setCheckingSource] = (0, react.useState)(null);
|
|
3073
|
+
const [syncingSource, setSyncingSource] = (0, react.useState)(null);
|
|
3074
|
+
const [conflictDialog, setConflictDialog] = (0, react.useState)(null);
|
|
3075
|
+
const [confirmDialog, setConfirmDialog] = (0, react.useState)(null);
|
|
3076
|
+
const [deleteSkillDialog, setDeleteSkillDialog] = (0, react.useState)(null);
|
|
3077
|
+
const [confirmClearTrash, setConfirmClearTrash] = (0, react.useState)(false);
|
|
3078
|
+
/** 「全部更新」确认对话框(市场 tab)。 */
|
|
3079
|
+
const [updateAllDialog, setUpdateAllDialog] = (0, react.useState)(false);
|
|
3080
|
+
const [editingTag, setEditingTag] = (0, react.useState)(null);
|
|
3081
|
+
const [editName, setEditName] = (0, react.useState)("");
|
|
3082
|
+
const [membersDraft, setMembersDraft] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
3083
|
+
const [newTagName, setNewTagName] = (0, react.useState)("");
|
|
3084
|
+
const [tagBusy, setTagBusy] = (0, react.useState)(false);
|
|
3085
|
+
const [editSearch, setEditSearch] = (0, react.useState)("");
|
|
3086
|
+
/** 分组视图里收起的分组(key 为 tag:<id> 或 col:<name>)。 */
|
|
3087
|
+
const [collapsedGroups, setCollapsedGroups] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
3088
|
+
const [showLegend, setShowLegend] = (0, react.useState)(false);
|
|
3089
|
+
const toggleGroupCollapse = (0, react.useCallback)((key) => {
|
|
3090
|
+
setCollapsedGroups((previous) => {
|
|
3091
|
+
const next = new Set(previous);
|
|
3092
|
+
if (next.has(key)) next.delete(key);
|
|
3093
|
+
else next.add(key);
|
|
3094
|
+
return next;
|
|
3095
|
+
});
|
|
3096
|
+
}, []);
|
|
3097
|
+
const load = (0, react.useCallback)(async () => {
|
|
3098
|
+
try {
|
|
3099
|
+
const next = await api.catalog();
|
|
3100
|
+
setCatalog(next);
|
|
3101
|
+
setLoadError(null);
|
|
3102
|
+
} catch (error) {
|
|
3103
|
+
setLoadError(errorMessage(error));
|
|
3104
|
+
} finally {
|
|
3105
|
+
setLoading(false);
|
|
3106
|
+
}
|
|
3107
|
+
}, [api]);
|
|
3108
|
+
const loadUses = (0, react.useCallback)(async () => {
|
|
3109
|
+
try {
|
|
3110
|
+
const result = await api.stats();
|
|
3111
|
+
if (result.available) setUses(new Map(result.stats.map((stat) => [stat.name, {
|
|
3112
|
+
count: stat.count,
|
|
3113
|
+
...stat.lastUsed !== void 0 ? { lastUsed: stat.lastUsed } : {}
|
|
3114
|
+
}])));
|
|
3115
|
+
} catch {}
|
|
3116
|
+
}, [api]);
|
|
3117
|
+
const loadConfig = (0, react.useCallback)(async () => {
|
|
3118
|
+
try {
|
|
3119
|
+
setHubConfig((await api.config()).config);
|
|
3120
|
+
} catch {}
|
|
3121
|
+
}, [api]);
|
|
3122
|
+
/** Check the hub's own GitHub release once, and again on demand. */
|
|
3123
|
+
const checkUpdate = (0, react.useCallback)(async () => {
|
|
3124
|
+
setUpdateState({ status: "checking" });
|
|
3125
|
+
try {
|
|
3126
|
+
setUpdateState({
|
|
3127
|
+
status: "ready",
|
|
3128
|
+
data: await api.updateCheck()
|
|
3129
|
+
});
|
|
3130
|
+
} catch (error) {
|
|
3131
|
+
setUpdateState({
|
|
3132
|
+
status: "error",
|
|
3133
|
+
message: errorMessage(error)
|
|
3134
|
+
});
|
|
3135
|
+
}
|
|
3136
|
+
}, [api]);
|
|
3137
|
+
/** 加载用户 tag 分组 + 系统集合组 + origin 映射。 */
|
|
3138
|
+
const loadGroups = (0, react.useCallback)(async () => {
|
|
3139
|
+
try {
|
|
3140
|
+
setGroupsState(await api.groups());
|
|
3141
|
+
} catch (error) {
|
|
3142
|
+
setLoadError(errorMessage(error));
|
|
3143
|
+
}
|
|
3144
|
+
}, [api]);
|
|
3145
|
+
/** 加载来源记录 + 回收站。 */
|
|
3146
|
+
const loadSources = (0, react.useCallback)(async () => {
|
|
3147
|
+
try {
|
|
3148
|
+
setSourcesState(await api.sources());
|
|
3149
|
+
} catch (error) {
|
|
3150
|
+
setLoadError(errorMessage(error));
|
|
3151
|
+
}
|
|
3152
|
+
}, [api]);
|
|
3153
|
+
/** 加载市场源列表。 */
|
|
3154
|
+
const loadMarket = (0, react.useCallback)(async () => {
|
|
3155
|
+
try {
|
|
3156
|
+
const next = await api.market();
|
|
3157
|
+
setMarketState({
|
|
3158
|
+
status: "ready",
|
|
3159
|
+
repos: next.repos
|
|
3160
|
+
});
|
|
3161
|
+
} catch (error) {
|
|
3162
|
+
setMarketState({
|
|
3163
|
+
status: "error",
|
|
3164
|
+
repos: []
|
|
3165
|
+
});
|
|
3166
|
+
setLoadError(errorMessage(error));
|
|
3167
|
+
}
|
|
3168
|
+
}, [api]);
|
|
3169
|
+
/** 把最新的 tag 列表合并进 groups 状态(tags 以外的字段保持原样)。 */
|
|
3170
|
+
const applyTags = (0, react.useCallback)((tags) => {
|
|
3171
|
+
setGroupsState((previous) => previous === null ? {
|
|
3172
|
+
ok: true,
|
|
3173
|
+
tags,
|
|
3174
|
+
collections: [],
|
|
3175
|
+
origins: {}
|
|
3176
|
+
} : {
|
|
3177
|
+
...previous,
|
|
3178
|
+
tags
|
|
3179
|
+
});
|
|
3180
|
+
}, []);
|
|
3181
|
+
(0, react.useEffect)(() => {
|
|
3182
|
+
load();
|
|
3183
|
+
loadUses();
|
|
3184
|
+
loadGroups();
|
|
3185
|
+
loadSources();
|
|
3186
|
+
loadConfig();
|
|
3187
|
+
const fast = window.setInterval(() => {
|
|
3188
|
+
load();
|
|
3189
|
+
}, POLL_MS);
|
|
3190
|
+
const slow = window.setInterval(() => {
|
|
3191
|
+
loadUses();
|
|
3192
|
+
loadGroups();
|
|
3193
|
+
loadSources();
|
|
3194
|
+
loadConfig();
|
|
3195
|
+
}, SLOW_POLL_MS);
|
|
3196
|
+
return () => {
|
|
3197
|
+
window.clearInterval(fast);
|
|
3198
|
+
window.clearInterval(slow);
|
|
3199
|
+
};
|
|
3200
|
+
}, [
|
|
3201
|
+
load,
|
|
3202
|
+
loadUses,
|
|
3203
|
+
loadGroups,
|
|
3204
|
+
loadSources,
|
|
3205
|
+
loadConfig
|
|
3206
|
+
]);
|
|
3207
|
+
const openDetail = (0, react.useCallback)(async (name) => {
|
|
3208
|
+
setDetailLoading(true);
|
|
3209
|
+
setLoadError(null);
|
|
3210
|
+
try {
|
|
3211
|
+
setDetail(await api.skill(name));
|
|
3212
|
+
} catch (error) {
|
|
3213
|
+
setLoadError(errorMessage(error));
|
|
3214
|
+
} finally {
|
|
3215
|
+
setDetailLoading(false);
|
|
3216
|
+
}
|
|
3217
|
+
}, [api]);
|
|
3218
|
+
const toggle = (0, react.useCallback)(async (skill, enabled) => {
|
|
3219
|
+
setBusyNames((previous) => new Set(previous).add(skill.name));
|
|
3220
|
+
setLoadError(null);
|
|
3221
|
+
try {
|
|
3222
|
+
const next = await api.toggle(skill.name, enabled);
|
|
3223
|
+
setCatalog(next);
|
|
3224
|
+
} catch (error) {
|
|
3225
|
+
setLoadError(errorMessage(error));
|
|
3226
|
+
} finally {
|
|
3227
|
+
setBusyNames((previous) => {
|
|
3228
|
+
const next = new Set(previous);
|
|
3229
|
+
next.delete(skill.name);
|
|
3230
|
+
return next;
|
|
3231
|
+
});
|
|
3232
|
+
}
|
|
3233
|
+
}, [api]);
|
|
3234
|
+
const enableDisabled = (0, react.useCallback)(async (record) => {
|
|
3235
|
+
setBusyNames((previous) => new Set(previous).add(record.name));
|
|
3236
|
+
setLoadError(null);
|
|
3237
|
+
try {
|
|
3238
|
+
const next = await api.toggle(record.name, true);
|
|
3239
|
+
setCatalog(next);
|
|
3240
|
+
} catch (error) {
|
|
3241
|
+
setLoadError(errorMessage(error));
|
|
3242
|
+
} finally {
|
|
3243
|
+
setBusyNames((previous) => {
|
|
3244
|
+
const next = new Set(previous);
|
|
3245
|
+
next.delete(record.name);
|
|
3246
|
+
return next;
|
|
3247
|
+
});
|
|
3248
|
+
}
|
|
3249
|
+
}, [api]);
|
|
3250
|
+
/** Toggle an explicit name set in one write (enables disabled members too). */
|
|
3251
|
+
const batchToggleNames = (0, react.useCallback)(async (names, enabled) => {
|
|
3252
|
+
if (names.length === 0) return;
|
|
3253
|
+
setBatchBusy(true);
|
|
3254
|
+
setLoadError(null);
|
|
3255
|
+
try {
|
|
3256
|
+
const next = await api.toggleBatch(names, enabled);
|
|
3257
|
+
setCatalog(next.catalog);
|
|
3258
|
+
if (next.failures.length > 0) setLoadError("toggle-batch: " + next.failures.map((failure) => failure.name + ": " + failure.error).join("; "));
|
|
3259
|
+
} catch (error) {
|
|
3260
|
+
setLoadError(errorMessage(error));
|
|
3261
|
+
} finally {
|
|
3262
|
+
setBatchBusy(false);
|
|
3263
|
+
}
|
|
3264
|
+
}, [api]);
|
|
3265
|
+
/** 所有分组:key → 成员名(tag 与来源集合)。 */
|
|
3266
|
+
const groupMap = (0, react.useCallback)(() => {
|
|
3267
|
+
const map = /* @__PURE__ */ new Map();
|
|
3268
|
+
for (const tag of groupsState?.tags ?? []) map.set("tag:" + tag.id, tag.skillNames);
|
|
3269
|
+
for (const collection of groupsState?.collections ?? []) map.set("col:" + collection.name, collection.skillNames);
|
|
3270
|
+
return map;
|
|
3271
|
+
}, [groupsState]);
|
|
3272
|
+
/** 当前启用且可写的技能名(组开关只作用于它们)。 */
|
|
3273
|
+
const actionNames = (0, react.useMemo)(() => new Set((catalog?.skills ?? []).filter((skill) => skill.writable).map((skill) => skill.name)), [catalog]);
|
|
3274
|
+
/** 当前启用的全部技能名(含只读,用于派生开关状态)。 */
|
|
3275
|
+
const viewNames = (0, react.useMemo)(() => new Set((catalog?.skills ?? []).map((skill) => skill.name)), [catalog]);
|
|
3276
|
+
/**
|
|
3277
|
+
* Toggle a whole group via its tri-state switch. Closing with members that
|
|
3278
|
+
* are enabled in other groups opens the conflict dialog instead.
|
|
3279
|
+
*/
|
|
3280
|
+
const toggleGroup = (0, react.useCallback)((key, name, view) => {
|
|
3281
|
+
const members = groupMap().get(key) ?? [];
|
|
3282
|
+
if (members.length === 0) return;
|
|
3283
|
+
if (view === "off") {
|
|
3284
|
+
batchToggleNames(members, true);
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
const others = [...groupMap().entries()].filter(([otherKey]) => otherKey !== key).map(([, memberNames]) => ({ members: memberNames }));
|
|
3288
|
+
const conflicts = conflictsOnClose(members, actionNames, others);
|
|
3289
|
+
if (conflicts.length > 0) setConflictDialog({
|
|
3290
|
+
key,
|
|
3291
|
+
name,
|
|
3292
|
+
conflicts
|
|
3293
|
+
});
|
|
3294
|
+
else batchToggleNames(members.filter((member) => actionNames.has(member)), false);
|
|
3295
|
+
}, [
|
|
3296
|
+
groupMap,
|
|
3297
|
+
actionNames,
|
|
3298
|
+
batchToggleNames
|
|
3299
|
+
]);
|
|
3300
|
+
/** Resolve the open conflict dialog. */
|
|
3301
|
+
const resolveConflict = (0, react.useCallback)(async (closeAll) => {
|
|
3302
|
+
const dialog = conflictDialog;
|
|
3303
|
+
if (dialog === null) return;
|
|
3304
|
+
setConflictDialog(null);
|
|
3305
|
+
const members = groupMap().get(dialog.key) ?? [];
|
|
3306
|
+
if (closeAll) await batchToggleNames(members.filter((member) => actionNames.has(member)), false);
|
|
3307
|
+
else await batchToggleNames(members.filter((member) => actionNames.has(member) && !dialog.conflicts.includes(member)), false);
|
|
3308
|
+
}, [
|
|
3309
|
+
conflictDialog,
|
|
3310
|
+
groupMap,
|
|
3311
|
+
actionNames,
|
|
3312
|
+
batchToggleNames
|
|
3313
|
+
]);
|
|
3314
|
+
/** Dismiss a confirm dialog and run the pending source action. */
|
|
3315
|
+
const runConfirmed = (0, react.useCallback)(async () => {
|
|
3316
|
+
const dialog = confirmDialog;
|
|
3317
|
+
if (dialog === null) return;
|
|
3318
|
+
setConfirmDialog(null);
|
|
3319
|
+
if (dialog.kind === "sync") {
|
|
3320
|
+
setSyncingSource(dialog.repo);
|
|
3321
|
+
setLoadError(null);
|
|
3322
|
+
try {
|
|
3323
|
+
const result = await api.syncSource(dialog.repo, dialog.skills);
|
|
3324
|
+
await Promise.all([
|
|
3325
|
+
load(),
|
|
3326
|
+
loadGroups(),
|
|
3327
|
+
loadSources()
|
|
3328
|
+
]);
|
|
3329
|
+
if (result.failed.length > 0) setLoadError("sync: " + result.failed.map((failure) => failure.name + ": " + failure.error).join("; "));
|
|
3330
|
+
} catch (error) {
|
|
3331
|
+
setLoadError(errorMessage(error));
|
|
3332
|
+
} finally {
|
|
3333
|
+
setSyncingSource(null);
|
|
3334
|
+
}
|
|
3335
|
+
} else {
|
|
3336
|
+
setLoadError(null);
|
|
3337
|
+
try {
|
|
3338
|
+
await api.confirmDeleteSource(dialog.repo, dialog.skills);
|
|
3339
|
+
await Promise.all([
|
|
3340
|
+
load(),
|
|
3341
|
+
loadGroups(),
|
|
3342
|
+
loadSources()
|
|
3343
|
+
]);
|
|
3344
|
+
} catch (error) {
|
|
3345
|
+
setLoadError(errorMessage(error));
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
}, [
|
|
3349
|
+
confirmDialog,
|
|
3350
|
+
api,
|
|
3351
|
+
load,
|
|
3352
|
+
loadGroups,
|
|
3353
|
+
loadSources
|
|
3354
|
+
]);
|
|
3355
|
+
/** 检查全部来源的上游更新(服务端 5 分钟节流)。 */
|
|
3356
|
+
const checkSources = (0, react.useCallback)(async (repo) => {
|
|
3357
|
+
setCheckingSource(repo ?? "all");
|
|
3358
|
+
setLoadError(null);
|
|
3359
|
+
try {
|
|
3360
|
+
const result = await api.checkSources(repo);
|
|
3361
|
+
const next = { ...sourceCheck };
|
|
3362
|
+
for (const item of result.results) next[item.repo] = item;
|
|
3363
|
+
setSourceCheck(next);
|
|
3364
|
+
} catch (error) {
|
|
3365
|
+
setLoadError(errorMessage(error));
|
|
3366
|
+
} finally {
|
|
3367
|
+
setCheckingSource(null);
|
|
3368
|
+
}
|
|
3369
|
+
}, [api, sourceCheck]);
|
|
3370
|
+
/** 请求同步某个来源的所选技能(弹确认,因为会覆盖本地修改)。 */
|
|
3371
|
+
const requestSync = (0, react.useCallback)((repo, skills) => {
|
|
3372
|
+
setConfirmDialog({
|
|
3373
|
+
kind: "sync",
|
|
3374
|
+
repo,
|
|
3375
|
+
skills
|
|
3376
|
+
});
|
|
3377
|
+
}, []);
|
|
3378
|
+
/** 请求跟进上游删除(弹确认,移入回收站)。 */
|
|
3379
|
+
const requestDelete = (0, react.useCallback)((repo, skills) => {
|
|
3380
|
+
setConfirmDialog({
|
|
3381
|
+
kind: "delete",
|
|
3382
|
+
repo,
|
|
3383
|
+
skills
|
|
3384
|
+
});
|
|
3385
|
+
}, []);
|
|
3386
|
+
/** 从回收站恢复一个技能。 */
|
|
3387
|
+
const restoreTrash = (0, react.useCallback)(async (name) => {
|
|
3388
|
+
setTagBusy(true);
|
|
3389
|
+
setLoadError(null);
|
|
3390
|
+
try {
|
|
3391
|
+
await api.restoreSource(name);
|
|
3392
|
+
await Promise.all([
|
|
3393
|
+
load(),
|
|
3394
|
+
loadGroups(),
|
|
3395
|
+
loadSources()
|
|
3396
|
+
]);
|
|
3397
|
+
} catch (error) {
|
|
3398
|
+
setLoadError(errorMessage(error));
|
|
3399
|
+
} finally {
|
|
3400
|
+
setTagBusy(false);
|
|
3401
|
+
}
|
|
3402
|
+
}, [
|
|
3403
|
+
api,
|
|
3404
|
+
load,
|
|
3405
|
+
loadGroups,
|
|
3406
|
+
loadSources
|
|
3407
|
+
]);
|
|
3408
|
+
/** 确认后永久删除回收站里的全部技能。 */
|
|
3409
|
+
const clearTrash = (0, react.useCallback)(async () => {
|
|
3410
|
+
setConfirmClearTrash(false);
|
|
3411
|
+
setTagBusy(true);
|
|
3412
|
+
setLoadError(null);
|
|
3413
|
+
try {
|
|
3414
|
+
const result = await api.clearTrash();
|
|
3415
|
+
if (result.failed.length > 0) setLoadError("clear trash: " + result.failed.map((failure) => failure.name + ": " + failure.error).join("; "));
|
|
3416
|
+
await Promise.all([load(), loadSources()]);
|
|
3417
|
+
} catch (error) {
|
|
3418
|
+
setLoadError(errorMessage(error));
|
|
3419
|
+
} finally {
|
|
3420
|
+
setTagBusy(false);
|
|
3421
|
+
}
|
|
3422
|
+
}, [
|
|
3423
|
+
api,
|
|
3424
|
+
load,
|
|
3425
|
+
loadSources
|
|
3426
|
+
]);
|
|
3427
|
+
/** 打开单个技能的删除确认(移入回收站,可恢复)。 */
|
|
3428
|
+
const requestDeleteSkill = (0, react.useCallback)((name) => {
|
|
3429
|
+
setDeleteSkillDialog(name);
|
|
3430
|
+
}, []);
|
|
3431
|
+
/** 执行删除确认后的回收站迁移。 */
|
|
3432
|
+
const runDeleteSkill = (0, react.useCallback)(async () => {
|
|
3433
|
+
const name = deleteSkillDialog;
|
|
3434
|
+
if (name === null) return;
|
|
3435
|
+
setDeleteSkillDialog(null);
|
|
3436
|
+
setTagBusy(true);
|
|
3437
|
+
setLoadError(null);
|
|
3438
|
+
try {
|
|
3439
|
+
await api.deleteSkill(name);
|
|
3440
|
+
await Promise.all([
|
|
3441
|
+
load(),
|
|
3442
|
+
loadGroups(),
|
|
3443
|
+
loadSources()
|
|
3444
|
+
]);
|
|
3445
|
+
} catch (error) {
|
|
3446
|
+
setLoadError(errorMessage(error));
|
|
3447
|
+
} finally {
|
|
3448
|
+
setTagBusy(false);
|
|
3449
|
+
}
|
|
3450
|
+
}, [
|
|
3451
|
+
api,
|
|
3452
|
+
deleteSkillDialog,
|
|
3453
|
+
load,
|
|
3454
|
+
loadGroups,
|
|
3455
|
+
loadSources
|
|
3456
|
+
]);
|
|
3457
|
+
/** 新建一个空 tag 分组。 */
|
|
3458
|
+
const createTag = (0, react.useCallback)(async (event) => {
|
|
3459
|
+
event.preventDefault();
|
|
3460
|
+
const name = newTagName.trim();
|
|
3461
|
+
if (name === "") return;
|
|
3462
|
+
setTagBusy(true);
|
|
3463
|
+
setLoadError(null);
|
|
3464
|
+
try {
|
|
3465
|
+
applyTags(await api.saveTag({ name }));
|
|
3466
|
+
setNewTagName("");
|
|
3467
|
+
} catch (error) {
|
|
3468
|
+
setLoadError(errorMessage(error));
|
|
3469
|
+
} finally {
|
|
3470
|
+
setTagBusy(false);
|
|
3471
|
+
}
|
|
3472
|
+
}, [
|
|
3473
|
+
api,
|
|
3474
|
+
applyTags,
|
|
3475
|
+
newTagName
|
|
3476
|
+
]);
|
|
3477
|
+
/** 删除一个 tag 分组(不影响技能文件)。 */
|
|
3478
|
+
const deleteTag = (0, react.useCallback)(async (id) => {
|
|
3479
|
+
setTagBusy(true);
|
|
3480
|
+
setLoadError(null);
|
|
3481
|
+
try {
|
|
3482
|
+
applyTags(await api.deleteTag(id));
|
|
3483
|
+
setEditingTag(null);
|
|
3484
|
+
} catch (error) {
|
|
3485
|
+
setLoadError(errorMessage(error));
|
|
3486
|
+
} finally {
|
|
3487
|
+
setTagBusy(false);
|
|
3488
|
+
}
|
|
3489
|
+
}, [api, applyTags]);
|
|
3490
|
+
/** 重命名 tag,或保存成员勾选后回到列表。 */
|
|
3491
|
+
const saveTag = (0, react.useCallback)(async (id, name, memberNames) => {
|
|
3492
|
+
setTagBusy(true);
|
|
3493
|
+
setLoadError(null);
|
|
3494
|
+
try {
|
|
3495
|
+
const safeName = name.trim();
|
|
3496
|
+
let tags = null;
|
|
3497
|
+
if (safeName !== "") tags = await api.saveTag({
|
|
3498
|
+
id,
|
|
3499
|
+
name: safeName
|
|
3500
|
+
});
|
|
3501
|
+
if (memberNames !== null) tags = await api.setTagMembers(id, memberNames);
|
|
3502
|
+
if (tags === null) return;
|
|
3503
|
+
applyTags(tags);
|
|
3504
|
+
setEditingTag(null);
|
|
3505
|
+
} catch (error) {
|
|
3506
|
+
setLoadError(errorMessage(error));
|
|
3507
|
+
} finally {
|
|
3508
|
+
setTagBusy(false);
|
|
3509
|
+
}
|
|
3510
|
+
}, [api, applyTags]);
|
|
3511
|
+
/** 添加一个市场源(内置市场目录与手动输入共用),并立即扫描它。 */
|
|
3512
|
+
const addSource = (0, react.useCallback)(async (input) => {
|
|
3513
|
+
const value = input.trim();
|
|
3514
|
+
if (value === "") return;
|
|
3515
|
+
setLoadError(null);
|
|
3516
|
+
try {
|
|
3517
|
+
const result = await api.addMarketSource(value);
|
|
3518
|
+
setMarketState({
|
|
3519
|
+
status: "ready",
|
|
3520
|
+
repos: result.repos
|
|
3521
|
+
});
|
|
3522
|
+
setNewSourceName("");
|
|
3523
|
+
setRepoResult(null);
|
|
3524
|
+
setRepoSelected(/* @__PURE__ */ new Set());
|
|
3525
|
+
const slug = result.repos[result.repos.length - 1].repo;
|
|
3526
|
+
setScanningRepo(slug);
|
|
3527
|
+
setRepoDiscoverState({ status: "scanning" });
|
|
3528
|
+
try {
|
|
3529
|
+
const data = await api.repoDiscover(slug);
|
|
3530
|
+
setRepoDiscoverState({
|
|
3531
|
+
status: "ready",
|
|
3532
|
+
data
|
|
3533
|
+
});
|
|
3534
|
+
} catch (error) {
|
|
3535
|
+
setRepoDiscoverState({
|
|
3536
|
+
status: "error",
|
|
3537
|
+
message: errorMessage(error)
|
|
3538
|
+
});
|
|
3539
|
+
} finally {
|
|
3540
|
+
setScanningRepo(null);
|
|
3541
|
+
}
|
|
3542
|
+
} catch (error) {
|
|
3543
|
+
setLoadError(errorMessage(error));
|
|
3544
|
+
}
|
|
3545
|
+
}, [api]);
|
|
3546
|
+
/** 手动输入框的添加动作。 */
|
|
3547
|
+
const addMarketSource = (0, react.useCallback)(async () => {
|
|
3548
|
+
await addSource(newSourceName);
|
|
3549
|
+
}, [addSource, newSourceName]);
|
|
3550
|
+
/** 删除一个市场源(不影响已装技能)。 */
|
|
3551
|
+
const removeMarketSource = (0, react.useCallback)(async (repo) => {
|
|
3552
|
+
setTagBusy(true);
|
|
3553
|
+
setLoadError(null);
|
|
3554
|
+
try {
|
|
3555
|
+
const result = await api.removeMarketSource(repo);
|
|
3556
|
+
setMarketState({
|
|
3557
|
+
status: "ready",
|
|
3558
|
+
repos: result.repos
|
|
3559
|
+
});
|
|
3560
|
+
} catch (error) {
|
|
3561
|
+
setLoadError(errorMessage(error));
|
|
3562
|
+
} finally {
|
|
3563
|
+
setTagBusy(false);
|
|
3564
|
+
}
|
|
3565
|
+
}, [api]);
|
|
3566
|
+
/** 扫描一个市场源(或手动输入)的仓库。 */
|
|
3567
|
+
const scanRepo = (0, react.useCallback)(async (input) => {
|
|
3568
|
+
const value = input.trim();
|
|
3569
|
+
if (value === "") return;
|
|
3570
|
+
setRepoResult(null);
|
|
3571
|
+
setRepoSelected(/* @__PURE__ */ new Set());
|
|
3572
|
+
setScanningRepo(value);
|
|
3573
|
+
setRepoDiscoverState({ status: "scanning" });
|
|
3574
|
+
setLoadError(null);
|
|
3575
|
+
try {
|
|
3576
|
+
const data = await api.repoDiscover(value);
|
|
3577
|
+
if (data.ref === null) {
|
|
3578
|
+
setBranchChoice({
|
|
3579
|
+
repo: data.repo,
|
|
3580
|
+
branches: data.branches ?? [],
|
|
3581
|
+
selected: (data.branches ?? [])[0] ?? "main"
|
|
3582
|
+
});
|
|
3583
|
+
setRepoDiscoverState({ status: "idle" });
|
|
3584
|
+
return;
|
|
3585
|
+
}
|
|
3586
|
+
setRepoDiscoverState({
|
|
3587
|
+
status: "ready",
|
|
3588
|
+
data
|
|
3589
|
+
});
|
|
3590
|
+
} catch (error) {
|
|
3591
|
+
setRepoDiscoverState({
|
|
3592
|
+
status: "error",
|
|
3593
|
+
message: errorMessage(error)
|
|
3594
|
+
});
|
|
3595
|
+
} finally {
|
|
3596
|
+
setScanningRepo(null);
|
|
3597
|
+
}
|
|
3598
|
+
}, [api]);
|
|
3599
|
+
/** 确认分支选择:持久化 ref 后重新扫描。 */
|
|
3600
|
+
const confirmBranchChoice = (0, react.useCallback)(async () => {
|
|
3601
|
+
if (branchChoice === null) return;
|
|
3602
|
+
setBranchBusy(true);
|
|
3603
|
+
setLoadError(null);
|
|
3604
|
+
try {
|
|
3605
|
+
await api.setMarketSourceRef(branchChoice.repo, branchChoice.selected);
|
|
3606
|
+
setBranchChoice(null);
|
|
3607
|
+
await scanRepo(branchChoice.repo);
|
|
3608
|
+
} catch (error) {
|
|
3609
|
+
setLoadError(errorMessage(error));
|
|
3610
|
+
} finally {
|
|
3611
|
+
setBranchBusy(false);
|
|
3612
|
+
}
|
|
3613
|
+
}, [
|
|
3614
|
+
api,
|
|
3615
|
+
branchChoice,
|
|
3616
|
+
scanRepo
|
|
3617
|
+
]);
|
|
3618
|
+
/** Toggle one repo preview row. */
|
|
3619
|
+
const toggleRepoSelected = (0, react.useCallback)((path, checked) => {
|
|
3620
|
+
setRepoSelected((previous) => {
|
|
3621
|
+
const next = new Set(previous);
|
|
3622
|
+
if (checked) next.add(path);
|
|
3623
|
+
else next.delete(path);
|
|
3624
|
+
return next;
|
|
3625
|
+
});
|
|
3626
|
+
}, []);
|
|
3627
|
+
/** Import every checked, non-existing repo skill, then refresh the hub. */
|
|
3628
|
+
const importRepo = (0, react.useCallback)(async () => {
|
|
3629
|
+
if (repoDiscoverState.status !== "ready") return;
|
|
3630
|
+
setRepoImporting(true);
|
|
3631
|
+
setRepoResult(null);
|
|
3632
|
+
setLoadError(null);
|
|
3633
|
+
try {
|
|
3634
|
+
const result = await api.repoImport(repoDiscoverState.data.repo, [...repoSelected], repoDiscoverState.data.ref ?? void 0);
|
|
3635
|
+
setRepoResult(result);
|
|
3636
|
+
await Promise.all([
|
|
3637
|
+
load(),
|
|
3638
|
+
loadMarket(),
|
|
3639
|
+
loadGroups(),
|
|
3640
|
+
loadSources()
|
|
3641
|
+
]);
|
|
3642
|
+
} catch (error) {
|
|
3643
|
+
setLoadError(errorMessage(error));
|
|
3644
|
+
} finally {
|
|
3645
|
+
setRepoImporting(false);
|
|
3646
|
+
}
|
|
3647
|
+
}, [
|
|
3648
|
+
api,
|
|
3649
|
+
repoDiscoverState,
|
|
3650
|
+
repoSelected,
|
|
3651
|
+
load,
|
|
3652
|
+
loadMarket,
|
|
3653
|
+
loadGroups,
|
|
3654
|
+
loadSources
|
|
3655
|
+
]);
|
|
3656
|
+
/** 检查所有市场源的上游更新(服务端节流)。 */
|
|
3657
|
+
const checkMarket = (0, react.useCallback)(async () => {
|
|
3658
|
+
try {
|
|
3659
|
+
const result = await api.marketCheck();
|
|
3660
|
+
const next = {};
|
|
3661
|
+
for (const item of result.results) next[item.repo] = item;
|
|
3662
|
+
setMarketCheck(next);
|
|
3663
|
+
} catch {}
|
|
3664
|
+
}, [api]);
|
|
3665
|
+
/** 市场源同步:版本对齐后询问是否批量更新本地技能。 */
|
|
3666
|
+
const syncMarketSource = (0, react.useCallback)(async (repo) => {
|
|
3667
|
+
setSyncingMarket(repo);
|
|
3668
|
+
setLoadError(null);
|
|
3669
|
+
try {
|
|
3670
|
+
const result = await api.marketSync(repo);
|
|
3671
|
+
setMarketSyncDialog({
|
|
3672
|
+
repo: result.repo,
|
|
3673
|
+
ref: result.ref,
|
|
3674
|
+
skills: result.skills,
|
|
3675
|
+
selected: new Set(result.skills)
|
|
3676
|
+
});
|
|
3677
|
+
await loadMarket();
|
|
3678
|
+
} catch (error) {
|
|
3679
|
+
setLoadError(errorMessage(error));
|
|
3680
|
+
} finally {
|
|
3681
|
+
setSyncingMarket(null);
|
|
3682
|
+
}
|
|
3683
|
+
}, [api, loadMarket]);
|
|
3684
|
+
/** 批量更新本地技能到市场源当前版本(复用来源同步路径)。 */
|
|
3685
|
+
const confirmMarketSync = (0, react.useCallback)(async () => {
|
|
3686
|
+
if (marketSyncDialog === null) return;
|
|
3687
|
+
const selected = [...marketSyncDialog.selected];
|
|
3688
|
+
setSyncBusy(true);
|
|
3689
|
+
setLoadError(null);
|
|
3690
|
+
try {
|
|
3691
|
+
if (selected.length > 0) {
|
|
3692
|
+
const result = await api.syncSource(marketSyncDialog.repo, selected);
|
|
3693
|
+
if (result.failed.length > 0) setLoadError(result.failed.map((item) => item.name + ": " + item.error).join("\n"));
|
|
3694
|
+
}
|
|
3695
|
+
setMarketSyncDialog(null);
|
|
3696
|
+
await Promise.all([
|
|
3697
|
+
load(),
|
|
3698
|
+
loadGroups(),
|
|
3699
|
+
loadSources()
|
|
3700
|
+
]);
|
|
3701
|
+
checkMarket();
|
|
3702
|
+
} catch (error) {
|
|
3703
|
+
setLoadError(errorMessage(error));
|
|
3704
|
+
} finally {
|
|
3705
|
+
setSyncBusy(false);
|
|
3706
|
+
}
|
|
3707
|
+
}, [
|
|
3708
|
+
api,
|
|
3709
|
+
marketSyncDialog,
|
|
3710
|
+
load,
|
|
3711
|
+
loadGroups,
|
|
3712
|
+
loadSources,
|
|
3713
|
+
checkMarket
|
|
3714
|
+
]);
|
|
3715
|
+
/**
|
|
3716
|
+
* 一键全部更新:把每个「有可更新技能」的来源逐个同步到上游最新版本
|
|
3717
|
+
* (复用来源同步路径,覆盖本地修改),随后刷新并重新检查。
|
|
3718
|
+
*/
|
|
3719
|
+
const updateAll = (0, react.useCallback)(async () => {
|
|
3720
|
+
setUpdateAllDialog(false);
|
|
3721
|
+
setBatchBusy(true);
|
|
3722
|
+
setLoadError(null);
|
|
3723
|
+
const failures = [];
|
|
3724
|
+
let done = 0;
|
|
3725
|
+
try {
|
|
3726
|
+
for (const [repo, check] of Object.entries(sourceCheck)) {
|
|
3727
|
+
if (!check.changed || check.updated.length === 0) continue;
|
|
3728
|
+
try {
|
|
3729
|
+
const result = await api.syncSource(repo, check.updated);
|
|
3730
|
+
done += 1;
|
|
3731
|
+
if (result.failed.length > 0) failures.push(tt("market.updateAllItem", {
|
|
3732
|
+
repo,
|
|
3733
|
+
count: result.failed.length
|
|
3734
|
+
}) + ": " + result.failed.map((item) => item.name + ": " + item.error).join("; "));
|
|
3735
|
+
} catch (error) {
|
|
3736
|
+
failures.push(repo + ": " + errorMessage(error));
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
await Promise.all([
|
|
3740
|
+
load(),
|
|
3741
|
+
loadGroups(),
|
|
3742
|
+
loadSources()
|
|
3743
|
+
]);
|
|
3744
|
+
await checkSources();
|
|
3745
|
+
checkMarket();
|
|
3746
|
+
if (failures.length > 0) setLoadError(failures.join("\n"));
|
|
3747
|
+
else if (done > 0) setSuccessBanner(tt("market.updateAllDone", { count: done }));
|
|
3748
|
+
} finally {
|
|
3749
|
+
setBatchBusy(false);
|
|
3750
|
+
}
|
|
3751
|
+
}, [
|
|
3752
|
+
api,
|
|
3753
|
+
sourceCheck,
|
|
3754
|
+
checkSources,
|
|
3755
|
+
checkMarket,
|
|
3756
|
+
load,
|
|
3757
|
+
loadGroups,
|
|
3758
|
+
loadSources
|
|
3759
|
+
]);
|
|
3760
|
+
(0, react.useEffect)(() => {
|
|
3761
|
+
if (!shouldAutoCheck()) return;
|
|
3762
|
+
markAutoChecked();
|
|
3763
|
+
checkUpdate();
|
|
3764
|
+
checkSources();
|
|
3765
|
+
checkMarket();
|
|
3766
|
+
}, []);
|
|
3767
|
+
const create = (0, react.useCallback)(async (event) => {
|
|
3768
|
+
event.preventDefault();
|
|
3769
|
+
setFormBusy(true);
|
|
3770
|
+
setFormMessage(null);
|
|
3771
|
+
setSuccessBanner(null);
|
|
3772
|
+
try {
|
|
3773
|
+
const result = await api.create({
|
|
3774
|
+
name: formName,
|
|
3775
|
+
description: formDesc,
|
|
3776
|
+
root: formRoot
|
|
3777
|
+
});
|
|
3778
|
+
setSuccessBanner(tt("form.success") + result.path);
|
|
3779
|
+
setFormName("");
|
|
3780
|
+
setFormDesc("");
|
|
3781
|
+
setShowForm(false);
|
|
3782
|
+
await load();
|
|
3783
|
+
} catch (error) {
|
|
3784
|
+
setFormMessage({
|
|
3785
|
+
kind: "error",
|
|
3786
|
+
text: tt("form.error") + errorMessage(error)
|
|
3787
|
+
});
|
|
3788
|
+
} finally {
|
|
3789
|
+
setFormBusy(false);
|
|
3790
|
+
}
|
|
3791
|
+
}, [
|
|
3792
|
+
api,
|
|
3793
|
+
formName,
|
|
3794
|
+
formDesc,
|
|
3795
|
+
formRoot,
|
|
3796
|
+
load
|
|
3797
|
+
]);
|
|
3798
|
+
const normalized = search.trim().toLocaleLowerCase();
|
|
3799
|
+
/** skillName → origin repo(无来源记录的技能不在此映射中,筛选中视为 private)。 */
|
|
3800
|
+
const origins = groupsState?.origins ?? {};
|
|
3801
|
+
/** 来源筛选选项:来源记录中的仓库(排序)+ 末尾的「个人」;没有技能的仓库不列。 */
|
|
3802
|
+
const sourceOptions = (0, react.useMemo)(() => {
|
|
3803
|
+
const skills = catalog?.skills ?? [];
|
|
3804
|
+
const repos = [...new Set(skills.map((skill) => origins[skill.name]).filter((repo) => repo !== void 0))].sort();
|
|
3805
|
+
const hasPrivate = skills.some((skill) => origins[skill.name] === void 0);
|
|
3806
|
+
return [...repos, ...hasPrivate ? [PRIVATE_SOURCE] : []];
|
|
3807
|
+
}, [catalog, origins]);
|
|
3808
|
+
const filtered = (0, react.useMemo)(() => (catalog?.skills ?? []).filter((skill) => normalized.length === 0 || skill.name.toLocaleLowerCase().includes(normalized) || skill.description.toLocaleLowerCase().includes(normalized)), [catalog, normalized]);
|
|
3809
|
+
return {
|
|
3810
|
+
catalog,
|
|
3811
|
+
loading,
|
|
3812
|
+
loadError,
|
|
3813
|
+
successBanner,
|
|
3814
|
+
updateState,
|
|
3815
|
+
repoDiscoverState,
|
|
3816
|
+
scanningRepo,
|
|
3817
|
+
repoSelected,
|
|
3818
|
+
repoImporting,
|
|
3819
|
+
repoResult,
|
|
3820
|
+
search,
|
|
3821
|
+
detail,
|
|
3822
|
+
detailLoading,
|
|
3823
|
+
busyNames,
|
|
3824
|
+
batchBusy,
|
|
3825
|
+
showForm,
|
|
3826
|
+
formName,
|
|
3827
|
+
formDesc,
|
|
3828
|
+
formRoot,
|
|
3829
|
+
formBusy,
|
|
3830
|
+
formMessage,
|
|
3831
|
+
uses,
|
|
3832
|
+
hubConfig,
|
|
3833
|
+
tab,
|
|
3834
|
+
skillView,
|
|
3835
|
+
sourceFilter,
|
|
3836
|
+
sortKey,
|
|
3837
|
+
marketState,
|
|
3838
|
+
marketCheck,
|
|
3839
|
+
branchChoice,
|
|
3840
|
+
branchBusy,
|
|
3841
|
+
marketSyncDialog,
|
|
3842
|
+
syncingMarket,
|
|
3843
|
+
syncBusy,
|
|
3844
|
+
newSourceName,
|
|
3845
|
+
groupsState,
|
|
3846
|
+
sourcesState,
|
|
3847
|
+
sourceCheck,
|
|
3848
|
+
checkingSource,
|
|
3849
|
+
syncingSource,
|
|
3850
|
+
conflictDialog,
|
|
3851
|
+
confirmDialog,
|
|
3852
|
+
deleteSkillDialog,
|
|
3853
|
+
confirmClearTrash,
|
|
3854
|
+
updateAllDialog,
|
|
3855
|
+
editingTag,
|
|
3856
|
+
editName,
|
|
3857
|
+
membersDraft,
|
|
3858
|
+
newTagName,
|
|
3859
|
+
tagBusy,
|
|
3860
|
+
editSearch,
|
|
3861
|
+
collapsedGroups,
|
|
3862
|
+
showLegend,
|
|
3863
|
+
actionNames,
|
|
3864
|
+
viewNames,
|
|
3865
|
+
normalized,
|
|
3866
|
+
origins,
|
|
3867
|
+
sourceOptions,
|
|
3868
|
+
filtered,
|
|
3869
|
+
sorted: (0, react.useMemo)(() => sortSkills(filtered, sortKey, (name) => uses.get(name)?.count), [
|
|
3870
|
+
filtered,
|
|
3871
|
+
sortKey,
|
|
3872
|
+
uses
|
|
3873
|
+
]),
|
|
3874
|
+
setLoadError,
|
|
3875
|
+
setSuccessBanner,
|
|
3876
|
+
setSearch,
|
|
3877
|
+
setDetail,
|
|
3878
|
+
setShowForm,
|
|
3879
|
+
setFormName,
|
|
3880
|
+
setFormDesc,
|
|
3881
|
+
setFormRoot,
|
|
3882
|
+
setFormMessage,
|
|
3883
|
+
setRepoSelected,
|
|
3884
|
+
setTab,
|
|
3885
|
+
setSkillView,
|
|
3886
|
+
setSourceFilter,
|
|
3887
|
+
setSortKey,
|
|
3888
|
+
setBranchChoice,
|
|
3889
|
+
setMarketSyncDialog,
|
|
3890
|
+
setNewSourceName,
|
|
3891
|
+
setConflictDialog,
|
|
3892
|
+
setConfirmDialog,
|
|
3893
|
+
setDeleteSkillDialog,
|
|
3894
|
+
setConfirmClearTrash,
|
|
3895
|
+
setUpdateAllDialog,
|
|
3896
|
+
setEditingTag,
|
|
3897
|
+
setEditName,
|
|
3898
|
+
setMembersDraft,
|
|
3899
|
+
setNewTagName,
|
|
3900
|
+
setEditSearch,
|
|
3901
|
+
setShowLegend,
|
|
3902
|
+
toggleGroupCollapse,
|
|
3903
|
+
checkUpdate,
|
|
3904
|
+
loadMarket,
|
|
3905
|
+
openDetail,
|
|
3906
|
+
toggle,
|
|
3907
|
+
enableDisabled,
|
|
3908
|
+
batchToggleNames,
|
|
3909
|
+
toggleGroup,
|
|
3910
|
+
resolveConflict,
|
|
3911
|
+
runConfirmed,
|
|
3912
|
+
checkSources,
|
|
3913
|
+
requestSync,
|
|
3914
|
+
requestDelete,
|
|
3915
|
+
restoreTrash,
|
|
3916
|
+
clearTrash,
|
|
3917
|
+
requestDeleteSkill,
|
|
3918
|
+
runDeleteSkill,
|
|
3919
|
+
createTag,
|
|
3920
|
+
deleteTag,
|
|
3921
|
+
saveTag,
|
|
3922
|
+
addSource,
|
|
3923
|
+
addMarketSource,
|
|
3924
|
+
removeMarketSource,
|
|
3925
|
+
scanRepo,
|
|
3926
|
+
confirmBranchChoice,
|
|
3927
|
+
toggleRepoSelected,
|
|
3928
|
+
importRepo,
|
|
3929
|
+
checkMarket,
|
|
3930
|
+
syncMarketSource,
|
|
3931
|
+
confirmMarketSync,
|
|
3932
|
+
updateAll,
|
|
3933
|
+
create
|
|
3934
|
+
};
|
|
3935
|
+
}
|
|
3936
|
+
//#endregion
|
|
3937
|
+
//#region src/client/panel/SkillHubPanel.tsx
|
|
3938
|
+
function SkillHubPanel(props) {
|
|
3939
|
+
const hub = useSkillHub(props.api);
|
|
3940
|
+
const { catalog, loading, loadError, successBanner, updateState, detail, detailLoading, showForm, formName, formDesc, formRoot, formBusy, formMessage, hubConfig, tab, skillView, sourceFilter, sortKey, search, sourcesState, tagBusy, busyNames, normalized, origins, sourceOptions, filtered, conflictDialog, confirmDialog, deleteSkillDialog, confirmClearTrash, branchChoice, branchBusy, marketSyncDialog, syncBusy, editingTag, editName, membersDraft, editSearch, uses, groupsState, sourceCheck, checkingSource, syncingSource, showLegend, setLoadError, setSuccessBanner, setDetail, setShowForm, setFormName, setFormDesc, setFormRoot, setFormMessage, setTab, setSkillView, setSourceFilter, setSortKey, setSearch, setConflictDialog, setConfirmDialog, setDeleteSkillDialog, setConfirmClearTrash, setBranchChoice, setMarketSyncDialog, setEditingTag, setEditName, setMembersDraft, setEditSearch, setShowLegend, checkUpdate, loadMarket, checkSources, requestSync, requestDelete, restoreTrash, clearTrash, runDeleteSkill, runConfirmed, resolveConflict, confirmBranchChoice, confirmMarketSync, create, saveTag, deleteTag, enableDisabled } = hub;
|
|
3941
|
+
if (detail !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillDetailView, {
|
|
3942
|
+
detail,
|
|
3943
|
+
hubConfig,
|
|
3944
|
+
uses,
|
|
3945
|
+
groupsState,
|
|
3946
|
+
sourcesState,
|
|
3947
|
+
sourceCheck,
|
|
3948
|
+
checkingSource,
|
|
3949
|
+
syncingSource,
|
|
3950
|
+
loading: detailLoading,
|
|
3951
|
+
onBack: () => {
|
|
3952
|
+
setDetail(null);
|
|
3953
|
+
},
|
|
3954
|
+
onCheck: (repo) => {
|
|
3955
|
+
checkSources(repo);
|
|
3956
|
+
},
|
|
3957
|
+
onSync: requestSync,
|
|
3958
|
+
onFollowDelete: requestDelete
|
|
3959
|
+
});
|
|
3960
|
+
if (editingTag !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TagEditorView, {
|
|
3961
|
+
tag: editingTag,
|
|
3962
|
+
editName,
|
|
3963
|
+
membersDraft,
|
|
3964
|
+
editSearch,
|
|
3965
|
+
catalog,
|
|
3966
|
+
tagBusy,
|
|
3967
|
+
onBack: () => {
|
|
3968
|
+
setEditingTag(null);
|
|
3969
|
+
},
|
|
3970
|
+
onEditName: setEditName,
|
|
3971
|
+
onEditSearch: setEditSearch,
|
|
3972
|
+
onToggleMember: (name, checked) => {
|
|
3973
|
+
setMembersDraft((previous) => {
|
|
3974
|
+
const next = new Set(previous);
|
|
3975
|
+
if (checked) next.add(name);
|
|
3976
|
+
else next.delete(name);
|
|
3977
|
+
return next;
|
|
3978
|
+
});
|
|
3979
|
+
},
|
|
3980
|
+
onRename: () => {
|
|
3981
|
+
saveTag(editingTag.id, editName, null);
|
|
3982
|
+
},
|
|
3983
|
+
onDelete: () => {
|
|
3984
|
+
deleteTag(editingTag.id);
|
|
3985
|
+
},
|
|
3986
|
+
onSaveMembers: () => {
|
|
3987
|
+
saveTag(editingTag.id, editName, [...membersDraft]);
|
|
3988
|
+
}
|
|
3989
|
+
});
|
|
3990
|
+
/** 检查结果的悬停提示:收敛到「检查更新」按钮上,不再占面板顶部横幅。 */
|
|
3991
|
+
const updateTitle = (() => {
|
|
3992
|
+
if (updateState.status === "checking") return tt("update.checking");
|
|
3993
|
+
if (updateState.status === "error") return tt("update.error", { error: updateState.message });
|
|
3994
|
+
if (updateState.status === "ready") {
|
|
3995
|
+
const data = updateState.data;
|
|
3996
|
+
if (data.error !== void 0) return tt("update.error", { error: data.error });
|
|
3997
|
+
if (data.updateAvailable) return tt("update.available", {
|
|
3998
|
+
version: data.latestVersion ?? "",
|
|
3999
|
+
current: data.currentVersion
|
|
4000
|
+
});
|
|
4001
|
+
if (data.latestVersion === null) return tt("update.unavailable");
|
|
4002
|
+
return tt("update.upToDate", { version: data.latestVersion });
|
|
4003
|
+
}
|
|
4004
|
+
})();
|
|
4005
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4006
|
+
className: panel_module_css_default.panel,
|
|
4007
|
+
children: [
|
|
4008
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4009
|
+
className: panel_module_css_default.header,
|
|
4010
|
+
children: [
|
|
4011
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h2", {
|
|
4012
|
+
className: panel_module_css_default.title,
|
|
4013
|
+
children: [
|
|
4014
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSkillOutline16, {
|
|
4015
|
+
size: 16,
|
|
4016
|
+
className: panel_module_css_default.titleIcon
|
|
4017
|
+
}),
|
|
4018
|
+
" ",
|
|
4019
|
+
tt("panel.title")
|
|
4020
|
+
]
|
|
4021
|
+
}),
|
|
4022
|
+
catalog !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4023
|
+
className: panel_module_css_default.headerCount,
|
|
4024
|
+
children: [tt("panel.count", { count: catalog.skills.length + catalog.disabled.length }), catalog.disabled.length > 0 ? " · " + tt("panel.disabledCount", { count: catalog.disabled.length }) : null]
|
|
4025
|
+
}) : null,
|
|
4026
|
+
catalog !== null && !catalog.complete ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4027
|
+
className: panel_module_css_default.hint,
|
|
4028
|
+
children: tt("panel.incomplete")
|
|
4029
|
+
}) : null,
|
|
4030
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4031
|
+
className: panel_module_css_default.actions,
|
|
4032
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4033
|
+
type: "button",
|
|
4034
|
+
className: panel_module_css_default.button,
|
|
4035
|
+
disabled: updateState.status === "checking",
|
|
4036
|
+
title: updateTitle,
|
|
4037
|
+
onClick: () => {
|
|
4038
|
+
checkUpdate();
|
|
4039
|
+
},
|
|
4040
|
+
children: updateState.status === "checking" ? tt("update.checking") : tt("update.check")
|
|
4041
|
+
}), updateState.status === "ready" && updateState.data.updateAvailable && updateState.data.url !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
4042
|
+
className: panel_module_css_default.updateLink,
|
|
4043
|
+
href: updateState.data.url,
|
|
4044
|
+
target: "_blank",
|
|
4045
|
+
rel: "noreferrer",
|
|
4046
|
+
children: tt("update.newVersion", { version: updateState.data.latestVersion ?? "" })
|
|
4047
|
+
}) : null]
|
|
4048
|
+
})
|
|
4049
|
+
]
|
|
4050
|
+
}),
|
|
4051
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4052
|
+
className: panel_module_css_default.subbar,
|
|
4053
|
+
children: [
|
|
4054
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4055
|
+
className: panel_module_css_default.segmented,
|
|
4056
|
+
children: [
|
|
4057
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4058
|
+
type: "button",
|
|
4059
|
+
className: panel_module_css_default.segBtn + (tab === "sources" ? " " + panel_module_css_default.segBtnActive : ""),
|
|
4060
|
+
onClick: () => {
|
|
4061
|
+
setTab("sources");
|
|
4062
|
+
},
|
|
4063
|
+
children: tt("view.sources")
|
|
4064
|
+
}),
|
|
4065
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4066
|
+
type: "button",
|
|
4067
|
+
className: panel_module_css_default.segBtn + (tab === "scenes" ? " " + panel_module_css_default.segBtnActive : ""),
|
|
4068
|
+
onClick: () => {
|
|
4069
|
+
setTab("scenes");
|
|
4070
|
+
},
|
|
4071
|
+
children: tt("view.scenes")
|
|
4072
|
+
}),
|
|
4073
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4074
|
+
type: "button",
|
|
4075
|
+
className: panel_module_css_default.segBtn + (tab === "market" ? " " + panel_module_css_default.segBtnActive : ""),
|
|
4076
|
+
onClick: () => {
|
|
4077
|
+
setTab("market");
|
|
4078
|
+
loadMarket();
|
|
4079
|
+
},
|
|
4080
|
+
children: tt("view.market")
|
|
4081
|
+
})
|
|
4082
|
+
]
|
|
4083
|
+
}),
|
|
4084
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4085
|
+
type: "button",
|
|
4086
|
+
className: panel_module_css_default.legendToggle + (showLegend ? " " + panel_module_css_default.legendToggleActive : ""),
|
|
4087
|
+
onClick: () => {
|
|
4088
|
+
setShowLegend((value) => !value);
|
|
4089
|
+
},
|
|
4090
|
+
title: tt("legend.hint"),
|
|
4091
|
+
children: "?"
|
|
4092
|
+
}),
|
|
4093
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4094
|
+
className: panel_module_css_default.actions,
|
|
4095
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4096
|
+
type: "button",
|
|
4097
|
+
className: panel_module_css_default.button + " " + panel_module_css_default.primary,
|
|
4098
|
+
onClick: () => {
|
|
4099
|
+
setShowForm((value) => !value);
|
|
4100
|
+
},
|
|
4101
|
+
children: tt("panel.new")
|
|
4102
|
+
})
|
|
4103
|
+
})
|
|
4104
|
+
]
|
|
4105
|
+
}),
|
|
4106
|
+
showForm ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
4107
|
+
className: panel_module_css_default.form,
|
|
4108
|
+
onSubmit: (event) => {
|
|
4109
|
+
create(event);
|
|
4110
|
+
},
|
|
4111
|
+
children: [
|
|
4112
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4113
|
+
className: panel_module_css_default.hintLine,
|
|
4114
|
+
children: tt("form.capabilityHint")
|
|
4115
|
+
}),
|
|
4116
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4117
|
+
className: panel_module_css_default.formRow,
|
|
4118
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
4119
|
+
className: panel_module_css_default.formLabel,
|
|
4120
|
+
children: tt("form.name")
|
|
4121
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4122
|
+
className: panel_module_css_default.input,
|
|
4123
|
+
value: formName,
|
|
4124
|
+
onChange: (event) => {
|
|
4125
|
+
setFormName(event.target.value);
|
|
4126
|
+
},
|
|
4127
|
+
placeholder: "code-review"
|
|
4128
|
+
})]
|
|
4129
|
+
}),
|
|
4130
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4131
|
+
className: panel_module_css_default.formRow,
|
|
4132
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
4133
|
+
className: panel_module_css_default.formLabel,
|
|
4134
|
+
children: tt("form.desc")
|
|
4135
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4136
|
+
className: panel_module_css_default.input,
|
|
4137
|
+
value: formDesc,
|
|
4138
|
+
onChange: (event) => {
|
|
4139
|
+
setFormDesc(event.target.value);
|
|
4140
|
+
},
|
|
4141
|
+
placeholder: tt("form.descPlaceholder")
|
|
4142
|
+
})]
|
|
4143
|
+
}),
|
|
4144
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4145
|
+
className: panel_module_css_default.formRow,
|
|
4146
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
4147
|
+
className: panel_module_css_default.formLabel,
|
|
4148
|
+
children: tt("form.root")
|
|
4149
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
4150
|
+
className: panel_module_css_default.select,
|
|
4151
|
+
value: formRoot,
|
|
4152
|
+
onChange: (event) => {
|
|
4153
|
+
setFormRoot(event.target.value);
|
|
4154
|
+
},
|
|
4155
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4156
|
+
value: "user-dsh",
|
|
4157
|
+
children: "~/.dsh/skills"
|
|
4158
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4159
|
+
value: "user-agents",
|
|
4160
|
+
children: "~/.agents/skills"
|
|
4161
|
+
})]
|
|
4162
|
+
})]
|
|
4163
|
+
}),
|
|
4164
|
+
formMessage !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4165
|
+
className: formMessage.kind === "error" ? panel_module_css_default.formError : panel_module_css_default.formSuccess,
|
|
4166
|
+
children: formMessage.text
|
|
4167
|
+
}) : null,
|
|
4168
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4169
|
+
className: panel_module_css_default.buttons,
|
|
4170
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4171
|
+
type: "submit",
|
|
4172
|
+
className: panel_module_css_default.button + " " + panel_module_css_default.primary,
|
|
4173
|
+
disabled: formBusy,
|
|
4174
|
+
children: formBusy ? tt("form.busy") : tt("form.submit")
|
|
4175
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4176
|
+
type: "button",
|
|
4177
|
+
className: panel_module_css_default.button,
|
|
4178
|
+
onClick: () => {
|
|
4179
|
+
setShowForm(false);
|
|
4180
|
+
setFormMessage(null);
|
|
4181
|
+
},
|
|
4182
|
+
children: tt("form.cancel")
|
|
4183
|
+
})]
|
|
4184
|
+
})
|
|
4185
|
+
]
|
|
4186
|
+
}) : null,
|
|
4187
|
+
loadError !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4188
|
+
className: panel_module_css_default.errorBanner,
|
|
4189
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: loadError }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4190
|
+
type: "button",
|
|
4191
|
+
className: panel_module_css_default.button,
|
|
4192
|
+
onClick: () => {
|
|
4193
|
+
setLoadError(null);
|
|
4194
|
+
},
|
|
4195
|
+
children: tt("err.dismiss")
|
|
4196
|
+
})]
|
|
4197
|
+
}) : null,
|
|
4198
|
+
successBanner !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4199
|
+
className: panel_module_css_default.successBanner,
|
|
4200
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: successBanner }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4201
|
+
type: "button",
|
|
4202
|
+
className: panel_module_css_default.button,
|
|
4203
|
+
onClick: () => {
|
|
4204
|
+
setSuccessBanner(null);
|
|
4205
|
+
},
|
|
4206
|
+
children: tt("err.dismiss")
|
|
4207
|
+
})]
|
|
4208
|
+
}) : null,
|
|
4209
|
+
showLegend ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4210
|
+
className: panel_module_css_default.legend,
|
|
4211
|
+
children: [
|
|
4212
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4213
|
+
className: panel_module_css_default.legendItem,
|
|
4214
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4215
|
+
className: panel_module_css_default.dot + " " + panel_module_css_default.dotModel,
|
|
4216
|
+
style: dotStyle(hubConfig?.dotModelColor)
|
|
4217
|
+
}), tt("legend.model")]
|
|
4218
|
+
}),
|
|
4219
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4220
|
+
className: panel_module_css_default.legendItem,
|
|
4221
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4222
|
+
className: panel_module_css_default.dot + " " + panel_module_css_default.dotUser,
|
|
4223
|
+
style: dotStyle(hubConfig?.dotUserColor)
|
|
4224
|
+
}), tt("legend.user")]
|
|
4225
|
+
}),
|
|
4226
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4227
|
+
className: panel_module_css_default.legendHint,
|
|
4228
|
+
children: tt("legend.hint")
|
|
4229
|
+
})
|
|
4230
|
+
]
|
|
4231
|
+
}) : null,
|
|
4232
|
+
loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4233
|
+
className: panel_module_css_default.empty,
|
|
4234
|
+
children: tt("panel.loading")
|
|
4235
|
+
}) : null,
|
|
4236
|
+
detailLoading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4237
|
+
className: panel_module_css_default.empty,
|
|
4238
|
+
children: tt("detail.loading")
|
|
4239
|
+
}) : null,
|
|
4240
|
+
tab === "market" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarketView, { hub }) : catalog !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
4241
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4242
|
+
className: panel_module_css_default.filterBar,
|
|
4243
|
+
children: [
|
|
4244
|
+
tab === "sources" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4245
|
+
className: panel_module_css_default.segmented,
|
|
4246
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4247
|
+
type: "button",
|
|
4248
|
+
className: panel_module_css_default.segBtn + (skillView === "flat" ? " " + panel_module_css_default.segBtnActive : ""),
|
|
4249
|
+
onClick: () => {
|
|
4250
|
+
setSkillView("flat");
|
|
4251
|
+
},
|
|
4252
|
+
children: tt("view.flat")
|
|
4253
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4254
|
+
type: "button",
|
|
4255
|
+
className: panel_module_css_default.segBtn + (skillView === "groups" ? " " + panel_module_css_default.segBtnActive : ""),
|
|
4256
|
+
onClick: () => {
|
|
4257
|
+
setSkillView("groups");
|
|
4258
|
+
},
|
|
4259
|
+
children: tt("view.grouped")
|
|
4260
|
+
})]
|
|
4261
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
4262
|
+
className: panel_module_css_default.select,
|
|
4263
|
+
value: sourceFilter,
|
|
4264
|
+
onChange: (event) => {
|
|
4265
|
+
setSourceFilter(event.target.value);
|
|
4266
|
+
},
|
|
4267
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4268
|
+
value: "all",
|
|
4269
|
+
children: tt("filter.allSources")
|
|
4270
|
+
}), sourceOptions.map((source) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4271
|
+
value: source,
|
|
4272
|
+
children: source === "private" ? tt("filter.private") : source
|
|
4273
|
+
}, source))]
|
|
4274
|
+
})] }) : null,
|
|
4275
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
4276
|
+
className: panel_module_css_default.select,
|
|
4277
|
+
value: sortKey,
|
|
4278
|
+
onChange: (event) => {
|
|
4279
|
+
setSortKey(event.target.value);
|
|
4280
|
+
},
|
|
4281
|
+
children: [
|
|
4282
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4283
|
+
value: "name",
|
|
4284
|
+
children: tt("sort.name")
|
|
4285
|
+
}),
|
|
4286
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4287
|
+
value: "added",
|
|
4288
|
+
children: tt("sort.added")
|
|
4289
|
+
}),
|
|
4290
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4291
|
+
value: "uses",
|
|
4292
|
+
children: tt("sort.uses")
|
|
4293
|
+
})
|
|
4294
|
+
]
|
|
4295
|
+
}),
|
|
4296
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4297
|
+
className: panel_module_css_default.search,
|
|
4298
|
+
value: search,
|
|
4299
|
+
onChange: (event) => {
|
|
4300
|
+
setSearch(event.target.value);
|
|
4301
|
+
},
|
|
4302
|
+
placeholder: tt("panel.search")
|
|
4303
|
+
})
|
|
4304
|
+
]
|
|
4305
|
+
}),
|
|
4306
|
+
filtered.length === 0 && search.trim() !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4307
|
+
className: panel_module_css_default.empty,
|
|
4308
|
+
children: tt("panel.empty")
|
|
4309
|
+
}) : null,
|
|
4310
|
+
filtered.length === 0 && search.trim() === "" && catalog.skills.length === 0 && catalog.disabled.length === 0 && catalog.diagnostics.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4311
|
+
className: panel_module_css_default.empty,
|
|
4312
|
+
children: tt("panel.emptyAll")
|
|
4313
|
+
}) : null,
|
|
4314
|
+
tab === "sources" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SourcesView, { hub }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScenesView, { hub }),
|
|
4315
|
+
sourcesState !== null && sourcesState.trash.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
4316
|
+
className: panel_module_css_default.section,
|
|
4317
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4318
|
+
className: panel_module_css_default.sectionTitle + " " + panel_module_css_default.sectionHeadRow,
|
|
4319
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4320
|
+
className: panel_module_css_default.sectionTitleFill,
|
|
4321
|
+
children: tt("source.trash")
|
|
4322
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4323
|
+
type: "button",
|
|
4324
|
+
className: panel_module_css_default.opBtn + " " + panel_module_css_default.opDanger,
|
|
4325
|
+
disabled: tagBusy,
|
|
4326
|
+
onClick: () => {
|
|
4327
|
+
setConfirmClearTrash(true);
|
|
4328
|
+
},
|
|
4329
|
+
children: tt("source.clearTrash")
|
|
4330
|
+
})]
|
|
4331
|
+
}), sourcesState.trash.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4332
|
+
className: panel_module_css_default.row + " " + panel_module_css_default.rowStatic,
|
|
4333
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4334
|
+
className: panel_module_css_default.rowMain,
|
|
4335
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4336
|
+
className: panel_module_css_default.rowName,
|
|
4337
|
+
children: entry.name
|
|
4338
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4339
|
+
className: panel_module_css_default.rowDesc,
|
|
4340
|
+
children: relativeTimeText(entry.movedAt)
|
|
4341
|
+
})]
|
|
4342
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4343
|
+
type: "button",
|
|
4344
|
+
className: panel_module_css_default.opBtn,
|
|
4345
|
+
disabled: tagBusy,
|
|
4346
|
+
onClick: () => {
|
|
4347
|
+
restoreTrash(entry.name);
|
|
4348
|
+
},
|
|
4349
|
+
children: tt("source.restore")
|
|
4350
|
+
})]
|
|
4351
|
+
}, entry.name))]
|
|
4352
|
+
}) : null,
|
|
4353
|
+
catalog.disabled.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
4354
|
+
className: panel_module_css_default.section,
|
|
4355
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4356
|
+
className: panel_module_css_default.sectionTitle,
|
|
4357
|
+
children: tt("panel.disabled")
|
|
4358
|
+
}), catalog.disabled.filter((record) => (normalized.length === 0 || record.name.toLocaleLowerCase().includes(normalized) || record.description.toLocaleLowerCase().includes(normalized)) && (sourceFilter === "all" || (origins[record.name] ?? "private") === sourceFilter)).map((record) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DisabledRow, {
|
|
4359
|
+
record,
|
|
4360
|
+
busy: busyNames.has(record.name),
|
|
4361
|
+
onEnable: () => {
|
|
4362
|
+
enableDisabled(record);
|
|
4363
|
+
}
|
|
4364
|
+
}, record.name))]
|
|
4365
|
+
}) : null,
|
|
4366
|
+
catalog.diagnostics.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
4367
|
+
className: panel_module_css_default.section,
|
|
4368
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4369
|
+
className: panel_module_css_default.sectionTitle,
|
|
4370
|
+
children: tt("panel.diagnostics")
|
|
4371
|
+
}), catalog.diagnostics.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4372
|
+
className: panel_module_css_default.diagRow,
|
|
4373
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4374
|
+
className: panel_module_css_default.diagPath,
|
|
4375
|
+
children: entry.path
|
|
4376
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4377
|
+
className: panel_module_css_default.diagReason,
|
|
4378
|
+
children: entry.reason
|
|
4379
|
+
})]
|
|
4380
|
+
}, entry.path))]
|
|
4381
|
+
}) : null
|
|
4382
|
+
] }) : null,
|
|
4383
|
+
conflictDialog !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConflictDialog, {
|
|
4384
|
+
dialog: conflictDialog,
|
|
4385
|
+
tags: groupsState?.tags ?? [],
|
|
4386
|
+
collections: groupsState?.collections ?? [],
|
|
4387
|
+
onClose: () => {
|
|
4388
|
+
setConflictDialog(null);
|
|
4389
|
+
},
|
|
4390
|
+
onKeepOn: () => {
|
|
4391
|
+
resolveConflict(false);
|
|
4392
|
+
},
|
|
4393
|
+
onCloseAll: () => {
|
|
4394
|
+
resolveConflict(true);
|
|
4395
|
+
}
|
|
4396
|
+
}) : null,
|
|
4397
|
+
confirmDialog !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
|
|
4398
|
+
title: confirmDialog.kind === "sync" ? tt("source.syncConfirmTitle") : tt("source.deleteConfirmTitle"),
|
|
4399
|
+
text: confirmDialog.kind === "sync" ? tt("source.syncConfirmText") : tt("source.deleteConfirmText"),
|
|
4400
|
+
items: confirmDialog.skills,
|
|
4401
|
+
confirmLabel: confirmDialog.kind === "sync" ? tt("source.sync") : tt("source.followDelete"),
|
|
4402
|
+
danger: confirmDialog.kind === "delete",
|
|
4403
|
+
onCancel: () => {
|
|
4404
|
+
setConfirmDialog(null);
|
|
4405
|
+
},
|
|
4406
|
+
onConfirm: () => {
|
|
4407
|
+
runConfirmed();
|
|
4408
|
+
}
|
|
4409
|
+
}) : null,
|
|
4410
|
+
branchChoice !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BranchChoiceDialog, {
|
|
4411
|
+
choice: branchChoice,
|
|
4412
|
+
busy: branchBusy,
|
|
4413
|
+
onSelect: (selected) => {
|
|
4414
|
+
setBranchChoice({
|
|
4415
|
+
...branchChoice,
|
|
4416
|
+
selected
|
|
4417
|
+
});
|
|
4418
|
+
},
|
|
4419
|
+
onCancel: () => {
|
|
4420
|
+
setBranchChoice(null);
|
|
4421
|
+
},
|
|
4422
|
+
onConfirm: () => {
|
|
4423
|
+
confirmBranchChoice();
|
|
4424
|
+
}
|
|
4425
|
+
}) : null,
|
|
4426
|
+
marketSyncDialog !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarketSyncDialog, {
|
|
4427
|
+
dialog: marketSyncDialog,
|
|
4428
|
+
busy: syncBusy,
|
|
4429
|
+
onToggle: (name, checked) => {
|
|
4430
|
+
const next = new Set(marketSyncDialog.selected);
|
|
4431
|
+
if (checked) next.add(name);
|
|
4432
|
+
else next.delete(name);
|
|
4433
|
+
setMarketSyncDialog({
|
|
4434
|
+
...marketSyncDialog,
|
|
4435
|
+
selected: next
|
|
4436
|
+
});
|
|
4437
|
+
},
|
|
4438
|
+
onCancel: () => {
|
|
4439
|
+
setMarketSyncDialog(null);
|
|
4440
|
+
},
|
|
4441
|
+
onConfirm: () => {
|
|
4442
|
+
confirmMarketSync();
|
|
4443
|
+
}
|
|
4444
|
+
}) : null,
|
|
4445
|
+
deleteSkillDialog !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
|
|
4446
|
+
title: tt("delete.confirmTitle"),
|
|
4447
|
+
text: tt("delete.confirmText", { name: deleteSkillDialog }),
|
|
4448
|
+
confirmLabel: tt("delete.confirm"),
|
|
4449
|
+
danger: true,
|
|
4450
|
+
onCancel: () => {
|
|
4451
|
+
setDeleteSkillDialog(null);
|
|
4452
|
+
},
|
|
4453
|
+
onConfirm: () => {
|
|
4454
|
+
runDeleteSkill();
|
|
4455
|
+
}
|
|
4456
|
+
}) : null,
|
|
4457
|
+
confirmClearTrash ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
|
|
4458
|
+
title: tt("source.clearTrashConfirmTitle"),
|
|
4459
|
+
text: tt("source.clearTrashConfirmText"),
|
|
4460
|
+
confirmLabel: tt("source.clearTrashConfirm"),
|
|
4461
|
+
danger: true,
|
|
4462
|
+
onCancel: () => {
|
|
4463
|
+
setConfirmClearTrash(false);
|
|
4464
|
+
},
|
|
4465
|
+
onConfirm: () => {
|
|
4466
|
+
clearTrash();
|
|
4467
|
+
}
|
|
4468
|
+
}) : null
|
|
4469
|
+
]
|
|
4470
|
+
});
|
|
4471
|
+
}
|
|
4472
|
+
//#endregion
|
|
4473
|
+
//#region src/client/index.tsx
|
|
4474
|
+
/** Locale namespace this plugin owns. */
|
|
4475
|
+
const NS = "dsh-skill-hub";
|
|
4476
|
+
/** Required services (fiber inject waiting — the runtime must be up first). */
|
|
4477
|
+
const inject = ["slots", "locale"];
|
|
4478
|
+
/**
|
|
4479
|
+
* Mount the settings card and the skill hub section.
|
|
4480
|
+
* @param ctx - client root context (slots, locale).
|
|
4481
|
+
*/
|
|
4482
|
+
function apply(ctx) {
|
|
4483
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
4484
|
+
zh,
|
|
4485
|
+
en
|
|
4486
|
+
}), "dsh-skill-hub: dictionaries");
|
|
4487
|
+
const t = ctx.locale.bind(NS);
|
|
4488
|
+
const api = new SkillHubApi();
|
|
4489
|
+
const settingsCard = new SkillHubSettingsCardController(new ApiConfigScope(api));
|
|
4490
|
+
ctx.effect(() => ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
|
|
4491
|
+
name: "settings.plugin.item",
|
|
4492
|
+
id: "dsh-skill-hub",
|
|
4493
|
+
order: 115,
|
|
4494
|
+
locale: NS,
|
|
4495
|
+
inject: () => settingsCard.inject()
|
|
4496
|
+
}, SkillHubSettingsCard)), "dsh-skill-hub: settings card");
|
|
4497
|
+
ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
4498
|
+
name: "settings.section",
|
|
4499
|
+
id: "skill-hub",
|
|
4500
|
+
order: 12,
|
|
4501
|
+
label: () => t("entry.label"),
|
|
4502
|
+
locale: NS,
|
|
4503
|
+
inject: () => ({})
|
|
4504
|
+
}, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillHubPanel, { api }))), "dsh-skill-hub: settings section");
|
|
4505
|
+
ctx.effect(() => applySettingsNavIcon(), "dsh-skill-hub: settings nav icon");
|
|
4506
|
+
}
|
|
4507
|
+
//#endregion
|
|
4508
|
+
exports.apply = apply;
|
|
4509
|
+
exports.inject = inject;
|
|
4510
|
+
return module.exports;
|
|
4511
|
+
}
|
|
4512
|
+
});
|
|
4513
|
+
|
|
4514
|
+
//# sourceMappingURL=client.js.map
|