diff --git a/frontend/src/components/workspace/input-box.tsx b/frontend/src/components/workspace/input-box.tsx index 7c75ba605..dad469888 100644 --- a/frontend/src/components/workspace/input-box.tsx +++ b/frontend/src/components/workspace/input-box.tsx @@ -417,6 +417,9 @@ export function InputBox({ useState(null); const lastGeneratedForAiIdRef = useRef(null); const wasStreamingRef = useRef(false); + // Set when the user stops a streaming turn. Such a turn ends on a + // half-finished response, so we must NOT generate follow-up suggestions for it. + const stoppedByUserRef = useRef(false); const messagesRef = useRef(thread.messages); const clearVoiceRestartTimer = useCallback(() => { @@ -1148,6 +1151,16 @@ export function InputBox({ ], ); + const handleStopStreaming = useCallback(() => { + // Mark the in-progress turn as user-interrupted so the next + // streaming->ready transition does not suggest follow-ups for it. + stoppedByUserRef.current = true; + setFollowups([]); + setFollowupsHidden(true); + setFollowupsLoading(false); + onStop?.(); + }, [onStop]); + const handleSubmit = useCallback( async (message: PromptInputMessage) => { if (status === "streaming") { @@ -1200,7 +1213,7 @@ export function InputBox({ return handleCompactCommand(); } if (submitAction.kind === "stop") { - onStop?.(); + handleStopStreaming(); return; } if (submitAction.kind === "empty") { @@ -1215,7 +1228,7 @@ export function InputBox({ abortVoiceInput, handleCompactCommand, handleGoalCommand, - onStop, + handleStopStreaming, selectedSlashSkill, status, submitThreadMessage, @@ -1923,6 +1936,10 @@ export function InputBox({ !showSkillSuggestions && !selectedSlashSkill && !followupsHidden && + // Never show stale follow-up chips while a turn is streaming: a message + // sent before the previous response finished would otherwise leave the + // old chips (and the lone close button) overlapping the input box. + status !== "streaming" && (followupsLoading || followups.length > 0); useEffect(() => { @@ -1945,6 +1962,13 @@ export function InputBox({ return; } + // The turn was interrupted by the user, so skip generating follow-ups for + // this half-finished response. + if (stoppedByUserRef.current) { + stoppedByUserRef.current = false; + return; + } + if (disabled || isMock) { return; } @@ -2666,7 +2690,7 @@ export function InputBox({ onClick={(e) => { if (status === "streaming") { e.preventDefault(); - onStop?.(); + handleStopStreaming(); } }} />