From ba928f642b0a11789d3a45ca5b9cd2d7fc241d29 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 17 Mar 2026 21:04:58 -0700 Subject: [PATCH 01/11] feat(home): resizable chat/resource panel divider --- .../resource-content/resource-content.tsx | 4 +- .../mothership-view/mothership-view.tsx | 131 ++++++++++-------- .../app/workspace/[workspaceId]/home/home.tsx | 49 ++++--- .../[workspaceId]/home/hooks/index.ts | 1 + .../home/hooks/use-mothership-resize.ts | 65 +++++++++ apps/sim/stores/constants.ts | 7 + 6 files changed, 179 insertions(+), 78 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/home/hooks/use-mothership-resize.ts 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 9ceeb35ac5f..6ba4f70e76e 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 @@ -160,8 +160,8 @@ export function EmbeddedWorkflowActions({ workspaceId, workflowId }: EmbeddedWor ]) const handleOpenWorkflow = useCallback(() => { - router.push(`/workspace/${workspaceId}/w/${workflowId}`) - }, [router, workspaceId, workflowId]) + window.open(`/workspace/${workspaceId}/w/${workflowId}`, '_blank') + }, [workspaceId, workflowId]) 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 b95d761ee6f..d0cf8336cea 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 @@ -1,6 +1,6 @@ 'use client' -import { memo, useCallback, useEffect, useState } from 'react' +import { forwardRef, memo, useCallback, useState } from 'react' import { cn } from '@/lib/core/utils/cn' import { getFileExtension } from '@/lib/uploads/utils/file-utils' import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer' @@ -31,68 +31,79 @@ interface MothershipViewProps { className?: string } -export const MothershipView = memo(function MothershipView({ - workspaceId, - chatId, - resources, - activeResourceId, - onSelectResource, - onAddResource, - onRemoveResource, - onReorderResources, - onCollapse, - isCollapsed, - className, -}: MothershipViewProps) { - const active = resources.find((r) => r.id === activeResourceId) ?? resources[0] ?? null +export const MothershipView = memo( + forwardRef(function MothershipView( + { + workspaceId, + chatId, + resources, + activeResourceId, + onSelectResource, + onAddResource, + onRemoveResource, + onReorderResources, + onCollapse, + isCollapsed, + className, + }: MothershipViewProps, + ref + ) { + const active = resources.find((r) => r.id === activeResourceId) ?? resources[0] ?? null - const [previewMode, setPreviewMode] = useState('preview') - const handleCyclePreview = useCallback(() => setPreviewMode((m) => PREVIEW_CYCLE[m]), []) + const [previewMode, setPreviewMode] = useState('preview') + const [prevActiveId, setPrevActiveId] = useState(active?.id) + const handleCyclePreview = useCallback(() => setPreviewMode((m) => PREVIEW_CYCLE[m]), []) - useEffect(() => { - setPreviewMode('preview') - }, [active?.id]) + // Reset preview mode to default when the active resource changes (guarded render-phase update) + if (active?.id !== prevActiveId) { + setPrevActiveId(active?.id) + setPreviewMode('preview') + } - const isActivePreviewable = - active?.type === 'file' && RICH_PREVIEWABLE_EXTENSIONS.has(getFileExtension(active.title)) + const isActivePreviewable = + active?.type === 'file' && RICH_PREVIEWABLE_EXTENSIONS.has(getFileExtension(active.title)) - return ( -
-
- : null} - previewMode={isActivePreviewable ? previewMode : undefined} - onCyclePreviewMode={isActivePreviewable ? handleCyclePreview : undefined} - /> -
- {active ? ( - - ) : ( -
- Click "+" above to add a resource -
- )} + return ( +
+
+ : null + } + previewMode={isActivePreviewable ? previewMode : undefined} + onCyclePreviewMode={isActivePreviewable ? handleCyclePreview : undefined} + /> +
+ {active ? ( + + ) : ( +
+ Click "+" above to add a resource +
+ )} +
-
- ) -}) + ) + }) +) diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index b238e60ca82..7b69debb3bb 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -26,7 +26,7 @@ import { UserMessageContent, } from './components' import { PendingTagIndicator } from './components/message-content/components/special-tags' -import { useAutoScroll, useChat } from './hooks' +import { useAutoScroll, useChat, useMothershipResize } from './hooks' import type { FileAttachmentForApi, MothershipResource, MothershipResourceType } from './types' const logger = createLogger('Home') @@ -138,13 +138,18 @@ export function Home({ chatId }: HomeProps = {}) { useChatHistory(chatId) const { mutate: markRead } = useMarkTaskRead(workspaceId) + const { mothershipRef, handleResizeMouseDown, clearWidth } = useMothershipResize() + const [isResourceCollapsed, setIsResourceCollapsed] = useState(true) const [isResourceAnimatingIn, setIsResourceAnimatingIn] = useState(false) const [skipResourceTransition, setSkipResourceTransition] = useState(false) const isResourceCollapsedRef = useRef(isResourceCollapsed) isResourceCollapsedRef.current = isResourceCollapsed - const collapseResource = useCallback(() => setIsResourceCollapsed(true), []) + const collapseResource = useCallback(() => { + clearWidth() + setIsResourceCollapsed(true) + }, [clearWidth]) const expandResource = useCallback(() => { setIsResourceCollapsed(false) setIsResourceAnimatingIn(true) @@ -178,8 +183,15 @@ export function Home({ chatId }: HomeProps = {}) { } = useChat(workspaceId, chatId, { onResourceEvent: handleResourceEvent }) const [editingInputValue, setEditingInputValue] = useState('') + const [prevChatId, setPrevChatId] = useState(chatId) const clearEditingValue = useCallback(() => setEditingInputValue(''), []) + // Clear editing value when navigating to a different chat (guarded render-phase update) + if (chatId !== prevChatId) { + setPrevChatId(chatId) + setEditingInputValue('') + } + const handleEditQueuedMessage = useCallback( (id: string) => { const msg = editQueuedMessage(id) @@ -190,10 +202,6 @@ export function Home({ chatId }: HomeProps = {}) { [editQueuedMessage] ) - useEffect(() => { - setEditingInputValue('') - }, [chatId]) - useEffect(() => { wasSendingRef.current = false if (resolvedChatId) markRead(resolvedChatId) @@ -213,17 +221,12 @@ export function Home({ chatId }: HomeProps = {}) { }, [isResourceAnimatingIn]) useEffect(() => { - if (resources.length > 0 && isResourceCollapsedRef.current) { - setSkipResourceTransition(true) - setIsResourceCollapsed(false) - } - }, [resources]) - - useEffect(() => { - if (!skipResourceTransition) return + if (!(resources.length > 0 && isResourceCollapsedRef.current)) return + setIsResourceCollapsed(false) + setSkipResourceTransition(true) const id = requestAnimationFrame(() => setSkipResourceTransition(false)) return () => cancelAnimationFrame(id) - }, [skipResourceTransition]) + }, [resources]) const handleSubmit = useCallback( (text: string, fileAttachments?: FileAttachmentForApi[], contexts?: ChatContext[]) => { @@ -359,7 +362,7 @@ export function Home({ chatId }: HomeProps = {}) { return (
-
+
+ {/* Resize handle — zero-width flex child whose absolute child straddles the border */} + {!isResourceCollapsed && ( +