Skip to content

Commit db0fd90

Browse files
andresdjassoclaude
andcommitted
feat(workflow): draggable divider between the docked chat and the editor
Mirrors useMothershipResize (imperative width, pointer capture, zero re-renders during drag) measured from the pane's left edge since the chat leads the row. Clamped to 360px–55% of the viewport, re-clamped on window resize, cursor/selection restored on release or cancel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 65ee6ad commit db0fd90

1 file changed

Lines changed: 92 additions & 12 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/docked-chat/workflow-with-chat.tsx

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
'use client'
22

3-
import { useCallback, useState } from 'react'
3+
import { useCallback, useEffect, useRef, useState } from 'react'
44
import { useParams, useSearchParams } from 'next/navigation'
55
import Workflow from '@/app/workspace/[workspaceId]/w/[workflowId]/workflow'
66
import { DockedChat } from './docked-chat'
77

88
/** Sentinel `?chat=` value for a docked chat that hasn't been created yet. */
99
const NEW_CHAT_PARAM = 'new'
1010

11+
/** Drag bounds for the docked chat pane. */
12+
const CHAT_PANE = { MIN: 360, MAX_PERCENTAGE: 0.55 } as const
13+
1114
interface DockState {
1215
open: boolean
1316
chatId?: string
@@ -63,22 +66,99 @@ export function WorkflowWithChat() {
6366
[reflectChatParam]
6467
)
6568

69+
// Divider drag mirrors useMothershipResize (imperative width, pointer
70+
// capture, zero re-renders) but measures from the pane's LEFT edge — this
71+
// pane leads the row instead of trailing it.
72+
const chatPaneRef = useRef<HTMLDivElement | null>(null)
73+
const dragCleanupRef = useRef<(() => void) | null>(null)
74+
75+
const handleResizePointerDown = useCallback((e: React.PointerEvent) => {
76+
e.preventDefault()
77+
const el = chatPaneRef.current
78+
if (!el) return
79+
80+
const handle = e.currentTarget as HTMLElement
81+
handle.setPointerCapture(e.pointerId)
82+
el.style.width = `${el.getBoundingClientRect().width}px`
83+
document.body.style.cursor = 'ew-resize'
84+
document.body.style.userSelect = 'none'
85+
86+
const ac = new AbortController()
87+
const { signal } = ac
88+
const cleanup = () => {
89+
ac.abort()
90+
document.body.style.cursor = ''
91+
document.body.style.userSelect = ''
92+
dragCleanupRef.current = null
93+
}
94+
dragCleanupRef.current = cleanup
95+
96+
handle.addEventListener(
97+
'pointermove',
98+
(moveEvent: PointerEvent) => {
99+
const newWidth = moveEvent.clientX - el.getBoundingClientRect().left
100+
const maxWidth = window.innerWidth * CHAT_PANE.MAX_PERCENTAGE
101+
el.style.width = `${Math.min(Math.max(newWidth, CHAT_PANE.MIN), maxWidth)}px`
102+
},
103+
{ signal }
104+
)
105+
handle.addEventListener(
106+
'pointerup',
107+
(upEvent: PointerEvent) => {
108+
handle.releasePointerCapture(upEvent.pointerId)
109+
cleanup()
110+
},
111+
{ signal }
112+
)
113+
handle.addEventListener('pointercancel', cleanup, { signal })
114+
}, [])
115+
116+
useEffect(() => () => dragCleanupRef.current?.(), [])
117+
118+
useEffect(() => {
119+
const handleWindowResize = () => {
120+
const el = chatPaneRef.current
121+
if (!el || !el.style.width) return
122+
const maxWidth = window.innerWidth * CHAT_PANE.MAX_PERCENTAGE
123+
if (el.getBoundingClientRect().width > maxWidth) {
124+
el.style.width = `${maxWidth}px`
125+
}
126+
}
127+
window.addEventListener('resize', handleWindowResize)
128+
return () => window.removeEventListener('resize', handleWindowResize)
129+
}, [])
130+
66131
if (!workspaceId || !workflowId) return null
67132

68133
return (
69134
<div className='flex h-full w-full'>
70135
{dock.open && (
71-
<div className='flex h-full w-[clamp(360px,34%,520px)] flex-shrink-0 flex-col border-[var(--border)] border-r'>
72-
<DockedChat
73-
key={dock.chatId ?? 'new'}
74-
workspaceId={workspaceId}
75-
workflowId={workflowId}
76-
chatId={dock.chatId}
77-
onClose={closeChat}
78-
onSelectChat={openChat}
79-
onChatResolved={handleChatResolved}
80-
/>
81-
</div>
136+
<>
137+
<div
138+
ref={chatPaneRef}
139+
className='flex h-full w-[clamp(360px,34%,520px)] flex-shrink-0 flex-col border-[var(--border)] border-r'
140+
>
141+
<DockedChat
142+
key={dock.chatId ?? 'new'}
143+
workspaceId={workspaceId}
144+
workflowId={workflowId}
145+
chatId={dock.chatId}
146+
onClose={closeChat}
147+
onSelectChat={openChat}
148+
onChatResolved={handleChatResolved}
149+
/>
150+
</div>
151+
{/* Zero-width flex child whose absolute child straddles the border. */}
152+
<div className='relative z-20 w-0 flex-none'>
153+
<div
154+
className='absolute inset-y-0 left-[-4px] w-[8px] cursor-ew-resize'
155+
role='separator'
156+
aria-orientation='vertical'
157+
aria-label='Resize chat pane'
158+
onPointerDown={handleResizePointerDown}
159+
/>
160+
</div>
161+
</>
82162
)}
83163
<div className='h-full min-w-0 flex-1'>
84164
<Workflow

0 commit comments

Comments
 (0)