dsh-skill-importer 0.1.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/LICENSE +21 -0
- package/README.md +98 -0
- package/lib/client.js +1235 -0
- package/lib/index.js +463 -0
- package/lib/types/client/SkillImporterSection.d.ts +16 -0
- package/lib/types/client/SkillsPicker.d.ts +16 -0
- package/lib/types/client/index.d.ts +39 -0
- package/lib/types/client/locales.d.ts +117 -0
- package/lib/types/client/name.d.ts +5 -0
- package/lib/types/frontmatter.d.ts +39 -0
- package/lib/types/index.d.ts +34 -0
- package/lib/types/server.d.ts +83 -0
- package/lib/types/types.d.ts +64 -0
- package/package.json +90 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,1235 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-skill-importer",
|
|
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 react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
|
+
//#region src/frontmatter.ts
|
|
11
|
+
/** Kebab-case name rule shared with `dsh-skill` (`^[a-z0-9]+(?:-[a-z0-9]+)*$`). */
|
|
12
|
+
const KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
13
|
+
/** True when the name satisfies the harness skill-name rule. */
|
|
14
|
+
function isValidSkillName(name) {
|
|
15
|
+
return KEBAB_CASE.test(name);
|
|
16
|
+
}
|
|
17
|
+
const BOOLEAN_TRUE = /* @__PURE__ */ new Set([
|
|
18
|
+
"true",
|
|
19
|
+
"yes",
|
|
20
|
+
"on",
|
|
21
|
+
"1"
|
|
22
|
+
]);
|
|
23
|
+
const BOOLEAN_FALSE = /* @__PURE__ */ new Set([
|
|
24
|
+
"false",
|
|
25
|
+
"no",
|
|
26
|
+
"off",
|
|
27
|
+
"0"
|
|
28
|
+
]);
|
|
29
|
+
/** Parse one frontmatter scalar: quoted strings, booleans, or plain text. */
|
|
30
|
+
function parseScalar(raw) {
|
|
31
|
+
const value = raw.trim();
|
|
32
|
+
if (value.length === 0) return void 0;
|
|
33
|
+
const lower = value.toLowerCase();
|
|
34
|
+
if (BOOLEAN_TRUE.has(lower)) return true;
|
|
35
|
+
if (BOOLEAN_FALSE.has(lower)) return false;
|
|
36
|
+
if (value.startsWith("\"") && value.endsWith("\"") && value.length >= 2 || value.startsWith("'") && value.endsWith("'") && value.length >= 2) return value.slice(1, -1);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parse a skill Markdown file's frontmatter block.
|
|
41
|
+
* @param text - full file text.
|
|
42
|
+
* @returns parsed frontmatter and body; a file without a leading `---` block
|
|
43
|
+
* yields an empty frontmatter with the whole text as body.
|
|
44
|
+
*/
|
|
45
|
+
function parseSkillFile(text) {
|
|
46
|
+
const frontmatter = {};
|
|
47
|
+
if (!text.startsWith("---\n")) return {
|
|
48
|
+
frontmatter,
|
|
49
|
+
body: text
|
|
50
|
+
};
|
|
51
|
+
const end = text.indexOf("\n---", 4);
|
|
52
|
+
if (end < 0) return {
|
|
53
|
+
frontmatter,
|
|
54
|
+
body: text
|
|
55
|
+
};
|
|
56
|
+
const block = text.slice(4, end);
|
|
57
|
+
const body = text.slice(end + 4).replace(/^\n/, "");
|
|
58
|
+
for (const line of block.split("\n")) {
|
|
59
|
+
const colon = line.indexOf(":");
|
|
60
|
+
if (colon <= 0) continue;
|
|
61
|
+
const key = line.slice(0, colon).trim();
|
|
62
|
+
const value = parseScalar(line.slice(colon + 1));
|
|
63
|
+
if (value === void 0) continue;
|
|
64
|
+
switch (key) {
|
|
65
|
+
case "name":
|
|
66
|
+
if (typeof value === "string") frontmatter.name = value;
|
|
67
|
+
break;
|
|
68
|
+
case "description":
|
|
69
|
+
if (typeof value === "string") frontmatter.description = value;
|
|
70
|
+
break;
|
|
71
|
+
case "whenToUse":
|
|
72
|
+
if (typeof value === "string") frontmatter.whenToUse = value;
|
|
73
|
+
break;
|
|
74
|
+
case "disable-model-invocation":
|
|
75
|
+
if (typeof value === "boolean") frontmatter.disableModelInvocation = value;
|
|
76
|
+
break;
|
|
77
|
+
case "user-invocable": if (typeof value === "boolean") frontmatter.userInvocable = value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
frontmatter,
|
|
82
|
+
body
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Why a parsed file cannot be imported as a skill (or undefined when it can). */
|
|
86
|
+
function validateSkillFile(text) {
|
|
87
|
+
const { frontmatter } = parseSkillFile(text);
|
|
88
|
+
if (frontmatter.name === void 0 || frontmatter.name.length === 0) return "frontmatter 缺少 name 字段";
|
|
89
|
+
if (!isValidSkillName(frontmatter.name)) return "name 必须是 kebab-case(小写字母、数字、短横线)";
|
|
90
|
+
if (frontmatter.description === void 0 || frontmatter.description.length === 0) return "frontmatter 缺少 description 字段";
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/client/name.ts
|
|
94
|
+
/**
|
|
95
|
+
* URL-derived kebab-case skill name (pure, browser-safe).
|
|
96
|
+
*/
|
|
97
|
+
/** Kebab-case name for a URL import (derived from the URL's last path segment). */
|
|
98
|
+
function nameFromUrl(url) {
|
|
99
|
+
return ((url.split(/[?#]/)[0]?.replace(/\/+$/, "") ?? "").split("/").filter(Boolean).pop() ?? "skill").replace(/\.md$/i, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "skill";
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/client/SkillImporterSection.tsx
|
|
103
|
+
/**
|
|
104
|
+
* Skill importer settings section, styled after the harness's General
|
|
105
|
+
* settings rows: one Setting-Cell per block (title + control, hairline
|
|
106
|
+
* separator), design tokens throughout, full zh/en copy.
|
|
107
|
+
*
|
|
108
|
+
* Rows: entry-points description · installed list · file/URL import · target
|
|
109
|
+
* directory · import action. Pure presentation — all data and actions arrive
|
|
110
|
+
* through the four props shares.
|
|
111
|
+
*/
|
|
112
|
+
const MAX_FILE_BYTES = 262144;
|
|
113
|
+
/** Read a picked file's text (browser-local; nothing crosses the wire except the import itself). */
|
|
114
|
+
function readFileText(file) {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const reader = new FileReader();
|
|
117
|
+
reader.onload = () => resolve(String(reader.result ?? ""));
|
|
118
|
+
reader.onerror = () => reject(/* @__PURE__ */ new Error("FileReader failed"));
|
|
119
|
+
reader.readAsText(file);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/** The settings section's page body. */
|
|
123
|
+
function SkillImporterSection({ t, useSkills, useWorkspaces, actions }) {
|
|
124
|
+
const skills = useSkills((value) => value);
|
|
125
|
+
const workspaces = useWorkspaces((value) => value);
|
|
126
|
+
const workspace = (0, react.useMemo)(() => workspaces.items.find((item) => item.workspaceId === workspaces.recentWorkspaceId) ?? workspaces.items[0], [workspaces.items, workspaces.recentWorkspaceId]);
|
|
127
|
+
const [listState, setListState] = (0, react.useState)("idle");
|
|
128
|
+
const [listError, setListError] = (0, react.useState)();
|
|
129
|
+
const [listEpoch, setListEpoch] = (0, react.useState)(0);
|
|
130
|
+
(0, react.useEffect)(() => {
|
|
131
|
+
let cancelled = false;
|
|
132
|
+
setListState("loading");
|
|
133
|
+
actions.refreshSkills().then(() => {
|
|
134
|
+
if (!cancelled) setListState("idle");
|
|
135
|
+
}, (error) => {
|
|
136
|
+
if (!cancelled) {
|
|
137
|
+
setListState("error");
|
|
138
|
+
setListError(String(error));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
return () => {
|
|
142
|
+
cancelled = true;
|
|
143
|
+
};
|
|
144
|
+
}, [listEpoch, actions]);
|
|
145
|
+
const [mode, setMode] = (0, react.useState)("file");
|
|
146
|
+
const [target, setTarget] = (0, react.useState)("project-agents");
|
|
147
|
+
const [fileDraft, setFileDraft] = (0, react.useState)();
|
|
148
|
+
const [fileError, setFileError] = (0, react.useState)();
|
|
149
|
+
const [url, setUrl] = (0, react.useState)("");
|
|
150
|
+
const [urlName, setUrlName] = (0, react.useState)("");
|
|
151
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
152
|
+
const [message, setMessage] = (0, react.useState)();
|
|
153
|
+
const [messageError, setMessageError] = (0, react.useState)(false);
|
|
154
|
+
const fileInput = (0, react.useRef)(null);
|
|
155
|
+
const onFilePicked = async (file) => {
|
|
156
|
+
setFileDraft(void 0);
|
|
157
|
+
setFileError(void 0);
|
|
158
|
+
if (file === void 0) return;
|
|
159
|
+
if (file.size > MAX_FILE_BYTES) {
|
|
160
|
+
setFileError(t("fileTooLarge"));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
let text;
|
|
164
|
+
try {
|
|
165
|
+
text = await readFileText(file);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
setFileError(`${t("unreadableFile")}: ${String(error)}`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const problem = validateSkillFile(text);
|
|
171
|
+
if (problem !== void 0) {
|
|
172
|
+
setFileError(problem);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const { frontmatter } = parseSkillFile(text);
|
|
176
|
+
setFileDraft({
|
|
177
|
+
fileName: file.name,
|
|
178
|
+
text,
|
|
179
|
+
name: frontmatter.name ?? "",
|
|
180
|
+
description: frontmatter.description ?? "",
|
|
181
|
+
whenToUse: frontmatter.whenToUse,
|
|
182
|
+
userInvocable: frontmatter.userInvocable !== false,
|
|
183
|
+
modelInvocable: frontmatter.disableModelInvocation !== true
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
const effectiveUrlName = urlName.trim() || nameFromUrl(url);
|
|
187
|
+
const pollForSkill = (name, attemptsLeft) => {
|
|
188
|
+
if (attemptsLeft <= 0) return;
|
|
189
|
+
window.setTimeout(() => {
|
|
190
|
+
actions.refreshSkills().then((rows) => {
|
|
191
|
+
if (!rows.some((skill) => skill.name === name)) pollForSkill(name, attemptsLeft - 1);
|
|
192
|
+
}, () => pollForSkill(name, attemptsLeft - 1));
|
|
193
|
+
}, 2e3);
|
|
194
|
+
};
|
|
195
|
+
const removeSkill = async (skill) => {
|
|
196
|
+
if (!window.confirm(t("deleteConfirm", { name: skill.name }))) return;
|
|
197
|
+
setBusy(true);
|
|
198
|
+
setMessage(void 0);
|
|
199
|
+
setMessageError(false);
|
|
200
|
+
try {
|
|
201
|
+
if (await actions.deleteSkill({
|
|
202
|
+
name: skill.name,
|
|
203
|
+
source: skill.source,
|
|
204
|
+
workspacePath: workspace?.path
|
|
205
|
+
})) setMessage(t("deleted"));
|
|
206
|
+
setListEpoch((epoch) => epoch + 1);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
setMessageError(true);
|
|
209
|
+
setMessage(`${t("deleteFailed")}:${String(error)}`);
|
|
210
|
+
} finally {
|
|
211
|
+
setBusy(false);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const submit = async () => {
|
|
215
|
+
setBusy(true);
|
|
216
|
+
setMessage(void 0);
|
|
217
|
+
setMessageError(false);
|
|
218
|
+
try {
|
|
219
|
+
let path;
|
|
220
|
+
if (mode === "file") {
|
|
221
|
+
if (fileDraft === void 0) return;
|
|
222
|
+
path = await actions.importSkill({
|
|
223
|
+
name: fileDraft.name,
|
|
224
|
+
target,
|
|
225
|
+
content: fileDraft.text,
|
|
226
|
+
workspacePath: workspace?.path
|
|
227
|
+
});
|
|
228
|
+
pollForSkill(fileDraft.name, 10);
|
|
229
|
+
} else {
|
|
230
|
+
path = await actions.importUrl({
|
|
231
|
+
name: effectiveUrlName,
|
|
232
|
+
target,
|
|
233
|
+
url: url.trim(),
|
|
234
|
+
workspacePath: workspace?.path
|
|
235
|
+
});
|
|
236
|
+
pollForSkill(effectiveUrlName, 10);
|
|
237
|
+
}
|
|
238
|
+
setMessage(`${t("imported")}${path}`);
|
|
239
|
+
setFileDraft(void 0);
|
|
240
|
+
setFileError(void 0);
|
|
241
|
+
if (fileInput.current !== null) fileInput.current.value = "";
|
|
242
|
+
setUrl("");
|
|
243
|
+
setUrlName("");
|
|
244
|
+
setListEpoch((epoch) => epoch + 1);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
setMessageError(true);
|
|
247
|
+
setMessage(`${t("importFailed")}:${String(error)}`);
|
|
248
|
+
} finally {
|
|
249
|
+
setBusy(false);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const canSubmit = busy || mode === "file" && fileDraft === void 0 || mode === "url" && (url.trim().length === 0 || !/^https?:\/\//i.test(url.trim())) || target !== "user" && workspace === void 0;
|
|
253
|
+
const flags = (skill) => {
|
|
254
|
+
if (!skill.modelInvocable && skill.userInvocable) return t("userOnly");
|
|
255
|
+
if (skill.modelInvocable && !skill.userInvocable) return t("modelOnly");
|
|
256
|
+
};
|
|
257
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
258
|
+
style: {
|
|
259
|
+
display: "flex",
|
|
260
|
+
flexDirection: "column",
|
|
261
|
+
width: "100%"
|
|
262
|
+
},
|
|
263
|
+
children: [
|
|
264
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
265
|
+
style: rowStyle$1,
|
|
266
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
267
|
+
style: rowTextStyle,
|
|
268
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
269
|
+
style: titleStyle,
|
|
270
|
+
children: t("entryTitle")
|
|
271
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
272
|
+
style: descriptionStyle,
|
|
273
|
+
children: t("entryDescription")
|
|
274
|
+
})]
|
|
275
|
+
})
|
|
276
|
+
}),
|
|
277
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
278
|
+
style: rowStyle$1,
|
|
279
|
+
children: [
|
|
280
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
281
|
+
style: {
|
|
282
|
+
display: "flex",
|
|
283
|
+
alignItems: "center",
|
|
284
|
+
gap: 8
|
|
285
|
+
},
|
|
286
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
287
|
+
style: {
|
|
288
|
+
...titleStyle,
|
|
289
|
+
flex: 1
|
|
290
|
+
},
|
|
291
|
+
children: t("installedTitle")
|
|
292
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
293
|
+
type: "button",
|
|
294
|
+
onClick: () => setListEpoch((epoch) => epoch + 1),
|
|
295
|
+
style: pillStyle,
|
|
296
|
+
children: listState === "loading" ? "…" : t("refresh")
|
|
297
|
+
})]
|
|
298
|
+
}),
|
|
299
|
+
listState === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
300
|
+
style: {
|
|
301
|
+
...descriptionStyle,
|
|
302
|
+
color: "var(--dsw-alias-state-error-primary)"
|
|
303
|
+
},
|
|
304
|
+
children: [
|
|
305
|
+
t("installedFailed"),
|
|
306
|
+
": ",
|
|
307
|
+
listError
|
|
308
|
+
]
|
|
309
|
+
}) : null,
|
|
310
|
+
listState === "idle" && skills.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
311
|
+
style: descriptionStyle,
|
|
312
|
+
children: t("installedEmpty")
|
|
313
|
+
}) : null,
|
|
314
|
+
skills.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
315
|
+
style: {
|
|
316
|
+
display: "flex",
|
|
317
|
+
flexDirection: "column"
|
|
318
|
+
},
|
|
319
|
+
children: [
|
|
320
|
+
"project-agents",
|
|
321
|
+
"project-dsh",
|
|
322
|
+
"user"
|
|
323
|
+
].map((source) => {
|
|
324
|
+
const rows = skills.filter((skill) => skill.source === source);
|
|
325
|
+
if (rows.length === 0) return null;
|
|
326
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
327
|
+
style: {
|
|
328
|
+
display: "flex",
|
|
329
|
+
flexDirection: "column",
|
|
330
|
+
gap: 4
|
|
331
|
+
},
|
|
332
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
333
|
+
style: {
|
|
334
|
+
...descriptionStyle,
|
|
335
|
+
fontWeight: 600,
|
|
336
|
+
textTransform: "uppercase",
|
|
337
|
+
letterSpacing: "0.04em",
|
|
338
|
+
marginTop: 8
|
|
339
|
+
},
|
|
340
|
+
children: t(`source${source === "project-agents" ? "Agents" : source === "project-dsh" ? "Dsh" : "User"}`)
|
|
341
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
342
|
+
style: {
|
|
343
|
+
listStyle: "none",
|
|
344
|
+
margin: 0,
|
|
345
|
+
padding: 0,
|
|
346
|
+
display: "flex",
|
|
347
|
+
flexDirection: "column"
|
|
348
|
+
},
|
|
349
|
+
children: rows.map((skill) => {
|
|
350
|
+
const flag = flags(skill);
|
|
351
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
352
|
+
style: {
|
|
353
|
+
display: "flex",
|
|
354
|
+
alignItems: "center",
|
|
355
|
+
gap: 8,
|
|
356
|
+
padding: "6px 0"
|
|
357
|
+
},
|
|
358
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
359
|
+
style: {
|
|
360
|
+
flex: 1,
|
|
361
|
+
minWidth: 0,
|
|
362
|
+
display: "flex",
|
|
363
|
+
flexDirection: "column",
|
|
364
|
+
gap: 2
|
|
365
|
+
},
|
|
366
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
367
|
+
style: {
|
|
368
|
+
fontSize: 14,
|
|
369
|
+
fontWeight: 600,
|
|
370
|
+
color: "var(--dsw-alias-label-primary)"
|
|
371
|
+
},
|
|
372
|
+
children: [
|
|
373
|
+
"/",
|
|
374
|
+
skill.name,
|
|
375
|
+
flag !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
376
|
+
style: {
|
|
377
|
+
opacity: .6,
|
|
378
|
+
fontWeight: 400,
|
|
379
|
+
marginLeft: 8
|
|
380
|
+
},
|
|
381
|
+
children: flag
|
|
382
|
+
}) : null
|
|
383
|
+
]
|
|
384
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
385
|
+
style: {
|
|
386
|
+
fontSize: 13,
|
|
387
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
388
|
+
},
|
|
389
|
+
children: skill.description
|
|
390
|
+
})]
|
|
391
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
392
|
+
type: "button",
|
|
393
|
+
onClick: () => {
|
|
394
|
+
removeSkill(skill);
|
|
395
|
+
},
|
|
396
|
+
style: {
|
|
397
|
+
...pillStyle,
|
|
398
|
+
flex: "none",
|
|
399
|
+
color: "var(--dsw-alias-state-error-primary)"
|
|
400
|
+
},
|
|
401
|
+
children: t("delete")
|
|
402
|
+
})]
|
|
403
|
+
}, skill.source + ":" + skill.name);
|
|
404
|
+
})
|
|
405
|
+
})]
|
|
406
|
+
}, source);
|
|
407
|
+
})
|
|
408
|
+
}) : null
|
|
409
|
+
]
|
|
410
|
+
}),
|
|
411
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
412
|
+
style: rowStyle$1,
|
|
413
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
414
|
+
style: rowTextStyle,
|
|
415
|
+
children: [
|
|
416
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
417
|
+
style: titleStyle,
|
|
418
|
+
children: t("importTitle")
|
|
419
|
+
}),
|
|
420
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
421
|
+
role: "tablist",
|
|
422
|
+
style: {
|
|
423
|
+
display: "flex",
|
|
424
|
+
gap: 8,
|
|
425
|
+
marginTop: 4
|
|
426
|
+
},
|
|
427
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
428
|
+
type: "button",
|
|
429
|
+
role: "tab",
|
|
430
|
+
"aria-selected": mode === "file",
|
|
431
|
+
onClick: () => setMode("file"),
|
|
432
|
+
style: segmentStyle(mode === "file"),
|
|
433
|
+
children: t("fileTab")
|
|
434
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
435
|
+
type: "button",
|
|
436
|
+
role: "tab",
|
|
437
|
+
"aria-selected": mode === "url",
|
|
438
|
+
onClick: () => setMode("url"),
|
|
439
|
+
style: segmentStyle(mode === "url"),
|
|
440
|
+
children: t("urlTab")
|
|
441
|
+
})]
|
|
442
|
+
}),
|
|
443
|
+
mode === "file" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
444
|
+
style: {
|
|
445
|
+
display: "flex",
|
|
446
|
+
flexDirection: "column",
|
|
447
|
+
gap: 8,
|
|
448
|
+
marginTop: 12
|
|
449
|
+
},
|
|
450
|
+
children: [
|
|
451
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
452
|
+
ref: fileInput,
|
|
453
|
+
type: "file",
|
|
454
|
+
accept: ".md,.markdown,text/markdown,text/plain",
|
|
455
|
+
onChange: (event) => {
|
|
456
|
+
onFilePicked(event.target.files?.[0]);
|
|
457
|
+
},
|
|
458
|
+
style: {
|
|
459
|
+
...controlStyle,
|
|
460
|
+
padding: 8
|
|
461
|
+
}
|
|
462
|
+
}),
|
|
463
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
464
|
+
style: descriptionStyle,
|
|
465
|
+
children: t("fileHelp")
|
|
466
|
+
}),
|
|
467
|
+
fileError !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
468
|
+
style: {
|
|
469
|
+
...descriptionStyle,
|
|
470
|
+
color: "var(--dsw-alias-state-error-primary)"
|
|
471
|
+
},
|
|
472
|
+
children: fileError
|
|
473
|
+
}) : null,
|
|
474
|
+
fileDraft !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
475
|
+
style: {
|
|
476
|
+
display: "flex",
|
|
477
|
+
flexDirection: "column",
|
|
478
|
+
gap: 4,
|
|
479
|
+
fontSize: 13,
|
|
480
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
481
|
+
borderRadius: 8,
|
|
482
|
+
padding: 10,
|
|
483
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
484
|
+
},
|
|
485
|
+
children: [
|
|
486
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
487
|
+
style: {
|
|
488
|
+
fontWeight: 600,
|
|
489
|
+
color: "var(--dsw-alias-label-primary)"
|
|
490
|
+
},
|
|
491
|
+
children: t("previewTitle")
|
|
492
|
+
}),
|
|
493
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
494
|
+
t("previewName"),
|
|
495
|
+
": ",
|
|
496
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: fileDraft.name })
|
|
497
|
+
] }),
|
|
498
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
499
|
+
t("previewDescription"),
|
|
500
|
+
": ",
|
|
501
|
+
fileDraft.description
|
|
502
|
+
] }),
|
|
503
|
+
fileDraft.whenToUse !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
504
|
+
t("previewWhenToUse"),
|
|
505
|
+
": ",
|
|
506
|
+
fileDraft.whenToUse
|
|
507
|
+
] }) : null,
|
|
508
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
509
|
+
style: { opacity: .8 },
|
|
510
|
+
children: [
|
|
511
|
+
fileDraft.fileName,
|
|
512
|
+
" · ",
|
|
513
|
+
t("previewOk")
|
|
514
|
+
]
|
|
515
|
+
})
|
|
516
|
+
]
|
|
517
|
+
}) : null
|
|
518
|
+
]
|
|
519
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
520
|
+
style: {
|
|
521
|
+
display: "flex",
|
|
522
|
+
flexDirection: "column",
|
|
523
|
+
gap: 8,
|
|
524
|
+
marginTop: 12
|
|
525
|
+
},
|
|
526
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
527
|
+
style: {
|
|
528
|
+
display: "flex",
|
|
529
|
+
flexDirection: "column",
|
|
530
|
+
gap: 4,
|
|
531
|
+
fontSize: 13
|
|
532
|
+
},
|
|
533
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
534
|
+
style: {
|
|
535
|
+
fontWeight: 600,
|
|
536
|
+
color: "var(--dsw-alias-label-primary)"
|
|
537
|
+
},
|
|
538
|
+
children: t("urlLabel")
|
|
539
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
540
|
+
type: "url",
|
|
541
|
+
value: url,
|
|
542
|
+
placeholder: "https://example.com/my-skill.md",
|
|
543
|
+
onChange: (event) => setUrl(event.target.value),
|
|
544
|
+
style: controlStyle
|
|
545
|
+
})]
|
|
546
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
547
|
+
style: {
|
|
548
|
+
display: "flex",
|
|
549
|
+
flexDirection: "column",
|
|
550
|
+
gap: 4,
|
|
551
|
+
fontSize: 13
|
|
552
|
+
},
|
|
553
|
+
children: [
|
|
554
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
555
|
+
style: {
|
|
556
|
+
fontWeight: 600,
|
|
557
|
+
color: "var(--dsw-alias-label-primary)"
|
|
558
|
+
},
|
|
559
|
+
children: t("urlNameLabel")
|
|
560
|
+
}),
|
|
561
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
562
|
+
type: "text",
|
|
563
|
+
value: urlName,
|
|
564
|
+
placeholder: nameFromUrl(url) || "my-skill",
|
|
565
|
+
onChange: (event) => setUrlName(event.target.value),
|
|
566
|
+
style: controlStyle
|
|
567
|
+
}),
|
|
568
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
569
|
+
style: descriptionStyle,
|
|
570
|
+
children: t("urlNameHelp")
|
|
571
|
+
})
|
|
572
|
+
]
|
|
573
|
+
})]
|
|
574
|
+
}),
|
|
575
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
576
|
+
style: {
|
|
577
|
+
display: "flex",
|
|
578
|
+
flexDirection: "column",
|
|
579
|
+
gap: 4,
|
|
580
|
+
fontSize: 13,
|
|
581
|
+
marginTop: 12
|
|
582
|
+
},
|
|
583
|
+
children: [
|
|
584
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
585
|
+
style: {
|
|
586
|
+
fontWeight: 600,
|
|
587
|
+
color: "var(--dsw-alias-label-primary)"
|
|
588
|
+
},
|
|
589
|
+
children: t("targetLabel")
|
|
590
|
+
}),
|
|
591
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
592
|
+
value: target,
|
|
593
|
+
onChange: (event) => setTarget(event.target.value),
|
|
594
|
+
style: selectStyle,
|
|
595
|
+
children: [
|
|
596
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
597
|
+
value: "project-agents",
|
|
598
|
+
disabled: workspace === void 0,
|
|
599
|
+
children: workspace === void 0 ? t("targetProjectNoWorkspace") : t("targetProjectAgents", { title: workspace.title })
|
|
600
|
+
}),
|
|
601
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
602
|
+
value: "project-dsh",
|
|
603
|
+
disabled: workspace === void 0,
|
|
604
|
+
children: workspace === void 0 ? t("targetProjectNoWorkspace") : t("targetProjectDsh", { title: workspace.title })
|
|
605
|
+
}),
|
|
606
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
607
|
+
value: "user",
|
|
608
|
+
children: t("targetUser")
|
|
609
|
+
})
|
|
610
|
+
]
|
|
611
|
+
}),
|
|
612
|
+
workspace === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
613
|
+
style: descriptionStyle,
|
|
614
|
+
children: t("noWorkspace")
|
|
615
|
+
}) : null
|
|
616
|
+
]
|
|
617
|
+
}),
|
|
618
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
619
|
+
style: {
|
|
620
|
+
display: "flex",
|
|
621
|
+
alignItems: "center",
|
|
622
|
+
gap: 10,
|
|
623
|
+
marginTop: 12
|
|
624
|
+
},
|
|
625
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
626
|
+
type: "button",
|
|
627
|
+
disabled: canSubmit,
|
|
628
|
+
onClick: () => {
|
|
629
|
+
submit();
|
|
630
|
+
},
|
|
631
|
+
style: canSubmit ? primaryDisabledStyle : primaryStyle,
|
|
632
|
+
children: busy ? t("importing") : t("import")
|
|
633
|
+
}), message !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
634
|
+
style: {
|
|
635
|
+
fontSize: 12,
|
|
636
|
+
color: messageError ? "var(--dsw-alias-state-error-primary)" : "var(--dsw-alias-label-secondary)",
|
|
637
|
+
wordBreak: "break-all"
|
|
638
|
+
},
|
|
639
|
+
children: message
|
|
640
|
+
}) : null]
|
|
641
|
+
})
|
|
642
|
+
]
|
|
643
|
+
})
|
|
644
|
+
})
|
|
645
|
+
]
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
const rowStyle$1 = {
|
|
649
|
+
display: "flex",
|
|
650
|
+
flexDirection: "column",
|
|
651
|
+
gap: 8,
|
|
652
|
+
padding: "16px 0",
|
|
653
|
+
borderBottom: "1px solid var(--dsw-alias-border-l2)"
|
|
654
|
+
};
|
|
655
|
+
const rowTextStyle = {
|
|
656
|
+
display: "flex",
|
|
657
|
+
flexDirection: "column",
|
|
658
|
+
gap: 4,
|
|
659
|
+
minWidth: 0
|
|
660
|
+
};
|
|
661
|
+
const titleStyle = {
|
|
662
|
+
fontSize: 14,
|
|
663
|
+
fontWeight: 600,
|
|
664
|
+
lineHeight: "22px",
|
|
665
|
+
color: "var(--dsw-alias-label-primary)"
|
|
666
|
+
};
|
|
667
|
+
const descriptionStyle = {
|
|
668
|
+
fontSize: 12,
|
|
669
|
+
lineHeight: "18px",
|
|
670
|
+
color: "var(--dsw-alias-label-caption)"
|
|
671
|
+
};
|
|
672
|
+
const pillStyle = {
|
|
673
|
+
display: "inline-flex",
|
|
674
|
+
alignItems: "center",
|
|
675
|
+
height: 28,
|
|
676
|
+
padding: "0 12px",
|
|
677
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
678
|
+
borderRadius: 14,
|
|
679
|
+
background: "transparent",
|
|
680
|
+
font: "inherit",
|
|
681
|
+
fontSize: 13,
|
|
682
|
+
color: "var(--dsw-alias-label-primary)",
|
|
683
|
+
cursor: "pointer"
|
|
684
|
+
};
|
|
685
|
+
const controlStyle = {
|
|
686
|
+
padding: "7px 12px",
|
|
687
|
+
borderRadius: 8,
|
|
688
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
689
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
690
|
+
font: "inherit",
|
|
691
|
+
fontSize: 14,
|
|
692
|
+
lineHeight: "22px",
|
|
693
|
+
color: "var(--dsw-alias-label-primary)",
|
|
694
|
+
outline: "none"
|
|
695
|
+
};
|
|
696
|
+
/** Select variant: replace the OS arrow with the shared 12px chevron inset
|
|
697
|
+
* (same treatment as the composer chips / models page selects). */
|
|
698
|
+
const selectStyle = {
|
|
699
|
+
...controlStyle,
|
|
700
|
+
appearance: "none",
|
|
701
|
+
paddingRight: 32,
|
|
702
|
+
backgroundImage: "url(\"data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2712%27 height=%2712%27 viewBox=%270 0 12 12%27 fill=%27none%27%3E%3Cpath d=%27M3 4.5L6 7.5L9 4.5%27 stroke=%27%2381858C%27 stroke-width=%271.5%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27/%3E%3C/svg%3E\")",
|
|
703
|
+
backgroundRepeat: "no-repeat",
|
|
704
|
+
backgroundPosition: "right 12px center",
|
|
705
|
+
backgroundSize: "12px 12px",
|
|
706
|
+
cursor: "pointer"
|
|
707
|
+
};
|
|
708
|
+
const segmentStyle = (active) => ({
|
|
709
|
+
padding: "4px 14px",
|
|
710
|
+
borderRadius: 14,
|
|
711
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
712
|
+
background: active ? "var(--dsw-alias-bg-module-platform)" : "transparent",
|
|
713
|
+
color: active ? "var(--dsw-alias-label-primary)" : "var(--dsw-alias-label-secondary)",
|
|
714
|
+
font: "inherit",
|
|
715
|
+
fontSize: 13,
|
|
716
|
+
cursor: "pointer"
|
|
717
|
+
});
|
|
718
|
+
const primaryStyle = {
|
|
719
|
+
display: "inline-flex",
|
|
720
|
+
alignItems: "center",
|
|
721
|
+
justifyContent: "center",
|
|
722
|
+
height: 36,
|
|
723
|
+
padding: "0 14px",
|
|
724
|
+
border: "none",
|
|
725
|
+
borderRadius: 18,
|
|
726
|
+
background: "var(--dsw-alias-button-primary-fill)",
|
|
727
|
+
color: "var(--dsw-alias-label-primary-foreground)",
|
|
728
|
+
font: "inherit",
|
|
729
|
+
fontSize: 14,
|
|
730
|
+
lineHeight: "22px",
|
|
731
|
+
cursor: "pointer"
|
|
732
|
+
};
|
|
733
|
+
/** Disabled import: the action is unavailable until input exists, so the
|
|
734
|
+
* button visibly greys out (matches the config pages' disabled controls). */
|
|
735
|
+
const primaryDisabledStyle = {
|
|
736
|
+
...primaryStyle,
|
|
737
|
+
opacity: .6,
|
|
738
|
+
cursor: "default"
|
|
739
|
+
};
|
|
740
|
+
//#endregion
|
|
741
|
+
//#region src/client/SkillsPicker.tsx
|
|
742
|
+
/**
|
|
743
|
+
* Composer skill picker: a compact pill trigger in the composer's tool row
|
|
744
|
+
* (`conversation.input.left`, right after the resident chrome), styled like
|
|
745
|
+
* the permission (access-mode) select beside it. Opens a top-anchored panel
|
|
746
|
+
* with a name-filter search box; each row shows a first-letter avatar and
|
|
747
|
+
* the bare skill name (no leading `/`). Picking a skill prefixes the draft
|
|
748
|
+
* with `/name ` — the native text gesture — highlighted via the plugin's
|
|
749
|
+
* lexicon contribution.
|
|
750
|
+
*/
|
|
751
|
+
/** One row of the composer skill picker. */
|
|
752
|
+
function SkillsPicker({ t, useSkills, useInput, inputActions, actions }) {
|
|
753
|
+
const skills = useSkills((value) => value);
|
|
754
|
+
const input = useInput((value) => value.draft);
|
|
755
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
756
|
+
const [query, setQuery] = (0, react.useState)("");
|
|
757
|
+
const rootRef = (0, react.useRef)(null);
|
|
758
|
+
(0, react.useEffect)(() => {
|
|
759
|
+
actions.refreshSkills().catch(() => {});
|
|
760
|
+
}, [actions]);
|
|
761
|
+
(0, react.useEffect)(() => {
|
|
762
|
+
if (!open) return;
|
|
763
|
+
const onDown = (event) => {
|
|
764
|
+
if (rootRef.current !== null && !rootRef.current.contains(event.target)) setOpen(false);
|
|
765
|
+
};
|
|
766
|
+
document.addEventListener("mousedown", onDown);
|
|
767
|
+
return () => document.removeEventListener("mousedown", onDown);
|
|
768
|
+
}, [open]);
|
|
769
|
+
(0, react.useEffect)(() => {
|
|
770
|
+
if (open) setQuery("");
|
|
771
|
+
}, [open]);
|
|
772
|
+
const usable = skills.filter((skill) => skill.userInvocable);
|
|
773
|
+
if (usable.length === 0) return null;
|
|
774
|
+
const filtered = query.trim().length === 0 ? usable : usable.filter((skill) => skill.name.toLowerCase().includes(query.trim().toLowerCase()));
|
|
775
|
+
const onSelect = (name) => {
|
|
776
|
+
setOpen(false);
|
|
777
|
+
inputActions.setDraft(`/${name} ${input}`);
|
|
778
|
+
};
|
|
779
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
780
|
+
ref: rootRef,
|
|
781
|
+
style: { position: "relative" },
|
|
782
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
783
|
+
type: "button",
|
|
784
|
+
"aria-label": t("pickerLabel"),
|
|
785
|
+
"aria-expanded": open,
|
|
786
|
+
onClick: () => setOpen((value) => !value),
|
|
787
|
+
style: triggerStyle,
|
|
788
|
+
children: [
|
|
789
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
790
|
+
style: triggerIconStyle,
|
|
791
|
+
"aria-hidden": "true",
|
|
792
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChecklistOutline14, {})
|
|
793
|
+
}),
|
|
794
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
795
|
+
style: triggerLabelStyle,
|
|
796
|
+
children: t("pickerPlaceholder")
|
|
797
|
+
}),
|
|
798
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
799
|
+
style: {
|
|
800
|
+
...chevronStyle,
|
|
801
|
+
transform: open ? "rotate(180deg)" : void 0
|
|
802
|
+
},
|
|
803
|
+
"aria-hidden": "true",
|
|
804
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
|
|
805
|
+
})
|
|
806
|
+
]
|
|
807
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
808
|
+
style: panelStyle,
|
|
809
|
+
children: [
|
|
810
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
811
|
+
autoFocus: true,
|
|
812
|
+
type: "text",
|
|
813
|
+
value: query,
|
|
814
|
+
onChange: (event) => setQuery(event.target.value),
|
|
815
|
+
placeholder: t("pickerSearchPlaceholder"),
|
|
816
|
+
style: searchStyle
|
|
817
|
+
}),
|
|
818
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
819
|
+
style: listStyle,
|
|
820
|
+
children: filtered.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
821
|
+
type: "button",
|
|
822
|
+
onClick: () => onSelect(skill.name),
|
|
823
|
+
style: rowStyle,
|
|
824
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
825
|
+
style: avatarStyle,
|
|
826
|
+
"aria-hidden": "true",
|
|
827
|
+
children: skill.name.charAt(0).toUpperCase()
|
|
828
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
829
|
+
style: rowLabelStyle,
|
|
830
|
+
children: skill.name
|
|
831
|
+
})]
|
|
832
|
+
}) }, skill.name))
|
|
833
|
+
}),
|
|
834
|
+
filtered.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
835
|
+
style: emptyStyle,
|
|
836
|
+
children: t("pickerEmpty")
|
|
837
|
+
}) : null
|
|
838
|
+
]
|
|
839
|
+
}) : null]
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
const triggerStyle = {
|
|
843
|
+
minWidth: 0,
|
|
844
|
+
maxWidth: 220,
|
|
845
|
+
height: 28,
|
|
846
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
847
|
+
cursor: "pointer",
|
|
848
|
+
background: "transparent",
|
|
849
|
+
border: "none",
|
|
850
|
+
borderRadius: 24,
|
|
851
|
+
outline: "none",
|
|
852
|
+
alignItems: "center",
|
|
853
|
+
gap: 4,
|
|
854
|
+
padding: "0 4px 0 8px",
|
|
855
|
+
fontSize: 13,
|
|
856
|
+
fontWeight: 500,
|
|
857
|
+
lineHeight: "20px",
|
|
858
|
+
display: "inline-flex"
|
|
859
|
+
};
|
|
860
|
+
const triggerIconStyle = {
|
|
861
|
+
flex: "none",
|
|
862
|
+
display: "inline-flex"
|
|
863
|
+
};
|
|
864
|
+
const triggerLabelStyle = {
|
|
865
|
+
textOverflow: "ellipsis",
|
|
866
|
+
whiteSpace: "nowrap",
|
|
867
|
+
minWidth: 0,
|
|
868
|
+
overflow: "hidden"
|
|
869
|
+
};
|
|
870
|
+
const chevronStyle = {
|
|
871
|
+
color: "var(--dsw-alias-label-caption)",
|
|
872
|
+
flex: "none",
|
|
873
|
+
transition: "transform .12s",
|
|
874
|
+
display: "inline-flex"
|
|
875
|
+
};
|
|
876
|
+
const panelStyle = {
|
|
877
|
+
position: "absolute",
|
|
878
|
+
bottom: "calc(100% + 8px)",
|
|
879
|
+
left: 0,
|
|
880
|
+
zIndex: 100,
|
|
881
|
+
boxSizing: "border-box",
|
|
882
|
+
width: 264,
|
|
883
|
+
maxHeight: 320,
|
|
884
|
+
display: "flex",
|
|
885
|
+
flexDirection: "column",
|
|
886
|
+
gap: 4,
|
|
887
|
+
padding: 8,
|
|
888
|
+
border: "1px solid var(--dsw-alias-border-inverted)",
|
|
889
|
+
borderRadius: 12,
|
|
890
|
+
background: "var(--dsw-specific-menu)",
|
|
891
|
+
boxShadow: "var(--dsw-shadow-lv3)",
|
|
892
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
893
|
+
fontSize: 13,
|
|
894
|
+
lineHeight: "20px"
|
|
895
|
+
};
|
|
896
|
+
const searchStyle = {
|
|
897
|
+
padding: "6px 10px",
|
|
898
|
+
borderRadius: 8,
|
|
899
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
900
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
901
|
+
font: "inherit",
|
|
902
|
+
fontSize: 13,
|
|
903
|
+
color: "var(--dsw-alias-label-primary)",
|
|
904
|
+
outline: "none"
|
|
905
|
+
};
|
|
906
|
+
const listStyle = {
|
|
907
|
+
listStyle: "none",
|
|
908
|
+
margin: 0,
|
|
909
|
+
padding: 0,
|
|
910
|
+
display: "flex",
|
|
911
|
+
flexDirection: "column",
|
|
912
|
+
overflowY: "auto",
|
|
913
|
+
maxHeight: 260
|
|
914
|
+
};
|
|
915
|
+
const rowStyle = {
|
|
916
|
+
display: "flex",
|
|
917
|
+
alignItems: "center",
|
|
918
|
+
gap: 8,
|
|
919
|
+
width: "100%",
|
|
920
|
+
padding: "5px 8px",
|
|
921
|
+
border: "none",
|
|
922
|
+
borderRadius: 8,
|
|
923
|
+
background: "transparent",
|
|
924
|
+
font: "inherit",
|
|
925
|
+
fontSize: 13,
|
|
926
|
+
color: "var(--dsw-alias-label-primary)",
|
|
927
|
+
cursor: "pointer",
|
|
928
|
+
textAlign: "left"
|
|
929
|
+
};
|
|
930
|
+
const avatarStyle = {
|
|
931
|
+
flex: "none",
|
|
932
|
+
display: "inline-flex",
|
|
933
|
+
alignItems: "center",
|
|
934
|
+
justifyContent: "center",
|
|
935
|
+
width: 24,
|
|
936
|
+
height: 24,
|
|
937
|
+
borderRadius: 999,
|
|
938
|
+
background: "var(--dsw-alias-interactive-bg-hover)",
|
|
939
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
940
|
+
fontSize: 12,
|
|
941
|
+
fontWeight: 600
|
|
942
|
+
};
|
|
943
|
+
const rowLabelStyle = {
|
|
944
|
+
textOverflow: "ellipsis",
|
|
945
|
+
whiteSpace: "nowrap",
|
|
946
|
+
minWidth: 0,
|
|
947
|
+
overflow: "hidden"
|
|
948
|
+
};
|
|
949
|
+
const emptyStyle = {
|
|
950
|
+
padding: "8px 10px",
|
|
951
|
+
fontSize: 12,
|
|
952
|
+
color: "var(--dsw-alias-label-caption)"
|
|
953
|
+
};
|
|
954
|
+
//#endregion
|
|
955
|
+
//#region src/client/locales.ts
|
|
956
|
+
/**
|
|
957
|
+
* Locale dictionaries for the skill importer settings section.
|
|
958
|
+
*
|
|
959
|
+
* Product copy is Chinese; English is the fallback dictionary.
|
|
960
|
+
* The key-set source of truth is the zh dictionary (`keyof typeof zh`),
|
|
961
|
+
* matching the locale contract's namespace-key union shape.
|
|
962
|
+
*/
|
|
963
|
+
/** Dictionary namespace owned by this plugin. */
|
|
964
|
+
const NS = "skills.importer";
|
|
965
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
966
|
+
const zh = {
|
|
967
|
+
nav: "技能",
|
|
968
|
+
pickerLabel: "技能",
|
|
969
|
+
pickerPlaceholder: "技能",
|
|
970
|
+
pickerSearchPlaceholder: "筛选技能…",
|
|
971
|
+
pickerEmpty: "没有匹配的技能",
|
|
972
|
+
commandDescription: "选择技能:弹出技能列表,选中后填入 /<名称>",
|
|
973
|
+
entryTitle: "技能入口",
|
|
974
|
+
entryDescription: "通过输入框工具行的技能下拉或 /skills 命令选择技能;也可直接输入 /<名称>。选中的技能会高亮并注入指令。",
|
|
975
|
+
installedTitle: "已安装技能",
|
|
976
|
+
refresh: "刷新",
|
|
977
|
+
installedEmpty: "暂无技能。导入新技能或手动放入技能目录后会自动出现。",
|
|
978
|
+
sourceAgents: "工作区 .agents/skills",
|
|
979
|
+
sourceDsh: "工作区 .dsh/skills",
|
|
980
|
+
sourceUser: "用户技能目录",
|
|
981
|
+
delete: "删除",
|
|
982
|
+
deleteConfirm: "确定删除技能 {name} 吗?将删除对应目录下的 SKILL.md 及资源文件。",
|
|
983
|
+
deleteFailed: "删除失败",
|
|
984
|
+
deleted: "已删除。",
|
|
985
|
+
installedFailed: "无法读取技能列表",
|
|
986
|
+
userOnly: "仅斜杠可用",
|
|
987
|
+
modelOnly: "仅模型可用",
|
|
988
|
+
importTitle: "导入技能",
|
|
989
|
+
fileTab: "从文件",
|
|
990
|
+
urlTab: "从 URL",
|
|
991
|
+
chooseFile: "选择 Markdown 文件",
|
|
992
|
+
fileHelp: "SKILL.md 或 <名称>.md,需带 frontmatter(name、description 必填)。选择后先预览,再点击导入。",
|
|
993
|
+
previewTitle: "文件预览",
|
|
994
|
+
previewName: "名称",
|
|
995
|
+
previewDescription: "描述",
|
|
996
|
+
previewWhenToUse: "适用场景",
|
|
997
|
+
previewMissingName: "frontmatter 缺少 name 字段",
|
|
998
|
+
previewInvalidName: "name 必须是 kebab-case(小写字母、数字、短横线)",
|
|
999
|
+
previewMissingDescription: "frontmatter 缺少 description 字段",
|
|
1000
|
+
previewOk: "校验通过,可以导入",
|
|
1001
|
+
fileTooLarge: "文件过大(超过 256 KB,请精简后重试)",
|
|
1002
|
+
unreadableFile: "无法读取文件",
|
|
1003
|
+
urlLabel: "技能 URL",
|
|
1004
|
+
urlNameLabel: "技能名称",
|
|
1005
|
+
urlNameHelp: "kebab-case;留空则从 URL 自动推导",
|
|
1006
|
+
targetLabel: "安装到",
|
|
1007
|
+
targetUser: "用户技能目录(~/.dsh/skills)",
|
|
1008
|
+
targetProjectAgents: "当前工作区 {title} 的 .agents/skills",
|
|
1009
|
+
targetProjectDsh: "当前工作区 {title} 的 .dsh/skills",
|
|
1010
|
+
targetProjectNoWorkspace: "项目目录(需先添加工作区)",
|
|
1011
|
+
noWorkspace: "未注册工作区:请先在会话页选择/创建工作区,或改用用户技能目录。",
|
|
1012
|
+
import: "导入",
|
|
1013
|
+
importing: "导入中…",
|
|
1014
|
+
imported: "已写入:",
|
|
1015
|
+
importFailed: "导入失败",
|
|
1016
|
+
hostUnreachable: "无法连接 dsh 服务(插件路由未生效)。请确认插件已安装,并重启 dsh web 后刷新页面。"
|
|
1017
|
+
};
|
|
1018
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
1019
|
+
const en = {
|
|
1020
|
+
nav: "Skills",
|
|
1021
|
+
pickerLabel: "Skills",
|
|
1022
|
+
pickerPlaceholder: "Skills",
|
|
1023
|
+
pickerSearchPlaceholder: "Filter skills…",
|
|
1024
|
+
pickerEmpty: "No matching skills",
|
|
1025
|
+
commandDescription: "Pick a skill: opens a popup list; selecting fills /<name>",
|
|
1026
|
+
entryTitle: "Skill entry points",
|
|
1027
|
+
entryDescription: "Pick a skill from the composer tool-row dropdown or the /skills command; or type /<name> directly. The picked skill is highlighted and its instructions are injected.",
|
|
1028
|
+
installedTitle: "Installed skills",
|
|
1029
|
+
refresh: "Refresh",
|
|
1030
|
+
installedEmpty: "No skills yet. Import one or drop files into a skill directory and they will appear automatically.",
|
|
1031
|
+
sourceAgents: "Workspace .agents/skills",
|
|
1032
|
+
sourceDsh: "Workspace .dsh/skills",
|
|
1033
|
+
sourceUser: "User skill directory",
|
|
1034
|
+
delete: "Delete",
|
|
1035
|
+
deleteConfirm: "Delete the skill {name}? This removes its SKILL.md and resources from that directory.",
|
|
1036
|
+
deleteFailed: "Failed to delete",
|
|
1037
|
+
deleted: "Deleted.",
|
|
1038
|
+
installedFailed: "Unable to read the skill list",
|
|
1039
|
+
userOnly: "Slash only",
|
|
1040
|
+
modelOnly: "Model only",
|
|
1041
|
+
importTitle: "Import a skill",
|
|
1042
|
+
fileTab: "From file",
|
|
1043
|
+
urlTab: "From URL",
|
|
1044
|
+
chooseFile: "Choose a Markdown file",
|
|
1045
|
+
fileHelp: "SKILL.md or <name>.md with frontmatter (name and description required). Preview first, then import.",
|
|
1046
|
+
previewTitle: "File preview",
|
|
1047
|
+
previewName: "Name",
|
|
1048
|
+
previewDescription: "Description",
|
|
1049
|
+
previewWhenToUse: "When to use",
|
|
1050
|
+
previewMissingName: "frontmatter is missing the name field",
|
|
1051
|
+
previewInvalidName: "name must be kebab-case (lowercase letters, digits, hyphens)",
|
|
1052
|
+
previewMissingDescription: "frontmatter is missing the description field",
|
|
1053
|
+
previewOk: "Valid, ready to import",
|
|
1054
|
+
fileTooLarge: "File too large (over 256 KB; trim it and retry)",
|
|
1055
|
+
unreadableFile: "Unable to read the file",
|
|
1056
|
+
urlLabel: "Skill URL",
|
|
1057
|
+
urlNameLabel: "Skill name",
|
|
1058
|
+
urlNameHelp: "kebab-case; leave empty to derive from the URL",
|
|
1059
|
+
targetLabel: "Install into",
|
|
1060
|
+
targetUser: "User skill directory (~/.dsh/skills)",
|
|
1061
|
+
targetProjectAgents: "Workspace {title} .agents/skills",
|
|
1062
|
+
targetProjectDsh: "Workspace {title} .dsh/skills",
|
|
1063
|
+
targetProjectNoWorkspace: "Project directory (add a workspace first)",
|
|
1064
|
+
noWorkspace: "No registered workspace: create/select one in a session, or use the user skill directory.",
|
|
1065
|
+
import: "Import",
|
|
1066
|
+
importing: "Importing…",
|
|
1067
|
+
imported: "Written to: ",
|
|
1068
|
+
importFailed: "Import failed",
|
|
1069
|
+
hostUnreachable: "Cannot reach the dsh service (plugin routes are not active). Make sure the plugin is installed, restart dsh web, and refresh the page."
|
|
1070
|
+
};
|
|
1071
|
+
//#endregion
|
|
1072
|
+
//#region src/client/index.ts
|
|
1073
|
+
/** Required services (cordis fiber inject). */
|
|
1074
|
+
const inject = [
|
|
1075
|
+
"slots",
|
|
1076
|
+
"locale",
|
|
1077
|
+
"connection"
|
|
1078
|
+
];
|
|
1079
|
+
/** Same-origin route literals (mirrors the host half's ROUTES). */
|
|
1080
|
+
const ROUTES = {
|
|
1081
|
+
list: "/skill-importer/list",
|
|
1082
|
+
import: "/skill-importer/import",
|
|
1083
|
+
importUrl: "/skill-importer/import-url",
|
|
1084
|
+
delete: "/skill-importer/delete"
|
|
1085
|
+
};
|
|
1086
|
+
/** Fetch one route and unwrap the JSON envelope; throws on transport/HTTP errors. */
|
|
1087
|
+
async function call(path, body) {
|
|
1088
|
+
let response;
|
|
1089
|
+
try {
|
|
1090
|
+
response = await fetch(path, body === void 0 ? void 0 : {
|
|
1091
|
+
method: "POST",
|
|
1092
|
+
headers: { "content-type": "application/json" },
|
|
1093
|
+
body: JSON.stringify(body)
|
|
1094
|
+
});
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
throw new Error(`无法连接 dsh 服务(${path}):${error instanceof Error ? error.message : String(error)}`);
|
|
1097
|
+
}
|
|
1098
|
+
if (!response.ok) {
|
|
1099
|
+
let detail = "";
|
|
1100
|
+
try {
|
|
1101
|
+
detail = (await response.json()).error ?? "";
|
|
1102
|
+
} catch {}
|
|
1103
|
+
throw new Error(`dsh 服务返回 ${response.status}${detail ? `:${detail}` : ""}`);
|
|
1104
|
+
}
|
|
1105
|
+
return await response.json();
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Client plugin body: register the section and its dictionaries.
|
|
1109
|
+
* @param ctx - the browser plugin context.
|
|
1110
|
+
*/
|
|
1111
|
+
function apply(ctx) {
|
|
1112
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
1113
|
+
zh,
|
|
1114
|
+
en
|
|
1115
|
+
}), "dsh-skill-importer: dictionaries");
|
|
1116
|
+
const t = ctx.locale.bind(NS);
|
|
1117
|
+
let catalog = [];
|
|
1118
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1119
|
+
const notify = () => {
|
|
1120
|
+
for (const listener of [...listeners]) try {
|
|
1121
|
+
listener();
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
console.error("[dsh-skill-importer] listener failed:", error);
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
const skillsApi = ctx.get("connection").api.skills;
|
|
1127
|
+
skillsApi.list.bind(skillsApi);
|
|
1128
|
+
skillsApi.list = ((_request, _signal) => {
|
|
1129
|
+
return Promise.reject(/* @__PURE__ */ new Error("skill group hidden: use the composer skill picker or /skills"));
|
|
1130
|
+
});
|
|
1131
|
+
const usableNames = () => catalog.filter((skill) => skill.userInvocable).map((skill) => skill.name);
|
|
1132
|
+
ctx.effect(() => {
|
|
1133
|
+
return ctx.get("inputTriggers").registerSource({
|
|
1134
|
+
trigger: "/",
|
|
1135
|
+
name: "skill-importer",
|
|
1136
|
+
order: 99,
|
|
1137
|
+
async candidates() {
|
|
1138
|
+
throw new Error("skill group hidden: use the composer skill picker");
|
|
1139
|
+
},
|
|
1140
|
+
onPick() {},
|
|
1141
|
+
warm() {},
|
|
1142
|
+
lexicon() {
|
|
1143
|
+
return usableNames();
|
|
1144
|
+
},
|
|
1145
|
+
subscribeLexicon(_session, listener) {
|
|
1146
|
+
listeners.add(listener);
|
|
1147
|
+
return () => {
|
|
1148
|
+
listeners.delete(listener);
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
}, "dsh-skill-importer: lexicon source");
|
|
1153
|
+
const commandUi = ctx.get("commandUi");
|
|
1154
|
+
const sessions = ctx.get("sessions");
|
|
1155
|
+
const conversation = ctx.get("conversation");
|
|
1156
|
+
if (commandUi !== void 0 && sessions !== void 0 && conversation !== void 0) ctx.effect(() => {
|
|
1157
|
+
return commandUi.register({
|
|
1158
|
+
name: "skills",
|
|
1159
|
+
description: t("commandDescription"),
|
|
1160
|
+
available: () => true,
|
|
1161
|
+
ui: {
|
|
1162
|
+
kind: "popupSelect",
|
|
1163
|
+
async options(_session, _signal) {
|
|
1164
|
+
return catalog.filter((skill) => skill.userInvocable).map((skill) => ({
|
|
1165
|
+
id: skill.name,
|
|
1166
|
+
label: skill.name
|
|
1167
|
+
}));
|
|
1168
|
+
},
|
|
1169
|
+
onSelect(option, session) {
|
|
1170
|
+
const actx = sessions.scope(session.sessionId);
|
|
1171
|
+
if (actx === void 0) return;
|
|
1172
|
+
const input = conversation.input.for(actx);
|
|
1173
|
+
const rest = input.state.getSnapshot().draft.replace(/^\s*\/[a-zA-Z][a-zA-Z0-9-]*\s*/, "");
|
|
1174
|
+
input.setDraft(`/${option.id} ${rest}`);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
}, "dsh-skill-importer: /skills popupSelect command");
|
|
1179
|
+
const refreshSkills = async () => {
|
|
1180
|
+
catalog = (await call(ROUTES.list)).skills;
|
|
1181
|
+
notify();
|
|
1182
|
+
return catalog;
|
|
1183
|
+
};
|
|
1184
|
+
const importSkill = async (request) => {
|
|
1185
|
+
return (await call(ROUTES.import, request)).path;
|
|
1186
|
+
};
|
|
1187
|
+
const importUrl = async (request) => {
|
|
1188
|
+
return (await call(ROUTES.importUrl, request)).path;
|
|
1189
|
+
};
|
|
1190
|
+
const deleteSkill = async (request) => {
|
|
1191
|
+
return (await call(ROUTES.delete, request)).removed;
|
|
1192
|
+
};
|
|
1193
|
+
const face = () => ({
|
|
1194
|
+
hooks: { skills: {
|
|
1195
|
+
getSnapshot: () => catalog,
|
|
1196
|
+
subscribe: (listener) => {
|
|
1197
|
+
listeners.add(listener);
|
|
1198
|
+
return () => {
|
|
1199
|
+
listeners.delete(listener);
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
} },
|
|
1203
|
+
actions: {
|
|
1204
|
+
refreshSkills,
|
|
1205
|
+
importSkill,
|
|
1206
|
+
importUrl,
|
|
1207
|
+
deleteSkill
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
1211
|
+
name: "settings.section",
|
|
1212
|
+
id: "skills",
|
|
1213
|
+
order: 30,
|
|
1214
|
+
label: () => t("nav"),
|
|
1215
|
+
locale: NS,
|
|
1216
|
+
inject: face,
|
|
1217
|
+
children: {}
|
|
1218
|
+
}, SkillImporterSection));
|
|
1219
|
+
ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
|
|
1220
|
+
name: "conversation.input.left",
|
|
1221
|
+
id: "skills-picker",
|
|
1222
|
+
order: 10,
|
|
1223
|
+
locale: NS,
|
|
1224
|
+
inject: face,
|
|
1225
|
+
children: {}
|
|
1226
|
+
}, SkillsPicker));
|
|
1227
|
+
}
|
|
1228
|
+
//#endregion
|
|
1229
|
+
exports.apply = apply;
|
|
1230
|
+
exports.inject = inject;
|
|
1231
|
+
return module.exports;
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
//# sourceMappingURL=client.js.map
|