@nextclaw/agent-chat-ui 0.3.2 → 0.3.4

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
+ }
1237
+ //#endregion
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
+ }
1114
1345
  //#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;
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,569 +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.hasPendingImeComposition = false;
1428
- this.bindRootElement = (node) => {
1429
- this.rootElement = node;
1430
- };
1431
- this.update = (config) => {
1432
- this.config = config;
1433
- this.snapshot = this.viewController.sync(config.nodes, this.selectedRange);
1434
- return {
1435
- snapshot: this.snapshot,
1436
- selectedRange: this.selectedRange,
1437
- bindRootElement: this.bindRootElement,
1438
- handleBeforeInput: this.handleBeforeInput,
1439
- handleInput: this.handleInput,
1440
- handleCompositionStart: this.handleCompositionStart,
1441
- handleCompositionEnd: this.handleCompositionEnd,
1442
- handleKeyDown: this.handleKeyDown,
1443
- handlePaste: this.handlePaste,
1444
- handleBlur: this.handleBlur,
1445
- syncSelectionState: this.syncSelectionState,
1446
- imperativeHandle: this.createHandle()
1447
- };
1448
- };
1449
- this.createHandle = () => {
1450
- return {
1451
- insertSlashItem: (item) => {
1452
- this.config?.onSlashItemSelect?.(item);
1453
- this.viewController.insertSlashItem(item, this.commitSnapshot);
1454
- },
1455
- insertFileToken: (tokenKey, label) => {
1456
- this.commitSnapshot(this.controller.insertFileToken(tokenKey, label));
1457
- this.focusComposerSoon();
1458
- },
1459
- insertFileTokens: (tokens) => {
1460
- let nextSnapshot = null;
1461
- for (const token of tokens) nextSnapshot = this.controller.insertFileToken(token.tokenKey, token.label);
1462
- if (nextSnapshot) {
1463
- this.commitSnapshot(nextSnapshot);
1464
- this.focusComposerSoon();
1465
- }
1466
- },
1467
- focusComposer: () => {
1468
- this.focusComposer();
1469
- },
1470
- syncSelectedSkills: (nextKeys, options) => {
1471
- this.viewController.syncSelectedSkills(nextKeys, options, this.commitSnapshot);
1472
- }
1473
- };
1474
- };
1475
- this.restoreDomAfterCommit = () => {
1476
- if (this.shouldSuspendDomSync()) return;
1477
- this.viewController.restoreSelectionIfFocused(this.rootElement, this.selection);
1478
- };
1479
- this.renderSurface = () => {
1480
- if (this.shouldSuspendDomSync()) return;
1481
- this.viewController.renderSurface({
1482
- root: this.rootElement,
1483
- snapshot: this.snapshot,
1484
- selectedRange: this.selectedRange
1485
- });
1486
- };
1487
- this.syncViewport = () => {
1488
- this.viewController.syncViewport(this.rootElement);
1489
- };
1490
- this.syncSelectionState = () => {
1491
- if (!this.rootElement || this.shouldSuspendDomSync()) return;
1492
- const nextSnapshot = this.viewController.syncSelectionFromRoot(this.rootElement);
1493
- this.selection = nextSnapshot.selection;
1494
- this.selectedRange = nextSnapshot.selection;
1495
- this.syncSlashState(nextSnapshot);
1496
- this.requestRender();
1497
- };
1498
- this.handleBeforeInput = (event) => {
1499
- this.syncImeGuardFromInputEvent(event.nativeEvent);
1500
- this.viewController.handleBeforeInput({
1501
- event,
1502
- disabled: this.requireConfig().disabled,
1503
- isComposing: this.shouldSuspendDomSync(),
1504
- commitSnapshot: this.commitSnapshot
1505
- });
1506
- };
1507
- this.handleInput = (event) => {
1508
- this.syncImeGuardFromInputEvent(event.nativeEvent);
1509
- this.viewController.handleInput({
1510
- event,
1511
- isComposing: this.shouldSuspendDomSync(),
1512
- commitSnapshot: this.commitSnapshot
1513
- });
1514
- };
1515
- this.handleCompositionStart = () => {
1516
- this.hasPendingImeComposition = false;
1517
- this.isComposing = true;
1518
- };
1519
- this.handleCompositionEnd = (event) => {
1520
- this.hasPendingImeComposition = false;
1521
- this.isComposing = false;
1522
- this.viewController.handleCompositionEnd({
1523
- event,
1524
- commitSnapshot: this.commitSnapshot
1525
- });
1526
- };
1527
- this.handleKeyDown = (event) => {
1528
- const config = this.requireConfig();
1529
- this.syncImeGuardFromKeyboardEvent(event);
1530
- if (this.rootElement && !this.shouldSuspendDomSync()) {
1531
- const nextSnapshot = this.viewController.syncSelectionFromRoot(this.rootElement);
1532
- this.selection = nextSnapshot.selection;
1533
- this.selectedRange = nextSnapshot.selection;
1534
- this.snapshot = nextSnapshot;
1535
- }
1536
- const activeSlashItem = config.slashItems[config.activeSlashIndex] ?? null;
1537
- this.viewController.handleKeyDown({
1538
- event,
1539
- slashItems: config.slashItems,
1540
- activeSlashIndex: config.activeSlashIndex,
1541
- activeSlashItem,
1542
- actions: config.actions,
1543
- commitSnapshot: this.commitSnapshot,
1544
- insertSkillToken: this.insertSkillToken,
1545
- onSlashItemSelect: config.onSlashItemSelect,
1546
- onSlashActiveIndexChange: config.onSlashActiveIndexChange,
1547
- onSlashQueryChange: config.onSlashQueryChange,
1548
- onSlashOpenChange: config.onSlashOpenChange
1549
- });
1550
- };
1551
- this.handlePaste = (event) => {
1552
- this.viewController.handlePaste({
1553
- event,
1554
- onFilesAdd: this.requireConfig().onFilesAdd,
1555
- commitSnapshot: this.commitSnapshot
1556
- });
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;
1557
1526
  };
