@nextclaw/agent-chat-ui 0.3.1 → 0.3.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/dist/index.js CHANGED
@@ -5,29 +5,38 @@ import { twMerge } from "tailwind-merge";
5
5
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
6
  import * as PopoverPrimitive from "@radix-ui/react-popover";
7
7
  import * as SelectPrimitive from "@radix-ui/react-select";
8
- import { AlertTriangle, ArrowUp, ArrowUpRight, Bot, Brain, BrainCircuit, Check, ChevronDown, ChevronRight, ChevronUp, Code2, Copy, ExternalLink, FileText, Globe, Loader2, Minus, Paperclip, Puzzle, Search, Sparkles, Terminal, User, Wrench } from "lucide-react";
8
+ import { AlertTriangle, ArrowUp, ArrowUpRight, Bot, Brain, BrainCircuit, Check, ChevronDown, ChevronRight, ChevronUp, Code2, Copy, ExternalLink, File, FileArchive, FileAudio2, FileCode2, FileImage, FileJson2, FileSpreadsheet, FileText, FileVideo2, Globe, Loader2, Minus, Paperclip, Puzzle, Search, Sparkles, Terminal, User, Wrench } from "lucide-react";
9
9
  import * as TooltipPrimitive from "@radix-ui/react-tooltip";
10
10
  import { cva } from "class-variance-authority";
11
+ import { ContentEditable } from "@lexical/react/LexicalContentEditable";
12
+ import { LexicalComposer } from "@lexical/react/LexicalComposer";
13
+ import { EditorRefPlugin } from "@lexical/react/LexicalEditorRefPlugin";
14
+ import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
15
+ import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
16
+ import { $applyNodeReplacement, $createLineBreakNode, $createParagraphNode, $createRangeSelection, $createTextNode, $getRoot, $getSelection, $isElementNode, $isRangeSelection, $isTextNode, $setSelection, BLUR_COMMAND, COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_HIGH, DecoratorNode, KEY_DOWN_COMMAND, SELECTION_CHANGE_COMMAND, mergeRegister } from "lexical";
17
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
11
18
  import ReactMarkdown from "react-markdown";
12
19
  import remarkGfm from "remark-gfm";
13
20
  //#region src/components/chat/hooks/use-active-item-scroll.ts
14
21
  const defaultGetItemSelector = (index) => `[data-item-index="${index}"]`;
