feat: replace optimistic message removal with error response handling

- Updated error handling in `threadSlice` to add agent responses instead of removing user messages.
- Simplified Redux actions by consolidating error handling logic for failed message transmissions.
- Ensured conversation flow consistency by providing fallback error messages to users.
This commit is contained in:
cyrus
2026-03-26 18:40:18 +05:30
parent 2738d3180f
commit 3e76689c24
2 changed files with 24 additions and 54 deletions
+17 -49
View File
@@ -44,7 +44,6 @@ import {
setLastViewed,
setPanelWidth,
setSelectedThread,
updateMessagesForThread,
} from '../store/threadSlice';
import type { ThreadMessage } from '../types/thread';
@@ -344,31 +343,19 @@ const Conversations = () => {
},
onError: event => {
if (event.thread_id !== selectedThreadIdRef.current) return;
if (event.error_type !== 'cancelled') {
setSendError(event.message);
}
setIsSending(false);
setActiveToolCall(null);
dispatch(setActiveThread(null));
// Remove the optimistic user message on error
dispatch((innerDispatch, getState) => {
const state = getState() as {
thread: { messagesByThreadId: Record<string, ThreadMessage[]> };
};
const persistedMessages = state.thread.messagesByThreadId[event.thread_id] || [];
const lastUserIdx = [...persistedMessages]
.reverse()
.findIndex(m => m.sender === 'user');
if (lastUserIdx !== -1) {
const actualIdx = persistedMessages.length - 1 - lastUserIdx;
const updated = persistedMessages.filter((_, i) => i !== actualIdx);
innerDispatch(updateMessagesForThread({ threadId: event.thread_id, messages: updated }));
if (event.thread_id === selectedThreadIdRef.current) {
innerDispatch(setSelectedThread(event.thread_id));
}
}
});
if (event.error_type !== 'cancelled') {
dispatch(
addInferenceResponse({
content: 'Something went wrong — please try again.',
threadId: event.thread_id,
})
);
} else {
dispatch(setActiveThread(null));
}
},
}).then(fn => {
if (mounted) cleanup = fn;
@@ -633,32 +620,13 @@ const Conversations = () => {
// Pass the original sending thread ID to ensure response goes to correct thread
dispatch(addInferenceResponse({ content: finalContent, threadId: sendingThreadId }));
} catch (err) {
// Remove the user message from persistent storage on error
// We'll use a thunk-like approach to access current state
dispatch((innerDispatch, getState) => {
const state = getState() as {
thread: { messagesByThreadId: Record<string, ThreadMessage[]> };
};
const persistedMessages = state.thread.messagesByThreadId[sendingThreadId] || [];
const currentMessages = persistedMessages.filter(m => m.id !== userMessage.id);
innerDispatch(
updateMessagesForThread({ threadId: sendingThreadId, messages: currentMessages })
);
// Also remove from current view if this is the selected thread
if (sendingThreadId === selectedThreadId) {
innerDispatch(setSelectedThread(sendingThreadId));
}
});
const msg =
err && typeof err === 'object' && 'error' in err
? String((err as { error: unknown }).error)
: 'Failed to get response';
setSendError(msg);
// Clear active thread on error
dispatch(setActiveThread(null));
} catch {
dispatch(
addInferenceResponse({
content: 'Something went wrong — please try again.',
threadId: sendingThreadId,
})
);
} finally {
clearTimeout(safetyTimeout);
setIsSending(false);
+7 -5
View File
@@ -114,11 +114,13 @@ export const sendMessage = createAsyncThunk(
return data;
} catch (error) {
// Remove optimistic user message on failure
const state = (getState() as { thread: ThreadState }).thread;
const messages = state.messagesByThreadId[threadId] || [];
const filteredMessages = messages.filter(m => m.id !== userMessage.id);
dispatch(updateMessagesForThread({ threadId, messages: filteredMessages }));
// Add an error message as an agent response so the conversation flow continues
dispatch(
addInferenceResponse({
content: 'Something went wrong — please try again.',
threadId,
})
);
const msg =
error && typeof error === 'object' && 'error' in error