Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
  • Loading branch information
graycyrus committed Mar 26, 2026
commit 3e76689c247b56fed7e8f7201df10a5237b03210
66 changes: 17 additions & 49 deletions src/pages/Conversations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
setLastViewed,
setPanelWidth,
setSelectedThread,
updateMessagesForThread,
} from '../store/threadSlice';
import type { ThreadMessage } from '../types/thread';

Expand Down Expand Up @@ -344,31 +343,19 @@
},
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;
Expand Down Expand Up @@ -419,7 +406,7 @@
const handleSendMessageWeb = async (
sendingThreadId: string,
trimmed: string,
userMessage: ThreadMessage,

Check failure on line 409 in src/pages/Conversations.tsx

View workflow job for this annotation

GitHub Actions / Type Check TypeScript

'userMessage' is declared but its value is never read.

Check failure on line 409 in src/pages/Conversations.tsx

View workflow job for this annotation

GitHub Actions / Build Tauri App

'userMessage' is declared but its value is never read.
historySnapshot: ThreadMessage[]
) => {
// Safety-net timeout: force-clear loading states if everything hangs
Expand Down Expand Up @@ -633,32 +620,13 @@

// 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);
Expand Down
12 changes: 7 additions & 5 deletions src/store/threadSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
'thread/sendMessage',
async (
{ threadId, message }: { threadId: string; message: string },
{ dispatch, getState, rejectWithValue }

Check failure on line 86 in src/store/threadSlice.ts

View workflow job for this annotation

GitHub Actions / Type Check TypeScript

'getState' is declared but its value is never read.

Check failure on line 86 in src/store/threadSlice.ts

View workflow job for this annotation

GitHub Actions / Build Tauri App

'getState' is declared but its value is never read.
) => {
// 1. Add user message locally immediately (optimistic update)
const userMessage: ThreadMessage = {
Expand Down Expand Up @@ -114,11 +114,13 @@

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
Expand Down
Loading