@@ -89,6 +89,7 @@ import {
8989 DEFAULT_INTERACTION_MODE ,
9090 DEFAULT_RUNTIME_MODE ,
9191 DEFAULT_THREAD_TERMINAL_ID ,
92+ DEFAULT_THREAD_TERMINAL_WIDTH ,
9293 MAX_TERMINALS_PER_GROUP ,
9394 type ChatMessage ,
9495 type SessionPhase ,
@@ -429,6 +430,17 @@ function useLocalDispatchState(input: {
429430 } ;
430431}
431432
433+ const MIN_FLOATING_TERMINAL_WIDTH = 400 ;
434+ const MAX_FLOATING_TERMINAL_WIDTH_RATIO = 0.97 ;
435+
436+ function clampFloatingTerminalWidth ( w : number ) : number {
437+ if ( typeof window === "undefined" ) return DEFAULT_THREAD_TERMINAL_WIDTH ;
438+ return Math . min (
439+ Math . max ( Math . round ( w ) , MIN_FLOATING_TERMINAL_WIDTH ) ,
440+ Math . floor ( window . innerWidth * MAX_FLOATING_TERMINAL_WIDTH_RATIO ) ,
441+ ) ;
442+ }
443+
432444interface PersistentThreadTerminalDrawerProps {
433445 threadRef : { environmentId : EnvironmentId ; threadId : ThreadId } ;
434446 threadId : ThreadId ;
@@ -468,13 +480,27 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
468480 selectThreadTerminalState ( state . terminalStateByThreadKey , threadRef ) ,
469481 ) ;
470482 const storeSetTerminalHeight = useTerminalStateStore ( ( state ) => state . setTerminalHeight ) ;
483+ const storeSetTerminalWidth = useTerminalStateStore ( ( state ) => state . setTerminalWidth ) ;
471484 const storeSplitTerminal = useTerminalStateStore ( ( state ) => state . splitTerminal ) ;
472485 const storeNewTerminal = useTerminalStateStore ( ( state ) => state . newTerminal ) ;
473486 const storeSetActiveTerminal = useTerminalStateStore ( ( state ) => state . setActiveTerminal ) ;
474487 const storeCloseTerminal = useTerminalStateStore ( ( state ) => state . closeTerminal ) ;
475488 const storeSetTerminalOpen = useTerminalStateStore ( ( state ) => state . setTerminalOpen ) ;
476489 const [ localFocusRequestId , setLocalFocusRequestId ] = useState ( 0 ) ;
477490 const floatingTerminalTitleId = useId ( ) ;
491+
492+ const [ floatingWidth , setFloatingWidth ] = useState ( ( ) =>
493+ clampFloatingTerminalWidth ( terminalState . terminalWidth ) ,
494+ ) ;
495+ const floatingWidthRef = useRef ( floatingWidth ) ;
496+ const widthResizeStateRef = useRef < {
497+ pointerId : number ;
498+ side : "left" | "right" ;
499+ startX : number ;
500+ startWidth : number ;
501+ } | null > ( null ) ;
502+ const didWidthResizeDuringDragRef = useRef ( false ) ;
503+
478504 const worktreePath = serverThread ?. worktreePath ?? draftThread ?. worktreePath ?? null ;
479505 const effectiveWorktreePath = useMemo ( ( ) => {
480506 if ( launchContext !== null ) {
@@ -518,6 +544,87 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
518544 [ storeSetTerminalHeight , threadRef ] ,
519545 ) ;
520546
547+ const setTerminalWidth = useCallback (
548+ ( width : number ) => {
549+ storeSetTerminalWidth ( threadRef , width ) ;
550+ } ,
551+ [ storeSetTerminalWidth , threadRef ] ,
552+ ) ;
553+
554+ useEffect ( ( ) => {
555+ floatingWidthRef . current = floatingWidth ;
556+ } , [ floatingWidth ] ) ;
557+
558+ useEffect ( ( ) => {
559+ if ( widthResizeStateRef . current ) return ;
560+ const clamped = clampFloatingTerminalWidth ( terminalState . terminalWidth ) ;
561+ floatingWidthRef . current = clamped ;
562+ setFloatingWidth ( clamped ) ;
563+ } , [ terminalState . terminalWidth , threadId ] ) ;
564+
565+ const handleWidthResizePointerDownLeft = useCallback (
566+ ( event : React . PointerEvent < HTMLDivElement > ) => {
567+ if ( event . button !== 0 ) return ;
568+ event . preventDefault ( ) ;
569+ event . currentTarget . setPointerCapture ( event . pointerId ) ;
570+ didWidthResizeDuringDragRef . current = false ;
571+ widthResizeStateRef . current = {
572+ pointerId : event . pointerId ,
573+ side : "left" ,
574+ startX : event . clientX ,
575+ startWidth : floatingWidthRef . current ,
576+ } ;
577+ } ,
578+ [ ] ,
579+ ) ;
580+
581+ const handleWidthResizePointerDownRight = useCallback (
582+ ( event : React . PointerEvent < HTMLDivElement > ) => {
583+ if ( event . button !== 0 ) return ;
584+ event . preventDefault ( ) ;
585+ event . currentTarget . setPointerCapture ( event . pointerId ) ;
586+ didWidthResizeDuringDragRef . current = false ;
587+ widthResizeStateRef . current = {
588+ pointerId : event . pointerId ,
589+ side : "right" ,
590+ startX : event . clientX ,
591+ startWidth : floatingWidthRef . current ,
592+ } ;
593+ } ,
594+ [ ] ,
595+ ) ;
596+
597+ const handleWidthResizePointerMove = useCallback (
598+ ( event : React . PointerEvent < HTMLDivElement > ) => {
599+ const state = widthResizeStateRef . current ;
600+ if ( ! state || state . pointerId !== event . pointerId ) return ;
601+ event . preventDefault ( ) ;
602+ const delta = event . clientX - state . startX ;
603+ const rawWidth =
604+ state . side === "right" ? state . startWidth + delta : state . startWidth - delta ;
605+ const clamped = clampFloatingTerminalWidth ( rawWidth ) ;
606+ if ( clamped === floatingWidthRef . current ) return ;
607+ didWidthResizeDuringDragRef . current = true ;
608+ floatingWidthRef . current = clamped ;
609+ setFloatingWidth ( clamped ) ;
610+ } ,
611+ [ ] ,
612+ ) ;
613+
614+ const handleWidthResizePointerEnd = useCallback (
615+ ( event : React . PointerEvent < HTMLDivElement > ) => {
616+ const state = widthResizeStateRef . current ;
617+ if ( ! state || state . pointerId !== event . pointerId ) return ;
618+ widthResizeStateRef . current = null ;
619+ if ( event . currentTarget . hasPointerCapture ( event . pointerId ) ) {
620+ event . currentTarget . releasePointerCapture ( event . pointerId ) ;
621+ }
622+ if ( ! didWidthResizeDuringDragRef . current ) return ;
623+ setTerminalWidth ( floatingWidthRef . current ) ;
624+ } ,
625+ [ setTerminalWidth ] ,
626+ ) ;
627+
521628 const splitTerminal = useCallback ( ( ) => {
522629 storeSplitTerminal ( threadRef , `terminal-${ randomUUID ( ) } ` ) ;
523630 bumpFocusRequestId ( ) ;
@@ -615,7 +722,7 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
615722 < div
616723 className = { cn (
617724 "fixed inset-0 z-50 bg-black/32 backdrop-blur-sm" ,
618- visible ? "grid grid-rows-[1fr_auto_3fr] justify-items- center p-4 " : "hidden" ,
725+ visible ? "flex items-center justify-center p-3 " : "hidden" ,
619726 ) }
620727 onMouseDown = { ( event ) => {
621728 if ( event . target === event . currentTarget ) {
@@ -627,8 +734,25 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
627734 role = "dialog"
628735 aria-modal = "true"
629736 aria-labelledby = { floatingTerminalTitleId }
630- className = "row-start-2 w-[min(96vw,72rem)] max-w-[min(96vw,72rem)] overflow-hidden rounded-lg border bg-background p-0 shadow-xl"
737+ className = "relative overflow-hidden rounded-lg border bg-background p-0 shadow-xl"
738+ style = { { width : `${ floatingWidth } px` } }
631739 >
740+ { /* Left resize handle */ }
741+ < div
742+ className = "absolute inset-y-0 left-0 z-20 w-1.5 cursor-col-resize"
743+ onPointerDown = { handleWidthResizePointerDownLeft }
744+ onPointerMove = { handleWidthResizePointerMove }
745+ onPointerUp = { handleWidthResizePointerEnd }
746+ onPointerCancel = { handleWidthResizePointerEnd }
747+ />
748+ { /* Right resize handle */ }
749+ < div
750+ className = "absolute inset-y-0 right-0 z-20 w-1.5 cursor-col-resize"
751+ onPointerDown = { handleWidthResizePointerDownRight }
752+ onPointerMove = { handleWidthResizePointerMove }
753+ onPointerUp = { handleWidthResizePointerEnd }
754+ onPointerCancel = { handleWidthResizePointerEnd }
755+ />
632756 < div className = "flex h-8 shrink-0 items-center justify-between border-b border-border/80 px-2" >
633757 < h2 id = { floatingTerminalTitleId } className = "text-xs font-medium leading-none" >
634758 Terminal
0 commit comments