fix: stream Goosed page replies into chat without waiting for agent run
Subscribe session SSE as soon as the Goosed session is known, keep request context during waiting, and poll session snapshots so page results appear in the conversation while generation is still running. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+74
-21
@@ -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<string | null>(null);
|
||||
const activeRequestMissingTimerRef = useRef<ReturnType<typeof window.setTimeout> | null>(null);
|
||||
const chatStateRef = useRef<ChatState>(chatState);
|
||||
const subscribedSessionIdRef = useRef<string | null>(null);
|
||||
const unsubscribeRef = useRef<(() => void) | null>(null);
|
||||
const connectTokenRef = useRef(0);
|
||||
const messagesRef = useRef<Message[]>([]);
|
||||
@@ -295,6 +302,10 @@ export function useTKMindChat(
|
||||
const onUserUpdateRef = useRef(onUserUpdate);
|
||||
const chatImageCategoryIdRef = useRef<string | null>(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;
|
||||
|
||||
Reference in New Issue
Block a user