1558
- this.handleBlur = () => {
1559
- const config = this.requireConfig();
1560
- this.hasPendingImeComposition = false;
1561
- this.isComposing = false;
1562
- this.viewController.handleBlur({
1563
- clearSelectedRange: this.clearSelectedRange,
1564
- onSlashQueryChange: config.onSlashQueryChange,
1565
- 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);
1566
1551
  });
1567
- };
1568
- this.setSelectedRange = (selection) => {
1569
- this.selectedRange = selection;
1570
- this.selection = selection;
1571
- this.requestRender();
1572
- };
1573
- this.clearSelectedRange = () => {
1574
- this.selectedRange = null;
1575
- this.requestRender();
1576
- };
1577
- this.commitSnapshot = (nextSnapshot) => {
1578
- const config = this.requireConfig();
1579
- this.selection = nextSnapshot.selection;
1580
- this.selectedRange = nextSnapshot.selection;
1581
- this.snapshot = nextSnapshot;
1582
- config.onNodesChange(nextSnapshot.nodes);
1583
- this.syncSlashState(nextSnapshot);
1584
- };
1585
- this.insertSkillToken = (tokenKey, label) => {
1586
- this.commitSnapshot(this.controller.insertSkillToken(tokenKey, label));
1587
- };
1588
- this.syncSlashState = (nextSnapshot) => {
1589
- const config = this.requireConfig();
1590
- config.onSlashTriggerChange?.(nextSnapshot.slashTrigger);
1591
- config.onSlashQueryChange?.(nextSnapshot.slashTrigger?.query ?? null);
1592
- config.onSlashOpenChange(nextSnapshot.slashTrigger !== null);
1593
- };
1594
- this.requestRender = () => {
1595
- this.requireConfig().requestRender();
1596
- };
1597
- this.shouldSuspendDomSync = () => {
1598
- return this.isComposing || this.hasPendingImeComposition;
1599
- };
1600
- this.syncImeGuardFromKeyboardEvent = (event) => {
1601
- const { nativeEvent } = event;
1602
- const keyCode = nativeEvent.keyCode || nativeEvent.which || event.keyCode || event.which;
1603
- if (nativeEvent.isComposing || event.key === "Process" || event.key === "Unidentified" || keyCode === 229) {
1604
- this.hasPendingImeComposition = true;
1605
- return;
1606
- }
1607
- this.hasPendingImeComposition = false;
1608
- };
1609
- this.syncImeGuardFromInputEvent = (nativeEvent) => {
1610
- if (nativeEvent.isComposing || this.isCompositionInputType(nativeEvent.inputType)) {
1611
- this.hasPendingImeComposition = true;
1612
- return;
1613
- }
1614
- this.hasPendingImeComposition = false;
1615
- };
1616
- this.isCompositionInputType = (inputType) => {
1617
- return typeof inputType === "string" && inputType.toLowerCase().includes("composition");
1618
- };
1619
- this.focusComposerSoon = () => {
1620
- if (typeof requestAnimationFrame === "function") {
1621
- requestAnimationFrame(() => this.focusComposer());
1622
- return;
1623
- }
1624
- this.focusComposer();
1625
- };
1626
- this.focusComposer = () => {
1627
- if (!this.rootElement) return;
1628
- const targetSelection = this.selection;
1629
- this.rootElement.focus();
1630
- const restoreSelection = () => {
1631
- if (!this.rootElement) return;
1632
- this.selection = targetSelection;
1633
- this.selectedRange = targetSelection;
1634
- this.viewController.restoreSelectionIfFocused(this.rootElement, targetSelection);
1635
- };
1636
- if (typeof requestAnimationFrame === "function") {
1637
- requestAnimationFrame(restoreSelection);
1638
- return;
1639
- }
1640
- restoreSelection();
1641
- };
1642
- this.requireConfig = () => {
1643
- if (!this.config) throw new Error("ChatComposerRuntime is not configured.");
1644
- return this.config;
1645
- };
1646
- }
1647
- };
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
+ }
1648
1600
  //#endregion
