fix(chat): soften aborted turn message

This commit is contained in:
Bohdan Triapitsyn
2026-04-30 00:51:28 +03:00
parent e6c7cc5589
commit 6cb5e4342f
2 changed files with 39 additions and 7 deletions
@@ -659,7 +659,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
// Summary body removed — flat rendering means text is always inline. // Summary body removed — flat rendering means text is always inline.
const assistantErrorText = React.useMemo(() => { const assistantError = React.useMemo(() => {
if (isUser) { if (isUser) {
return undefined; return undefined;
} }
@@ -677,14 +677,32 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
return undefined; return undefined;
} }
if (errorName === 'SessionRetry') { 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)) { 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]); }, [isUser, message.info]);
const assistantErrorText = assistantError?.text;
const assistantErrorVariant = assistantError?.variant;
const messageTextContent = React.useMemo(() => { const messageTextContent = React.useMemo(() => {
if (isUser) { if (isUser) {
const shellOutputs = displayParts const shellOutputs = displayParts
@@ -1031,6 +1049,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
onRevert={handleRevert} onRevert={handleRevert}
onFork={isUser ? handleFork : undefined} onFork={isUser ? handleFork : undefined}
errorMessage={assistantErrorText} errorMessage={assistantErrorText}
errorVariant={assistantErrorVariant}
userActionsMode={useExternalUserActionsRow ? 'external-content' : 'inline'} userActionsMode={useExternalUserActionsRow ? 'external-content' : 'inline'}
stickyUserHeaderEnabled={stickyUserHeader} stickyUserHeaderEnabled={stickyUserHeader}
/> />
@@ -1063,6 +1082,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
onRevert={handleRevert} onRevert={handleRevert}
onFork={isUser ? handleFork : undefined} onFork={isUser ? handleFork : undefined}
errorMessage={assistantErrorText} errorMessage={assistantErrorText}
errorVariant={assistantErrorVariant}
userActionsMode="external-actions" userActionsMode="external-actions"
stickyUserHeaderEnabled={stickyUserHeader} stickyUserHeaderEnabled={stickyUserHeader}
/> />
@@ -1113,6 +1133,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
agentMention={agentMention} agentMention={agentMention}
turnGroupingContext={turnGroupingContext} turnGroupingContext={turnGroupingContext}
errorMessage={assistantErrorText} errorMessage={assistantErrorText}
errorVariant={assistantErrorVariant}
/> />
</div> </div>
@@ -16,7 +16,7 @@ import { FadeInOnReveal } from './FadeInOnReveal';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { SaveProjectPlanDialog } from '@/components/session/SaveProjectPlanDialog'; import { SaveProjectPlanDialog } from '@/components/session/SaveProjectPlanDialog';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; 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 { ArrowsMerge } from '@/components/icons/ArrowsMerge';
import type { ContentChangeReason } from '@/hooks/useChatScrollManager'; import type { ContentChangeReason } from '@/hooks/useChatScrollManager';
@@ -303,6 +303,7 @@ interface MessageBodyProps {
onRevert?: () => void; onRevert?: () => void;
onFork?: () => void; onFork?: () => void;
errorMessage?: string; errorMessage?: string;
errorVariant?: 'error' | 'info';
userActionsMode?: 'inline' | 'external-content' | 'external-actions'; userActionsMode?: 'inline' | 'external-content' | 'external-actions';
stickyUserHeaderEnabled?: boolean; stickyUserHeaderEnabled?: boolean;
} }
@@ -850,6 +851,7 @@ const AssistantMessageBody = React.memo(({
showReasoningTraces = false, showReasoningTraces = false,
turnGroupingContext, turnGroupingContext,
errorMessage, errorMessage,
errorVariant = 'error',
}: Omit<MessageBodyProps, 'isUser'>) => { }: Omit<MessageBodyProps, 'isUser'>) => {
const { t } = useI18n(); const { t } = useI18n();
const streamPhase = _streamPhase; const streamPhase = _streamPhase;
@@ -1386,6 +1388,7 @@ const AssistantMessageBody = React.memo(({
const shouldDeferSortedInlineText = isSortedRenderMode && !hasStopFinish; const shouldDeferSortedInlineText = isSortedRenderMode && !hasStopFinish;
const showErrorMessage = Boolean(errorMessage); const showErrorMessage = Boolean(errorMessage);
const ErrorIcon = errorVariant === 'info' ? RiInformationLine : RiErrorWarningLine;
const shouldShowMessageActions = hasCopyableText; const shouldShowMessageActions = hasCopyableText;
const shouldShowTurnFooter = isLastAssistantInTurn && hasTextContent && (hasStopFinish || Boolean(errorMessage)); const shouldShowTurnFooter = isLastAssistantInTurn && hasTextContent && (hasStopFinish || Boolean(errorMessage));
const shouldRenderActionsInActivity = isSortedRenderMode; const shouldRenderActionsInActivity = isSortedRenderMode;
@@ -1798,9 +1801,17 @@ const AssistantMessageBody = React.memo(({
{renderedParts} {renderedParts}
{showErrorMessage && ( {showErrorMessage && (
<FadeInOnReveal key="assistant-error"> <FadeInOnReveal key="assistant-error">
<div className="group/assistant-text relative mt-3 p-3 rounded-lg border bg-[var(--status-error-background)] border-[var(--status-error-border)] break-words max-w-full"> <div className={cn(
'group/assistant-text relative mt-3 p-3 rounded-lg border break-words max-w-full',
errorVariant === 'info'
? 'bg-[var(--status-info-background)] border-[var(--status-info-border)]'
: 'bg-[var(--status-error-background)] border-[var(--status-error-border)]',
)}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<RiErrorWarningLine className="h-4 w-4 shrink-0 text-[var(--status-error)]" /> <ErrorIcon className={cn(
'h-4 w-4 shrink-0',
errorVariant === 'info' ? 'text-[var(--status-info)]' : 'text-[var(--status-error)]',
)} />
<div className="min-w-0 flex-1 break-words"> <div className="min-w-0 flex-1 break-words">
<SimpleMarkdownRenderer <SimpleMarkdownRenderer
content={errorMessage ?? ''} content={errorMessage ?? ''}