one-design-next 0.0.113 → 0.0.114

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.
@@ -29,7 +29,10 @@ function createInvocationChipData(skill) {
29
29
  kind: 'invocation',
30
30
  state: skill.initialState,
31
31
  refId: skill.id,
32
- payload: {}
32
+ payload: {},
33
+ /* 提示文案随 chip 快照下来(而不是渲染时回查技能目录):
34
+ * 草稿用 defaultValue + defaultChips 还原时,目录可能已经变了。 */
35
+ hint: skill.hint
33
36
  };
34
37
  }
35
38
  var TOOL_BTN_ACTIVE = 'color-mix(in srgb, var(--odn-color-brand-6) 10%, transparent)';
@@ -417,6 +420,10 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
417
420
  * - segments:有序 IR(text / invocation / mention 交替)
418
421
  * - meta:attachments / webSearch 等非正文数据;都为空时不传
419
422
  *
423
+ * 派生 hint:最后一个 chip 是 skill 且其后没有内容时,`rawValueToSegments`
424
+ * 会在末尾追加一段 `hint`(与该 chip 后面渲染的灰字同源、同谓词)。
425
+ * 它不属于用户输入,也不是受控 value 的一部分。
426
+ *
420
427
  * 需要单串文本的业务方在 onSend 内自行调 segmentsToReadableText。
421
428
  * onSend 返回 false 或 Promise<false> 时视为发送失败,不清空;否则正常清空。
422
429
  *
@@ -681,7 +688,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
681
688
 
682
689
  /* editor 在 trigger 激活时把 ↑↓Enter Esc Tab 丢过来。返回 true = editor 别管了。
683
690
  * - ↑↓ 改高亮 index(夹在 [0, max] 内)
684
- * - Enter / Tab 选中当前高亮项
691
+ * - Enter / Tab 选中当前高亮项(Tab 默认落在第一个可用技能上)
685
692
  * - Escape 关 menu,焦点保留在 editor */
