Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(home): resizable chat/resource panel divider
  • Loading branch information
waleedlatif1 committed Mar 18, 2026
commit ba928f642b0a11789d3a45ca5b9cd2d7fc241d29
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Comment thread
waleedlatif1 marked this conversation as resolved.
}, [workspaceId, workflowId])

return (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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<HTMLDivElement, MothershipViewProps>(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<PreviewMode>('preview')
const handleCyclePreview = useCallback(() => setPreviewMode((m) => PREVIEW_CYCLE[m]), [])
const [previewMode, setPreviewMode] = useState<PreviewMode>('preview')
const [prevActiveId, setPrevActiveId] = useState<string | null | undefined>(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 (
<div
className={cn(
'relative z-10 flex h-full flex-col overflow-hidden border-[var(--border)] bg-[var(--bg)] transition-[width,min-width,border-width] duration-300 ease-out',
isCollapsed ? 'w-0 min-w-0 border-l-0' : 'w-[60%] border-l',
className
)}
>
<div className='flex min-h-0 flex-1 flex-col'>
<ResourceTabs
workspaceId={workspaceId}
chatId={chatId}
resources={resources}
activeId={active?.id ?? null}
onSelect={onSelectResource}
onAddResource={onAddResource}
onRemoveResource={onRemoveResource}
onReorderResources={onReorderResources}
onCollapse={onCollapse}
actions={active ? <ResourceActions workspaceId={workspaceId} resource={active} /> : null}
previewMode={isActivePreviewable ? previewMode : undefined}
onCyclePreviewMode={isActivePreviewable ? handleCyclePreview : undefined}
/>
<div className='min-h-0 flex-1 overflow-hidden'>
{active ? (
<ResourceContent
workspaceId={workspaceId}
resource={active}
previewMode={isActivePreviewable ? previewMode : undefined}
/>
) : (
<div className='flex h-full items-center justify-center text-[14px] text-[var(--text-muted)]'>
Click "+" above to add a resource
</div>
)}
return (
<div
ref={ref}
className={cn(
'relative z-10 flex h-full flex-col overflow-hidden border-[var(--border)] bg-[var(--bg)] transition-[width,min-width,border-width] duration-300 ease-out',
isCollapsed ? 'w-0 min-w-0 border-l-0' : 'w-[60%] border-l',
className
)}
>
<div className='flex min-h-0 flex-1 flex-col'>
<ResourceTabs
workspaceId={workspaceId}
chatId={chatId}
resources={resources}
activeId={active?.id ?? null}
onSelect={onSelectResource}
onAddResource={onAddResource}
onRemoveResource={onRemoveResource}
onReorderResources={onReorderResources}
onCollapse={onCollapse}
actions={
active ? <ResourceActions workspaceId={workspaceId} resource={active} /> : null
}
previewMode={isActivePreviewable ? previewMode : undefined}
onCyclePreviewMode={isActivePreviewable ? handleCyclePreview : undefined}
/>
<div className='min-h-0 flex-1 overflow-hidden'>
{active ? (
<ResourceContent
workspaceId={workspaceId}
resource={active}
previewMode={isActivePreviewable ? previewMode : undefined}
/>
) : (
<div className='flex h-full items-center justify-center text-[14px] text-[var(--text-muted)]'>
Click "+" above to add a resource
</div>
)}
</div>
</div>
</div>
</div>
)
})
)
})
)
49 changes: 33 additions & 16 deletions apps/sim/app/workspace/[workspaceId]/home/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -190,10 +202,6 @@ export function Home({ chatId }: HomeProps = {}) {
[editQueuedMessage]
)

useEffect(() => {
setEditingInputValue('')
}, [chatId])

