From 3a317615427b451dff7315fb9d3179f06f6945fc Mon Sep 17 00:00:00 2001 From: Auto Date: Sun, 8 Feb 2026 15:45:21 +0200 Subject: [PATCH] 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 --- ui/src/components/AssistantPanel.tsx | 59 +++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/ui/src/components/AssistantPanel.tsx b/ui/src/components/AssistantPanel.tsx index 36e8448..ae91681 100644 --- a/ui/src/components/AssistantPanel.tsx +++ b/ui/src/components/AssistantPanel.tsx @@ -6,7 +6,7 @@ * Manages conversation state with localStorage persistence. */ -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback, useRef } from 'react' import { X, Bot } from 'lucide-react' import { AssistantChat } from './AssistantChat' import { useConversation } from '../hooks/useConversations' @@ -20,6 +20,10 @@ interface AssistantPanelProps { } const STORAGE_KEY_PREFIX = 'assistant-conversation-' +const WIDTH_STORAGE_KEY = 'assistant-panel-width' +const DEFAULT_WIDTH = 400 +const MIN_WIDTH = 300 +const MAX_WIDTH_VW = 90 function getStoredConversationId(projectName: string): number | null { try { @@ -100,6 +104,49 @@ export function AssistantPanel({ projectName, isOpen, onClose }: AssistantPanelP setConversationId(id) }, []) + // Resizable panel width + const [panelWidth, setPanelWidth] = useState(() => { + try { + const stored = localStorage.getItem(WIDTH_STORAGE_KEY) + if (stored) return Math.max(MIN_WIDTH, parseInt(stored, 10)) + } catch { /* ignore */ } + return DEFAULT_WIDTH + }) + const isResizing = useRef(false) + + const handleMouseDown = useCallback((e: React.MouseEvent) => { + e.preventDefault() + isResizing.current = true + const startX = e.clientX + const startWidth = panelWidth + const maxWidth = window.innerWidth * (MAX_WIDTH_VW / 100) + + const handleMouseMove = (e: MouseEvent) => { + if (!isResizing.current) return + const delta = startX - e.clientX + const newWidth = Math.min(maxWidth, Math.max(MIN_WIDTH, startWidth + delta)) + setPanelWidth(newWidth) + } + + const handleMouseUp = () => { + isResizing.current = false + document.removeEventListener('mousemove', handleMouseMove) + document.removeEventListener('mouseup', handleMouseUp) + document.body.style.cursor = '' + document.body.style.userSelect = '' + // Persist width + setPanelWidth((w) => { + localStorage.setItem(WIDTH_STORAGE_KEY, String(w)) + return w + }) + } + + document.body.style.cursor = 'col-resize' + document.body.style.userSelect = 'none' + document.addEventListener('mousemove', handleMouseMove) + document.addEventListener('mouseup', handleMouseUp) + }, [panelWidth]) + return ( <> {/* Backdrop - click to close */} @@ -115,17 +162,25 @@ export function AssistantPanel({ projectName, isOpen, onClose }: AssistantPanelP
+ {/* Resize handle */} +
+
+
+ {/* Header */}