diff --git a/apps/sim/app/api/copilot/chats/route.ts b/apps/sim/app/api/copilot/chats/route.ts
index 0aecdb462b9..7e58b525649 100644
--- a/apps/sim/app/api/copilot/chats/route.ts
+++ b/apps/sim/app/api/copilot/chats/route.ts
@@ -36,6 +36,8 @@ export const GET = withRouteHandler(async (_request: NextRequest) => {
workspaceId: copilotChats.workspaceId,
activeStreamId: copilotChats.conversationId,
updatedAt: copilotChats.updatedAt,
+ type: copilotChats.type,
+ resources: copilotChats.resources,
})
.from(copilotChats)
.leftJoin(workflow, eq(copilotChats.workflowId, workflow.id))
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..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
@@ -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}`
+ router.push(url)
}
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..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
@@ -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 {
@@ -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'
@@ -118,7 +122,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)
@@ -250,11 +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 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 isMothershipChat = selectedCopilotChat?.type === 'mothership'
const queryClient = useQueryClient()
const loadCopilotChats = useCallback(() => {
@@ -264,8 +282,14 @@ 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. 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
@@ -274,12 +298,23 @@ 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)
setCopilotChatId(copilotChatList[0].id)
- }, [copilotChatList, copilotChatId, activeWorkflowId, setCopilotChatId])
+ }, [copilotChatList, copilotChatId, activeWorkflowId, setCopilotChatId, urlChatIdParam])
useEffect(() => {
posthogRef.current = posthog
@@ -335,6 +370,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,
@@ -347,23 +416,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
@@ -444,6 +497,20 @@ 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.
+ * Re-applies whenever the param changes so returning to a workflow with
+ * a fresh `?chatId=` switches the tab again.
+ */
+ const handledTabSwitchForChatIdRef = useRef(null)
+ useEffect(() => {
+ if (!urlChatIdParam) return
+ if (handledTabSwitchForChatIdRef.current === urlChatIdParam) return
+ handledTabSwitchForChatIdRef.current = urlChatIdParam
+ setActiveTab('copilot')
+ }, [urlChatIdParam, setActiveTab])
+
useEffect(() => {
const handler = (e: Event) => {
const message = (e as CustomEvent<{ message: string }>).detail?.message
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..53789308d8f 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,8 @@ export const copilotChatListItemSchema = z.object({
workspaceId: z.string().nullable().optional(),
activeStreamId: z.string().nullable(),
updatedAt: z.string().nullable(),
+ type: z.enum(['mothership', 'copilot']).nullable().optional(),
+ resources: z.array(copilotChatResourceSchema).nullable().optional(),
})
export type CopilotChatListItem = z.output
@@ -378,12 +386,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(),