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 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 (skill) => {
306
- if (!window.confirm(t("deleteConfirm", { name: skill.name }))) return;
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
- })) setMessage(t("deleted"));
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 commitBatchDirectory = async () => {
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
- if (!window.confirm(t("batchConfirm", {
418
+ setBatchConfirmation({
413
419
  importing,
414
420
  replacing,
415
421
  skipped
416
- }))) return;
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 commitBatchDirectory();
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(`/${name} ${input}`);
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
- input.setDraft(`/${option.id} ${rest}`);
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.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')