Skip to content

Commit a9d4222

Browse files
fix(copilot): use different chats for different workflows
1 parent 3afcad2 commit a9d4222

3 files changed

Lines changed: 119 additions & 5 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]
5959
import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
6060
import { getWorkflowLockToggleIds } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils'
6161
import { useDeleteWorkflow, useImportWorkflow } from '@/app/workspace/[workspaceId]/w/hooks'
62+
import { useCopilotChatSelection } from '@/hooks/queries/copilot-chat-selection'
6263
import { useDuplicateWorkflowMutation, useWorkflowMap } from '@/hooks/queries/workflows'
6364
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
6465
import { usePermissionConfig } from '@/hooks/use-permission-config'
@@ -232,12 +233,48 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
232233
const copilotChatIdRef = useRef(copilotChatId)
233234
copilotChatIdRef.current = copilotChatId
234235
const copilotInitialLoadDoneRef = useRef(false)
236+
// Tracks the live workflow so async chat-list fetches can detect
237+
// workflow switches that happened mid-flight and bail out.
238+
const activeWorkflowIdRef = useRef(activeWorkflowId)
239+
activeWorkflowIdRef.current = activeWorkflowId
240+
241+
// Per-workflow chat memory: switching A→B→A returns to A's last-used chat
242+
// instead of jumping to A's most recent. Backed by the React Query cache so
243+
// it survives in-session workflow switches and clears on hard refresh.
244+
const { getChatId: getRememberedChatId, setChatId: setRememberedChatId } =
245+
useCopilotChatSelection()
246+
const lastWorkflowIdRef = useRef<string | null>(null)
247+
248+
useEffect(() => {
249+
const previous = lastWorkflowIdRef.current
250+
lastWorkflowIdRef.current = activeWorkflowId ?? null
251+
if (previous === activeWorkflowId) return
252+
253+
if (previous && copilotChatIdRef.current) {
254+
setRememberedChatId(previous, copilotChatIdRef.current)
255+
}
256+
257+
if (activeWorkflowId) {
258+
const remembered = getRememberedChatId(activeWorkflowId)
259+
setCopilotChatId(remembered)
260+
setCopilotChatTitle(null)
261+
} else {
262+
setCopilotChatId(undefined)
263+
setCopilotChatTitle(null)
264+
}
265+
}, [activeWorkflowId, getRememberedChatId, setRememberedChatId])
235266

236267
const loadCopilotChats = useCallback(() => {
237268
if (!activeWorkflowId) return
269+
const requestWorkflowId = activeWorkflowId
238270
fetch('/api/copilot/chats')
239271
.then((res) => (res.ok ? res.json() : { chats: [] }))
240272
.then((data) => {
273+
// Stale-fetch guard: bail if the user switched workflows mid-flight.
274+
// Without this the in-flight response would clobber the new
275+
// workflow's state (filtering against the old workflow id, clearing
276+
// the restored chat, and auto-selecting the wrong list's first chat).
277+
if (requestWorkflowId !== activeWorkflowIdRef.current) return
241278
const allChats = Array.isArray(data?.chats) ? data.chats : []
242279
const filtered = allChats.filter(
243280
(c: { workflowId?: string }) => c.workflowId === activeWorkflowId
@@ -250,20 +287,29 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
250287
setCopilotChatList(filtered)
251288

252289
const currentId = copilotChatIdRef.current
290+
let resolvedCurrentId = currentId
253291
if (currentId) {
254292
const match = filtered.find((c: { id: string }) => c.id === currentId)
255-
if (match?.title) setCopilotChatTitle(match.title)
293+
if (match) {
294+
if (match.title) setCopilotChatTitle(match.title)
295+
} else {
296+
// Remembered chat was deleted (here or in another tab). Drop it
297+
// so the next send doesn't hit a 404, and forget it for next time.
298+
setRememberedChatId(activeWorkflowId, undefined)
299+
setCopilotChatId(undefined)
300+
setCopilotChatTitle(null)
301+
resolvedCurrentId = undefined
302+
}
256303
}
257304

258-
if (!copilotInitialLoadDoneRef.current && !currentId && filtered.length > 0) {
259-
copilotInitialLoadDoneRef.current = true
305+
if (!copilotInitialLoadDoneRef.current && !resolvedCurrentId && filtered.length > 0) {
260306
setCopilotChatId(filtered[0].id)
261307
setCopilotChatTitle(filtered[0].title)
262308
}
263309
copilotInitialLoadDoneRef.current = true
264310
})
265311
.catch(() => {})
266-
}, [activeWorkflowId])
312+
}, [activeWorkflowId, setRememberedChatId])
267313