1649
- //#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
1650
1602
  const ChatInputBarTokenizedComposer = forwardRef(function ChatInputBarTokenizedComposer(props, ref) {
1651
- const { nodes, placeholder, disabled, slashItems, onSlashItemSelect, actions, onNodesChange, onFilesAdd, onSlashQueryChange, onSlashTriggerChange, onSlashOpenChange, onSlashActiveIndexChange, activeSlashIndex } = props;
1652
- const [renderTick, setRenderTick] = useState(0);
1653
- const [runtime] = useState(() => new ChatComposerRuntime());
1654
- const { snapshot, bindRootElement, handleBeforeInput, handleInput, handleCompositionStart, handleCompositionEnd, handleKeyDown, handlePaste, handleBlur, syncSelectionState, imperativeHandle } = runtime.update({
1655
- nodes,
1656
- disabled,
1657
- slashItems,
1658
- onSlashItemSelect,
1659
- actions,
1660
- onNodesChange,
1661
- onFilesAdd,
1662
- onSlashQueryChange,
1663
- onSlashTriggerChange,
1664
- onSlashOpenChange,
1665
- onSlashActiveIndexChange,
1666
- activeSlashIndex,
1667
- 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()
1668
1649
  });
1669
- useImperativeHandle(ref, () => imperativeHandle, [imperativeHandle]);
1670
- useLayoutEffect(() => {
1671
- runtime.renderSurface();
1672
- runtime.restoreDomAfterCommit();
1673
- runtime.syncViewport();
1674
- }, [
1675
- runtime,
1676
- snapshot.nodes,
1677
- snapshot.nodeStartMap,
1678
- renderTick
1679
- ]);
1680
- return /* @__PURE__ */ jsx("div", {
1681
- className: "px-4 py-2.5",
1682
- children: /* @__PURE__ */ jsx("div", {
1683
- className: "min-h-[60px]",
1684
- children: /* @__PURE__ */ jsx("div", {
1685
- ref: bindRootElement,
1686
- contentEditable: !disabled,
1687
- suppressContentEditableWarning: true,
1688
- role: "textbox",
1689
- "aria-multiline": "true",
1690
- "data-placeholder": placeholder,
1691
- onBeforeInput: handleBeforeInput,
1692
- onInput: handleInput,
1693
- onCompositionStart: handleCompositionStart,
1694
- onCompositionEnd: handleCompositionEnd,
1695
- onKeyDown: handleKeyDown,
1696
- onKeyUp: syncSelectionState,
1697
- onMouseUp: syncSelectionState,
1698
- onFocus: syncSelectionState,
1699
- onBlur: handleBlur,
1700
- onPaste: handlePaste,
1701
- 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
1702
1754
  })
1703
- })
1755
+ ]
1704
1756
  });
1705
1757
  });
