dsh-prompt-shelf 0.1.0 → 0.1.1

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/README.en.md CHANGED
@@ -27,6 +27,7 @@ New-session blank pages show it too.
27
27
 
28
28
  Write `{{name}}` or `{{name=default}}` in the body and that spot becomes an input field on the card:
29
29
  inserting substitutes the filled value, falling back to the default, and same-named variables share one value.
30
+ A variable box also accepts `@` to reference workspace files (see below).
30
31
 
31
32
  Editing a card shows the raw `{{…}}` template, which is where you change it. While creating or editing,
32
33
  "+ Variable" below the body box inserts `{{variable}}` at the cursor and selects the word, so typing renames it.
@@ -38,6 +39,9 @@ In that body box, **Alt+V** (or Ctrl+Alt+V) does the same; it only works there,
38
39
  - In the card editor's inputs, typing `@` lists workspace files and folders (arrow keys to move, Enter or Tab
39
40
  to pick, Esc to close); picking inserts `@path` (folders end with a slash, and a path containing spaces
40
41
  is written as `@"path"`).
42
+ - In a card's **variable box**, typing `@` lists the same files, sessions and plugins (the list floats below
43
+ the input and flips above when there is no room); picking fills `@path` into that variable's value, so after
44
+ inserting or replacing into the composer the model reads the file as an @ reference.
41
45
 
42
46
  ## Where the data lives
43
47
 
@@ -62,7 +66,7 @@ dsh plugin --profile desktop add dsh-prompt-shelf
62
66
  ```
63
67
 
64
68
  Restart DSH afterwards. Replace `desktop` with your own profile name if you use another one.
65
- Requirements: the DSH 0.2.1 line, desktop app or `dsh web`. Compatible with DSH 0.2.1; 0.3 and later are not guaranteed.
69
+ Requirements: DSH 0.2.1 or later, desktop app or `dsh web` (verified on 0.2.1-alpha.3 and 0.2.2-rc.1). Requires DSH >= 0.2.1-alpha.3; verified on 0.2.1-alpha.3 and 0.2.2-rc.1, later versions untested.
66
70
 
67
71
  ## How it works
68
72
 
package/README.md CHANGED
@@ -24,7 +24,7 @@
24
24
  ## 变量
25
25
 
26
26
  正文里写 `{{名称}}` 或 `{{名称=默认值}}`,这张卡片的正文里那个位置就变成一个输入框:
27
- 插入时用填的值替换,不填就用默认值,同名变量共用一份值。
27
+ 插入时用填的值替换,不填就用默认值,同名变量共用一份值。变量框里打 `@` 可以引用工作区文件(见下)。
28
28
 
29
29
  编辑卡片时看到的是带 `{{…}}` 的原文——改模板在那里改。新建或编辑时正文框下方有「+ 变量」,
30
30
  把光标放到要放变量的位置点它就在光标处插入 `{{变量}}` 并选中「变量」两字,直接打字就是改名。
@@ -35,6 +35,8 @@
35
35
  - **任务输入框**里打 `@`:菜单里多出「常用提示词」一组,选中一条把正文落到光标处。
36
36
  - **新建 / 编辑卡片的输入框**里打 `@`:列出工作区文件与文件夹(上下键选、回车或 Tab 选中、Esc 关),
37
37
  选中插入 `@路径`(目录带尾斜杠,路径含空格时用 `@"路径"`)。
38
+ - **卡片上的变量框**里打 `@`:同样列出文件、对话、插件(浮层出现在输入框下方,放不下时翻到上方),
39
+ 选中把 `@路径` 填进这个变量的值;插入或替换进任务输入框后,模型按 @ 引用去读这个文件。
38
40
 
39
41
  ## 数据存哪
40
42
 
@@ -57,7 +59,7 @@ dsh plugin --profile desktop add dsh-prompt-shelf
57
59
  ```
58
60
 
59
61
  装完重启 DSH。用别的 profile 就把 `desktop` 换成你的 profile 名。
60
- 要求:DSH 0.2.1 系列的桌面端或 `dsh web`。兼容:DSH 0.2.1 系列;0.3 起不保证。
62
+ 要求:DSH 0.2.1 及以上的桌面端或 `dsh web`(0.2.1-alpha.3、0.2.2-rc.1 验证通过)。要求 DSH >= 0.2.1-alpha.3;实测 0.2.1-alpha.3、0.2.2-rc.1,更高版本未验证。
61
63
 
62
64
  ## 机制
63
65
 
package/lib/client.js CHANGED
@@ -39,7 +39,7 @@ var API_URL = "/dsh-prompt-shelf/api/state";
39
39
  var FILES_API = "/dsh-prompt-shelf/api/files";
40
40
  var PLUGINS_API = "/dsh-prompt-shelf/api/plugins";
41
41
  var TITLE_MAX = 28;
42
- var TEMPLATE_HINT = "\u6B63\u6587\u91CC\u5199 {{\u53D8\u91CF\u540D}}\uFF0C\u6216 {{\u53D8\u91CF\u540D=\u9ED8\u8BA4\u503C}}\uFF1B\u63D2\u5165\u65F6\u7528\u5361\u7247\u91CC\u586B\u7684\u503C\u66FF\u6362\uFF08\u4E0D\u586B\u5C31\u7528\u9ED8\u8BA4\u503C\uFF09\u3002\u6B63\u6587\u6846\u91CC\u6309 Alt+V\uFF08\u6216 Ctrl+Alt+V\uFF09\u4E5F\u80FD\u63D2\u5165\u53D8\u91CF\u3002";
42
+ var TEMPLATE_HINT = "\u6B63\u6587\u91CC\u5199 {{\u53D8\u91CF\u540D}}\uFF0C\u6216 {{\u53D8\u91CF\u540D=\u9ED8\u8BA4\u503C}}\uFF1B\u63D2\u5165\u65F6\u7528\u5361\u7247\u91CC\u586B\u7684\u503C\u66FF\u6362\uFF08\u4E0D\u586B\u5C31\u7528\u9ED8\u8BA4\u503C\uFF09\u3002\u6B63\u6587\u6846\u91CC\u6309 Alt+V\uFF08\u6216 Ctrl+Alt+V\uFF09\u4E5F\u80FD\u63D2\u5165\u53D8\u91CF\uFF1B\u5361\u7247\u4E0A\u7684\u53D8\u91CF\u6846\u91CC\u6253 @ \u53EF\u5F15\u7528\u5DE5\u4F5C\u533A\u6587\u4EF6\u3002";
43
43
  var EMPTY_INPUT = { draft: "" };
