Skip to content

Commit 031866e

Browse files
committed
fix(copilot): persist thinking blocks on page refresh via sendBeacon
- Use navigator.sendBeacon in beforeunload handler to reliably persist in-progress messages (including thinking blocks) during page teardown - Flush batched streaming updates before beacon persistence - Fall back to sendBeacon in abortMessage when page is unloading - Fix double-digit ordered list clipping in thinking block (pl-6 → pl-8)
1 parent 3d5bd00 commit 031866e

File tree

3 files changed

+79
-20
lines changed
  • apps/sim
    • app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block
    • lib/copilot/messages
    • stores/panel/copilot

3 files changed

+79
-20
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ const SmoothThinkingText = memo(
108108
return (
109109
<div
110110
ref={textRef}
111-
className='[&_*]:!text-[var(--text-muted)] [&_*]:!text-[12px] [&_*]:!leading-[1.4] [&_p]:!m-0 [&_p]:!mb-1 [&_h1]:!text-[12px] [&_h1]:!font-semibold [&_h1]:!m-0 [&_h1]:!mb-1 [&_h2]:!text-[12px] [&_h2]:!font-semibold [&_h2]:!m-0 [&_h2]:!mb-1 [&_h3]:!text-[12px] [&_h3]:!font-semibold [&_h3]:!m-0 [&_h3]:!mb-1 [&_code]:!text-[11px] [&_ul]:!pl-5 [&_ul]:!my-1 [&_ol]:!pl-6 [&_ol]:!my-1 [&_li]:!my-0.5 [&_li]:!py-0 font-season text-[12px] text-[var(--text-muted)]'
111+
className='[&_*]:!text-[var(--text-muted)] [&_*]:!text-[12px] [&_*]:!leading-[1.4] [&_p]:!m-0 [&_p]:!mb-1 [&_h1]:!text-[12px] [&_h1]:!font-semibold [&_h1]:!m-0 [&_h1]:!mb-1 [&_h2]:!text-[12px] [&_h2]:!font-semibold [&_h2]:!m-0 [&_h2]:!mb-1 [&_h3]:!text-[12px] [&_h3]:!font-semibold [&_h3]:!m-0 [&_h3]:!mb-1 [&_code]:!text-[11px] [&_ul]:!pl-5 [&_ul]:!my-1 [&_ol]:!pl-8 [&_ol]:!my-1 [&_li]:!my-0.5 [&_li]:!py-0 font-season text-[12px] text-[var(--text-muted)]'
112112
>
113113
<CopilotMarkdownRenderer content={displayedContent} />
114114
</div>
@@ -355,7 +355,7 @@ export function ThinkingBlock({
355355
isExpanded ? 'mt-1.5 max-h-[150px] opacity-100' : 'max-h-0 opacity-0'
356356
)}
357357
>
358-
<div className='[&_*]:!text-[var(--text-muted)] [&_*]:!text-[12px] [&_*]:!leading-[1.4] [&_p]:!m-0 [&_p]:!mb-1 [&_h1]:!text-[12px] [&_h1]:!font-semibold [&_h1]:!m-0 [&_h1]:!mb-1 [&_h2]:!text-[12px] [&_h2]:!font-semibold [&_h2]:!m-0 [&_h2]:!mb-1 [&_h3]:!text-[12px] [&_h3]:!font-semibold [&_h3]:!m-0 [&_h3]:!mb-1 [&_code]:!text-[11px] [&_ul]:!pl-5 [&_ul]:!my-1 [&_ol]:!pl-6 [&_ol]:!my-1 [&_li]:!my-0.5 [&_li]:!py-0 font-season text-[12px] text-[var(--text-muted)]'>
358+
<div className='[&_*]:!text-[var(--text-muted)] [&_*]:!text-[12px] [&_*]:!leading-[1.4] [&_p]:!m-0 [&_p]:!mb-1 [&_h1]:!text-[12px] [&_h1]:!font-semibold [&_h1]:!m-0 [&_h1]:!mb-1 [&_h2]:!text-[12px] [&_h2]:!font-semibold [&_h2]:!m-0 [&_h2]:!mb-1 [&_h3]:!text-[12px] [&_h3]:!font-semibold [&_h3]:!m-0 [&_h3]:!mb-1 [&_code]:!text-[11px] [&_ul]:!pl-5 [&_ul]:!my-1 [&_ol]:!pl-8 [&_ol]:!my-1 [&_li]:!my-0.5 [&_li]:!py-0 font-season text-[12px] text-[var(--text-muted)]'>
359359
<CopilotMarkdownRenderer content={cleanContent} />
360360
</div>
361361
</div>

apps/sim/lib/copilot/messages/persist.ts

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,37 @@ import { serializeMessagesForDB } from './serialization'
55

66
const logger = createLogger('CopilotMessagePersistence')
77

8-
export async function persistMessages(params: {
8+
interface PersistParams {
99
chatId: string
1010
messages: CopilotMessage[]
1111
sensitiveCredentialIds?: Set<string>
1212
planArtifact?: string | null
1313
mode?: string
1414
model?: string
1515
conversationId?: string
16-
}): Promise<boolean> {
16+
}
17+
18+
/** Builds the JSON body used by both fetch and sendBeacon persistence paths. */
19+
function buildPersistBody(params: PersistParams): string {
20+
const dbMessages = serializeMessagesForDB(
21+
params.messages,
22+
params.sensitiveCredentialIds ?? new Set<string>()
23+
)
24+
return JSON.stringify({
25+
chatId: params.chatId,
26+
messages: dbMessages,
27+
...(params.planArtifact !== undefined ? { planArtifact: params.planArtifact } : {}),
28+
...(params.mode || params.model ? { config: { mode: params.mode, model: params.model } } : {}),
29+
...(params.conversationId ? { conversationId: params.conversationId } : {}),
30+
})
31+
}
32+
33+
export async function persistMessages(params: PersistParams): Promise<boolean> {
1734
try {
18-
const dbMessages = serializeMessagesForDB(
19-
params.messages,
20-
params.sensitiveCredentialIds ?? new Set<string>()
21-
)
2235
const response = await fetch(COPILOT_UPDATE_MESSAGES_API_PATH, {
2336
method: 'POST',
2437
headers: { 'Content-Type': 'application/json' },
25-
body: JSON.stringify({
26-
chatId: params.chatId,
27-
messages: dbMessages,
28-
...(params.planArtifact !== undefined ? { planArtifact: params.planArtifact } : {}),
29-
...(params.mode || params.model
30-
? { config: { mode: params.mode, model: params.model } }
31-
: {}),
32-
...(params.conversationId ? { conversationId: params.conversationId } : {}),
33-
}),
38+
body: buildPersistBody(params),
3439
})
3540
return response.ok
3641
} catch (error) {
@@ -41,3 +46,27 @@ export async function persistMessages(params: {
4146
return false
4247
}
4348
}
49+
50+
/**
51+
* Persists messages using navigator.sendBeacon, which is reliable during page unload.
52+
* Unlike fetch, sendBeacon is guaranteed to be queued even when the page is being torn down.
53+
*/
54+
export function persistMessagesBeacon(params: PersistParams): boolean {
55+
try {
56+
const body = buildPersistBody(params)
57+
const blob = new Blob([body], { type: 'application/json' })
58+
const sent = navigator.sendBeacon(COPILOT_UPDATE_MESSAGES_API_PATH, blob)
59+
if (!sent) {
60+
logger.warn('sendBeacon returned false — browser may have rejected the request', {
61+
chatId: params.chatId,
62+
})
63+
}
64+
return sent
65+
} catch (error) {
66+
logger.warn('Failed to persist messages via sendBeacon', {
67+
chatId: params.chatId,
68+
error: error instanceof Error ? error.message : String(error),
69+
})
70+
return false
71+
}
72+
}

apps/sim/stores/panel/copilot/store.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
buildToolCallsById,
4040
normalizeMessagesForUI,
4141
persistMessages,
42+
persistMessagesBeacon,
4243
saveMessageCheckpoint,
4344
} from '@/lib/copilot/messages'
4445
import type { CopilotTransportMode } from '@/lib/copilot/models'
@@ -78,6 +79,28 @@ let _isPageUnloading = false
7879
if (typeof window !== 'undefined') {
7980
window.addEventListener('beforeunload', () => {
8081
_isPageUnloading = true
82+
83+
// Emergency persistence: flush any pending streaming updates to the store and
84+
// persist via sendBeacon (which is guaranteed to be queued during page teardown).
85+
// Without this, thinking blocks and in-progress content are lost on refresh.
86+
try {
87+
const state = useCopilotStore.getState()
88+
if (state.isSendingMessage && state.currentChat) {
89+
// Flush batched streaming updates into the store messages
90+
flushStreamingUpdates(useCopilotStore.setState.bind(useCopilotStore))
91+
const flushedState = useCopilotStore.getState()
92+
persistMessagesBeacon({
93+
chatId: flushedState.currentChat!.id,
94+
messages: flushedState.messages,
95+
sensitiveCredentialIds: flushedState.sensitiveCredentialIds,
96+
planArtifact: flushedState.streamingPlanContent || null,
97+
mode: flushedState.mode,
98+
model: flushedState.selectedModel,
99+
})
100+
}
101+
} catch {
102+
// Best-effort — don't let errors prevent page unload
103+
}
81104
})
82105
}
83106
function isPageUnloading(): boolean {
@@ -1461,19 +1484,26 @@ export const useCopilotStore = create<CopilotStore>()(
14611484
// Immediately put all in-progress tools into aborted state
14621485
abortAllInProgressTools(set, get)
14631486

1464-
// Persist whatever contentBlocks/text we have to keep ordering for reloads
1487+
// Persist whatever contentBlocks/text we have to keep ordering for reloads.
1488+
// During page unload, use sendBeacon which is guaranteed to be queued even
1489+
// as the page tears down. Regular async fetch won't complete in time.
14651490
const { currentChat, streamingPlanContent, mode, selectedModel } = get()
14661491
if (currentChat) {
14671492
try {
14681493
const currentMessages = get().messages
1469-
void persistMessages({
1494+
const persistParams = {
14701495
chatId: currentChat.id,
14711496
messages: currentMessages,
14721497
sensitiveCredentialIds: get().sensitiveCredentialIds,
14731498
planArtifact: streamingPlanContent || null,
14741499
mode,
14751500
model: selectedModel,
1476-
})
1501+
}
1502+
if (isPageUnloading()) {
1503+
persistMessagesBeacon(persistParams)
1504+
} else {
1505+
void persistMessages(persistParams)
1506+
}
14771507
} catch (error) {
14781508
logger.warn('[Copilot] Failed to queue abort snapshot persistence', {
14791509
error: error instanceof Error ? error.message : String(error),

0 commit comments

Comments
 (0)