authorgravatar for noah@wts.devNoah Gregory <noah@wts.dev> 2026-03-12 14:24:10-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-12 11:24:10-07:00
logf63cda22fa6e9b98de1caec057960dce28248d37
treebbff4995b79c62645056785d4dc17c56d6af0d7c
parentb60a34eba46ffb3e97d90a644280ba743870d85d
signature Signed by PGP key B5690EEEBB952194

fix: improve business logic in prompt editor and fix cursor bugs in Plan mode (#867)

Co-authored-by: Julius Marminge <julius0216@outlook.com>

2 files changed, 50 insertions(+), 20 deletions(-)

apps/web/src/components/ChatView.tsx+32-9
......@@ -633,23 +633,46 @@ export default function ChatView({ threadId }: ChatViewProps) {
633633 pendingUserInputs.length > 0 ||
634634 (showPlanFollowUpPrompt && activeProposedPlan !== null);
635635 const composerFooterHasWideActions = showPlanFollowUpPrompt || activePendingProgress !== null;
636 const lastSyncedPendingInputRef = useRef<{
637 requestId: string | null;
638 questionId: string | null;
639 } | null>(null);
636640 useEffect(() => {
637 if (!activePendingProgress) {
641 const nextCustomAnswer = activePendingProgress?.customAnswer;
642 if (typeof nextCustomAnswer !== "string") {
643 lastSyncedPendingInputRef.current = null;
638644 return;
639645 }
640 promptRef.current = activePendingProgress.customAnswer;
641 setComposerCursor(activePendingProgress.customAnswer.length);
646 const nextRequestId = activePendingUserInput?.requestId ?? null;
647 const nextQuestionId = activePendingProgress?.activeQuestion?.id ?? null;
648 const questionChanged =
649 lastSyncedPendingInputRef.current?.requestId !== nextRequestId ||
650 lastSyncedPendingInputRef.current?.questionId !== nextQuestionId;
651 const textChangedExternally = promptRef.current !== nextCustomAnswer;
652
653 lastSyncedPendingInputRef.current = {
654 requestId: nextRequestId,
655 questionId: nextQuestionId,
656 };
657
658 if (!questionChanged && !textChangedExternally) {
659 return;
660 }
661
662 promptRef.current = nextCustomAnswer;
663 setComposerCursor(nextCustomAnswer.length);
642664 setComposerTrigger(
643665 detectComposerTrigger(
644 activePendingProgress.customAnswer,
645 expandCollapsedComposerCursor(
646 activePendingProgress.customAnswer,
647 activePendingProgress.customAnswer.length,
648 ),
666 nextCustomAnswer,
667 expandCollapsedComposerCursor(nextCustomAnswer, nextCustomAnswer.length),
649668 ),
650669 );
651670 setComposerHighlightedItemId(null);
652 }, [activePendingProgress, activePendingUserInput?.requestId]);
671 }, [
672 activePendingProgress?.customAnswer,
673 activePendingUserInput?.requestId,
674 activePendingProgress?.activeQuestion?.id,
675 ]);
653676 useEffect(() => {
654677 attachmentPreviewHandoffByMessageIdRef.current = attachmentPreviewHandoffByMessageId;
655678 }, [attachmentPreviewHandoffByMessageId]);
apps/web/src/components/ComposerPromptEditor.tsx+18-11
......@@ -629,6 +629,7 @@ function ComposerPromptEditorInner({
629629 const [editor] = useLexicalComposerContext();
630630 const onChangeRef = useRef(onChange);
631631 const snapshotRef = useRef({ value, cursor: clampCursor(value, cursor) });
632 const isApplyingControlledUpdateRef = useRef(false);
632633
633634 useEffect(() => {
634635 onChangeRef.current = onChange;
......@@ -645,21 +646,26 @@ function ComposerPromptEditorInner({
645646 return;
646647 }
647648
648 if (previousSnapshot.value !== value) {
649 editor.update(() => {
650 $setComposerEditorPrompt(value);
651 });
652 }
653
654649 snapshotRef.current = { value, cursor: normalizedCursor };
655650
656651 const rootElement = editor.getRootElement();
657 if (!rootElement || document.activeElement !== rootElement) {
652 const isFocused = Boolean(rootElement && document.activeElement === rootElement);
653 if (previousSnapshot.value === value && !isFocused) {
658654 return;
659655 }
660656
657 isApplyingControlledUpdateRef.current = true;
661658 editor.update(() => {
662 $setSelectionAtComposerOffset(normalizedCursor);
659 const valueChanged = previousSnapshot.value !== value;
660 if (previousSnapshot.value !== value) {
661 $setComposerEditorPrompt(value);
662 }
663 if (valueChanged || isFocused) {
664 $setSelectionAtComposerOffset(normalizedCursor);
665 }
666 });
667 queueMicrotask(() => {
668 isApplyingControlledUpdateRef.current = false;
663669 });
664670 }, [cursor, editor, value]);
665671
......@@ -705,9 +711,7 @@ function ComposerPromptEditorInner({
705711 focus: () => {
706712 focusAt(snapshotRef.current.cursor);
707713 },
708 focusAt: (nextCursor: number) => {
709 focusAt(nextCursor);
710 },
714 focusAt,
711715 focusAtEnd: () => {
712716 focusAt(snapshotRef.current.value.length);
713717 },
......@@ -728,6 +732,9 @@ function ComposerPromptEditorInner({
728732 if (previousSnapshot.value === nextValue && previousSnapshot.cursor === nextCursor) {
729733 return;
730734 }
735 if (isApplyingControlledUpdateRef.current) {
736 return;
737 }
731738 snapshotRef.current = {
732739 value: nextValue,
733740 cursor: nextCursor,