useEffect(() => {
wasSendingRef.current = false
if (resolvedChatId) markRead(resolvedChatId)
Expand All @@ -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[]) => {
Expand Down Expand Up @@ -359,7 +362,7 @@ export function Home({ chatId }: HomeProps = {}) {

return (
<div className='relative flex h-full bg-[var(--bg)]'>
<div className='flex h-full min-w-0 flex-1 flex-col'>
<div className='flex h-full min-w-[320px] flex-1 flex-col'>
<div
ref={scrollContainerRef}
className='min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-6 pt-4 pb-8 [scrollbar-gutter:stable]'
Expand Down Expand Up @@ -458,7 +461,21 @@ export function Home({ chatId }: HomeProps = {}) {
</div>
</div>

{/* Resize handle — zero-width flex child whose absolute child straddles the border */}
{!isResourceCollapsed && (
<div className='relative z-20 w-0 flex-none' aria-hidden='true'>
<div
className='absolute inset-y-0 left-[-4px] w-[8px] cursor-ew-resize'
role='separator'
aria-orientation='vertical'
aria-label='Resize resource panel'
onMouseDown={handleResizeMouseDown}
/>
</div>
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated
)}

<MothershipView
ref={mothershipRef}
workspaceId={workspaceId}
chatId={resolvedChatId}
resources={resources}
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/workspace/[workspaceId]/home/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export { useAnimatedPlaceholder } from './use-animated-placeholder'
export { useAutoScroll } from './use-auto-scroll'
export type { UseChatReturn } from './use-chat'
export { useChat } from './use-chat'
export { useMothershipResize } from './use-mothership-resize'
export { useStreamingReveal } from './use-streaming-reveal'
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useCallback, useEffect, useRef } from 'react'
import { MOTHERSHIP_WIDTH } from '@/stores/constants'

/**
* Hook for managing resize of the MothershipView resource panel.
*
* Uses imperative DOM manipulation (zero React re-renders during drag).
* Attach `mothershipRef` to the MothershipView root div and call
* `handleResizeMouseDown` from the drag handle's onMouseDown.
* Call `clearWidth` when the panel collapses so the CSS class retakes control.
*/
export function useMothershipResize() {
const mothershipRef = useRef<HTMLDivElement | null>(null)
// Stored so the useEffect cleanup can tear down listeners if the component unmounts mid-drag
const cleanupRef = useRef<(() => void) | null>(null)

const handleResizeMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault()

const el = mothershipRef.current
if (!el) return

// Pin to current rendered width so drag starts from the visual position
el.style.width = `${el.getBoundingClientRect().width}px`

// Disable CSS transition to prevent animation lag during drag
const prevTransition = el.style.transition
el.style.transition = 'none'
document.body.style.cursor = 'ew-resize'
document.body.style.userSelect = 'none'

const handleMouseMove = (moveEvent: MouseEvent) => {
const newWidth = window.innerWidth - moveEvent.clientX
const maxWidth = window.innerWidth * MOTHERSHIP_WIDTH.MAX_PERCENTAGE
el.style.width = `${Math.min(Math.max(newWidth, MOTHERSHIP_WIDTH.MIN), maxWidth)}px`
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated
}

const handleMouseUp = () => {
el.style.transition = prevTransition
document.body.style.cursor = ''
document.body.style.userSelect = ''
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
cleanupRef.current = null
}
Comment thread
waleedlatif1 marked this conversation as resolved.

cleanupRef.current = handleMouseUp
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}, [])
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated

// Tear down any active drag if the component unmounts mid-drag
useEffect(() => {
return () => {
cleanupRef.current?.()
}
}, [])

/** Remove inline width so the collapse CSS class retakes control */
const clearWidth = useCallback(() => {
mothershipRef.current?.style.removeProperty('width')
}, [])

return { mothershipRef, handleResizeMouseDown, clearWidth }
}
7 changes: 7 additions & 0 deletions apps/sim/stores/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,12 @@ export const OUTPUT_PANEL_WIDTH = {
MIN: 280,
} as const

/** Home chat resource panel (MothershipView) width constraints */
export const MOTHERSHIP_WIDTH = {
MIN: 280,
/** Maximum is 70% of viewport, enforced dynamically */
MAX_PERCENTAGE: 0.7,
} as const

/** Terminal block column width - minimum width for the logs column */
export const TERMINAL_BLOCK_COLUMN_WIDTH = 240 as const
Loading