From 165b4dd1368cf41022e9bae8110d55e250243e57 Mon Sep 17 00:00:00 2001 From: Noah Gregory Date: Tue, 10 Mar 2026 17:56:58 -0400 Subject: [PATCH 1/4] refactor: remove redunant `focusAt` function wrapping in `ComposerPromptEditorHandle` --- apps/web/src/components/ComposerPromptEditor.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/web/src/components/ComposerPromptEditor.tsx b/apps/web/src/components/ComposerPromptEditor.tsx index 8b1ca4dc7..586d69cb6 100644 --- a/apps/web/src/components/ComposerPromptEditor.tsx +++ b/apps/web/src/components/ComposerPromptEditor.tsx @@ -705,9 +705,7 @@ function ComposerPromptEditorInner({ focus: () => { focusAt(snapshotRef.current.cursor); }, - focusAt: (nextCursor: number) => { - focusAt(nextCursor); - }, + focusAt, focusAtEnd: () => { focusAt(snapshotRef.current.value.length); }, From 8b6e97e20e8e963b8e34142dc7d71310b265a5f0 Mon Sep 17 00:00:00 2001 From: Noah Gregory Date: Tue, 10 Mar 2026 18:39:31 -0400 Subject: [PATCH 2/4] fix: skip firing the `onChange` handler in `ComposerPromptEditor` for prop-controlled updates --- .../src/components/ComposerPromptEditor.tsx | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/ComposerPromptEditor.tsx b/apps/web/src/components/ComposerPromptEditor.tsx index 586d69cb6..706a899f7 100644 --- a/apps/web/src/components/ComposerPromptEditor.tsx +++ b/apps/web/src/components/ComposerPromptEditor.tsx @@ -629,6 +629,7 @@ function ComposerPromptEditorInner({ const [editor] = useLexicalComposerContext(); const onChangeRef = useRef(onChange); const snapshotRef = useRef({ value, cursor: clampCursor(value, cursor) }); + const isApplyingControlledUpdateRef = useRef(false); useEffect(() => { onChangeRef.current = onChange; @@ -645,21 +646,25 @@ function ComposerPromptEditorInner({ return; } - if (previousSnapshot.value !== value) { - editor.update(() => { - $setComposerEditorPrompt(value); - }); - } - snapshotRef.current = { value, cursor: normalizedCursor }; const rootElement = editor.getRootElement(); - if (!rootElement || document.activeElement !== rootElement) { + const isFocused = Boolean(rootElement && document.activeElement === rootElement); + if (previousSnapshot.value === value && !isFocused) { return; } + isApplyingControlledUpdateRef.current = true; editor.update(() => { - $setSelectionAtComposerOffset(normalizedCursor); + if (previousSnapshot.value !== value) { + $setComposerEditorPrompt(value); + } + if (isFocused) { + $setSelectionAtComposerOffset(normalizedCursor); + } + }); + queueMicrotask(() => { + isApplyingControlledUpdateRef.current = false; }); }, [cursor, editor, value]); @@ -726,6 +731,9 @@ function ComposerPromptEditorInner({ if (previousSnapshot.value === nextValue && previousSnapshot.cursor === nextCursor) { return; } + if (isApplyingControlledUpdateRef.current) { + return; + } snapshotRef.current = { value: nextValue, cursor: nextCursor, From c2cda8f9f6d73bbff35555b52b94c36e64d8d0b3 Mon Sep 17 00:00:00 2001 From: Noah Gregory Date: Tue, 10 Mar 2026 18:47:25 -0400 Subject: [PATCH 3/4] fix: still set composer cursor when input is unfocused (e.g. after button press) --- apps/web/src/components/ComposerPromptEditor.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/ComposerPromptEditor.tsx b/apps/web/src/components/ComposerPromptEditor.tsx index 706a899f7..47f31325f 100644 --- a/apps/web/src/components/ComposerPromptEditor.tsx +++ b/apps/web/src/components/ComposerPromptEditor.tsx @@ -656,10 +656,11 @@ function ComposerPromptEditorInner({ isApplyingControlledUpdateRef.current = true; editor.update(() => { + const valueChanged = previousSnapshot.value !== value; if (previousSnapshot.value !== value) { $setComposerEditorPrompt(value); } - if (isFocused) { + if (valueChanged || isFocused) { $setSelectionAtComposerOffset(normalizedCursor); } }); From a9291437c8dc07f4793480f60764721537f2f373 Mon Sep 17 00:00:00 2001 From: Noah Gregory Date: Tue, 10 Mar 2026 19:34:07 -0400 Subject: [PATCH 4/4] refactor: tighten up pending input sync behavior to fix cursor bugs --- apps/web/src/components/ChatView.tsx | 41 ++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 1bd167291..b057d5e9e 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1031,23 +1031,46 @@ export default function ChatView({ threadId }: ChatViewProps) { pendingUserInputs.length > 0 || (showPlanFollowUpPrompt && activeProposedPlan !== null); const composerFooterHasWideActions = showPlanFollowUpPrompt || activePendingProgress !== null; + const lastSyncedPendingInputRef = useRef<{ + requestId: string | null; + questionId: string | null; + } | null>(null); useEffect(() => { - if (!activePendingProgress) { + const nextCustomAnswer = activePendingProgress?.customAnswer; + if (typeof nextCustomAnswer !== "string") { + lastSyncedPendingInputRef.current = null; return; } - promptRef.current = activePendingProgress.customAnswer; - setComposerCursor(activePendingProgress.customAnswer.length); + const nextRequestId = activePendingUserInput?.requestId ?? null; + const nextQuestionId = activePendingProgress?.activeQuestion?.id ?? null; + const questionChanged = + lastSyncedPendingInputRef.current?.requestId !== nextRequestId || + lastSyncedPendingInputRef.current?.questionId !== nextQuestionId; + const textChangedExternally = promptRef.current !== nextCustomAnswer; + + lastSyncedPendingInputRef.current = { + requestId: nextRequestId, + questionId: nextQuestionId, + }; + + if (!questionChanged && !textChangedExternally) { + return; + } + + promptRef.current = nextCustomAnswer; + setComposerCursor(nextCustomAnswer.length); setComposerTrigger( detectComposerTrigger( - activePendingProgress.customAnswer, - expandCollapsedComposerCursor( - activePendingProgress.customAnswer, - activePendingProgress.customAnswer.length, - ), + nextCustomAnswer, + expandCollapsedComposerCursor(nextCustomAnswer, nextCustomAnswer.length), ), ); setComposerHighlightedItemId(null); - }, [activePendingProgress, activePendingUserInput?.requestId]); + }, [ + activePendingProgress?.customAnswer, + activePendingUserInput?.requestId, + activePendingProgress?.activeQuestion?.id, + ]); useEffect(() => { attachmentPreviewHandoffByMessageIdRef.current = attachmentPreviewHandoffByMessageId; }, [attachmentPreviewHandoffByMessageId]);