fix(ai): offer Retry when the failed turn persisted partial assistant output (#22478)
## Rationale When a turn fails mid-stream *after* emitting some text, the failed turn's partial assistant message is persisted — so the last message in the thread is the assistant's, and `AiChatErrorUnderMessageList` (which owns the Retry button, gated on the last message being the user's) never renders. The error surfaces through `AiChatMessage` → `AiChatErrorRenderer` instead, and that path never passed `onRetry`. Result: an error banner with no action for the most common failure shape (mid-stream provider errors), most visibly after a reload. ## Why this is the root cause, not a symptom patch This is a wiring omission, not a designed gate. `AiChatErrorRenderer` already accepts `onRetry`, and the server's `retryLastFailedTurn` already deletes the failed turn's assistant messages before re-streaming — the entire retry path for partial-output turns exists and works; only the prop was never threaded. Verified there's no hidden protective reason: retrying with partial output cannot duplicate content, because regeneration is delete-then-restream by design. ## User impact A mid-stream failure currently strands the user: their only options are re-typing the message or reloading. With this, the same Retry affordance appears whether the turn died before or after the first token (Sentry shows 126 users/30d hitting zero-output failures alone — the with-output shape shares the same recovery need). ## Test plan - [x] `AiChatErrorRenderer` retry behavior already covered by existing rendering; change is prop threading only (~10 lines) - [ ] CI green - [ ] Manual: fail a turn mid-stream (kill provider), observe Retry on the in-message error banner https://claude.ai/code/session_01Lyi6zTema2FMVVh8MD6c38 --- _Generated by [Claude Code](https://claude.ai/code/session_01Lyi6zTema2FMVVh8MD6c38)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22478?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+3
@@ -1,4 +1,5 @@
|
||||
import { AiChatMessage } from '@/ai/components/AiChatMessage';
|
||||
import { useRetryChatMessage } from '@/ai/hooks/useRetryChatMessage';
|
||||
import { agentChatDisplayedThreadState } from '@/ai/states/agentChatDisplayedThreadState';
|
||||
import { agentChatErrorComponentFamilyState } from '@/ai/states/agentChatErrorComponentFamilyState';
|
||||
import { agentChatIsStreamingComponentFamilyState } from '@/ai/states/agentChatIsStreamingComponentFamilyState';
|
||||
@@ -9,6 +10,7 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const AiChatLastMessageWithStreamingState = () => {
|
||||
const { retryChatMessage } = useRetryChatMessage();
|
||||
const lastMessageId = useAtomComponentSelectorValue(
|
||||
agentChatLastMessageIdComponentSelector,
|
||||
);
|
||||
@@ -34,6 +36,7 @@ export const AiChatLastMessageWithStreamingState = () => {
|
||||
messageId={lastMessageId}
|
||||
isLastMessageStreaming={agentChatIsStreaming}
|
||||
error={agentChatError ?? undefined}
|
||||
onRetry={retryChatMessage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -144,12 +144,14 @@ type AiChatMessageProps = {
|
||||
messageId: string;
|
||||
isLastMessageStreaming?: boolean;
|
||||
error?: AiChatError | undefined;
|
||||
onRetry?: () => void;
|
||||
};
|
||||
|
||||
export const AiChatMessage = ({
|
||||
messageId,
|
||||
isLastMessageStreaming = false,
|
||||
error,
|
||||
onRetry,
|
||||
}: AiChatMessageProps) => {
|
||||
const agentChatMessage = useAtomComponentFamilySelectorValue(
|
||||
agentChatMessageComponentFamilySelector,
|
||||
@@ -187,7 +189,7 @@ export const AiChatMessage = ({
|
||||
</StyledFilesContainer>
|
||||
)}
|
||||
{shouldShowError && isDefined(error) && (
|
||||
<AiChatErrorRenderer error={error} />
|
||||
<AiChatErrorRenderer error={error} onRetry={onRetry} />
|
||||
)}
|
||||
</StyledMessageContainer>
|
||||
{agentChatMessage.parts.length > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user