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 3c95d83d4..b6e94d633 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 9ca3a24fe..957c8a6da 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 e7261a229..0c04ad29e 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),