diff --git a/src/hooks/useTKMindChat.ts b/src/hooks/useTKMindChat.ts index cb60848..75f65e7 100644 --- a/src/hooks/useTKMindChat.ts +++ b/src/hooks/useTKMindChat.ts @@ -82,6 +82,11 @@ function isDirectChatSessionId(sessionId?: string | null) { return typeof sessionId === 'string' && sessionId.startsWith(DIRECT_CHAT_SESSION_PREFIX); } +function getSessionEventRequestId(event: SessionEvent): string | undefined { + const raw = event as SessionEvent & { chat_request_id?: string; request_id?: string }; + return raw.chat_request_id ?? raw.request_id; +} + function sessionFinishedViaPortalDirectChat(messages: Message[], userMessage: Message) { const lastAssistant = [...messages].reverse().find((message) => message.role === 'assistant'); if (lastAssistant?.metadata?.source !== 'portal-direct-chat') return false; @@ -278,6 +283,8 @@ export function useTKMindChat( const activeRequestId = useRef(null); const activeRequestMissingTimerRef = useRef | null>(null); + const chatStateRef = useRef(chatState); + const subscribedSessionIdRef = useRef(null); const unsubscribeRef = useRef<(() => void) | null>(null); const connectTokenRef = useRef(0); const messagesRef = useRef([]); @@ -295,6 +302,10 @@ export function useTKMindChat( const onUserUpdateRef = useRef(onUserUpdate); const chatImageCategoryIdRef = useRef(null); + useEffect(() => { + chatStateRef.current = chatState; + }, [chatState]); + const clearActiveRequestMissingTimer = useCallback(() => { if (!activeRequestMissingTimerRef.current) return; window.clearTimeout(activeRequestMissingTimerRef.current); @@ -718,10 +729,22 @@ export function useTKMindChat( } }, []); + const scheduleReplyRecoverySync = useCallback( + (sessionId: string, submitToken: number) => { + for (const delay of REPLY_RECOVERY_SYNC_DELAYS_MS) { + window.setTimeout(() => { + if (connectTokenRef.current !== submitToken) return; + if (sessionRef.current?.id !== sessionId) return; + void syncSessionMessages(sessionId); + }, delay); + } + }, + [syncSessionMessages], + ); + const processEvent = useCallback( (event: SessionEvent, requestId: string, sessionId: string) => { - const raw = event as SessionEvent & { chat_request_id?: string; request_id?: string }; - const eventRequestId = raw.chat_request_id ?? raw.request_id; + const eventRequestId = getSessionEventRequestId(event); if (eventRequestId && eventRequestId !== requestId) return; clearActiveRequestMissingTimer(); @@ -824,6 +847,7 @@ export function useTKMindChat( if (isDirectChatSessionId(sessionId)) { unsubscribeRef.current?.(); unsubscribeRef.current = null; + subscribedSessionIdRef.current = null; } setSessions((prev) => touchSession(prev, sessionId, 0)); if (userRef.current && onUserUpdateRef.current) { @@ -857,11 +881,15 @@ export function useTKMindChat( const subscribeToSession = useCallback( (sessionId: string) => { + if (subscribedSessionIdRef.current === sessionId && unsubscribeRef.current) { + return; + } + subscribedSessionIdRef.current = sessionId; unsubscribeRef.current?.(); unsubscribeRef.current = subscribeSessionEvents( sessionId, (event) => { - const rid = activeRequestId.current; + let rid = activeRequestId.current; if (event.type === 'ActiveRequests') { if (!rid && event.request_ids.length > 0) { // SSE reconnected while agent was running — adopt the active request. @@ -871,6 +899,11 @@ export function useTKMindChat( } else if (rid && event.request_ids.includes(rid)) { clearActiveRequestMissingTimer(); } else if (rid && !event.request_ids.includes(rid)) { + // While waiting for the agent run gate, keep request context alive so + // Goose streaming events are not dropped before the UI subscribes. + if (chatStateRef.current === 'waiting') { + return; + } // The backend can briefly report no active request between tool phases. // Confirm the absence before turning the UI idle, otherwise MindSpace // refreshes the page while tools are still mutating it. @@ -887,6 +920,17 @@ export function useTKMindChat( } return; } + const eventRequestId = getSessionEventRequestId(event); + if ( + !rid && + eventRequestId && + (chatStateRef.current === 'waiting' || chatStateRef.current === 'streaming') && + (event.type === 'Message' || event.type === 'Finish') + ) { + activeRequestId.current = eventRequestId; + rid = eventRequestId; + clearActiveRequestMissingTimer(); + } if ( rid || (isDirectChatSessionId(sessionId) && @@ -930,6 +974,7 @@ export function useTKMindChat( connectTokenRef.current += 1; unsubscribeRef.current?.(); unsubscribeRef.current = null; + subscribedSessionIdRef.current = null; clearActiveRequestMissingTimer(); setError(null); setPendingTool(null); @@ -959,6 +1004,7 @@ export function useTKMindChat( const token = ++connectTokenRef.current; unsubscribeRef.current?.(); unsubscribeRef.current = null; + subscribedSessionIdRef.current = null; clearActiveRequestMissingTimer(); if (showLoading) { @@ -1282,6 +1328,11 @@ export function useTKMindChat( ...(options?.forceDeepReasoning ? { forceDeepReasoning: true } : {}), }, ); + const runSessionId = createdRun.sessionId ?? activeSessionId; + if (runSessionId && !isDirectChatSessionId(runSessionId)) { + activeSessionId = runSessionId; + subscribeToSession(runSessionId); + } const finishedRun = createdRun.status === 'succeeded' ? createdRun @@ -1289,17 +1340,22 @@ export function useTKMindChat( isCancelled: () => submitToken !== connectTokenRef.current, onSessionId: (sessionId) => { if (submitToken !== connectTokenRef.current) return; - if (activeSessionId === sessionId) return; - activeSessionId = sessionId; - const nextSession: Session = { - id: sessionId, - name: 'New Chat', - message_count: messagesRef.current.length, - working_dir: '', - }; - writeStoredSessionId(userRef.current?.id, sessionId); - setSession(nextSession); - setSessions((prev) => prependUnique(prev, nextSession)); + if (activeSessionId !== sessionId) { + activeSessionId = sessionId; + const nextSession: Session = { + id: sessionId, + name: 'New Chat', + message_count: messagesRef.current.length, + working_dir: '', + }; + writeStoredSessionId(userRef.current?.id, sessionId); + setSession(nextSession); + setSessions((prev) => prependUnique(prev, nextSession)); + } + if (!isDirectChatSessionId(sessionId)) { + subscribeToSession(sessionId); + setChatState('streaming'); + } }, onMessages: (snapshotMessages) => { if (submitToken !== connectTokenRef.current) return; @@ -1381,6 +1437,7 @@ export function useTKMindChat( setChatState('idle'); } else { setChatState('streaming'); + scheduleReplyRecoverySync(activeSessionId, submitToken); } } } catch (err) { @@ -1388,13 +1445,7 @@ export function useTKMindChat( subscribeToSession(activeSessionId); setChatState('streaming'); setError(null); - for (const delay of REPLY_RECOVERY_SYNC_DELAYS_MS) { - window.setTimeout(() => { - if (sessionRef.current?.id === activeSessionId) { - void syncSessionMessages(activeSessionId); - } - }, delay); - } + scheduleReplyRecoverySync(activeSessionId, submitToken); return; } if (session && activeSessionId) setSessions((prev) => touchSession(prev, activeSessionId!, -1)); @@ -1415,6 +1466,7 @@ export function useTKMindChat( grantedSkills, clearActiveRequestMissingTimer, subscribeToSession, + scheduleReplyRecoverySync, ensureProvider, loadProjectMemory, refreshSessions, @@ -1459,6 +1511,7 @@ export function useTKMindChat( unsubscribeRef.current?.(); unsubscribeRef.current = null; + subscribedSessionIdRef.current = null; clearActiveRequestMissingTimer(); clearStoredSessionId(userRef.current?.id); activeRequestId.current = null;