@@ -1466,114 +1602,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction
)}
- {task && (
- <>
-
- isAtBottom || stickyFollowRef.current}
- atBottomStateChange={(isAtBottom: boolean) => {
- setIsAtBottom(isAtBottom)
- // Only show the scroll-to-bottom button if not at bottom
- setShowScrollToBottom(!isAtBottom)
- }}
- atBottomThreshold={10}
- initialTopMostItemIndex={groupedMessages.length - 1}
- />
-
- {areButtonsVisible && (
-
- {showScrollToBottom ? (
-
-
-
- ) : (
- <>
- {primaryButtonText && !isStreaming && (
-
-
-
- )}
- {(secondaryButtonText || isStreaming) && (
-
-
-
- )}
- >
- )}
-
- )}
- >
- )}
-
{
diff --git a/webview-ui/src/components/chat/ResizablePanels.tsx b/webview-ui/src/components/chat/ResizablePanels.tsx
new file mode 100644
index 0000000000..42a6333215
--- /dev/null
+++ b/webview-ui/src/components/chat/ResizablePanels.tsx
@@ -0,0 +1,134 @@
+import React, { useState, useCallback, useEffect, useRef } from "react"
+import { GripVertical } from "lucide-react"
+import { cn } from "@src/lib/utils"
+
+interface ResizablePanelsProps {
+ leftPanel: React.ReactNode
+ rightPanel: React.ReactNode
+ minLeftWidth?: number // as percentage (default 20)
+ minRightWidth?: number // as percentage (default 35)
+ storageKey?: string // localStorage key for persisting widths
+ className?: string
+}
+
+const DEFAULT_LEFT_WIDTH = 25 // Default 25% for sidebar
+const DEFAULT_MIN_LEFT = 20 // Minimum 20% for sidebar
+const DEFAULT_MIN_RIGHT = 35 // Minimum 35% for conversation
+
+export const ResizablePanels: React.FC = ({
+ leftPanel,
+ rightPanel,
+ minLeftWidth = DEFAULT_MIN_LEFT,
+ minRightWidth = DEFAULT_MIN_RIGHT,
+ storageKey = "resizable-panels-width",
+ className,
+}) => {
+ const containerRef = useRef(null)
+ const [isDragging, setIsDragging] = useState(false)
+ const [leftWidth, setLeftWidth] = useState(() => {
+ // Load from localStorage if available
+ if (storageKey) {
+ const stored = localStorage.getItem(storageKey)
+ if (stored) {
+ const parsed = parseFloat(stored)
+ if (!isNaN(parsed) && parsed >= minLeftWidth && parsed <= 100 - minRightWidth) {
+ return parsed
+ }
+ }
+ }
+ return DEFAULT_LEFT_WIDTH
+ })
+
+ // Persist to localStorage whenever leftWidth changes
+ useEffect(() => {
+ if (storageKey) {
+ localStorage.setItem(storageKey, leftWidth.toString())
+ }
+ }, [leftWidth, storageKey])
+
+ const handleMouseDown = useCallback((e: React.MouseEvent) => {
+ e.preventDefault()
+ setIsDragging(true)
+ }, [])
+
+ const handleMouseMove = useCallback(
+ (e: MouseEvent) => {
+ if (!isDragging || !containerRef.current) return
+
+ const container = containerRef.current
+ const rect = container.getBoundingClientRect()
+ const offsetX = e.clientX - rect.left
+ const newLeftWidth = (offsetX / rect.width) * 100
+
+ // Clamp to min/max values
+ const clampedWidth = Math.max(minLeftWidth, Math.min(100 - minRightWidth, newLeftWidth))
+ setLeftWidth(clampedWidth)
+ },
+ [isDragging, minLeftWidth, minRightWidth],
+ )
+
+ const handleMouseUp = useCallback(() => {
+ setIsDragging(false)
+ }, [])
+
+ useEffect(() => {
+ if (isDragging) {
+ window.addEventListener("mousemove", handleMouseMove)
+ window.addEventListener("mouseup", handleMouseUp)
+ // Prevent text selection while dragging
+ document.body.style.userSelect = "none"
+ document.body.style.cursor = "ew-resize"
+
+ return () => {
+ window.removeEventListener("mousemove", handleMouseMove)
+ window.removeEventListener("mouseup", handleMouseUp)
+ document.body.style.userSelect = ""
+ document.body.style.cursor = ""
+ }
+ }
+ }, [isDragging, handleMouseMove, handleMouseUp])
+
+ const rightWidth = 100 - leftWidth
+
+ return (
+
+ {/* Left Panel */}
+
+ {leftPanel}
+
+
+ {/* Divider with Drag Handle */}
+
+ {/* Visual divider line */}
+
+ {/* Drag handle icon */}
+
+
+
+
+
+ {/* Right Panel */}
+
+ {rightPanel}
+
+
+ )
+}