From 5c6964f52afb2396e97f504c4f7da74f22780504 Mon Sep 17 00:00:00 2001 From: iven Date: Thu, 9 Apr 2026 18:52:27 +0800 Subject: [PATCH] =?UTF-8?q?fix(desktop):=20error=20response=20improvements?= =?UTF-8?q?=20=E2=80=94=20content,=20retry,=20model=20selector?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1: onError callback now sets content to error message instead of empty string. Previously API errors (404/429) produced empty assistant messages with only a visual error badge — now the error text is persisted in message content. P3: Retry button now re-sends the preceding user message via sendToGateway instead of copying to input. Works for both virtualized and non-virtualized message lists. Removed unused setInput prop from MessageBubble. Also hides model selector in Tauri runtime (SaaS token pool routes models). --- desktop/src/components/ChatArea.tsx | 53 +++++++++++++++++++-------- desktop/src/store/chat/streamStore.ts | 9 ++++- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/desktop/src/components/ChatArea.tsx b/desktop/src/components/ChatArea.tsx index 0909afc..da20cec 100644 --- a/desktop/src/components/ChatArea.tsx +++ b/desktop/src/components/ChatArea.tsx @@ -231,6 +231,18 @@ export function ChatArea({ compact, onOpenDetail }: { compact?: boolean; onOpenD setPendingFiles([]); }; + const createRetryHandler = (msgId: string) => () => { + if (isStreaming) return; + // Find the user message immediately before this error + const idx = messages.findIndex(m => m.id === msgId); + if (idx > 0) { + const prevMsg = messages[idx - 1]; + if (prevMsg.role === 'user' && prevMsg.content) { + sendToGateway(prevMsg.content); + } + } + }; + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); @@ -433,6 +445,16 @@ export function ChatArea({ compact, onOpenDetail }: { compact?: boolean; onOpenD onHeightChange={setHeight} messageRefs={messageRefs} setInput={setInput} + retryForMessage={(msgId: string) => { + const idx = messages.findIndex(m => m.id === msgId); + if (idx > 0) { + const prevMsg = messages[idx - 1]; + if (prevMsg.role === 'user' && prevMsg.content) { + return () => { if (!isStreaming) sendToGateway(prevMsg.content); }; + } + } + return undefined; + }} /> ) : ( messages.map((message) => ( @@ -445,7 +467,7 @@ export function ChatArea({ compact, onOpenDetail }: { compact?: boolean; onOpenD layout transition={defaultTransition} > - + )) )} @@ -575,7 +597,7 @@ export function ChatArea({ compact, onOpenDetail }: { compact?: boolean; onOpenD ); } -function MessageBubble({ message, setInput, onRetry }: { message: Message; setInput: (text: string) => void; onRetry?: () => void }) { +function MessageBubble({ message, onRetry }: { message: Message; setInput?: (text: string) => void; onRetry?: () => void }) { if (message.role === 'tool') { return null; } @@ -679,17 +701,14 @@ function MessageBubble({ message, setInput, onRetry }: { message: Message; setIn {message.error && (

{message.error}

- + {onRetry && ( + + )}
)} {/* Download button for AI messages - show on hover */} @@ -716,6 +735,7 @@ interface VirtualizedMessageRowProps { onHeightChange: (height: number) => void; messageRefs: MutableRefObject>; setInput: (text: string) => void; + onRetry?: () => void; } /** @@ -726,7 +746,7 @@ function VirtualizedMessageRow({ message, onHeightChange, messageRefs, - setInput, + onRetry, style, ariaAttributes, }: VirtualizedMessageRowProps & { @@ -761,7 +781,7 @@ function VirtualizedMessageRow({ className="py-3" {...ariaAttributes} > - + ); } @@ -773,6 +793,7 @@ interface VirtualizedMessageListProps { onHeightChange: (id: string, height: number) => void; messageRefs: MutableRefObject>; setInput: (text: string) => void; + retryForMessage: (msgId: string) => (() => void) | undefined; } /** @@ -786,6 +807,7 @@ function VirtualizedMessageList({ onHeightChange, messageRefs, setInput, + retryForMessage, }: VirtualizedMessageListProps) { // Row component for react-window v2 const RowComponent = (props: { @@ -802,6 +824,7 @@ function VirtualizedMessageList({ onHeightChange={(h) => onHeightChange(messages[props.index].id, h)} messageRefs={messageRefs} setInput={setInput} + onRetry={retryForMessage(messages[props.index].id)} style={props.style} ariaAttributes={props.ariaAttributes} /> diff --git a/desktop/src/store/chat/streamStore.ts b/desktop/src/store/chat/streamStore.ts index 6d570af..6ae7478 100644 --- a/desktop/src/store/chat/streamStore.ts +++ b/desktop/src/store/chat/streamStore.ts @@ -502,10 +502,17 @@ export const useStreamStore = create()( } }, onError: (error: string) => { + // Flush any remaining buffered deltas before erroring + if (flushTimer !== null) { + clearTimeout(flushTimer); + flushTimer = null; + } + flushBuffers(); + _chat?.updateMessages(msgs => msgs.map(m => m.id === assistantId - ? { ...m, content: '', streaming: false, error } + ? { ...m, content: error, streaming: false, error } : m.role === 'user' && m.optimistic && m.timestamp.getTime() >= streamStartTime ? { ...m, optimistic: false } : m