686
693
  var handleTriggerKeyDown = useCallback(function (e) {
687
694
  if (!triggerMenuVisible) return false;
@@ -700,7 +707,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
700
707
  (_triggerSkillSlotRef$2 = triggerSkillSlotRef.current) === null || _triggerSkillSlotRef$2 === void 0 || _triggerSkillSlotRef$2.moveActive('up');
701
708
  return true;
702
709
  }
703
- if (e.key === 'Enter' && !e.shiftKey || e.key === 'Tab') {
710
+ if (e.key === 'Enter' && !e.shiftKey) {
704
711
  var _triggerSkillSlotRef$3, _triggerSkillSlotRef$4;
705
712
  e.preventDefault();
706
713
  var _skill = (_triggerSkillSlotRef$3 = (_triggerSkillSlotRef$4 = triggerSkillSlotRef.current) === null || _triggerSkillSlotRef$4 === void 0 ? void 0 : _triggerSkillSlotRef$4.getActiveSkill()) !== null && _triggerSkillSlotRef$3 !== void 0 ? _triggerSkillSlotRef$3 : filteredSkills === null || filteredSkills === void 0 ? void 0 : filteredSkills[triggerActiveIndex];
@@ -708,6 +715,20 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
708
715
  handleSelectSkill(_skill, 'trigger');
709
716
  return true;
710
717
  }
718
+ /* Tab 与 Enter 同义:接受当前高亮项。query 变化时高亮已复位到第一个
719
+ * 可用项,所以「/ + 关键字有命中 → Tab」就是插入第一个匹配技能。
720
+ * 没有可选项时不 preventDefault,把 Tab 让回浏览器(editor 里还可能
721
+ * 落到 hint 采纳)。 */
722
+ if (e.key === 'Tab') {
723
+ var _ref4, _ref5, _triggerSkillSlotRef$5, _triggerSkillSlotRef$6;
724
+ var _skill2 = (_ref4 = (_ref5 = (_triggerSkillSlotRef$5 = (_triggerSkillSlotRef$6 = triggerSkillSlotRef.current) === null || _triggerSkillSlotRef$6 === void 0 ? void 0 : _triggerSkillSlotRef$6.getActiveSkill()) !== null && _triggerSkillSlotRef$5 !== void 0 ? _triggerSkillSlotRef$5 : filteredSkills === null || filteredSkills === void 0 ? void 0 : filteredSkills[triggerActiveIndex]) !== null && _ref5 !== void 0 ? _ref5 : filteredSkills === null || filteredSkills === void 0 ? void 0 : filteredSkills.find(function (s) {
725
+ return !s.disabled;
726
+ })) !== null && _ref4 !== void 0 ? _ref4 : null;
727
+ if (!_skill2) return false;
728
+ e.preventDefault();
729
+ handleSelectSkill(_skill2, 'trigger');
730
+ return true;
731
+ }
711
732
  if (e.key === 'Escape') {
712
733
  var _editorRef$current19;
713
734
  e.preventDefault();
@@ -741,8 +762,8 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
741
762
  useLayoutEffect(function () {
742
763
  if (!toolMenuOpen && !triggerMenuVisible) return undefined;
743
764
  var sync = function sync() {
744
- var _skillTriggerRef$curr, _skillTriggerRef$curr2, _ref4, _triggerInfo$rect$top, _triggerAnchorRef$cur;
745
- var triggerTop = toolMenuOpen ? (_skillTriggerRef$curr = (_skillTriggerRef$curr2 = skillTriggerRef.current) === null || _skillTriggerRef$curr2 === void 0 ? void 0 : _skillTriggerRef$curr2.getBoundingClientRect().top) !== null && _skillTriggerRef$curr !== void 0 ? _skillTriggerRef$curr : null : (_ref4 = (_triggerInfo$rect$top = triggerInfo === null || triggerInfo === void 0 ? void 0 : triggerInfo.rect.top) !== null && _triggerInfo$rect$top !== void 0 ? _triggerInfo$rect$top : (_triggerAnchorRef$cur = triggerAnchorRef.current) === null || _triggerAnchorRef$cur === void 0 ? void 0 : _triggerAnchorRef$cur.getBoundingClientRect().top) !== null && _ref4 !== void 0 ? _ref4 : null;
765
+ var _skillTriggerRef$curr, _skillTriggerRef$curr2, _ref6, _triggerInfo$rect$top, _triggerAnchorRef$cur;
766
+ var triggerTop = toolMenuOpen ? (_skillTriggerRef$curr = (_skillTriggerRef$curr2 = skillTriggerRef.current) === null || _skillTriggerRef$curr2 === void 0 ? void 0 : _skillTriggerRef$curr2.getBoundingClientRect().top) !== null && _skillTriggerRef$curr !== void 0 ? _skillTriggerRef$curr : null : (_ref6 = (_triggerInfo$rect$top = triggerInfo === null || triggerInfo === void 0 ? void 0 : triggerInfo.rect.top) !== null && _triggerInfo$rect$top !== void 0 ? _triggerInfo$rect$top : (_triggerAnchorRef$cur = triggerAnchorRef.current) === null || _triggerAnchorRef$cur === void 0 ? void 0 : _triggerAnchorRef$cur.getBoundingClientRect().top) !== null && _ref6 !== void 0 ? _ref6 : null;
746
767
  if (triggerTop === null) return;
747
768
  var next = resolveComposerSkillMenuMaxHeight({
748
769
  triggerTop: triggerTop,
@@ -848,7 +869,10 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
848
869
  activeColor: TOOL_BTN_ACTIVE,
849
870
  disabled: btnDisabled
850
871
  }, /*#__PURE__*/React.createElement("button", {
851
- type: "button",
872
+ type: "button"
873
+ /* 不进入 Tab 焦点序列:composer 内的 Tab 只用于 hint 采纳,
874
+ * 不允许它逐个跳到工具栏按钮上(鼠标点击仍可正常使用)。 */,
875
+ tabIndex: -1,
852
876
  onClick: function onClick() {
853
877
  var _fileInputRef$current;
854
878
  onToolClick === null || onToolClick === void 0 || onToolClick('attachment');
@@ -888,6 +912,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
888
912
  disabled: disabled || webSearch
889
913
  }, /*#__PURE__*/React.createElement("button", {
890
914
  type: "button",
915
+ tabIndex: -1,
891
916
  onClick: function onClick() {
892
917
  onToolClick === null || onToolClick === void 0 || onToolClick('webSearch');
893
918
  setWebSearch(function (v) {
@@ -965,6 +990,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
965
990
  disabled: disabled || toolMenuOpen
966
991
  }, /*#__PURE__*/React.createElement("button", {
967
992
  type: "button",
993
+ tabIndex: -1,
968
994
  disabled: disabled,
969
995
  onMouseDown: function onMouseDown(e) {
970
996
  return e.preventDefault();
@@ -1038,6 +1064,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
1038
1064
  * 因为整体禁用语义包含"现在什么都不能做",包括中断。 */
1039
1065
  var sendOrStopButton = !disabled && isGenerating ? /*#__PURE__*/React.createElement("button", {
1040
1066
  type: "button",
1067
+ tabIndex: -1,
1041
1068
  onClick: onStop,
1042
1069
  "data-odn-composer-stop-btn": true,
1043
1070
  title: "\u505C\u6B62\u751F\u6210"
@@ -1046,6 +1073,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
1046
1073
  size: 18
1047
1074
  })) : /*#__PURE__*/React.createElement("button", {
1048
1075
  type: "button",
1076
+ tabIndex: -1,
1049
1077
  onClick: handleSend,
1050
1078
  disabled: disabled || !canSend,
1051
1079
  "data-odn-composer-send-btn": true,
@@ -1222,6 +1250,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
1222
1250
  placement: "top"
1223
1251
  }, /*#__PURE__*/React.createElement("button", {
1224
1252
  type: "button",
1253
+ tabIndex: -1,
1225
1254
  "data-odn-composer-expand-btn": true,
1226
1255
  "aria-label": fullExpandLabel,
1227
1256
  "aria-expanded": fullExpanded,
@@ -10,6 +10,11 @@ export interface ComposerInlineRefProps {
10
10
  /** 有参数面板时由 Composer 注入;点击打开面板 */
11
11
  onClick?: (anchor: HTMLElement) => void;
12
12
  }
13
- /** Composer 内联引用视觉:按 kind 派发 Invocation(/)或 Mention(@)。 */
13
+ /**
14
+ * Composer 内联引用视觉:按 kind 派发 Invocation(/)或 Mention(@)。
15
+ *
16
+ * 注:派生 hint 不在 host 内渲染——它是 host 的兄弟节点(见 `editor.tsx` 的
17
+ * `syncHintNode`),否则「chip 之后、hint 之前」这个 caret 位置会不存在。
18
+ */
14
19
  export declare function ComposerInlineRef({ data, active, onClick, }: ComposerInlineRefProps): React.JSX.Element;
15
20
  export default ComposerInlineRef;
@@ -16,7 +16,12 @@ export function chipHasEditableParams(chip) {
16
16
  return v !== null && v !== undefined && v !== '';
17
17
  });
18
18
  }
19
- /** Composer 内联引用视觉:按 kind 派发 Invocation(/)或 Mention(@)。 */
19
+ /**
20
+ * Composer 内联引用视觉:按 kind 派发 Invocation(/)或 Mention(@)。
21
+ *
22
+ * 注:派生 hint 不在 host 内渲染——它是 host 的兄弟节点(见 `editor.tsx` 的
23
+ * `syncHintNode`),否则「chip 之后、hint 之前」这个 caret 位置会不存在。
24
+ */
20
25
  export function ComposerInlineRef(_ref) {
21
26
  var data = _ref.data,
22
27
  _ref$active = _ref.active,
@@ -5,18 +5,44 @@
5
5
  *
6
6
  * - `rawValueToSegments`:库内部用,把含零宽 marker 的 raw value + chip 列表
7
7
  * 还原为 `ComposerSegment[]`。
8
+ * - `resolveHintTarget`:结构谓词——「最后一个 chip 之后一个字符都没有
9
+ * (空格 / 换行都算内容)+ 该 chip 带 hint」,渲染与发送都靠它。
8
10
  * - `segmentsToReadableText`:业务可选工具,按指定占位符格式把 segments 拼成
9
11
  * 一句话(默认 `[Skill: label]` / `[@label]`,与 ComposerMention 兼容)。
10
12
  * - `segmentsHasContent`:是否含可见内容;库内 send 校验用,业务可复用。
11
13
  */
12
- import type { ChipData, ComposerInvocationSegment, ComposerMentionSegment, ComposerSegment, ComposerTextSegment, TextData } from '../_genui-types';
14
+ import type { ChipData, ComposerHintSegment, ComposerInvocationSegment, ComposerMentionSegment, ComposerSegment, ComposerTextSegment, HintData, TextData } from '../_genui-types';
13
15
  import { ComposerSegmentType } from '../_genui-types';
14
- export type { ComposerInvocationSegment, ComposerMentionSegment, ComposerSegment, ComposerTextSegment, TextData, };
15
16
  export { ComposerSegmentType };
17
+ export type { ComposerHintSegment, ComposerInvocationSegment, ComposerMentionSegment, ComposerSegment, ComposerTextSegment, HintData, TextData, };
16
18
  export declare const isTextSegment: (s: ComposerSegment) => s is ComposerTextSegment;
17
19
  export declare const isInvocationSegment: (s: ComposerSegment) => s is ComposerInvocationSegment;
18
20
  export declare const isMentionSegment: (s: ComposerSegment) => s is ComposerMentionSegment;
19
- /** 从含零宽 marker 的 raw value + chip 列表构建有序 segments。 */
21
+ export declare const isHintSegment: (s: ComposerSegment) => s is ComposerHintSegment;
22
+ /** hint 命中结果。 */
23
+ export interface ComposerHintTarget {
24
+ /** 命中 hint 的 chip id(渲染时用它定位到具体 chip) */
25
+ chipId: string;
26
+ /** 提示文本(skill 模板文案,发送时原样作为 hint 段的 data.text) */
27
+ text: string;
28
+ }
29
+ /**
30
+ * 判定当前内容是否命中 hint:
31
+ * 1. `value` 里存在 marker(即至少有一个 chip)
32
+ * 2. 最后一个 marker 之后**一个字符都没有**(空格 / 换行都算内容)
33
+ * 3. 该 marker 对应的 chip 带 hint 文本(`@` 触发的 mention chip 不参与)
34
+ */
35
+ export declare function resolveHintTarget(raw: string, chips: ChipData[]): ComposerHintTarget | null;
36
+ /**
37
+ * 从含零宽 marker 的 raw value + chip 列表构建有序 segments。
38
+ *
39
+ * 这是「编辑器状态 → 发送内容」的**唯一映射**:
40
+ * 1. marker 依次还原为 invocation / mention,与文本段按出现顺序交替
41
+ * 2. 末尾按结构谓词补一段派生的 `hint`(skill 在最后且其后一个字符都没有时)
42
+ *
43
+ * 因此 `rawValueToSegments(value, chips)` 与 `onSend` 收到的 segments 一致,
44
+ * 业务可以放心用它做预览 / 调试。
45
+ */
20
46
  export declare function rawValueToSegments(raw: string, chips: ChipData[]): ComposerSegment[];
21
47
  export interface SegmentsToReadableTextOptions {
22
48
  /** invocation 占位格式,默认 `[Skill: {label}]` */
@@ -27,7 +53,9 @@ export interface SegmentsToReadableTextOptions {
27
53
  /**
28
54
  * 把 segments 拼成一条字符串,供需要单串 string 的 LLM / 旧后端使用。
29
55
  * 库**不内置调用**此函数;由业务在 onSend 内自行选择。
56
+ *
57
+ * `hint` 段直接输出原文(它本身就是给模型的模板文案,不加 `[Skill: …]` 包裹)。
30
58
  */
31
59
  export declare function segmentsToReadableText(segments: ComposerSegment[], options?: SegmentsToReadableTextOptions): string;
32
- /** segments 是否含可见内容(文本非空或含内联引用)。 */
60
+ /** segments 是否含可见内容(文本非空、含内联引用,或含派生 hint)。 */
33
61
  export declare function segmentsHasContent(segments: ComposerSegment[]): boolean;
@@ -5,6 +5,8 @@
5
5
  *
6
6
  * - `rawValueToSegments`:库内部用,把含零宽 marker 的 raw value + chip 列表
7
7
  * 还原为 `ComposerSegment[]`。
8
+ * - `resolveHintTarget`:结构谓词——「最后一个 chip 之后一个字符都没有
9
+ * (空格 / 换行都算内容)+ 该 chip 带 hint」,渲染与发送都靠它。
8
10
  * - `segmentsToReadableText`:业务可选工具,按指定占位符格式把 segments 拼成
9
11
  * 一句话(默认 `[Skill: label]` / `[@label]`,与 ComposerMention 兼容)。
10
12
  * - `segmentsHasContent`:是否含可见内容;库内 send 校验用,业务可复用。
@@ -12,9 +14,8 @@
12
14
 
13
15
  import { ComposerSegmentType } from "../_genui-types";
14
16
  import { chipToInvocationData, chipToMentionData } from "./send-meta";
15
- import { forEachMarkerSegment } from "./utils";
17
+ import { forEachMarkerSegment, parseMarkers } from "./utils";
16
18
  export { ComposerSegmentType };
17
-
18
19
  /* ────────────────────────────────────────────────────────────────────
19
20
  * Type guards · 给业务方 filter / find 用,避免现场写 Extract<…>。
20
21
  *
@@ -30,8 +31,75 @@ export var isInvocationSegment = function isInvocationSegment(s) {
30
31
  export var isMentionSegment = function isMentionSegment(s) {
31
32
  return s.type === ComposerSegmentType.Mention;
32
33
  };
34
+ export var isHintSegment = function isHintSegment(s) {
35
+ return s.type === ComposerSegmentType.Hint;
36
+ };
37
+
38
+ /* ────────────────────────────────────────────────────────────────────
39
+ * Hint 结构谓词 · 「最后一个 chip + 其后一个字符都没有」
40
+ *
41
+ * hint 不是内容,而是内容结构的投影:只要最后一个 chip 之后的 value 是
42
+ * **空串**,且该 chip 带 hint,就命中。
43
+ *
44
+ * 空格、换行、文字**都算内容**:用户在 chip 后面敲下第一个字符 / 按下
45
+ * Enter(含 Shift+Enter)都说明 skill 块之后已经有东西了,hint 必须让位。
46
+ * 反过来,把这些字符(包括换行)删干净、chip 后面重新变成空串,hint 才回来。
47
+ * 插入 chip 本身不会写空格(见 useChipManager.insertChip):行尾插入后尾串
48
+ * 就是空串,hint 立刻命中,不需要为「自动补的空格」做特殊判断。
49
+ *
50
+ * 谓词与光标位置无关——用户在前面打字不影响。**渲染(Editor)与发送
51
+ * (Composer)都只依赖这一个谓词**,每次内容变化重算一次,两处永远一致。
52
+ * ──────────────────────────────────────────────────────────────────── */
53
+
54
+ /** hint 命中结果。 */
55
+
56
+ /**
57
+ * 最后一个 marker 的 chip id 与它之后的尾串;没有 chip 时返回 null。
58
+ *
59
+ * 「尾串」是 hint 判定的唯一输入:谓词要求它是**空串**。
60
+ */
61
+ function getLastMarkerTail(raw) {
62
+ var markers = parseMarkers(raw);
63
+ var last = markers[markers.length - 1];
64
+ if (!last) return null;
65
+ return {
66
+ chipId: last.id,
67
+ tail: raw.slice(last.rawEnd)
68
+ };
69
+ }
33
70
 
34
- /** 从含零宽 marker 的 raw value + chip 列表构建有序 segments。 */
71
+ /**
72
+ * 判定当前内容是否命中 hint:
73
+ * 1. `value` 里存在 marker(即至少有一个 chip)
74
+ * 2. 最后一个 marker 之后**一个字符都没有**(空格 / 换行都算内容)
75
+ * 3. 该 marker 对应的 chip 带 hint 文本(`@` 触发的 mention chip 不参与)
76
+ */
77
+ export function resolveHintTarget(raw, chips) {
78
+ var _chip$hint;
79
+ var info = getLastMarkerTail(raw);
80
+ if (!info || info.tail.length > 0) return null;
81
+ var chip = chips.find(function (c) {
82
+ return c.id === info.chipId;
83
+ });
84
+ if (!chip || chip.kind === 'mention') return null;
85
+ var text = (_chip$hint = chip.hint) === null || _chip$hint === void 0 ? void 0 : _chip$hint.trim();
86
+ if (!text) return null;
87
+ return {
88
+ chipId: chip.id,
89
+ text: text
90
+ };
91
+ }
92
+
93
+ /**
94
+ * 从含零宽 marker 的 raw value + chip 列表构建有序 segments。
95
+ *
96
+ * 这是「编辑器状态 → 发送内容」的**唯一映射**:
97
+ * 1. marker 依次还原为 invocation / mention,与文本段按出现顺序交替
98
+ * 2. 末尾按结构谓词补一段派生的 `hint`(skill 在最后且其后一个字符都没有时)
99
+ *
100
+ * 因此 `rawValueToSegments(value, chips)` 与 `onSend` 收到的 segments 一致,
101
+ * 业务可以放心用它做预览 / 调试。
102
+ */
35
103
  export function rawValueToSegments(raw, chips) {
36
104
  var chipById = new Map(chips.map(function (c) {
37
105
  return [c.id, c];
@@ -63,6 +131,15 @@ export function rawValueToSegments(raw, chips) {
63
131
  });
64
132
  }
65
133
  });
134
+ var hintTarget = resolveHintTarget(raw, chips);
135
+ if (hintTarget) {
136
+ segments.push({
137
+ type: 'hint',
138
+ data: {
139
+ text: hintTarget.text
140
+ }
141
+ });
142
+ }
66
143
  return segments;
67
144
  }
68
145
  var defaultFormatInvocation = function defaultFormatInvocation(label) {
@@ -75,6 +152,8 @@ var defaultFormatMention = function defaultFormatMention(label) {
75
152
  /**
76
153
  * 把 segments 拼成一条字符串,供需要单串 string 的 LLM / 旧后端使用。
77
154
  * 库**不内置调用**此函数;由业务在 onSend 内自行选择。
155
+ *
156
+ * `hint` 段直接输出原文(它本身就是给模型的模板文案,不加 `[Skill: …]` 包裹)。
78
157
  */
79
158
  export function segmentsToReadableText(segments, options) {
80
159
  var _options$formatInvoca, _options$formatMentio;
@@ -88,15 +167,17 @@ export function segmentsToReadableText(segments, options) {
88
167
  return fmtInv(seg.data.label);
89
168
  case 'mention':
90
169
  return fmtMen(seg.data.label);
170
+ case 'hint':
171
+ return seg.data.text;
91
172
  default:
92
173
  return '';
93
174
  }
94
175
  }).join('');
95
176
  }
96
177
 
97
- /** segments 是否含可见内容(文本非空或含内联引用)。 */
178
+ /** segments 是否含可见内容(文本非空、含内联引用,或含派生 hint)。 */
98
179
  export function segmentsHasContent(segments) {
99
180
  return segments.some(function (s) {
100
- return s.type === 'invocation' || s.type === 'mention' || s.type === 'text' && s.data.text.trim().length > 0;
181
+ return s.type === 'invocation' || s.type === 'mention' || s.type === 'hint' || s.type === 'text' && s.data.text.trim().length > 0;
101
182
  });
102
183
  }
@@ -169,7 +169,12 @@
169
169
  width: 100%;
170
170
  }
171
171
 
172
+ /* 升格态:box 必须回到正常流。
173
+ * 单行态是 absolute + bottom: 0(贴 inner 底部,inner 只留 52px)。内容一旦
174
+ * 变高(多行文本 / 派生 hint),绝对定位的 box 会向上溢出 inner 的占位、盖住
175
+ * 上方所有内容(demo 里表现为输入框飘到外面的文案上)。 */
172
176
  [data-odn-composer][data-odn-composer-variant=lite][data-odn-composer-expanded] [data-odn-composer-box] {
177
+ position: relative;
173
178
  padding-bottom: 42px;
174
179
  }
175
180
 
@@ -287,6 +292,23 @@
287
292
  line-height: inherit;
288
293
  }
289
294
 
295
+ /* ------------------------------------------------------------------
296
+ * 派生 hint(结构谓词:chip 是最后一个内容且其后无可见内容)
297
+ * 它是 chip host 的**兄弟节点**(不是 host 内部):
298
+ * - 只有兄弟关系才存在「chip 之后、hint 之前」这个 caret 落点;
299
+ * host 是 contentEditable=false 原子块,塞进 host 就只能落在整块之后
300
+ * - hint 之后不允许落 caret(caret.ts · normalizeCollapsedCaretBeforeHint)
301
+ * - 非内容:getPlainText 整棵子树跳过,不进 value / 剪贴板 / segments
302
+ * - pre-wrap:skills 的模板文案可含换行
303
+ * ----------------------------------------------------------------- */
304
+ [data-odn-composer] [data-odn-composer-skill-hint] {
305
+ margin-left: 4px;
306
+ color: var(--odn-color-black-8);
307
+ pointer-events: none;
308
+ user-select: none;
309
+ white-space: pre-wrap;
310
+ }
311
+
290
312
  /* ------------------------------------------------------------------
291
313
  * Block 行模型(caret-rewrite step 1/5)
292
314
  * editor 直接子节点都是 [data-composer-line] div,每行渲染为独立 block。
@@ -13,6 +13,15 @@
13
13
  export declare const LINE_DATASET_KEY = "composerLine";
14
14
  /** 行容器 DOM 属性名。 */
15
15
  export declare const LINE_ATTR_NAME = "data-composer-line";
16
+ /**
17
+ * 派生 hint 的 DOM 钩子(skill 在最后且其后无内容时,渲染在 chip 之后的只读文本)。
18
+ *
19
+ * **不是正文**:它是 chip host 的兄弟节点,`getPlainText` 整棵子树跳过,
20
+ * 因此不进 value / 剪贴板;caret 也不允许停在它之后(见 `caret.ts`)。
21
+ * 注意别和底部快捷键提示的 `data-odn-composer-hint` 混用。
22
+ */
23
+ export declare const COMPOSER_HINT_ATTR = "data-odn-composer-skill-hint";
24
+ export declare const COMPOSER_HINT_SELECTOR = "[data-odn-composer-skill-hint]";
16
25
  /** 是否是行容器。 */
17
26
  export declare function isLineRoot(node: Node | null | undefined): node is HTMLDivElement;
18
27
  /** 创建一个新的空行容器。 */
@@ -70,6 +79,16 @@ export declare function forEachMarkerSegment(raw: string, visit: (segment: {
70
79
  type: 'marker';
71
80
  id: string;
72
81
  }) => void): void;
82
+ /**
83
+ * 行容器内的 `<br>` 是否只是浏览器给「空行」补的占位。
84
+ *
85
+ * 行模型下换行由 line div 表达,行内的 `<br>` 不承载用户换行;Chrome 在
86
+ * 用户删空一行后会把该行变成 `<div><br></div>`(占位,撑住行高)。若把它
87
+ * 当成 '\n',`value` 就会凭空多出一行(`marker + '\n'` 变成 `marker + '\n\n'`)。
88
+ *
89
+ * 判定:该 `<br>` 之后只剩不可见文本(ZWSP 等)或更多 `<br>`。
90
+ */
91
+ export declare const isPlaceholderBr: (node: Node | null | undefined) => boolean;
73
92
  /**
74
93
  * 从 contenteditable 里把所有 chip 替换成 marker,得到上层 onChange / onSend
75
94
  * 关心的 raw string。
@@ -77,7 +96,8 @@ export declare function forEachMarkerSegment(raw: string, visit: (segment: {
77
96
  * 原则:
78
97
  * - chip host 元素 → 替换为 `createMarker(id)`
79
98
  * - 编辑器自塞的 ZWSP 锚点 → 全部剔除(不进入 value)
80
- * - `<br>` → `\n`
99
+ * - 派生 hint 子树 → 整棵跳过(它不是正文)
100
+ * - `<br>` → `\n`;但行容器内的「占位 `<br>`」(空行撑高用)不算换行
81
101
  * - 行容器 `<div data-composer-line>` → 行间输出 `\n`(首行除外)
82
102
  *
83
103
  * @param editor 编辑区根节点(contentEditable=true)
@@ -29,6 +29,16 @@ export var LINE_DATASET_KEY = 'composerLine';
29
29
  /** 行容器 DOM 属性名。 */
30
30
  export var LINE_ATTR_NAME = 'data-composer-line';
31
31
 
32
+ /**
33
+ * 派生 hint 的 DOM 钩子(skill 在最后且其后无内容时,渲染在 chip 之后的只读文本)。
34
+ *
35
+ * **不是正文**:它是 chip host 的兄弟节点,`getPlainText` 整棵子树跳过,
36
+ * 因此不进 value / 剪贴板;caret 也不允许停在它之后(见 `caret.ts`)。
37
+ * 注意别和底部快捷键提示的 `data-odn-composer-hint` 混用。
38
+ */
39
+ export var COMPOSER_HINT_ATTR = 'data-odn-composer-skill-hint';
40
+ export var COMPOSER_HINT_SELECTOR = "[".concat(COMPOSER_HINT_ATTR, "]");
41
+
32
42
  /** 是否是行容器。 */
33
43
  export function isLineRoot(node) {
34
44
  var _hasAttribute, _ref;
@@ -233,6 +243,35 @@ export function forEachMarkerSegment(raw, visit) {
233
243
  * getPlainText · 从 DOM 提取 marker 化的字符串
234
244
  * ──────────────────────────────────────────────────────────────────── */
235
245
 
246
+ /**
247
+ * 行容器内的 `<br>` 是否只是浏览器给「空行」补的占位。
248
+ *
249
+ * 行模型下换行由 line div 表达,行内的 `<br>` 不承载用户换行;Chrome 在
250
+ * 用户删空一行后会把该行变成 `<div><br></div>`(占位,撑住行高)。若把它
251
+ * 当成 '\n',`value` 就会凭空多出一行(`marker + '\n'` 变成 `marker + '\n\n'`)。
252
+ *
253
+ * 判定:该 `<br>` 之后只剩不可见文本(ZWSP 等)或更多 `<br>`。
254
+ */
255
+ export var isPlaceholderBr = function isPlaceholderBr(node) {
256
+ if (!node || node.nodeType !== Node.ELEMENT_NODE) return false;
257
+ if (node.tagName !== 'BR') return false;
258
+ var cur = node.nextSibling;
259
+ while (cur) {
260
+ if (cur.nodeType === Node.TEXT_NODE) {
261
+ var _cur$textContent;
262
+ if (stripInvisibleChars((_cur$textContent = cur.textContent) !== null && _cur$textContent !== void 0 ? _cur$textContent : '') !== '') return false;
263
+ cur = cur.nextSibling;
264
+ continue;
265
+ }
266
+ if (cur.nodeType === Node.ELEMENT_NODE && cur.tagName === 'BR') {
267
+ cur = cur.nextSibling;
268
+ continue;
269
+ }
270
+ return false;
271
+ }
272
+ return true;
273
+ };
274
+
236
275
  /**
237
276
  * 从 contenteditable 里把所有 chip 替换成 marker,得到上层 onChange / onSend
238
277
  * 关心的 raw string。
@@ -240,7 +279,8 @@ export function forEachMarkerSegment(raw, visit) {
240
279
  * 原则:
241
280
  * - chip host 元素 → 替换为 `createMarker(id)`
242
281
  * - 编辑器自塞的 ZWSP 锚点 → 全部剔除(不进入 value)
243
- * - `<br>` → `\n`
282
+ * - 派生 hint 子树 → 整棵跳过(它不是正文)
283
+ * - `<br>` → `\n`;但行容器内的「占位 `<br>`」(空行撑高用)不算换行
244
284
  * - 行容器 `<div data-composer-line>` → 行间输出 `\n`(首行除外)
245
285
  *
246
286
  * @param editor 编辑区根节点(contentEditable=true)
@@ -252,6 +292,8 @@ export function getPlainText(editor) {
252
292
  var out = '';
253
293
  var visit = function visit(node) {
254
294
  if (node instanceof HTMLElement) {
295
+ // 派生 hint 整棵子树都不是正文,直接跳过(含其内部文本节点)
296
+ if (node.hasAttribute(COMPOSER_HINT_ATTR)) return;
255
297
  var id = node.dataset[chipIdAttr];
256
298
  if (id) {
257
299
  out += createMarker(id);
@@ -286,6 +328,8 @@ export function getPlainText(editor) {
286
328
  firstLineSeen = true;
287
329
  for (var _i3 = 0, _Array$from3 = Array.from(c.childNodes); _i3 < _Array$from3.length; _i3++) {
288
330
  var cc = _Array$from3[_i3];
331
+ /* 行容器内的占位 <br> 不算换行(换行已由行边界表达)。 */
332
+ if (isPlaceholderBr(cc)) continue;
289
333
  visit(cc);
290
334
  }
291
335
  continue;
@@ -412,8 +456,8 @@ var isInvisibleChar = function isInvisibleChar(ch) {
412
456
  * 影响触发判定。 */
413
457
  var prevSignificantSibling = function prevSignificantSibling(node) {
414
458
  var cur = node.previousSibling;
415
- while (cur && cur.nodeType === Node.TEXT_NODE && ((_cur$textContent = cur.textContent) !== null && _cur$textContent !== void 0 ? _cur$textContent : '') === '') {
416
- var _cur$textContent;
459
+ while (cur && cur.nodeType === Node.TEXT_NODE && ((_cur$textContent2 = cur.textContent) !== null && _cur$textContent2 !== void 0 ? _cur$textContent2 : '') === '') {
460
+ var _cur$textContent2;
417
461
  cur = cur.previousSibling;
418
462
  }
419
463
  return cur;