From 8c4e6a3510d634d7b73e693b2eb7b6228e255475 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 24 Mar 2026 16:37:15 -0700 Subject: [PATCH 1/4] improvement(ui): Merge ui definitions for mothership chat --- .../[workspaceId]/home/components/index.ts | 1 + .../mothership-chat/mothership-chat.tsx | 190 ++++++++++++++++++ .../app/workspace/[workspaceId]/home/home.tsx | 139 +++---------- .../[workspaceId]/home/hooks/index.ts | 6 +- .../[workspaceId]/home/hooks/use-chat.ts | 25 ++- .../w/[workflowId]/components/panel/panel.tsx | 118 +++-------- 6 files changed, 272 insertions(+), 207 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/index.ts index 558486dae7b..209ca78170d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/index.ts @@ -3,6 +3,7 @@ export { assistantMessageHasRenderableContent, MessageContent, } from './message-content' +export { MothershipChat } from './mothership-chat/mothership-chat' export { MothershipView } from './mothership-view' export { QueuedMessages } from './queued-messages' export { TemplatePrompts } from './template-prompts' 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 new file mode 100644 index 00000000000..e3d32331d25 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx @@ -0,0 +1,190 @@ +'use client' + +import { useLayoutEffect, useRef } from 'react' +import { cn } from '@/lib/core/utils/cn' +import { MessageActions } from '@/app/workspace/[workspaceId]/components' +import { ChatMessageAttachments } from '@/app/workspace/[workspaceId]/home/components/chat-message-attachments' +import { + assistantMessageHasRenderableContent, + MessageContent, +} from '@/app/workspace/[workspaceId]/home/components/message-content' +import { PendingTagIndicator } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' +import { QueuedMessages } from '@/app/workspace/[workspaceId]/home/components/queued-messages' +import { UserInput } from '@/app/workspace/[workspaceId]/home/components/user-input' +import { UserMessageContent } from '@/app/workspace/[workspaceId]/home/components/user-message-content' +import { useAutoScroll } from '@/app/workspace/[workspaceId]/home/hooks' +import type { + ChatMessage, + FileAttachmentForApi, + QueuedMessage, +} from '@/app/workspace/[workspaceId]/home/types' +import type { ChatContext } from '@/stores/panel' + +interface MothershipChatProps { + messages: ChatMessage[] + isSending: boolean + onSubmit: ( + text: string, + fileAttachments?: FileAttachmentForApi[], + contexts?: ChatContext[] + ) => void + onStopGeneration: () => void + messageQueue: QueuedMessage[] + onRemoveQueuedMessage: (id: string) => void + onSendQueuedMessage: (id: string) => Promise + onEditQueuedMessage: (id: string) => void + userId?: string + onContextAdd?: (context: ChatContext) => void + editValue?: string + onEditValueConsumed?: () => void + layout?: 'mothership-view' | 'copilot-view' + initialScrollBlocked?: boolean + animateInput?: boolean + onInputAnimationEnd?: () => void + className?: string +} + +const LAYOUT_STYLES = { + 'mothership-view': { + scrollContainer: + 'min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-6 pt-4 pb-8 [scrollbar-gutter:stable]', + content: 'mx-auto max-w-[42rem] space-y-6', + userRow: 'flex flex-col items-end gap-[6px] pt-3', + attachmentWidth: 'max-w-[70%]', + userBubble: 'max-w-[70%] overflow-hidden rounded-[16px] bg-[var(--surface-5)] px-3.5 py-2', + assistantRow: 'group/msg relative pb-5', + footer: 'flex-shrink-0 px-[24px] pb-[16px]', + footerInner: 'mx-auto max-w-[42rem]', + }, + 'copilot-view': { + scrollContainer: 'min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-3 pt-2 pb-4', + content: 'space-y-4', + userRow: 'flex flex-col items-end gap-[6px] pt-2', + attachmentWidth: 'max-w-[85%]', + userBubble: 'max-w-[85%] overflow-hidden rounded-[16px] bg-[var(--surface-5)] px-3 py-2', + assistantRow: 'group/msg relative pb-3', + footer: 'flex-shrink-0 px-3 pb-3', + footerInner: '', + }, +} as const + +export function MothershipChat({ + messages, + isSending, + onSubmit, + onStopGeneration, + messageQueue, + onRemoveQueuedMessage, + onSendQueuedMessage, + onEditQueuedMessage, + userId, + onContextAdd, + editValue, + onEditValueConsumed, + layout = 'mothership-view', + initialScrollBlocked = false, + animateInput = false, + onInputAnimationEnd, + className, +}: MothershipChatProps) { + const styles = LAYOUT_STYLES[layout] + const { ref: scrollContainerRef, scrollToBottom } = useAutoScroll(isSending) + const hasMessages = messages.length > 0 + const initialScrollDoneRef = useRef(false) + + useLayoutEffect(() => { + if (!hasMessages) { + initialScrollDoneRef.current = false + return + } + if (initialScrollDoneRef.current || initialScrollBlocked) return + initialScrollDoneRef.current = true + scrollToBottom() + }, [hasMessages, initialScrollBlocked, scrollToBottom]) + + return ( +
+
+
+ {messages.map((msg, index) => { + if (msg.role === 'user') { + const hasAttachments = Boolean(msg.attachments?.length) + return ( +
+ {hasAttachments && ( + + )} +
+ +
+
+ ) + } + + const hasAnyBlocks = Boolean(msg.contentBlocks?.length) + const hasRenderableAssistant = assistantMessageHasRenderableContent( + msg.contentBlocks ?? [], + msg.content ?? '' + ) + const isLastAssistant = index === messages.length - 1 + const isThisStreaming = isSending && isLastAssistant + + if (!hasAnyBlocks && !msg.content?.trim() && isThisStreaming) { + return + } + + if (!hasRenderableAssistant && !msg.content?.trim() && !isThisStreaming) { + return null + } + + const isLastMessage = index === messages.length - 1 + + return ( +
+ {!isThisStreaming && (msg.content || msg.contentBlocks?.length) && ( +
+ +
+ )} + +
+ ) + })} +
+
+ +
+
+ + +
+
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index 1f5c5b785a2..14c14d75213 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { useParams, useRouter } from 'next/navigation' import { PanelLeft } from '@/components/emcn/icons' @@ -11,21 +11,10 @@ import { LandingWorkflowSeedStorage, } from '@/lib/core/utils/browser-storage' import { persistImportedWorkflow } from '@/lib/workflows/operations/import-export' -import { MessageActions } from '@/app/workspace/[workspaceId]/components' import { useChatHistory, useMarkTaskRead } from '@/hooks/queries/tasks' import type { ChatContext } from '@/stores/panel' -import { - assistantMessageHasRenderableContent, - ChatMessageAttachments, - MessageContent, - MothershipView, - QueuedMessages, - TemplatePrompts, - UserInput, - UserMessageContent, -} from './components' -import { PendingTagIndicator } from './components/message-content/components/special-tags' -import { useAutoScroll, useChat, useMothershipResize } from './hooks' +import { MothershipChat, MothershipView, TemplatePrompts, UserInput } from './components' +import { getMothershipUseChatOptions, useChat, useMothershipResize } from './hooks' import type { FileAttachmentForApi, MothershipResource, MothershipResourceType } from './types' const logger = createLogger('Home') @@ -173,7 +162,11 @@ export function Home({ chatId }: HomeProps = {}) { sendNow, editQueuedMessage, streamingFile, - } = useChat(workspaceId, chatId, { onResourceEvent: handleResourceEvent }) + } = useChat( + workspaceId, + chatId, + getMothershipUseChatOptions({ onResourceEvent: handleResourceEvent }) + ) const [editingInputValue, setEditingInputValue] = useState('') const [prevChatId, setPrevChatId] = useState(chatId) @@ -285,22 +278,7 @@ export function Home({ chatId }: HomeProps = {}) { [addResource, handleResourceEvent] ) - const { ref: scrollContainerRef, scrollToBottom } = useAutoScroll(isSending) - const hasMessages = messages.length > 0 - const initialScrollDoneRef = useRef(false) - - useLayoutEffect(() => { - if (!hasMessages) { - initialScrollDoneRef.current = false - return - } - if (initialScrollDoneRef.current) return - if (resources.length > 0 && isResourceCollapsed) return - - initialScrollDoneRef.current = true - scrollToBottom() - }, [hasMessages, resources.length, isResourceCollapsed, scrollToBottom]) useEffect(() => { if (hasMessages) return @@ -354,90 +332,23 @@ export function Home({ chatId }: HomeProps = {}) { return (
-
-
- {messages.map((msg, index) => { - if (msg.role === 'user') { - const hasAttachments = msg.attachments && msg.attachments.length > 0 - return ( -
- {hasAttachments && ( - - )} -
- -
-
- ) - } - - const hasAnyBlocks = Boolean(msg.contentBlocks?.length) - const hasRenderableAssistant = assistantMessageHasRenderableContent( - msg.contentBlocks ?? [], - msg.content ?? '' - ) - const isLastAssistant = msg.role === 'assistant' && index === messages.length - 1 - const isThisStreaming = isSending && isLastAssistant - - if (!hasAnyBlocks && !msg.content?.trim() && isThisStreaming) { - return - } - - if (!hasRenderableAssistant && !msg.content?.trim() && !isThisStreaming) { - return null - } - - const isLastMessage = index === messages.length - 1 - - return ( -
- {!isThisStreaming && (msg.content || msg.contentBlocks?.length) && ( -
- -
- )} - -
- ) - })} -
-
- -
setIsInputEntering(false) : undefined} - > -
- - -
-
+ setIsInputEntering(false) : undefined} + initialScrollBlocked={resources.length > 0 && isResourceCollapsed} + />
{/* Resize handle — zero-width flex child whose absolute child straddles the border */} diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/index.ts index 85a9a257c68..9cce1f6eec1 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/index.ts @@ -1,6 +1,10 @@ export { useAnimatedPlaceholder } from './use-animated-placeholder' export { useAutoScroll } from './use-auto-scroll' export type { UseChatReturn } from './use-chat' -export { useChat } from './use-chat' +export { + getMothershipUseChatOptions, + getWorkflowCopilotUseChatOptions, + useChat, +} from './use-chat' export { useMothershipResize } from './use-mothership-resize' export { useStreamingReveal } from './use-streaming-reveal' diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index 8aa8b1a43b9..b2667577992 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -8,7 +8,7 @@ import { markRunToolManuallyStopped, reportManualRunToolStop, } from '@/lib/copilot/client-sse/run-tool-execution' -import { MOTHERSHIP_CHAT_API_PATH } from '@/lib/copilot/constants' +import { COPILOT_CHAT_API_PATH, MOTHERSHIP_CHAT_API_PATH } from '@/lib/copilot/constants' import { extractResourcesFromToolResult, isResourceToolName, @@ -263,6 +263,29 @@ export interface UseChatOptions { onStreamEnd?: (chatId: string, messages: ChatMessage[]) => void } +export function getMothershipUseChatOptions( + options: Pick = {} +): UseChatOptions { + return { + apiPath: MOTHERSHIP_CHAT_API_PATH, + stopPath: '/api/mothership/chat/stop', + ...options, + } +} + +export function getWorkflowCopilotUseChatOptions( + options: Pick< + UseChatOptions, + 'workflowId' | 'onToolResult' | 'onTitleUpdate' | 'onStreamEnd' + > = {} +): UseChatOptions { + return { + apiPath: COPILOT_CHAT_API_PATH, + stopPath: '/api/mothership/chat/stop', + ...options, + } +} + export function useChat( workspaceId: string, initialChatId?: string, 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 a0a9526b18d..b6f7541aaa6 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 @@ -34,16 +34,9 @@ import { Lock, Unlock, Upload } from '@/components/emcn/icons' import { VariableIcon } from '@/components/icons' import { useSession } from '@/lib/auth/auth-client' import { generateWorkflowJson } from '@/lib/workflows/operations/import-export' -import { ConversationListItem, MessageActions } from '@/app/workspace/[workspaceId]/components' -import { - assistantMessageHasRenderableContent, - MessageContent, - QueuedMessages, - UserInput, - UserMessageContent, -} from '@/app/workspace/[workspaceId]/home/components' -import { PendingTagIndicator } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' -import { useAutoScroll, useChat } from '@/app/workspace/[workspaceId]/home/hooks' +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 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' @@ -332,13 +325,15 @@ export const Panel = memo(function Panel() { removeFromQueue: copilotRemoveFromQueue, sendNow: copilotSendNow, editQueuedMessage: copilotEditQueuedMessage, - } = useChat(workspaceId, copilotChatId, { - apiPath: '/api/copilot/chat', - stopPath: '/api/mothership/chat/stop', - workflowId: activeWorkflowId || undefined, - onTitleUpdate: loadCopilotChats, - onToolResult: handleCopilotToolResult, - }) + } = useChat( + workspaceId, + copilotChatId, + getWorkflowCopilotUseChatOptions({ + workflowId: activeWorkflowId || undefined, + onTitleUpdate: loadCopilotChats, + onToolResult: handleCopilotToolResult, + }) + ) const handleCopilotNewChat = useCallback(() => { if (!activeWorkflowId || !workspaceId) return @@ -403,9 +398,6 @@ export const Panel = memo(function Panel() { [copilotSendMessage] ) - const { ref: copilotScrollRef, scrollToBottom: copilotScrollToBottom } = - useAutoScroll(copilotIsSending) - /** * Mark hydration as complete on mount * This allows React to take over visibility control from CSS @@ -813,77 +805,21 @@ export const Panel = memo(function Panel() {
-
-
- {copilotMessages.map((msg, index) => { - if (msg.role === 'user') { - return ( -
-
- -
-
- ) - } - - const hasAnyBlocks = Boolean(msg.contentBlocks?.length) - const hasRenderableAssistant = assistantMessageHasRenderableContent( - msg.contentBlocks ?? [], - msg.content ?? '' - ) - const isLastAssistant = - msg.role === 'assistant' && index === copilotMessages.length - 1 - const isThisStreaming = copilotIsSending && isLastAssistant - - if (!hasAnyBlocks && !msg.content?.trim() && isThisStreaming) { - return - } - - if (!hasRenderableAssistant && !msg.content?.trim() && !isThisStreaming) { - return null - } - - const isLastMessage = index === copilotMessages.length - 1 - - return ( -
- {!isThisStreaming && (msg.content || msg.contentBlocks?.length) && ( -
- -
- )} - -
- ) - })} -
-
- -
- - -
+ )}
Date: Tue, 24 Mar 2026 16:43:51 -0700 Subject: [PATCH 2/4] Fix lint --- .../[workspaceId]/w/[workflowId]/components/panel/panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b6f7541aaa6..3c1c50530be 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 @@ -818,7 +818,7 @@ export const Panel = memo(function Panel() { userId={session?.user?.id} editValue={copilotEditingInputValue} onEditValueConsumed={clearCopilotEditingValue} - layout='copilot-view' + layout='mothership-view' />
)} From b8ff43ca072b4e11cd900aa5964e3e6841bfb813 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 24 Mar 2026 16:48:26 -0700 Subject: [PATCH 3/4] Restore copilot layout --- .../[workspaceId]/w/[workflowId]/components/panel/panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3c1c50530be..b6f7541aaa6 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 @@ -818,7 +818,7 @@ export const Panel = memo(function Panel() { userId={session?.user?.id} editValue={copilotEditingInputValue} onEditValueConsumed={clearCopilotEditingValue} - layout='mothership-view' + layout='copilot-view' /> )} From 6a6e62bb3d04a426b1368306952d31ddc5b94a67 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 24 Mar 2026 17:02:50 -0700 Subject: [PATCH 4/4] Fix subagent text not animating collapses --- .../components/agent-group/agent-group.tsx | 20 ++++++++++++++++++- .../message-content/message-content.tsx | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx index ac024b84c81..12f5f454d42 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx @@ -44,6 +44,21 @@ export function AgentGroup({ const [expanded, setExpanded] = useState(defaultExpanded || !allDone) const [mounted, setMounted] = useState(defaultExpanded || !allDone) const didAutoCollapseRef = useRef(allDone) + const wasAutoExpandedRef = useRef(defaultExpanded) + + useEffect(() => { + if (defaultExpanded) { + wasAutoExpandedRef.current = true + setMounted(true) + setExpanded(true) + return + } + + if (wasAutoExpandedRef.current && allDone) { + wasAutoExpandedRef.current = false + setExpanded(false) + } + }, [defaultExpanded, allDone]) useEffect(() => { if (!autoCollapse || didAutoCollapseRef.current) return @@ -65,7 +80,10 @@ export function AgentGroup({ {hasItems ? (