authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-02 00:52:56-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-02 14:16:08-08:00
logbf7002209e6663d6c208c6dd0546277582077aea
treef0633b45fb5f815fc61389ffd8753ef3685f3b4a
parenta3aa4bd189721a87271e9d7a1bed0cb605bc5eee

Add Lexical-based composer editor with file mention pills

Introduce ComposerPromptEditor component using Lexical for rich text editing with @-mention support for files. Includes mention detection utilities and tests. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

7 files changed, 949 insertions(+), 133 deletions(-)

apps/web/package.json+2
...@@ -12,6 +12,7 @@...@@ -12,6 +12,7 @@
12 },12 },
13 "dependencies": {13 "dependencies": {
14 "@base-ui/react": "^1.2.0",14 "@base-ui/react": "^1.2.0",
15 "@lexical/react": "^0.41.0",
15 "@pierre/diffs": "^1.1.0-beta.16",16 "@pierre/diffs": "^1.1.0-beta.16",
16 "@t3tools/contracts": "workspace:*",17 "@t3tools/contracts": "workspace:*",
17 "@tanstack/react-pacer": "^0.19.4",18 "@tanstack/react-pacer": "^0.19.4",
...@@ -22,6 +23,7 @@...@@ -22,6 +23,7 @@
22 "@xterm/xterm": "^6.0.0",23 "@xterm/xterm": "^6.0.0",
23 "class-variance-authority": "^0.7.1",24 "class-variance-authority": "^0.7.1",
24 "effect": "catalog:",25 "effect": "catalog:",
26 "lexical": "^0.41.0",
25 "lucide-react": "^0.564.0",27 "lucide-react": "^0.564.0",
26 "react": "^19.0.0",28 "react": "^19.0.0",
27 "react-dom": "^19.0.0",29 "react-dom": "^19.0.0",
apps/web/src/components/ChatView.tsx+138-93
...@@ -37,6 +37,7 @@ import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuer...@@ -37,6 +37,7 @@ import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuer
37import { isElectron } from "../env";37import { isElectron } from "../env";
38import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";38import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";
39import {39import {
40 type ComposerTrigger,
40 type ComposerTriggerKind,41 type ComposerTriggerKind,
41 detectComposerTrigger,42 detectComposerTrigger,
42 replaceTextRange,43 replaceTextRange,
...@@ -127,6 +128,7 @@ import {...@@ -127,6 +128,7 @@ import {
127} from "../composerDraftStore";128} from "../composerDraftStore";
128import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";129import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";
129import { clamp } from "effect/Number";130import { clamp } from "effect/Number";
131import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "./ComposerPromptEditor";
130132
131function formatMessageMeta(createdAt: string, duration: string | null): string {133function formatMessageMeta(createdAt: string, duration: string | null): string {
132 if (!duration) return formatTimestamp(createdAt);134 if (!duration) return formatTimestamp(createdAt);
...@@ -458,10 +460,8 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -458,10 +460,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
458 const composerDraft = useComposerThreadDraft(threadId);460 const composerDraft = useComposerThreadDraft(threadId);
459 const prompt = composerDraft.prompt;461 const prompt = composerDraft.prompt;
460 const composerImages = composerDraft.images;462 const composerImages = composerDraft.images;
461 const composerCursor = composerDraft.cursor;
462 const nonPersistedComposerImageIds = composerDraft.nonPersistedImageIds;463 const nonPersistedComposerImageIds = composerDraft.nonPersistedImageIds;
463 const setComposerDraftPrompt = useComposerDraftStore((store) => store.setPrompt);464 const setComposerDraftPrompt = useComposerDraftStore((store) => store.setPrompt);
464 const setComposerDraftCursor = useComposerDraftStore((store) => store.setCursor);
465 const setComposerDraftModel = useComposerDraftStore((store) => store.setModel);465 const setComposerDraftModel = useComposerDraftStore((store) => store.setModel);
466 const setComposerDraftEffort = useComposerDraftStore((store) => store.setEffort);466 const setComposerDraftEffort = useComposerDraftStore((store) => store.setEffort);
467 const addComposerDraftImage = useComposerDraftStore((store) => store.addImage);467 const addComposerDraftImage = useComposerDraftStore((store) => store.addImage);
...@@ -500,6 +500,10 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -500,6 +500,10 @@ export default function ChatView({ threadId }: ChatViewProps) {
500 const [attachmentPreviewHandoffByMessageId, setAttachmentPreviewHandoffByMessageId] = useState<500 const [attachmentPreviewHandoffByMessageId, setAttachmentPreviewHandoffByMessageId] = useState<
501 Record<string, string[]>501 Record<string, string[]>
502 >({});502 >({});
503 const [composerCursor, setComposerCursor] = useState(() => prompt.length);
504 const [composerTrigger, setComposerTrigger] = useState<ComposerTrigger | null>(() =>
505 detectComposerTrigger(prompt, prompt.length),
506 );
503 const [lastInvokedScriptByProjectId, setLastInvokedScriptByProjectId] = useState<507 const [lastInvokedScriptByProjectId, setLastInvokedScriptByProjectId] = useState<
504 Record<string, string>508 Record<string, string>
505 >(() => readLastInvokedScriptByProjectFromStorage());509 >(() => readLastInvokedScriptByProjectFromStorage());
...@@ -507,10 +511,15 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -507,10 +511,15 @@ export default function ChatView({ threadId }: ChatViewProps) {
507 const [messagesScrollElement, setMessagesScrollElement] = useState<HTMLDivElement | null>(null);511 const [messagesScrollElement, setMessagesScrollElement] = useState<HTMLDivElement | null>(null);
508 const shouldAutoScrollRef = useRef(true);512 const shouldAutoScrollRef = useRef(true);
509 const pendingAutoScrollFrameRef = useRef<number | null>(null);513 const pendingAutoScrollFrameRef = useRef<number | null>(null);
510 const textareaRef = useRef<HTMLTextAreaElement>(null);514 const composerEditorRef = useRef<ComposerPromptEditorHandle>(null);
515 const composerCommandInputRef = useRef<HTMLInputElement>(null);
511 const composerFormRef = useRef<HTMLFormElement>(null);516 const composerFormRef = useRef<HTMLFormElement>(null);
512 const composerFormHeightRef = useRef(0);517 const composerFormHeightRef = useRef(0);
513 const composerImagesRef = useRef<ComposerImageAttachment[]>([]);518 const composerImagesRef = useRef<ComposerImageAttachment[]>([]);
519 const composerSelectLockRef = useRef(false);
520 const composerMenuOpenRef = useRef(false);
521 const composerMenuItemsRef = useRef<ComposerCommandItem[]>([]);
522 const activeComposerMenuItemRef = useRef<ComposerCommandItem | null>(null);
514 const attachmentPreviewHandoffByMessageIdRef = useRef<Record<string, string[]>>({});523 const attachmentPreviewHandoffByMessageIdRef = useRef<Record<string, string[]>>({});
515 const attachmentPreviewHandoffTimeoutByMessageIdRef = useRef<Record<string, number>>({});524 const attachmentPreviewHandoffTimeoutByMessageIdRef = useRef<Record<string, number>>({});
516 const sendInFlightRef = useRef(false);525 const sendInFlightRef = useRef(false);
...@@ -537,12 +546,6 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -537,12 +546,6 @@ export default function ChatView({ threadId }: ChatViewProps) {
537 },546 },
538 [setComposerDraftPrompt, threadId],547 [setComposerDraftPrompt, threadId],
539 );548 );
540 const setComposerCursor = useCallback(
541 (nextCursor: number) => {
542 setComposerDraftCursor(threadId, nextCursor);
543 },
544 [setComposerDraftCursor, threadId],
545 );
546 const addComposerImage = useCallback(549 const addComposerImage = useCallback(
547 (image: ComposerImageAttachment) => {550 (image: ComposerImageAttachment) => {
548 addComposerDraftImage(threadId, image);551 addComposerDraftImage(threadId, image);
...@@ -845,10 +848,6 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -845,10 +848,6 @@ export default function ChatView({ threadId }: ChatViewProps) {
845 timelineEntries,848 timelineEntries,
846 ]);849 ]);
847 const gitCwd = activeThread?.worktreePath ?? activeProject?.cwd ?? null;850 const gitCwd = activeThread?.worktreePath ?? activeProject?.cwd ?? null;
848 const composerTrigger = useMemo(
849 () => detectComposerTrigger(prompt, composerCursor),
850 [prompt, composerCursor],
851 );
852 const composerTriggerKind = composerTrigger?.kind ?? null;851 const composerTriggerKind = composerTrigger?.kind ?? null;
853 const pathTriggerQuery = composerTrigger?.kind === "path" ? composerTrigger.query : "";852 const pathTriggerQuery = composerTrigger?.kind === "path" ? composerTrigger.query : "";
854 const isPathTrigger = composerTriggerKind === "path";853 const isPathTrigger = composerTriggerKind === "path";
...@@ -927,6 +926,9 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -927,6 +926,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
927 null,926 null,
928 [composerHighlightedItemId, composerMenuItems],927 [composerHighlightedItemId, composerMenuItems],
929 );928 );
929 composerMenuOpenRef.current = composerMenuOpen;
930 composerMenuItemsRef.current = composerMenuItems;
931 activeComposerMenuItemRef.current = activeComposerMenuItem;
930 const nonPersistedComposerImageIdSet = useMemo(932 const nonPersistedComposerImageIdSet = useMemo(
931 () => new Set(nonPersistedComposerImageIds),933 () => new Set(nonPersistedComposerImageIds),
932 [nonPersistedComposerImageIds],934 [nonPersistedComposerImageIds],
...@@ -1000,11 +1002,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1000,11 +1002,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
1000 );1002 );
10011003
1002 const focusComposer = useCallback(() => {1004 const focusComposer = useCallback(() => {
1003 const textarea = textareaRef.current;1005 composerEditorRef.current?.focusAtEnd();
1004 if (!textarea) return;
1005 textarea.focus();
1006 const cursor = textarea.value.length;
1007 textarea.setSelectionRange(cursor, cursor);
1008 }, []);1006 }, []);
1009 const scheduleComposerFocus = useCallback(() => {1007 const scheduleComposerFocus = useCallback(() => {
1010 window.requestAnimationFrame(() => {1008 window.requestAnimationFrame(() => {
...@@ -1434,8 +1432,25 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1434,8 +1432,25 @@ export default function ChatView({ threadId }: ChatViewProps) {
14341432
1435 useEffect(() => {1433 useEffect(() => {
1436 promptRef.current = prompt;1434 promptRef.current = prompt;
1435 setComposerCursor((existing) => Math.min(Math.max(0, existing), prompt.length));
1437 }, [prompt]);1436 }, [prompt]);
14381437
1438 useEffect(() => {
1439 setOptimisticUserMessages((existing) => {
1440 for (const message of existing) {
1441 revokeUserMessagePreviewUrls(message);
1442 }
1443 return [];
1444 });
1445 setSendPhase("idle");
1446 setComposerHighlightedItemId(null);
1447 setComposerCursor(promptRef.current.length);
1448 setComposerTrigger(detectComposerTrigger(promptRef.current, promptRef.current.length));
1449 dragDepthRef.current = 0;
1450 setIsDragOverComposer(false);
1451 setExpandedImage(null);
1452 }, [threadId]);
1453
1439 useEffect(() => {1454 useEffect(() => {
1440 let cancelled = false;1455 let cancelled = false;
1441 void (async () => {1456 void (async () => {
...@@ -1557,14 +1572,6 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1557,14 +1572,6 @@ export default function ChatView({ threadId }: ChatViewProps) {
1557 ? (draftThread?.envMode ?? "local")1572 ? (draftThread?.envMode ?? "local")
1558 : "local";1573 : "local";
15591574
1560 // Auto-resize textarea
1561 useEffect(() => {
1562 const ta = textareaRef.current;
1563 if (!ta) return;
1564 ta.style.height = "auto";
1565 ta.style.height = `${Math.min(ta.scrollHeight, 200)}px`;
1566 }, [prompt]);
1567
1568 useEffect(() => {1575 useEffect(() => {
1569 if (phase !== "running") return;1576 if (phase !== "running") return;
1570 const timer = window.setInterval(() => {1577 const timer = window.setInterval(() => {
...@@ -1727,7 +1734,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1727,7 +1734,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
1727 removeComposerImageFromDraft(imageId);1734 removeComposerImageFromDraft(imageId);
1728 };1735 };
17291736
1730 const onComposerPaste = (event: React.ClipboardEvent<HTMLTextAreaElement>) => {1737 const onComposerPaste = (event: React.ClipboardEvent<HTMLElement>) => {
1731 const files = Array.from(event.clipboardData.files);1738 const files = Array.from(event.clipboardData.files);
1732 if (files.length === 0) {1739 if (files.length === 0) {
1733 return;1740 return;
...@@ -1826,8 +1833,8 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1826,8 +1833,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
1826 [activeThread, isConnecting, isRevertingCheckpoint, isSendBusy, phase, setThreadError],1833 [activeThread, isConnecting, isRevertingCheckpoint, isSendBusy, phase, setThreadError],
1827 );1834 );
18281835
1829 const onSend = async (e: React.SubmitEvent | React.KeyboardEvent) => {1836 const onSend = async (e?: { preventDefault: () => void }) => {
1830 e.preventDefault();1837 e?.preventDefault();
1831 const api = readNativeApi();1838 const api = readNativeApi();
1832 if (!api || !activeThread || isSendBusy || isConnecting || sendInFlightRef.current) return;1839 if (!api || !activeThread || isSendBusy || isConnecting || sendInFlightRef.current) return;
1833 const trimmed = prompt.trim();1840 const trimmed = prompt.trim();
...@@ -1894,6 +1901,8 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1894,6 +1901,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
1894 promptRef.current = "";1901 promptRef.current = "";
1895 clearComposerDraftContent(threadIdForSend);1902 clearComposerDraftContent(threadIdForSend);
1896 setComposerHighlightedItemId(null);1903 setComposerHighlightedItemId(null);
1904 setComposerCursor(0);
1905 setComposerTrigger(null);
18971906
1898 let createdServerThreadForLocalDraft = false;1907 let createdServerThreadForLocalDraft = false;
1899 let turnStartSucceeded = false;1908 let turnStartSucceeded = false;
...@@ -2050,8 +2059,9 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -2050,8 +2059,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
2050 });2059 });
2051 promptRef.current = trimmed;2060 promptRef.current = trimmed;
2052 setPrompt(trimmed);2061 setPrompt(trimmed);
2053 addComposerImagesToDraft(composerImagesSnapshot.map(cloneComposerImageForRetry));
2054 setComposerCursor(trimmed.length);2062 setComposerCursor(trimmed.length);
2063 addComposerImagesToDraft(composerImagesSnapshot.map(cloneComposerImageForRetry));
2064 setComposerTrigger(detectComposerTrigger(trimmed, trimmed.length));
2055 }2065 }
2056 setThreadError(2066 setThreadError(
2057 threadIdForSend,2067 threadIdForSend,
...@@ -2144,40 +2154,96 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -2144,40 +2154,96 @@ export default function ChatView({ threadId }: ChatViewProps) {
2144 );2154 );
21452155
2146 const applyPromptReplacement = useCallback(2156 const applyPromptReplacement = useCallback(
2147 (rangeStart: number, rangeEnd: number, replacement: string) => {2157 (
2158 rangeStart: number,
2159 rangeEnd: number,
2160 replacement: string,
2161 options?: { expectedText?: string },
2162 ): boolean => {
2163 const currentText = promptRef.current;
2164 const safeStart = Math.max(0, Math.min(currentText.length, rangeStart));
2165 const safeEnd = Math.max(safeStart, Math.min(currentText.length, rangeEnd));
2166 if (
2167 options?.expectedText !== undefined &&
2168 currentText.slice(safeStart, safeEnd) !== options.expectedText
2169 ) {
2170 return false;
2171 }
2148 const next = replaceTextRange(promptRef.current, rangeStart, rangeEnd, replacement);2172 const next = replaceTextRange(promptRef.current, rangeStart, rangeEnd, replacement);
2149 promptRef.current = next.text;2173 promptRef.current = next.text;
2150 setPrompt(next.text);2174 setPrompt(next.text);
2175 setComposerCursor(next.cursor);
2176 setComposerTrigger(detectComposerTrigger(next.text, next.cursor));
2151 window.requestAnimationFrame(() => {2177 window.requestAnimationFrame(() => {
2152 const textarea = textareaRef.current;2178 composerEditorRef.current?.focusAt(next.cursor);
2153 if (!textarea) return;
2154 textarea.focus();
2155 textarea.setSelectionRange(next.cursor, next.cursor);
2156 setComposerCursor(next.cursor);
2157 });2179 });
2180 return true;
2158 },2181 },
2159 [setComposerCursor, setPrompt],2182 [setPrompt],
2160 );2183 );
21612184
2185 const readComposerSnapshot = useCallback((): { value: string; cursor: number } => {
2186 const editorSnapshot = composerEditorRef.current?.readSnapshot();
2187 if (editorSnapshot) {
2188 return editorSnapshot;
2189 }
2190 return { value: promptRef.current, cursor: composerCursor };
2191 }, [composerCursor]);
2192
2193 const resolveActiveComposerTrigger = useCallback((): {
2194 snapshot: { value: string; cursor: number };
2195 trigger: ComposerTrigger | null;
2196 } => {
2197 const snapshot = readComposerSnapshot();
2198 return {
2199 snapshot,
2200 trigger: detectComposerTrigger(snapshot.value, snapshot.cursor),
2201 };
2202 }, [readComposerSnapshot]);
2203
2162 const onSelectComposerItem = useCallback(2204 const onSelectComposerItem = useCallback(
2163 (item: ComposerCommandItem) => {2205 (item: ComposerCommandItem) => {
2164 if (!composerTrigger) return;2206 if (composerSelectLockRef.current) return;
2207 composerSelectLockRef.current = true;
2208 window.requestAnimationFrame(() => {
2209 composerSelectLockRef.current = false;
2210 });
2211 const { snapshot, trigger } = resolveActiveComposerTrigger();
2212 if (!trigger) return;
2213 const expectedToken = snapshot.value.slice(trigger.rangeStart, trigger.rangeEnd);
2165 if (item.type === "path") {2214 if (item.type === "path") {
2166 applyPromptReplacement(2215 const applied = applyPromptReplacement(
2167 composerTrigger.rangeStart,2216 trigger.rangeStart,
2168 composerTrigger.rangeEnd,2217 trigger.rangeEnd,
2169 `@${item.path} `,2218 `@${item.path} `,
2219 { expectedText: expectedToken },
2170 );2220 );
2221 if (applied) {
2222 setComposerHighlightedItemId(null);
2223 }
2171 return;2224 return;
2172 }2225 }
2173 if (item.type === "slash-command") {2226 if (item.type === "slash-command") {
2174 applyPromptReplacement(composerTrigger.rangeStart, composerTrigger.rangeEnd, "/model ");2227 const applied = applyPromptReplacement(
2228 trigger.rangeStart,
2229 trigger.rangeEnd,
2230 "/model ",
2231 { expectedText: expectedToken },
2232 );
2233 if (applied) {
2234 setComposerHighlightedItemId(null);
2235 }
2175 return;2236 return;
2176 }2237 }
2177 onModelSelect(item.model);2238 onModelSelect(item.model);
2178 applyPromptReplacement(composerTrigger.rangeStart, composerTrigger.rangeEnd, "");2239 const applied = applyPromptReplacement(trigger.rangeStart, trigger.rangeEnd, "", {
2240 expectedText: expectedToken,
2241 });
2242 if (applied) {
2243 setComposerHighlightedItemId(null);
2244 }
2179 },2245 },
2180 [applyPromptReplacement, composerTrigger, onModelSelect],2246 [applyPromptReplacement, onModelSelect, resolveActiveComposerTrigger],
2181 );2247 );
2182 const onComposerMenuItemHighlighted = useCallback((itemId: string | null) => {2248 const onComposerMenuItemHighlighted = useCallback((itemId: string | null) => {
2183 setComposerHighlightedItemId(itemId);2249 setComposerHighlightedItemId(itemId);
...@@ -2206,41 +2272,41 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -2206,41 +2272,41 @@ export default function ChatView({ threadId }: ChatViewProps) {
2206 workspaceEntriesQuery.isLoading ||2272 workspaceEntriesQuery.isLoading ||
2207 workspaceEntriesQuery.isFetching);2273 workspaceEntriesQuery.isFetching);
22082274
2209 const onPromptChange = useCallback(2275 const onPromptChange = useCallback((nextPrompt: string, nextCursor: number) => {
2210 (nextPrompt: string, nextCursor: number) => {2276 promptRef.current = nextPrompt;
2211 promptRef.current = nextPrompt;2277 setPrompt(nextPrompt);
2212 setPrompt(nextPrompt);2278 setComposerCursor(nextCursor);
2213 setComposerCursor(nextCursor);2279 setComposerTrigger(detectComposerTrigger(nextPrompt, nextCursor));
2214 },2280 }, [setPrompt]);
2215 [setComposerCursor, setPrompt],2281
2216 );2282 const onComposerCommandKey = (key: "ArrowDown" | "ArrowUp" | "Enter" | "Tab", event: KeyboardEvent) => {
2283 const { trigger } = resolveActiveComposerTrigger();
2284 const menuIsActive = composerMenuOpenRef.current || trigger !== null;
22172285
2218 const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {2286 if (menuIsActive) {
2219 if (composerMenuOpen && composerMenuItems.length > 0) {2287 const currentItems = composerMenuItemsRef.current;
2220 if (e.key === "ArrowDown") {2288 if (key === "ArrowDown" && currentItems.length > 0) {
2221 e.preventDefault();
2222 nudgeComposerMenuHighlight("ArrowDown");2289 nudgeComposerMenuHighlight("ArrowDown");
2223 return;2290 return true;
2224 }2291 }
2225 if (e.key === "ArrowUp") {2292 if (key === "ArrowUp" && currentItems.length > 0) {
2226 e.preventDefault();
2227 nudgeComposerMenuHighlight("ArrowUp");2293 nudgeComposerMenuHighlight("ArrowUp");
2228 return;2294 return true;
2229 }2295 }
2230 if (e.key === "Tab" || e.key === "Enter") {2296 if (key === "Tab" || key === "Enter") {
2231 const selectedItem = activeComposerMenuItem ?? composerMenuItems[0];2297 const selectedItem = activeComposerMenuItemRef.current ?? currentItems[0];
2232 if (selectedItem) {2298 if (selectedItem) {
2233 e.preventDefault();
2234 onSelectComposerItem(selectedItem);2299 onSelectComposerItem(selectedItem);
2235 return;
2236 }2300 }
2301 return true;
2237 }2302 }
2238 }2303 }
22392304
2240 if (e.key === "Enter" && !e.shiftKey) {2305 if (key === "Enter" && !event.shiftKey) {
2241 e.preventDefault();2306 void onSend();
2242 void onSend(e);2307 return true;
2243 }2308 }
2309 return false;
2244 };2310 };
2245 const onToggleWorkGroup = useCallback((groupId: string) => {2311 const onToggleWorkGroup = useCallback((groupId: string) => {
2246 setExpandedWorkGroups((existing) => ({2312 setExpandedWorkGroups((existing) => ({
...@@ -2464,33 +2530,12 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -2464,33 +2530,12 @@ export default function ChatView({ threadId }: ChatViewProps) {
2464 ))}2530 ))}
2465 </div>2531 </div>
2466 )}2532 )}
2467 <textarea2533 <ComposerPromptEditor
2468 ref={textareaRef}2534 ref={composerEditorRef}
2469 className="w-full resize-none bg-transparent text-[14px] leading-relaxed text-foreground placeholder:text-muted-foreground/35 focus:outline-none"
2470 rows={2}
2471 value={prompt}2535 value={prompt}
2472 onChange={(event) =>2536 cursor={composerCursor}
2473 onPromptChange(2537 onChange={onPromptChange}
2474 event.target.value,2538 onCommandKeyDown={onComposerCommandKey}
2475 event.target.selectionStart ?? event.target.value.length,
2476 )
2477 }
2478 onKeyDown={onKeyDown}
2479 onKeyUp={(event) =>
2480 setComposerCursor(
2481 event.currentTarget.selectionStart ?? event.currentTarget.value.length,
2482 )
2483 }
2484 onClick={(event) =>
2485 setComposerCursor(
2486 event.currentTarget.selectionStart ?? event.currentTarget.value.length,
2487 )
2488 }
2489 onSelect={(event) =>
2490 setComposerCursor(
2491 event.currentTarget.selectionStart ?? event.currentTarget.value.length,
2492 )
2493 }
2494 onPaste={onComposerPaste}2539 onPaste={onComposerPaste}
2495 placeholder={2540 placeholder={
2496 phase === "disconnected"2541 phase === "disconnected"
apps/web/src/components/ComposerPromptEditor.tsx created+664
...@@ -0,0 +1,664 @@
1import {
2 LexicalComposer,
3 type InitialConfigType,
4} from "@lexical/react/LexicalComposer";
5import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
6import { ContentEditable } from "@lexical/react/LexicalContentEditable";
7import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
8import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
9import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
10import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
11import {
12 $applyNodeReplacement,
13 $createRangeSelection,
14 $getSelection,
15 $setSelection,
16 $isElementNode,
17 $isLineBreakNode,
18 $isRangeSelection,
19 $isTextNode,
20 $createLineBreakNode,
21 $createParagraphNode,
22 $createTextNode,
23 KEY_ARROW_DOWN_COMMAND,
24 KEY_ARROW_UP_COMMAND,
25 KEY_ENTER_COMMAND,
26 KEY_TAB_COMMAND,
27 COMMAND_PRIORITY_HIGH,
28 KEY_BACKSPACE_COMMAND,
29 $getRoot,
30 type ElementNode,
31 type LexicalNode,
32 TextNode,
33 type EditorConfig,
34 type EditorState,
35 type NodeKey,
36 type SerializedTextNode,
37 type Spread,
38} from "lexical";
39import {
40 forwardRef,
41 useCallback,
42 useEffect,
43 useImperativeHandle,
44 useLayoutEffect,
45 useMemo,
46 useRef,
47 type ClipboardEventHandler,
48 type Ref,
49} from "react";
50
51import { splitPromptIntoComposerSegments } from "~/composer-editor-mentions";
52import { cn } from "~/lib/utils";
53import { basenameOfPath, getVscodeIconUrlForEntry } from "~/vscode-icons";
54
55const COMPOSER_EDITOR_HMR_KEY = `composer-editor-${Math.random().toString(36).slice(2)}`;
56
57type SerializedComposerMentionNode = Spread<
58 {
59 path: string;
60 type: "composer-mention";
61 version: 1;
62 },
63 SerializedTextNode
64>;
65
66class ComposerMentionNode extends TextNode {
67 __path: string;
68
69 static override getType(): string {
70 return "composer-mention";
71 }
72
73 static override clone(node: ComposerMentionNode): ComposerMentionNode {
74 return new ComposerMentionNode(node.__path, node.__key);
75 }
76
77 static override importJSON(serializedNode: SerializedComposerMentionNode): ComposerMentionNode {
78 return $createComposerMentionNode(serializedNode.path);
79 }
80
81 constructor(path: string, key?: NodeKey) {
82 const normalizedPath = path.startsWith("@") ? path.slice(1) : path;
83 super(`@${normalizedPath}`, key);
84 this.__path = normalizedPath;
85 }
86
87 override exportJSON(): SerializedComposerMentionNode {
88 return {
89 ...super.exportJSON(),
90 path: this.__path,
91 type: "composer-mention",
92 version: 1,
93 };
94 }
95
96 override createDOM(_config: EditorConfig): HTMLElement {
97 const dom = document.createElement("span");
98 dom.className =
99 "inline-flex select-none items-center gap-1 rounded-md border border-border/70 bg-accent/40 px-1.5 py-px font-medium text-[12px] leading-[1.1] text-foreground align-middle";
100 renderMentionChipDom(dom, this.__path);
101 return dom;
102 }
103
104 override updateDOM(
105 prevNode: ComposerMentionNode,
106 dom: HTMLElement,
107 _config: EditorConfig,
108 ): boolean {
109 if (prevNode.__text !== this.__text || prevNode.__path !== this.__path) {
110 renderMentionChipDom(dom, this.__path);
111 }
112 return false;
113 }
114
115 override canInsertTextBefore(): false {
116 return false;
117 }
118
119 override canInsertTextAfter(): false {
120 return false;
121 }
122
123 override isTextEntity(): true {
124 return true;
125 }
126
127 override isToken(): true {
128 return true;
129 }
130}
131
132function $createComposerMentionNode(path: string): ComposerMentionNode {
133 return $applyNodeReplacement(new ComposerMentionNode(path));
134}
135
136function inferMentionPathKind(pathValue: string): "file" | "directory" {
137 const base = basenameOfPath(pathValue);
138 if (base.startsWith(".") && !base.slice(1).includes(".")) {
139 return "directory";
140 }
141 if (base.includes(".")) {
142 return "file";
143 }
144 return "directory";
145}
146
147function renderMentionChipDom(container: HTMLElement, pathValue: string): void {
148 container.textContent = "";
149
150 const icon = document.createElement("img");
151 icon.alt = "";
152 icon.ariaHidden = "true";
153 icon.className = "size-3.5 shrink-0 opacity-85";
154 icon.loading = "lazy";
155 icon.src = getVscodeIconUrlForEntry(pathValue, inferMentionPathKind(pathValue), "dark");
156
157 const label = document.createElement("span");
158 label.className = "truncate leading-tight";
159 label.textContent = basenameOfPath(pathValue);
160
161 container.append(icon, label);
162}
163
164function clampCursor(value: string, cursor: number): number {
165 if (!Number.isFinite(cursor)) return value.length;
166 return Math.max(0, Math.min(value.length, Math.floor(cursor)));
167}
168
169function getComposerNodeTextLength(node: LexicalNode): number {
170 if ($isTextNode(node)) {
171 return node.getTextContentSize();
172 }
173 if ($isLineBreakNode(node)) {
174 return 1;
175 }
176 if ($isElementNode(node)) {
177 return node.getChildren().reduce((total, child) => total + getComposerNodeTextLength(child), 0);
178 }
179 return 0;
180}
181
182function getAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: number): number {
183 let offset = 0;
184 let current: LexicalNode | null = node;
185
186 while (current) {
187 const nextParent = current.getParent() as LexicalNode | null;
188 if (!nextParent || !$isElementNode(nextParent)) {
189 break;
190 }
191 const siblings = nextParent.getChildren();
192 const index = current.getIndexWithinParent();
193 for (let i = 0; i < index; i += 1) {
194 const sibling = siblings[i];
195 if (!sibling) continue;
196 offset += getComposerNodeTextLength(sibling);
197 }
198 current = nextParent;
199 }
200
201 if ($isTextNode(node)) {
202 return offset + Math.min(pointOffset, node.getTextContentSize());
203 }
204
205 if ($isLineBreakNode(node)) {
206 return offset + Math.min(pointOffset, 1);
207 }
208
209 if ($isElementNode(node)) {
210 const children = node.getChildren();
211 const clampedOffset = Math.max(0, Math.min(pointOffset, children.length));
212 for (let i = 0; i < clampedOffset; i += 1) {
213 const child = children[i];
214 if (!child) continue;
215 offset += getComposerNodeTextLength(child);
216 }
217 return offset;
218 }
219
220 return offset;
221}
222
223function findSelectionPointAtOffset(
224 node: LexicalNode,
225 remainingRef: { value: number },
226): { key: string; offset: number; type: "text" | "element" } | null {
227 if ($isTextNode(node)) {
228 const size = node.getTextContentSize();
229 if (remainingRef.value <= size) {
230 return {
231 key: node.getKey(),
232 offset: remainingRef.value,
233 type: "text",
234 };
235 }
236 remainingRef.value -= size;
237 return null;
238 }
239
240 if ($isLineBreakNode(node)) {
241 const parent = node.getParent();
242 if (!parent) return null;
243 const index = node.getIndexWithinParent();
244 if (remainingRef.value === 0) {
245 return {
246 key: parent.getKey(),
247 offset: index,
248 type: "element",
249 };
250 }
251 if (remainingRef.value === 1) {
252 return {
253 key: parent.getKey(),
254 offset: index + 1,
255 type: "element",
256 };
257 }
258 remainingRef.value -= 1;
259 return null;
260 }
261
262 if ($isElementNode(node)) {
263 const children = node.getChildren();
264 for (const child of children) {
265 const point = findSelectionPointAtOffset(child, remainingRef);
266 if (point) {
267 return point;
268 }
269 }
270 if (remainingRef.value === 0) {
271 return {
272 key: node.getKey(),
273 offset: children.length,
274 type: "element",
275 };
276 }
277 }
278
279 return null;
280}
281
282function $setSelectionAtComposerOffset(nextOffset: number): void {
283 const root = $getRoot();
284 const rootText = root.getTextContent();
285 const boundedOffset = clampCursor(rootText, nextOffset);
286 const remainingRef = { value: boundedOffset };
287 const point =
288 findSelectionPointAtOffset(root, remainingRef) ?? {
289 key: root.getKey(),
290 offset: root.getChildren().length,
291 type: "element" as const,
292 };
293 const selection = $createRangeSelection();
294 selection.anchor.set(point.key, point.offset, point.type);
295 selection.focus.set(point.key, point.offset, point.type);
296 $setSelection(selection);
297}
298
299function $readSelectionOffsetFromEditorState(fallback: number): number {
300 const selection = $getSelection();
301 if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
302 return fallback;
303 }
304 const rootText = $getRoot().getTextContent();
305 const anchorNode = selection.anchor.getNode();
306 return clampCursor(rootText, getAbsoluteOffsetForPoint(anchorNode, selection.anchor.offset));
307}
308
309function $appendTextWithLineBreaks(parent: ElementNode, text: string): void {
310 const lines = text.split("\n");
311 for (let index = 0; index < lines.length; index += 1) {
312 const line = lines[index] ?? "";
313 if (line.length > 0) {
314 parent.append($createTextNode(line));
315 }
316 if (index < lines.length - 1) {
317 parent.append($createLineBreakNode());
318 }
319 }
320}
321
322function $setComposerEditorPrompt(prompt: string): void {
323 const root = $getRoot();
324 root.clear();
325 const paragraph = $createParagraphNode();
326 root.append(paragraph);
327
328 const segments = splitPromptIntoComposerSegments(prompt);
329 for (const segment of segments) {
330 if (segment.type === "mention") {
331 paragraph.append($createComposerMentionNode(segment.path));
332 continue;
333 }
334 $appendTextWithLineBreaks(paragraph, segment.text);
335 }
336}
337
338export interface ComposerPromptEditorHandle {
339 focus: () => void;
340 focusAt: (cursor: number) => void;
341 focusAtEnd: () => void;
342 readSnapshot: () => { value: string; cursor: number };
343}
344
345interface ComposerPromptEditorProps {
346 value: string;
347 cursor: number;
348 disabled: boolean;
349 placeholder: string;
350 className?: string;
351 onChange: (nextValue: string, nextCursor: number) => void;
352 onCommandKeyDown?: (
353 key: "ArrowDown" | "ArrowUp" | "Enter" | "Tab",
354 event: KeyboardEvent,
355 ) => boolean;
356 onPaste: ClipboardEventHandler<HTMLElement>;
357}
358
359interface ComposerPromptEditorInnerProps extends ComposerPromptEditorProps {
360 editorRef: Ref<ComposerPromptEditorHandle>;
361}
362
363function ComposerCommandKeyPlugin(props: {
364 onCommandKeyDown?: (
365 key: "ArrowDown" | "ArrowUp" | "Enter" | "Tab",
366 event: KeyboardEvent,
367 ) => boolean;
368}) {
369 const [editor] = useLexicalComposerContext();
370
371 useEffect(() => {
372 const handleCommand = (
373 key: "ArrowDown" | "ArrowUp" | "Enter" | "Tab",
374 event: KeyboardEvent | null,
375 ): boolean => {
376 if (!props.onCommandKeyDown || !event) {
377 return false;
378 }
379 const handled = props.onCommandKeyDown(key, event);
380 if (handled) {
381 event.preventDefault();
382 event.stopPropagation();
383 }
384 return handled;
385 };
386
387 const unregisterArrowDown = editor.registerCommand(
388 KEY_ARROW_DOWN_COMMAND,
389 (event) => handleCommand("ArrowDown", event),
390 COMMAND_PRIORITY_HIGH,
391 );
392 const unregisterArrowUp = editor.registerCommand(
393 KEY_ARROW_UP_COMMAND,
394 (event) => handleCommand("ArrowUp", event),
395 COMMAND_PRIORITY_HIGH,
396 );
397 const unregisterEnter = editor.registerCommand(
398 KEY_ENTER_COMMAND,
399 (event) => handleCommand("Enter", event),
400 COMMAND_PRIORITY_HIGH,
401 );
402 const unregisterTab = editor.registerCommand(
403 KEY_TAB_COMMAND,
404 (event) => handleCommand("Tab", event),
405 COMMAND_PRIORITY_HIGH,
406 );
407
408 return () => {
409 unregisterArrowDown();
410 unregisterArrowUp();
411 unregisterEnter();
412 unregisterTab();
413 };
414 }, [editor, props]);
415
416 return null;
417}
418
419function ComposerMentionBackspacePlugin() {
420 const [editor] = useLexicalComposerContext();
421
422 useEffect(() => {
423 return editor.registerCommand(
424 KEY_BACKSPACE_COMMAND,
425 (event) => {
426 const selection = $getSelection();
427 if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
428 return false;
429 }
430
431 const anchorNode = selection.anchor.getNode();
432 const removeMentionNode = (candidate: unknown): boolean => {
433 if (!(candidate instanceof ComposerMentionNode)) {
434 return false;
435 }
436 const mentionStart = getAbsoluteOffsetForPoint(candidate, 0);
437 candidate.remove();
438 $setSelectionAtComposerOffset(mentionStart);
439 event?.preventDefault();
440 return true;
441 };
442
443 if (removeMentionNode(anchorNode)) {
444 return true;
445 }
446
447 if ($isTextNode(anchorNode)) {
448 if (selection.anchor.offset > 0) {
449 return false;
450 }
451 if (removeMentionNode(anchorNode.getPreviousSibling())) {
452 return true;
453 }
454 const parent = anchorNode.getParent();
455 if ($isElementNode(parent)) {
456 const index = anchorNode.getIndexWithinParent();
457 if (index > 0 && removeMentionNode(parent.getChildAtIndex(index - 1))) {
458 return true;
459 }
460 }
461 return false;
462 }
463
464 if ($isElementNode(anchorNode)) {
465 const childIndex = selection.anchor.offset - 1;
466 if (childIndex >= 0 && removeMentionNode(anchorNode.getChildAtIndex(childIndex))) {
467 return true;
468 }
469 }
470
471 return false;
472 },
473 COMMAND_PRIORITY_HIGH,
474 );
475 }, [editor]);
476
477 return null;
478}
479
480function ComposerPromptEditorInner({
481 value,
482 cursor,
483 disabled,
484 placeholder,
485 className,
486 onChange,
487 onCommandKeyDown,
488 onPaste,
489 editorRef,
490}: ComposerPromptEditorInnerProps) {
491 const [editor] = useLexicalComposerContext();
492 const onChangeRef = useRef(onChange);
493 const snapshotRef = useRef({ value, cursor: clampCursor(value, cursor) });
494
495 useEffect(() => {
496 onChangeRef.current = onChange;
497 }, [onChange]);
498
499 useEffect(() => {
500 editor.setEditable(!disabled);
501 }, [disabled, editor]);
502
503 useLayoutEffect(() => {
504 const normalizedCursor = clampCursor(value, cursor);
505 const previousSnapshot = snapshotRef.current;
506 if (previousSnapshot.value === value && previousSnapshot.cursor === normalizedCursor) {
507 return;
508 }
509
510 if (previousSnapshot.value !== value) {
511 editor.update(() => {
512 $setComposerEditorPrompt(value);
513 });
514 }
515
516 snapshotRef.current = { value, cursor: normalizedCursor };
517
518 const rootElement = editor.getRootElement();
519 if (!rootElement || document.activeElement !== rootElement) {
520 return;
521 }
522
523 editor.update(() => {
524 $setSelectionAtComposerOffset(normalizedCursor);
525 });
526 }, [cursor, editor, value]);
527
528 const focusAt = useCallback(
529 (nextCursor: number) => {
530 const rootElement = editor.getRootElement();
531 if (!rootElement) return;
532 const boundedCursor = clampCursor(snapshotRef.current.value, nextCursor);
533 rootElement.focus();
534 editor.update(() => {
535 $setSelectionAtComposerOffset(boundedCursor);
536 });
537 snapshotRef.current = {
538 value: snapshotRef.current.value,
539 cursor: boundedCursor,
540 };
541 onChangeRef.current(snapshotRef.current.value, boundedCursor);
542 },
543 [editor],
544 );
545
546 const readSnapshot = useCallback((): { value: string; cursor: number } => {
547 let snapshot = snapshotRef.current;
548 editor.getEditorState().read(() => {
549 const nextValue = $getRoot().getTextContent();
550 const fallbackCursor = clampCursor(nextValue, snapshotRef.current.cursor);
551 const nextCursor = clampCursor(
552 nextValue,
553 $readSelectionOffsetFromEditorState(fallbackCursor),
554 );
555 snapshot = {
556 value: nextValue,
557 cursor: nextCursor,
558 };
559 });
560 snapshotRef.current = snapshot;
561 return snapshot;
562 }, [editor]);
563
564 useImperativeHandle(
565 editorRef,
566 () => ({
567 focus: () => {
568 focusAt(snapshotRef.current.cursor);
569 },
570 focusAt: (nextCursor: number) => {
571 focusAt(nextCursor);
572 },
573 focusAtEnd: () => {
574 focusAt(snapshotRef.current.value.length);
575 },
576 readSnapshot,
577 }),
578 [focusAt, readSnapshot],
579 );
580
581 const handleEditorChange = useCallback((editorState: EditorState) => {
582 editorState.read(() => {
583 const nextValue = $getRoot().getTextContent();
584 const fallbackCursor = clampCursor(nextValue, snapshotRef.current.cursor);
585 const nextCursor = clampCursor(nextValue, $readSelectionOffsetFromEditorState(fallbackCursor));
586 const previousSnapshot = snapshotRef.current;
587 if (previousSnapshot.value === nextValue && previousSnapshot.cursor === nextCursor) {
588 return;
589 }
590 snapshotRef.current = {
591 value: nextValue,
592 cursor: nextCursor,
593 };
594 onChangeRef.current(nextValue, nextCursor);
595 });
596 }, []);
597
598 return (
599 <div className="relative">
600 <PlainTextPlugin
601 contentEditable={
602 <ContentEditable
603 className={cn(
604 "block max-h-[200px] min-h-17.5 w-full overflow-y-auto whitespace-pre-wrap break-words bg-transparent text-[14px] leading-relaxed text-foreground focus:outline-none",
605 className,
606 )}
607 aria-placeholder={placeholder}
608 placeholder={<span />}
609 onPaste={onPaste}
610 />
611 }
612 placeholder={
613 <div className="pointer-events-none absolute inset-0 text-[14px] leading-relaxed text-muted-foreground/35">
614 {placeholder}
615 </div>
616 }
617 ErrorBoundary={LexicalErrorBoundary}
618 />
619 <OnChangePlugin onChange={handleEditorChange} />
620 <ComposerCommandKeyPlugin {...(onCommandKeyDown ? { onCommandKeyDown } : {})} />
621 <ComposerMentionBackspacePlugin />
622 <HistoryPlugin />
623 </div>
624 );
625}
626
627export const ComposerPromptEditor = forwardRef<ComposerPromptEditorHandle, ComposerPromptEditorProps>(
628 function ComposerPromptEditor(
629 { value, cursor, disabled, placeholder, className, onChange, onCommandKeyDown, onPaste },
630 ref,
631 ) {
632 const initialValueRef = useRef(value);
633 const initialConfig = useMemo<InitialConfigType>(
634 () => ({
635 namespace: "t3tools-composer-editor",
636 editable: true,
637 nodes: [ComposerMentionNode],
638 editorState: () => {
639 $setComposerEditorPrompt(initialValueRef.current);
640 },
641 onError: (error) => {
642 throw error;
643 },
644 }),
645 [],
646 );
647
648 return (
649 <LexicalComposer key={COMPOSER_EDITOR_HMR_KEY} initialConfig={initialConfig}>
650 <ComposerPromptEditorInner
651 value={value}
652 cursor={cursor}
653 disabled={disabled}
654 placeholder={placeholder}
655 onChange={onChange}
656 onPaste={onPaste}
657 editorRef={ref}
658 {...(onCommandKeyDown ? { onCommandKeyDown } : {})}
659 {...(className ? { className } : {})}
660 />
661 </LexicalComposer>
662 );
663 },
664);
apps/web/src/composer-editor-mentions.test.ts created+27
...@@ -0,0 +1,27 @@
1import { describe, expect, it } from "vitest";
2
3import { splitPromptIntoComposerSegments } from "./composer-editor-mentions";
4
5describe("splitPromptIntoComposerSegments", () => {
6 it("splits mention tokens followed by whitespace into mention segments", () => {
7 expect(splitPromptIntoComposerSegments("Inspect @AGENTS.md please")).toEqual([
8 { type: "text", text: "Inspect " },
9 { type: "mention", path: "AGENTS.md" },
10 { type: "text", text: " please" },
11 ]);
12 });
13
14 it("does not convert an incomplete trailing mention token", () => {
15 expect(splitPromptIntoComposerSegments("Inspect @AGENTS.md")).toEqual([
16 { type: "text", text: "Inspect @AGENTS.md" },
17 ]);
18 });
19
20 it("keeps newlines around mention tokens", () => {
21 expect(splitPromptIntoComposerSegments("one\n@src/index.ts \ntwo")).toEqual([
22 { type: "text", text: "one\n" },
23 { type: "mention", path: "src/index.ts" },
24 { type: "text", text: " \ntwo" },
25 ]);
26 });
27});
apps/web/src/composer-editor-mentions.ts created+56
...@@ -0,0 +1,56 @@
1export type ComposerPromptSegment =
2 | {
3 type: "text";
4 text: string;
5 }
6 | {
7 type: "mention";
8 path: string;
9 };
10
11const MENTION_TOKEN_REGEX = /(^|\s)@([^\s@]+)(?=\s)/g;
12
13function pushTextSegment(segments: ComposerPromptSegment[], text: string): void {
14 if (!text) return;
15 const last = segments[segments.length - 1];
16 if (last && last.type === "text") {
17 last.text += text;
18 return;
19 }
20 segments.push({ type: "text", text });
21}
22
23export function splitPromptIntoComposerSegments(prompt: string): ComposerPromptSegment[] {
24 const segments: ComposerPromptSegment[] = [];
25 if (!prompt) {
26 return segments;
27 }
28
29 let cursor = 0;
30 for (const match of prompt.matchAll(MENTION_TOKEN_REGEX)) {
31 const fullMatch = match[0];
32 const prefix = match[1] ?? "";
33 const path = match[2] ?? "";
34 const matchIndex = match.index ?? 0;
35 const mentionStart = matchIndex + prefix.length;
36 const mentionEnd = mentionStart + fullMatch.length - prefix.length;
37
38 if (mentionStart > cursor) {
39 pushTextSegment(segments, prompt.slice(cursor, mentionStart));
40 }
41
42 if (path.length > 0) {
43 segments.push({ type: "mention", path });
44 } else {
45 pushTextSegment(segments, prompt.slice(mentionStart, mentionEnd));
46 }
47
48 cursor = mentionEnd;
49 }
50
51 if (cursor < prompt.length) {
52 pushTextSegment(segments, prompt.slice(cursor));
53 }
54
55 return segments;
56}
apps/web/src/composerDraftStore.ts-40
...@@ -28,7 +28,6 @@ export interface ComposerImageAttachment extends Omit<ChatImageAttachment, "prev...@@ -28,7 +28,6 @@ export interface ComposerImageAttachment extends Omit<ChatImageAttachment, "prev
2828
29interface PersistedComposerThreadDraftState {29interface PersistedComposerThreadDraftState {
30 prompt: string;30 prompt: string;
31 cursor: number;
32 attachments: PersistedComposerImageAttachment[];31 attachments: PersistedComposerImageAttachment[];
33 model?: string | null;32 model?: string | null;
34 effort?: ReasoningEffort | null;33 effort?: ReasoningEffort | null;
...@@ -50,7 +49,6 @@ interface PersistedComposerDraftStoreState {...@@ -50,7 +49,6 @@ interface PersistedComposerDraftStoreState {
5049
51interface ComposerThreadDraftState {50interface ComposerThreadDraftState {
52 prompt: string;51 prompt: string;
53 cursor: number;
54 images: ComposerImageAttachment[];52 images: ComposerImageAttachment[];
55 nonPersistedImageIds: string[];53 nonPersistedImageIds: string[];
56 persistedAttachments: PersistedComposerImageAttachment[];54 persistedAttachments: PersistedComposerImageAttachment[];
...@@ -100,7 +98,6 @@ interface ComposerDraftStoreState {...@@ -100,7 +98,6 @@ interface ComposerDraftStoreState {
100 clearProjectDraftThreadById: (projectId: ProjectId, threadId: ThreadId) => void;98 clearProjectDraftThreadById: (projectId: ProjectId, threadId: ThreadId) => void;
101 clearDraftThread: (threadId: ThreadId) => void;99 clearDraftThread: (threadId: ThreadId) => void;
102 setPrompt: (threadId: ThreadId, prompt: string) => void;100 setPrompt: (threadId: ThreadId, prompt: string) => void;
103 setCursor: (threadId: ThreadId, cursor: number) => void;
104 setModel: (threadId: ThreadId, model: string | null | undefined) => void;101 setModel: (threadId: ThreadId, model: string | null | undefined) => void;
105 setEffort: (threadId: ThreadId, effort: ReasoningEffort | null | undefined) => void;102 setEffort: (threadId: ThreadId, effort: ReasoningEffort | null | undefined) => void;
106 addImage: (threadId: ThreadId, image: ComposerImageAttachment) => void;103 addImage: (threadId: ThreadId, image: ComposerImageAttachment) => void;
...@@ -129,7 +126,6 @@ Object.freeze(EMPTY_IDS);...@@ -129,7 +126,6 @@ Object.freeze(EMPTY_IDS);
129Object.freeze(EMPTY_PERSISTED_ATTACHMENTS);126Object.freeze(EMPTY_PERSISTED_ATTACHMENTS);
130const EMPTY_THREAD_DRAFT = Object.freeze({127const EMPTY_THREAD_DRAFT = Object.freeze({
131 prompt: "",128 prompt: "",
132 cursor: 0,
133 images: EMPTY_IMAGES,129 images: EMPTY_IMAGES,
134 nonPersistedImageIds: EMPTY_IDS,130 nonPersistedImageIds: EMPTY_IDS,
135 persistedAttachments: EMPTY_PERSISTED_ATTACHMENTS,131 persistedAttachments: EMPTY_PERSISTED_ATTACHMENTS,
...@@ -142,7 +138,6 @@ const REASONING_EFFORT_VALUES = new Set<ReasoningEffort>(REASONING_OPTIONS);...@@ -142,7 +138,6 @@ const REASONING_EFFORT_VALUES = new Set<ReasoningEffort>(REASONING_OPTIONS);
142function createEmptyThreadDraft(): ComposerThreadDraftState {138function createEmptyThreadDraft(): ComposerThreadDraftState {
143 return {139 return {
144 prompt: "",140 prompt: "",
145 cursor: 0,
146 images: [],141 images: [],
147 nonPersistedImageIds: [],142 nonPersistedImageIds: [],
148 persistedAttachments: [],143 persistedAttachments: [],
...@@ -304,10 +299,6 @@ function normalizePersistedComposerDraftState(value: unknown): PersistedComposer...@@ -304,10 +299,6 @@ function normalizePersistedComposerDraftState(value: unknown): PersistedComposer
304 }299 }
305 const draftCandidate = draftValue as Record<string, unknown>;300 const draftCandidate = draftValue as Record<string, unknown>;
306 const prompt = typeof draftCandidate.prompt === "string" ? draftCandidate.prompt : "";301 const prompt = typeof draftCandidate.prompt === "string" ? draftCandidate.prompt : "";
307 const cursor =
308 typeof draftCandidate.cursor === "number" && Number.isFinite(draftCandidate.cursor)
309 ? Math.max(0, Math.floor(draftCandidate.cursor))
310 : 0;
311 const attachments = Array.isArray(draftCandidate.attachments)302 const attachments = Array.isArray(draftCandidate.attachments)
312 ? draftCandidate.attachments.flatMap((entry) => {303 ? draftCandidate.attachments.flatMap((entry) => {
313 const normalized = normalizePersistedAttachment(entry);304 const normalized = normalizePersistedAttachment(entry);
...@@ -327,7 +318,6 @@ function normalizePersistedComposerDraftState(value: unknown): PersistedComposer...@@ -327,7 +318,6 @@ function normalizePersistedComposerDraftState(value: unknown): PersistedComposer
327 }318 }
328 nextDraftsByThreadId[threadId as ThreadId] = {319 nextDraftsByThreadId[threadId as ThreadId] = {
329 prompt,320 prompt,
330 cursor,
331 attachments,321 attachments,
332 ...(model ? { model } : {}),322 ...(model ? { model } : {}),
333 ...(effort ? { effort } : {}),323 ...(effort ? { effort } : {}),
...@@ -429,7 +419,6 @@ function toHydratedThreadDraft(...@@ -429,7 +419,6 @@ function toHydratedThreadDraft(
429): ComposerThreadDraftState {419): ComposerThreadDraftState {
430 return {420 return {
431 prompt: persistedDraft.prompt,421 prompt: persistedDraft.prompt,
432 cursor: persistedDraft.cursor,
433 images: hydrateImagesFromPersisted(persistedDraft.attachments),422 images: hydrateImagesFromPersisted(persistedDraft.attachments),
434 nonPersistedImageIds: [],423 nonPersistedImageIds: [],
435 persistedAttachments: persistedDraft.attachments,424 persistedAttachments: persistedDraft.attachments,
...@@ -672,36 +661,9 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()(...@@ -672,36 +661,9 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()(
672 }661 }
673 set((state) => {662 set((state) => {
674 const existing = state.draftsByThreadId[threadId] ?? createEmptyThreadDraft();663 const existing = state.draftsByThreadId[threadId] ?? createEmptyThreadDraft();
675 const cursor = Math.min(existing.cursor, prompt.length);
676 const nextDraft: ComposerThreadDraftState = {664 const nextDraft: ComposerThreadDraftState = {
677 ...existing,665 ...existing,
678 prompt,666 prompt,
679 cursor,
680 };
681 const nextDraftsByThreadId = { ...state.draftsByThreadId };
682 if (shouldRemoveDraft(nextDraft)) {
683 delete nextDraftsByThreadId[threadId];
684 } else {
685 nextDraftsByThreadId[threadId] = nextDraft;
686 }
687 return { draftsByThreadId: nextDraftsByThreadId };
688 });
689 },
690 setCursor: (threadId, cursor) => {
691 if (threadId.length === 0) {
692 return;
693 }
694 const nextCursor = Number.isFinite(cursor) ? Math.max(0, Math.floor(cursor)) : 0;
695 set((state) => {
696 const existing = state.draftsByThreadId[threadId];
697 if (!existing && nextCursor === 0) {
698 return state;
699 }
700 const base = existing ?? createEmptyThreadDraft();
701 const boundedCursor = Math.min(nextCursor, base.prompt.length);
702 const nextDraft: ComposerThreadDraftState = {
703 ...base,
704 cursor: boundedCursor,
705 };667 };
706 const nextDraftsByThreadId = { ...state.draftsByThreadId };668 const nextDraftsByThreadId = { ...state.draftsByThreadId };
707 if (shouldRemoveDraft(nextDraft)) {669 if (shouldRemoveDraft(nextDraft)) {
...@@ -939,7 +901,6 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()(...@@ -939,7 +901,6 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()(
939 const nextDraft: ComposerThreadDraftState = {901 const nextDraft: ComposerThreadDraftState = {
940 ...current,902 ...current,
941 prompt: "",903 prompt: "",
942 cursor: 0,
943 images: [],904 images: [],
944 nonPersistedImageIds: [],905 nonPersistedImageIds: [],
945 persistedAttachments: [],906 persistedAttachments: [],
...@@ -1009,7 +970,6 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()(...@@ -1009,7 +970,6 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()(
1009 }970 }
1010 const persistedDraft: PersistedComposerThreadDraftState = {971 const persistedDraft: PersistedComposerThreadDraftState = {
1011 prompt: draft.prompt,972 prompt: draft.prompt,
1012 cursor: Math.min(Math.max(0, draft.cursor), draft.prompt.length),
1013 attachments: draft.persistedAttachments,973 attachments: draft.persistedAttachments,
1014 };974 };
1015 if (draft.model) {975 if (draft.model) {
bun.lock+62
...@@ -63,6 +63,7 @@...@@ -63,6 +63,7 @@
63 "version": "0.0.0-alpha.4",63 "version": "0.0.0-alpha.4",
64 "dependencies": {64 "dependencies": {
65 "@base-ui/react": "^1.2.0",65 "@base-ui/react": "^1.2.0",
66 "@lexical/react": "^0.41.0",
66 "@pierre/diffs": "^1.1.0-beta.16",67 "@pierre/diffs": "^1.1.0-beta.16",
67 "@t3tools/contracts": "workspace:*",68 "@t3tools/contracts": "workspace:*",
68 "@tanstack/react-pacer": "^0.19.4",69 "@tanstack/react-pacer": "^0.19.4",
...@@ -73,6 +74,7 @@...@@ -73,6 +74,7 @@
73 "@xterm/xterm": "^6.0.0",74 "@xterm/xterm": "^6.0.0",
74 "class-variance-authority": "^0.7.1",75 "class-variance-authority": "^0.7.1",
75 "effect": "catalog:",76 "effect": "catalog:",
77 "lexical": "^0.41.0",
76 "lucide-react": "^0.564.0",78 "lucide-react": "^0.564.0",
77 "react": "^19.0.0",79 "react": "^19.0.0",
78 "react-dom": "^19.0.0",80 "react-dom": "^19.0.0",
...@@ -238,6 +240,8 @@...@@ -238,6 +240,8 @@
238240
239 "@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="],241 "@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="],
240242
243 "@floating-ui/react": ["@floating-ui/react@0.27.18", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.7", "@floating-ui/utils": "^0.2.10", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-xJWJxvmy3a05j643gQt+pRbht5XnTlGpsEsAPnMi5F5YTOEEJymA90uZKBD8OvIv5XvZ1qi4GcccSlqT3Bq44Q=="],
244
241 "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="],245 "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="],
242246
243 "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],247 "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
...@@ -276,6 +280,50 @@...@@ -276,6 +280,50 @@
276280
277 "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],281 "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
278282
283 "@lexical/clipboard": ["@lexical/clipboard@0.41.0", "", { "dependencies": { "@lexical/html": "0.41.0", "@lexical/list": "0.41.0", "@lexical/selection": "0.41.0", "@lexical/utils": "0.41.0", "lexical": "0.41.0" } }, "sha512-Ex5lPkb4NBBX1DCPzOAIeHBJFH1bJcmATjREaqpnTfxCbuOeQkt44wchezUA0oDl+iAxNZ3+pLLWiUju9icoSA=="],
284
285 "@lexical/code": ["@lexical/code@0.41.0", "", { "dependencies": { "@lexical/utils": "0.41.0", "lexical": "0.41.0", "prismjs": "^1.30.0" } }, "sha512-0hoNi1KC9/N3SBOGcOcFqnT0OpwmcRRAhfxTKMGqfCtCvAMzULVwZ8RWc9/NV9bKYESgBTW5D9xkDANP2mspHg=="],
286
287 "@lexical/devtools-core": ["@lexical/devtools-core@0.41.0", "", { "dependencies": { "@lexical/html": "0.41.0", "@lexical/link": "0.41.0", "@lexical/mark": "0.41.0", "@lexical/table": "0.41.0", "@lexical/utils": "0.41.0", "lexical": "0.41.0" }, "peerDependencies": { "react": ">=17.x", "react-dom": ">=17.x" } }, "sha512-FzJtluBhBc8bKS11TUZe72KoZN/hnzIyiiM0SPJAsPwGpoXuM01jqpXQGybWf/1bWB+bmmhOae7O4Nywi/Csuw=="],
288
289 "@lexical/dragon": ["@lexical/dragon@0.41.0", "", { "dependencies": { "@lexical/extension": "0.41.0", "lexical": "0.41.0" } }, "sha512-gBEqkk8Q6ZPruvDaRcOdF1EK9suCVBODzOCcR+EnoJTaTjfDkCM7pkPAm4w90Wa1wCZEtFHvCfas+jU9MDSumg=="],
290
291 "@lexical/extension": ["@lexical/extension@0.41.0", "", { "dependencies": { "@lexical/utils": "0.41.0", "@preact/signals-core": "^1.11.0", "lexical": "0.41.0" } }, "sha512-sF4SPiP72yXvIGchmmIZ7Yg2XZTxNLOpFEIIzdqG7X/1fa1Ham9P/T7VbrblWpF6Ei5LJtK9JgNVB0hb4l3o1g=="],
292
293 "@lexical/hashtag": ["@lexical/hashtag@0.41.0", "", { "dependencies": { "@lexical/text": "0.41.0", "@lexical/utils": "0.41.0", "lexical": "0.41.0" } }, "sha512-tFWM74RW4KU0E/sj2aowfWl26vmLUTp331CgVESnhQKcZBfT40KJYd57HEqBDTfQKn4MUhylQCCA0hbpw6EeFQ=="],
294
295 "@lexical/history": ["@lexical/history@0.41.0", "", { "dependencies": { "@lexical/extension": "0.41.0", "@lexical/utils": "0.41.0", "lexical": "0.41.0" } }, "sha512-kGoVWsiOn62+RMjRolRa+NXZl8jFwxav6GNDiHH8yzivtoaH8n1SwUfLJELXCzeqzs81HySqD4q30VLJVTGoDg=="],
296
297 "@lexical/html": ["@lexical/html@0.41.0", "", { "dependencies": { "@lexical/selection": "0.41.0", "@lexical/utils": "0.41.0", "lexical": "0.41.0" } }, "sha512-3RyZy+H/IDKz2D66rNN/NqYx87xVFrngfEbyu1OWtbY963RUFnopiVHCQvsge/8kT04QSZ7U/DzjVFqeNS6clg=="],
298
299 "@lexical/link": ["@lexical/link@0.41.0", "", { "dependencies": { "@lexical/extension": "0.41.0", "@lexical/utils": "0.41.0", "lexical": "0.41.0" } }, "sha512-Rjtx5cGWAkKcnacncbVsZ1TqRnUB2Wm4eEVKpaAEG41+kHgqghzM2P+UGT15yROroxJu8KvAC9ISiYFiU4XE1w=="],
300
301 "@lexical/list": ["@lexical/list@0.41.0", "", { "dependencies": { "@lexical/extension": "0.41.0", "@lexical/selection": "0.41.0", "@lexical/utils": "0.41.0", "lexical": "0.41.0" } }, "sha512-RXvB+xcbzVoQLGRDOBRCacztG7V+bI95tdoTwl8pz5xvgPtAaRnkZWMDP+yMNzMJZsqEChdtpxbf0NgtMkun6g=="],
302
303 "@lexical/mark": ["@lexical/mark@0.41.0", "", { "dependencies": { "@lexical/utils": "0.41.0", "lexical": "0.41.0" } }, "sha512-UO5WVs9uJAYIKHSlYh4Z1gHrBBchTOi21UCYBIZ7eAs4suK84hPzD+3/LAX5CB7ZltL6ke5Sly3FOwNXv/wfpA=="],
304
305 "@lexical/markdown": ["@lexical/markdown@0.41.0", "", { "dependencies": { "@lexical/code": "0.41.0", "@lexical/link": "0.41.0", "@lexical/list": "0.41.0", "@lexical/rich-text": "0.41.0", "@lexical/text": "0.41.0", "@lexical/utils": "0.41.0", "lexical": "0.41.0" } }, "sha512-bzI73JMXpjGFhqUWNV6KqfjWcgAWzwFT+J3RHtbCF5rysC8HLldBYojOgAAtPfXqfxyv2mDzsY7SoJ75s9uHZA=="],
306
307 "@lexical/offset": ["@lexical/offset@0.41.0", "", { "dependencies": { "lexical": "0.41.0" } }, "sha512-2RHBXZqC8gm3X9C0AyRb0M8w7zJu5dKiasrif+jSKzsxPjAUeF1m95OtIOsWs1XLNUgASOSUqGovDZxKJslZfA=="],
308
309 "@lexical/overflow": ["@lexical/overflow@0.41.0", "", { "dependencies": { "lexical": "0.41.0" } }, "sha512-Iy6ZiJip8X14EBYt1zKPOrXyQ4eG9JLBEoPoSVBTiSbVd+lYicdUvaOThT0k0/qeVTN9nqTaEltBjm56IrVKCQ=="],
310
311 "@lexical/plain-text": ["@lexical/plain-text@0.41.0", "", { "dependencies": { "@lexical/clipboard": "0.41.0", "@lexical/dragon": "0.41.0", "@lexical/selection": "0.41.0", "@lexical/utils": "0.41.0", "lexical": "0.41.0" } }, "sha512-HIsGgmFUYRUNNyvckun33UQfU7LRzDlxymHUq67+Bxd5bXqdZOrStEKJXuDX+LuLh/GXZbaWNbDLqwLBObfbQg=="],
312
313 "@lexical/react": ["@lexical/react@0.41.0", "", { "dependencies": { "@floating-ui/react": "^0.27.16", "@lexical/devtools-core": "0.41.0", "@lexical/dragon": "0.41.0", "@lexical/extension": "0.41.0", "@lexical/hashtag": "0.41.0", "@lexical/history": "0.41.0", "@lexical/link": "0.41.0", "@lexical/list": "0.41.0", "@lexical/mark": "0.41.0", "@lexical/markdown": "0.41.0", "@lexical/overflow": "0.41.0", "@lexical/plain-text": "0.41.0", "@lexical/rich-text": "0.41.0", "@lexical/table": "0.41.0", "@lexical/text": "0.41.0", "@lexical/utils": "0.41.0", "@lexical/yjs": "0.41.0", "lexical": "0.41.0", "react-error-boundary": "^6.0.0" }, "peerDependencies": { "react": ">=17.x", "react-dom": ">=17.x" } }, "sha512-7+GUdZUm6sofWm+zdsWAs6cFBwKNsvsHezZTrf6k8jrZxL461ZQmbz/16b4DvjCGL9r5P1fR7md9/LCmk8TiCg=="],
314
315 "@lexical/rich-text": ["@lexical/rich-text@0.41.0", "", { "dependencies": { "@lexical/clipboard": "0.41.0", "@lexical/dragon": "0.41.0", "@lexical/selection": "0.41.0", "@lexical/utils": "0.41.0", "lexical": "0.41.0" } }, "sha512-yUcr7ZaaVTZNi8bow4CK1M8jy2qyyls1Vr+5dVjwBclVShOL/F/nFyzBOSb6RtXXRbd3Ahuk9fEleppX/RNIdw=="],
316
317 "@lexical/selection": ["@lexical/selection@0.41.0", "", { "dependencies": { "lexical": "0.41.0" } }, "sha512-1s7/kNyRzcv5uaTwsUL28NpiisqTf5xZ1zNukLsCN1xY+TWbv9RE9OxIv+748wMm4pxNczQe/UbIBODkbeknLw=="],
318
319 "@lexical/table": ["@lexical/table@0.41.0", "", { "dependencies": { "@lexical/clipboard": "0.41.0", "@lexical/extension": "0.41.0", "@lexical/utils": "0.41.0", "lexical": "0.41.0" } }, "sha512-d3SPThBAr+oZ8O74TXU0iXM3rLbrAVC7/HcOnSAq7/AhWQW8yMutT51JQGN+0fMLP9kqoWSAojNtkdvzXfU/+A=="],
320
321 "@lexical/text": ["@lexical/text@0.41.0", "", { "dependencies": { "lexical": "0.41.0" } }, "sha512-gGA+Anc7ck110EXo4KVKtq6Ui3M7Vz3OpGJ4QE6zJHWW8nV5h273koUGSutAMeoZgRVb6t01Izh3ORoFt/j1CA=="],
322
323 "@lexical/utils": ["@lexical/utils@0.41.0", "", { "dependencies": { "@lexical/selection": "0.41.0", "lexical": "0.41.0" } }, "sha512-Wlsokr5NQCq83D+7kxZ9qs5yQ3dU3Qaf2M+uXxLRoPoDaXqW8xTWZq1+ZFoEzsHzx06QoPa4Vu/40BZR91uQPg=="],
324
325 "@lexical/yjs": ["@lexical/yjs@0.41.0", "", { "dependencies": { "@lexical/offset": "0.41.0", "@lexical/selection": "0.41.0", "lexical": "0.41.0" }, "peerDependencies": { "yjs": ">=13.5.22" } }, "sha512-PaKTxSbVC4fpqUjQ7vUL9RkNF1PjL8TFl5jRe03PqoPYpE33buf3VXX6+cOUEfv9+uknSqLCPHoBS/4jN3a97w=="],
326
279 "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="],327 "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="],
280328
281 "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="],329 "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="],
...@@ -380,6 +428,8 @@...@@ -380,6 +428,8 @@
380428
381 "@pierre/diffs": ["@pierre/diffs@1.1.0-beta.16", "", { "dependencies": { "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-McjTuEPuacSIcXdoI2O9W6VSHIOs9ApEHnEUwONKZnKqIo2GGv1vNg9Pr8tgBOL7lgBWNEHX5ROJ5z1X74sENQ=="],429 "@pierre/diffs": ["@pierre/diffs@1.1.0-beta.16", "", { "dependencies": { "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-McjTuEPuacSIcXdoI2O9W6VSHIOs9ApEHnEUwONKZnKqIo2GGv1vNg9Pr8tgBOL7lgBWNEHX5ROJ5z1X74sENQ=="],
382430
431 "@preact/signals-core": ["@preact/signals-core@1.13.0", "", {}, "sha512-slT6XeTCAbdql61GVLlGU4x7XHI7kCZV5Um5uhE4zLX4ApgiiXc0UYFvVOKq06xcovzp7p+61l68oPi563ARKg=="],
432
383 "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="],433 "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="],
384434
385 "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.3", "", { "os": "android", "cpu": "arm64" }, "sha512-0T1k9FinuBZ/t7rZ8jN6OpUKPnUjNdYHoj/cESWrQ3ZraAJ4OMm6z7QjSfCxqj8mOp9kTKc1zHK3kGz5vMu+nQ=="],435 "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.3", "", { "os": "android", "cpu": "arm64" }, "sha512-0T1k9FinuBZ/t7rZ8jN6OpUKPnUjNdYHoj/cESWrQ3ZraAJ4OMm6z7QjSfCxqj8mOp9kTKc1zHK3kGz5vMu+nQ=="],
...@@ -922,6 +972,8 @@...@@ -922,6 +972,8 @@
922972
923 "isbot": ["isbot@5.1.35", "", {}, "sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg=="],973 "isbot": ["isbot@5.1.35", "", {}, "sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg=="],
924974
975 "isomorphic.js": ["isomorphic.js@0.2.5", "", {}, "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw=="],
976
925 "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],977 "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
926978
927 "joi": ["joi@18.0.2", "", { "dependencies": { "@hapi/address": "^5.1.1", "@hapi/formula": "^3.0.2", "@hapi/hoek": "^11.0.7", "@hapi/pinpoint": "^2.0.1", "@hapi/tlds": "^1.1.1", "@hapi/topo": "^6.0.2", "@standard-schema/spec": "^1.0.0" } }, "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA=="],979 "joi": ["joi@18.0.2", "", { "dependencies": { "@hapi/address": "^5.1.1", "@hapi/formula": "^3.0.2", "@hapi/hoek": "^11.0.7", "@hapi/pinpoint": "^2.0.1", "@hapi/tlds": "^1.1.1", "@hapi/topo": "^6.0.2", "@standard-schema/spec": "^1.0.0" } }, "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA=="],
...@@ -942,6 +994,10 @@...@@ -942,6 +994,10 @@
942994
943 "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="],995 "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="],
944996
997 "lexical": ["lexical@0.41.0", "", {}, "sha512-pNIm5+n+hVnJHB9gYPDYsIO5Y59dNaDU9rJmPPsfqQhP2ojKFnUoPbcRnrI9FJLXB14sSumcY8LUw7Sq70TZqA=="],
998
999 "lib0": ["lib0@0.2.117", "", { "dependencies": { "isomorphic.js": "^0.2.4" }, "bin": { "0serve": "bin/0serve.js", "0gentesthtml": "bin/gentesthtml.js", "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js" } }, "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw=="],
1000
945 "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],1001 "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
9461002
947 "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],1003 "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
...@@ -1154,6 +1210,8 @@...@@ -1154,6 +1210,8 @@
11541210
1155 "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],1211 "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
11561212
1213 "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
1214
1157 "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],1215 "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
11581216
1159 "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],1217 "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
...@@ -1172,6 +1230,8 @@...@@ -1172,6 +1230,8 @@
11721230
1173 "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],1231 "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
11741232
1233 "react-error-boundary": ["react-error-boundary@6.1.1", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w=="],
1234
1175 "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],1235 "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
11761236
1177 "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],1237 "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
...@@ -1414,6 +1474,8 @@...@@ -1414,6 +1474,8 @@
14141474
1415 "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],1475 "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
14161476
1477 "yjs": ["yjs@13.6.29", "", { "dependencies": { "lib0": "^0.2.99" } }, "sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ=="],
1478
1417 "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],1479 "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
14181480
1419 "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],1481 "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],