dsh-skill-importer 0.2.4 → 0.2.5

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
@@ -2102,15 +2102,70 @@ window.__ModuleLoader__.load({
2102
2102
  * with `/name ` — the native text gesture — highlighted via the plugin's
2103
2103
  * lexicon contribution.
2104
2104
  */
2105
- /** Find the one composer textarea owned by the picker nearest in the slot tree. */
2106
- function composerTextareaFor(root) {
2105
+ /** Find the one legacy textarea or Lexical contenteditable owned by this picker. */
2106
+ function composerInputFor(root) {
2107
2107
  let node = root?.parentElement;
2108
2108
  while (node !== null && node !== void 0) {
2109
- const textareas = node.querySelectorAll("textarea");
2110
- if (textareas.length > 0) return textareas.length === 1 ? textareas[0] : void 0;
2109
+ const inputs = node.querySelectorAll("textarea, [data-composer-input]");
2110
+ if (inputs.length > 0) return inputs.length === 1 ? inputs[0] : void 0;
2111
2111
  node = node.parentElement;
2112
2112
  }
2113
2113
  }
2114
+ function composerText(input) {
2115
+ return input instanceof HTMLTextAreaElement ? input.value : input.textContent ?? "";
2116
+ }
2117
+ function composerCaret(input) {
2118
+ if (input instanceof HTMLTextAreaElement) {
2119
+ if (input.selectionStart === null || input.selectionEnd === null) return void 0;
2120
+ return {
2121
+ start: input.selectionStart,
2122
+ end: input.selectionEnd
2123
+ };
2124
+ }
2125
+ const selection = window.getSelection();
2126
+ if (selection === null || selection.rangeCount !== 1) return void 0;
2127
+ const range = selection.getRangeAt(0);
2128
+ if (!input.contains(range.startContainer) || !input.contains(range.endContainer)) return void 0;
2129
+ const offsetOf = (container, offset) => {
2130
+ const prefix = range.cloneRange();
2131
+ prefix.selectNodeContents(input);
2132
+ prefix.setEnd(container, offset);
2133
+ return prefix.toString().length;
2134
+ };
2135
+ return {
2136
+ start: offsetOf(range.startContainer, range.startOffset),
2137
+ end: offsetOf(range.endContainer, range.endOffset)
2138
+ };
2139
+ }
2140
+ function setComposerCaret(input, caret) {
2141
+ input.focus({ preventScroll: true });
2142
+ if (input instanceof HTMLTextAreaElement) {
2143
+ input.setSelectionRange(caret, caret);
2144
+ return;
2145
+ }
2146
+ const range = document.createRange();
2147
+ let remaining = caret;
2148
+ const walker = document.createTreeWalker(input, NodeFilter.SHOW_TEXT);
2149
+ let text = walker.nextNode();
2150
+ while (text !== null) {
2151
+ const length = text.textContent?.length ?? 0;
2152
+ if (remaining <= length) {
2153
+ range.setStart(text, remaining);
2154
+ range.collapse(true);
2155
+ const selection = window.getSelection();
2156
+ selection?.removeAllRanges();
2157
+ selection?.addRange(range);
2158
+ return;
2159
+ }
2160
+ remaining -= length;
2161
+ text = walker.nextNode();
2162
+ }
2163
+ range.selectNodeContents(input);
2164
+ range.collapse(false);
2165
+ const selection = window.getSelection();
2166
+ selection?.removeAllRanges();
2167
+ selection?.addRange(range);
2168
+ }
2114
2169
  /** One row of the composer skill picker. */
2115
2170
  function SkillsPicker({ t, sessionId, useInput, inputActions, actions }) {
2116
2171
  const input = useInput((value) => value.draft);
@@ -2164,13 +2219,12 @@ window.__ModuleLoader__.load({
2164
2219
  draftRef.current = input;
2165
2220
  skillNamesRef.current = usable.map((skill) => skill.name);
2166
2221
  const scheduleComposerFocus = (expectedDraft, caret) => {
2167
- const textarea = composerTextareaFor(rootRef.current);
2222
+ const input = composerInputFor(rootRef.current);
2168
2223
  if (caretFrameRef.current !== void 0) window.cancelAnimationFrame(caretFrameRef.current);
2169
2224
  caretFrameRef.current = window.requestAnimationFrame(() => {
2170
2225
  caretFrameRef.current = void 0;
2171
- if (textarea === void 0 || !textarea.isConnected || textarea.value !== expectedDraft) return;
2172
- textarea.focus({ preventScroll: true });
2173
- textarea.setSelectionRange(caret, caret);
2226
+ if (input === void 0 || !input.isConnected || composerText(input) !== expectedDraft) return;
2227
+ setComposerCaret(input, caret);
2174
2228
  });
2175
2229
  };
2176
2230
  (0, react.useEffect)(() => {
@@ -2192,18 +2246,20 @@ window.__ModuleLoader__.load({
2192
2246
  if (event.defaultPrevented || event.isComposing || event.keyCode === 229) return;
2193
2247
  if (event.metaKey || event.ctrlKey || event.altKey) return;
2194
2248
  if (event.shiftKey && event.key !== "Backspace") return;
2195
- const textarea = event.target;
2196
- if (!(textarea instanceof HTMLTextAreaElement) || document.activeElement !== textarea) return;
2197
- if (composerTextareaFor(rootRef.current) !== textarea || textarea.value !== draftRef.current) return;
2198
- const start = textarea.selectionStart;
2199
- const end = textarea.selectionEnd;
2200
- if (start === null || end === null || start !== end) return;
2249
+ const target = event.target;
2250
+ if (!(target instanceof Node)) return;
2251
+ const input = composerInputFor(rootRef.current);
2252
+ if (input === void 0 || document.activeElement !== input || !input.contains(target)) return;
2253
+ if (composerText(input) !== draftRef.current) return;
2254
+ const selection = composerCaret(input);
2255
+ if (selection === void 0 || selection.start !== selection.end) return;
2256
+ const start = selection.start;
2201
2257
  if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
2202
2258
  const caret = atomicSkillArrow(draftRef.current, start, event.key === "ArrowLeft" ? "left" : "right", skillNamesRef.current);
2203
2259
  if (caret === void 0) return;
2204
2260
  event.preventDefault();
2205
2261
  event.stopPropagation();
2206
- textarea.setSelectionRange(caret, caret);
2262
+ setComposerCaret(input, caret);
2207
2263
  return;
2208
2264
  }
2209
2265
  const edit = atomicSkillBackspace(draftRef.current, start, skillNamesRef.current);
@@ -2214,8 +2270,8 @@ window.__ModuleLoader__.load({
2214
2270
  if (caretFrameRef.current !== void 0) window.cancelAnimationFrame(caretFrameRef.current);
2215
2271
  caretFrameRef.current = window.requestAnimationFrame(() => {
2216
2272
  caretFrameRef.current = void 0;
2217
- if (!textarea.isConnected || document.activeElement !== textarea || textarea.value !== edit.draft) return;
2218
- textarea.setSelectionRange(edit.caret, edit.caret);
2273
+ if (!input.isConnected || document.activeElement !== input || composerText(input) !== edit.draft) return;
2274
+ setComposerCaret(input, edit.caret);
2219
2275
  });
2220
2276
  };
2221
2277
  document.addEventListener("keydown", onKeyDown, true);
@@ -2703,12 +2759,46 @@ window.__ModuleLoader__.load({
2703
2759
  };
2704
2760
  //#endregion
2705
2761
  //#region src/client/index.ts
2706
- /** Required services (cordis fiber inject). */
2762
+ /** Every client service used during apply is explicit; alpha DSH no longer tolerates load-order access. */
2707
2763
  const inject = [
2708
2764
  "slots",
2709
2765
  "locale",
2710
- "connection"
2766
+ "connection",
2767
+ "remote",
2768
+ "remote.skills",
2769
+ "remote.directoryPicker",
2770
+ "inputTriggers",
2771
+ "commandUi",
2772
+ "sessions",
2773
+ "conversation"
2711
2774
  ];
2775
+ const popupEscapeHandlers = /* @__PURE__ */ new WeakMap();
2776
+ /** Restore keyboard ownership when DSH opens this plugin's popup from the composer. */
2777
+ function focusSkillsPopupSearch(options) {
2778
+ const focus = () => {
2779
+ for (const input of document.querySelectorAll("input[type=\"text\"]")) {
2780
+ let node = input;
2781
+ while (node !== null) {
2782
+ if ((node.getAttribute("aria-label") ?? "").toLowerCase().includes("skills")) {
2783
+ const previous = popupEscapeHandlers.get(input);
2784
+ if (previous !== void 0) input.removeEventListener("keydown", previous);
2785
+ const onKeyDown = (event) => {
2786
+ if (event.key !== "Escape") return;
2787
+ window.setTimeout(options.onEscape, 0);
2788
+ };
2789
+ popupEscapeHandlers.set(input, onKeyDown);
2790
+ input.addEventListener("keydown", onKeyDown);
2791
+ input.focus({ preventScroll: true });
2792
+ options.resetActive();
2793
+ return;
2794
+ }
2795
+ node = node.parentElement;
2796
+ }
2797
+ }
2798
+ };
2799
+ window.requestAnimationFrame(focus);
2800
+ window.setTimeout(focus, 0);
2801
+ }
2712
2802
  /** Same-origin route literals (mirrors the host half's ROUTES). */
2713
2803
  const ROUTES = {
2714
2804
  list: "/skill-importer/list",
@@ -2762,12 +2852,10 @@ window.__ModuleLoader__.load({
2762
2852
  console.error("[dsh-skill-importer] listener failed:", error);
2763
2853
  }
2764
2854
  };
2765
- const connection = ctx.get("connection");
2766
- const skillsApi = connection.api.skills;
2767
- skillsApi.list.bind(skillsApi);
2768
- skillsApi.list = ((_request, _signal) => {
2769
- return Promise.reject(/* @__PURE__ */ new Error("skill group hidden: use the composer skill picker or /skills"));
2770
- });
2855
+ const legacyApi = ctx.get("connection").api;
2856
+ const remote = ctx.remote ?? ctx.get("remote");
2857
+ const skillsApi = remote?.skills ?? legacyApi?.skills;
2858
+ if (skillsApi !== void 0) skillsApi.list = () => Promise.reject(/* @__PURE__ */ new Error("skill group hidden: use the composer skill picker or /skills"));
2771
2859
  const usableNames = () => [...new Set([...sessionCatalogs.values()].flat().filter((skill) => skill.userInvocable).map((skill) => skill.name))];
2772
2860
  ctx.effect(() => {
2773
2861
  return ctx.get("inputTriggers").registerSource({
@@ -2792,8 +2880,7 @@ window.__ModuleLoader__.load({
2792
2880
  }, "dsh-skill-importer: lexicon source");
2793
2881
  const commandUi = ctx.get("commandUi");
2794
2882
  const sessions = ctx.get("sessions");
2795
- const conversation = ctx.get("conversation");
2796
- if (commandUi !== void 0 && sessions !== void 0 && conversation !== void 0) ctx.effect(() => {
2883
+ if (commandUi !== void 0 && sessions !== void 0) ctx.effect(() => {
2797
2884
  return commandUi.register({
2798
2885
  name: "skills",
2799
2886
  description: t("commandDescription"),
@@ -2801,24 +2888,53 @@ window.__ModuleLoader__.load({
2801
2888
  ui: {
2802
2889
  kind: "popupSelect",
2803
2890
  async options(session, _signal) {
2804
- return (await refreshSkillsForSession(session.sessionId)).filter((skill) => skill.userInvocable).map((skill) => ({
2891
+ const rows = await refreshSkillsForSession(session.sessionId);
2892
+ const restoreComposerFocus = () => {
2893
+ const actx = sessions.scope(session.sessionId);
2894
+ if (actx === void 0) return;
2895
+ const conversation = actx.get("conversation");
2896
+ if (conversation === void 0) return;
2897
+ const draft = conversation.input.for(actx).state.getSnapshot().draft;
2898
+ requestComposerFocus({
2899
+ sessionId: session.sessionId,
2900
+ expectedDraft: draft,
2901
+ caret: draft.length
2902
+ });
2903
+ };
2904
+ const resetActive = () => {
2905
+ const actx = sessions.scope(session.sessionId);
2906
+ if (actx === void 0) return;
2907
+ commandUi.popupFor(actx).highlight?.(0);
2908
+ };
2909
+ focusSkillsPopupSearch({
2910
+ onEscape: restoreComposerFocus,
2911
+ resetActive
2912
+ });
2913
+ return rows.filter((skill) => skill.userInvocable).map((skill) => ({
2805
2914
  id: skill.name,
2806
2915
  label: `${skill.name} · ${sourceLabel(skill.source)}`
2807
2916
  }));
2808
2917
  },
2809
2918
  onSelect(option, session) {
2810
2919
  const actx = sessions.scope(session.sessionId);
2811
- if (actx === void 0) return;
2812
- const input = conversation.input.for(actx);
2813
- const rest = input.state.getSnapshot().draft.replace(/^\s*\/[a-zA-Z][a-zA-Z0-9-]*\s*/, "");
2920
+ if (actx === void 0) throw new Error("this session is no longer available");
2921
+ const conversation = actx.get("conversation");
2922
+ if (conversation === void 0) throw new Error("conversation input is unavailable");
2923
+ const rest = conversation.input.for(actx).state.getSnapshot().draft.replace(/^\s*\/[a-zA-Z][a-zA-Z0-9-]*\s*/, "");
2814
2924
  const prefix = `/${option.id} `;
2815
2925
  const nextDraft = `${prefix}${rest}`;
2816
- input.setDraft(nextDraft);
2817
- requestComposerFocus({
2818
- sessionId: session.sessionId,
2819
- expectedDraft: nextDraft,
2820
- caret: prefix.length
2821
- });
2926
+ window.setTimeout(() => {
2927
+ const liveScope = sessions.scope(session.sessionId);
2928
+ if (liveScope === void 0) return;
2929
+ const liveConversation = liveScope.get("conversation");
2930
+ if (liveConversation === void 0) return;
2931
+ liveConversation.input.for(liveScope).setDraft(nextDraft);
2932
+ requestComposerFocus({
2933
+ sessionId: session.sessionId,
2934
+ expectedDraft: nextDraft,
2935
+ caret: prefix.length
2936
+ });
2937
+ }, 0);
2822
2938
  }
2823
2939
  }
2824
2940
  });
@@ -2847,7 +2963,13 @@ window.__ModuleLoader__.load({
2847
2963
  return (await call(ROUTES.delete, request)).removed;
2848
2964
  };
2849
2965
  const pickDirectory = async () => {
2850
- const response = await connection.api.host.pickDirectory({});
2966
+ if (remote?.directoryPicker !== void 0) {
2967
+ const result = await remote.directoryPicker.pick();
2968
+ if (!result.ok) throw new Error(result.error.message);
2969
+ return result.value;
2970
+ }
2971
+ if (legacyApi?.host === void 0) throw new Error("当前 DSH 未提供目录选择能力");
2972
+ const response = await legacyApi.host.pickDirectory({});
2851
2973
  if (!response.result.ok) throw new Error(response.result.error.message);
2852
2974
  return response.result.value.path;
2853
2975
  };
@@ -43,7 +43,7 @@ export interface SkillImporterInjected {
43
43
  checkForUpdates: () => Promise<UpdateStatusResponse>;
44
44
  };
45
45
  }
46
- /** Required services (cordis fiber inject). */
46
+ /** Every client service used during apply is explicit; alpha DSH no longer tolerates load-order access. */
47
47
  export declare const inject: string[];
48
48
  /**
49
49
  * Client plugin body: register the section and its dictionaries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-skill-importer",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
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",
@@ -14,7 +14,7 @@ test('orders prereleases before stable releases', () => {
14
14
  })
15
15
 
16
16
  test('reads package version and produces an exact update command', () => {
17
- assert.equal(currentVersion(), '0.2.4')
17
+ assert.equal(currentVersion(), '0.2.5')
18
18
  assert.equal(
19
19
  updateCommand('0.2.4'),
20
20
  'npx @deepseek-ai/dsh plugin --profile web add dsh-skill-importer@0.2.4',