From 6cb5e4342f241870e47fc85147aa2fb5e5297ada Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Thu, 30 Apr 2026 00:51:28 +0300 Subject: [PATCH] fix(chat): soften aborted turn message --- .../ui/src/components/chat/ChatMessage.tsx | 29 ++++++++++++++++--- .../components/chat/message/MessageBody.tsx | 17 +++++++++-- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/components/chat/ChatMessage.tsx b/packages/ui/src/components/chat/ChatMessage.tsx index a2ecf62d..31304e75 100644 --- a/packages/ui/src/components/chat/ChatMessage.tsx +++ b/packages/ui/src/components/chat/ChatMessage.tsx @@ -659,7 +659,7 @@ const ChatMessage: React.FC = ({ // Summary body removed — flat rendering means text is always inline. - const assistantErrorText = React.useMemo(() => { + const assistantError = React.useMemo(() => { if (isUser) { return undefined; } @@ -677,14 +677,32 @@ const ChatMessage: React.FC = ({ return undefined; } if (errorName === 'SessionRetry') { - return `Opencode failed to send a message. Retry attempt info: \n\`${detail}\``; + return { + text: `Opencode failed to send a message. Retry attempt info: \n\`${detail}\``, + variant: 'info' as const, + }; } if (isLikelyProviderAuthFailure(detail)) { - return PROVIDER_AUTH_FAILURE_MESSAGE; + return { + text: PROVIDER_AUTH_FAILURE_MESSAGE, + variant: 'error' as const, + }; } - return `Opencode failed to send message with error:\n\`${detail}\``; + if (detail.trim().toLowerCase() === 'aborted') { + return { + text: 'The running turn was stopped before OpenCode could send the next message.', + variant: 'info' as const, + }; + } + return { + text: `Opencode failed to send message with error:\n\`${detail}\``, + variant: 'error' as const, + }; }, [isUser, message.info]); + const assistantErrorText = assistantError?.text; + const assistantErrorVariant = assistantError?.variant; + const messageTextContent = React.useMemo(() => { if (isUser) { const shellOutputs = displayParts @@ -1031,6 +1049,7 @@ const ChatMessage: React.FC = ({ onRevert={handleRevert} onFork={isUser ? handleFork : undefined} errorMessage={assistantErrorText} + errorVariant={assistantErrorVariant} userActionsMode={useExternalUserActionsRow ? 'external-content' : 'inline'} stickyUserHeaderEnabled={stickyUserHeader} /> @@ -1063,6 +1082,7 @@ const ChatMessage: React.FC = ({ onRevert={handleRevert} onFork={isUser ? handleFork : undefined} errorMessage={assistantErrorText} + errorVariant={assistantErrorVariant} userActionsMode="external-actions" stickyUserHeaderEnabled={stickyUserHeader} /> @@ -1113,6 +1133,7 @@ const ChatMessage: React.FC = ({ agentMention={agentMention} turnGroupingContext={turnGroupingContext} errorMessage={assistantErrorText} + errorVariant={assistantErrorVariant} /> diff --git a/packages/ui/src/components/chat/message/MessageBody.tsx b/packages/ui/src/components/chat/message/MessageBody.tsx index ea83a31b..c0b54af6 100644 --- a/packages/ui/src/components/chat/message/MessageBody.tsx +++ b/packages/ui/src/components/chat/message/MessageBody.tsx @@ -16,7 +16,7 @@ import { FadeInOnReveal } from './FadeInOnReveal'; import { Button } from '@/components/ui/button'; import { SaveProjectPlanDialog } from '@/components/session/SaveProjectPlanDialog'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; -import { RiCheckLine, RiFileCopyLine, RiChatNewLine, RiArrowGoBackLine, RiGitBranchLine, RiHourglassLine, RiTimeLine, RiVolumeUpLine, RiStopLine, RiImageDownloadLine, RiLoader4Line, RiErrorWarningLine, RiBookletLine, RiGlobalLine } from '@remixicon/react'; +import { RiCheckLine, RiFileCopyLine, RiChatNewLine, RiArrowGoBackLine, RiGitBranchLine, RiHourglassLine, RiTimeLine, RiVolumeUpLine, RiStopLine, RiImageDownloadLine, RiLoader4Line, RiErrorWarningLine, RiBookletLine, RiGlobalLine, RiInformationLine } from '@remixicon/react'; import { ArrowsMerge } from '@/components/icons/ArrowsMerge'; import type { ContentChangeReason } from '@/hooks/useChatScrollManager'; @@ -303,6 +303,7 @@ interface MessageBodyProps { onRevert?: () => void; onFork?: () => void; errorMessage?: string; + errorVariant?: 'error' | 'info'; userActionsMode?: 'inline' | 'external-content' | 'external-actions'; stickyUserHeaderEnabled?: boolean; } @@ -850,6 +851,7 @@ const AssistantMessageBody = React.memo(({ showReasoningTraces = false, turnGroupingContext, errorMessage, + errorVariant = 'error', }: Omit) => { const { t } = useI18n(); const streamPhase = _streamPhase; @@ -1386,6 +1388,7 @@ const AssistantMessageBody = React.memo(({ const shouldDeferSortedInlineText = isSortedRenderMode && !hasStopFinish; const showErrorMessage = Boolean(errorMessage); + const ErrorIcon = errorVariant === 'info' ? RiInformationLine : RiErrorWarningLine; const shouldShowMessageActions = hasCopyableText; const shouldShowTurnFooter = isLastAssistantInTurn && hasTextContent && (hasStopFinish || Boolean(errorMessage)); const shouldRenderActionsInActivity = isSortedRenderMode; @@ -1798,9 +1801,17 @@ const AssistantMessageBody = React.memo(({ {renderedParts} {showErrorMessage && ( -
+
- +