-
Notifications
You must be signed in to change notification settings - Fork 52
feat(threads): dedicated threads controller with per-thread session scoping #590
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
803ba2e
46a38f7
5e00a1e
13d23ed
f095001
bc0e56d
666cadf
bffff60
10a6b17
2ceecd0
bc4644b
084bf3a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -18,13 +18,13 @@ import { useAppDispatch, useAppSelector } from '../store/hooks'; | |||||||||||||||||||||||||||||||||||||||||||
| import { selectSocketStatus } from '../store/socketSelectors'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||
| addMessageLocal, | ||||||||||||||||||||||||||||||||||||||||||||
| createThreadLocal, | ||||||||||||||||||||||||||||||||||||||||||||
| createNewThread, | ||||||||||||||||||||||||||||||||||||||||||||
| deleteThread, | ||||||||||||||||||||||||||||||||||||||||||||
| fetchSuggestedQuestions, | ||||||||||||||||||||||||||||||||||||||||||||
| loadThreadMessages, | ||||||||||||||||||||||||||||||||||||||||||||
| loadThreads, | ||||||||||||||||||||||||||||||||||||||||||||
| persistReaction, | ||||||||||||||||||||||||||||||||||||||||||||
| setActiveThread, | ||||||||||||||||||||||||||||||||||||||||||||
| setLastViewed, | ||||||||||||||||||||||||||||||||||||||||||||
| setSelectedThread, | ||||||||||||||||||||||||||||||||||||||||||||
| } from '../store/threadSlice'; | ||||||||||||||||||||||||||||||||||||||||||||
| import type { ThreadMessage } from '../types/thread'; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -38,8 +38,6 @@ import { | |||||||||||||||||||||||||||||||||||||||||||
| openhumanVoiceTts, | ||||||||||||||||||||||||||||||||||||||||||||
| } from '../utils/tauriCommands'; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const DEFAULT_THREAD_ID = 'default-thread'; | ||||||||||||||||||||||||||||||||||||||||||||
| const DEFAULT_THREAD_TITLE = 'Conversation'; | ||||||||||||||||||||||||||||||||||||||||||||
| const AGENTIC_MODEL_ID = 'agentic-v1'; | ||||||||||||||||||||||||||||||||||||||||||||
| /** Maximum trailing characters rendered in the live-streaming assistant | ||||||||||||||||||||||||||||||||||||||||||||
| * preview bubble. The full response is revealed via `addInferenceResponse` | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -150,6 +148,7 @@ const Conversations = () => { | |||||||||||||||||||||||||||||||||||||||||||
| const dispatch = useAppDispatch(); | ||||||||||||||||||||||||||||||||||||||||||||
| const navigate = useNavigate(); | ||||||||||||||||||||||||||||||||||||||||||||
| const { | ||||||||||||||||||||||||||||||||||||||||||||
| threads, | ||||||||||||||||||||||||||||||||||||||||||||
| selectedThreadId, | ||||||||||||||||||||||||||||||||||||||||||||
| messages, | ||||||||||||||||||||||||||||||||||||||||||||
| isLoadingMessages, | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -159,6 +158,7 @@ const Conversations = () => { | |||||||||||||||||||||||||||||||||||||||||||
| activeThreadId, | ||||||||||||||||||||||||||||||||||||||||||||
| } = useAppSelector(state => state.thread); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const [showSidebar, setShowSidebar] = useState(true); | ||||||||||||||||||||||||||||||||||||||||||||
| const [inputValue, setInputValue] = useState(''); | ||||||||||||||||||||||||||||||||||||||||||||
| const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||
| const [inputMode, setInputMode] = useState<InputMode>('text'); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -222,23 +222,26 @@ const Conversations = () => { | |||||||||||||||||||||||||||||||||||||||||||
| typeof navigator.mediaDevices !== 'undefined' && | ||||||||||||||||||||||||||||||||||||||||||||
| typeof navigator.mediaDevices.getUserMedia === 'function'; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||
| void dispatch(loadThreads()); | ||||||||||||||||||||||||||||||||||||||||||||
| void dispatch( | ||||||||||||||||||||||||||||||||||||||||||||
| createThreadLocal({ | ||||||||||||||||||||||||||||||||||||||||||||
| id: DEFAULT_THREAD_ID, | ||||||||||||||||||||||||||||||||||||||||||||
| title: DEFAULT_THREAD_TITLE, | ||||||||||||||||||||||||||||||||||||||||||||
| createdAt: new Date().toISOString(), | ||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
| ).then(() => { | ||||||||||||||||||||||||||||||||||||||||||||
| dispatch(setSelectedThread(DEFAULT_THREAD_ID)); | ||||||||||||||||||||||||||||||||||||||||||||
| void dispatch(loadThreadMessages(DEFAULT_THREAD_ID)); | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| }, [dispatch]); | ||||||||||||||||||||||||||||||||||||||||||||
| const handleCreateNewThread = async () => { | ||||||||||||||||||||||||||||||||||||||||||||
| const thread = await dispatch(createNewThread()).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||
| dispatch(setSelectedThread(thread.id)); | ||||||||||||||||||||||||||||||||||||||||||||
| void dispatch(loadThreadMessages(thread.id)); | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||
| if (selectedThreadId) dispatch(setLastViewed(selectedThreadId)); | ||||||||||||||||||||||||||||||||||||||||||||
| }, [selectedThreadId, dispatch]); | ||||||||||||||||||||||||||||||||||||||||||||
| void dispatch(loadThreads()) | ||||||||||||||||||||||||||||||||||||||||||||
| .unwrap() | ||||||||||||||||||||||||||||||||||||||||||||
| .then(data => { | ||||||||||||||||||||||||||||||||||||||||||||
| if (data.threads.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||
| const mostRecent = data.threads[0]; | ||||||||||||||||||||||||||||||||||||||||||||
| dispatch(setSelectedThread(mostRecent.id)); | ||||||||||||||||||||||||||||||||||||||||||||
| void dispatch(loadThreadMessages(mostRecent.id)); | ||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||
| void handleCreateNewThread(); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||||||||||||||||||||||||||||||||||||||||||||
| }, [dispatch]); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||
| if (selectedThreadId) { | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -376,11 +379,24 @@ const Conversations = () => { | |||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
| }, [inputMode, rustChat]); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const handleSlashCommand = (command: string): boolean => { | ||||||||||||||||||||||||||||||||||||||||||||
| const cmd = command.toLowerCase(); | ||||||||||||||||||||||||||||||||||||||||||||
| if (cmd === '/new' || cmd === '/clear') { | ||||||||||||||||||||||||||||||||||||||||||||
| setInputValue(''); | ||||||||||||||||||||||||||||||||||||||||||||
| void handleCreateNewThread(); | ||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+384
to
+391
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This block treats Suggested fix const handleSlashCommand = (command: string): boolean => {
const cmd = command.toLowerCase();
- if (cmd === '/new' || cmd === '/clear') {
+ if (cmd === '/clear') {
+ setInputValue('');
+ return true;
+ }
+ if (cmd === '/new') {
setInputValue('');
void handleCreateNewThread();
return true;
}
return false;
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const handleSendMessage = async (text?: string) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const normalized = text ?? inputValue; | ||||||||||||||||||||||||||||||||||||||||||||
| const trimmed = normalized.trim(); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| if (!trimmed || !selectedThreadId || composerBlocked) return; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| if (handleSlashCommand(trimmed)) return; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| if (isAtLimit) { | ||||||||||||||||||||||||||||||||||||||||||||
| setShowLimitModal(true); | ||||||||||||||||||||||||||||||||||||||||||||
| setSendError( | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -685,9 +701,121 @@ const Conversations = () => { | |||||||||||||||||||||||||||||||||||||||||||
| inferenceTurnLifecycleByThread[selectedThreadId] === 'streaming') | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const sortedThreads = [...threads].sort( | ||||||||||||||||||||||||||||||||||||||||||||
| (a, b) => new Date(b.lastMessageAt).getTime() - new Date(a.lastMessageAt).getTime() | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="h-full relative z-10 flex justify-center overflow-hidden p-4 pt-6"> | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="h-full relative z-10 flex overflow-hidden p-4 pt-6 gap-3"> | ||||||||||||||||||||||||||||||||||||||||||||
| {/* Thread sidebar */} | ||||||||||||||||||||||||||||||||||||||||||||
| {showSidebar && ( | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-64 flex-shrink-0 flex flex-col bg-white rounded-2xl shadow-soft border border-stone-200 overflow-hidden"> | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center justify-between px-4 py-3 border-b border-stone-100"> | ||||||||||||||||||||||||||||||||||||||||||||
| <h2 className="text-sm font-semibold text-stone-700">Threads</h2> | ||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => void handleCreateNewThread()} | ||||||||||||||||||||||||||||||||||||||||||||
| className="w-7 h-7 flex items-center justify-center rounded-lg hover:bg-stone-100 text-stone-500 hover:text-stone-700 transition-colors" | ||||||||||||||||||||||||||||||||||||||||||||
| title="New thread"> | ||||||||||||||||||||||||||||||||||||||||||||
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||||||||||||||||||||||||||||||||||||||||||||
| <path | ||||||||||||||||||||||||||||||||||||||||||||
| strokeLinecap="round" | ||||||||||||||||||||||||||||||||||||||||||||
| strokeLinejoin="round" | ||||||||||||||||||||||||||||||||||||||||||||
| strokeWidth={2} | ||||||||||||||||||||||||||||||||||||||||||||
| d="M12 4v16m8-8H4" | ||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||
| </svg> | ||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex-1 overflow-y-auto"> | ||||||||||||||||||||||||||||||||||||||||||||
| {sortedThreads.length === 0 ? ( | ||||||||||||||||||||||||||||||||||||||||||||
| <p className="px-4 py-6 text-xs text-stone-400 text-center">No threads yet</p> | ||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||
| sortedThreads.map(thread => ( | ||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||
| key={thread.id} | ||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => { | ||||||||||||||||||||||||||||||||||||||||||||
| dispatch(setSelectedThread(thread.id)); | ||||||||||||||||||||||||||||||||||||||||||||
| void dispatch(loadThreadMessages(thread.id)); | ||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||
| className={`w-full text-left px-4 py-3 border-b border-stone-50 transition-colors group ${ | ||||||||||||||||||||||||||||||||||||||||||||
| selectedThreadId === thread.id | ||||||||||||||||||||||||||||||||||||||||||||
| ? 'bg-primary-50 border-l-2 border-l-primary-500' | ||||||||||||||||||||||||||||||||||||||||||||
| : 'hover:bg-stone-50' | ||||||||||||||||||||||||||||||||||||||||||||
| }`}> | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center justify-between"> | ||||||||||||||||||||||||||||||||||||||||||||
| <p | ||||||||||||||||||||||||||||||||||||||||||||
| className={`text-sm truncate flex-1 ${ | ||||||||||||||||||||||||||||||||||||||||||||
| selectedThreadId === thread.id | ||||||||||||||||||||||||||||||||||||||||||||
| ? 'font-medium text-primary-700' | ||||||||||||||||||||||||||||||||||||||||||||
| : 'text-stone-700' | ||||||||||||||||||||||||||||||||||||||||||||
| }`}> | ||||||||||||||||||||||||||||||||||||||||||||
| {thread.title} | ||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||
| onClick={e => { | ||||||||||||||||||||||||||||||||||||||||||||
| e.stopPropagation(); | ||||||||||||||||||||||||||||||||||||||||||||
| void dispatch(deleteThread(thread.id)); | ||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+757
to
+760
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider handling delete errors. The delete action fires-and-forgets without user feedback on failure. For destructive operations, users should know if deletion failed. Suggested improvement onClick={e => {
e.stopPropagation();
- void dispatch(deleteThread(thread.id));
+ void dispatch(deleteThread(thread.id))
+ .unwrap()
+ .catch(err => {
+ console.error('[threads] delete failed', err);
+ setSendError(chatSendError('thread_delete_failed', 'Failed to delete thread.'));
+ });
}}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| className="ml-2 p-1 rounded opacity-0 group-hover:opacity-100 hover:bg-stone-200 text-stone-400 hover:text-coral-500 transition-all flex-shrink-0" | ||||||||||||||||||||||||||||||||||||||||||||
| title="Delete thread"> | ||||||||||||||||||||||||||||||||||||||||||||
| <svg | ||||||||||||||||||||||||||||||||||||||||||||
| className="w-3 h-3" | ||||||||||||||||||||||||||||||||||||||||||||
| fill="none" | ||||||||||||||||||||||||||||||||||||||||||||
| stroke="currentColor" | ||||||||||||||||||||||||||||||||||||||||||||
| viewBox="0 0 24 24"> | ||||||||||||||||||||||||||||||||||||||||||||
| <path | ||||||||||||||||||||||||||||||||||||||||||||
| strokeLinecap="round" | ||||||||||||||||||||||||||||||||||||||||||||
| strokeLinejoin="round" | ||||||||||||||||||||||||||||||||||||||||||||
| strokeWidth={2} | ||||||||||||||||||||||||||||||||||||||||||||
| d="M6 18L18 6M6 6l12 12" | ||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||
| </svg> | ||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center gap-2 mt-0.5"> | ||||||||||||||||||||||||||||||||||||||||||||
| <span className="text-[10px] text-stone-400"> | ||||||||||||||||||||||||||||||||||||||||||||
| {formatRelativeTime(thread.lastMessageAt)} | ||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||
| {thread.messageCount > 0 && ( | ||||||||||||||||||||||||||||||||||||||||||||
| <span className="text-[10px] text-stone-400"> | ||||||||||||||||||||||||||||||||||||||||||||
| {thread.messageCount} msg{thread.messageCount !== 1 ? 's' : ''} | ||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+735
to
+787
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid nesting the delete button inside the thread row button. A 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| )) | ||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| {/* Main chat area */} | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex-1 flex flex-col min-w-0 max-w-2xl bg-white rounded-2xl shadow-soft border border-stone-200 overflow-hidden"> | ||||||||||||||||||||||||||||||||||||||||||||
| {/* Chat header */} | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center gap-2 px-4 py-2.5 border-b border-stone-100"> | ||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => setShowSidebar(prev => !prev)} | ||||||||||||||||||||||||||||||||||||||||||||
| className="w-7 h-7 flex items-center justify-center rounded-lg hover:bg-stone-100 text-stone-500 hover:text-stone-700 transition-colors" | ||||||||||||||||||||||||||||||||||||||||||||
| title={showSidebar ? 'Hide sidebar' : 'Show sidebar'}> | ||||||||||||||||||||||||||||||||||||||||||||
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||||||||||||||||||||||||||||||||||||||||||||
| <path | ||||||||||||||||||||||||||||||||||||||||||||
| strokeLinecap="round" | ||||||||||||||||||||||||||||||||||||||||||||
| strokeLinejoin="round" | ||||||||||||||||||||||||||||||||||||||||||||
| strokeWidth={2} | ||||||||||||||||||||||||||||||||||||||||||||
| d="M4 6h16M4 12h16M4 18h16" | ||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||
| </svg> | ||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||
| <h3 className="text-sm font-medium text-stone-700 truncate flex-1"> | ||||||||||||||||||||||||||||||||||||||||||||
| {threads.find(t => t.id === selectedThreadId)?.title ?? 'Select a thread'} | ||||||||||||||||||||||||||||||||||||||||||||
| </h3> | ||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => void handleCreateNewThread()} | ||||||||||||||||||||||||||||||||||||||||||||
| className="px-2.5 py-1 rounded-lg text-xs font-medium text-primary-600 hover:bg-primary-50 transition-colors" | ||||||||||||||||||||||||||||||||||||||||||||
| title="New thread (/new)"> | ||||||||||||||||||||||||||||||||||||||||||||
| + New | ||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex-1 overflow-y-auto px-5 py-4 bg-stone-50"> | ||||||||||||||||||||||||||||||||||||||||||||
| {isLoadingMessages ? ( | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="space-y-4"> | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle bootstrap/create failures instead of dropping the promise.
Both
dispatch(loadThreads()).unwrap()anddispatch(createNewThread()).unwrap()can reject, but this flow launches them fire-and-forget and never catches the error. A backend failure here becomes an unhandled promise rejection and can leave the page with no selected thread or user feedback.🤖 Prompt for AI Agents