66 * Manages conversation state with localStorage persistence.
77 */
88
9- import { useState , useEffect , useCallback } from 'react'
9+ import { useState , useEffect , useCallback , useRef } from 'react'
1010import { X , Bot } from 'lucide-react'
1111import { AssistantChat } from './AssistantChat'
1212import { useConversation } from '../hooks/useConversations'
@@ -20,6 +20,10 @@ interface AssistantPanelProps {
2020}
2121
2222const 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
2428function 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