Skip to content

Commit 6be5374

Browse files
committed
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.
1 parent 528bcfb commit 6be5374

4 files changed

Lines changed: 79 additions & 69 deletions

File tree

apps/sim/app/api/copilot/chats/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const GET = withRouteHandler(async (_request: NextRequest) => {
3636
workspaceId: copilotChats.workspaceId,
3737
activeStreamId: copilotChats.conversationId,
3838
updatedAt: copilotChats.updatedAt,
39+
type: copilotChats.type,
3940
resources: copilotChats.resources,
4041
})
4142
.from(copilotChats)

apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,6 @@ interface MothershipChatProps {
5252
animateInput?: boolean
5353
onInputAnimationEnd?: () => void
5454
className?: string
55-
/**
56-
* When true, hides the input footer so the conversation is shown as
57-
* read-only history. Used when a chat is surfaced outside the context
58-
* where it can be safely continued (e.g. a Mothership chat opened from
59-
* a workflow page).
60-
*/
61-
readOnly?: boolean
6255
}
6356

6457
const LAYOUT_STYLES = {
@@ -107,7 +100,6 @@ export function MothershipChat({
107100
animateInput = false,
108101
onInputAnimationEnd,
109102
className,
110-
readOnly = false,
111103
}: MothershipChatProps) {
112104
const styles = LAYOUT_STYLES[layout]
113105
const isStreamActive = isSending || isReconnecting
@@ -235,34 +227,32 @@ export function MothershipChat({
235227
)}
236228
</div>
237229

238-
{!readOnly && (
239-
<div
240-
className={cn(styles.footer, animateInput && 'animate-slide-in-bottom')}
241-
onAnimationEnd={animateInput ? onInputAnimationEnd : undefined}
242-
>
243-
<div className={styles.footerInner}>
244-
<QueuedMessages
245-
messageQueue={messageQueue}
246-
onRemove={onRemoveQueuedMessage}
247-
onSendNow={onSendQueuedMessage}
248-
onEdit={handleEditQueued}
249-
/>
250-
<UserInput
251-
ref={userInputRef}
252-
onSubmit={onSubmit}
253-
isSending={isStreamActive}
254-
onStopGeneration={onStopGeneration}
255-
isInitialView={false}
256-
userId={userId}
257-
onContextAdd={onContextAdd}
258-
onContextRemove={onContextRemove}
259-
onSendQueuedHead={handleSendQueuedHead}
260-
onEditQueuedTail={handleEditQueuedTail}
261-
draftScopeKey={draftScopeKey}
262-
/>
263-
</div>
230+
<div
231+
className={cn(styles.footer, animateInput && 'animate-slide-in-bottom')}
232+
onAnimationEnd={animateInput ? onInputAnimationEnd : undefined}
233+
>
234+
<div className={styles.footerInner}>
235+
<QueuedMessages
236+
messageQueue={messageQueue}
237+
onRemove={onRemoveQueuedMessage}
238+
onSendNow={onSendQueuedMessage}
239+
onEdit={handleEditQueued}
240+
/>
241+
<UserInput
242+
ref={userInputRef}
243+
onSubmit={onSubmit}
244+
isSending={isStreamActive}
245+
onStopGeneration={onStopGeneration}
246+
isInitialView={false}
247+
userId={userId}
248+
onContextAdd={onContextAdd}
249+
onContextRemove={onContextRemove}
250+
onSendQueuedHead={handleSendQueuedHead}
251+
onEditQueuedTail={handleEditQueuedTail}
252+
draftScopeKey={draftScopeKey}
253+
/>
264254
</div>
265-
)}
255+
</div>
266256
</div>
267257
)
268258
}

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

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ import { captureEvent } from '@/lib/posthog/client'
4646
import { generateWorkflowJson } from '@/lib/workflows/operations/import-export'
4747
import { ConversationListItem } from '@/app/workspace/[workspaceId]/components'
4848
import { MothershipChat } from '@/app/workspace/[workspaceId]/home/components'
49-
import { getWorkflowCopilotUseChatOptions, useChat } from '@/app/workspace/[workspaceId]/home/hooks'
49+
import {
50+
getMothershipUseChatOptions,
51+
getWorkflowCopilotUseChatOptions,
52+
useChat,
53+
} from '@/app/workspace/[workspaceId]/home/hooks'
5054
import type { FileAttachmentForApi } from '@/app/workspace/[workspaceId]/home/types'
5155
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
5256
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
@@ -252,26 +256,23 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
252256
)
253257
const [isCopilotHistoryOpen, setIsCopilotHistoryOpen] = useState(false)
254258