268314
useEffect(() => {
269315
copilotInitialLoadDoneRef.current = false
@@ -291,12 +337,15 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
291337
if (copilotChatId === chatId) {
292338
setCopilotChatId(undefined)
293339
setCopilotChatTitle(null)
340+
if (activeWorkflowId) {
341+
setRememberedChatId(activeWorkflowId, undefined)
342+
}
294343
}
295344
loadCopilotChats()
296345
})
297346
.catch(() => {})
298347
},
299-
[copilotChatId, loadCopilotChats]
348+
[copilotChatId, loadCopilotChats, activeWorkflowId, setRememberedChatId]
300349
)
301350

302351
const handleCopilotToolResult = useCallback(
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useCallback } from 'react'
2+
import { useQueryClient } from '@tanstack/react-query'
3+
4+
export const copilotChatSelectionKeys = {
5+
all: ['copilot-chat-selection'] as const,
6+
workflows: () => [...copilotChatSelectionKeys.all, 'workflow'] as const,
7+
workflow: (workflowId?: string) =>
8+
[...copilotChatSelectionKeys.workflows(), workflowId ?? ''] as const,
9+
}
10+
11+
/**
12+
* In-memory selection of which copilot chat is active per workflow.
13+
* Backed by the React Query cache as a keyed KV store — no `queryFn`,
14+
* values only land via `setChatId`. Survives in-session workflow switches
15+
* so A → B → A returns to A's last-used chat; cleared on hard refresh.
16+
*/
17+
export function useCopilotChatSelection() {
18+
const queryClient = useQueryClient()
19+
20+
const getChatId = useCallback(
21+
(workflowId: string): string | undefined =>
22+
queryClient.getQueryData<string>(copilotChatSelectionKeys.workflow(workflowId)),
23+
[queryClient]
24+
)
25+
26+
const setChatId = useCallback(
27+
(workflowId: string, chatId: string | undefined) => {
28+
queryClient.setQueryData<string | undefined>(
29+
copilotChatSelectionKeys.workflow(workflowId),
30+
chatId
31+
)
32+
},
33+
[queryClient]
34+
)
35+
36+
return { getChatId, setChatId }
37+
}

apps/sim/lib/copilot/chat/lifecycle.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export async function getAccessibleCopilotChat(chatId: string, userId: string) {
2828
.limit(1)
2929

3030
if (!chat) {
31+
logger.warn('Copilot chat not found or not owned by user', { chatId, userId })
3132
return null
3233
}
3334

@@ -38,11 +39,21 @@ export async function getAccessibleCopilotChat(chatId: string, userId: string) {
3839
action: 'read',
3940
})
4041
if (!authorization.allowed || !authorization.workflow) {
42+
logger.warn('Copilot chat workflow not authorized for user', {
43+
chatId,
44+
userId,
45+
workflowId: chat.workflowId,
46+
})
4147
return null
4248
}
4349
} else if (chat.workspaceId) {
4450
const access = await checkWorkspaceAccess(chat.workspaceId, userId)
4551
if (!access.exists || !access.hasAccess) {
52+
logger.warn('Copilot chat workspace not accessible to user', {
53+
chatId,
54+
userId,
55+
workspaceId: chat.workspaceId,
56+
})
4657
return null
4758
}
4859
}
@@ -74,16 +85,33 @@ export async function resolveOrCreateChat(params: {
7485

7586
if (chat) {
7687
if (workflowId && chat.workflowId !== workflowId) {
88+
logger.warn('Copilot chat workflow mismatch', {
89+
chatId,
90+
userId,
91+
requestWorkflowId: workflowId,
92+
chatWorkflowId: chat.workflowId,
93+
})
7794
return { chatId, chat: null, conversationHistory: [], isNew: false }
7895
}
7996

8097
if (workspaceId && chat.workspaceId !== workspaceId) {
98+
logger.warn('Copilot chat workspace mismatch', {
99+
chatId,
100+
userId,
101+
requestWorkspaceId: workspaceId,
102+
chatWorkspaceId: chat.workspaceId,
103+
})
81104
return { chatId, chat: null, conversationHistory: [], isNew: false }
82105
}
83106

84107
if (chat.workflowId) {
85108
const activeWorkflow = await getActiveWorkflowRecord(chat.workflowId)
86109
if (!activeWorkflow) {
110+
logger.warn('Copilot chat workflow no longer active', {
111+
chatId,
112+
userId,
113+
workflowId: chat.workflowId,
114+
})
87115
return { chatId, chat: null, conversationHistory: [], isNew: false }
88116
}
89117
}

0 commit comments

Comments
 (0)