15
22
  function useActiveItemScroll(params) {
16
- const getItemSelector = params.getItemSelector ?? defaultGetItemSelector;
23
+ const { activeIndex, containerRef, getItemSelector: customGetItemSelector, isEnabled, itemCount } = params;
24
+ const getItemSelector = customGetItemSelector ?? defaultGetItemSelector;
17
25
  useEffect(() => {
18
- if (!params.isEnabled || params.itemCount === 0) return;
19
- const container = params.containerRef.current;
26
+ if (!isEnabled || itemCount === 0) return;
27
+ const container = containerRef.current;
20
28
  if (!container) return;
21
- container.querySelector(getItemSelector(params.activeIndex))?.scrollIntoView({
29
+ const activeItem = container.querySelector(getItemSelector(activeIndex));
30
+ if (typeof activeItem?.scrollIntoView === "function") activeItem.scrollIntoView({
22
31
  block: "nearest",
23
32
  inline: "nearest"
24
33
  });
25
34
  }, [
35
+ activeIndex,
36
+ containerRef,
26
37
  getItemSelector,
27
- params.activeIndex,
28
- params.containerRef,
29
- params.isEnabled,
30
- params.itemCount
38
+ isEnabled,
39
+ itemCount
31
40
  ]);
32
41
  }
33
42
  //#endregion
@@ -841,280 +850,502 @@ function resolveChatComposerSlashTrigger(nodes, selection) {
841
850
  end: caret
842
851
  };
843
852
  }
844
- function isChatComposerSelectionInsideRange(selection, start, end) {
845
- if (!selection) return false;
846
- return selection.start < end && selection.end > start;
847
- }
848
853
  //#endregion
849
- //#region src/components/chat/ui/chat-input-bar/chat-composer-dom.utils.ts
850
- function buildNodeStartMap(nodes) {
851
- const map = /* @__PURE__ */ new Map();
852
- let cursor = 0;
853
- for (const node of nodes) {
854
- map.set(node.id, cursor);
855
- cursor += getChatComposerNodeLength(node);
854
+ //#region src/components/chat/ui/chat-input-bar/lexical/chat-composer-token-node.tsx
855
+ function buildTokenClassName(tokenKind) {
856
+ if (tokenKind === "file") return [
857
+ "mx-[2px]",
858
+ "inline-flex",
859
+ "h-7",
860
+ "max-w-[min(100%,17rem)]",
861
+ "items-center",
862
+ "gap-1.5",
863
+ "rounded-lg",
864
+ "border",
865
+ "border-slate-200/80",
866
+ "bg-slate-50",
867
+ "px-2",
868
+ "align-baseline",
869
+ "text-slate-700",
870
+ "transition-[border-color,background-color,box-shadow,color]",
871
+ "duration-150"
872
+ ].join(" ");
873
+ return [
874
+ "mx-[2px]",
875
+ "inline-flex",
876
+ "h-7",
877
+ "max-w-full",
878
+ "items-center",
879
+ "gap-1.5",
880
+ "rounded-lg",
881
+ "border",
882
+ "border-primary/12",
883
+ "bg-primary/8",
884
+ "px-2",
885
+ "align-baseline",
886
+ "text-[11px]",
887
+ "font-medium",
888
+ "text-primary",
889
+ "transition"
890
+ ].join(" ");
891
+ }
892
+ function ChatComposerTokenChip(props) {
893
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
894
+ className: props.tokenKind === "file" ? "inline-flex h-4.5 w-4.5 shrink-0 items-center justify-center rounded-md bg-white text-slate-500 ring-1 ring-black/5" : "inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center text-primary/70",
895
+ children: props.tokenKind === "file" ? /* @__PURE__ */ jsxs("svg", {
896
+ viewBox: "0 0 16 16",
897
+ fill: "none",
898
+ stroke: "currentColor",
899
+ strokeWidth: "1.25",
900
+ strokeLinecap: "round",
901
+ strokeLinejoin: "round",
902
+ "aria-hidden": "true",
903
+ className: "h-3 w-3",
904
+ children: [
905
+ /* @__PURE__ */ jsx("path", { d: "M3.25 4.25A1.5 1.5 0 0 1 4.75 2.75h6.5a1.5 1.5 0 0 1 1.5 1.5v7.5a1.5 1.5 0 0 1-1.5 1.5h-6.5a1.5 1.5 0 0 1-1.5-1.5v-7.5Z" }),
906
+ /* @__PURE__ */ jsx("path", { d: "m4.75 10 2.25-2.5 1.75 1.75 1.25-1.25 2 2" }),
907
+ /* @__PURE__ */ jsx("path", { d: "M9.75 6.25h.01" })
908
+ ]
909
+ }) : /* @__PURE__ */ jsxs("svg", {
910
+ viewBox: "0 0 16 16",
911
+ fill: "none",
912
+ stroke: "currentColor",
913
+ strokeWidth: "1.25",
914
+ strokeLinecap: "round",
915
+ strokeLinejoin: "round",
916
+ "aria-hidden": "true",
917
+ className: "h-3 w-3",
918
+ children: [
919
+ /* @__PURE__ */ jsx("path", { d: "M8.5 2.75 2.75 6l5.75 3.25L14.25 6 8.5 2.75Z" }),
920
+ /* @__PURE__ */ jsx("path", { d: "M2.75 10 8.5 13.25 14.25 10" }),
921
+ /* @__PURE__ */ jsx("path", { d: "M2.75 6v4l5.75 3.25V9.25L2.75 6Z" }),
922
+ /* @__PURE__ */ jsx("path", { d: "M14.25 6v4L8.5 13.25V9.25L14.25 6Z" })
923
+ ]
924
+ })
925
+ }), /* @__PURE__ */ jsx("span", {
926
+ className: props.tokenKind === "file" ? "min-w-0 flex-1 truncate text-[12px] font-medium text-slate-700" : "truncate",
927
+ children: props.label
928
+ })] });
929
+ }
930
+ var ChatComposerTokenNode = class ChatComposerTokenNode extends DecoratorNode {
931
+ static getType() {
932
+ return "chat-composer-token";
856
933
  }
857
- return map;
858
- }
859
- function resolveRootChildIndex(root, target) {
860
- let current = target;
861
- while (current && current.parentNode !== root) current = current.parentNode;
862
- if (!current) return root.childNodes.length;
863
- return Array.prototype.indexOf.call(root.childNodes, current);
864
- }
865
- function findNodeByDomChild(child, index, nodes) {
866
- if (child instanceof HTMLElement) {
867
- const nodeId = child.dataset.composerNodeId;
868
- if (nodeId) return nodes.find((node) => node.id === nodeId);
934
+ static clone(node) {
935
+ return new ChatComposerTokenNode(node.__composerId, node.__tokenKind, node.__tokenKey, node.__label, node.__key);
869
936
  }
870
- return nodes[index];
871
- }
872
- function sumNodeLengthsBeforeChildIndex(root, nodes, childIndex) {
873
- let value = 0;
874
- for (let index = 0; index < childIndex; index += 1) {
875
- const matched = findNodeByDomChild(root.childNodes[index], index, nodes);
876
- if (matched) value += getChatComposerNodeLength(matched);
937
+ static importJSON(serializedNode) {
938
+ return $createChatComposerTokenNode({
939
+ composerId: serializedNode.composerId,
940
+ label: serializedNode.label,
941
+ tokenKey: serializedNode.tokenKey,
942
+ tokenKind: serializedNode.tokenKind
943
+ });
877
944
  }
878
- return value;
879
- }
880
- function resolveDirectRootTextNodeOffset(root, container, offset, nodes) {
881
- const childIndex = resolveRootChildIndex(root, container);
882
- const valueBeforeNode = sumNodeLengthsBeforeChildIndex(root, nodes, childIndex);
883
- const currentNode = nodes[childIndex];
884
- if (currentNode?.type === "text") return valueBeforeNode + Math.min(offset, currentNode.text.length);
885
- return valueBeforeNode + Math.min(offset, container.textContent?.length ?? 0);
886
- }
887
- function resolveElementBackedOffset(container, offset, matched, nodeStart, element) {
888
- if (matched.type === "text") {
889
- if (container.nodeType === Node.TEXT_NODE) return nodeStart + Math.min(offset, matched.text.length);
890
- if (container === element) return nodeStart + (offset > 0 ? matched.text.length : 0);
891
- return nodeStart + matched.text.length;
945
+ constructor(composerId, tokenKind, tokenKey, label, key) {
946
+ super(key);
947
+ this.applyTokenDom = (element) => {
948
+ element.contentEditable = "false";
949
+ element.dataset.composerNodeId = this.__composerId;
950
+ element.dataset.composerNodeType = "token";
951
+ element.dataset.composerTokenKind = this.__tokenKind;
952
+ element.dataset.composerTokenKey = this.__tokenKey;
953
+ element.dataset.composerLabel = this.__label;
954
+ element.title = this.__label;
955
+ element.className = buildTokenClassName(this.__tokenKind);
956
+ };
957
+ this.createDOM = (_config, _editor) => {
958
+ const element = document.createElement("span");
959
+ this.applyTokenDom(element);
960
+ return element;
961
+ };
962
+ this.updateDOM = (_prevNode, dom) => {
963
+ this.applyTokenDom(dom);
964
+ return false;
965
+ };
966
+ this.decorate = () => {
967
+ return /* @__PURE__ */ jsx(ChatComposerTokenChip, {
968
+ label: this.__label,
969
+ tokenKind: this.__tokenKind
970
+ });
971
+ };
972
+ this.exportJSON = () => {
973
+ return {
974
+ composerId: this.__composerId,
975
+ label: this.__label,
976
+ tokenKey: this.__tokenKey,
977
+ tokenKind: this.__tokenKind,
978
+ type: "chat-composer-token",
979
+ version: 1
980
+ };
981
+ };
982
+ this.getComposerId = () => {
983
+ return this.getLatest().__composerId;
984
+ };
985
+ this.getTokenKind = () => {
986
+ return this.getLatest().__tokenKind;
987
+ };
988
+ this.getTokenKey = () => {
989
+ return this.getLatest().__tokenKey;
990
+ };
991
+ this.getLabel = () => {
992
+ return this.getLatest().__label;
993
+ };
994
+ this.getTextContent = () => {
995
+ return "";
996
+ };
997
+ this.isInline = () => {
998
+ return true;
999
+ };
1000
+ this.isIsolated = () => {
1001
+ return true;
1002
+ };
1003
+ this.isKeyboardSelectable = () => {
1004
+ return false;
1005
+ };
1006
+ this.__composerId = composerId;
1007
+ this.__tokenKind = tokenKind;
1008
+ this.__tokenKey = tokenKey;
1009
+ this.__label = label;
1010
+ }
1011
+ };
1012
+ function $createChatComposerTokenNode(params) {
1013
+ const { composerId, label, tokenKey, tokenKind } = params;
1014
+ return $applyNodeReplacement(new ChatComposerTokenNode(composerId, tokenKind, tokenKey, label));
1015
+ }
1016
+ function $isChatComposerTokenNode(node) {
1017
+ return node instanceof ChatComposerTokenNode;
1018
+ }
1019
+ //#endregion
1020
+ //#region src/components/chat/ui/chat-input-bar/lexical/chat-composer-lexical-editor-state.ts
1021
+ function getComposerLeafDescriptors() {
1022
+ const root = $getRoot();
1023
+ const paragraph = root.getFirstChild();
1024
+ if (!$isElementNode(paragraph)) return {
1025
+ descriptors: [],
1026
+ paragraphKey: root.getKey(),
1027
+ paragraphSize: 0,
1028
+ totalLength: 0
1029
+ };
1030
+ const children = paragraph.getChildren();
1031
+ const descriptors = [];
1032
+ let cursor = 0;
1033
+ for (const [index, child] of children.entries()) {
1034
+ if ($isTextNode(child)) {
1035
+ const text = child.getTextContent();
1036
+ descriptors.push({
1037
+ index,
1038
+ key: child.getKey(),
1039
+ length: text.length,
1040
+ node: child,
1041
+ start: cursor,
1042
+ text,
1043
+ type: "text"
1044
+ });
1045
+ cursor += text.length;
1046
+ continue;
1047
+ }
1048
+ if ($isChatComposerTokenNode(child)) {
1049
+ descriptors.push({
1050
+ index,
1051
+ key: child.getKey(),
1052
+ length: 1,
1053
+ node: child,
1054
+ start: cursor,
1055
+ type: "token"
1056
+ });
1057
+ cursor += 1;
1058
+ continue;
1059
+ }
1060
+ descriptors.push({
1061
+ index,
1062
+ key: child.getKey(),
1063
+ length: child.getTextContent().length,
1064
+ node: child,
1065
+ start: cursor,
1066
+ type: "linebreak"
1067
+ });
1068
+ cursor += child.getTextContent().length;
892
1069
  }
893
- if (container === element) return nodeStart + (offset > 0 ? 1 : 0);
894
- return nodeStart + 1;
895
- }
896
- function resolveSelectionPointOffset(root, container, offset, nodes, nodeStartMap) {
897
- if (container === root) return sumNodeLengthsBeforeChildIndex(root, nodes, offset);
898
- if (container.nodeType === Node.TEXT_NODE && container.parentNode === root) return resolveDirectRootTextNodeOffset(root, container, offset, nodes);
899
- const element = container instanceof HTMLElement ? container.closest("[data-composer-node-id]") : container.parentElement?.closest("[data-composer-node-id]") ?? null;
900
- if (!(element instanceof HTMLElement)) return resolveSelectionPointOffset(root, root, resolveRootChildIndex(root, container), nodes, nodeStartMap);
901
- const nodeId = element.dataset.composerNodeId;
902
- if (!nodeId) return 0;
903
- const matched = nodes.find((node) => node.id === nodeId);
904
- const nodeStart = nodeStartMap.get(nodeId) ?? 0;
905
- if (!matched) return nodeStart;
906
- return resolveElementBackedOffset(container, offset, matched, nodeStart, element);
907
- }
908
- function readComposerSelection(root, nodes) {
909
- const selection = window.getSelection();
910
- if (!selection || selection.rangeCount === 0) return null;
911
- const range = selection.getRangeAt(0);
912
- if (!root.contains(range.startContainer) || !root.contains(range.endContainer)) return null;
913
- const nodeStartMap = buildNodeStartMap(nodes);
914
- const start = resolveSelectionPointOffset(root, range.startContainer, range.startOffset, nodes, nodeStartMap);
915
- const end = resolveSelectionPointOffset(root, range.endContainer, range.endOffset, nodes, nodeStartMap);
916
1070
  return {
917
- start: Math.min(start, end),
918
- end: Math.max(start, end)
1071
+ descriptors,
1072
+ paragraphKey: paragraph.getKey(),
1073
+ paragraphSize: children.length,
1074
+ totalLength: cursor
919
1075
  };
920
1076
  }
921
- function restoreComposerSelection(root, nodes, selection) {
922
- if (!selection) return;
923
- const browserSelection = window.getSelection();
924
- if (!browserSelection) return;
925
- const resolveBoundary = (docOffset) => {
926
- let cursor = 0;
927
- for (let index = 0; index < nodes.length; index += 1) {
928
- const node = nodes[index];
929
- const nodeLength = getChatComposerNodeLength(node);
930
- const nodeStart = cursor;
931
- const nodeEnd = cursor + nodeLength;
932
- const element = root.childNodes[index];
933
- if (!element) {
934
- cursor = nodeEnd;
935
- continue;
936
- }
937
- if (node.type === "text") {
938
- if (docOffset <= nodeEnd) return {
939
- container: element.firstChild ?? element,
940
- offset: Math.max(0, Math.min(docOffset - nodeStart, node.text.length))
941
- };
942
- } else {
943
- if (docOffset <= nodeStart) return {
944
- container: root,
945
- offset: index
1077
+ function buildSelectionPointFromOffset(offset) {
1078
+ const { descriptors, paragraphKey, paragraphSize, totalLength } = getComposerLeafDescriptors();
1079
+ const boundedOffset = Math.max(0, Math.min(offset, totalLength));
1080
+ if (descriptors.length === 0) return {
1081
+ key: paragraphKey,
1082
+ offset: 0,
1083
+ type: "element"
1084
+ };
1085
+ for (const descriptor of descriptors) {
1086
+ const end = descriptor.start + descriptor.length;
1087
+ if (descriptor.type === "text" && boundedOffset >= descriptor.start && boundedOffset <= end) return {
1088
+ key: descriptor.key,
1089
+ offset: boundedOffset - descriptor.start,
1090
+ type: "text"
1091
+ };
1092
+ if (descriptor.type !== "text") {
1093
+ if (boundedOffset === descriptor.start) return {
1094
+ key: paragraphKey,
1095
+ offset: descriptor.index,
1096
+ type: "element"
1097
+ };
1098
+ if (boundedOffset === end) {
1099
+ const nextDescriptor = descriptors[descriptor.index + 1];
1100
+ if (nextDescriptor?.type === "text" && nextDescriptor.length === 0) return {
1101
+ key: nextDescriptor.key,
1102
+ offset: 0,
1103
+ type: "text"
946
1104
  };
947
- if (docOffset <= nodeEnd) return {
948
- container: root,
949
- offset: index + 1
1105
+ return {
1106
+ key: paragraphKey,
1107
+ offset: descriptor.index + 1,
1108
+ type: "element"
950
1109
  };
951
1110
  }
952
- cursor = nodeEnd;
953
1111
  }
954
- return {
955
- container: root,
956
- offset: root.childNodes.length
957
- };
1112
+ }
1113
+ return {
1114
+ key: paragraphKey,
1115
+ offset: paragraphSize,
1116
+ type: "element"
1117
+ };
1118
+ }
1119
+ function getOffsetFromElementPoint(point) {
1120
+ const { descriptors, paragraphKey, totalLength } = getComposerLeafDescriptors();
1121
+ if (point.key === paragraphKey) {
1122
+ const previous = descriptors[point.offset - 1];
1123
+ return previous ? previous.start + previous.length : 0;
1124
+ }
1125
+ const descriptor = descriptors.find((item) => item.key === point.key);
1126
+ if (!descriptor) return totalLength;
1127
+ return descriptor.start + Math.min(point.offset, descriptor.length);
1128
+ }
1129
+ function getOffsetFromPoint(point) {
1130
+ if (point.type === "element") return getOffsetFromElementPoint(point);
1131
+ const { descriptors, totalLength } = getComposerLeafDescriptors();
1132
+ const descriptor = descriptors.find((item) => item.key === point.key);
1133
+ if (!descriptor) return totalLength;
1134
+ return descriptor.start + Math.min(point.offset, descriptor.length);
1135
+ }
1136
+ function readNodesFromEditor() {
1137
+ const { descriptors } = getComposerLeafDescriptors();
1138
+ const nextNodes = [];
1139
+ let textBuffer = "";
1140
+ let textId = "";
1141
+ const flushTextBuffer = () => {
1142
+ if (textBuffer.length === 0) return;
1143
+ nextNodes.push({
1144
+ id: textId,
1145
+ type: "text",
1146
+ text: textBuffer
1147
+ });
1148
+ textBuffer = "";
1149
+ textId = "";
958
1150
  };
959
- const startBoundary = resolveBoundary(selection.start);
960
- const endBoundary = resolveBoundary(selection.end);
961
- const range = document.createRange();
962
- range.setStart(startBoundary.container, startBoundary.offset);
963
- range.setEnd(endBoundary.container, endBoundary.offset);
964
- browserSelection.removeAllRanges();
965
- browserSelection.addRange(range);
966
- }
967
- function parseComposerNodesFromDom(root) {
968
- const parsedNodes = [];
969
- for (const child of Array.from(root.childNodes)) {
970
- if (child.nodeType === Node.TEXT_NODE) {
971
- parsedNodes.push(createChatComposerTextNode(child.textContent ?? ""));
1151
+ for (const descriptor of descriptors) {
1152
+ if (descriptor.type === "text") {
1153
+ if (textBuffer.length === 0) textId = descriptor.key;
1154
+ textBuffer += descriptor.text;
972
1155
  continue;
973
1156
  }
974
- if (!(child instanceof HTMLElement)) continue;
975
- if (child.dataset.composerNodeType === "token") {
976
- const tokenKind = child.dataset.composerTokenKind;
977
- const tokenKey = child.dataset.composerTokenKey;
978
- const label = child.dataset.composerLabel;
979
- if (tokenKind && tokenKey && label) parsedNodes.push({
980
- id: child.dataset.composerNodeId ?? createChatComposerTokenNode({
981
- tokenKind,
982
- tokenKey,
983
- label
984
- }).id,
985
- type: "token",
986
- tokenKind,
987
- tokenKey,
988
- label
989
- });
1157
+ if (descriptor.type === "linebreak") {
1158
+ if (textBuffer.length === 0) textId = descriptor.key;
1159
+ textBuffer += "\n";
990
1160
  continue;
991
1161
  }
992
- const text = child.textContent ?? "";
993
- parsedNodes.push({
994
- id: child.dataset.composerNodeId ?? createChatComposerTextNode(text).id,
995
- type: "text",
996
- text
1162
+ flushTextBuffer();
1163
+ const tokenNode = descriptor.node;
1164
+ nextNodes.push({
1165
+ id: tokenNode.getComposerId(),
1166
+ label: tokenNode.getLabel(),
1167
+ tokenKey: tokenNode.getTokenKey(),
1168
+ tokenKind: tokenNode.getTokenKind(),
1169
+ type: "token"
997
1170
  });
998
1171
  }
999
- return normalizeChatComposerNodes(parsedNodes);
1000
- }
1001
- function readComposerDocumentStateFromDom(root) {
1002
- const nodes = parseComposerNodesFromDom(root);
1003
- return {
1004
- nodes,
1005
- selection: readComposerSelection(root, nodes)
1006
- };
1172
+ flushTextBuffer();
1173
+ return normalizeChatComposerNodes(nextNodes);
1007
1174
  }
1008
- //#endregion
1009
- //#region src/components/chat/ui/chat-input-bar/chat-composer-controller.ts
1010
- var ChatComposerController = class {
1011
- constructor() {
1012
- this.nodes = createEmptyChatComposerNodes();
1013
- this.selection = null;
1014
- this.sync = (nodes, selection) => {
1015
- this.nodes = normalizeChatComposerNodes(nodes);
1016
- this.selection = selection;
1017
- return this.getSnapshot();
1175
+ function readChatComposerSnapshotFromEditorState(editorState) {
1176
+ return editorState.read(() => {
1177
+ const selection = $getSelection();
1178
+ const nodes = readNodesFromEditor();
1179
+ if (!$isRangeSelection(selection)) return {
1180
+ nodes,
1181
+ selection: null
1018
1182
  };
1019
- this.setSelection = (selection) => {
1020
- this.selection = selection;
1021
- return this.getSnapshot();
1022
- };
1023
- this.replaceDocument = (nodes, selection) => {
1024
- this.nodes = normalizeChatComposerNodes(nodes);
1025
- this.selection = selection;
1026
- return this.getSnapshot();
1027
- };
1028
- this.insertText = (text) => {
1029
- const selection = this.selection ?? {
1030
- start: 0,
1031
- end: 0
1032
- };
1033
- this.nodes = replaceChatComposerRange(this.nodes, selection.start, selection.end, [createChatComposerTextNode(text)]);
1034
- const nextOffset = selection.start + text.length;
1035
- this.selection = {
1036
- start: nextOffset,
1037
- end: nextOffset
1038
- };
1039
- return this.getSnapshot();
1040
- };
1041
- this.insertFileToken = (tokenKey, label) => {
1042
- return this.insertToken("file", tokenKey, label);
1043
- };
1044
- this.insertSkillToken = (tokenKey, label) => {
1045
- if (this.getSelectedSkillKeys().includes(tokenKey)) return this.getSnapshot();
1046
- return this.insertToken("skill", tokenKey, label, this.getSlashTrigger());
1047
- };
1048
- this.syncSelectedSkills = (nextKeys, options) => {
1049
- const selectedSkillKeys = this.getSelectedSkillKeys();
1050
- const optionMap = new Map(options.map((option) => [option.key, option]));
1051
- const addedKey = nextKeys.find((key) => !selectedSkillKeys.includes(key));
1052
- if (addedKey) {
1053
- const option = optionMap.get(addedKey);
1054
- return this.insertSkillToken(addedKey, option?.label ?? addedKey);
1183
+ return {
1184
+ nodes,
1185
+ selection: {
1186
+ start: getOffsetFromPoint(selection.anchor),
1187
+ end: getOffsetFromPoint(selection.focus)
1055
1188
  }
1056
- const removedKey = selectedSkillKeys.find((key) => !nextKeys.includes(key));
1057
- if (removedKey) this.nodes = removeChatComposerTokenNodes(this.nodes, (node) => node.tokenKind === "skill" && node.tokenKey === removedKey);
1058
- return this.getSnapshot();
1059
- };
1060
- this.deleteContent = (direction) => {
1061
- const documentLength = this.getDocumentLength();
1062
- const selection = this.selection ?? {
1063
- start: documentLength,
1064
- end: documentLength
1065
- };
1066
- let rangeStart = selection.start;
1067
- let rangeEnd = selection.end;
1068
- if (selection.start === selection.end) if (direction === "backward" && selection.start > 0) rangeStart = selection.start - 1;
1069
- else if (direction === "forward") rangeEnd = selection.end + 1;
1070
- else return this.getSnapshot();
1071
- this.nodes = replaceChatComposerRange(this.nodes, rangeStart, rangeEnd, []);
1072
- this.selection = {
1073
- start: rangeStart,
1074
- end: rangeStart
1075
- };
1076
- return this.getSnapshot();
1077
- };
1078
- this.getSnapshot = () => {
1079
- return {
1080
- nodes: this.nodes,
1081
- selection: this.selection,
1082
- nodeStartMap: buildNodeStartMap(this.nodes),
1083
- documentLength: this.getDocumentLength(),
1084
- selectedSkillKeys: this.getSelectedSkillKeys(),
1085
- slashTrigger: this.getSlashTrigger()
1086
- };
1087
- };
1088
- this.getDocumentLength = () => {
1089
- return this.nodes.reduce((sum, node) => sum + getChatComposerNodeLength(node), 0);
1090
- };
1091
- this.getSelectedSkillKeys = () => {
1092
- return extractChatComposerTokenKeys(this.nodes, "skill");
1093
- };
1094
- this.insertToken = (tokenKind, tokenKey, label, trigger = null) => {
1095
- const documentLength = this.getDocumentLength();
1096
- const replaceStart = trigger?.start ?? this.selection?.start ?? documentLength;
1097
- const replaceEnd = trigger?.end ?? this.selection?.end ?? replaceStart;
1098
- this.nodes = replaceChatComposerRange(this.nodes, replaceStart, replaceEnd, [createChatComposerTokenNode({
1099
- tokenKind,
1100
- tokenKey,
1101
- label
1102
- })]);
1103
- this.selection = {
1104
- start: replaceStart + 1,
1105
- end: replaceStart + 1
1106
- };
1107
- return this.getSnapshot();
1108
- };
1109
- this.getSlashTrigger = () => {
1110
- return resolveChatComposerSlashTrigger(this.nodes, this.selection);
1111
1189
  };
1190
+ });
1191
+ }
1192
+ function writeChatComposerStateToLexicalRoot(nodes, selection) {
1193
+ const root = $getRoot();
1194
+ root.clear();
1195
+ const paragraph = $createParagraphNode();
1196
+ root.append(paragraph);
1197
+ for (const node of normalizeChatComposerNodes(nodes)) {
1198
+ if (node.type === "token") {
1199
+ paragraph.append($createChatComposerTokenNode({
1200
+ composerId: node.id,
1201
+ label: node.label,
1202
+ tokenKey: node.tokenKey,
1203
+ tokenKind: node.tokenKind
1204
+ }));
1205
+ continue;
1206
+ }
1207
+ const parts = node.text.split("\n");
1208
+ for (const [index, part] of parts.entries()) {
1209
+ if (part.length > 0) paragraph.append($createTextNode(part));
1210
+ if (index < parts.length - 1) paragraph.append($createLineBreakNode());
1211
+ }
1112
1212
  }
1113
- };
1213
+ if ($isChatComposerTokenNode(paragraph.getLastChild())) paragraph.append($createTextNode(""));
1214
+ if (!selection) return;
1215
+ const nextSelection = $createRangeSelection();
1216
+ const anchor = buildSelectionPointFromOffset(selection.start);
1217
+ const focus = buildSelectionPointFromOffset(selection.end);
1218
+ nextSelection.anchor.set(anchor.key, anchor.offset, anchor.type);
1219
+ nextSelection.focus.set(focus.key, focus.offset, focus.type);
1220
+ $setSelection(nextSelection);
1221
+ }
1222
+ function syncLexicalEditorFromChatComposerState(editor, nodes, selection) {
1223
+ editor.update(() => {
1224
+ writeChatComposerStateToLexicalRoot(nodes, selection);
1225
+ });
1226
+ }
1227
+ function syncLexicalSelectionFromChatComposerSelection(editor, selection) {
1228
+ editor.update(() => {
1229
+ const nextSelection = $createRangeSelection();
1230
+ const anchor = buildSelectionPointFromOffset(selection.start);
1231
+ const focus = buildSelectionPointFromOffset(selection.end);
1232
+ nextSelection.anchor.set(anchor.key, anchor.offset, anchor.type);
1233
+ nextSelection.focus.set(focus.key, focus.offset, focus.type);
1234
+ $setSelection(nextSelection);
1235
+ });
1236
+ }
1114
1237
  //#endregion
1115
- //#region src/components/chat/ui/chat-input-bar/chat-composer-keyboard.utils.ts
1116
- function resolveChatComposerKeyboardAction(params) {
1117
- const { key, shiftKey, isComposing, isSlashMenuOpen, slashItemCount, activeSlashIndex, isSending, canStopGeneration } = params;
1238
+ //#region src/components/chat/ui/chat-input-bar/lexical/chat-composer-lexical-operations.ts
1239
+ function getDocumentLength(nodes) {
1240
+ return nodes.reduce((sum, node) => sum + getChatComposerNodeLength(node), 0);
1241
+ }
1242
+ function insertToken(params) {
1243
+ const { label, nodes, selection, tokenKey, tokenKind, trigger } = params;
1244
+ const documentLength = getDocumentLength(nodes);
1245
+ const replaceStart = trigger?.start ?? selection?.start ?? documentLength;
1246
+ return {
1247
+ nodes: replaceChatComposerRange(nodes, replaceStart, trigger?.end ?? selection?.end ?? replaceStart, [createChatComposerTokenNode({
1248
+ label,
1249
+ tokenKey,
1250
+ tokenKind
1251
+ })]),
1252
+ selection: {
1253
+ start: replaceStart + 1,
1254
+ end: replaceStart + 1
1255
+ }
1256
+ };
1257
+ }
1258
+ function getChatComposerNodesSignature(nodes) {
1259
+ return nodes.map((node) => node.type === "text" ? `text:${node.id}:${node.text}` : `token:${node.id}:${node.tokenKind}:${node.tokenKey}:${node.label}`).join("");
1260
+ }
1261
+ function replaceChatComposerSelectionWithText(params) {
1262
+ const { nodes, selection, text } = params;
1263
+ const currentSelection = selection ?? {
1264
+ start: getDocumentLength(nodes),
1265
+ end: getDocumentLength(nodes)
1266
+ };
1267
+ const nextOffset = currentSelection.start + text.length;
1268
+ return {
1269
+ nodes: replaceChatComposerRange(nodes, currentSelection.start, currentSelection.end, [createChatComposerTextNode(text)]),
1270
+ selection: {
1271
+ start: nextOffset,
1272
+ end: nextOffset
1273
+ }
1274
+ };
1275
+ }
1276
+ function insertFileTokenIntoChatComposer(params) {
1277
+ const { label, nodes, selection, tokenKey } = params;
1278
+ return insertToken({
1279
+ label,
1280
+ nodes,
1281
+ selection,
1282
+ tokenKey,
1283
+ tokenKind: "file"
1284
+ });
1285
+ }
1286
+ function insertSkillTokenIntoChatComposer(params) {
1287
+ const { label, nodes, selection, tokenKey } = params;
1288
+ if (extractChatComposerTokenKeys(nodes, "skill").includes(tokenKey)) return {
1289
+ nodes: normalizeChatComposerNodes(nodes),
1290
+ selection
1291
+ };
1292
+ return insertToken({
1293
+ label,
1294
+ nodes,
1295
+ selection,
1296
+ tokenKey,
1297
+ tokenKind: "skill",
1298
+ trigger: resolveChatComposerSlashTrigger(nodes, selection)
1299
+ });
1300
+ }
1301
+ function syncSelectedSkillsIntoChatComposer(params) {
1302
+ const { nextKeys, nodes, options, selection } = params;
1303
+ const selectedSkillKeys = extractChatComposerTokenKeys(nodes, "skill");
1304
+ const optionMap = new Map(options.map((option) => [option.key, option]));
1305
+ const addedKey = nextKeys.find((key) => !selectedSkillKeys.includes(key));
1306
+ if (addedKey) return insertSkillTokenIntoChatComposer({
1307
+ label: optionMap.get(addedKey)?.label ?? addedKey,
1308
+ nodes,
1309
+ selection,
1310
+ tokenKey: addedKey
1311
+ });
1312
+ const removedKey = selectedSkillKeys.find((key) => !nextKeys.includes(key));
1313
+ if (!removedKey) return {
1314
+ nodes: normalizeChatComposerNodes(nodes),
1315
+ selection
1316
+ };
1317
+ return {
1318
+ nodes: removeChatComposerTokenNodes(nodes, (node) => node.tokenKind === "skill" && node.tokenKey === removedKey),
1319
+ selection
1320
+ };
1321
+ }
1322
+ function deleteChatComposerContent(params) {
1323
+ const { direction, nodes, selection: currentSelection } = params;
1324
+ const documentLength = getDocumentLength(nodes);
1325
+ const selection = currentSelection ?? {
1326
+ start: documentLength,
1327
+ end: documentLength
1328
+ };
1329
+ let rangeStart = selection.start;
1330
+ let rangeEnd = selection.end;
1331
+ if (selection.start === selection.end) if (direction === "backward" && selection.start > 0) rangeStart = selection.start - 1;
1332
+ else if (direction === "forward" && selection.end < documentLength) rangeEnd = selection.end + 1;
1333
+ else return {
1334
+ nodes: normalizeChatComposerNodes(nodes),
1335
+ selection
1336
+ };
1337
+ return {
1338
+ nodes: replaceChatComposerRange(nodes, rangeStart, rangeEnd, []),
1339
+ selection: {
1340
+ start: rangeStart,
1341
+ end: rangeStart
1342
+ }
1343
+ };
1344
+ }
1345
+ //#endregion
1346
+ //#region src/components/chat/ui/chat-input-bar/lexical/chat-composer-lexical-controller.ts
1347
+ function resolveLexicalComposerKeyboardAction(params) {
1348
+ const { activeSlashIndex, canStopGeneration, isComposing, isSending, isSlashMenuOpen, key, shiftKey, slashItemCount } = params;
1118
1349
  if (key === "Enter" && !shiftKey && isSending) return { type: "consume" };
1119
1350
  if (isSlashMenuOpen && slashItemCount > 0) {
1120
1351
  if (key === "ArrowDown") return {
@@ -1140,540 +1371,391 @@ function resolveChatComposerKeyboardAction(params) {
1140
1371
  };
1141
1372
  return { type: "noop" };
1142
1373
  }
1143
- //#endregion
1144
- //#region src/components/chat/ui/chat-input-bar/chat-composer-surface-renderer.ts
1145
- const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
1146
- var ChatComposerSurfaceRenderer = class {
1147
- constructor() {
1148
- this.render = (root, params) => {
1149
- if (!root) return;
1150
- const fragment = document.createDocumentFragment();
1151
- for (const node of params.nodes) {
1152
- const element = node.type === "text" ? this.createTextNodeElement(node) : this.createTokenNodeElement(node, params.selectedRange, params.nodeStartMap);
1153
- if (element) fragment.appendChild(element);
1154
- }
1155
- root.replaceChildren(fragment);
1156
- };
1157
- this.createTextNodeElement = (node) => {
1158
- if (node.text.length === 0) return null;
1159
- const element = document.createElement("span");
1160
- element.dataset.composerNodeId = node.id;
1161
- element.dataset.composerNodeType = "text";
1162
- element.textContent = node.text;
1163
- return element;
1164
- };
1165
- this.createTokenNodeElement = (node, selectedRange, nodeStartMap) => {
1166
- const nodeStart = nodeStartMap.get(node.id) ?? 0;
1167
- const isSelected = isChatComposerSelectionInsideRange(selectedRange, nodeStart, nodeStart + 1);
1168
- const element = document.createElement("span");
1169
- element.contentEditable = "false";
1170
- element.dataset.composerNodeId = node.id;
1171
- element.dataset.composerNodeType = "token";
1172
- element.dataset.composerTokenKind = node.tokenKind;
1173
- element.dataset.composerTokenKey = node.tokenKey;
1174
- element.dataset.composerLabel = node.label;
1175
- element.title = node.label;
1176
- element.className = this.buildTokenClassName(node.tokenKind, isSelected);
1177
- element.append(this.createTokenIcon(node.tokenKind));
1178
- const label = document.createElement("span");
1179
- label.className = node.tokenKind === "file" ? "min-w-0 flex-1 truncate text-[12px] font-medium text-slate-700" : "truncate";
1180
- label.textContent = node.label;
1181
- element.append(label);
1182
- return element;
1183
- };
1184
- this.buildTokenClassName = (tokenKind, isSelected) => {
1185
- if (tokenKind === "file") return [
1186
- "mx-[2px]",
1187
- "inline-flex",
1188
- "h-7",
1189
- "max-w-[min(100%,17rem)]",
1190
- "items-center",
1191
- "gap-1.5",
1192
- "rounded-lg",
1193
- "border",
1194
- "px-2",
1195
- "align-baseline",
1196
- "transition-[border-color,background-color,box-shadow,color]",
1197
- "duration-150",
1198
- isSelected ? "border-slate-300 bg-slate-100 text-slate-800 shadow-[0_0_0_2px_rgba(148,163,184,0.14)]" : "border-slate-200/80 bg-slate-50 text-slate-700"
1199
- ].join(" ");
1200
- return [
1201
- "mx-[2px]",
1202
- "inline-flex",
1203
- "h-7",
1204
- "max-w-full",
1205
- "items-center",
1206
- "gap-1.5",
1207
- "rounded-lg",
1208
- "border",
1209
- "px-2",
1210
- "align-baseline",
1211
- "text-[11px]",
1212
- "font-medium",
1213
- "transition",
1214
- isSelected ? "border-primary/30 bg-primary/18 text-primary" : "border-primary/12 bg-primary/8 text-primary"
1215
- ].join(" ");
1216
- };
1217
- this.createTokenIcon = (tokenKind) => {
1218
- const wrapper = document.createElement("span");
1219
- wrapper.className = tokenKind === "file" ? "inline-flex h-4.5 w-4.5 shrink-0 items-center justify-center rounded-md bg-white text-slate-500 ring-1 ring-black/5" : "inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center text-primary/70";
1220
- wrapper.append(tokenKind === "file" ? this.createFileIcon() : this.createSkillIcon());
1221
- return wrapper;
1374
+ var LexicalComposerHandleOwner = class {
1375
+ constructor(params) {
1376
+ this.params = params;
1377
+ this.insertSlashItem = (item) => {
1378
+ if (!item.value) return;
1379
+ this.params.onSlashItemSelect?.(item);
1380
+ this.params.publishSnapshot(insertSkillTokenIntoChatComposer({
1381
+ label: item.title,
1382
+ nodes: this.params.optionsReader().nodes,
1383
+ selection: this.params.optionsReader().selection,
1384
+ tokenKey: item.value
1385
+ }), { focusAfterSync: true });
1222
1386
  };
1223
- this.createSkillIcon = () => {
1224
- return this.createSvgIcon([
1225
- {
1226
- tag: "path",
1227
- attrs: { d: "M8.5 2.75 2.75 6l5.75 3.25L14.25 6 8.5 2.75Z" }
1228
- },
1229
- {
1230
- tag: "path",
1231
- attrs: { d: "M2.75 10 8.5 13.25 14.25 10" }
1232
- },
1233
- {
1234
- tag: "path",
1235
- attrs: { d: "M2.75 6v4l5.75 3.25V9.25L2.75 6Z" }
1236
- },
1237
- {
1238
- tag: "path",
1239
- attrs: { d: "M14.25 6v4L8.5 13.25V9.25L14.25 6Z" }
1240
- }
1241
- ]);
1387
+ this.insertFileToken = (tokenKey, label) => {
1388
+ this.params.publishSnapshot(insertFileTokenIntoChatComposer({
1389
+ label,
1390
+ nodes: this.params.optionsReader().nodes,
1391
+ selection: this.params.optionsReader().selection,
1392
+ tokenKey
1393
+ }), { focusAfterSync: true });
1394
+ };
1395
+ this.insertFileTokens = (tokens) => {
1396
+ let nextNodes = this.params.optionsReader().nodes;
1397
+ let nextSelection = this.params.optionsReader().selection;
1398
+ for (const token of tokens) {
1399
+ const snapshot = insertFileTokenIntoChatComposer({
1400
+ label: token.label,
1401
+ nodes: nextNodes,
1402
+ selection: nextSelection,
1403
+ tokenKey: token.tokenKey
1404
+ });
1405
+ nextNodes = snapshot.nodes;
1406
+ nextSelection = snapshot.selection;
1407
+ }
1408
+ this.params.publishSnapshot({
1409
+ nodes: nextNodes,
1410
+ selection: nextSelection
1411
+ }, { focusAfterSync: true });
1242
1412
  };
1243
- this.createFileIcon = () => {
1244
- return this.createSvgIcon([
1245
- {
1246
- tag: "path",
1247
- attrs: { d: "M3.25 4.25A1.5 1.5 0 0 1 4.75 2.75h6.5a1.5 1.5 0 0 1 1.5 1.5v7.5a1.5 1.5 0 0 1-1.5 1.5h-6.5a1.5 1.5 0 0 1-1.5-1.5v-7.5Z" }
1248
- },
1249
- {
1250
- tag: "path",
1251
- attrs: { d: "m4.75 10 2.25-2.5 1.75 1.75 1.25-1.25 2 2" }
1252
- },
1253
- {
1254
- tag: "path",
1255
- attrs: { d: "M9.75 6.25h.01" }
1256
- }
1257
- ]);
1413
+ this.focusComposer = () => {
1414
+ this.params.focusComposer();
1258
1415
  };
1259
- this.createSvgIcon = (children) => {
1260
- const svg = document.createElementNS(SVG_NAMESPACE, "svg");
1261
- svg.setAttribute("viewBox", "0 0 16 16");
1262
- svg.setAttribute("fill", "none");
1263
- svg.setAttribute("stroke", "currentColor");
1264
- svg.setAttribute("stroke-width", "1.25");
1265
- svg.setAttribute("stroke-linecap", "round");
1266
- svg.setAttribute("stroke-linejoin", "round");
1267
- svg.setAttribute("aria-hidden", "true");
1268
- svg.setAttribute("class", "h-3 w-3");
1269
- for (const child of children) {
1270
- const element = document.createElementNS(SVG_NAMESPACE, child.tag);
1271
- for (const [key, value] of Object.entries(child.attrs)) element.setAttribute(key, value);
1272
- svg.append(element);
1273
- }
1274
- return svg;
1416
+ this.syncSelectedSkills = (nextKeys, options) => {
1417
+ this.params.publishSnapshot(syncSelectedSkillsIntoChatComposer({
1418
+ nextKeys,
1419
+ nodes: this.params.optionsReader().nodes,
1420
+ options,
1421
+ selection: this.params.optionsReader().selection
1422
+ }), { focusAfterSync: true });
1275
1423
  };
1276
1424
  }
1277
1425
  };
1278
- //#endregion
1279
- //#region src/components/chat/ui/chat-input-bar/chat-composer-view-controller.ts
1280
- const CHAT_INPUT_MAX_HEIGHT = 188;
1281
- var ChatComposerViewController = class {
1282
- constructor(controller) {
1283
- this.controller = controller;
1284
- this.surfaceRenderer = new ChatComposerSurfaceRenderer();
1285
- this.sync = (nodes, selection) => {
1286
- return this.controller.sync(nodes, selection);
1287
- };
1288
- this.syncSelectionFromRoot = (root) => {
1289
- return this.controller.setSelection(readComposerSelection(root, this.controller.getSnapshot().nodes));
1290
- };
1291
- this.restoreSelectionIfFocused = (root, selection) => {
1292
- if (!root || document.activeElement !== root) return;
1293
- restoreComposerSelection(root, this.controller.getSnapshot().nodes, selection);
1294
- };
1295
- this.syncViewport = (root) => {
1296
- if (!root) return;
1297
- root.style.maxHeight = `${CHAT_INPUT_MAX_HEIGHT}px`;
1298
- root.style.overflowY = root.scrollHeight > CHAT_INPUT_MAX_HEIGHT ? "auto" : "hidden";
1299
- };
1300
- this.renderSurface = (params) => {
1301
- this.surfaceRenderer.render(params.root, {
1302
- nodes: params.snapshot.nodes,
1303
- selectedRange: params.selectedRange,
1304
- nodeStartMap: params.snapshot.nodeStartMap
1305
- });
1306
- };
1307
- this.insertSlashItem = (item, commitSnapshot) => {
1308
- if (!item.value) return;
1309
- commitSnapshot(this.controller.insertSkillToken(item.value, item.title));
1310
- };
1311
- this.syncSelectedSkills = (nextKeys, options, commitSnapshot) => {
1312
- commitSnapshot(this.controller.syncSelectedSkills(nextKeys, options));
1313
- };
1314
- this.handleBeforeInput = (params) => {
1315
- const { event, disabled, isComposing, commitSnapshot } = params;
1316
- const nativeEvent = event.nativeEvent;
1317
- if (disabled || isComposing || nativeEvent.isComposing) return;
1318
- if (!(nativeEvent.inputType === "insertText" || nativeEvent.inputType === "insertReplacementText") || !nativeEvent.data) return;
1319
- event.preventDefault();
1320
- commitSnapshot(this.controller.insertText(nativeEvent.data));
1321
- };
1322
- this.handleInput = (params) => {
1323
- const { event, isComposing, commitSnapshot } = params;
1324
- const nativeEvent = event.nativeEvent;
1325
- if (isComposing || nativeEvent.isComposing) return;
1326
- const root = event.currentTarget;
1327
- const nextDocumentState = readComposerDocumentStateFromDom(root);
1328
- commitSnapshot(this.controller.replaceDocument(nextDocumentState.nodes, nextDocumentState.selection));
1329
- };
1330
- this.handleCompositionEnd = (params) => {
1331
- const { event, commitSnapshot } = params;
1332
- const root = event.currentTarget;
1333
- const nextDocumentState = readComposerDocumentStateFromDom(root);
1334
- commitSnapshot(this.controller.replaceDocument(nextDocumentState.nodes, nextDocumentState.selection));
1335
- };
1336
- this.handleKeyDown = (params) => {
1337
- const { event, slashItems, activeSlashIndex, activeSlashItem, actions, commitSnapshot, insertSkillToken, onSlashItemSelect, onSlashActiveIndexChange, onSlashQueryChange, onSlashOpenChange } = params;
1338
- const currentSnapshot = this.controller.getSnapshot();
1339
- const action = resolveChatComposerKeyboardAction({
1340
- key: event.key,
1341
- shiftKey: event.shiftKey,
1342
- isComposing: event.nativeEvent.isComposing,
1343
- isSlashMenuOpen: currentSnapshot.slashTrigger !== null,
1344
- slashItemCount: slashItems.length,
1345
- activeSlashIndex,
1346
- isSending: actions.isSending,
1347
- canStopGeneration: actions.canStopGeneration
1348
- });
1349
- if (action.type === "noop") return;
1350
- event.preventDefault();
1351
- this.applyKeyboardAction({
1352
- action,
1353
- activeSlashItem,
1354
- actions,
1355
- commitSnapshot,
1356
- insertSkillToken,
1357
- onSlashItemSelect,
1358
- onSlashActiveIndexChange,
1359
- onSlashQueryChange,
1360
- onSlashOpenChange
1361
- });
1362
- };
1363
- this.handlePaste = (params) => {
1364
- const { event, onFilesAdd, commitSnapshot } = params;
1365
- const files = Array.from(event.clipboardData.files ?? []);
1366
- if (files.length > 0 && onFilesAdd) {
1367
- event.preventDefault();
1368
- onFilesAdd(files);
1369
- return;
1370
- }
1371
- const text = event.clipboardData.getData("text/plain");
1372
- if (!text) return;
1373
- event.preventDefault();
1374
- commitSnapshot(this.controller.insertText(text));
1375
- };
1376
- this.handleBlur = (params) => {
1377
- const { clearSelectedRange, onSlashQueryChange, onSlashOpenChange } = params;
1378
- clearSelectedRange();
1426
+ function createLexicalComposerHandle(params) {
1427
+ return new LexicalComposerHandleOwner(params);
1428
+ }
1429
+ function getChatComposerContentSignature(nodes) {
1430
+ return JSON.stringify(nodes.map((node) => node.type === "text" ? {
1431
+ text: node.text,
1432
+ type: node.type
1433
+ } : {
1434
+ label: node.label,
1435
+ tokenKey: node.tokenKey,
1436
+ tokenKind: node.tokenKind,
1437
+ type: node.type
1438
+ }));
1439
+ }
1440
+ function handleLexicalComposerBeforeInput(params) {
1441
+ const { disabled, event, isComposing, publishSnapshot, snapshotReader } = params;
1442
+ const nativeEvent = event.nativeEvent;
1443
+ const shouldInsertText = nativeEvent.inputType === "insertText" || nativeEvent.inputType === "insertReplacementText";
1444
+ if (disabled || isComposing || nativeEvent.isComposing || !shouldInsertText || !nativeEvent.data) return;
1445
+ event.preventDefault();
1446
+ publishSnapshot(replaceChatComposerSelectionWithText({
1447
+ nodes: snapshotReader().nodes,
1448
+ selection: snapshotReader().selection,
1449
+ text: nativeEvent.data
1450
+ }));
1451
+ }
1452
+ function handleLexicalComposerCompositionEnd(params) {
1453
+ const { data, fallbackSnapshot, publishSnapshot, snapshotReader } = params;
1454
+ const currentSnapshot = snapshotReader();
1455
+ const editorSnapshot = fallbackSnapshot();
1456
+ publishSnapshot(getChatComposerContentSignature(editorSnapshot.nodes) !== getChatComposerContentSignature(currentSnapshot.nodes) ? editorSnapshot : data.length > 0 ? replaceChatComposerSelectionWithText({
1457
+ nodes: currentSnapshot.nodes,
1458
+ selection: currentSnapshot.selection,
1459
+ text: data
1460
+ }) : editorSnapshot, { forcePublish: true });
1461
+ }
1462
+ function handleLexicalComposerKeyboardCommand(params) {
1463
+ const { actions, activeSlashIndex, nativeEvent, onSlashActiveIndexChange, onSlashItemSelect, onSlashOpenChange, onSlashQueryChange, publishSnapshot, slashItems, snapshot } = params;
1464
+ const action = resolveLexicalComposerKeyboardAction({
1465
+ activeSlashIndex,
1466
+ canStopGeneration: actions.canStopGeneration,
1467
+ isComposing: nativeEvent.isComposing,
1468
+ isSending: actions.isSending,
1469
+ isSlashMenuOpen: resolveChatComposerSlashTrigger(snapshot.nodes, snapshot.selection) !== null,
1470
+ key: nativeEvent.key,
1471
+ shiftKey: nativeEvent.shiftKey,
1472
+ slashItemCount: slashItems.length
1473
+ });
1474
+ const activeSlashItem = slashItems[activeSlashIndex] ?? null;
1475
+ if (action.type !== "noop") nativeEvent.preventDefault();
1476
+ switch (action.type) {
1477
+ case "move-slash-index":
1478
+ onSlashActiveIndexChange(action.index);
1479
+ return true;
1480
+ case "insert-active-slash-item":
1481
+ if (!activeSlashItem) return true;
1482
+ onSlashItemSelect?.(activeSlashItem);
1483
+ publishSnapshot(insertSkillTokenIntoChatComposer({
1484
+ label: activeSlashItem.title,
1485
+ nodes: snapshot.nodes,
1486
+ selection: snapshot.selection,
1487
+ tokenKey: activeSlashItem.value ?? activeSlashItem.key
1488
+ }), { focusAfterSync: true });
1489
+ return true;
1490
+ case "close-slash":
1379
1491
  onSlashQueryChange?.(null);
1380
1492
  onSlashOpenChange(false);
1381
- };
1382
- this.applyKeyboardAction = (params) => {
1383
- const { action, activeSlashItem, actions, commitSnapshot, insertSkillToken, onSlashItemSelect, onSlashActiveIndexChange, onSlashQueryChange, onSlashOpenChange } = params;
1384
- switch (action.type) {
1385
- case "move-slash-index":
1386
- onSlashActiveIndexChange(action.index);
1387
- return;
1388
- case "insert-active-slash-item":
1389
- if (!activeSlashItem) return;
1390
- onSlashItemSelect?.(activeSlashItem);
1391
- insertSkillToken(activeSlashItem.value ?? activeSlashItem.key, activeSlashItem.title);
1392
- return;
1393
- case "close-slash":
1394
- onSlashQueryChange?.(null);
1395
- onSlashOpenChange(false);
1396
- return;
1397
- case "consume": return;
1398
- case "stop-generation":
1399
- actions.onStop();
1400
- return;
1401
- case "insert-line-break":
1402
- commitSnapshot(this.controller.insertText("\n"));
1403
- return;
1404
- case "send-message":
1405
- actions.onSend();
1406
- return;
1407
- case "delete-content":
1408
- commitSnapshot(this.controller.deleteContent(action.direction));
1409
- return;
1410
- case "noop": return;
1411
- }
1412
- };
1493
+ return true;
1494
+ case "consume": return true;
1495
+ case "stop-generation":
1496
+ actions.onStop();
1497
+ return true;
1498
+ case "insert-line-break":
1499
+ publishSnapshot(replaceChatComposerSelectionWithText({
1500
+ nodes: snapshot.nodes,
1501
+ selection: snapshot.selection,
1502
+ text: "\n"
1503
+ }));
1504
+ return true;
1505
+ case "send-message":
1506
+ actions.onSend();
1507
+ return true;
1508
+ case "delete-content":
1509
+ publishSnapshot(deleteChatComposerContent({
1510
+ direction: action.direction,
1511
+ nodes: snapshot.nodes,
1512
+ selection: snapshot.selection
1513
+ }));
1514
+ return true;
1515
+ case "noop": return false;
1413
1516
  }
1414
- };
1517
+ }
1415
1518
  //#endregion
1416
- //#region src/components/chat/ui/chat-input-bar/chat-composer-runtime.ts
1417
- var ChatComposerRuntime = class {
1418
- constructor() {
1419
- this.controller = new ChatComposerController();
1420
- this.viewController = new ChatComposerViewController(this.controller);
1421
- this.rootElement = null;
1422
- this.selection = null;
1423
- this.selectedRange = null;
1424
- this.snapshot = this.controller.getSnapshot();
1425
- this.config = null;
1426
- this.isComposing = false;
1427
- this.bindRootElement = (node) => {
1428
- this.rootElement = node;
1429
- };
1430
- this.update = (config) => {
1431
- this.config = config;
1432
- this.snapshot = this.viewController.sync(config.nodes, this.selectedRange);
1433
- return {
1434
- snapshot: this.snapshot,
1435
- selectedRange: this.selectedRange,
1436
- bindRootElement: this.bindRootElement,
1437
- handleBeforeInput: this.handleBeforeInput,
1438
- handleInput: this.handleInput,
1439
- handleCompositionStart: this.handleCompositionStart,
1440
- handleCompositionEnd: this.handleCompositionEnd,
1441
- handleKeyDown: this.handleKeyDown,
1442
- handlePaste: this.handlePaste,
1443
- handleBlur: this.handleBlur,
1444
- syncSelectionState: this.syncSelectionState,
1445
- imperativeHandle: this.createHandle()
1446
- };
1447
- };
1448
- this.createHandle = () => {
1449
- return {
1450
- insertSlashItem: (item) => {
1451
- this.config?.onSlashItemSelect?.(item);
1452
- this.viewController.insertSlashItem(item, this.commitSnapshot);
1453
- },
1454
- insertFileToken: (tokenKey, label) => {
1455
- this.commitSnapshot(this.controller.insertFileToken(tokenKey, label));
1456
- this.focusComposerSoon();
1457
- },
1458
- insertFileTokens: (tokens) => {
1459
- let nextSnapshot = null;
1460
- for (const token of tokens) nextSnapshot = this.controller.insertFileToken(token.tokenKey, token.label);
1461
- if (nextSnapshot) {
1462
- this.commitSnapshot(nextSnapshot);
1463
- this.focusComposerSoon();
1464
- }
1465
- },
1466
- focusComposer: () => {
1467
- this.focusComposer();
1468
- },
1469
- syncSelectedSkills: (nextKeys, options) => {
1470
- this.viewController.syncSelectedSkills(nextKeys, options, this.commitSnapshot);
1471
- }
1472
- };
1473
- };
1474
- this.restoreDomAfterCommit = () => {
1475
- if (this.isComposing) return;
1476
- this.viewController.restoreSelectionIfFocused(this.rootElement, this.selection);
1477
- };
1478
- this.renderSurface = () => {
1479
- if (this.isComposing) return;
1480
- this.viewController.renderSurface({
1481
- root: this.rootElement,
1482
- snapshot: this.snapshot,
1483
- selectedRange: this.selectedRange
1484
- });
1485
- };
1486
- this.syncViewport = () => {
1487
- this.viewController.syncViewport(this.rootElement);
1488
- };
1489
- this.syncSelectionState = () => {
1490
- if (!this.rootElement || this.isComposing) return;
1491
- const nextSnapshot = this.viewController.syncSelectionFromRoot(this.rootElement);
1492
- this.selection = nextSnapshot.selection;
1493
- this.selectedRange = nextSnapshot.selection;
1494
- this.syncSlashState(nextSnapshot);
1495
- this.requestRender();
1496
- };
1497
- this.handleBeforeInput = (event) => {
1498
- this.viewController.handleBeforeInput({
1499
- event,
1500
- disabled: this.requireConfig().disabled,
1501
- isComposing: this.isComposing,
1502
- commitSnapshot: this.commitSnapshot
1503
- });
1504
- };
1505
- this.handleInput = (event) => {
1506
- this.viewController.handleInput({
1507
- event,
1508
- isComposing: this.isComposing,
1509
- commitSnapshot: this.commitSnapshot
1510
- });
1511
- };
1512
- this.handleCompositionStart = () => {
1513
- this.isComposing = true;
1514
- };
1515
- this.handleCompositionEnd = (event) => {
1516
- this.isComposing = false;
1517
- this.viewController.handleCompositionEnd({
1518
- event,
1519
- commitSnapshot: this.commitSnapshot
1520
- });
1521
- };
1522
- this.handleKeyDown = (event) => {
1523
- const config = this.requireConfig();
1524
- if (this.rootElement && !this.isComposing) {
1525
- const nextSnapshot = this.viewController.syncSelectionFromRoot(this.rootElement);
1526
- this.selection = nextSnapshot.selection;
1527
- this.selectedRange = nextSnapshot.selection;
1528
- this.snapshot = nextSnapshot;
1529
- }
1530
- const activeSlashItem = config.slashItems[config.activeSlashIndex] ?? null;
1531
- this.viewController.handleKeyDown({
1532
- event,
1533
- slashItems: config.slashItems,
1534
- activeSlashIndex: config.activeSlashIndex,
1535
- activeSlashItem,
1536
- actions: config.actions,
1537
- commitSnapshot: this.commitSnapshot,
1538
- insertSkillToken: this.insertSkillToken,
1539
- onSlashItemSelect: config.onSlashItemSelect,
1540
- onSlashActiveIndexChange: config.onSlashActiveIndexChange,
1541
- onSlashQueryChange: config.onSlashQueryChange,
1542
- onSlashOpenChange: config.onSlashOpenChange
1543
- });
1544
- };
1545
- this.handlePaste = (event) => {
1546
- this.viewController.handlePaste({
1547
- event,
1548
- onFilesAdd: this.requireConfig().onFilesAdd,
1549
- commitSnapshot: this.commitSnapshot
1550
- });
1519
+ //#region src/components/chat/ui/chat-input-bar/lexical/chat-composer-plugins.tsx
1520
+ function ChatComposerBindingsPlugin(props) {
1521
+ const [editor] = useLexicalComposerContext();
1522
+ useLayoutEffect(() => {
1523
+ props.editorRef.current = editor;
1524
+ return () => {
1525
+ if (props.editorRef.current === editor) props.editorRef.current = null;
1551
1526
  };
1552
- this.handleBlur = () => {
1553
- const config = this.requireConfig();
1554
- this.isComposing = false;
1555
- this.viewController.handleBlur({
1556
- clearSelectedRange: this.clearSelectedRange,
1557
- onSlashQueryChange: config.onSlashQueryChange,
1558
- onSlashOpenChange: config.onSlashOpenChange
1527
+ }, [editor, props.editorRef]);
1528
+ useLayoutEffect(() => {
1529
+ editor.setEditable(!props.disabled);
1530
+ }, [editor, props.disabled]);
1531
+ useLayoutEffect(() => {
1532
+ const nextSignature = getChatComposerNodesSignature(props.nodes);
1533
+ const pendingSelection = props.pendingSelectionRef.current;
1534
+ const shouldSyncDocument = nextSignature !== props.editorSignatureRef.current;
1535
+ if (!shouldSyncDocument && !pendingSelection) return;
1536
+ props.isApplyingExternalUpdateRef.current = true;
1537
+ if (shouldSyncDocument) {
1538
+ syncLexicalEditorFromChatComposerState(editor, props.nodes, pendingSelection);
1539
+ props.editorSignatureRef.current = nextSignature;
1540
+ props.lastPublishedSignatureRef.current = nextSignature;
1541
+ } else if (pendingSelection) syncLexicalSelectionFromChatComposerSelection(editor, pendingSelection);
1542
+ if (pendingSelection) {
1543
+ props.selectionRef.current = pendingSelection;
1544
+ props.pendingSelectionRef.current = null;
1545
+ }
1546
+ if (props.shouldFocusAfterSyncRef.current) {
1547
+ props.shouldFocusAfterSyncRef.current = false;
1548
+ const targetSelection = props.selectionRef.current;
1549
+ editor.focus(() => {
1550
+ if (targetSelection) syncLexicalSelectionFromChatComposerSelection(editor, targetSelection);
1559
1551
  });
1560
- };
1561
- this.setSelectedRange = (selection) => {
1562
- this.selectedRange = selection;
1563
- this.selection = selection;
1564
- this.requestRender();
1565
- };
1566
- this.clearSelectedRange = () => {
1567
- this.selectedRange = null;
1568
- this.requestRender();
1569
- };
1570
- this.commitSnapshot = (nextSnapshot) => {
1571
- const config = this.requireConfig();
1572
- this.selection = nextSnapshot.selection;
1573
- this.selectedRange = nextSnapshot.selection;
1574
- this.snapshot = nextSnapshot;
1575
- config.onNodesChange(nextSnapshot.nodes);
1576
- this.syncSlashState(nextSnapshot);
1577
- };
1578
- this.insertSkillToken = (tokenKey, label) => {
1579
- this.commitSnapshot(this.controller.insertSkillToken(tokenKey, label));
1580
- };
1581
- this.syncSlashState = (nextSnapshot) => {
1582
- const config = this.requireConfig();
1583
- config.onSlashTriggerChange?.(nextSnapshot.slashTrigger);
1584
- config.onSlashQueryChange?.(nextSnapshot.slashTrigger?.query ?? null);
1585
- config.onSlashOpenChange(nextSnapshot.slashTrigger !== null);
1586
- };
1587
- this.requestRender = () => {
1588
- this.requireConfig().requestRender();
1589
- };
1590
- this.focusComposerSoon = () => {
1591
- if (typeof requestAnimationFrame === "function") {
1592
- requestAnimationFrame(() => this.focusComposer());
1593
- return;
1594
- }
1595
- this.focusComposer();
1596
- };
1597
- this.focusComposer = () => {
1598
- if (!this.rootElement) return;
1599
- const targetSelection = this.selection;
1600
- this.rootElement.focus();
1601
- const restoreSelection = () => {
1602
- if (!this.rootElement) return;
1603
- this.selection = targetSelection;
1604
- this.selectedRange = targetSelection;
1605
- this.viewController.restoreSelectionIfFocused(this.rootElement, targetSelection);
1606
- };
1607
- if (typeof requestAnimationFrame === "function") {
1608
- requestAnimationFrame(restoreSelection);
1609
- return;
1610
- }
1611
- restoreSelection();
1612
- };
1613
- this.requireConfig = () => {
1614
- if (!this.config) throw new Error("ChatComposerRuntime is not configured.");
1615
- return this.config;
1616
- };
1617
- }
1618
- };
1552
+ }
1553
+ requestAnimationFrame(() => {
1554
+ props.isApplyingExternalUpdateRef.current = false;
1555
+ });
1556
+ }, [
1557
+ editor,
1558
+ props.editorSignatureRef,
1559
+ props.isApplyingExternalUpdateRef,
1560
+ props.lastPublishedSignatureRef,
1561
+ props.nodes,
1562
+ props.pendingSelectionRef,
1563
+ props.selectionRef,
1564
+ props.shouldFocusAfterSyncRef
1565
+ ]);
1566
+ useEffect(() => {
1567
+ return mergeRegister(editor.registerUpdateListener(({ editorState }) => {
1568
+ const snapshot = readChatComposerSnapshotFromEditorState(editorState);
1569
+ const signature = getChatComposerNodesSignature(snapshot.nodes);
1570
+ props.selectionRef.current = snapshot.selection;
1571
+ props.editorSignatureRef.current = signature;
1572
+ props.syncSlashState(snapshot.nodes, snapshot.selection);
1573
+ if (props.isApplyingExternalUpdateRef.current || props.isComposingRef.current) return;
1574
+ if (signature === props.lastPublishedSignatureRef.current) return;
1575
+ props.lastPublishedSignatureRef.current = signature;
1576
+ props.onNodesChange(snapshot.nodes);
1577
+ }), editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
1578
+ const snapshot = readChatComposerSnapshotFromEditorState(editor.getEditorState());
1579
+ props.selectionRef.current = snapshot.selection;
1580
+ if (!props.isComposingRef.current) props.syncSlashState(snapshot.nodes, snapshot.selection);
1581
+ return false;
1582
+ }, COMMAND_PRIORITY_EDITOR), editor.registerCommand(BLUR_COMMAND, () => {
1583
+ props.onBlur();
1584
+ return false;
1585
+ }, COMMAND_PRIORITY_EDITOR), editor.registerCommand(KEY_DOWN_COMMAND, (event) => props.onKeyDown(event), COMMAND_PRIORITY_HIGH));
1586
+ }, [
1587
+ editor,
1588
+ props.editorSignatureRef,
1589
+ props.isApplyingExternalUpdateRef,
1590
+ props.isComposingRef,
1591
+ props.lastPublishedSignatureRef,
1592
+ props.onBlur,
1593
+ props.onKeyDown,
1594
+ props.onNodesChange,
1595
+ props.selectionRef,
1596
+ props.syncSlashState
1597
+ ]);
1598
+ return null;
1599
+ }
1619
1600
  //#endregion
1620
- //#region src/components/chat/ui/chat-input-bar/chat-input-bar-tokenized-composer.tsx
1601
+ //#region src/components/chat/ui/chat-input-bar/lexical/chat-input-bar-tokenized-composer.tsx
1621
1602
  const ChatInputBarTokenizedComposer = forwardRef(function ChatInputBarTokenizedComposer(props, ref) {
1622
- const { nodes, placeholder, disabled, slashItems, onSlashItemSelect, actions, onNodesChange, onFilesAdd, onSlashQueryChange, onSlashTriggerChange, onSlashOpenChange, onSlashActiveIndexChange, activeSlashIndex } = props;
1623
- const [renderTick, setRenderTick] = useState(0);
1624
- const [runtime] = useState(() => new ChatComposerRuntime());
1625
- const { snapshot, bindRootElement, handleBeforeInput, handleInput, handleCompositionStart, handleCompositionEnd, handleKeyDown, handlePaste, handleBlur, syncSelectionState, imperativeHandle } = runtime.update({
1626
- nodes,
1627
- disabled,
1628
- slashItems,
1629
- onSlashItemSelect,
1630
- actions,
1631
- onNodesChange,
1632
- onFilesAdd,
1633
- onSlashQueryChange,
1634
- onSlashTriggerChange,
1635
- onSlashOpenChange,
1636
- onSlashActiveIndexChange,
1637
- activeSlashIndex,
1638
- requestRender: () => setRenderTick((value) => value + 1)
1603
+ const editorRef = useRef(null);
1604
+ const selectionRef = useRef(null);
1605
+ const pendingSelectionRef = useRef(null);
1606
+ const shouldFocusAfterSyncRef = useRef(false);
1607
+ const isComposingRef = useRef(false);
1608
+ const isApplyingExternalUpdateRef = useRef(false);
1609
+ const editorSignatureRef = useRef("");
1610
+ const lastPublishedSignatureRef = useRef("");
1611
+ const syncSlashState = (nodes, selection) => {
1612
+ const trigger = resolveChatComposerSlashTrigger(nodes, selection);
1613
+ props.onSlashTriggerChange?.(trigger);
1614
+ props.onSlashQueryChange?.(trigger?.query ?? null);
1615
+ props.onSlashOpenChange(trigger !== null);
1616
+ };
1617
+ const readCurrentNodes = () => {
1618
+ return props.nodes;
1619
+ };
1620
+ const readCurrentSelection = () => {
1621
+ if (selectionRef.current) return selectionRef.current;
1622
+ if (!editorRef.current) return null;
1623
+ const snapshot = readChatComposerSnapshotFromEditorState(editorRef.current.getEditorState());
1624
+ selectionRef.current = snapshot.selection;
1625
+ return snapshot.selection;
1626
+ };
1627
+ const publishSnapshot = (snapshot, options) => {
1628
+ selectionRef.current = snapshot.selection;
1629
+ pendingSelectionRef.current = snapshot.selection;
1630
+ if (options?.focusAfterSync) shouldFocusAfterSyncRef.current = true;
1631
+ const signature = getChatComposerNodesSignature(snapshot.nodes);
1632
+ syncSlashState(snapshot.nodes, snapshot.selection);
1633
+ if (options?.forcePublish || signature !== lastPublishedSignatureRef.current) {
1634
+ lastPublishedSignatureRef.current = signature;
1635
+ props.onNodesChange(snapshot.nodes);
1636
+ }
1637
+ };
1638
+ const focusComposer = () => {
1639
+ const editor = editorRef.current;
1640
+ if (!editor) return;
1641
+ const targetSelection = selectionRef.current;
1642
+ editor.focus(() => {
1643
+ if (targetSelection) syncLexicalSelectionFromChatComposerSelection(editor, targetSelection);
1644
+ });
1645
+ };
1646
+ const readComposerSnapshot = () => ({
1647
+ nodes: readCurrentNodes(),
1648
+ selection: readCurrentSelection()
1639
1649
  });
1640
- useImperativeHandle(ref, () => imperativeHandle, [imperativeHandle]);
1641
- useLayoutEffect(() => {
1642
- runtime.renderSurface();
1643
- runtime.restoreDomAfterCommit();
1644
- runtime.syncViewport();
1645
- }, [
1646
- runtime,
1647
- snapshot.nodes,
1648
- snapshot.nodeStartMap,
1649
- renderTick
1650
- ]);
1651
- return /* @__PURE__ */ jsx("div", {
1652
- className: "px-4 py-2.5",
1653
- children: /* @__PURE__ */ jsx("div", {
1654
- className: "min-h-[60px]",
1655
- children: /* @__PURE__ */ jsx("div", {
1656
- ref: bindRootElement,
1657
- contentEditable: !disabled,
1658
- suppressContentEditableWarning: true,
1659
- role: "textbox",
1660
- "aria-multiline": "true",
1661
- "data-placeholder": placeholder,
1662
- onBeforeInput: handleBeforeInput,
1663
- onInput: handleInput,
1664
- onCompositionStart: handleCompositionStart,
1665
- onCompositionEnd: handleCompositionEnd,
1666
- onKeyDown: handleKeyDown,
1667
- onKeyUp: syncSelectionState,
1668
- onMouseUp: syncSelectionState,
1669
- onFocus: syncSelectionState,
1670
- onBlur: handleBlur,
1671
- onPaste: handlePaste,
1672
- className: "min-h-7 max-h-[188px] w-full overflow-y-auto whitespace-pre-wrap break-words bg-transparent py-0.5 text-sm leading-6 text-gray-800 outline-none empty:before:pointer-events-none empty:before:text-gray-400 empty:before:content-[attr(data-placeholder)]"
1650
+ useImperativeHandle(ref, () => createLexicalComposerHandle({
1651
+ focusComposer,
1652
+ onSlashItemSelect: props.onSlashItemSelect,
1653
+ optionsReader: readComposerSnapshot,
1654
+ publishSnapshot
1655
+ }), [props.onSlashItemSelect]);
1656
+ return /* @__PURE__ */ jsxs(LexicalComposer, {
1657
+ initialConfig: useMemo(() => ({
1658
+ editable: !props.disabled,
1659
+ editorState: () => {
1660
+ writeChatComposerStateToLexicalRoot(props.nodes, null);
1661
+ },
1662
+ namespace: "NextClawChatComposerLexical",
1663
+ nodes: [ChatComposerTokenNode],
1664
+ onError: (error) => {
1665
+ throw error;
1666
+ },
1667
+ theme: {}
1668
+ }), [props.disabled, props.nodes]),
1669
+ children: [
1670
+ /* @__PURE__ */ jsx("div", {
1671
+ className: "px-4 py-2.5",
1672
+ children: /* @__PURE__ */ jsx("div", {
1673
+ className: "min-h-[60px]",
1674
+ children: /* @__PURE__ */ jsx(PlainTextPlugin, {
1675
+ contentEditable: /* @__PURE__ */ jsx(ContentEditable, {
1676
+ className: "min-h-7 max-h-[188px] w-full overflow-y-auto whitespace-pre-wrap break-words bg-transparent py-0.5 text-sm leading-6 text-gray-800 outline-none",
1677
+ onBeforeInput: (event) => {
1678
+ handleLexicalComposerBeforeInput({
1679
+ disabled: props.disabled,
1680
+ event,
1681
+ isComposing: isComposingRef.current,
1682
+ publishSnapshot,
1683
+ snapshotReader: readComposerSnapshot
1684
+ });
1685
+ },
1686
+ onCompositionEnd: (event) => {
1687
+ isComposingRef.current = false;
1688
+ const nativeEvent = event.nativeEvent;
1689
+ handleLexicalComposerCompositionEnd({
1690
+ data: typeof nativeEvent.data === "string" ? nativeEvent.data : "",
1691
+ fallbackSnapshot: () => {
1692
+ const editor = editorRef.current;
1693
+ return editor ? readChatComposerSnapshotFromEditorState(editor.getEditorState()) : readComposerSnapshot();
1694
+ },
1695
+ publishSnapshot,
1696
+ snapshotReader: readComposerSnapshot
1697
+ });
1698
+ },
1699
+ onCompositionStart: () => {
1700
+ isComposingRef.current = true;
1701
+ },
1702
+ onPaste: (event) => {
1703
+ const files = Array.from(event.clipboardData.files ?? []);
1704
+ if (files.length > 0 && props.onFilesAdd) {
1705
+ event.preventDefault();
1706
+ props.onFilesAdd(files);
1707
+ }
1708
+ }
1709
+ }),
1710
+ placeholder: /* @__PURE__ */ jsx("div", {
1711
+ className: "pointer-events-none absolute left-4 top-2.5 select-none text-sm leading-6 text-gray-400",
1712
+ children: props.placeholder
1713
+ }),
1714
+ ErrorBoundary: LexicalErrorBoundary
1715
+ })
1716
+ })
1717
+ }),
1718
+ /* @__PURE__ */ jsx(EditorRefPlugin, { editorRef }),
1719
+ /* @__PURE__ */ jsx(ChatComposerBindingsPlugin, {
1720
+ disabled: props.disabled,
1721
+ editorRef,
1722
+ editorSignatureRef,
1723
+ isApplyingExternalUpdateRef,
1724
+ isComposingRef,
1725
+ lastPublishedSignatureRef,
1726
+ nodes: props.nodes,
1727
+ onBlur: () => {
1728
+ props.onSlashQueryChange?.(null);
1729
+ props.onSlashOpenChange(false);
1730
+ },
1731
+ onKeyDown: (event) => {
1732
+ const editor = editorRef.current;
1733
+ if (!editor) return false;
1734
+ const snapshot = readChatComposerSnapshotFromEditorState(editor.getEditorState());
1735
+ selectionRef.current = snapshot.selection;
1736
+ return handleLexicalComposerKeyboardCommand({
1737
+ actions: props.actions,
1738
+ activeSlashIndex: props.activeSlashIndex,
1739
+ nativeEvent: event,
1740
+ onSlashActiveIndexChange: props.onSlashActiveIndexChange,
1741
+ onSlashItemSelect: props.onSlashItemSelect,
1742
+ onSlashOpenChange: props.onSlashOpenChange,
1743
+ onSlashQueryChange: props.onSlashQueryChange,
1744
+ publishSnapshot,
1745
+ slashItems: props.slashItems,
1746
+ snapshot
1747
+ });
1748
+ },
1749
+ onNodesChange: props.onNodesChange,
1750
+ pendingSelectionRef,
1751
+ selectionRef,
1752
+ shouldFocusAfterSyncRef,
1753
+ syncSlashState
1673
1754
  })
1674
- })
1755
+ ]
1675
1756
  });
1676
1757
  });
1758
+ ChatInputBarTokenizedComposer.displayName = "LexicalChatInputBarTokenizedComposer";
1677
1759
  //#endregion
1678
1760
  //#region src/components/chat/ui/chat-input-bar/chat-input-bar.tsx
1679
1761
  function InputBarHint({ hint }) {
@@ -1726,6 +1808,7 @@ const ChatInputBar = forwardRef(function ChatInputBar(props, ref) {
1726
1808
  ...props.toolbar.skillPicker,
1727
1809
  onSelectedKeysChange: (nextKeys) => {
1728
1810
  composerRef.current?.syncSelectedSkills(nextKeys, props.toolbar.skillPicker?.options ?? []);
1811
+ props.toolbar.skillPicker?.onSelectedKeysChange(nextKeys);
1729
1812
  }
1730
1813
  }
1731
1814
  };
@@ -2100,29 +2183,17 @@ function ChatMessageInlineContent({ segments, role, texts }) {
2100
2183
  }
2101
2184
  //#endregion
2102
2185
  //#region src/components/chat/ui/chat-message-list/chat-message-file/meta.ts
2103
- const FILE_CATEGORY_LABELS = {
2104
- archive: "Archive",
2105
- audio: "Audio",
2106
- code: "Code",
2107
- data: "Data",
2108
- document: "Document",
2109
- generic: "File",
2110
- image: "Image",
2111
- pdf: "PDF",
2112
- sheet: "Sheet",
2113
- video: "Video"
2114
- };
2115
2186
  const FILE_CATEGORY_TILE_CLASSES = {
2116
- archive: "border-amber-200/80 bg-amber-100 text-amber-700",
2117
- audio: "border-fuchsia-200/80 bg-fuchsia-100 text-fuchsia-700",
2118
- code: "border-cyan-200/80 bg-cyan-100 text-cyan-700",
2119
- data: "border-slate-200/80 bg-slate-100 text-slate-700",
2120
- document: "border-blue-200/80 bg-blue-100 text-blue-700",
2121
- generic: "border-slate-200/80 bg-slate-100 text-slate-700",
2122
- image: "border-emerald-200/80 bg-emerald-100 text-emerald-700",
2123
- pdf: "border-rose-200/80 bg-rose-100 text-rose-700",
2124
- sheet: "border-lime-200/80 bg-lime-100 text-lime-700",
2125
- video: "border-violet-200/80 bg-violet-100 text-violet-700"
2187
+ archive: "border-amber-200/80 bg-amber-50 text-amber-700",
2188
+ audio: "border-fuchsia-200/80 bg-fuchsia-50 text-fuchsia-700",
2189
+ code: "border-cyan-200/80 bg-cyan-50 text-cyan-700",
2190
+ data: "border-slate-200/80 bg-slate-50 text-slate-700",
2191
+ document: "border-blue-200/80 bg-blue-50 text-blue-700",
2192
+ generic: "border-slate-200/80 bg-slate-50 text-slate-700",
2193
+ image: "border-emerald-200/80 bg-emerald-50 text-emerald-700",
2194
+ pdf: "border-rose-200/80 bg-rose-50 text-rose-700",
2195
+ sheet: "border-lime-200/80 bg-lime-50 text-lime-700",
2196
+ video: "border-violet-200/80 bg-violet-50 text-violet-700"
2126
2197
  };
2127
2198
  const CODE_EXTENSIONS = new Set([
2128
2199
  "c",
@@ -2249,22 +2320,30 @@ function buildChatMessageFileMeta(file) {
2249
2320
  return {
2250
2321
  category,
2251
2322
  extension: getFileExtension(file.label, file.mimeType),
2252
- sizeLabel,
2253
- metaBadges: [FILE_CATEGORY_LABELS[category], sizeLabel].filter((value) => Boolean(value))
2323
+ sizeLabel
2254
2324
  };
2255
2325
  }
2256
2326
  //#endregion
2257
2327
  //#region src/components/chat/ui/chat-message-list/chat-message-file/index.tsx
2258
- function renderMetaBadge(label, isUser) {
2259
- return /* @__PURE__ */ jsx("span", {
2260
- className: cn("inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-medium", isUser ? "border-white/12 bg-white/10 text-white/82" : "border-slate-200/80 bg-slate-950/[0.04] text-slate-600"),
2261
- children: label
2262
- }, label);
2328
+ const DEFAULT_FILE_CATEGORY_LABELS = {
2329
+ archive: "Archive",
2330
+ audio: "Audio",
2331
+ code: "Code file",
2332
+ data: "Data file",
2333
+ document: "Document",
2334
+ generic: "File",
2335
+ image: "Image",
2336
+ pdf: "PDF document",
2337
+ sheet: "Spreadsheet",
2338
+ video: "Video"
2339
+ };
2340
+ function readFileCategoryLabel(category, texts) {
2341
+ return texts?.attachmentCategoryLabels?.[category] ?? DEFAULT_FILE_CATEGORY_LABELS[category];
2263
2342
  }
2264
- function renderMimeType(mimeType, isUser) {
2343
+ function renderMetaLine(categoryLabel, sizeLabel, isUser) {
2265
2344
  return /* @__PURE__ */ jsx("div", {
2266
- className: cn("truncate text-[11px]", isUser ? "text-white/58" : "text-slate-500"),
2267
- children: mimeType
2345
+ className: cn("mt-1 text-xs leading-5", isUser ? "text-white/70" : "text-slate-500"),
2346
+ children: sizeLabel ? `${categoryLabel} · ${sizeLabel}` : categoryLabel
2268
2347
  });
2269
2348
  }
2270
2349
  function renderActionPill(label, isUser, isInteractive) {
@@ -2273,18 +2352,39 @@ function renderActionPill(label, isUser, isInteractive) {
2273
2352
  children: label
2274
2353
  });
2275
2354
  }
2276
- function ChatMessageFile({ file, isUser = false }) {
2277
- const { category, extension, metaBadges, sizeLabel } = buildChatMessageFileMeta(file);
2355
+ function readFileCategoryIcon(category) {
2356
+ if (category === "archive") return FileArchive;
2357
+ if (category === "audio") return FileAudio2;
2358
+ if (category === "code") return FileCode2;
2359
+ if (category === "data") return FileJson2;
2360
+ if (category === "pdf") return FileText;
2361
+ if (category === "sheet") return FileSpreadsheet;
2362
+ if (category === "image") return FileImage;
2363
+ if (category === "video") return FileVideo2;
2364
+ if (category === "document") return FileText;
2365
+ return File;
2366
+ }
2367
+ function FileCategoryGlyph({ category, isUser }) {
2368
+ const Icon = readFileCategoryIcon(category);
2369
+ return /* @__PURE__ */ jsx("div", {
2370
+ className: cn("flex h-14 w-14 shrink-0 items-center justify-center rounded-[1rem] border", isUser ? "border-white/12 bg-white/10 text-white" : FILE_CATEGORY_TILE_CLASSES[category]),
2371
+ children: /* @__PURE__ */ jsx(Icon, {
2372
+ className: cn("h-7 w-7", isUser ? "text-white/92" : "text-current"),
2373
+ strokeWidth: 2.2
2374
+ })
2375
+ });
2376
+ }
2377
+ function ChatMessageFile({ file, isUser = false, texts }) {
2378
+ const { category, extension, sizeLabel } = buildChatMessageFileMeta(file);
2278
2379
  const renderAsImage = isImageFileLike(file) && Boolean(file.dataUrl);
2279
2380
  const isInteractive = Boolean(file.dataUrl);
2280
- const actionLabel = isInteractive ? renderAsImage ? "Open preview" : "Open file" : "Attached";
2281
- const tileLabel = extension.slice(0, 4);
2282
- const shellClasses = cn("block overflow-hidden rounded-[1.25rem] border transition duration-200", isUser ? "border-white/12 bg-white/10 text-white shadow-[0_18px_40px_-28px_rgba(15,23,42,0.85)]" : "border-slate-200/80 bg-white/95 text-slate-900 shadow-[0_20px_45px_-30px_rgba(15,23,42,0.28)]", isInteractive && (isUser ? "hover:border-white/18 hover:bg-white/13" : "hover:border-slate-300 hover:bg-white hover:shadow-[0_24px_50px_-30px_rgba(15,23,42,0.3)]"));
2381
+ const actionLabel = isInteractive ? texts?.attachmentOpenLabel ?? "Open" : texts?.attachmentAttachedLabel ?? "Attached";
2382
+ const categoryLabel = readFileCategoryLabel(category, texts);
2383
+ const shellClasses = cn("block overflow-hidden rounded-[1.25rem] border transition duration-200", isUser ? "border-white/12 bg-white/10 text-white" : "border-slate-200/80 bg-white/95 text-slate-900", isInteractive && (isUser ? "hover:border-white/18 hover:bg-white/13" : "hover:border-slate-300 hover:bg-white"));
2283
2384
  if (renderAsImage && file.dataUrl) return /* @__PURE__ */ jsx("a", {
2284
2385
  href: file.dataUrl,
2285
2386
  target: "_blank",
2286
2387
  rel: "noreferrer",
2287
- "aria-label": `Open preview: ${file.label}`,
2288
2388
  className: "group block",
2289
2389
  children: /* @__PURE__ */ jsxs("div", {
2290
2390
  className: cn("relative overflow-hidden rounded-[1rem]", isUser ? "ring-1 ring-white/10" : "bg-slate-100/80 ring-1 ring-slate-200/80"),
@@ -2296,7 +2396,7 @@ function ChatMessageFile({ file, isUser = false }) {
2296
2396
  className: "pointer-events-none absolute right-3 top-3 flex items-center gap-2",
2297
2397
  children: [/* @__PURE__ */ jsx("span", {
2298
2398
  className: cn("inline-flex items-center rounded-full px-2 py-1 text-[10px] font-semibold tracking-[0.18em] text-white backdrop-blur-sm", isUser ? "bg-slate-950/36" : "bg-slate-950/58"),
2299
- children: tileLabel
2399
+ children: categoryLabel
2300
2400
  }), sizeLabel ? /* @__PURE__ */ jsx("span", {
2301
2401
  className: cn("inline-flex items-center rounded-full px-2 py-1 text-[10px] font-medium text-white/92 backdrop-blur-sm", isUser ? "bg-slate-950/28" : "bg-slate-950/46"),
2302
2402
  children: sizeLabel
@@ -2305,35 +2405,22 @@ function ChatMessageFile({ file, isUser = false }) {
2305
2405
  })
2306
2406
  });
2307
2407
  const content = /* @__PURE__ */ jsxs("div", {
2308
- className: "flex items-start gap-3 p-3.5",
2309
- children: [/* @__PURE__ */ jsx("div", {
2310
- className: cn("flex h-14 w-14 shrink-0 items-center justify-center rounded-[1rem] border text-xs font-semibold tracking-[0.22em]", isUser ? "border-white/12 bg-white/10 text-white" : FILE_CATEGORY_TILE_CLASSES[category]),
2311
- children: tileLabel
2312
- }), /* @__PURE__ */ jsxs("div", {
2408
+ className: "flex items-center gap-3 p-3.5",
2409
+ children: [/* @__PURE__ */ jsx(FileCategoryGlyph, {
2410
+ category,
2411
+ isUser
2412
+ }), /* @__PURE__ */ jsx("div", {
2313
2413
  className: "min-w-0 flex-1",
2314
- children: [
2315
- /* @__PURE__ */ jsxs("div", {
2316
- className: "flex items-start gap-3",
2317
- children: [/* @__PURE__ */ jsxs("div", {
2318
- className: "min-w-0 flex-1",
2319
- children: [/* @__PURE__ */ jsx("div", {
2320
- className: "truncate text-sm font-semibold leading-5",
2321
- children: file.label
2322
- }), /* @__PURE__ */ jsxs("div", {
2323
- className: cn("mt-1 text-xs", isUser ? "text-white/68" : "text-slate-500"),
2324
- children: [FILE_CATEGORY_LABELS[category], " attachment"]
2325
- })]
2326
- }), renderActionPill(actionLabel, isUser, isInteractive)]
2327
- }),
2328
- /* @__PURE__ */ jsx("div", {
2329
- className: "mt-3 flex flex-wrap gap-2",
2330
- children: metaBadges.map((label) => renderMetaBadge(label, isUser))
2331
- }),
2332
- /* @__PURE__ */ jsx("div", {
2333
- className: "mt-2",
2334
- children: renderMimeType(file.mimeType, isUser)
2335
- })
2336
- ]
2414
+ children: /* @__PURE__ */ jsxs("div", {
2415
+ className: "flex items-center gap-3",
2416
+ children: [/* @__PURE__ */ jsxs("div", {
2417
+ className: "min-w-0 flex-1",
2418
+ children: [/* @__PURE__ */ jsx("div", {
2419
+ className: "truncate text-[15px] font-semibold leading-5",
2420
+ children: file.label
2421
+ }), renderMetaLine(categoryLabel, sizeLabel, isUser)]
2422
+ }), renderActionPill(actionLabel, isUser, isInteractive)]
2423
+ })
2337
2424
  })]
2338
2425
  });
2339
2426
  if (!isInteractive) return /* @__PURE__ */ jsx("div", {
@@ -2344,7 +2431,6 @@ function ChatMessageFile({ file, isUser = false }) {
2344
2431
  href: file.dataUrl,
2345
2432
  target: "_blank",
2346
2433
  rel: "noreferrer",
2347
- "aria-label": `Open file: ${file.label}`,
2348
2434
  className: cn(shellClasses, "group"),
2349
2435
  children: content
2350
2436
  });
@@ -2380,19 +2466,113 @@ function useReasoningBlockOpenState(params) {
2380
2466
  };
2381
2467
  }
2382
2468
  //#endregion
2469
+ //#region src/components/chat/hooks/use-sticky-bottom-scroll.ts
2470
+ const DEFAULT_STICKY_THRESHOLD_PX = 10;
2471
+ function scrollElementToBottom(element) {
2472
+ element.scrollTop = element.scrollHeight;
2473
+ }
2474
+ function queueScrollToBottom(params) {
2475
+ if (!params.scrollRef.current) return;
2476
+ if (params.scheduledScrollFrameRef.current !== null) cancelAnimationFrame(params.scheduledScrollFrameRef.current);
2477
+ params.scheduledScrollFrameRef.current = requestAnimationFrame(() => {
2478
+ params.scheduledScrollFrameRef.current = null;
2479
+ const currentElement = params.scrollRef.current;
2480
+ if (!currentElement) return;
2481
+ params.isProgrammaticScrollRef.current = true;
2482
+ scrollElementToBottom(currentElement);
2483
+ });
2484
+ }
2485
+ function useStickyBottomScroll(params) {
2486
+ const isStickyRef = useRef(true);
2487
+ const isProgrammaticScrollRef = useRef(false);
2488
+ const previousResetKeyRef = useRef(null);
2489
+ const pendingInitialScrollRef = useRef(false);
2490
+ const scheduledScrollFrameRef = useRef(null);
2491
+ const onScroll = () => {
2492
+ if (isProgrammaticScrollRef.current) {
2493
+ isProgrammaticScrollRef.current = false;
2494
+ return;
2495
+ }
2496
+ const element = params.scrollRef.current;
2497
+ if (!element) return;
2498
+ isStickyRef.current = element.scrollHeight - element.scrollTop - element.clientHeight <= (params.stickyThresholdPx ?? DEFAULT_STICKY_THRESHOLD_PX);
2499
+ };
2500
+ useEffect(() => {
2501
+ if (previousResetKeyRef.current === params.resetKey) return;
2502
+ previousResetKeyRef.current = params.resetKey;
2503
+ isStickyRef.current = true;
2504
+ pendingInitialScrollRef.current = true;
2505
+ }, [params.resetKey]);
2506
+ useEffect(() => {
2507
+ const scheduledScrollFrame = scheduledScrollFrameRef.current;
2508
+ return () => {
2509
+ if (scheduledScrollFrame !== null) cancelAnimationFrame(scheduledScrollFrame);
2510
+ };
2511
+ }, []);
2512
+ useLayoutEffect(() => {
2513
+ if (!pendingInitialScrollRef.current || params.isLoading || !params.hasContent) return;
2514
+ if (!params.scrollRef.current) return;
2515
+ pendingInitialScrollRef.current = false;
2516
+ queueScrollToBottom({
2517
+ scrollRef: params.scrollRef,
2518
+ scheduledScrollFrameRef,
2519
+ isProgrammaticScrollRef
2520
+ });
2521
+ }, [
2522
+ params.hasContent,
2523
+ params.isLoading,
2524
+ params.scrollRef
2525
+ ]);
2526
+ useLayoutEffect(() => {
2527
+ if (!isStickyRef.current || !params.hasContent) return;
2528
+ if (!params.scrollRef.current) return;
2529
+ queueScrollToBottom({
2530
+ scrollRef: params.scrollRef,
2531
+ scheduledScrollFrameRef,
2532
+ isProgrammaticScrollRef
2533
+ });
2534
+ }, [
2535
+ params.contentVersion,
2536
+ params.hasContent,
2537
+ params.scrollRef
2538
+ ]);
2539
+ return { onScroll };
2540
+ }
2541
+ //#endregion
2383
2542
  //#region src/components/chat/ui/chat-message-list/chat-reasoning-block.tsx
2543
+ function normalizeReasoningLabel(label) {
2544
+ const normalizedLabel = label.trim().toLowerCase();
2545
+ if (normalizedLabel === "reasoning" || normalizedLabel === "thinking" || normalizedLabel === "推理" || normalizedLabel === "推理过程" || normalizedLabel === "思考过程" || normalizedLabel === "思考") return "思考";
2546
+ return label;
2547
+ }
2384
2548
  function ChatReasoningBlock({ label, text, isUser, isInProgress }) {
2385
2549
  const { isOpen, onSummaryClick } = useReasoningBlockOpenState({ isInProgress });
2550
+ const scrollRef = useRef(null);
2551
+ const displayLabel = normalizeReasoningLabel(label);
2552
+ const { onScroll } = useStickyBottomScroll({
2553
+ scrollRef,
2554
+ resetKey: `${displayLabel}:${isInProgress ? "streaming" : "idle"}`,
2555
+ isLoading: false,
2556
+ hasContent: text.length > 0,
2557
+ contentVersion: text,
2558
+ stickyThresholdPx: 20
2559
+ });
2386
2560
  return /* @__PURE__ */ jsxs("details", {
2387
2561
  className: "mt-2",
2388
2562
  open: isOpen,
2389
2563
  children: [/* @__PURE__ */ jsx("summary", {
2390
2564
  className: cn("cursor-pointer text-xs", isUser ? "text-primary-100" : "text-gray-500"),
2391
2565
  onClick: onSummaryClick,
2392
- children: label
2393
- }), /* @__PURE__ */ jsx("pre", {
2394
- className: cn("mt-2 w-fit max-w-[500px] whitespace-pre-wrap break-all rounded-lg p-2 text-[11px]", isUser ? "bg-primary-700/60" : "bg-gray-100"),
2395
- children: text
2566
+ children: displayLabel
2567
+ }), /* @__PURE__ */ jsx("div", {
2568
+ ref: scrollRef,
2569
+ onScroll,
2570
+ "data-reasoning-scroll": "true",
2571
+ className: cn("mt-2 w-fit max-w-[500px] max-h-56 overflow-y-auto overscroll-contain rounded-lg custom-scrollbar-amber", isUser ? "bg-primary-700/60" : "bg-gray-100"),
2572
+ children: /* @__PURE__ */ jsx("pre", {
2573
+ className: "min-w-0 whitespace-pre-wrap break-all p-2 text-[11px]",
2574
+ children: text
2575
+ })
2396
2576
  })]
2397
2577
  });
2398
2578
  }
@@ -2512,79 +2692,6 @@ function ToolCardHeaderSessionAction({ action, onAction }) {
2512
2692
  });
2513
2693
  }
2514
2694
  //#endregion
2515
- //#region src/components/chat/hooks/use-sticky-bottom-scroll.ts
2516
- const DEFAULT_STICKY_THRESHOLD_PX = 10;
2517
- function scrollElementToBottom(element) {
2518
- element.scrollTop = element.scrollHeight;
2519
- }
2520
- function queueScrollToBottom(params) {
2521
- if (!params.scrollRef.current) return;
2522
- if (params.scheduledScrollFrameRef.current !== null) cancelAnimationFrame(params.scheduledScrollFrameRef.current);
2523
- params.scheduledScrollFrameRef.current = requestAnimationFrame(() => {
2524
- params.scheduledScrollFrameRef.current = null;
2525
- const currentElement = params.scrollRef.current;
2526
- if (!currentElement) return;
2527
- params.isProgrammaticScrollRef.current = true;
2528
- scrollElementToBottom(currentElement);
2529
- });
2530
- }
2531
- function useStickyBottomScroll(params) {
2532
- const isStickyRef = useRef(true);
2533
- const isProgrammaticScrollRef = useRef(false);
2534
- const previousResetKeyRef = useRef(null);
2535
- const pendingInitialScrollRef = useRef(false);
2536
- const scheduledScrollFrameRef = useRef(null);
2537
- const onScroll = () => {
2538
- if (isProgrammaticScrollRef.current) {
2539
- isProgrammaticScrollRef.current = false;
2540
- return;
2541
- }
2542
- const element = params.scrollRef.current;
2543
- if (!element) return;
2544
- isStickyRef.current = element.scrollHeight - element.scrollTop - element.clientHeight <= (params.stickyThresholdPx ?? DEFAULT_STICKY_THRESHOLD_PX);
2545
- };
2546
- useEffect(() => {
2547
- if (previousResetKeyRef.current === params.resetKey) return;
2548
- previousResetKeyRef.current = params.resetKey;
2549
- isStickyRef.current = true;
2550
- pendingInitialScrollRef.current = true;
2551
- }, [params.resetKey]);
2552
- useEffect(() => {
2553
- const scheduledScrollFrame = scheduledScrollFrameRef.current;
2554
- return () => {
2555
- if (scheduledScrollFrame !== null) cancelAnimationFrame(scheduledScrollFrame);
2556
- };
2557
- }, []);
2558
- useLayoutEffect(() => {
2559
- if (!pendingInitialScrollRef.current || params.isLoading || !params.hasContent) return;
2560
- if (!params.scrollRef.current) return;
2561
- pendingInitialScrollRef.current = false;
2562
- queueScrollToBottom({
2563
- scrollRef: params.scrollRef,
2564
- scheduledScrollFrameRef,
2565
- isProgrammaticScrollRef
2566
- });
2567
- }, [
2568
- params.hasContent,
2569
- params.isLoading,
2570
- params.scrollRef
2571
- ]);
2572
- useLayoutEffect(() => {
2573
- if (!isStickyRef.current || !params.hasContent) return;
2574
- if (!params.scrollRef.current) return;
2575
- queueScrollToBottom({
2576
- scrollRef: params.scrollRef,
2577
- scheduledScrollFrameRef,
2578
- isProgrammaticScrollRef
2579
- });
2580
- }, [
2581
- params.contentVersion,
2582
- params.hasContent,
2583
- params.scrollRef
2584
- ]);
2585
- return { onScroll };
2586
- }
2587
- //#endregion
2588
2695
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-file-operation-lines.tsx
2589
2696
  const FILE_ROW_CLASS_NAME = "h-5 font-mono text-[11px] leading-5";
2590
2697
  const FILE_GUTTER_NUMBER_CELL_CLASS_NAME = "sticky left-0 z-[1] flex h-5 items-center justify-center px-2.5 tabular-nums select-none";
@@ -2833,6 +2940,9 @@ function normalizeTerminalOutput(rawOutput, structuredOutput) {
2833
2940
  return stripAnsi(rawOutput);
2834
2941
  }
2835
2942
  }
2943
+ function shouldAutoExpandRunningFileOperation(toolName) {
2944
+ return toolName === "write_file" || toolName === "edit_file";
2945
+ }
2836
2946
  function useToolCardExpandedState({ canExpand, isRunning, autoExpandWhileRunning = true, expandOnError = false, statusTone }) {
2837
2947
  const [expanded, setExpanded] = useState(false);
2838
2948
  const [hasUserToggled, setHasUserToggled] = useState(false);
@@ -2933,6 +3043,7 @@ function TerminalExecutionView({ card }) {
2933
3043
  const { expanded, onToggle } = useToolCardExpandedState({
2934
3044
  canExpand: output.trim().length > 0 || isRunning,
2935
3045
  isRunning,
3046
+ autoExpandWhileRunning: false,
2936
3047
  expandOnError: hasContent,
2937
3048
  statusTone: card.statusTone
2938
3049
  });
@@ -2974,7 +3085,7 @@ function FileOperationView({ card }) {
2974
3085
  if (block.rawText) return count + block.rawText.length;
2975
3086
  return count + block.lines.reduce((lineCount, line) => lineCount + line.text.length + 1, 0);
2976
3087
  }, 0);
2977
- const shouldAutoExpandWhileRunning = !(isRunning && card.toolName === "write_file" && (previewLineCount > 24 || previewCharCount > 1200));
3088
+ const shouldAutoExpandWhileRunning = shouldAutoExpandRunningFileOperation(card.toolName) && !(isRunning && card.toolName === "write_file" && (previewLineCount > 24 || previewCharCount > 1200));
2978
3089
  const { expanded, onToggle } = useToolCardExpandedState({
2979
3090
  canExpand: hasContent || isRunning,
2980
3091
  isRunning,
@@ -3000,6 +3111,7 @@ function SearchSnippetView({ card }) {
3000
3111
  const { expanded, onToggle } = useToolCardExpandedState({
3001
3112
  canExpand: !!output || isRunning,
3002
3113
  isRunning,
3114
+ autoExpandWhileRunning: false,
3003
3115
  statusTone: card.statusTone
3004
3116
  });
3005
3117
  return /* @__PURE__ */ jsxs(ToolCardRoot, { children: [/* @__PURE__ */ jsx(ToolCardHeader, {
@@ -3025,6 +3137,7 @@ function GenericToolCard({ card, onToolAction, renderToolAgent }) {
3025
3137
  const { expanded, onToggle } = useToolCardExpandedState({
3026
3138
  canExpand: hasContent || isRunning,
3027
3139
  isRunning,
3140
+ autoExpandWhileRunning: false,
3028
3141
  statusTone: card.statusTone
3029
3142
  });
3030
3143
  return /* @__PURE__ */ jsxs(ToolCardRoot, { children: [/* @__PURE__ */ jsx(ToolCardHeader, {
@@ -3135,7 +3248,8 @@ const ChatMessage = memo(function ChatMessage(props) {
3135
3248
  }, `tool-${index}`);
3136
3249
  if (type === "file") return /* @__PURE__ */ jsx(ChatMessageFile, {
3137
3250
  file: part.file,
3138
- isUser
3251
+ isUser,
3252
+ texts
3139
3253
  }, `file-${index}`);
3140
3254
  if (type === "unknown") return /* @__PURE__ */ jsx(ChatUnknownPart, {
3141
3255
  label: part.label,