Skip to content

Commit 4175e32

Browse files
address greptile comments
1 parent 2a64808 commit 4175e32

3 files changed

Lines changed: 76 additions & 48 deletions

File tree

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

Lines changed: 32 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { toError } from '@sim/utils/errors'
6+
import { useQueryClient } from '@tanstack/react-query'
67
import { History, Plus, Square } from 'lucide-react'
78
import { useParams, useRouter } from 'next/navigation'
89
import { usePostHog } from 'posthog-js/react'
@@ -60,6 +61,11 @@ import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowI
6061
import { getWorkflowLockToggleIds } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils'
6162
import { useDeleteWorkflow, useImportWorkflow } from '@/app/workspace/[workspaceId]/w/hooks'
6263
import { useCopilotChatSelection } from '@/hooks/queries/copilot-chat-selection'
64+
import {
65+
type CopilotChatListItem,
66+
copilotChatsKeys,
67+
useCopilotChats,
68+
} from '@/hooks/queries/copilot-chats'
6369
import { useDuplicateWorkflowMutation, useWorkflowMap } from '@/hooks/queries/workflows'
6470
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
6571
import { usePermissionConfig } from '@/hooks/use-permission-config'
@@ -78,6 +84,7 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
7884
import type { WorkflowState } from '@/stores/workflows/workflow/types'
7985

