diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index 790be10175..c58ed8cb34 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -77,6 +77,7 @@ import { getNextWorkflowColor } from '@/lib/workflows/colors' import { getQueryClient } from '@/app/_shell/providers/get-query-client' import { invalidateResourceQueries } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry' import { + buildCompletedPreviewSessions, type FilePreviewSessionsState, INITIAL_FILE_PREVIEW_SESSIONS_STATE, reduceFilePreviewSessions, @@ -766,6 +767,18 @@ export function useChat( [completePreviewSession, syncPreviewSessionRefs] ) + const reconcileTerminalPreviewSessions = useCallback(() => { + const completedAt = new Date().toISOString() + const completedSessions = buildCompletedPreviewSessions( + previewSessionsStateRef.current.sessions, + completedAt + ) + + for (const session of completedSessions) { + applyCompletedPreviewSession(session) + } + }, [applyCompletedPreviewSession]) + const removePreviewSessionImmediate = useCallback( (sessionId: string) => { const nextState = reduceFilePreviewSessions(previewSessionsStateRef.current, { @@ -2305,6 +2318,7 @@ export function useChat( const finalize = useCallback( (options?: { error?: boolean }) => { + reconcileTerminalPreviewSessions() sendingRef.current = false setIsSending(false) setIsReconnecting(false) @@ -2333,7 +2347,7 @@ export function useChat( }) } }, - [invalidateChatQueries] + [invalidateChatQueries, reconcileTerminalPreviewSessions] ) finalizeRef.current = finalize diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-file-preview-sessions.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-file-preview-sessions.test.tsx index 70209af163..863df51596 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-file-preview-sessions.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-file-preview-sessions.test.tsx @@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest' import type { FilePreviewSession } from '@/lib/copilot/request/session' import { + buildCompletedPreviewSessions, INITIAL_FILE_PREVIEW_SESSIONS_STATE, reduceFilePreviewSessions, } from '@/app/workspace/[workspaceId]/home/hooks/use-file-preview-sessions' @@ -30,6 +31,40 @@ function createSession( } describe('reduceFilePreviewSessions', () => { + it('builds complete sessions for terminal stream reconciliation', () => { + const completedAt = '2026-04-10T00:00:10.000Z' + const nextSessions = buildCompletedPreviewSessions( + { + 'preview-1': createSession({ + id: 'preview-1', + toolCallId: 'preview-1', + status: 'pending', + previewText: 'draft', + }), + 'preview-2': createSession({ + id: 'preview-2', + toolCallId: 'preview-2', + status: 'streaming', + previewText: 'partial', + }), + 'preview-3': createSession({ + id: 'preview-3', + toolCallId: 'preview-3', + status: 'complete', + previewText: 'done', + completedAt: '2026-04-10T00:00:03.000Z', + }), + }, + completedAt + ) + + expect(nextSessions).toHaveLength(2) + expect(nextSessions.map((session) => session.id)).toEqual(['preview-1', 'preview-2']) + expect(nextSessions.every((session) => session.status === 'complete')).toBe(true) + expect(nextSessions.every((session) => session.updatedAt === completedAt)).toBe(true) + expect(nextSessions.every((session) => session.completedAt === completedAt)).toBe(true) + }) + it('hydrates the latest active preview session', () => { const state = reduceFilePreviewSessions(INITIAL_FILE_PREVIEW_SESSIONS_STATE, { type: 'hydrate', diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-file-preview-sessions.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-file-preview-sessions.ts index 075b6c549b..6782585bbb 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-file-preview-sessions.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-file-preview-sessions.ts @@ -48,6 +48,20 @@ export function pickActiveSessionId( return latestActive?.id ?? null } +export function buildCompletedPreviewSessions( + sessions: Record, + completedAt: string +): FilePreviewSession[] { + return Object.values(sessions) + .filter((session) => session.status !== 'complete') + .map((session) => ({ + ...session, + status: 'complete' as const, + updatedAt: completedAt, + completedAt, + })) +} + export function reduceFilePreviewSessions( state: FilePreviewSessionsState, action: FilePreviewSessionsAction