255-
const copilotChatTitle = useMemo(
256-
() =>
257-
copilotChatId ? (copilotChatList.find((c) => c.id === copilotChatId)?.title ?? null) : null,
259+
const selectedCopilotChat = useMemo(
260+
() => (copilotChatId ? (copilotChatList.find((c) => c.id === copilotChatId) ?? null) : null),
258261
[copilotChatId, copilotChatList]
259262
)
263+
const copilotChatTitle = selectedCopilotChat?.title ?? null
260264

261265
/**
262-
* A chat is read-only on this workflow page when it doesn't natively
263-
* belong to the active workflow — currently the case for Mothership
264-
* chats whose `workflowId` is null but whose `resources` reference this
265-
* workflow. Continuing the conversation would route through the
266-
* workflow copilot agent rather than the original Mothership context,
267-
* so we surface the history without the input.
266+
* A selected chat is "foreign" to this workflow when it was started in
267+
* Mothership (`type === 'mothership'`) but ended up referencing this
268+
* workflow via `resources`. We keep the conversation continuable by
269+
* routing sends through the Mothership branch — i.e. the request goes
270+
* out without `workflowId`, so the server uses the broader Mothership
271+
* agent surface that originally produced the chat. The trade-off is
272+
* that resources spawned during continuation only show up in the
273+
* Mothership view; this panel shows the conversation only.
268274
*/
269-
const isCopilotChatReadOnly = useMemo(() => {
270-
if (!copilotChatId || !activeWorkflowId) return false
271-
const chat = copilotChatList.find((c) => c.id === copilotChatId)
272-
if (!chat) return false
273-
return chat.workflowId !== activeWorkflowId
274-
}, [copilotChatId, copilotChatList, activeWorkflowId])
275+
const isMothershipChat = selectedCopilotChat?.type === 'mothership'
275276

276277
const queryClient = useQueryClient()
277278
const loadCopilotChats = useCallback(() => {
@@ -359,6 +360,40 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
359360
[activeWorkflowId]
360361
)
361362

363+
const handleCopilotRequestStarted = useCallback(
364+
({ requestId, userMessageId }: { requestId: string; userMessageId: string }) => {
365+
captureEvent(posthogRef.current, 'task_request_started', {
366+
workspace_id: workspaceId,
367+
view: 'copilot',
368+
request_id: requestId,
369+
user_message_id: userMessageId,
370+
})
371+
},
372+
[workspaceId]
373+
)
374+
375+
const copilotChatOptions = useMemo(
376+
() =>
377+
isMothershipChat
378+
? getMothershipUseChatOptions({
379+
onStreamEnd: loadCopilotChats,
380+
onRequestStarted: handleCopilotRequestStarted,
381+
})
382+
: getWorkflowCopilotUseChatOptions({
383+
workflowId: activeWorkflowId || undefined,
384+
onTitleUpdate: loadCopilotChats,
385+
onToolResult: handleCopilotToolResult,
386+
onRequestStarted: handleCopilotRequestStarted,
387+
}),
388+
[
389+
isMothershipChat,
390+
activeWorkflowId,
391+
loadCopilotChats,
392+
handleCopilotToolResult,
393+
handleCopilotRequestStarted,
394+
]
395+
)
396+
362397
const {
363398
messages: copilotMessages,
364399
isSending: copilotIsSending,
@@ -371,23 +406,7 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
371406
sendNow: copilotSendNow,
372407
editQueuedMessage: copilotEditQueuedMessage,
373408
getCurrentRequestId: getCopilotCurrentRequestId,
374-
} = useChat(
375-
workspaceId,
376-
copilotChatId,
377-
getWorkflowCopilotUseChatOptions({
378-
workflowId: activeWorkflowId || undefined,
379-
onTitleUpdate: loadCopilotChats,
380-
onToolResult: handleCopilotToolResult,
381-
onRequestStarted: ({ requestId, userMessageId }) => {
382-
captureEvent(posthogRef.current, 'task_request_started', {
383-
workspace_id: workspaceId,
384-
view: 'copilot',
385-
request_id: requestId,
386-
user_message_id: userMessageId,
387-
})
388-
},
389-
})
390-
)
409+
} = useChat(workspaceId, copilotChatId, copilotChatOptions)
391410

392411
const handleCopilotNewChat = useCallback(() => {
393412
if (!activeWorkflowId || !workspaceId) return
@@ -925,7 +944,6 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
925944
userId={session?.user?.id}
926945
chatId={copilotResolvedChatId}
927946
layout='copilot-view'
928-
readOnly={isCopilotChatReadOnly}
929947
/>
930948
</div>
931949
)}

apps/sim/lib/api/contracts/copilot.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ export const copilotChatListItemSchema = z.object({
307307
workspaceId: z.string().nullable().optional(),
308308
activeStreamId: z.string().nullable(),
309309
updatedAt: z.string().nullable(),
310+
type: z.enum(['mothership', 'copilot']).optional(),
310311
resources: z.array(copilotChatResourceSchema).optional(),
311312
})
312313
export type CopilotChatListItem = z.output<typeof copilotChatListItemSchema>

0 commit comments

Comments
 (0)