8086
const logger = createLogger('Panel')
87+
const EMPTY_COPILOT_CHATS: readonly CopilotChatListItem[] = []
8188
/**
8289
* Panel component with resizable width and tab navigation that persists across page refreshes.
8390
*
@@ -227,9 +234,9 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
227234
activeWorkflowId ?? undefined
228235
)
229236

230-
const [copilotChatList, setCopilotChatList] = useState<
231-
{ id: string; title: string | null; updatedAt: string; activeStreamId: string | null }[]
232-
>([])
237+
const { data: copilotChatList = EMPTY_COPILOT_CHATS } = useCopilotChats(
238+
activeWorkflowId ?? undefined
239+
)
233240
const [isCopilotHistoryOpen, setIsCopilotHistoryOpen] = useState(false)
234241

235242
const copilotChatTitle = useMemo(
@@ -238,50 +245,32 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
238245
[copilotChatId, copilotChatList]
239246
)
240247

241-
const copilotChatIdRef = useRef(copilotChatId)
242-
copilotChatIdRef.current = copilotChatId
243-
const copilotInitialLoadDoneRef = useRef(false)
244-
const activeWorkflowIdRef = useRef(activeWorkflowId)
245-
activeWorkflowIdRef.current = activeWorkflowId
246-
248+
const queryClient = useQueryClient()
247249
const loadCopilotChats = useCallback(() => {
248250
if (!activeWorkflowId) return
249-
const requestWorkflowId = activeWorkflowId
250-
fetch('/api/copilot/chats')
251-
.then((res) => (res.ok ? res.json() : { chats: [] }))
252-
.then((data) => {
253-
// Drop responses for a workflow we've already switched away from.
254-
if (requestWorkflowId !== activeWorkflowIdRef.current) return
255-
const allChats = Array.isArray(data?.chats) ? data.chats : []
256-
const filtered = allChats.filter(
257-
(c: { workflowId?: string }) => c.workflowId === activeWorkflowId
258-
) as Array<{
259-
id: string
260-
title: string | null
261-
updatedAt: string
262-
activeStreamId: string | null
263-
}>
264-
setCopilotChatList(filtered)
265-
266-
const currentId = copilotChatIdRef.current
267-
let resolvedCurrentId = currentId
268-
if (currentId && !filtered.find((c: { id: string }) => c.id === currentId)) {
269-
setCopilotChatId(undefined)
270-
resolvedCurrentId = undefined
271-
}
272-
273-
if (!copilotInitialLoadDoneRef.current && !resolvedCurrentId && filtered.length > 0) {
274-
setCopilotChatId(filtered[0].id)
275-
}
276-
copilotInitialLoadDoneRef.current = true
277-
})
278-
.catch(() => {})
279-
}, [activeWorkflowId, setCopilotChatId])
251+
queryClient.invalidateQueries({ queryKey: copilotChatsKeys.list(activeWorkflowId) })
252+
}, [activeWorkflowId, queryClient])
280253

254+
// Auto-select most recent on first list arrival per workflow, and drop a
255+
// selection that no longer matches anything in the current list (e.g. the
256+
// chat was deleted in another tab).
257+
const autoSelectAttemptedForRef = useRef<Set<string>>(new Set())
281258
useEffect(() => {
282-
copilotInitialLoadDoneRef.current = false
283-
loadCopilotChats()
284-
}, [loadCopilotChats])
259+
if (!activeWorkflowId) return
260+
261+
if (copilotChatId && !copilotChatList.find((c) => c.id === copilotChatId)) {
262+
setCopilotChatId(undefined)
263+
return
264+
}
265+
266+
if (copilotChatId) return
267+
if (autoSelectAttemptedForRef.current.has(activeWorkflowId)) return
268+
autoSelectAttemptedForRef.current.add(activeWorkflowId)
269+
270+
if (copilotChatList.length > 0) {
271+
setCopilotChatId(copilotChatList[0].id)
272+
}
273+
}, [copilotChatList, copilotChatId, activeWorkflowId, setCopilotChatId])
285274

286275
useEffect(() => {
287276
posthogRef.current = posthog

apps/sim/hooks/queries/copilot-chat-selection.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useCallback } from 'react'
2-
import { useQuery, useQueryClient } from '@tanstack/react-query'
2+
import { skipToken, useQuery, useQueryClient } from '@tanstack/react-query'
33

44
export const copilotChatSelectionKeys = {
55
all: ['copilot-chat-selection'] as const,
@@ -10,15 +10,15 @@ export const copilotChatSelectionKeys = {
1010

1111
/**
1212
* Reactive per-workflow copilot chat selection. Values are written via the
13-
* returned setter; the queryFn is never invoked.
13+
* returned setter; queryFn is `skipToken` so the cache only ever holds
14+
* what setQueryData puts there.
1415
*/
1516
export function useCopilotChatSelection(workflowId?: string) {
1617
const queryClient = useQueryClient()
1718

18-
const { data: chatId } = useQuery({
19+
const { data: chatId } = useQuery<string | null>({
1920
queryKey: copilotChatSelectionKeys.workflow(workflowId),
20-
queryFn: (): string | null => null,
21-
enabled: false,
21+
queryFn: skipToken,
2222
staleTime: Number.POSITIVE_INFINITY,
2323
initialData: null,
2424
})
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { skipToken, useQuery } from '@tanstack/react-query'
2+
3+
export interface CopilotChatListItem {
4+
id: string
5+
title: string | null
6+
workflowId?: string
7+
updatedAt: string
8+
activeStreamId: string | null
9+
}
10+
11+
export const copilotChatsKeys = {
12+
all: ['copilot-chats'] as const,
13+
lists: () => [...copilotChatsKeys.all, 'list'] as const,
14+
list: (workflowId?: string) => [...copilotChatsKeys.lists(), workflowId ?? ''] as const,
15+
}
16+
17+
async function fetchCopilotChats(
18+
workflowId: string,
19+
signal?: AbortSignal
20+
): Promise<CopilotChatListItem[]> {
21+
const res = await fetch('/api/copilot/chats', { signal })
22+
if (!res.ok) return []
23+
const data = await res.json()
24+
const all = Array.isArray(data?.chats) ? (data.chats as CopilotChatListItem[]) : []
25+
return all.filter((c) => c.workflowId === workflowId)
26+
}
27+
28+
/**
29+
* Workflow-scoped copilot chat list. Each workflowId has its own cache entry
30+
* so switching workflows reads the right list synchronously instead of
31+
* showing the previous workflow's chats during the refetch.
32+
*/
33+
export function useCopilotChats(workflowId?: string) {
34+
return useQuery<CopilotChatListItem[]>({
35+
queryKey: copilotChatsKeys.list(workflowId),
36+
queryFn: workflowId ? ({ signal }) => fetchCopilotChats(workflowId, signal) : skipToken,
37+
staleTime: 30 * 1000,
38+
})
39+
}

0 commit comments

Comments
 (0)