44
44
  var CSS = `
45
45
  .zps-root {
@@ -213,13 +213,13 @@ var CSS = `
213
213
  }
214
214
  /* \u6709\u53D8\u91CF\u7684\u5361\u7247\uFF1A\u6B63\u6587\u6309\u6BB5\u6E32\u67D3\uFF0C\u53D8\u91CF\u4F4D\u7F6E\u662F\u5C0F\u8F93\u5165\u6846\uFF0C\u76F4\u63A5\u539F\u4F4D\u6539\u3002 */
215
215
  .zps-cardtext-vars { display: block; cursor: default; }
216
+ .zps-varwrap { display: inline-block; margin: 0 2px; vertical-align: baseline; }
216
217
  .zps-var {
217
218
  display: inline-block;
218
219
  box-sizing: border-box;
219
220
  min-width: 84px;
220
221
  max-width: 260px;
221
222
  height: 22px;
222
- margin: 0 2px;
223
223
  padding: 0 6px;
224
224
  border: 1px solid var(--dsw-alias-border-l2);
225
225
  border-radius: var(--dsw-radius-xs);
@@ -230,6 +230,15 @@ var CSS = `
230
230
  vertical-align: baseline;
231
231
  }
232
232
  .zps-var:focus { outline: none; border-color: var(--dsw-alias-brand-primary); }
233
+ /* \u53D8\u91CF\u6846\u7684 @ \u5019\u9009\uFF1Aportal \u5230 body \u7684\u6D6E\u5C42\uFF0C\u4E0D\u88AB\u5361\u7247\u6EDA\u52A8\u5BB9\u5668\u88C1\u6389\u3002 */
234
+ .zps-varmenu {
235
+ position: fixed;
236
+ z-index: 1100;
237
+ width: 360px;
238
+ max-width: calc(100vw - 16px);
239
+ border-radius: 10px;
240
+ box-shadow: var(--dsw-elevation-prominent);
241
+ }
233
242
  .zps-varbadge { flex: none; font-size: 11px; color: var(--dsw-alias-label-secondary); }
234
243
  .zps-placeholder {
235
244
  flex: 1 1 auto;
@@ -522,7 +531,8 @@ function createPromptSource() {
522
531
  return {
523
532
  trigger: "@",
524
533
  name: "\u5E38\u7528\u63D0\u793A\u8BCD",
525
- order: 10,
534
+ // 20:排在「工作区文件」(10) 之后、「历史会话」(30) 之前,和别的 @ 源同序时顺序稳定。
535
+ order: 20,
526
536
  async candidates(_session, req) {
527
537
  const keyword = String(req.query || "").trim().toLowerCase();
528
538
  const used = /* @__PURE__ */ new Map();
@@ -537,6 +547,9 @@ function createPromptSource() {
537
547
  };
538
548
  });
539
549
  },
550
+ // 纯文本出口:返回 { text } 换掉触发词,草稿里只有文字、不生成引用卡片——
551
+ // 所以这个源不需要 codec。哪天改成返回 { insert: { source: … } } 插卡片,
552
+ // 就必须同时补 codec,否则发送会报 slash: no serializer for reference source "常用提示词"。
540
553
  onPick(pick) {
541
554
  const id = pick.candidate.value;
542
555
  const item = knownItems().find((it) => it.id === id);
@@ -603,6 +616,9 @@ function mentionAt(value, caret) {
603
616
  if (at > 0 && !/\s/.test(before[at - 1])) return null;
604
617
  return { start: at, end: pos, query };
605
618
  }
619
+ function typedAtChar(previous, next, caret) {
620
+ return String(next || "").length === String(previous || "").length + 1 && String(next || "").charAt(caret - 1) === "@";
621
+ }
606
622
  function MentionIcon(props) {
607
623
  const kind = props.kind;
608
624
  const d = kind === "folder" ? "M1.8 4.2h3.4l1.2 1.3h5.8v6.1H1.8z" : kind === "session" ? "M2 3.6h10v6.1H6.6L4.2 11.7V9.7H2z" : kind === "plugin" ? "M3.2 3.2h7.6v7.6H3.2zM3.2 6.4h7.6M6.4 3.2v7.6" : "M4 1.9h4.4L10.9 4.4v7.7H4z";
@@ -690,26 +706,18 @@ function MentionMenu(props) {
690
706
  }) })
691
707
  ] });
692
708
  }
693
- function TemplateEditor(props) {
694
- const editing = props.editing;
695
- const onTitle = props.onTitle;
696
- const onText = props.onText;
697
- const onCancel = props.onCancel;
698
- const onSave = props.onSave;
699
- const titleRef = (0, import_react.useRef)(null);
700
- const textareaRef = (0, import_react.useRef)(null);
709
+ function useMentionField(listFiles, applyPick) {
701
710
  const [menu, setMenu] = (0, import_react.useState)(null);
702
711
  const [chosen, setChosen] = (0, import_react.useState)(0);
703
712
  const [activeSection, setActiveSection] = (0, import_react.useState)(null);
704
713
  const [hits, setHits] = (0, import_react.useState)([]);
705
714
  const fetchTimer = (0, import_react.useRef)(0);
706
715
  const fetchSeq = (0, import_react.useRef)(0);
707
- const prevValue = (0, import_react.useRef)("");
708
716
  const refreshHits = (0, import_react.useCallback)((query) => {
709
717
  window.clearTimeout(fetchTimer.current);
710
718
  const seq = ++fetchSeq.current;
711
719
  fetchTimer.current = window.setTimeout(() => {
712
- const ask = props.listFiles;
720
+ const ask = listFiles;
713
721
  if (typeof ask !== "function") {
714
722
  setHits([]);
715
723
  return;
@@ -721,71 +729,51 @@ function TemplateEditor(props) {
721
729
  if (seq === fetchSeq.current) setHits([]);
722
730
  });
723
731
  }, 120);
724
- }, [props.listFiles]);
732
+ }, [listFiles]);
725
733
  (0, import_react.useEffect)(() => () => window.clearTimeout(fetchTimer.current), []);
726
- const rows = hits;
727
- const visibleRows = activeSection ? rows.filter((row) => row.section === activeSection) : rows;
728
- const openMention = (0, import_react.useCallback)((field, value, caret, element) => {
729
- if (!element) {
730
- setMenu(null);
731
- return;
732
- }
733
- let pos = typeof caret === "number" && caret > 0 ? caret : 0;
734
- if (pos === 0 && typeof element.selectionStart === "number" && element.selectionStart > 0) {
735
- pos = element.selectionStart;
736
- }
737
- if (pos === 0) pos = String(value || "").length;
734
+ const track = (0, import_react.useCallback)((field, value, caret, typedAt) => {
735
+ const pos = typeof caret === "number" && caret > 0 ? caret : String(value || "").length;
738
736
  const hit = mentionAt(value, pos);
739
- if (!hit) {
740
- setMenu(null);
737
+ if (typedAt) {
738
+ if (!hit) {
739
+ setMenu(null);
740
+ return;
741
+ }
742
+ setMenu({ field, start: hit.start, end: hit.end, query: hit.query });
743
+ setChosen(0);
744
+ setActiveSection(null);
745
+ refreshHits(hit.query);
741
746
  return;
742
747
  }
743
- setMenu({ field, start: hit.start, end: hit.end, query: hit.query });
744
- setChosen(0);
745
- setActiveSection(null);
746
- refreshHits(hit.query);
747
- }, [refreshHits]);
748
- const followMention = (0, import_react.useCallback)((field, value, caret) => {
749
748
  setMenu((prev) => {
750
749
  if (!prev || prev.field !== field) return prev;
751
- const pos = typeof caret === "number" && caret > 0 ? caret : String(value || "").length;
752
- const hit = mentionAt(value, pos);
753
750
  if (!hit) return null;
754
751
  refreshHits(hit.query);
755
- return { ...prev, start: hit.start, end: hit.end, query: hit.query };
752
+ return { field, start: hit.start, end: hit.end, query: hit.query };
756
753
  });
757
754
  }, [refreshHits]);
758
- const pickMention = (0, import_react.useCallback)((row) => {
755
+ const close = (0, import_react.useCallback)(() => setMenu(null), []);
756
+ const rows = activeSection ? hits.filter((row) => row.section === activeSection) : hits;
757
+ const pick = (0, import_react.useCallback)((row) => {
759
758
  if (!menu || !row) return;
760
- const isTitle = menu.field === "title";
761
- const current = isTitle ? String(editing.title || "") : String(editing.text || "");
762
- const next = current.slice(0, menu.start) + row.mention + current.slice(menu.end);
763
- if (isTitle) onTitle(next);
764
- else onText(next);
765
- const caret = menu.start + row.mention.length;
766
- prevValue.current = next;
759
+ const settled = menu;
767
760
  setMenu(null);
768
- window.requestAnimationFrame(() => {
769
- const el = isTitle ? titleRef.current : textareaRef.current;
770
- if (!el) return;
771
- el.focus();
772
- el.setSelectionRange(caret, caret);
773
- });
774
- }, [editing, menu, onText, onTitle]);
775
- const menuKey = (0, import_react.useCallback)((event) => {
761
+ applyPick(row, settled);
762
+ }, [applyPick, menu]);
763
+ const onKeyDown = (0, import_react.useCallback)((event) => {
776
764
  if (!menu) return false;
777
765
  if (event.key === "ArrowDown" || event.key === "ArrowUp") {
778
766
  event.preventDefault();
779
- if (visibleRows.length > 0) {
767
+ if (rows.length > 0) {
780
768
  const step = event.key === "ArrowDown" ? 1 : -1;
781
- setChosen((prev) => (prev + step + visibleRows.length) % visibleRows.length);
769
+ setChosen((prev) => (prev + step + rows.length) % rows.length);
782
770
  }
783
771
  return true;
784
772
  }
785
773
  if (event.key === "Enter" || event.key === "Tab") {
786
774
  event.preventDefault();
787
- const row = visibleRows[Math.min(chosen, Math.max(0, visibleRows.length - 1))] || visibleRows[0];
788
- if (row) pickMention(row);
775
+ const row = rows[Math.min(chosen, Math.max(0, rows.length - 1))] || rows[0];
776
+ if (row) pick(row);
789
777
  return true;
790
778
  }
791
779
  if (event.key === "Escape") {
@@ -794,7 +782,34 @@ function TemplateEditor(props) {
794
782
  return true;
795
783
  }
796
784
  return false;
797
- }, [chosen, menu, pickMention, visibleRows]);
785
+ }, [chosen, menu, pick, rows]);
786
+ return { menu, rows, chosen, activeSection, setActiveSection, setChosen, track, onKeyDown, close };
787
+ }
788
+ function TemplateEditor(props) {
789
+ const editing = props.editing;
790
+ const onTitle = props.onTitle;
791
+ const onText = props.onText;
792
+ const onCancel = props.onCancel;
793
+ const onSave = props.onSave;
794
+ const titleRef = (0, import_react.useRef)(null);
795
+ const textareaRef = (0, import_react.useRef)(null);
796
+ const prevValue = (0, import_react.useRef)("");
797
+ const applyMention = (0, import_react.useCallback)((row, menu) => {
798
+ const isTitle = menu.field === "title";
799
+ const current = isTitle ? String(editing.title || "") : String(editing.text || "");
800
+ const next = current.slice(0, menu.start) + row.mention + current.slice(menu.end);
801
+ if (isTitle) onTitle(next);
802
+ else onText(next);
803
+ const caret = menu.start + row.mention.length;
804
+ prevValue.current = next;
805
+ window.requestAnimationFrame(() => {
806
+ const el = isTitle ? titleRef.current : textareaRef.current;
807
+ if (!el) return;
808
+ el.focus();
809
+ el.setSelectionRange(caret, caret);
810
+ });
811
+ }, [editing, onText, onTitle]);
812
+ const mention = useMentionField(props.listFiles, applyMention);
798
813
  const insertVariable = (0, import_react.useCallback)(() => {
799
814
  const node = textareaRef.current;
800
815
  const text = String(editing.text || "");
@@ -820,14 +835,13 @@ function TemplateEditor(props) {
820
835
  onChange: (event) => {
821
836
  const value = event.target.value;
822
837
  const caret = event.target.selectionStart || 0;
823
- const typedAt = value.length === prevValue.current.length + 1 && value.charAt(caret - 1) === "@";
838
+ const typedAt = typedAtChar(prevValue.current, value, caret);
824
839
  prevValue.current = value;
825
840
  onTitle(value);
826
- if (typedAt) openMention("title", value, caret, event.target);
827
- else followMention("title", value, caret);
841
+ mention.track("title", value, caret, typedAt);
828
842
  },
829
843
  onKeyDown: (event) => {
830
- menuKey(event);
844
+ mention.onKeyDown(event);
831
845
  }
832
846
  }
833
847
  ),
@@ -841,14 +855,13 @@ function TemplateEditor(props) {
841
855
  onChange: (event) => {
842
856
  const value = event.target.value;
843
857
  const caret = event.target.selectionStart || 0;
844
- const typedAt = value.length === prevValue.current.length + 1 && value.charAt(caret - 1) === "@";
858
+ const typedAt = typedAtChar(prevValue.current, value, caret);
845
859
  prevValue.current = value;
846
860
  onText(value);
847
- if (typedAt) openMention("text", value, caret, event.target);
848
- else followMention("text", value, caret);
861
+ mention.track("text", value, caret, typedAt);
849
862
  },
850
863
  onKeyDown: (event) => {
851
- if (menuKey(event)) return;
864
+ if (mention.onKeyDown(event)) return;
852
865
  if (event.isComposing || event.repeat) return;
853
866
  const key = String(event.key || "").toLowerCase();
854
867
  if (key !== "v" || event.shiftKey || !event.altKey) return;
@@ -873,21 +886,109 @@ function TemplateEditor(props) {
873
886
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "zps-btn", onClick: onCancel, children: "\u53D6\u6D88" }),
874
887
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "zps-btn primary", onClick: onSave, children: "\u4FDD\u5B58" })
875
888
  ] }),
876
- menu ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
889
+ mention.menu ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
877
890
  MentionMenu,
878
891
  {
879
- rows: visibleRows,
880
- active: activeSection,
892
+ rows: mention.rows,
893
+ active: mention.activeSection,
881
894
  onSelect: (key) => {
882
- setActiveSection(key);
883
- setChosen(0);
895
+ mention.setActiveSection(key);
896
+ mention.setChosen(0);
884
897
  },
885
- chosen,
886
- onPick: pickMention
898
+ chosen: mention.chosen,
899
+ onPick: (row) => mention.pick(row)
887
900
  }
888
901
  ) : null
889
902
  ] });
890
903
  }
904
+ function VarField(props) {
905
+ const value = String(props.value || "");
906
+ const onChange = props.onChange;
907
+ const inputRef = (0, import_react.useRef)(null);
908
+ const prevValue = (0, import_react.useRef)(value);
909
+ const [anchor, setAnchor] = (0, import_react.useState)(null);
910
+ const applyMention = (0, import_react.useCallback)((row, menu) => {
911
+ const current = String(props.value || "");
912
+ const next = current.slice(0, menu.start) + row.mention + current.slice(menu.end);
913
+ onChange(next);
914
+ prevValue.current = next;
915
+ const caret = menu.start + row.mention.length;
916
+ window.requestAnimationFrame(() => {
917
+ const el = inputRef.current;
918
+ if (!el) return;
919
+ el.focus();
920
+ el.setSelectionRange(caret, caret);
921
+ });
922
+ }, [onChange, props.value]);
923
+ const mention = useMentionField(props.listFiles, applyMention);
924
+ const open = mention.menu !== null;
925
+ (0, import_react.useEffect)(() => {
926
+ if (!open) {
927
+ setAnchor(null);
928
+ return void 0;
929
+ }
930
+ const place = () => {
931
+ const el = inputRef.current;
932
+ if (!el) return;
933
+ const rect = el.getBoundingClientRect();
934
+ const width = Math.max(280, Math.min(420, rect.width + 240));
935
+ const height = 248;
936
+ const below = rect.bottom + 4 + height <= window.innerHeight;
937
+ setAnchor({
938
+ left: Math.max(8, Math.min(rect.left, window.innerWidth - width - 8)),
939
+ top: below ? rect.bottom + 4 : Math.max(8, rect.top - 4 - height),
940
+ width
941
+ });
942
+ };
943
+ place();
944
+ window.addEventListener("resize", place);
945
+ window.addEventListener("scroll", place, true);
946
+ return () => {
947
+ window.removeEventListener("resize", place);
948
+ window.removeEventListener("scroll", place, true);
949
+ };
950
+ }, [open]);
951
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "zps-varwrap", children: [
952
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
953
+ "input",
954
+ {
955
+ ref: inputRef,
956
+ className: "zps-var",
957
+ value,
958
+ placeholder: props.name,
959
+ title: "\u53D8\u91CF\uFF1A" + props.name + "\uFF08\u9ED8\u8BA4\u503C\uFF1A" + (props.fallback || "\u7A7A") + "\uFF1B\u6253 @ \u53EF\u5F15\u7528\u5DE5\u4F5C\u533A\u6587\u4EF6\uFF09",
960
+ onMouseDown: (event) => event.stopPropagation(),
961
+ onClick: (event) => event.stopPropagation(),
962
+ onBlur: () => mention.close(),
963
+ onChange: (event) => {
964
+ const next = event.target.value;
965
+ const caret = event.target.selectionStart || 0;
966
+ const typedAt = typedAtChar(prevValue.current, next, caret);
967
+ prevValue.current = next;
968
+ onChange(next);
969
+ mention.track("var", next, caret, typedAt);
970
+ },
971
+ onKeyDown: (event) => mention.onKeyDown(event)
972
+ }
973
+ ),
974
+ open && anchor ? (0, import_react_dom.createPortal)(
975
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "zps-varmenu", style: { left: anchor.left, top: anchor.top, width: anchor.width }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
976
+ MentionMenu,
977
+ {
978
+ rows: mention.rows,
979
+ active: mention.activeSection,
980
+ onSelect: (key) => {
981
+ mention.setActiveSection(key);
982
+ mention.setChosen(0);
983
+ },
984
+ chosen: mention.chosen,
985
+ onPick: (row) => mention.pick(row)
986
+ }
987
+ ) }),
988
+ document.body
989
+ ) : null
990
+ ] });
991
+ }
891
992
  function PromptShelf(props) {
892
993
  const useInput = props && typeof props.useInput === "function" ? props.useInput : null;
893
994
  const inputActions = props ? props.inputActions : void 0;
@@ -1242,15 +1343,13 @@ function PromptShelf(props) {
1242
1343
  listFiles: props.listFiles
1243
1344
  }
1244
1345
  ) : varNames.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "zps-cardtext zps-cardtext-vars", children: parts.map((part, index) => part.type === "text" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: part.value }, index) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1245
- "input",
1346
+ VarField,
1246
1347
  {
1247
- className: "zps-var",
1248
1348
  value: templateValue(varValues[item.id], part.name, part.fallback),
1249
- placeholder: part.name,
1250
- title: "\u53D8\u91CF\uFF1A" + part.name + "\uFF08\u9ED8\u8BA4\u503C\uFF1A" + (part.fallback || "\u7A7A") + "\uFF09",
1251
- onMouseDown: (event) => event.stopPropagation(),
1252
- onClick: (event) => event.stopPropagation(),
1253
- onChange: (event) => setVar(item.id, part.name, event.target.value)
1349
+ name: part.name,
1350
+ fallback: part.fallback,
1351
+ onChange: (value) => setVar(item.id, part.name, value),
1352
+ listFiles: props.listFiles
1254
1353
  },
1255
1354
  index
1256
1355
  )) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-prompt-shelf",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "在输入框工具行里存常用提示词:搜索、插入或整框替换,带变量与回收站。",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "engines": {
25
25
  "node": "^22.19.0 || >=24.0.0",
26
- "dsh": ">=0.2.1-0 <0.3.0-0"
26
+ "dsh": ">=0.2.1-0"
27
27
  },
28
28
  "dsh": {
29
29
  "client": {
package/src/entry.jsx CHANGED
@@ -31,7 +31,7 @@ const PLUGINS_API = '/dsh-prompt-shelf/api/plugins'
31
31
  /** 没写标题时,取正文首行前多少字当标题。 */
32
32
  const TITLE_MAX = 28
33
33
  /** 编辑表单下的占位符写法说明。 */
34
- const TEMPLATE_HINT = '正文里写 {{变量名}},或 {{变量名=默认值}};插入时用卡片里填的值替换(不填就用默认值)。正文框里按 Alt+V(或 Ctrl+Alt+V)也能插入变量。'
34
+ const TEMPLATE_HINT = '正文里写 {{变量名}},或 {{变量名=默认值}};插入时用卡片里填的值替换(不填就用默认值)。正文框里按 Alt+V(或 Ctrl+Alt+V)也能插入变量;卡片上的变量框里打 @ 可引用工作区文件。'
35
35
 
36
36
  /** 输入框不可用时的兜底状态(dock 是 session 范围,理论上永远有输入框)。 */
37
37
  const EMPTY_INPUT = { draft: '' }
@@ -208,13 +208,13 @@ const CSS = `
208
208
  }
