Skip to content

Commit 3a31761

Browse files
leonvanzylclaude
andcommitted
ui: add resizable drag handle to assistant chat panel
Add a draggable resize handle on the left edge of the AI assistant panel, allowing users to adjust the panel width by clicking and dragging. Width is persisted to localStorage across sessions. - Drag handle with hover highlight (border -> primary color) - Min width 300px, max width 90vw - Width saved to localStorage under 'assistant-panel-width' - Cursor changes to col-resize and text selection disabled during drag Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 96feb38 commit 3a31761

1 file changed

Lines changed: 57 additions & 2 deletions

File tree

ui/src/components/AssistantPanel.tsx

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* Manages conversation state with localStorage persistence.
77
*/
88

9-
import { useState, useEffect, useCallback } from 'react'
9+
import { useState, useEffect, useCallback, useRef } from 'react'
1010
import { X, Bot } from 'lucide-react'
1111
import { AssistantChat } from './AssistantChat'
1212
import { useConversation } from '../hooks/useConversations'
@@ -20,6 +20,10 @@ interface AssistantPanelProps {
2020
}
2121

2222
const STORAGE_KEY_PREFIX = 'assistant-conversation-'
23+
const WIDTH_STORAGE_KEY = 'assistant-panel-width'
24+
const DEFAULT_WIDTH = 400
25+
const MIN_WIDTH = 300
26+
const MAX_WIDTH_VW = 90
2327

2428
function getStoredConversationId(projectName: string): number | null {
2529
try {
@@ -100,6 +104,49 @@ export function AssistantPanel({ projectName, isOpen, onClose }: AssistantPanelP
100104
setConversationId(id)
101105
}, [])
102106

107+
// Resizable panel width
108+
const [panelWidth, setPanelWidth] = useState<number>(() => {
109+
try {
110+
const stored = localStorage.getItem(WIDTH_STORAGE_KEY)
111+
if (stored) return Math.max(MIN_WIDTH, parseInt(stored, 10))
112+
} catch { /* ignore */ }
113+
return DEFAULT_WIDTH
114+
})
115+
const isResizing = useRef(false)
116+
117+
const handleMouseDown = useCallback((e: React.MouseEvent) => {
118+
e.preventDefault()
119+
isResizing.current = true
120+
const startX = e.clientX
121+
const startWidth = panelWidth
122+
const maxWidth = window.innerWidth * (MAX_WIDTH_VW / 100)
123+
124+
const handleMouseMove = (e: MouseEvent) => {
125+
if (!isResizing.current) return
126+
const delta = startX - e.clientX
127+
const newWidth = Math.min(maxWidth, Math.max(MIN_WIDTH, startWidth + delta))
128+
setPanelWidth(newWidth)
129+
}
130+
131+
const handleMouseUp = () => {
132+
isResizing.current = false
133+
document.removeEventListener('mousemove', handleMouseMove)
134+
document.removeEventListener('mouseup', handleMouseUp)
135+
document.body.style.cursor = ''
136+
document.body.style.userSelect = ''
137+
// Persist width
138+
setPanelWidth((w) => {
139+
localStorage.setItem(WIDTH_STORAGE_KEY, String(w))
140+
return w
141+
})
142+
}
143+
144+
document.body.style.cursor = 'col-resize'
145+
document.body.style.userSelect = 'none'
146+
document.addEventListener('mousemove', handleMouseMove)
147+
document.addEventListener('mouseup', handleMouseUp)
148+
}, [panelWidth])
149+
103150
return (
104151
<>
105152
{/* Backdrop - click to close */}
@@ -115,17 +162,25 @@ export function AssistantPanel({ projectName, isOpen, onClose }: AssistantPanelP
115162
<div
116163
className={`
117164
fixed right-0 top-0 bottom-0 z-50
118-
w-[400px] max-w-[90vw]
119165
bg-card
120166
border-l border-border
121167
transform transition-transform duration-300 ease-out
122168
flex flex-col shadow-xl
123169
${isOpen ? 'translate-x-0' : 'translate-x-full'}
124170
`}
171+
style={{ width: `${panelWidth}px`, maxWidth: `${MAX_WIDTH_VW}vw` }}
125172
role="dialog"
126173
aria-label="Project Assistant"
127174
aria-hidden={!isOpen}
128175
>
176+
{/* Resize handle */}
177+
<div
178+
className="absolute left-0 top-0 bottom-0 w-1.5 cursor-col-resize z-10 group"
179+
onMouseDown={handleMouseDown}
180+
>
181+
<div className="absolute inset-y-0 left-0 w-0.5 bg-border group-hover:bg-primary transition-colors" />
182+
</div>
183+
129184
{/* Header */}
130185
<div className="flex items-center justify-between px-4 py-3 border-b border-border bg-primary text-primary-foreground">
131186
<div className="flex items-center gap-2">

0 commit comments

Comments
 (0)