1758
+ ChatInputBarTokenizedComposer.displayName = "LexicalChatInputBarTokenizedComposer";
1706
1759
  //#endregion
1707
1760
  //#region src/components/chat/ui/chat-input-bar/chat-input-bar.tsx
1708
1761
  function InputBarHint({ hint }) {
@@ -1755,6 +1808,7 @@ const ChatInputBar = forwardRef(function ChatInputBar(props, ref) {
1755
1808
  ...props.toolbar.skillPicker,
1756
1809
  onSelectedKeysChange: (nextKeys) => {
1757
1810
  composerRef.current?.syncSelectedSkills(nextKeys, props.toolbar.skillPicker?.options ?? []);
1811
+ props.toolbar.skillPicker?.onSelectedKeysChange(nextKeys);
1758
1812
  }
1759
1813
  }
1760
1814
  };
@@ -2129,29 +2183,17 @@ function ChatMessageInlineContent({ segments, role, texts }) {
2129
2183
  }
2130
2184
  //#endregion
2131
2185
  //#region src/components/chat/ui/chat-message-list/chat-message-file/meta.ts
2132
- const FILE_CATEGORY_LABELS = {
2133
- archive: "Archive",
2134
- audio: "Audio",
2135
- code: "Code",
2136
- data: "Data",
2137
- document: "Document",
2138
- generic: "File",
2139
- image: "Image",
2140
- pdf: "PDF",
2141
- sheet: "Sheet",
2142
- video: "Video"
2143
- };
2144
2186
  const FILE_CATEGORY_TILE_CLASSES = {
2145
- archive: "border-amber-200/80 bg-amber-100 text-amber-700",
2146
- audio: "border-fuchsia-200/80 bg-fuchsia-100 text-fuchsia-700",
2147
- code: "border-cyan-200/80 bg-cyan-100 text-cyan-700",
2148
- data: "border-slate-200/80 bg-slate-100 text-slate-700",
2149
- document: "border-blue-200/80 bg-blue-100 text-blue-700",
2150
- generic: "border-slate-200/80 bg-slate-100 text-slate-700",
2151
- image: "border-emerald-200/80 bg-emerald-100 text-emerald-700",
2152
- pdf: "border-rose-200/80 bg-rose-100 text-rose-700",
2153
- sheet: "border-lime-200/80 bg-lime-100 text-lime-700",
2154
- 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"
2155
2197
  };
2156
2198
  const CODE_EXTENSIONS = new Set([
2157
2199
  "c",
@@ -2278,22 +2320,30 @@ function buildChatMessageFileMeta(file) {
2278
2320
  return {
2279
2321
  category,
2280
2322
  extension: getFileExtension(file.label, file.mimeType),
2281
- sizeLabel,
2282
- metaBadges: [FILE_CATEGORY_LABELS[category], sizeLabel].filter((value) => Boolean(value))
2323
+ sizeLabel
2283
2324
  };
2284
2325
  }
2285
2326
  //#endregion
2286
2327
  //#region src/components/chat/ui/chat-message-list/chat-message-file/index.tsx
2287
- function renderMetaBadge(label, isUser) {
2288
- return /* @__PURE__ */ jsx("span", {
2289
- 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"),
2290
- children: label
2291
- }, 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];
2292
2342
  }
2293
- function renderMimeType(mimeType, isUser) {
2343
+ function renderMetaLine(categoryLabel, sizeLabel, isUser) {
2294
2344
  return /* @__PURE__ */ jsx("div", {
2295
- className: cn("truncate text-[11px]", isUser ? "text-white/58" : "text-slate-500"),
2296
- children: mimeType
2345
+ className: cn("mt-1 text-xs leading-5", isUser ? "text-white/70" : "text-slate-500"),
2346
+ children: sizeLabel ? `${categoryLabel} · ${sizeLabel}` : categoryLabel
2297
2347
  });
2298
2348
  }
2299
2349
  function renderActionPill(label, isUser, isInteractive) {
@@ -2302,18 +2352,39 @@ function renderActionPill(label, isUser, isInteractive) {
2302
2352
  children: label
2303
2353
  });
2304
2354
  }
2305
- function ChatMessageFile({ file, isUser = false }) {
2306
- 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);
2307
2379
  const renderAsImage = isImageFileLike(file) && Boolean(file.dataUrl);
2308
2380
  const isInteractive = Boolean(file.dataUrl);
2309
- const actionLabel = isInteractive ? renderAsImage ? "Open preview" : "Open file" : "Attached";
2310
- const tileLabel = extension.slice(0, 4);
2311
- 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"));
2312
2384
  if (renderAsImage && file.dataUrl) return /* @__PURE__ */ jsx("a", {
2313
2385
  href: file.dataUrl,
2314
2386
  target: "_blank",
2315
2387
  rel: "noreferrer",
2316
- "aria-label": `Open preview: ${file.label}`,
2317
2388
  className: "group block",
2318
2389
  children: /* @__PURE__ */ jsxs("div", {
2319
2390
  className: cn("relative overflow-hidden rounded-[1rem]", isUser ? "ring-1 ring-white/10" : "bg-slate-100/80 ring-1 ring-slate-200/80"),
@@ -2325,7 +2396,7 @@ function ChatMessageFile({ file, isUser = false }) {
2325
2396
  className: "pointer-events-none absolute right-3 top-3 flex items-center gap-2",
2326
2397
  children: [/* @__PURE__ */ jsx("span", {
2327
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"),
2328
- children: tileLabel
2399
+ children: categoryLabel
2329
2400
  }), sizeLabel ? /* @__PURE__ */ jsx("span", {
2330
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"),
2331
2402
  children: sizeLabel
@@ -2334,35 +2405,22 @@ function ChatMessageFile({ file, isUser = false }) {
2334
2405
  })
2335
2406
  });
2336
2407
  const content = /* @__PURE__ */ jsxs("div", {
2337
- className: "flex items-start gap-3 p-3.5",
2338
- children: [/* @__PURE__ */ jsx("div", {
2339
- 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]),
2340
- children: tileLabel
2341
- }), /* @__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", {
2342
2413
  className: "min-w-0 flex-1",
2343
- children: [
2344
- /* @__PURE__ */ jsxs("div", {
2345
- className: "flex items-start gap-3",
2346
- children: [/* @__PURE__ */ jsxs("div", {
2347
- className: "min-w-0 flex-1",
2348
- children: [/* @__PURE__ */ jsx("div", {
2349
- className: "truncate text-sm font-semibold leading-5",
2350
- children: file.label
2351
- }), /* @__PURE__ */ jsxs("div", {
2352
- className: cn("mt-1 text-xs", isUser ? "text-white/68" : "text-slate-500"),
2353
- children: [FILE_CATEGORY_LABELS[category], " attachment"]
2354
- })]
2355
- }), renderActionPill(actionLabel, isUser, isInteractive)]
2356
- }),
2357
- /* @__PURE__ */ jsx("div", {
2358
- className: "mt-3 flex flex-wrap gap-2",
2359
- children: metaBadges.map((label) => renderMetaBadge(label, isUser))
2360
- }),
2361
- /* @__PURE__ */ jsx("div", {
2362
- className: "mt-2",
2363
- children: renderMimeType(file.mimeType, isUser)
2364
- })
2365
- ]
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
+ })
2366
2424
  })]
2367
2425
  });
2368
2426
  if (!isInteractive) return /* @__PURE__ */ jsx("div", {
@@ -2373,7 +2431,6 @@ function ChatMessageFile({ file, isUser = false }) {
2373
2431
  href: file.dataUrl,
2374
2432
  target: "_blank",
2375
2433
  rel: "noreferrer",
2376
- "aria-label": `Open file: ${file.label}`,
2377
2434
  className: cn(shellClasses, "group"),
2378
2435
  children: content
2379
2436
  });
@@ -2409,19 +2466,113 @@ function useReasoningBlockOpenState(params) {
2409
2466
  };
2410
2467
  }
2411
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
2412
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
+ }
2413
2548
  function ChatReasoningBlock({ label, text, isUser, isInProgress }) {
2414
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
+ });
2415
2560
  return /* @__PURE__ */ jsxs("details", {
2416
2561
  className: "mt-2",
2417
2562
  open: isOpen,
2418
2563
  children: [/* @__PURE__ */ jsx("summary", {
2419
2564
  className: cn("cursor-pointer text-xs", isUser ? "text-primary-100" : "text-gray-500"),
2420
2565
  onClick: onSummaryClick,
2421
- children: label
2422
- }), /* @__PURE__ */ jsx("pre", {
2423
- 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"),
2424
- 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
+ })
2425
2576
  })]
