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) {...@@ -633,23 +633,46 @@ export default function ChatView({ threadId }: ChatViewProps) {
633 pendingUserInputs.length > 0 ||633 pendingUserInputs.length > 0 ||
634 (showPlanFollowUpPrompt && activeProposedPlan !== null);634 (showPlanFollowUpPrompt && activeProposedPlan !== null);
635 const composerFooterHasWideActions = showPlanFollowUpPrompt || activePendingProgress !== null;635 const composerFooterHasWideActions = showPlanFollowUpPrompt || activePendingProgress !== null;
636 const lastSyncedPendingInputRef = useRef<{
637 requestId: string | null;
638 questionId: string | null;
639 } | null>(null);
636 useEffect(() => {640 useEffect(() => {
637 if (!activePendingProgress) {641 const nextCustomAnswer = activePendingProgress?.customAnswer;
642 if (typeof nextCustomAnswer !== "string") {
643 lastSyncedPendingInputRef.current = null;
638 return;644 return;
639 }645 }
640 promptRef.current = activePendingProgress.customAnswer;646 const nextRequestId = activePendingUserInput?.requestId ?? null;
641 setComposerCursor(activePendingProgress.customAnswer.length);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);
642 setComposerTrigger(664 setComposerTrigger(
643 detectComposerTrigger(665 detectComposerTrigger(
644 activePendingProgress.customAnswer,666 nextCustomAnswer,
645 expandCollapsedComposerCursor(667 expandCollapsedComposerCursor(nextCustomAnswer, nextCustomAnswer.length),
646 activePendingProgress.customAnswer,
647 activePendingProgress.customAnswer.length,
648 ),
649 ),668 ),
650 );669 );
651 setComposerHighlightedItemId(null);670 setComposerHighlightedItemId(null);
652 }, [activePendingProgress, activePendingUserInput?.requestId]);671 }, [
672 activePendingProgress?.customAnswer,
673 activePendingUserInput?.requestId,
674 activePendingProgress?.activeQuestion?.id,
675 ]);
653 useEffect(() => {676 useEffect(() => {
654 attachmentPreviewHandoffByMessageIdRef.current = attachmentPreviewHandoffByMessageId;677 attachmentPreviewHandoffByMessageIdRef.current = attachmentPreviewHandoffByMessageId;
655 }, [attachmentPreviewHandoffByMessageId]);678 }, [attachmentPreviewHandoffByMessageId]);
apps/web/src/components/ComposerPromptEditor.tsx+18-11
...@@ -629,6 +629,7 @@ function ComposerPromptEditorInner({...@@ -629,6 +629,7 @@ function ComposerPromptEditorInner({
629 const [editor] = useLexicalComposerContext();629 const [editor] = useLexicalComposerContext();
630 const onChangeRef = useRef(onChange);630 const onChangeRef = useRef(onChange);
631 const snapshotRef = useRef({ value, cursor: clampCursor(value, cursor) });631 const snapshotRef = useRef({ value, cursor: clampCursor(value, cursor) });
632 const isApplyingControlledUpdateRef = useRef(false);
632633
633 useEffect(() => {634 useEffect(() => {
634 onChangeRef.current = onChange;635 onChangeRef.current = onChange;
...@@ -645,21 +646,26 @@ function ComposerPromptEditorInner({...@@ -645,21 +646,26 @@ function ComposerPromptEditorInner({
645 return;646 return;
646 }647 }
647648
648 if (previousSnapshot.value !== value) {
649 editor.update(() => {
650 $setComposerEditorPrompt(value);
651 });
652 }
653
654 snapshotRef.current = { value, cursor: normalizedCursor };649 snapshotRef.current = { value, cursor: normalizedCursor };
655650
656 const rootElement = editor.getRootElement();651 const rootElement = editor.getRootElement();
657 if (!rootElement || document.activeElement !== rootElement) {652 const isFocused = Boolean(rootElement && document.activeElement === rootElement);
653 if (previousSnapshot.value === value && !isFocused) {
658 return;654 return;
659 }655 }
660656
657 isApplyingControlledUpdateRef.current = true;
661 editor.update(() => {658 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;
663 });669 });
664 }, [cursor, editor, value]);670 }, [cursor, editor, value]);
665671
...@@ -705,9 +711,7 @@ function ComposerPromptEditorInner({...@@ -705,9 +711,7 @@ function ComposerPromptEditorInner({
705 focus: () => {711 focus: () => {
706 focusAt(snapshotRef.current.cursor);712 focusAt(snapshotRef.current.cursor);
707 },713 },
708 focusAt: (nextCursor: number) => {714 focusAt,
709 focusAt(nextCursor);
710 },
711 focusAtEnd: () => {715 focusAtEnd: () => {
712 focusAt(snapshotRef.current.value.length);716 focusAt(snapshotRef.current.value.length);
713 },717 },
...@@ -728,6 +732,9 @@ function ComposerPromptEditorInner({...@@ -728,6 +732,9 @@ function ComposerPromptEditorInner({
728 if (previousSnapshot.value === nextValue && previousSnapshot.cursor === nextCursor) {732 if (previousSnapshot.value === nextValue && previousSnapshot.cursor === nextCursor) {
729 return;733 return;
730 }734 }
735 if (isApplyingControlledUpdateRef.current) {
736 return;
737 }
731 snapshotRef.current = {738 snapshotRef.current = {
732 value: nextValue,739 value: nextValue,
733 cursor: nextCursor,740 cursor: nextCursor,