diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx index 3c95d83d43..b6e94d633e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx @@ -108,7 +108,7 @@ const SmoothThinkingText = memo( return (
@@ -355,7 +355,7 @@ export function ThinkingBlock({ isExpanded ? 'mt-1.5 max-h-[150px] opacity-100' : 'max-h-0 opacity-0' )} > -
+
diff --git a/apps/sim/lib/copilot/messages/persist.ts b/apps/sim/lib/copilot/messages/persist.ts index 9ca3a24fe7..957c8a6da2 100644 --- a/apps/sim/lib/copilot/messages/persist.ts +++ b/apps/sim/lib/copilot/messages/persist.ts @@ -5,7 +5,7 @@ import { serializeMessagesForDB } from './serialization' const logger = createLogger('CopilotMessagePersistence') -export async function persistMessages(params: { +interface PersistParams { chatId: string messages: CopilotMessage[] sensitiveCredentialIds?: Set @@ -13,24 +13,29 @@ export async function persistMessages(params: { mode?: string model?: string conversationId?: string -}): Promise { +} + +/** Builds the JSON body used by both fetch and sendBeacon persistence paths. */ +function buildPersistBody(params: PersistParams): string { + const dbMessages = serializeMessagesForDB( + params.messages, + params.sensitiveCredentialIds ?? new Set() + ) + return JSON.stringify({ + chatId: params.chatId, + messages: dbMessages, + ...(params.planArtifact !== undefined ? { planArtifact: params.planArtifact } : {}), + ...(params.mode || params.model ? { config: { mode: params.mode, model: params.model } } : {}), + ...(params.conversationId ? { conversationId: params.conversationId } : {}), + }) +} + +export async function persistMessages(params: PersistParams): Promise { try { - const dbMessages = serializeMessagesForDB( - params.messages, - params.sensitiveCredentialIds ?? new Set() - ) const response = await fetch(COPILOT_UPDATE_MESSAGES_API_PATH, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - chatId: params.chatId, - messages: dbMessages, - ...(params.planArtifact !== undefined ? { planArtifact: params.planArtifact } : {}), - ...(params.mode || params.model - ? { config: { mode: params.mode, model: params.model } } - : {}), - ...(params.conversationId ? { conversationId: params.conversationId } : {}), - }), + body: buildPersistBody(params), }) return response.ok } catch (error) { @@ -41,3 +46,27 @@ export async function persistMessages(params: { return false } } + +/** + * Persists messages using navigator.sendBeacon, which is reliable during page unload. + * Unlike fetch, sendBeacon is guaranteed to be queued even when the page is being torn down. + */ +export function persistMessagesBeacon(params: PersistParams): boolean { + try { + const body = buildPersistBody(params) + const blob = new Blob([body], { type: 'application/json' }) + const sent = navigator.sendBeacon(COPILOT_UPDATE_MESSAGES_API_PATH, blob) + if (!sent) { + logger.warn('sendBeacon returned false — browser may have rejected the request', { + chatId: params.chatId, + }) + } + return sent + } catch (error) { + logger.warn('Failed to persist messages via sendBeacon', { + chatId: params.chatId, + error: error instanceof Error ? error.message : String(error), + }) + return false + } +} diff --git a/apps/sim/stores/panel/copilot/store.ts b/apps/sim/stores/panel/copilot/store.ts index e7261a229d..0c04ad29eb 100644 --- a/apps/sim/stores/panel/copilot/store.ts +++ b/apps/sim/stores/panel/copilot/store.ts @@ -39,6 +39,7 @@ import { buildToolCallsById, normalizeMessagesForUI, persistMessages, + persistMessagesBeacon, saveMessageCheckpoint, } from '@/lib/copilot/messages' import type { CopilotTransportMode } from '@/lib/copilot/models' @@ -78,6 +79,28 @@ let _isPageUnloading = false if (typeof window !== 'undefined') { window.addEventListener('beforeunload', () => { _isPageUnloading = true + + // Emergency persistence: flush any pending streaming updates to the store and + // persist via sendBeacon (which is guaranteed to be queued during page teardown). + // Without this, thinking blocks and in-progress content are lost on refresh. + try { + const state = useCopilotStore.getState() + if (state.isSendingMessage && state.currentChat) { + // Flush batched streaming updates into the store messages + flushStreamingUpdates(useCopilotStore.setState.bind(useCopilotStore)) + const flushedState = useCopilotStore.getState() + persistMessagesBeacon({ + chatId: flushedState.currentChat!.id, + messages: flushedState.messages, + sensitiveCredentialIds: flushedState.sensitiveCredentialIds, + planArtifact: flushedState.streamingPlanContent || null, + mode: flushedState.mode, + model: flushedState.selectedModel, + }) + } + } catch { + // Best-effort — don't let errors prevent page unload + } }) } function isPageUnloading(): boolean { @@ -1461,19 +1484,26 @@ export const useCopilotStore = create()( // Immediately put all in-progress tools into aborted state abortAllInProgressTools(set, get) - // Persist whatever contentBlocks/text we have to keep ordering for reloads + // Persist whatever contentBlocks/text we have to keep ordering for reloads. + // During page unload, use sendBeacon which is guaranteed to be queued even + // as the page tears down. Regular async fetch won't complete in time. const { currentChat, streamingPlanContent, mode, selectedModel } = get() if (currentChat) { try { const currentMessages = get().messages - void persistMessages({ + const persistParams = { chatId: currentChat.id, messages: currentMessages, sensitiveCredentialIds: get().sensitiveCredentialIds, planArtifact: streamingPlanContent || null, mode, model: selectedModel, - }) + } + if (isPageUnloading()) { + persistMessagesBeacon(persistParams) + } else { + void persistMessages(persistParams) + } } catch (error) { logger.warn('[Copilot] Failed to queue abort snapshot persistence', { error: error instanceof Error ? error.message : String(error),