From 528bcfbe974e1d4e879479d1db29db2e99c709fe Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Fri, 8 May 2026 16:47:20 -0400 Subject: [PATCH 1/4] feat(copilot): preserve mothership chat when opening workflow Clicking "Open Workflow" from a Mothership task now deep-links the originating chat into the workflow page's copilot panel as read-only history, so users don't lose the conversation that produced the workflow. --- apps/sim/app/api/copilot/chats/route.ts | 1 + .../mothership-chat/mothership-chat.tsx | 60 +++++++++++-------- .../resource-content/resource-content.tsx | 23 +++++-- .../mothership-view/mothership-view.tsx | 4 +- .../w/[workflowId]/components/panel/panel.tsx | 44 ++++++++++++-- apps/sim/hooks/queries/copilot-chats.ts | 6 +- apps/sim/lib/api/contracts/copilot.ts | 13 ++-- 7 files changed, 110 insertions(+), 41 deletions(-) diff --git a/apps/sim/app/api/copilot/chats/route.ts b/apps/sim/app/api/copilot/chats/route.ts index 0aecdb462b9..1e4415c34ad 100644 --- a/apps/sim/app/api/copilot/chats/route.ts +++ b/apps/sim/app/api/copilot/chats/route.ts @@ -36,6 +36,7 @@ export const GET = withRouteHandler(async (_request: NextRequest) => { workspaceId: copilotChats.workspaceId, activeStreamId: copilotChats.conversationId, updatedAt: copilotChats.updatedAt, + resources: copilotChats.resources, }) .from(copilotChats) .leftJoin(workflow, eq(copilotChats.workflowId, workflow.id)) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx index 4693b19de4a..a64c58ca875 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx @@ -52,6 +52,13 @@ interface MothershipChatProps { animateInput?: boolean onInputAnimationEnd?: () => void className?: string + /** + * When true, hides the input footer so the conversation is shown as + * read-only history. Used when a chat is surfaced outside the context + * where it can be safely continued (e.g. a Mothership chat opened from + * a workflow page). + */ + readOnly?: boolean } const LAYOUT_STYLES = { @@ -100,6 +107,7 @@ export function MothershipChat({ animateInput = false, onInputAnimationEnd, className, + readOnly = false, }: MothershipChatProps) { const styles = LAYOUT_STYLES[layout] const isStreamActive = isSending || isReconnecting @@ -227,32 +235,34 @@ export function MothershipChat({ )} -
-
- - + {!readOnly && ( +
+
+ + +
-
+ )}
) } diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx index e93fe37cd6a..1ca64722469 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx @@ -211,12 +211,19 @@ export const ResourceContent = memo(function ResourceContent({ interface ResourceActionsProps { workspaceId: string resource: MothershipResource + chatId?: string } -export function ResourceActions({ workspaceId, resource }: ResourceActionsProps) { +export function ResourceActions({ workspaceId, resource, chatId }: ResourceActionsProps) { switch (resource.type) { case 'workflow': - return + return ( + + ) case 'file': return case 'knowledgebase': @@ -244,9 +251,14 @@ export function ResourceActions({ workspaceId, resource }: ResourceActionsProps) interface EmbeddedWorkflowActionsProps { workspaceId: string workflowId: string + chatId?: string } -export function EmbeddedWorkflowActions({ workspaceId, workflowId }: EmbeddedWorkflowActionsProps) { +export function EmbeddedWorkflowActions({ + workspaceId, + workflowId, + chatId, +}: EmbeddedWorkflowActionsProps) { const router = useRouter() const { navigateToSettings } = useSettingsNavigation() const { userPermissions: effectivePermissions } = useWorkspacePermissionsContext() @@ -284,7 +296,10 @@ export function EmbeddedWorkflowActions({ workspaceId, workflowId }: EmbeddedWor } const handleOpenWorkflow = () => { - window.open(`/workspace/${workspaceId}/w/${workflowId}`, '_blank') + const url = chatId + ? `/workspace/${workspaceId}/w/${workflowId}?chatId=${encodeURIComponent(chatId)}` + : `/workspace/${workspaceId}/w/${workflowId}` + window.open(url, '_blank') } return ( diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx index be06ee8481a..f59216f1b24 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx @@ -117,7 +117,9 @@ export const MothershipView = memo( onReorderResources={onReorderResources} onCollapse={onCollapse} actions={ - active ? : null + active ? ( + + ) : null } previewMode={isActivePreviewable ? previewMode : undefined} onCyclePreviewMode={isActivePreviewable ? handleCyclePreview : undefined} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 78ad3020518..c460f51077f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -5,7 +5,7 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { useQueryClient } from '@tanstack/react-query' import { History, Plus } from 'lucide-react' -import { useParams, useRouter } from 'next/navigation' +import { useParams, useRouter, useSearchParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import { useShallow } from 'zustand/react/shallow' import { @@ -118,7 +118,9 @@ interface PanelProps { export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: PanelProps = {}) { const router = useRouter() const params = useParams() + const searchParams = useSearchParams() const workspaceId = propWorkspaceId ?? (params.workspaceId as string) + const urlChatIdParam = searchParams?.get('chatId') ?? null const posthog = usePostHog() const posthogRef = useRef(posthog) @@ -256,6 +258,21 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel [copilotChatId, copilotChatList] ) + /** + * A chat is read-only on this workflow page when it doesn't natively + * belong to the active workflow — currently the case for Mothership + * chats whose `workflowId` is null but whose `resources` reference this + * workflow. Continuing the conversation would route through the + * workflow copilot agent rather than the original Mothership context, + * so we surface the history without the input. + */ + const isCopilotChatReadOnly = useMemo(() => { + if (!copilotChatId || !activeWorkflowId) return false + const chat = copilotChatList.find((c) => c.id === copilotChatId) + if (!chat) return false + return chat.workflowId !== activeWorkflowId + }, [copilotChatId, copilotChatList, activeWorkflowId]) + const queryClient = useQueryClient() const loadCopilotChats = useCallback(() => { if (!activeWorkflowId) return @@ -264,7 +281,10 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel // Auto-select most recent on first list arrival per workflow, and drop a // selection that no longer matches anything in the current list (e.g. the - // chat was deleted in another tab). + // chat was deleted in another tab). When a `?chatId=` param is present in + // the URL (e.g. after clicking "Open Workflow" from a Mothership task), + // prefer that chat over the most recent so the original conversation is + // shown right away. const autoSelectAttemptedForRef = useRef>(new Set()) useEffect(() => { if (!activeWorkflowId) return @@ -278,8 +298,12 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel if (autoSelectAttemptedForRef.current.has(activeWorkflowId)) return if (copilotChatList.length === 0) return autoSelectAttemptedForRef.current.add(activeWorkflowId) - setCopilotChatId(copilotChatList[0].id) - }, [copilotChatList, copilotChatId, activeWorkflowId, setCopilotChatId]) + const preferred = + urlChatIdParam && copilotChatList.find((c) => c.id === urlChatIdParam) + ? urlChatIdParam + : copilotChatList[0].id + setCopilotChatId(preferred) + }, [copilotChatList, copilotChatId, activeWorkflowId, setCopilotChatId, urlChatIdParam]) useEffect(() => { posthogRef.current = posthog @@ -444,6 +468,17 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel setHasHydrated(true) }, [setHasHydrated]) + /** + * If the workflow page was opened with `?chatId=`, surface the copilot + * tab so the linked conversation is visible without an extra click. + */ + const chatIdParamHandledRef = useRef(false) + useEffect(() => { + if (chatIdParamHandledRef.current || !urlChatIdParam) return + chatIdParamHandledRef.current = true + setActiveTab('copilot') + }, [urlChatIdParam, setActiveTab]) + useEffect(() => { const handler = (e: Event) => { const message = (e as CustomEvent<{ message: string }>).detail?.message @@ -890,6 +925,7 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel userId={session?.user?.id} chatId={copilotResolvedChatId} layout='copilot-view' + readOnly={isCopilotChatReadOnly} /> )} diff --git a/apps/sim/hooks/queries/copilot-chats.ts b/apps/sim/hooks/queries/copilot-chats.ts index fe6f7462d2b..af54e6a203b 100644 --- a/apps/sim/hooks/queries/copilot-chats.ts +++ b/apps/sim/hooks/queries/copilot-chats.ts @@ -17,7 +17,11 @@ async function fetchCopilotChats( ): Promise { try { const data = await requestJson(listCopilotChatsContract, { signal }) - return data.chats.filter((c) => c.workflowId === workflowId) + return data.chats.filter( + (c) => + c.workflowId === workflowId || + c.resources?.some((r) => r.type === 'workflow' && r.id === workflowId) + ) } catch (error) { if (error instanceof ApiClientError) return [] throw error diff --git a/apps/sim/lib/api/contracts/copilot.ts b/apps/sim/lib/api/contracts/copilot.ts index 365ae41b9f5..04bc70a4df0 100644 --- a/apps/sim/lib/api/contracts/copilot.ts +++ b/apps/sim/lib/api/contracts/copilot.ts @@ -112,6 +112,12 @@ const copilotResourceTypeSchema = z.enum([ 'log', ]) +const copilotChatResourceSchema = z.object({ + type: copilotResourceTypeSchema, + id: z.string(), + title: z.string(), +}) + export const addCopilotChatResourceBodySchema = z.object({ chatId: z.string(), resource: z.object({ @@ -301,6 +307,7 @@ export const copilotChatListItemSchema = z.object({ workspaceId: z.string().nullable().optional(), activeStreamId: z.string().nullable(), updatedAt: z.string().nullable(), + resources: z.array(copilotChatResourceSchema).optional(), }) export type CopilotChatListItem = z.output @@ -378,12 +385,6 @@ const copilotCheckpointSchema = z.object({ updatedAt: z.string().nullable(), }) -const copilotChatResourceSchema = z.object({ - type: copilotResourceTypeSchema, - id: z.string(), - title: z.string(), -}) - const copilotAvailableModelSchema = z.object({ id: z.string(), friendlyName: z.string(), From 6be537472ba53b0ca126c5ca2f062dc433cd32d9 Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Fri, 8 May 2026 16:58:33 -0400 Subject: [PATCH 2/4] feat(copilot): continue mothership chats from workflow panel Switch the workflow-panel useChat options to the Mothership branch when the selected chat has type 'mothership'. Sends from the workflow page then go through the broader Mothership agent surface that originally produced the chat, instead of the workflow-scoped copilot. Removes the read-only fallback since both modes are now continuable; resources spawned during continuation still only render in Mothership. --- apps/sim/app/api/copilot/chats/route.ts | 1 + .../mothership-chat/mothership-chat.tsx | 60 ++++++------- .../w/[workflowId]/components/panel/panel.tsx | 86 +++++++++++-------- apps/sim/lib/api/contracts/copilot.ts | 1 + 4 files changed, 79 insertions(+), 69 deletions(-) diff --git a/apps/sim/app/api/copilot/chats/route.ts b/apps/sim/app/api/copilot/chats/route.ts index 1e4415c34ad..7e58b525649 100644 --- a/apps/sim/app/api/copilot/chats/route.ts +++ b/apps/sim/app/api/copilot/chats/route.ts @@ -36,6 +36,7 @@ export const GET = withRouteHandler(async (_request: NextRequest) => { workspaceId: copilotChats.workspaceId, activeStreamId: copilotChats.conversationId, updatedAt: copilotChats.updatedAt, + type: copilotChats.type, resources: copilotChats.resources, }) .from(copilotChats) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx index a64c58ca875..4693b19de4a 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx @@ -52,13 +52,6 @@ interface MothershipChatProps { animateInput?: boolean onInputAnimationEnd?: () => void className?: string - /** - * When true, hides the input footer so the conversation is shown as - * read-only history. Used when a chat is surfaced outside the context - * where it can be safely continued (e.g. a Mothership chat opened from - * a workflow page). - */ - readOnly?: boolean } const LAYOUT_STYLES = { @@ -107,7 +100,6 @@ export function MothershipChat({ animateInput = false, onInputAnimationEnd, className, - readOnly = false, }: MothershipChatProps) { const styles = LAYOUT_STYLES[layout] const isStreamActive = isSending || isReconnecting @@ -235,34 +227,32 @@ export function MothershipChat({ )} - {!readOnly && ( -
-
- - -
+
+
+ +
- )} +
) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index c460f51077f..4ba69036664 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -46,7 +46,11 @@ import { captureEvent } from '@/lib/posthog/client' import { generateWorkflowJson } from '@/lib/workflows/operations/import-export' import { ConversationListItem } from '@/app/workspace/[workspaceId]/components' import { MothershipChat } from '@/app/workspace/[workspaceId]/home/components' -import { getWorkflowCopilotUseChatOptions, useChat } from '@/app/workspace/[workspaceId]/home/hooks' +import { + getMothershipUseChatOptions, + getWorkflowCopilotUseChatOptions, + useChat, +} from '@/app/workspace/[workspaceId]/home/hooks' import type { FileAttachmentForApi } from '@/app/workspace/[workspaceId]/home/types' import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' @@ -252,26 +256,23 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel ) const [isCopilotHistoryOpen, setIsCopilotHistoryOpen] = useState(false) - const copilotChatTitle = useMemo( - () => - copilotChatId ? (copilotChatList.find((c) => c.id === copilotChatId)?.title ?? null) : null, + const selectedCopilotChat = useMemo( + () => (copilotChatId ? (copilotChatList.find((c) => c.id === copilotChatId) ?? null) : null), [copilotChatId, copilotChatList] ) + const copilotChatTitle = selectedCopilotChat?.title ?? null /** - * A chat is read-only on this workflow page when it doesn't natively - * belong to the active workflow — currently the case for Mothership - * chats whose `workflowId` is null but whose `resources` reference this - * workflow. Continuing the conversation would route through the - * workflow copilot agent rather than the original Mothership context, - * so we surface the history without the input. + * A selected chat is "foreign" to this workflow when it was started in + * Mothership (`type === 'mothership'`) but ended up referencing this + * workflow via `resources`. We keep the conversation continuable by + * routing sends through the Mothership branch — i.e. the request goes + * out without `workflowId`, so the server uses the broader Mothership + * agent surface that originally produced the chat. The trade-off is + * that resources spawned during continuation only show up in the + * Mothership view; this panel shows the conversation only. */ - const isCopilotChatReadOnly = useMemo(() => { - if (!copilotChatId || !activeWorkflowId) return false - const chat = copilotChatList.find((c) => c.id === copilotChatId) - if (!chat) return false - return chat.workflowId !== activeWorkflowId - }, [copilotChatId, copilotChatList, activeWorkflowId]) + const isMothershipChat = selectedCopilotChat?.type === 'mothership' const queryClient = useQueryClient() const loadCopilotChats = useCallback(() => { @@ -359,6 +360,40 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel [activeWorkflowId] ) + const handleCopilotRequestStarted = useCallback( + ({ requestId, userMessageId }: { requestId: string; userMessageId: string }) => { + captureEvent(posthogRef.current, 'task_request_started', { + workspace_id: workspaceId, + view: 'copilot', + request_id: requestId, + user_message_id: userMessageId, + }) + }, + [workspaceId] + ) + + const copilotChatOptions = useMemo( + () => + isMothershipChat + ? getMothershipUseChatOptions({ + onStreamEnd: loadCopilotChats, + onRequestStarted: handleCopilotRequestStarted, + }) + : getWorkflowCopilotUseChatOptions({ + workflowId: activeWorkflowId || undefined, + onTitleUpdate: loadCopilotChats, + onToolResult: handleCopilotToolResult, + onRequestStarted: handleCopilotRequestStarted, + }), + [ + isMothershipChat, + activeWorkflowId, + loadCopilotChats, + handleCopilotToolResult, + handleCopilotRequestStarted, + ] + ) + const { messages: copilotMessages, isSending: copilotIsSending, @@ -371,23 +406,7 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel sendNow: copilotSendNow, editQueuedMessage: copilotEditQueuedMessage, getCurrentRequestId: getCopilotCurrentRequestId, - } = useChat( - workspaceId, - copilotChatId, - getWorkflowCopilotUseChatOptions({ - workflowId: activeWorkflowId || undefined, - onTitleUpdate: loadCopilotChats, - onToolResult: handleCopilotToolResult, - onRequestStarted: ({ requestId, userMessageId }) => { - captureEvent(posthogRef.current, 'task_request_started', { - workspace_id: workspaceId, - view: 'copilot', - request_id: requestId, - user_message_id: userMessageId, - }) - }, - }) - ) + } = useChat(workspaceId, copilotChatId, copilotChatOptions) const handleCopilotNewChat = useCallback(() => { if (!activeWorkflowId || !workspaceId) return @@ -925,7 +944,6 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel userId={session?.user?.id} chatId={copilotResolvedChatId} layout='copilot-view' - readOnly={isCopilotChatReadOnly} /> )} diff --git a/apps/sim/lib/api/contracts/copilot.ts b/apps/sim/lib/api/contracts/copilot.ts index 04bc70a4df0..38bbbd1d22c 100644 --- a/apps/sim/lib/api/contracts/copilot.ts +++ b/apps/sim/lib/api/contracts/copilot.ts @@ -307,6 +307,7 @@ export const copilotChatListItemSchema = z.object({ workspaceId: z.string().nullable().optional(), activeStreamId: z.string().nullable(), updatedAt: z.string().nullable(), + type: z.enum(['mothership', 'copilot']).optional(), resources: z.array(copilotChatResourceSchema).optional(), }) export type CopilotChatListItem = z.output From e8a692e66812cc61457ff15993714f7d09c58c14 Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Fri, 8 May 2026 17:30:32 -0400 Subject: [PATCH 3/4] fix(copilot): open workflow in current tab --- .../components/resource-content/resource-content.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx index 1ca64722469..5891db3b0a3 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx @@ -299,7 +299,7 @@ export function EmbeddedWorkflowActions({ const url = chatId ? `/workspace/${workspaceId}/w/${workflowId}?chatId=${encodeURIComponent(chatId)}` : `/workspace/${workspaceId}/w/${workflowId}` - window.open(url, '_blank') + router.push(url) } return ( From bc431ffd70ef06e8ce0d1b2f734c9af608a79d82 Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Fri, 8 May 2026 17:34:31 -0400 Subject: [PATCH 4/4] fix(copilot): re-honor ?chatId= on revisits, tolerate null jsonb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track the consumed URL chatId per value (not once per session) so returning to a workflow with a fresh `?chatId=` re-applies it instead of being shadowed by the once-per-workflow auto-select guard. Same treatment for the copilot-tab activation effect. Make `type` and `resources` on the chat list contract `.nullable()` to tolerate null jsonb reads from older rows even though the columns are declared NOT NULL with defaults — avoids a Zod parse failure silently emptying the chat list on the client. --- .../w/[workflowId]/components/panel/panel.tsx | 31 +++++++++++++------ apps/sim/lib/api/contracts/copilot.ts | 4 +-- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 4ba69036664..f2c33298ea1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -285,8 +285,11 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel // chat was deleted in another tab). When a `?chatId=` param is present in // the URL (e.g. after clicking "Open Workflow" from a Mothership task), // prefer that chat over the most recent so the original conversation is - // shown right away. + // shown right away. The URL param is honored once per distinct value so + // returning to a workflow with a fresh `?chatId=` re-applies it instead of + // being shadowed by the once-per-workflow auto-select guard. const autoSelectAttemptedForRef = useRef>(new Set()) + const consumedUrlChatIdRef = useRef(null) useEffect(() => { if (!activeWorkflowId) return @@ -295,15 +298,22 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel return } + if ( + urlChatIdParam && + consumedUrlChatIdRef.current !== urlChatIdParam && + copilotChatList.find((c) => c.id === urlChatIdParam) + ) { + consumedUrlChatIdRef.current = urlChatIdParam + autoSelectAttemptedForRef.current.add(activeWorkflowId) + setCopilotChatId(urlChatIdParam) + return + } + if (copilotChatId) return if (autoSelectAttemptedForRef.current.has(activeWorkflowId)) return if (copilotChatList.length === 0) return autoSelectAttemptedForRef.current.add(activeWorkflowId) - const preferred = - urlChatIdParam && copilotChatList.find((c) => c.id === urlChatIdParam) - ? urlChatIdParam - : copilotChatList[0].id - setCopilotChatId(preferred) + setCopilotChatId(copilotChatList[0].id) }, [copilotChatList, copilotChatId, activeWorkflowId, setCopilotChatId, urlChatIdParam]) useEffect(() => { @@ -490,11 +500,14 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel /** * If the workflow page was opened with `?chatId=`, surface the copilot * tab so the linked conversation is visible without an extra click. + * Re-applies whenever the param changes so returning to a workflow with + * a fresh `?chatId=` switches the tab again. */ - const chatIdParamHandledRef = useRef(false) + const handledTabSwitchForChatIdRef = useRef(null) useEffect(() => { - if (chatIdParamHandledRef.current || !urlChatIdParam) return - chatIdParamHandledRef.current = true + if (!urlChatIdParam) return + if (handledTabSwitchForChatIdRef.current === urlChatIdParam) return + handledTabSwitchForChatIdRef.current = urlChatIdParam setActiveTab('copilot') }, [urlChatIdParam, setActiveTab]) diff --git a/apps/sim/lib/api/contracts/copilot.ts b/apps/sim/lib/api/contracts/copilot.ts index 38bbbd1d22c..53789308d8f 100644 --- a/apps/sim/lib/api/contracts/copilot.ts +++ b/apps/sim/lib/api/contracts/copilot.ts @@ -307,8 +307,8 @@ export const copilotChatListItemSchema = z.object({ workspaceId: z.string().nullable().optional(), activeStreamId: z.string().nullable(), updatedAt: z.string().nullable(), - type: z.enum(['mothership', 'copilot']).optional(), - resources: z.array(copilotChatResourceSchema).optional(), + type: z.enum(['mothership', 'copilot']).nullable().optional(), + resources: z.array(copilotChatResourceSchema).nullable().optional(), }) export type CopilotChatListItem = z.output