fix(desktop): error response improvements — content, retry, model selector
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
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).
This commit is contained in:
@@ -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}
|
||||
>
|
||||
<MessageBubble message={message} setInput={setInput} />
|
||||
<MessageBubble message={message} onRetry={createRetryHandler(message.id)} />
|
||||
</motion.div>
|
||||
))
|
||||
)}
|
||||
@@ -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 && (
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<p className="text-xs text-red-500">{message.error}</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
const text = typeof message.content === 'string' ? message.content : '';
|
||||
if (text) {
|
||||
setInput(text);
|
||||
}
|
||||
}}
|
||||
className="text-xs px-2 py-0.5 rounded bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 hover:bg-red-200 dark:hover:bg-red-900/50 transition-colors"
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
{onRetry && (
|
||||
<button
|
||||
onClick={onRetry}
|
||||
className="text-xs px-2 py-0.5 rounded bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 hover:bg-red-200 dark:hover:bg-red-900/50 transition-colors"
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Download button for AI messages - show on hover */}
|
||||
@@ -716,6 +735,7 @@ interface VirtualizedMessageRowProps {
|
||||
onHeightChange: (height: number) => void;
|
||||
messageRefs: MutableRefObject<Map<string, HTMLDivElement>>;
|
||||
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}
|
||||
>
|
||||
<MessageBubble message={message} setInput={setInput} />
|
||||
<MessageBubble message={message} onRetry={onRetry} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -773,6 +793,7 @@ interface VirtualizedMessageListProps {
|
||||
onHeightChange: (id: string, height: number) => void;
|
||||
messageRefs: MutableRefObject<Map<string, HTMLDivElement>>;
|
||||
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}
|
||||
/>
|
||||
|
||||
@@ -502,10 +502,17 @@ export const useStreamStore = create<StreamState>()(
|
||||
}
|
||||
},
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user