209
209
  /* 有变量的卡片:正文按段渲染,变量位置是小输入框,直接原位改。 */
210
210
  .zps-cardtext-vars { display: block; cursor: default; }
211
+ .zps-varwrap { display: inline-block; margin: 0 2px; vertical-align: baseline; }
211
212
  .zps-var {
212
213
  display: inline-block;
213
214
  box-sizing: border-box;
214
215
  min-width: 84px;
215
216
  max-width: 260px;
216
217
  height: 22px;
217
- margin: 0 2px;
218
218
  padding: 0 6px;
219
219
  border: 1px solid var(--dsw-alias-border-l2);
220
220
  border-radius: var(--dsw-radius-xs);
@@ -225,6 +225,15 @@ const CSS = `
225
225
  vertical-align: baseline;
226
226
  }
227
227
  .zps-var:focus { outline: none; border-color: var(--dsw-alias-brand-primary); }
228
+ /* 变量框的 @ 候选:portal 到 body 的浮层,不被卡片滚动容器裁掉。 */
229
+ .zps-varmenu {
230
+ position: fixed;
231
+ z-index: 1100;
232
+ width: 360px;
233
+ max-width: calc(100vw - 16px);
234
+ border-radius: 10px;
235
+ box-shadow: var(--dsw-elevation-prominent);
236
+ }
228
237
  .zps-varbadge { flex: none; font-size: 11px; color: var(--dsw-alias-label-secondary); }
229
238
  .zps-placeholder {
230
239
  flex: 1 1 auto;
@@ -561,7 +570,8 @@ function createPromptSource() {
561
570
  return {
562
571
  trigger: '@',
563
572
  name: '常用提示词',
564
- order: 10,
573
+ // 20:排在「工作区文件」(10) 之后、「历史会话」(30) 之前,和别的 @ 源同序时顺序稳定。
574
+ order: 20,
565
575
  async candidates(_session, req) {
566
576
  const keyword = String(req.query || '').trim().toLowerCase()
567
577
  const used = new Map()
@@ -580,6 +590,9 @@ function createPromptSource() {
580
590
  }
581
591
  })
582
592
  },
593
+ // 纯文本出口:返回 { text } 换掉触发词,草稿里只有文字、不生成引用卡片——
594
+ // 所以这个源不需要 codec。哪天改成返回 { insert: { source: … } } 插卡片,
595
+ // 就必须同时补 codec,否则发送会报 slash: no serializer for reference source "常用提示词"。
583
596
  onPick(pick) {
584
597
  const id = pick.candidate.value
585
598
  const item = knownItems().find((it) => it.id === id)
@@ -689,6 +702,11 @@ function mentionAt(value, caret) {
689
702
  return { start: at, end: pos, query }
690
703
  }
691
704
 
705
+ /** 刚敲下一个 @:长度只多 1,且新字符就是 @。打开候选菜单的时机只认这个。 */
706
+ function typedAtChar(previous, next, caret) {
707
+ return String(next || '').length === String(previous || '').length + 1 && String(next || '').charAt(caret - 1) === '@'
708
+ }
709
+
692
710
  /** 行首图标:文件 / 文件夹 / 对话 / 插件(与输入框 @ 菜单同一套观感)。 */
693
711
  function MentionIcon(props) {
694
712
  const kind = props.kind
@@ -773,39 +791,26 @@ function MentionMenu(props) {
773
791
  }
774
792
 
775
793
  /**
776
- * 一张卡片的编辑表单(新建与编辑共用):标题 + 正文 +「+ 变量」。
777
- * 两个输入框里都能打 @ 选已有常用提示词:标题框插「名」,正文框插「整段正文」。
778
- * 「+ 变量」把 {{变量}} 插到正文编辑框的光标处,并把「变量」两字选中——直接打字就是改名。
779
- * @param props.editing - 正在编辑的草稿 {id, title, text}。
780
- * @param props.onTitle - 标题变更。
781
- * @param props.onText - 正文变更。
782
- * @param props.onCancel - 退出编辑。
783
- * @param props.onSave - 保存。
794
+ * 输入框里的 @ 候选状态:标题框、正文框、卡片上的变量框共用同一套。
795
+ * 触发规则与宿主一致:@ 在行首或空白之后、@ 后不含空白,才算触发词。
796
+ * @param listFiles - 查候选:query → 行数组([{ id, label, desc, section, mention }])。
797
+ * @param applyPick - 选中一行后怎么改值:applyPick(row, menu),menu 带 { field, start, end, query }。
798
+ * @returns track(输入变化时喂)、onKeyDown(键盘)、rows / chosen / activeSection / setActiveSection / setChosen(菜单渲染)、close。
784
799
  */
785
- function TemplateEditor(props) {
786
- const editing = props.editing
787
- const onTitle = props.onTitle
788
- const onText = props.onText
789
- const onCancel = props.onCancel
790
- const onSave = props.onSave
791
- const titleRef = useRef(null)
792
- const textareaRef = useRef(null)
800
+ function useMentionField(listFiles, applyPick) {
793
801
  const [menu, setMenu] = useState(null)
794
802
  const [chosen, setChosen] = useState(0)
795
- /** 类别栏的当前选择:null = 全部。 */
796
803
  const [activeSection, setActiveSection] = useState(null)
797
- /** @ 候选:宿主给的工作区文件/文件夹。 */
798
804
  const [hits, setHits] = useState([])
799
805
  const fetchTimer = useRef(0)
800
806
  const fetchSeq = useRef(0)
801
- const prevValue = useRef('')
802
807
 
803
808
  /** 按查询词问一下宿主:工作区里有哪些文件/文件夹。 */
804
809
  const refreshHits = useCallback((query) => {
805
810
  window.clearTimeout(fetchTimer.current)
806
811
  const seq = ++fetchSeq.current
807
812
  fetchTimer.current = window.setTimeout(() => {
808
- const ask = props.listFiles
813
+ const ask = listFiles
809
814
  if (typeof ask !== 'function') {
810
815
  setHits([])
811
816
  return
@@ -817,83 +822,67 @@ function TemplateEditor(props) {
817
822
  if (seq === fetchSeq.current) setHits([])
818
823
  })
819
824
  }, 120)
820
- }, [props.listFiles])
825
+ }, [listFiles])
821
826
 
822
827
  useEffect(() => () => window.clearTimeout(fetchTimer.current), [])
823
828
 
824
- /** 候选行由 lister 直接给(文件与文件夹在前、对话、插件),这里不再加工。 */
825
- const rows = hits
826
- /** 类别栏筛选后的可见行。 */
827
- const visibleRows = activeSection ? rows.filter((row) => row.section === activeSection) : rows
828
-
829
- /** 输入变化后看一眼光标前是不是 @ 触发词;光标位置拿不到就按文本末尾算。 */
830
- const openMention = useCallback((field, value, caret, element) => {
831
- if (!element) {
832
- setMenu(null)
833
- return
834
- }
835
- let pos = typeof caret === 'number' && caret > 0 ? caret : 0
836
- if (pos === 0 && typeof element.selectionStart === 'number' && element.selectionStart > 0) {
837
- pos = element.selectionStart
838
- }
839
- if (pos === 0) pos = String(value || '').length
829
+ /**
830
+ * 喂一次输入变化。
831
+ * @param field - 哪个输入框(同一个 hook 服务多个框时用来隔离)。
832
+ * @param value - 变化后的整段文字。
833
+ * @param caret - 光标位置。
834
+ * @param typedAt - 本次变化是不是刚敲下 @。
835
+ */
836
+ const track = useCallback((field, value, caret, typedAt) => {
837
+ const pos = typeof caret === 'number' && caret > 0 ? caret : String(value || '').length
840
838
  const hit = mentionAt(value, pos)
841
- if (!hit) {
842
- setMenu(null)
839
+ if (typedAt) {
840
+ if (!hit) {
841
+ setMenu(null)
842
+ return
843
+ }
844
+ setMenu({ field, start: hit.start, end: hit.end, query: hit.query })
845
+ setChosen(0)
846
+ setActiveSection(null)
847
+ refreshHits(hit.query)
843
848
  return
844
849
  }
845
- setMenu({ field, start: hit.start, end: hit.end, query: hit.query })
846
- setChosen(0)
847
- setActiveSection(null)
848
- refreshHits(hit.query)
849
- }, [refreshHits])
850
-
851
- /** 菜单已打开时跟随输入更新查询词;不再处于触发态就关掉。 */
852
- const followMention = useCallback((field, value, caret) => {
853
850
  setMenu((prev) => {
854
851
  if (!prev || prev.field !== field) return prev
855
- const pos = typeof caret === 'number' && caret > 0 ? caret : String(value || '').length
856
- const hit = mentionAt(value, pos)
857
852
  if (!hit) return null
858
853
  refreshHits(hit.query)
859
- return { ...prev, start: hit.start, end: hit.end, query: hit.query }
854
+ return { field, start: hit.start, end: hit.end, query: hit.query }
860
855
  })
861
856
  }, [refreshHits])
862
857
 
863
- /** 选中一行:把 @查询段换成 @路径。 */
864
- const pickMention = useCallback((row) => {
858
+ const close = useCallback(() => setMenu(null), [])
859
+
860
+ /** 类别栏筛选后的可见行。 */
861
+ const rows = activeSection ? hits.filter((row) => row.section === activeSection) : hits
862
+
863
+ /** 选中一行:关菜单,把「@查询段」交给调用方替换成 row.mention。 */
864
+ const pick = useCallback((row) => {
865
865
  if (!menu || !row) return
866
- const isTitle = menu.field === 'title'
867
- const current = isTitle ? String(editing.title || '') : String(editing.text || '')
868
- const next = current.slice(0, menu.start) + row.mention + current.slice(menu.end)
869
- if (isTitle) onTitle(next)
870
- else onText(next)
871
- const caret = menu.start + row.mention.length
872
- prevValue.current = next
866
+ const settled = menu
873
867
  setMenu(null)
874
- window.requestAnimationFrame(() => {
875
- const el = isTitle ? titleRef.current : textareaRef.current
876
- if (!el) return
877
- el.focus()
878
- el.setSelectionRange(caret, caret)
879
- })
880
- }, [editing, menu, onText, onTitle])
868
+ applyPick(row, settled)
869
+ }, [applyPick, menu])
881
870
 
882
871
  /** 菜单打开时的键盘:上下选、回车/制表选中、Esc 关。返回 true 表示已处理。 */
883
- const menuKey = useCallback((event) => {
872
+ const onKeyDown = useCallback((event) => {
884
873
  if (!menu) return false
885
874
  if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
886
875
  event.preventDefault()
887
- if (visibleRows.length > 0) {
876
+ if (rows.length > 0) {
888
877
  const step = event.key === 'ArrowDown' ? 1 : -1
889
- setChosen((prev) => (prev + step + visibleRows.length) % visibleRows.length)
878
+ setChosen((prev) => (prev + step + rows.length) % rows.length)
890
879
  }
891
880
  return true
892
881
  }
893
882
  if (event.key === 'Enter' || event.key === 'Tab') {
894
883
  event.preventDefault()
895
- const row = visibleRows[Math.min(chosen, Math.max(0, visibleRows.length - 1))] || visibleRows[0]
896
- if (row) pickMention(row)
884
+ const row = rows[Math.min(chosen, Math.max(0, rows.length - 1))] || rows[0]
885
+ if (row) pick(row)
897
886
  return true
898
887
  }
899
888
  if (event.key === 'Escape') {
@@ -902,7 +891,49 @@ function TemplateEditor(props) {
902
891
  return true
903
892
  }
904
893
  return false
905
- }, [chosen, menu, pickMention, visibleRows])
894
+ }, [chosen, menu, pick, rows])
895
+
896
+ return { menu, rows, chosen, activeSection, setActiveSection, setChosen, track, onKeyDown, close }
897
+ }
898
+
899
+ /**
900
+ * 一张卡片的编辑表单(新建与编辑共用):标题 + 正文 +「+ 变量」。
901
+ * 两个输入框里都能打 @ 选已有常用提示词:标题框插「名」,正文框插「整段正文」。
902
+ * 「+ 变量」把 {{变量}} 插到正文编辑框的光标处,并把「变量」两字选中——直接打字就是改名。
903
+ * @param props.editing - 正在编辑的草稿 {id, title, text}。
904
+ * @param props.onTitle - 标题变更。
905
+ * @param props.onText - 正文变更。
906
+ * @param props.onCancel - 退出编辑。
907
+ * @param props.onSave - 保存。
908
+ */
909
+ function TemplateEditor(props) {
910
+ const editing = props.editing
911
+ const onTitle = props.onTitle
912
+ const onText = props.onText
913
+ const onCancel = props.onCancel
914
+ const onSave = props.onSave
915
+ const titleRef = useRef(null)
916
+ const textareaRef = useRef(null)
917
+ const prevValue = useRef('')
918
+
919
+ /** 选中一行:把「@查询段」换成 row.mention,光标落在插入内容之后。 */
920
+ const applyMention = useCallback((row, menu) => {
921
+ const isTitle = menu.field === 'title'
922
+ const current = isTitle ? String(editing.title || '') : String(editing.text || '')
923
+ const next = current.slice(0, menu.start) + row.mention + current.slice(menu.end)
924
+ if (isTitle) onTitle(next)
925
+ else onText(next)
926
+ const caret = menu.start + row.mention.length
927
+ prevValue.current = next
928
+ window.requestAnimationFrame(() => {
929
+ const el = isTitle ? titleRef.current : textareaRef.current
930
+ if (!el) return
931
+ el.focus()
932
+ el.setSelectionRange(caret, caret)
933
+ })
934
+ }, [editing, onText, onTitle])
935
+
936
+ const mention = useMentionField(props.listFiles, applyMention)
906
937
 
907
938
  const insertVariable = useCallback(() => {
908
939
  const node = textareaRef.current
@@ -929,13 +960,12 @@ function TemplateEditor(props) {
929
960
  onChange={(event) => {
930
961
  const value = event.target.value
931
962
  const caret = event.target.selectionStart || 0
932
- const typedAt = value.length === prevValue.current.length + 1 && value.charAt(caret - 1) === '@'
963
+ const typedAt = typedAtChar(prevValue.current, value, caret)
933
964
  prevValue.current = value
934
965
  onTitle(value)
935
- if (typedAt) openMention('title', value, caret, event.target)
936
- else followMention('title', value, caret)
966
+ mention.track('title', value, caret, typedAt)
937
967
  }}
938
- onKeyDown={(event) => { menuKey(event) }}
968
+ onKeyDown={(event) => { mention.onKeyDown(event) }}
939
969
  />
940
970
  <textarea
941
971
  ref={textareaRef}
@@ -945,14 +975,13 @@ function TemplateEditor(props) {
945
975
  onChange={(event) => {
946
976
  const value = event.target.value
947
977
  const caret = event.target.selectionStart || 0
948
- const typedAt = value.length === prevValue.current.length + 1 && value.charAt(caret - 1) === '@'
978
+ const typedAt = typedAtChar(prevValue.current, value, caret)
949
979
  prevValue.current = value
950
980
  onText(value)
951
- if (typedAt) openMention('text', value, caret, event.target)
952
- else followMention('text', value, caret)
981
+ mention.track('text', value, caret, typedAt)
953
982
  }}
954
983
  onKeyDown={(event) => {
955
- if (menuKey(event)) return
984
+ if (mention.onKeyDown(event)) return
956
985
  if (event.isComposing || event.repeat) return
957
986
  const key = String(event.key || '').toLowerCase()
958
987
  // Alt+V(两键最省力);Ctrl+Alt+V 是备用(输入法拦 Alt 时用)。
@@ -975,14 +1004,14 @@ function TemplateEditor(props) {
975
1004
  <button type="button" className="zps-btn" onClick={onCancel}>取消</button>
976
1005
  <button type="button" className="zps-btn primary" onClick={onSave}>保存</button>
977
1006
  </div>
978
- {menu
1007
+ {mention.menu
979
1008
  ? (
980
1009
  <MentionMenu
981
- rows={visibleRows}
982
- active={activeSection}
983
- onSelect={(key) => { setActiveSection(key); setChosen(0) }}
984
- chosen={chosen}
985
- onPick={pickMention}
1010
+ rows={mention.rows}
1011
+ active={mention.activeSection}
1012
+ onSelect={(key) => { mention.setActiveSection(key); mention.setChosen(0) }}
1013
+ chosen={mention.chosen}
1014
+ onPick={(row) => mention.pick(row)}
986
1015
  />
987
1016
  )
988
1017
  : null}
@@ -990,6 +1019,108 @@ function TemplateEditor(props) {
990
1019
  )
991
1020
  }
992
1021
 
1022
+ /**
1023
+ * 卡片正文里的一个变量输入框:直接填值;打 @ 也能选工作区文件、对话、插件。
1024
+ * 候选浮层 portal 到 document.body——卡片列表是滚动容器,就地渲染会被裁掉。
1025
+ * @param props.value - 当前取值。
1026
+ * @param props.name - 变量名(占位文字)。
1027
+ * @param props.fallback - 默认值。
1028
+ * @param props.onChange - 值变化。
1029
+ * @param props.listFiles - @ 候选来源。
1030
+ */
1031
+ function VarField(props) {
1032
+ const value = String(props.value || '')
1033
+ const onChange = props.onChange
1034
+ const inputRef = useRef(null)
1035
+ const prevValue = useRef(value)
1036
+ /** 候选浮层的位置:相对视口的 fixed 坐标。 */
1037
+ const [anchor, setAnchor] = useState(null)
1038
+
1039
+ /** 选中一行:把「@查询段」换成 row.mention。 */
1040
+ const applyMention = useCallback((row, menu) => {
1041
+ const current = String(props.value || '')
1042
+ const next = current.slice(0, menu.start) + row.mention + current.slice(menu.end)
1043
+ onChange(next)
1044
+ prevValue.current = next
1045
+ const caret = menu.start + row.mention.length
1046
+ window.requestAnimationFrame(() => {
1047
+ const el = inputRef.current
1048
+ if (!el) return
1049
+ el.focus()
1050
+ el.setSelectionRange(caret, caret)
1051
+ })
1052
+ }, [onChange, props.value])
1053
+
1054
+ const mention = useMentionField(props.listFiles, applyMention)
1055
+ const open = mention.menu !== null
1056
+
1057
+ /** 菜单打开时按输入框的位置摆浮层:下方放不下就翻到上方。 */
1058
+ useEffect(() => {
1059
+ if (!open) {
1060
+ setAnchor(null)
1061
+ return undefined
1062
+ }
1063
+ const place = () => {
1064
+ const el = inputRef.current
1065
+ if (!el) return
1066
+ const rect = el.getBoundingClientRect()
1067
+ const width = Math.max(280, Math.min(420, rect.width + 240))
1068
+ const height = 248
1069
+ const below = rect.bottom + 4 + height <= window.innerHeight
1070
+ setAnchor({
1071
+ left: Math.max(8, Math.min(rect.left, window.innerWidth - width - 8)),
1072
+ top: below ? rect.bottom + 4 : Math.max(8, rect.top - 4 - height),
1073
+ width,
1074
+ })
1075
+ }
1076
+ place()
1077
+ window.addEventListener('resize', place)
1078
+ window.addEventListener('scroll', place, true)
1079
+ return () => {
1080
+ window.removeEventListener('resize', place)
1081
+ window.removeEventListener('scroll', place, true)
1082
+ }
1083
+ }, [open])
1084
+
1085
+ return (
1086
+ <div className="zps-varwrap">
1087
+ <input
1088
+ ref={inputRef}
1089
+ className="zps-var"
1090
+ value={value}
1091
+ placeholder={props.name}
1092
+ title={'变量:' + props.name + '(默认值:' + (props.fallback || '空') + ';打 @ 可引用工作区文件)'}
1093
+ onMouseDown={(event) => event.stopPropagation()}
1094
+ onClick={(event) => event.stopPropagation()}
1095
+ onBlur={() => mention.close()}
1096
+ onChange={(event) => {
1097
+ const next = event.target.value
1098
+ const caret = event.target.selectionStart || 0
1099
+ const typedAt = typedAtChar(prevValue.current, next, caret)
1100
+ prevValue.current = next
1101
+ onChange(next)
1102
+ mention.track('var', next, caret, typedAt)
1103
+ }}
1104
+ onKeyDown={(event) => mention.onKeyDown(event)}
1105
+ />
1106
+ {open && anchor
1107
+ ? createPortal(
1108
+ <div className="zps-varmenu" style={{ left: anchor.left, top: anchor.top, width: anchor.width }}>
1109
+ <MentionMenu
1110
+ rows={mention.rows}
1111
+ active={mention.activeSection}
1112
+ onSelect={(key) => { mention.setActiveSection(key); mention.setChosen(0) }}
1113
+ chosen={mention.chosen}
1114
+ onPick={(row) => mention.pick(row)}
1115
+ />
1116
+ </div>,
1117
+ document.body,
1118
+ )
1119
+ : null}
1120
+ </div>
1121
+ )
1122
+ }
1123
+
993
1124
  /**
994
1125
  * 输入栏下方的入口与独立大窗口(窗口内一条提示词一张卡片)。
995
1126
  * @param props - slot 标准 props:useInput(草稿选择器)与 inputActions(输入动作)。
@@ -1397,15 +1528,13 @@ export function PromptShelf(props) {
1397
1528
  part.type === 'text' ? (
1398
1529
  <span key={index}>{part.value}</span>
1399
1530
  ) : (
1400
- <input
1531
+ <VarField
1401
1532
  key={index}
1402
- className="zps-var"
1403
1533
  value={templateValue(varValues[item.id], part.name, part.fallback)}
1404
- placeholder={part.name}
1405
- title={'变量:' + part.name + '(默认值:' + (part.fallback || '空') + ')'}
1406
- onMouseDown={(event) => event.stopPropagation()}
1407
- onClick={(event) => event.stopPropagation()}
1408
- onChange={(event) => setVar(item.id, part.name, event.target.value)}
1534
+ name={part.name}
1535
+ fallback={part.fallback}
1536
+ onChange={(value) => setVar(item.id, part.name, value)}
1537
+ listFiles={props.listFiles}
1409
1538
  />
1410
1539
  )
1411
1540
  ))}