dsh-skill-importer 0.2.2 → 0.2.3
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/lib/client.js +213 -14
- package/lib/types/client/SkillsPicker.d.ts +1 -1
- package/lib/types/client/atomicSkillDelete.d.ts +19 -0
- package/lib/types/client/composerFocus.d.ts +9 -0
- package/lib/types/client/locales.d.ts +10 -0
- package/package.json +5 -1
- package/tests/atomic-skill-delete.test.mjs +24 -0
package/lib/client.js
CHANGED
|
@@ -5,8 +5,8 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
|
-
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
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
10
|
//#region src/frontmatter.ts
|
|
11
11
|
/** Kebab-case name rule shared with `dsh-skill` (`^[a-z0-9]+(?:-[a-z0-9]+)*$`). */
|
|
12
12
|
const KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -261,6 +261,8 @@ window.__ModuleLoader__.load({
|
|
|
261
261
|
const [batchScan, setBatchScan] = (0, react.useState)();
|
|
262
262
|
const [batchResults, setBatchResults] = (0, react.useState)();
|
|
263
263
|
const [replaceNames, setReplaceNames] = (0, react.useState)(() => /* @__PURE__ */ new Set());
|
|
264
|
+
const [pendingDelete, setPendingDelete] = (0, react.useState)();
|
|
265
|
+
const [batchConfirmation, setBatchConfirmation] = (0, react.useState)();
|
|
264
266
|
const fileInput = (0, react.useRef)(null);
|
|
265
267
|
const onFilePicked = async (file) => {
|
|
266
268
|
setFileDraft(void 0);
|
|
@@ -302,8 +304,9 @@ window.__ModuleLoader__.load({
|
|
|
302
304
|
}, () => pollForSkill(name, attemptsLeft - 1));
|
|
303
305
|
}, 2e3);
|
|
304
306
|
};
|
|
305
|
-
const removeSkill = async (
|
|
306
|
-
if (
|
|
307
|
+
const removeSkill = async () => {
|
|
308
|
+
if (pendingDelete === void 0) return;
|
|
309
|
+
const skill = pendingDelete;
|
|
307
310
|
setBusy(true);
|
|
308
311
|
setMessage(void 0);
|
|
309
312
|
setMessageError(false);
|
|
@@ -312,7 +315,10 @@ window.__ModuleLoader__.load({
|
|
|
312
315
|
name: skill.name,
|
|
313
316
|
source: skill.source,
|
|
314
317
|
workspacePath: workspace?.path
|
|
315
|
-
}))
|
|
318
|
+
})) {
|
|
319
|
+
setMessage(t("deleted"));
|
|
320
|
+
setPendingDelete(void 0);
|
|
321
|
+
}
|
|
316
322
|
setListEpoch((epoch) => epoch + 1);
|
|
317
323
|
} catch (error) {
|
|
318
324
|
setMessageError(true);
|
|
@@ -403,17 +409,20 @@ window.__ModuleLoader__.load({
|
|
|
403
409
|
setBusy(false);
|
|
404
410
|
}
|
|
405
411
|
};
|
|
406
|
-
const
|
|
412
|
+
const requestBatchCommit = () => {
|
|
407
413
|
if (batchScan === void 0) return;
|
|
408
414
|
const ready = batchScan.entries.filter((entry) => entry.status === "ready");
|
|
409
415
|
const replacing = ready.filter((entry) => replaceNames.has(entry.name)).length;
|
|
410
416
|
const importing = ready.filter((entry) => !entry.conflict).length;
|
|
411
417
|
const skipped = ready.filter((entry) => entry.conflict && !replaceNames.has(entry.name)).length;
|
|
412
|
-
|
|
418
|
+
setBatchConfirmation({
|
|
413
419
|
importing,
|
|
414
420
|
replacing,
|
|
415
421
|
skipped
|
|
416
|
-
})
|
|
422
|
+
});
|
|
423
|
+
};
|
|
424
|
+
const commitBatchDirectory = async () => {
|
|
425
|
+
if (batchScan === void 0 || batchConfirmation === void 0) return;
|
|
417
426
|
setBusy(true);
|
|
418
427
|
setMessage(void 0);
|
|
419
428
|
setMessageError(false);
|
|
@@ -425,6 +434,7 @@ window.__ModuleLoader__.load({
|
|
|
425
434
|
setBatchResults(result.results);
|
|
426
435
|
setBatchScan(void 0);
|
|
427
436
|
setReplaceNames(/* @__PURE__ */ new Set());
|
|
437
|
+
setBatchConfirmation(void 0);
|
|
428
438
|
setListEpoch((epoch) => epoch + 1);
|
|
429
439
|
} catch (error) {
|
|
430
440
|
setMessageError(true);
|
|
@@ -451,7 +461,7 @@ window.__ModuleLoader__.load({
|
|
|
451
461
|
return;
|
|
452
462
|
}
|
|
453
463
|
if (batchScan === void 0) scanBatchDirectory();
|
|
454
|
-
else
|
|
464
|
+
else requestBatchCommit();
|
|
455
465
|
};
|
|
456
466
|
const primaryLabel = mode !== "batch" ? busy ? t("importing") : t("import") : busy ? t("batchWorking") : batchScan === void 0 ? t("batchScan") : t("batchCommit");
|
|
457
467
|
const flags = (skill) => {
|
|
@@ -592,9 +602,7 @@ window.__ModuleLoader__.load({
|
|
|
592
602
|
type: "button",
|
|
593
603
|
"aria-label": `${t("delete")} ${skill.name}`,
|
|
594
604
|
title: t("delete"),
|
|
595
|
-
onClick: () =>
|
|
596
|
-
removeSkill(skill);
|
|
597
|
-
},
|
|
605
|
+
onClick: () => setPendingDelete(skill),
|
|
598
606
|
style: deleteIconButtonStyle,
|
|
599
607
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, {})
|
|
600
608
|
})
|
|
@@ -1048,6 +1056,50 @@ window.__ModuleLoader__.load({
|
|
|
1048
1056
|
})
|
|
1049
1057
|
]
|
|
1050
1058
|
})
|
|
1059
|
+
}),
|
|
1060
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
1061
|
+
open: pendingDelete !== void 0,
|
|
1062
|
+
onClose: () => {
|
|
1063
|
+
if (!busy) setPendingDelete(void 0);
|
|
1064
|
+
},
|
|
1065
|
+
title: t("deleteTitle"),
|
|
1066
|
+
closeLabel: t("close"),
|
|
1067
|
+
description: pendingDelete === void 0 ? void 0 : t("deleteConfirm", { name: pendingDelete.name }),
|
|
1068
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1069
|
+
variant: "outline",
|
|
1070
|
+
disabled: busy,
|
|
1071
|
+
onClick: () => setPendingDelete(void 0),
|
|
1072
|
+
children: t("cancel")
|
|
1073
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1074
|
+
variant: "primary",
|
|
1075
|
+
disabled: busy,
|
|
1076
|
+
onClick: () => {
|
|
1077
|
+
removeSkill();
|
|
1078
|
+
},
|
|
1079
|
+
children: busy ? t("deleting") : t("delete")
|
|
1080
|
+
})] })
|
|
1081
|
+
}),
|
|
1082
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
1083
|
+
open: batchConfirmation !== void 0,
|
|
1084
|
+
onClose: () => {
|
|
1085
|
+
if (!busy) setBatchConfirmation(void 0);
|
|
1086
|
+
},
|
|
1087
|
+
title: t("batchConfirmTitle"),
|
|
1088
|
+
closeLabel: t("close"),
|
|
1089
|
+
description: batchConfirmation === void 0 ? void 0 : t("batchConfirm", batchConfirmation),
|
|
1090
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1091
|
+
variant: "outline",
|
|
1092
|
+
disabled: busy,
|
|
1093
|
+
onClick: () => setBatchConfirmation(void 0),
|
|
1094
|
+
children: t("cancel")
|
|
1095
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1096
|
+
variant: "primary",
|
|
1097
|
+
disabled: busy,
|
|
1098
|
+
onClick: () => {
|
|
1099
|
+
commitBatchDirectory();
|
|
1100
|
+
},
|
|
1101
|
+
children: busy ? t("batchWorking") : t("batchCommit")
|
|
1102
|
+
})] })
|
|
1051
1103
|
})
|
|
1052
1104
|
]
|
|
1053
1105
|
});
|
|
@@ -1516,6 +1568,58 @@ window.__ModuleLoader__.load({
|
|
|
1516
1568
|
cursor: "default"
|
|
1517
1569
|
};
|
|
1518
1570
|
//#endregion
|
|
1571
|
+
//#region src/client/atomicSkillDelete.ts
|
|
1572
|
+
function leadingSkillToken(draft, names) {
|
|
1573
|
+
const ordered = [...new Set(names)].sort((a, b) => b.length - a.length);
|
|
1574
|
+
for (const name of ordered) {
|
|
1575
|
+
if (name.length === 0) continue;
|
|
1576
|
+
const token = `/${name}`;
|
|
1577
|
+
if (!draft.startsWith(token)) continue;
|
|
1578
|
+
const following = draft[token.length];
|
|
1579
|
+
if (following !== void 0 && !/\s/u.test(following)) continue;
|
|
1580
|
+
return {
|
|
1581
|
+
tokenEnd: token.length,
|
|
1582
|
+
boundaryEnd: token.length + (following === " " ? 1 : 0)
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Delete one exact, leading, installed `/name` token as a single edit.
|
|
1588
|
+
*
|
|
1589
|
+
* This deliberately handles only the common boundary gesture. Selections,
|
|
1590
|
+
* token-internal edits, unknown names, leading whitespace, and non-boundary
|
|
1591
|
+
* carets fall through to the textarea's normal behavior.
|
|
1592
|
+
*/
|
|
1593
|
+
function atomicSkillBackspace(draft, caret, names) {
|
|
1594
|
+
if (!Number.isInteger(caret) || caret < 0 || caret > draft.length) return void 0;
|
|
1595
|
+
const token = leadingSkillToken(draft, names);
|
|
1596
|
+
if (token === void 0) return void 0;
|
|
1597
|
+
if (caret !== token.tokenEnd && caret !== token.boundaryEnd) return void 0;
|
|
1598
|
+
return {
|
|
1599
|
+
draft: draft.slice(token.boundaryEnd),
|
|
1600
|
+
caret: 0
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Resolve one whole-token horizontal caret jump.
|
|
1605
|
+
* Left from anywhere inside/just after the leading skill goes before it;
|
|
1606
|
+
* right from before/inside it goes after its single separator.
|
|
1607
|
+
*/
|
|
1608
|
+
function atomicSkillArrow(draft, caret, direction, names) {
|
|
1609
|
+
if (!Number.isInteger(caret) || caret < 0 || caret > draft.length) return void 0;
|
|
1610
|
+
const token = leadingSkillToken(draft, names);
|
|
1611
|
+
if (token === void 0) return void 0;
|
|
1612
|
+
if (direction === "left" && caret > 0 && caret <= token.boundaryEnd) return 0;
|
|
1613
|
+
if (direction === "right" && caret >= 0 && caret < token.boundaryEnd) return token.boundaryEnd;
|
|
1614
|
+
}
|
|
1615
|
+
//#endregion
|
|
1616
|
+
//#region src/client/composerFocus.ts
|
|
1617
|
+
const COMPOSER_FOCUS_EVENT = "dsh-skill-importer/composer-focus";
|
|
1618
|
+
/** Ask the mounted picker in one session to restore its composer focus. */
|
|
1619
|
+
function requestComposerFocus(request) {
|
|
1620
|
+
window.dispatchEvent(new CustomEvent(COMPOSER_FOCUS_EVENT, { detail: request }));
|
|
1621
|
+
}
|
|
1622
|
+
//#endregion
|
|
1519
1623
|
//#region src/client/SkillsPicker.tsx
|
|
1520
1624
|
/**
|
|
1521
1625
|
* Composer skill picker: a compact pill trigger in the composer's tool row
|
|
@@ -1526,13 +1630,25 @@ window.__ModuleLoader__.load({
|
|
|
1526
1630
|
* with `/name ` — the native text gesture — highlighted via the plugin's
|
|
1527
1631
|
* lexicon contribution.
|
|
1528
1632
|
*/
|
|
1633
|
+
/** Find the one composer textarea owned by the picker nearest in the slot tree. */
|
|
1634
|
+
function composerTextareaFor(root) {
|
|
1635
|
+
let node = root?.parentElement;
|
|
1636
|
+
while (node !== null && node !== void 0) {
|
|
1637
|
+
const textareas = node.querySelectorAll("textarea");
|
|
1638
|
+
if (textareas.length > 0) return textareas.length === 1 ? textareas[0] : void 0;
|
|
1639
|
+
node = node.parentElement;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1529
1642
|
/** One row of the composer skill picker. */
|
|
1530
|
-
function SkillsPicker({ t, useSkills, useInput, inputActions, actions }) {
|
|
1643
|
+
function SkillsPicker({ t, sessionId, useSkills, useInput, inputActions, actions }) {
|
|
1531
1644
|
const skills = useSkills((value) => value);
|
|
1532
1645
|
const input = useInput((value) => value.draft);
|
|
1533
1646
|
const [open, setOpen] = (0, react.useState)(false);
|
|
1534
1647
|
const [query, setQuery] = (0, react.useState)("");
|
|
1535
1648
|
const rootRef = (0, react.useRef)(null);
|
|
1649
|
+
const draftRef = (0, react.useRef)(input);
|
|
1650
|
+
const skillNamesRef = (0, react.useRef)([]);
|
|
1651
|
+
const caretFrameRef = (0, react.useRef)();
|
|
1536
1652
|
(0, react.useEffect)(() => {
|
|
1537
1653
|
actions.refreshSkills().catch(() => {});
|
|
1538
1654
|
}, [actions]);
|
|
@@ -1548,11 +1664,77 @@ window.__ModuleLoader__.load({
|
|
|
1548
1664
|
if (open) setQuery("");
|
|
1549
1665
|
}, [open]);
|
|
1550
1666
|
const usable = skills.filter((skill) => skill.userInvocable);
|
|
1667
|
+
draftRef.current = input;
|
|
1668
|
+
skillNamesRef.current = usable.map((skill) => skill.name);
|
|
1669
|
+
const scheduleComposerFocus = (expectedDraft, caret) => {
|
|
1670
|
+
const textarea = composerTextareaFor(rootRef.current);
|
|
1671
|
+
if (caretFrameRef.current !== void 0) window.cancelAnimationFrame(caretFrameRef.current);
|
|
1672
|
+
caretFrameRef.current = window.requestAnimationFrame(() => {
|
|
1673
|
+
caretFrameRef.current = void 0;
|
|
1674
|
+
if (textarea === void 0 || !textarea.isConnected || textarea.value !== expectedDraft) return;
|
|
1675
|
+
textarea.focus({ preventScroll: true });
|
|
1676
|
+
textarea.setSelectionRange(caret, caret);
|
|
1677
|
+
});
|
|
1678
|
+
};
|
|
1679
|
+
(0, react.useEffect)(() => {
|
|
1680
|
+
const onFocusRequest = (event) => {
|
|
1681
|
+
const request = event.detail;
|
|
1682
|
+
if (request.sessionId !== sessionId) return;
|
|
1683
|
+
scheduleComposerFocus(request.expectedDraft, request.caret);
|
|
1684
|
+
};
|
|
1685
|
+
window.addEventListener(COMPOSER_FOCUS_EVENT, onFocusRequest);
|
|
1686
|
+
return () => window.removeEventListener(COMPOSER_FOCUS_EVENT, onFocusRequest);
|
|
1687
|
+
}, [sessionId]);
|
|
1688
|
+
(0, react.useEffect)(() => {
|
|
1689
|
+
const onKeyDown = (event) => {
|
|
1690
|
+
if (![
|
|
1691
|
+
"Backspace",
|
|
1692
|
+
"ArrowLeft",
|
|
1693
|
+
"ArrowRight"
|
|
1694
|
+
].includes(event.key)) return;
|
|
1695
|
+
if (event.defaultPrevented || event.isComposing || event.keyCode === 229) return;
|
|
1696
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
1697
|
+
if (event.shiftKey && event.key !== "Backspace") return;
|
|
1698
|
+
const textarea = event.target;
|
|
1699
|
+
if (!(textarea instanceof HTMLTextAreaElement) || document.activeElement !== textarea) return;
|
|
1700
|
+
if (composerTextareaFor(rootRef.current) !== textarea || textarea.value !== draftRef.current) return;
|
|
1701
|
+
const start = textarea.selectionStart;
|
|
1702
|
+
const end = textarea.selectionEnd;
|
|
1703
|
+
if (start === null || end === null || start !== end) return;
|
|
1704
|
+
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
1705
|
+
const caret = atomicSkillArrow(draftRef.current, start, event.key === "ArrowLeft" ? "left" : "right", skillNamesRef.current);
|
|
1706
|
+
if (caret === void 0) return;
|
|
1707
|
+
event.preventDefault();
|
|
1708
|
+
event.stopPropagation();
|
|
1709
|
+
textarea.setSelectionRange(caret, caret);
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
const edit = atomicSkillBackspace(draftRef.current, start, skillNamesRef.current);
|
|
1713
|
+
if (edit === void 0) return;
|
|
1714
|
+
event.preventDefault();
|
|
1715
|
+
event.stopPropagation();
|
|
1716
|
+
inputActions.setDraft(edit.draft);
|
|
1717
|
+
if (caretFrameRef.current !== void 0) window.cancelAnimationFrame(caretFrameRef.current);
|
|
1718
|
+
caretFrameRef.current = window.requestAnimationFrame(() => {
|
|
1719
|
+
caretFrameRef.current = void 0;
|
|
1720
|
+
if (!textarea.isConnected || document.activeElement !== textarea || textarea.value !== edit.draft) return;
|
|
1721
|
+
textarea.setSelectionRange(edit.caret, edit.caret);
|
|
1722
|
+
});
|
|
1723
|
+
};
|
|
1724
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
1725
|
+
return () => {
|
|
1726
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
1727
|
+
if (caretFrameRef.current !== void 0) window.cancelAnimationFrame(caretFrameRef.current);
|
|
1728
|
+
};
|
|
1729
|
+
}, [inputActions]);
|
|
1551
1730
|
if (usable.length === 0) return null;
|
|
1552
1731
|
const filtered = query.trim().length === 0 ? usable : usable.filter((skill) => skill.name.toLowerCase().includes(query.trim().toLowerCase()));
|
|
1553
1732
|
const onSelect = (name) => {
|
|
1733
|
+
const prefix = `/${name} `;
|
|
1734
|
+
const nextDraft = `${prefix}${input}`;
|
|
1554
1735
|
setOpen(false);
|
|
1555
|
-
inputActions.setDraft(
|
|
1736
|
+
inputActions.setDraft(nextDraft);
|
|
1737
|
+
scheduleComposerFocus(nextDraft, prefix.length);
|
|
1556
1738
|
};
|
|
1557
1739
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1558
1740
|
ref: rootRef,
|
|
@@ -1754,7 +1936,11 @@ window.__ModuleLoader__.load({
|
|
|
1754
1936
|
refresh: "刷新",
|
|
1755
1937
|
installedEmpty: "暂无技能。导入新技能或手动放入技能目录后会自动出现。",
|
|
1756
1938
|
delete: "删除",
|
|
1939
|
+
deleting: "删除中…",
|
|
1940
|
+
deleteTitle: "删除技能",
|
|
1757
1941
|
deleteConfirm: "确定删除技能 {name} 吗?将删除对应目录下的 SKILL.md 及资源文件。",
|
|
1942
|
+
cancel: "取消",
|
|
1943
|
+
close: "关闭",
|
|
1758
1944
|
deleteFailed: "删除失败",
|
|
1759
1945
|
deleted: "已删除。",
|
|
1760
1946
|
installedFailed: "无法读取技能列表",
|
|
@@ -1800,6 +1986,7 @@ window.__ModuleLoader__.load({
|
|
|
1800
1986
|
batchConflict: "名称重复",
|
|
1801
1987
|
batchReplace: "替换",
|
|
1802
1988
|
batchConfirm: "将新增 {importing} 个、替换 {replacing} 个、跳过 {skipped} 个技能。确认继续吗?",
|
|
1989
|
+
batchConfirmTitle: "确认批量导入",
|
|
1803
1990
|
batchResultTitle: "本次导入结果",
|
|
1804
1991
|
batchResultSummary: "新增 {imported} · 替换 {replaced} · 跳过 {skipped} · 失败 {failed}",
|
|
1805
1992
|
batchStatusImported: "已导入",
|
|
@@ -1831,7 +2018,11 @@ window.__ModuleLoader__.load({
|
|
|
1831
2018
|
refresh: "Refresh",
|
|
1832
2019
|
installedEmpty: "No skills yet. Import one or drop files into a skill directory and they will appear automatically.",
|
|
1833
2020
|
delete: "Delete",
|
|
2021
|
+
deleting: "Deleting…",
|
|
2022
|
+
deleteTitle: "Delete skill",
|
|
1834
2023
|
deleteConfirm: "Delete the skill {name}? This removes its SKILL.md and resources from that directory.",
|
|
2024
|
+
cancel: "Cancel",
|
|
2025
|
+
close: "Close",
|
|
1835
2026
|
deleteFailed: "Failed to delete",
|
|
1836
2027
|
deleted: "Deleted.",
|
|
1837
2028
|
installedFailed: "Unable to read the skill list",
|
|
@@ -1877,6 +2068,7 @@ window.__ModuleLoader__.load({
|
|
|
1877
2068
|
batchConflict: "Name conflict",
|
|
1878
2069
|
batchReplace: "Replace",
|
|
1879
2070
|
batchConfirm: "Add {importing}, replace {replacing}, and skip {skipped} skills. Continue?",
|
|
2071
|
+
batchConfirmTitle: "Confirm batch import",
|
|
1880
2072
|
batchResultTitle: "Import results",
|
|
1881
2073
|
batchResultSummary: "Added {imported} · Replaced {replaced} · Skipped {skipped} · Failed {failed}",
|
|
1882
2074
|
batchStatusImported: "Imported",
|
|
@@ -2000,7 +2192,14 @@ window.__ModuleLoader__.load({
|
|
|
2000
2192
|
if (actx === void 0) return;
|
|
2001
2193
|
const input = conversation.input.for(actx);
|
|
2002
2194
|
const rest = input.state.getSnapshot().draft.replace(/^\s*\/[a-zA-Z][a-zA-Z0-9-]*\s*/, "");
|
|
2003
|
-
|
|
2195
|
+
const prefix = `/${option.id} `;
|
|
2196
|
+
const nextDraft = `${prefix}${rest}`;
|
|
2197
|
+
input.setDraft(nextDraft);
|
|
2198
|
+
requestComposerFocus({
|
|
2199
|
+
sessionId: session.sessionId,
|
|
2200
|
+
expectedDraft: nextDraft,
|
|
2201
|
+
caret: prefix.length
|
|
2202
|
+
});
|
|
2004
2203
|
}
|
|
2005
2204
|
}
|
|
2006
2205
|
});
|
|
@@ -13,4 +13,4 @@ import type { SkillImporterInjected } from './index.ts';
|
|
|
13
13
|
/** Props the renderer binds for the picker. */
|
|
14
14
|
export type SkillsPickerProps = PropsRuntime<'conversation.input.left'> & PropsLocale<'skills.importer'> & InjectFace<SkillImporterInjected>;
|
|
15
15
|
/** One row of the composer skill picker. */
|
|
16
|
-
export declare function SkillsPicker({ t, useSkills, useInput, inputActions, actions }: SkillsPickerProps): ReactNode;
|
|
16
|
+
export declare function SkillsPicker({ t, sessionId, useSkills, useInput, inputActions, actions }: SkillsPickerProps): ReactNode;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** One safe, collapsed-caret Backspace rewrite over a leading skill token. */
|
|
2
|
+
export interface AtomicSkillDeleteEdit {
|
|
3
|
+
readonly draft: string;
|
|
4
|
+
readonly caret: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Delete one exact, leading, installed `/name` token as a single edit.
|
|
8
|
+
*
|
|
9
|
+
* This deliberately handles only the common boundary gesture. Selections,
|
|
10
|
+
* token-internal edits, unknown names, leading whitespace, and non-boundary
|
|
11
|
+
* carets fall through to the textarea's normal behavior.
|
|
12
|
+
*/
|
|
13
|
+
export declare function atomicSkillBackspace(draft: string, caret: number, names: readonly string[]): AtomicSkillDeleteEdit | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve one whole-token horizontal caret jump.
|
|
16
|
+
* Left from anywhere inside/just after the leading skill goes before it;
|
|
17
|
+
* right from before/inside it goes after its single separator.
|
|
18
|
+
*/
|
|
19
|
+
export declare function atomicSkillArrow(draft: string, caret: number, direction: 'left' | 'right', names: readonly string[]): number | undefined;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Session-routed request emitted after a non-React command popup edits the draft. */
|
|
2
|
+
export interface ComposerFocusRequest {
|
|
3
|
+
readonly sessionId: string;
|
|
4
|
+
readonly expectedDraft: string;
|
|
5
|
+
readonly caret: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const COMPOSER_FOCUS_EVENT = "dsh-skill-importer/composer-focus";
|
|
8
|
+
/** Ask the mounted picker in one session to restore its composer focus. */
|
|
9
|
+
export declare function requestComposerFocus(request: ComposerFocusRequest): void;
|
|
@@ -21,7 +21,11 @@ export declare const zh: {
|
|
|
21
21
|
refresh: string;
|
|
22
22
|
installedEmpty: string;
|
|
23
23
|
delete: string;
|
|
24
|
+
deleting: string;
|
|
25
|
+
deleteTitle: string;
|
|
24
26
|
deleteConfirm: string;
|
|
27
|
+
cancel: string;
|
|
28
|
+
close: string;
|
|
25
29
|
deleteFailed: string;
|
|
26
30
|
deleted: string;
|
|
27
31
|
installedFailed: string;
|
|
@@ -67,6 +71,7 @@ export declare const zh: {
|
|
|
67
71
|
batchConflict: string;
|
|
68
72
|
batchReplace: string;
|
|
69
73
|
batchConfirm: string;
|
|
74
|
+
batchConfirmTitle: string;
|
|
70
75
|
batchResultTitle: string;
|
|
71
76
|
batchResultSummary: string;
|
|
72
77
|
batchStatusImported: string;
|
|
@@ -100,7 +105,11 @@ export declare const en: {
|
|
|
100
105
|
refresh: string;
|
|
101
106
|
installedEmpty: string;
|
|
102
107
|
delete: string;
|
|
108
|
+
deleting: string;
|
|
109
|
+
deleteTitle: string;
|
|
103
110
|
deleteConfirm: string;
|
|
111
|
+
cancel: string;
|
|
112
|
+
close: string;
|
|
104
113
|
deleteFailed: string;
|
|
105
114
|
deleted: string;
|
|
106
115
|
installedFailed: string;
|
|
@@ -146,6 +155,7 @@ export declare const en: {
|
|
|
146
155
|
batchConflict: string;
|
|
147
156
|
batchReplace: string;
|
|
148
157
|
batchConfirm: string;
|
|
158
|
+
batchConfirmTitle: string;
|
|
149
159
|
batchResultTitle: string;
|
|
150
160
|
batchResultSummary: string;
|
|
151
161
|
batchStatusImported: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-skill-importer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Web UI plugin for DeepSeek Harness: import skills from local Markdown files or URLs into the skill roots the harness discovers automatically",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"@deepseek-ai/dsh-client-locale",
|
|
25
25
|
"@deepseek-ai/dsh-client-ui-slots",
|
|
26
26
|
"@deepseek-ai/dsh-client-ui-settings",
|
|
27
|
+
"@deepseek-ai/dsh-client-ui-primitives",
|
|
27
28
|
"@deepseek-ai/dsh-api-remotes"
|
|
28
29
|
],
|
|
29
30
|
"platform": "web"
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
"scripts": {
|
|
46
47
|
"build": "rm -rf lib && tsc -p tsconfig.json && tsdown",
|
|
47
48
|
"test:batch": "node --experimental-strip-types tests/batch-import.test.mjs",
|
|
49
|
+
"test:atomic-delete": "node --experimental-strip-types tests/atomic-skill-delete.test.mjs",
|
|
48
50
|
"watch": "tsdown --watch"
|
|
49
51
|
},
|
|
50
52
|
"license": "MIT",
|
|
@@ -55,6 +57,7 @@
|
|
|
55
57
|
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
56
58
|
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
57
59
|
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
|
|
60
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
|
|
58
61
|
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
59
62
|
"react": "^18.2.0",
|
|
60
63
|
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
@@ -71,6 +74,7 @@
|
|
|
71
74
|
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
72
75
|
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
73
76
|
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
|
|
74
78
|
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
75
79
|
"@types/react": "^18.3.0",
|
|
76
80
|
"react": "^18.2.0",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { atomicSkillArrow, atomicSkillBackspace } from '../src/client/atomicSkillDelete.ts'
|
|
3
|
+
|
|
4
|
+
const names = ['foo', 'foo-bar', 'pangu-agent']
|
|
5
|
+
|
|
6
|
+
assert.deepEqual(atomicSkillBackspace('/pangu-agent ', 13, names), { draft: '', caret: 0 })
|
|
7
|
+
assert.deepEqual(atomicSkillBackspace('/pangu-agent ', 12, names), { draft: '', caret: 0 })
|
|
8
|
+
assert.deepEqual(atomicSkillBackspace('/foo-bar explain', 9, names), { draft: 'explain', caret: 0 })
|
|
9
|
+
assert.deepEqual(atomicSkillBackspace('/foo-bar', 8, names), { draft: '', caret: 0 })
|
|
10
|
+
|
|
11
|
+
assert.equal(atomicSkillBackspace('/pangu-agent ', 6, names), undefined)
|
|
12
|
+
assert.equal(atomicSkillBackspace('/unknown ', 9, names), undefined)
|
|
13
|
+
assert.equal(atomicSkillBackspace(' /pangu-agent ', 14, names), undefined)
|
|
14
|
+
assert.equal(atomicSkillBackspace('/foo-barium ', 12, names), undefined)
|
|
15
|
+
assert.equal(atomicSkillBackspace('/foo\nnext', 5, names), undefined)
|
|
16
|
+
|
|
17
|
+
assert.equal(atomicSkillArrow('/pangu-agent ', 13, 'left', names), 0)
|
|
18
|
+
assert.equal(atomicSkillArrow('/pangu-agent ', 6, 'left', names), 0)
|
|
19
|
+
assert.equal(atomicSkillArrow('/pangu-agent ', 0, 'right', names), 13)
|
|
20
|
+
assert.equal(atomicSkillArrow('/pangu-agent explain', 13, 'right', names), undefined)
|
|
21
|
+
assert.equal(atomicSkillArrow('/pangu-agent explain', 14, 'left', names), undefined)
|
|
22
|
+
assert.equal(atomicSkillArrow('/unknown ', 0, 'right', names), undefined)
|
|
23
|
+
|
|
24
|
+
console.log('atomic skill delete test: OK')
|