diff --git a/frontend/src/app/(dashboard)/projects/[projectId]/thread/[threadId]/page.tsx b/frontend/src/app/(dashboard)/projects/[projectId]/thread/[threadId]/page.tsx index e9c7a34c..0173754f 100644 --- a/frontend/src/app/(dashboard)/projects/[projectId]/thread/[threadId]/page.tsx +++ b/frontend/src/app/(dashboard)/projects/[projectId]/thread/[threadId]/page.tsx @@ -19,15 +19,31 @@ import { isLocalMode } from '@/lib/config'; import { ThreadContent } from '@/components/thread/content/ThreadContent'; import { ThreadSkeleton } from '@/components/thread/content/ThreadSkeleton'; import { useAddUserMessageMutation } from '@/hooks/react-query/threads/use-messages'; -import { useStartAgentMutation, useStopAgentMutation } from '@/hooks/react-query/threads/use-agent-run'; +import { + useStartAgentMutation, + useStopAgentMutation, +} from '@/hooks/react-query/threads/use-agent-run'; import { useSharedSubscription } from '@/contexts/SubscriptionContext'; import { SubscriptionStatus } from '@/components/thread/chat-input/_use-model-selection'; -import { UnifiedMessage, ApiMessageType, ToolCallInput, Project } from '../_types'; -import { useThreadData, useToolCalls, useBilling, useKeyboardShortcuts } from '../_hooks'; +import { + UnifiedMessage, + ApiMessageType, + ToolCallInput, + Project, +} from '../_types'; +import { + useThreadData, + useToolCalls, + useBilling, + useKeyboardShortcuts, +} from '../_hooks'; import { ThreadError, UpgradeDialog, ThreadLayout } from '../_components'; -import { useThreadAgent, useAgents } from '@/hooks/react-query/agents/use-agents'; +import { + useThreadAgent, + useAgents, +} from '@/hooks/react-query/agents/use-agents'; import { AgentRunLimitDialog } from '@/components/thread/agent-run-limit-dialog'; import { useAgentSelection } from '@/lib/stores/agent-selection-store'; import { useQueryClient } from '@tanstack/react-query'; @@ -53,19 +69,22 @@ export default function ThreadPage({ const [isSending, setIsSending] = useState(false); const [fileViewerOpen, setFileViewerOpen] = useState(false); const [fileToView, setFileToView] = useState(null); - const [filePathList, setFilePathList] = useState(undefined); + const [filePathList, setFilePathList] = useState( + undefined, + ); const [showUpgradeDialog, setShowUpgradeDialog] = useState(false); const [debugMode, setDebugMode] = useState(false); - const [initialPanelOpenAttempted, setInitialPanelOpenAttempted] = useState(false); + const [initialPanelOpenAttempted, setInitialPanelOpenAttempted] = + useState(false); // Use Zustand store for agent selection persistence - const { - selectedAgentId, - setSelectedAgent, + const { + selectedAgentId, + setSelectedAgent, initializeFromAgents, getCurrentAgent, - isSunaAgent + isSunaAgent, } = useAgentSelection(); - + const { data: agentsResponse } = useAgents(); const agents = agentsResponse?.agents || []; const [isSidePanelAnimating, setIsSidePanelAnimating] = useState(false); @@ -77,7 +96,6 @@ export default function ThreadPage({ runningThreadIds: string[]; } | null>(null); - // Refs - simplified for flex-column-reverse const latestMessageRef = useRef(null); const initialLayoutAppliedRef = useRef(false); @@ -165,12 +183,13 @@ export default function ThreadPage({ }, [threadAgentData, agents, initializeFromAgents]); const { data: subscriptionData } = useSharedSubscription(); - const subscriptionStatus: SubscriptionStatus = (subscriptionData?.status === 'active' || subscriptionData?.status === 'trialing') - ? 'active' - : 'no_subscription'; + const subscriptionStatus: SubscriptionStatus = + subscriptionData?.status === 'active' || + subscriptionData?.status === 'trialing' + ? 'active' + : 'no_subscription'; - const handleProjectRenamed = useCallback((newName: string) => { - }, []); + const handleProjectRenamed = useCallback((newName: string) => {}, []); // scrollToBottom for flex-column-reverse layout const scrollToBottom = useCallback(() => { @@ -179,80 +198,91 @@ export default function ThreadPage({ } }, []); - const handleNewMessageFromStream = useCallback((message: UnifiedMessage) => { - if (!message.message_id) { - console.warn( - `[STREAM HANDLER] Received message is missing ID: Type=${message.type}`, - ); - } - - setMessages((prev) => { - const messageExists = prev.some( - (m) => m.message_id === message.message_id, - ); - if (messageExists) { - return prev.map((m) => - m.message_id === message.message_id ? message : m, + const handleNewMessageFromStream = useCallback( + (message: UnifiedMessage) => { + if (!message.message_id) { + console.warn( + `[STREAM HANDLER] Received message is missing ID: Type=${message.type}`, ); - } else { - // If this is a user message, replace any optimistic user message with temp ID - if (message.type === 'user') { - const optimisticIndex = prev.findIndex(m => - m.type === 'user' && - m.message_id?.startsWith('temp-') && - m.content === message.content - ); - if (optimisticIndex !== -1) { - // Replace the optimistic message with the real one - return prev.map((m, index) => - index === optimisticIndex ? message : m - ); - } - } - return [...prev, message]; } - }); - if (message.type === 'tool') { - setAutoOpenedPanel(false); - } - }, [setMessages, setAutoOpenedPanel]); + setMessages((prev) => { + const messageExists = prev.some( + (m) => m.message_id === message.message_id, + ); + if (messageExists) { + return prev.map((m) => + m.message_id === message.message_id ? message : m, + ); + } else { + // If this is a user message, replace any optimistic user message with temp ID + if (message.type === 'user') { + const optimisticIndex = prev.findIndex( + (m) => + m.type === 'user' && + m.message_id?.startsWith('temp-') && + m.content === message.content, + ); + if (optimisticIndex !== -1) { + // Replace the optimistic message with the real one + return prev.map((m, index) => + index === optimisticIndex ? message : m, + ); + } + } + return [...prev, message]; + } + }); - const handleStreamStatusChange = useCallback((hookStatus: string) => { - switch (hookStatus) { - case 'idle': - case 'completed': - case 'stopped': - case 'agent_not_running': - case 'error': - case 'failed': - setAgentStatus('idle'); - setAgentRunId(null); + if (message.type === 'tool') { setAutoOpenedPanel(false); + } + }, + [setMessages, setAutoOpenedPanel], + ); - // No scroll needed with flex-column-reverse - break; - case 'connecting': - setAgentStatus('connecting'); - break; - case 'streaming': - setAgentStatus('running'); - break; - } - }, [setAgentStatus, setAgentRunId, setAutoOpenedPanel]); + const handleStreamStatusChange = useCallback( + (hookStatus: string) => { + switch (hookStatus) { + case 'idle': + case 'completed': + case 'stopped': + case 'agent_not_running': + case 'error': + case 'failed': + setAgentStatus('idle'); + setAgentRunId(null); + setAutoOpenedPanel(false); + + // No scroll needed with flex-column-reverse + break; + case 'connecting': + setAgentStatus('connecting'); + break; + case 'streaming': + setAgentStatus('running'); + break; + } + }, + [setAgentStatus, setAgentRunId, setAutoOpenedPanel], + ); const handleStreamError = useCallback((errorMessage: string) => { - console.error(`[PAGE] Stream hook error: ${errorMessage}`); - if ( - !errorMessage.toLowerCase().includes('not found') && - !errorMessage.toLowerCase().includes('agent run is not running') - ) { - toast.error(`Stream Error: ${errorMessage}`); + const lower = errorMessage.toLowerCase(); + const isExpected = + lower.includes('not found') || lower.includes('agent run is not running'); + + // Downgrade log level for expected/benign cases (opening old conversations) + if (isExpected) { + console.info(`[PAGE] Stream skipped for inactive run: ${errorMessage}`); + return; } + + console.error(`[PAGE] Stream hook error: ${errorMessage}`); + toast.error(`Stream Error: ${errorMessage}`); }, []); - const handleStreamClose = useCallback(() => { - }, []); + const handleStreamClose = useCallback(() => {}, []); const { status: streamHookStatus, @@ -299,39 +329,50 @@ export default function ThreadPage({ try { const messagePromise = addUserMessageMutation.mutateAsync({ threadId, - message + message, }); const agentPromise = startAgentMutation.mutateAsync({ threadId, options: { ...options, - agent_id: selectedAgentId - } + agent_id: selectedAgentId, + }, }); - const results = await Promise.allSettled([messagePromise, agentPromise]); + const results = await Promise.allSettled([ + messagePromise, + agentPromise, + ]); if (results[0].status === 'rejected') { const reason = results[0].reason; - console.error("Failed to send message:", reason); - throw new Error(`Failed to send message: ${reason?.message || reason}`); + console.error('Failed to send message:', reason); + throw new Error( + `Failed to send message: ${reason?.message || reason}`, + ); } if (results[1].status === 'rejected') { const error = results[1].reason; - console.error("Failed to start agent:", error); + console.error('Failed to start agent:', error); if (error instanceof BillingError) { setBillingData({ currentUsage: error.detail.currentUsage as number | undefined, limit: error.detail.limit as number | undefined, - message: error.detail.message || 'Monthly usage limit reached. Please upgrade.', - accountId: null + message: + error.detail.message || + 'Monthly usage limit reached. Please upgrade.', + accountId: null, }); setShowBillingAlert(true); - setMessages(prev => prev.filter(m => m.message_id !== optimisticUserMessage.message_id)); + setMessages((prev) => + prev.filter( + (m) => m.message_id !== optimisticUserMessage.message_id, + ), + ); return; } @@ -344,7 +385,11 @@ export default function ThreadPage({ }); setShowAgentLimitDialog(true); - setMessages(prev => prev.filter(m => m.message_id !== optimisticUserMessage.message_id)); + setMessages((prev) => + prev.filter( + (m) => m.message_id !== optimisticUserMessage.message_id, + ), + ); return; } @@ -354,10 +399,12 @@ export default function ThreadPage({ const agentResult = results[1].value; setUserInitiatedRun(true); setAgentRunId(agentResult.agent_run_id); - } catch (err) { console.error('Error sending message or starting agent:', err); - if (!(err instanceof BillingError) && !(err instanceof AgentRunLimitError)) { + if ( + !(err instanceof BillingError) && + !(err instanceof AgentRunLimitError) + ) { toast.error(err instanceof Error ? err.message : 'Operation failed'); } setMessages((prev) => @@ -367,7 +414,16 @@ export default function ThreadPage({ setIsSending(false); } }, - [threadId, project?.account_id, addUserMessageMutation, startAgentMutation, setMessages, setBillingData, setShowBillingAlert, setAgentRunId], + [ + threadId, + project?.account_id, + addUserMessageMutation, + startAgentMutation, + setMessages, + setBillingData, + setShowBillingAlert, + setAgentRunId, + ], ); const handleStopAgent = useCallback(async () => { @@ -384,15 +440,18 @@ export default function ThreadPage({ } }, [stopStreaming, agentRunId, stopAgentMutation, setAgentStatus]); - const handleOpenFileViewer = useCallback((filePath?: string, filePathList?: string[]) => { - if (filePath) { - setFileToView(filePath); - } else { - setFileToView(null); - } - setFilePathList(filePathList); - setFileViewerOpen(true); - }, []); + const handleOpenFileViewer = useCallback( + (filePath?: string, filePathList?: string[]) => { + if (filePath) { + setFileToView(filePath); + } else { + setFileToView(null); + } + setFilePathList(filePathList); + setFileViewerOpen(true); + }, + [], + ); const toolViewAssistant = useCallback( (assistantContent?: string, toolContent?: string) => { @@ -404,7 +463,9 @@ export default function ThreadPage({ Assistant Message
-
{assistantContent}
+
+ {assistantContent} +
); @@ -423,16 +484,19 @@ export default function ThreadPage({ Tool Result
{isSuccess ? 'Success' : 'Failed'}
-
{toolContent}
+
+ {toolContent} +
); @@ -464,28 +528,55 @@ export default function ThreadPage({ } } } - }, [initialPanelOpenAttempted, messages, toolCalls, initialLoadCompleted, setIsSidePanelOpen, setCurrentToolIndex, isMobile]); + }, [ + initialPanelOpenAttempted, + messages, + toolCalls, + initialLoadCompleted, + setIsSidePanelOpen, + setCurrentToolIndex, + isMobile, + ]); useEffect(() => { // Start streaming if user initiated a run (don't wait for initialLoadCompleted for first-time users) if (agentRunId && agentRunId !== currentHookRunId && userInitiatedRun) { startStreaming(agentRunId); setUserInitiatedRun(false); // Reset flag after starting + return; } - // Also start streaming if this is from page load with recent active runs - else if (agentRunId && agentRunId !== currentHookRunId && initialLoadCompleted && !userInitiatedRun) { + + // Only auto-start streaming on page load if we know the agent is currently running + if ( + agentRunId && + agentRunId !== currentHookRunId && + initialLoadCompleted && + !userInitiatedRun && + agentStatus === 'running' + ) { startStreaming(agentRunId); } - }, [agentRunId, startStreaming, currentHookRunId, initialLoadCompleted, userInitiatedRun]); + }, [ + agentRunId, + startStreaming, + currentHookRunId, + initialLoadCompleted, + userInitiatedRun, + agentStatus, + ]); // No auto-scroll needed with flex-column-reverse // No intersection observer needed with flex-column-reverse useEffect(() => { - if ((streamHookStatus === 'completed' || streamHookStatus === 'stopped' || - streamHookStatus === 'agent_not_running' || streamHookStatus === 'error') && - (agentStatus === 'running' || agentStatus === 'connecting')) { + if ( + (streamHookStatus === 'completed' || + streamHookStatus === 'stopped' || + streamHookStatus === 'agent_not_running' || + streamHookStatus === 'error') && + (agentStatus === 'running' || agentStatus === 'connecting') + ) { setAgentStatus('idle'); setAgentRunId(null); } @@ -531,9 +622,15 @@ export default function ThreadPage({ const hasCheckedUpgradeDialog = useRef(false); useEffect(() => { - if (initialLoadCompleted && subscriptionData && !hasCheckedUpgradeDialog.current) { + if ( + initialLoadCompleted && + subscriptionData && + !hasCheckedUpgradeDialog.current + ) { hasCheckedUpgradeDialog.current = true; - const hasSeenUpgradeDialog = localStorage.getItem('suna_upgrade_dialog_displayed'); + const hasSeenUpgradeDialog = localStorage.getItem( + 'suna_upgrade_dialog_displayed', + ); const isFreeTier = subscriptionStatus === 'no_subscription'; if (!hasSeenUpgradeDialog && isFreeTier && !isLocalMode()) { setShowUpgradeDialog(true); @@ -576,7 +673,9 @@ export default function ThreadPage({ const scrollContainer = scrollContainerRef.current; if (scrollContainer) { - scrollContainer.addEventListener('scroll', handleScroll, { passive: true }); + scrollContainer.addEventListener('scroll', handleScroll, { + passive: true, + }); // Check initial state setTimeout(() => handleScroll(), 100); @@ -696,27 +795,36 @@ export default function ThreadPage({ scrollContainerRef={scrollContainerRef} /> -
-
+ 'fixed bottom-0 z-10 bg-gradient-to-t from-background via-background/90 to-transparent px-4 pt-8', + isSidePanelAnimating + ? '' + : 'transition-all duration-200 ease-in-out', + leftSidebarState === 'expanded' + ? 'left-[72px] md:left-[256px]' + : 'left-[40px]', + isSidePanelOpen && !isMobile + ? 'right-[90%] sm:right-[450px] md:right-[500px] lg:right-[550px] xl:right-[650px]' + : 'right-0', + isMobile ? 'left-0 right-0' : '', + )} + > +
); -} \ No newline at end of file +} diff --git a/frontend/src/hooks/useAgentStream.ts b/frontend/src/hooks/useAgentStream.ts index 1e369f71..482c8c99 100644 --- a/frontend/src/hooks/useAgentStream.ts +++ b/frontend/src/hooks/useAgentStream.ts @@ -85,7 +85,7 @@ export function useAgentStream( agentId?: string, // Optional agent ID for invalidation ): UseAgentStreamResult { const queryClient = useQueryClient(); - + const [status, setStatus] = useState('idle'); const [textContent, setTextContent] = useState< { content: string; sequence?: number }[] @@ -110,16 +110,16 @@ export function useAgentStream( const statusRef = useRef(status); const agentRunIdRef = useRef(agentRunId); const textContentRef = useRef(textContent); - + // Update refs whenever state changes useEffect(() => { statusRef.current = status; }, [status]); - + useEffect(() => { agentRunIdRef.current = agentRunId; }, [agentRunId]); - + useEffect(() => { textContentRef.current = textContent; }, [textContent]); @@ -127,7 +127,11 @@ export function useAgentStream( // On thread change, ensure any existing stream is cleaned up to avoid stale subscriptions useEffect(() => { const previousThreadId = threadIdRef.current; - if (previousThreadId && previousThreadId !== threadId && streamCleanupRef.current) { + if ( + previousThreadId && + previousThreadId !== threadId && + streamCleanupRef.current + ) { // Close the existing stream for the previous thread streamCleanupRef.current(); streamCleanupRef.current = null; @@ -188,14 +192,22 @@ export function useAgentStream( (finalStatus: string, runId: string | null = agentRunId) => { if (!isMountedRef.current) return; - console.log(`[useAgentStream] Finalizing stream with status: ${finalStatus}, runId: ${runId}`); + console.log( + `[useAgentStream] Finalizing stream with status: ${finalStatus}, runId: ${runId}`, + ); const currentThreadId = threadIdRef.current; // Get current threadId from ref const currentSetMessages = setMessagesRef.current; // Get current setMessages from ref // Only finalize if this is for the current run ID or if no specific run ID is provided - if (runId && currentRunIdRef.current && currentRunIdRef.current !== runId) { - console.log(`[useAgentStream] Ignoring finalization for old run ID ${runId}, current is ${currentRunIdRef.current}`); + if ( + runId && + currentRunIdRef.current && + currentRunIdRef.current !== runId + ) { + console.log( + `[useAgentStream] Ignoring finalization for old run ID ${runId}, current is ${currentRunIdRef.current}`, + ); return; } @@ -212,24 +224,34 @@ export function useAgentStream( updateStatus(finalStatus); setAgentRunId(null); currentRunIdRef.current = null; - + // Invalidate relevant queries to refresh data after agent run completes if (agentId) { queryClient.invalidateQueries({ queryKey: agentKeys.detail(agentId) }); queryClient.invalidateQueries({ queryKey: agentKeys.lists() }); queryClient.invalidateQueries({ queryKey: ['agent-tools', agentId] }); - - queryClient.invalidateQueries({ queryKey: ['custom-mcp-tools', agentId] }); + + queryClient.invalidateQueries({ + queryKey: ['custom-mcp-tools', agentId], + }); queryClient.invalidateQueries({ queryKey: composioKeys.mcpServers() }); - - queryClient.invalidateQueries({ queryKey: composioKeys.profiles.all() }); - queryClient.invalidateQueries({ queryKey: composioKeys.profiles.credentials() }); - + + queryClient.invalidateQueries({ + queryKey: composioKeys.profiles.all(), + }); + queryClient.invalidateQueries({ + queryKey: composioKeys.profiles.credentials(), + }); + queryClient.invalidateQueries({ queryKey: ['triggers', agentId] }); - - queryClient.invalidateQueries({ queryKey: workflowKeys.agent(agentId) }); - - queryClient.invalidateQueries({ queryKey: knowledgeBaseKeys.agent(agentId) }); + + queryClient.invalidateQueries({ + queryKey: workflowKeys.agent(agentId), + }); + + queryClient.invalidateQueries({ + queryKey: knowledgeBaseKeys.agent(agentId), + }); } if ( @@ -238,8 +260,7 @@ export function useAgentStream( finalStatus === 'stopped' || finalStatus === 'agent_not_running') ) { - getAgentStatus(runId).catch((err) => { - }); + getAgentStatus(runId).catch((err) => {}); } }, [agentRunId, updateStatus, agentId, queryClient], @@ -278,7 +299,10 @@ export function useAgentStream( try { const jsonData = JSON.parse(processedData); if (jsonData.status === 'error') { - console.error('[useAgentStream] Received error status message:', jsonData); + console.error( + '[useAgentStream] Received error status message:', + jsonData, + ); const errorMessage = jsonData.message || 'Unknown error occurred'; setError(errorMessage); toast.error(errorMessage, { duration: 15000 }); @@ -290,7 +314,10 @@ export function useAgentStream( } // --- Process JSON messages --- - const message = safeJsonParse(processedData, null) as UnifiedMessage | null; + const message = safeJsonParse( + processedData, + null, + ) as UnifiedMessage | null; if (!message) { console.warn( '[useAgentStream] Failed to parse streamed message:', @@ -408,11 +435,18 @@ export function useAgentStream( errorMessage = 'Stream connection error'; } - console.error('[useAgentStream] Streaming error:', errorMessage, err); - setError(errorMessage); - - // Show error toast with longer duration - toast.error(errorMessage, { duration: 15000 }); + const lower = errorMessage.toLowerCase(); + const isExpected = + lower.includes('not found') || lower.includes('not running'); + + if (isExpected) { + console.info('[useAgentStream] Streaming skipped/ended:', errorMessage); + } else { + console.error('[useAgentStream] Streaming error:', errorMessage, err); + setError(errorMessage); + // Show error toast with longer duration + toast.error(errorMessage, { duration: 15000 }); + } const runId = currentRunIdRef.current; if (!runId) { @@ -422,7 +456,6 @@ export function useAgentStream( finalizeStream('error'); // Finalize with generic error if no runId return; } - }, [finalizeStream], ); @@ -431,8 +464,10 @@ export function useAgentStream( if (!isMountedRef.current) return; const runId = currentRunIdRef.current; - console.log(`[useAgentStream] Stream closed for run ID: ${runId}, status: ${status}`); - + console.log( + `[useAgentStream] Stream closed for run ID: ${runId}, status: ${status}`, + ); + if (!runId) { console.warn('[useAgentStream] Stream closed but no active agentRunId.'); // If status was streaming, something went wrong, finalize as error @@ -460,11 +495,15 @@ export function useAgentStream( // Check if this is still the current run ID if (currentRunIdRef.current !== runId) { - console.log(`[useAgentStream] Run ID changed during status check in handleStreamClose, ignoring`); + console.log( + `[useAgentStream] Run ID changed during status check in handleStreamClose, ignoring`, + ); return; } - console.log(`[useAgentStream] Final status for run ID ${runId}: ${agentStatus.status}`); + console.log( + `[useAgentStream] Final status for run ID ${runId}: ${agentStatus.status}`, + ); if (agentStatus.status === 'running') { setError('Stream closed unexpectedly while agent was running.'); @@ -481,7 +520,9 @@ export function useAgentStream( // Check if this is still the current run ID if (currentRunIdRef.current !== runId) { - console.log(`[useAgentStream] Run ID changed during error handling in handleStreamClose, ignoring`); + console.log( + `[useAgentStream] Run ID changed during error handling in handleStreamClose, ignoring`, + ); return; } @@ -512,7 +553,7 @@ export function useAgentStream( // Cleanup function - be more conservative about stream cleanup return () => { isMountedRef.current = false; - + // Don't automatically cleanup streams on navigation // Only set mounted flag to false to prevent new operations // Streams will be cleaned up when they naturally complete or on explicit stop @@ -550,27 +591,31 @@ export function useAgentStream( // Check if this is still the current run ID we're trying to start if (currentRunIdRef.current !== runId) { - console.log(`[useAgentStream] Run ID changed during status check, aborting stream for ${runId}`); + console.log( + `[useAgentStream] Run ID changed during status check, aborting stream for ${runId}`, + ); return; } if (agentStatus.status !== 'running') { - console.warn( - `[useAgentStream] Agent run ${runId} is not in running state (status: ${agentStatus.status}). Cannot start stream.`, + // Expected when opening an old conversation; don't surface as error/toast + console.info( + `[useAgentStream] Skip streaming for inactive run ${runId} (status: ${agentStatus.status}).`, ); - - // Only set error and finalize if this is still the current run ID if (currentRunIdRef.current === runId) { - setError(`Agent run is not running (status: ${agentStatus.status})`); - finalizeStream( - mapAgentStatus(agentStatus.status) || 'agent_not_running', - runId, - ); + const final = + agentStatus.status === 'completed' || + agentStatus.status === 'stopped' + ? mapAgentStatus(agentStatus.status) + : 'agent_not_running'; + finalizeStream(final, runId); } return; } - console.log(`[useAgentStream] Agent run ${runId} is running, creating stream`); + console.log( + `[useAgentStream] Agent run ${runId} is running, creating stream`, + ); // Agent is running, proceed to create the stream const cleanup = streamAgent(runId, { @@ -593,8 +638,10 @@ export function useAgentStream( }, }); streamCleanupRef.current = cleanup; - console.log(`[useAgentStream] Stream created successfully for run ID: ${runId}`); - + console.log( + `[useAgentStream] Stream created successfully for run ID: ${runId}`, + ); + // Status will be updated to 'streaming' by the first message received in handleStreamMessage // If for some reason no message arrives shortly, verify liveness again to avoid zombie state setTimeout(async () => { @@ -606,7 +653,10 @@ export function useAgentStream( if (!isMountedRef.current) return; if (currentRunIdRef.current !== runId) return; if (latest.status !== 'running') { - finalizeStream(mapAgentStatus(latest.status) || 'agent_not_running', runId); + finalizeStream( + mapAgentStatus(latest.status) || 'agent_not_running', + runId, + ); } } catch { // ignore @@ -617,22 +667,32 @@ export function useAgentStream( // Only handle error if this is still the current run ID if (currentRunIdRef.current !== runId) { - console.log(`[useAgentStream] Error occurred for old run ID ${runId}, ignoring`); + console.log( + `[useAgentStream] Error occurred for old run ID ${runId}, ignoring`, + ); return; } const errorMessage = err instanceof Error ? err.message : String(err); - console.error( - `[useAgentStream] Error initiating stream for ${runId}: ${errorMessage}`, - ); - setError(errorMessage); + const lower = errorMessage.toLowerCase(); + const isExpected = + lower.includes('not found') || + lower.includes('404') || + lower.includes('does not exist') || + lower.includes('not running'); - const isNotFoundError = - errorMessage.includes('not found') || - errorMessage.includes('404') || - errorMessage.includes('does not exist'); - - finalizeStream(isNotFoundError ? 'agent_not_running' : 'error', runId); + if (isExpected) { + console.info( + `[useAgentStream] Stream not started for ${runId}: ${errorMessage}`, + ); + finalizeStream('agent_not_running', runId); + } else { + console.error( + `[useAgentStream] Error initiating stream for ${runId}: ${errorMessage}`, + ); + setError(errorMessage); + finalizeStream('error', runId); + } } }, [ @@ -676,4 +736,4 @@ export function useAgentStream( startStreaming, stopStreaming, }; -} \ No newline at end of file +}