1- import { memo , useState , useCallback } from "react" ;
1+ import { memo , useState , useCallback , useRef , useEffect } from "react" ;
22import { type TimestampFormat } from "../appSettings" ;
33import { Badge } from "./ui/badge" ;
44import { Button } from "./ui/button" ;
@@ -27,6 +27,13 @@ import { Menu, MenuItem, MenuPopup, MenuTrigger } from "./ui/menu";
2727import { readNativeApi } from "~/nativeApi" ;
2828import { toastManager } from "./ui/toast" ;
2929import { useCopyToClipboard } from "~/hooks/useCopyToClipboard" ;
30+ import { getLocalStorageItem , setLocalStorageItem } from "~/hooks/useLocalStorage" ;
31+ import { Schema } from "effect" ;
32+
33+ const PLAN_SIDEBAR_WIDTH_STORAGE_KEY = "plan_sidebar_width" ;
34+ const PLAN_SIDEBAR_DEFAULT_WIDTH = 340 ;
35+ const PLAN_SIDEBAR_MIN_WIDTH = 260 ;
36+ const PLAN_SIDEBAR_MAX_WIDTH = 800 ;
3037
3138function stepStatusIcon ( status : string ) : React . ReactNode {
3239 if ( status === "completed" ) {
@@ -59,6 +66,101 @@ interface PlanSidebarProps {
5966 onClose : ( ) => void ;
6067}
6168
69+ function clampWidth ( width : number ) : number {
70+ return Math . max ( PLAN_SIDEBAR_MIN_WIDTH , Math . min ( width , PLAN_SIDEBAR_MAX_WIDTH ) ) ;
71+ }
72+
73+ function useResizablePlanSidebar ( ) {
74+ const [ width , setWidth ] = useState < number > ( ( ) => {
75+ const stored = getLocalStorageItem ( PLAN_SIDEBAR_WIDTH_STORAGE_KEY , Schema . Finite ) ;
76+ return stored !== null ? clampWidth ( stored ) : PLAN_SIDEBAR_DEFAULT_WIDTH ;
77+ } ) ;
78+ const resizeRef = useRef < {
79+ startX : number ;
80+ startWidth : number ;
81+ pointerId : number ;
82+ moved : boolean ;
83+ } | null > ( null ) ;
84+ const railRef = useRef < HTMLButtonElement | null > ( null ) ;
85+
86+ const handlePointerDown = useCallback (
87+ ( event : React . PointerEvent < HTMLButtonElement > ) => {
88+ if ( event . button !== 0 ) return ;
89+ event . preventDefault ( ) ;
90+ event . stopPropagation ( ) ;
91+ resizeRef . current = {
92+ startX : event . clientX ,
93+ startWidth : width ,
94+ pointerId : event . pointerId ,
95+ moved : false ,
96+ } ;
97+ event . currentTarget . setPointerCapture ( event . pointerId ) ;
98+ document . body . style . cursor = "col-resize" ;
99+ document . body . style . userSelect = "none" ;
100+ } ,
101+ [ width ] ,
102+ ) ;
103+
104+ const handlePointerMove = useCallback ( ( event : React . PointerEvent < HTMLButtonElement > ) => {
105+ const state = resizeRef . current ;
106+ if ( ! state || state . pointerId !== event . pointerId ) return ;
107+ event . preventDefault ( ) ;
108+ // Dragging left increases width (right-side sidebar)
109+ const delta = state . startX - event . clientX ;
110+ if ( Math . abs ( delta ) > 2 ) {
111+ state . moved = true ;
112+ }
113+ const newWidth = clampWidth ( state . startWidth + delta ) ;
114+ setWidth ( newWidth ) ;
115+ } , [ ] ) ;
116+
117+ const handlePointerUp = useCallback ( ( event : React . PointerEvent < HTMLButtonElement > ) => {
118+ const state = resizeRef . current ;
119+ if ( ! state || state . pointerId !== event . pointerId ) return ;
120+ event . preventDefault ( ) ;
121+ const delta = state . startX - event . clientX ;
122+ const finalWidth = clampWidth ( state . startWidth + delta ) ;
123+ setLocalStorageItem ( PLAN_SIDEBAR_WIDTH_STORAGE_KEY , finalWidth , Schema . Finite ) ;
124+ resizeRef . current = null ;
125+ if ( event . currentTarget . hasPointerCapture ( event . pointerId ) ) {
126+ event . currentTarget . releasePointerCapture ( event . pointerId ) ;
127+ }
128+ document . body . style . removeProperty ( "cursor" ) ;
129+ document . body . style . removeProperty ( "user-select" ) ;
130+ } , [ ] ) ;
131+
132+ const handlePointerCancel = useCallback ( ( event : React . PointerEvent < HTMLButtonElement > ) => {
133+ const state = resizeRef . current ;
134+ if ( ! state || state . pointerId !== event . pointerId ) return ;
135+ resizeRef . current = null ;
136+ if ( event . currentTarget . hasPointerCapture ( event . pointerId ) ) {
137+ event . currentTarget . releasePointerCapture ( event . pointerId ) ;
138+ }
139+ document . body . style . removeProperty ( "cursor" ) ;
140+ document . body . style . removeProperty ( "user-select" ) ;
141+ } , [ ] ) ;
142+
143+ // Cleanup on unmount
144+ useEffect ( ( ) => {
145+ return ( ) => {
146+ document . body . style . removeProperty ( "cursor" ) ;
147+ document . body . style . removeProperty ( "user-select" ) ;
148+ } ;
149+ } , [ ] ) ;
150+
151+ return {
152+ width,
153+ railRef,
154+ railProps : {
155+ ref : railRef ,
156+ onPointerDown : handlePointerDown ,
157+ onPointerMove : handlePointerMove ,
158+ onPointerUp : handlePointerUp ,
159+ onPointerCancel : handlePointerCancel ,
160+ } ,
161+ } ;
162+ }
163+
62164const PlanSidebar = memo ( function PlanSidebar ( {
63165 activePlan,
64166 activeProposedPlan,
@@ -70,6 +172,7 @@ const PlanSidebar = memo(function PlanSidebar({
70172 const [ proposedPlanExpanded , setProposedPlanExpanded ] = useState ( false ) ;
71173 const [ isSavingToWorkspace , setIsSavingToWorkspace ] = useState ( false ) ;
72174 const { copyToClipboard, isCopied } = useCopyToClipboard ( ) ;
175+ const { width, railProps } = useResizablePlanSidebar ( ) ;
73176
74177 const planMarkdown = activeProposedPlan ?. planMarkdown ?? null ;
75178 const displayedPlanMarkdown = planMarkdown ? stripDisplayedPlanMarkdown ( planMarkdown ) : null ;
@@ -118,7 +221,18 @@ const PlanSidebar = memo(function PlanSidebar({
118221 } , [ planMarkdown , workspaceRoot ] ) ;
119222
120223 return (
121- < div className = "flex h-full w-[340px] shrink-0 flex-col border-l border-border/70 bg-card/50" >
224+ < div
225+ className = "relative flex h-full shrink-0 flex-col border-l border-border/70 bg-card/50"
226+ style = { { width : `${ width } px` } }
227+ >
228+ { /* Resize handle */ }
229+ < button
230+ type = "button"
231+ aria-label = "Resize plan sidebar"
232+ title = "Drag to resize"
233+ className = "absolute inset-y-0 left-0 z-20 w-1 -translate-x-1/2 cursor-col-resize touch-none select-none hover:bg-primary/20 active:bg-primary/30 transition-colors"
234+ { ...railProps }
235+ />
122236 { /* Header */ }
123237 < div className = "flex h-12 shrink-0 items-center justify-between border-b border-border/60 px-3" >
124238 < div className = "flex items-center gap-2" >
0 commit comments