2426
2577
  });
2427
2578
  }
@@ -2541,79 +2692,6 @@ function ToolCardHeaderSessionAction({ action, onAction }) {
2541
2692
  });
2542
2693
  }
2543
2694
  //#endregion
2544
- //#region src/components/chat/hooks/use-sticky-bottom-scroll.ts
2545
- const DEFAULT_STICKY_THRESHOLD_PX = 10;
2546
- function scrollElementToBottom(element) {
2547
- element.scrollTop = element.scrollHeight;
2548
- }
2549
- function queueScrollToBottom(params) {
2550
- if (!params.scrollRef.current) return;
2551
- if (params.scheduledScrollFrameRef.current !== null) cancelAnimationFrame(params.scheduledScrollFrameRef.current);
2552
- params.scheduledScrollFrameRef.current = requestAnimationFrame(() => {
2553
- params.scheduledScrollFrameRef.current = null;
2554
- const currentElement = params.scrollRef.current;
2555
- if (!currentElement) return;
2556
- params.isProgrammaticScrollRef.current = true;
2557
- scrollElementToBottom(currentElement);
2558
- });
2559
- }
2560
- function useStickyBottomScroll(params) {
2561
- const isStickyRef = useRef(true);
2562
- const isProgrammaticScrollRef = useRef(false);
2563
- const previousResetKeyRef = useRef(null);
2564
- const pendingInitialScrollRef = useRef(false);
2565
- const scheduledScrollFrameRef = useRef(null);
2566
- const onScroll = () => {
2567
- if (isProgrammaticScrollRef.current) {
2568
- isProgrammaticScrollRef.current = false;
2569
- return;
2570
- }
2571
- const element = params.scrollRef.current;
2572
- if (!element) return;
2573
- isStickyRef.current = element.scrollHeight - element.scrollTop - element.clientHeight <= (params.stickyThresholdPx ?? DEFAULT_STICKY_THRESHOLD_PX);
2574
- };
2575
- useEffect(() => {
2576
- if (previousResetKeyRef.current === params.resetKey) return;
2577
- previousResetKeyRef.current = params.resetKey;
2578
- isStickyRef.current = true;
2579
- pendingInitialScrollRef.current = true;
2580
- }, [params.resetKey]);
2581
- useEffect(() => {
2582
- const scheduledScrollFrame = scheduledScrollFrameRef.current;
2583
- return () => {
2584
- if (scheduledScrollFrame !== null) cancelAnimationFrame(scheduledScrollFrame);
2585
- };
2586
- }, []);
2587
- useLayoutEffect(() => {
2588
- if (!pendingInitialScrollRef.current || params.isLoading || !params.hasContent) return;
2589
- if (!params.scrollRef.current) return;
2590
- pendingInitialScrollRef.current = false;
2591
- queueScrollToBottom({
2592
- scrollRef: params.scrollRef,
2593
- scheduledScrollFrameRef,
2594
- isProgrammaticScrollRef
2595
- });
2596
- }, [
2597
- params.hasContent,
2598
- params.isLoading,
2599
- params.scrollRef
2600
- ]);
2601
- useLayoutEffect(() => {
2602
- if (!isStickyRef.current || !params.hasContent) return;
2603
- if (!params.scrollRef.current) return;
2604
- queueScrollToBottom({
2605
- scrollRef: params.scrollRef,
2606
- scheduledScrollFrameRef,
2607
- isProgrammaticScrollRef
2608
- });
2609
- }, [
2610
- params.contentVersion,
2611
- params.hasContent,
2612
- params.scrollRef
2613
- ]);
2614
- return { onScroll };
2615
- }
2616
- //#endregion
2617
2695
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-file-operation-lines.tsx
2618
2696
  const FILE_ROW_CLASS_NAME = "h-5 font-mono text-[11px] leading-5";
