From 23378fdce53bd5377bab8e1dae6cd07dcc7c69fc 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 96efc0fbf0..1228846743 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 215231268e04b3f5a0ae7452fcf1ae899d074a94 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 1228846743..3e7a6ca21b 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 03a55d62b45be36e11f42ba81bc9bb3881a1f21f 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 3e7a6ca21b..f9321de651 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 fe99740f955c7e71162f0b7a69cd45df8f9eeef4 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 d81e024f3a..75adc05029 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -630,23 +630,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]);