mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
fix(frontend): surface a toast when send is suppressed during reconnect
shouldSuppressDuplicateSend returned a boolean for two very different
cases (reconnect-in-flight vs duplicate text echo), so the caller had
to silently drop both. Flagged by Sentry (MEDIUM): the React state
driving the disabled-input UI lags the synchronous ref by one render,
so a user can click send against a still-enabled input and have the
message vanish with no feedback.
Split into getSendSuppressionReason() returning "reconnecting" |
"duplicate" | null. useCopilotStream shows a toast for reconnect
("Wait for the connection to resume before sending") and keeps the
silent drop for duplicate-text echoes from the session. Boolean wrapper
retained as deprecated for back-compat.
This commit is contained in:
@@ -82,17 +82,30 @@ interface SuppressDuplicateArgs {
|
||||
messages: UIMessage[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reason a sendMessage was suppressed, or ``null`` to pass through.
|
||||
*
|
||||
* - ``"reconnecting"``: the stream is reconnecting; the caller should
|
||||
* notify the user (the UI may not yet reflect the disabled state).
|
||||
* - ``"duplicate"``: the same text was just submitted and echoed back
|
||||
* by the session — safe to silently drop (user double-clicked).
|
||||
*/
|
||||
export type SuppressReason = "reconnecting" | "duplicate" | null;
|
||||
|
||||
/**
|
||||
* Determine whether a sendMessage call should be suppressed to prevent
|
||||
* duplicate POSTs during reconnect cycles or re-submits of the same text.
|
||||
*
|
||||
* Returns the reason so callers can surface user-visible feedback when
|
||||
* the suppression isn't just a silent duplicate.
|
||||
*/
|
||||
export function shouldSuppressDuplicateSend({
|
||||
export function getSendSuppressionReason({
|
||||
text,
|
||||
isReconnectScheduled,
|
||||
lastSubmittedText,
|
||||
messages,
|
||||
}: SuppressDuplicateArgs): boolean {
|
||||
if (isReconnectScheduled) return true;
|
||||
}: SuppressDuplicateArgs): SuppressReason {
|
||||
if (isReconnectScheduled) return "reconnecting";
|
||||
|
||||
if (text && lastSubmittedText === text) {
|
||||
const lastUserMsg = messages.filter((m) => m.role === "user").pop();
|
||||
@@ -100,10 +113,22 @@ export function shouldSuppressDuplicateSend({
|
||||
?.map((p) => ("text" in p ? p.text : ""))
|
||||
.join("")
|
||||
.trim();
|
||||
if (lastUserText === text) return true;
|
||||
if (lastUserText === text) return "duplicate";
|
||||
}
|
||||
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backwards-compatible boolean wrapper for ``getSendSuppressionReason``.
|
||||
*
|
||||
* @deprecated Call ``getSendSuppressionReason`` directly so callers can
|
||||
* distinguish between reconnect and duplicate suppression.
|
||||
*/
|
||||
export function shouldSuppressDuplicateSend(
|
||||
args: SuppressDuplicateArgs,
|
||||
): boolean {
|
||||
return getSendSuppressionReason(args) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
extractSendMessageText,
|
||||
hasActiveBackendStream,
|
||||
resolveInProgressTools,
|
||||
shouldSuppressDuplicateSend,
|
||||
getSendSuppressionReason,
|
||||
} from "./helpers";
|
||||
|
||||
const RECONNECT_BASE_DELAY_MS = 1_000;
|
||||
@@ -260,15 +260,25 @@ export function useCopilotStream({
|
||||
const sendMessage: typeof sdkSendMessage = async (...args) => {
|
||||
const text = extractSendMessageText(args[0]);
|
||||
|
||||
if (
|
||||
shouldSuppressDuplicateSend({
|
||||
text,
|
||||
isReconnectScheduled: isReconnectScheduledRef.current,
|
||||
lastSubmittedText: lastSubmittedMsgRef.current,
|
||||
messages: rawMessages,
|
||||
})
|
||||
)
|
||||
const suppressReason = getSendSuppressionReason({
|
||||
text,
|
||||
isReconnectScheduled: isReconnectScheduledRef.current,
|
||||
lastSubmittedText: lastSubmittedMsgRef.current,
|
||||
messages: rawMessages,
|
||||
});
|
||||
|
||||
if (suppressReason === "reconnecting") {
|
||||
// The ref flips to ``true`` synchronously while the React state that
|
||||
// drives the UI's disabled state only updates on the next render, so
|
||||
// the user may have clicked send against a still-enabled input. Tell
|
||||
// them their message wasn't dropped silently.
|
||||
toast({
|
||||
title: "Reconnecting",
|
||||
description: "Wait for the connection to resume before sending.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (suppressReason === "duplicate") return;
|
||||
|
||||
lastSubmittedMsgRef.current = text;
|
||||
return sdkSendMessage(...args);
|
||||
|
||||
Reference in New Issue
Block a user