2619
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";
@@ -2862,6 +2940,9 @@ function normalizeTerminalOutput(rawOutput, structuredOutput) {
2862
2940
  return stripAnsi(rawOutput);
2863
2941
  }
2864
2942
  }
2943
+ function shouldAutoExpandRunningFileOperation(toolName) {
2944
+ return toolName === "write_file" || toolName === "edit_file";
2945
+ }
2865
2946
  function useToolCardExpandedState({ canExpand, isRunning, autoExpandWhileRunning = true, expandOnError = false, statusTone }) {
2866
2947
  const [expanded, setExpanded] = useState(false);
2867
2948
  const [hasUserToggled, setHasUserToggled] = useState(false);
@@ -2962,6 +3043,7 @@ function TerminalExecutionView({ card }) {
2962
3043
  const { expanded, onToggle } = useToolCardExpandedState({
2963
3044
  canExpand: output.trim().length > 0 || isRunning,
2964
3045
  isRunning,
3046
+ autoExpandWhileRunning: false,
2965
3047
  expandOnError: hasContent,
2966
3048
  statusTone: card.statusTone
2967
3049
  });
@@ -3003,7 +3085,7 @@ function FileOperationView({ card }) {
3003
3085
  if (block.rawText) return count + block.rawText.length;
3004
3086
  return count + block.lines.reduce((lineCount, line) => lineCount + line.text.length + 1, 0);
3005
3087
  }, 0);
3006
- 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));
3007
3089
  const { expanded, onToggle } = useToolCardExpandedState({
3008
3090
  canExpand: hasContent || isRunning,
3009
3091
  isRunning,
@@ -3029,6 +3111,7 @@ function SearchSnippetView({ card }) {
3029
3111
  const { expanded, onToggle } = useToolCardExpandedState({
3030
3112
  canExpand: !!output || isRunning,
3031
3113
  isRunning,
3114
+ autoExpandWhileRunning: false,
3032
3115
  statusTone: card.statusTone
3033
3116
  });
3034
3117
  return /* @__PURE__ */ jsxs(ToolCardRoot, { children: [/* @__PURE__ */ jsx(ToolCardHeader, {
@@ -3054,6 +3137,7 @@ function GenericToolCard({ card, onToolAction, renderToolAgent }) {
3054
3137
  const { expanded, onToggle } = useToolCardExpandedState({
3055
3138
  canExpand: hasContent || isRunning,
3056
3139
  isRunning,
3140
+ autoExpandWhileRunning: false,
3057
3141
  statusTone: card.statusTone
3058
3142
  });
3059
3143
  return /* @__PURE__ */ jsxs(ToolCardRoot, { children: [/* @__PURE__ */ jsx(ToolCardHeader, {
@@ -3164,7 +3248,8 @@ const ChatMessage = memo(function ChatMessage(props) {
3164
3248
  }, `tool-${index}`);
3165
3249
  if (type === "file") return /* @__PURE__ */ jsx(ChatMessageFile, {
3166
3250
  file: part.file,
3167
- isUser
3251
+ isUser,
3252
+ texts
3168
3253
  }, `file-${index}`);
3169
3254
  if (type === "unknown") return /* @__PURE__ */ jsx(ChatUnknownPart, {
3170
3255
  label: part.label,