@@ -11,6 +11,7 @@ import {
1111 type ServerProvider ,
1212 type ResolvedKeybindingsConfig ,
1313 type ScopedThreadRef ,
14+ type TerminalLayout ,
1415 type ThreadId ,
1516 type TurnId ,
1617 type KeybindingCommand ,
@@ -102,7 +103,7 @@ import { BranchToolbar } from "./BranchToolbar";
102103import { resolveShortcutCommand , shortcutLabelForCommand } from "../keybindings" ;
103104import PlanSidebar from "./PlanSidebar" ;
104105import ThreadTerminalDrawer from "./ThreadTerminalDrawer" ;
105- import { ChevronDownIcon } from "lucide-react" ;
106+ import { ChevronDownIcon , XIcon } from "lucide-react" ;
106107import { cn , randomUUID } from "~/lib/utils" ;
107108import { stackedThreadToast , toastManager } from "./ui/toast" ;
108109import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings" ;
@@ -178,6 +179,7 @@ import {
178179import { sanitizeThreadErrorMessage } from "~/rpc/transportError" ;
179180import { retainThreadDetailSubscription } from "../environments/runtime/service" ;
180181import { RightPanelSheet } from "./RightPanelSheet" ;
182+ import { Dialog , DialogPopup , DialogTitle } from "./ui/dialog" ;
181183
182184const IMAGE_ONLY_BOOTSTRAP_PROMPT =
183185 "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]" ;
@@ -418,6 +420,7 @@ interface PersistentThreadTerminalDrawerProps {
418420 newShortcutLabel : string | undefined ;
419421 closeShortcutLabel : string | undefined ;
420422 keybindings : ResolvedKeybindingsConfig ;
423+ terminalLayout : TerminalLayout ;
421424 onAddTerminalContext : ( selection : TerminalContextSelection ) => void ;
422425}
423426
@@ -431,6 +434,7 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
431434 newShortcutLabel,
432435 closeShortcutLabel,
433436 keybindings,
437+ terminalLayout,
434438 onAddTerminalContext,
435439} : PersistentThreadTerminalDrawerProps ) {
436440 const serverThread = useStore ( useMemo ( ( ) => createThreadSelectorByRef ( threadRef ) , [ threadRef ] ) ) ;
@@ -449,6 +453,7 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
449453 const storeNewTerminal = useTerminalStateStore ( ( state ) => state . newTerminal ) ;
450454 const storeSetActiveTerminal = useTerminalStateStore ( ( state ) => state . setActiveTerminal ) ;
451455 const storeCloseTerminal = useTerminalStateStore ( ( state ) => state . closeTerminal ) ;
456+ const storeSetTerminalOpen = useTerminalStateStore ( ( state ) => state . setTerminalOpen ) ;
452457 const [ localFocusRequestId , setLocalFocusRequestId ] = useState ( 0 ) ;
453458 const worktreePath = serverThread ?. worktreePath ?? draftThread ?. worktreePath ?? null ;
454459 const effectiveWorktreePath = useMemo ( ( ) => {
@@ -549,39 +554,79 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
549554 } ,
550555 [ onAddTerminalContext , visible ] ,
551556 ) ;
557+ const closeTerminalWindow = useCallback ( ( ) => {
558+ storeSetTerminalOpen ( threadRef , false ) ;
559+ } , [ storeSetTerminalOpen , threadRef ] ) ;
552560
553561 if ( ! project || ! terminalState . terminalOpen || ! cwd ) {
554562 return null ;
555563 }
556564
557- return (
558- < div className = { visible ? undefined : "hidden" } >
559- < ThreadTerminalDrawer
560- threadRef = { threadRef }
561- threadId = { threadId }
562- cwd = { cwd }
563- worktreePath = { effectiveWorktreePath }
564- runtimeEnv = { runtimeEnv }
565- visible = { visible }
566- height = { terminalState . terminalHeight }
567- terminalIds = { terminalState . terminalIds }
568- activeTerminalId = { terminalState . activeTerminalId }
569- terminalGroups = { terminalState . terminalGroups }
570- activeTerminalGroupId = { terminalState . activeTerminalGroupId }
571- focusRequestId = { focusRequestId + localFocusRequestId + ( visible ? 1 : 0 ) }
572- onSplitTerminal = { splitTerminal }
573- onNewTerminal = { createNewTerminal }
574- splitShortcutLabel = { visible ? splitShortcutLabel : undefined }
575- newShortcutLabel = { visible ? newShortcutLabel : undefined }
576- closeShortcutLabel = { visible ? closeShortcutLabel : undefined }
577- keybindings = { keybindings }
578- onActiveTerminalChange = { activateTerminal }
579- onCloseTerminal = { closeTerminal }
580- onHeightChange = { setTerminalHeight }
581- onAddTerminalContext = { handleAddTerminalContext }
582- />
583- </ div >
584- ) ;
565+ const drawer = (
566+ < ThreadTerminalDrawer
567+ threadRef = { threadRef }
568+ threadId = { threadId }
569+ cwd = { cwd }
570+ worktreePath = { effectiveWorktreePath }
571+ runtimeEnv = { runtimeEnv }
572+ visible = { visible }
573+ height = { terminalState . terminalHeight }
574+ terminalIds = { terminalState . terminalIds }
575+ activeTerminalId = { terminalState . activeTerminalId }
576+ terminalGroups = { terminalState . terminalGroups }
577+ activeTerminalGroupId = { terminalState . activeTerminalGroupId }
578+ focusRequestId = { focusRequestId + localFocusRequestId + ( visible ? 1 : 0 ) }
579+ onSplitTerminal = { splitTerminal }
580+ onNewTerminal = { createNewTerminal }
581+ splitShortcutLabel = { visible ? splitShortcutLabel : undefined }
582+ newShortcutLabel = { visible ? newShortcutLabel : undefined }
583+ closeShortcutLabel = { visible ? closeShortcutLabel : undefined }
584+ keybindings = { keybindings }
585+ onActiveTerminalChange = { activateTerminal }
586+ onCloseTerminal = { closeTerminal }
587+ onHeightChange = { setTerminalHeight }
588+ onAddTerminalContext = { handleAddTerminalContext }
589+ layout = { terminalLayout }
590+ />
591+ ) ;
592+
593+ if ( terminalLayout === "floating" ) {
594+ if ( ! visible ) {
595+ return < div className = "hidden" > { drawer } </ div > ;
596+ }
597+
598+ return (
599+ < Dialog
600+ open
601+ onOpenChange = { ( open , eventDetails ) => {
602+ if ( ! open && eventDetails . reason !== "escape-key" ) {
603+ closeTerminalWindow ( ) ;
604+ }
605+ } }
606+ >
607+ < DialogPopup
608+ showCloseButton = { false }
609+ bottomStickOnMobile = { false }
610+ className = "w-[min(96vw,72rem)] max-w-[min(96vw,72rem)] overflow-hidden rounded-lg border bg-background p-0 shadow-xl"
611+ >
612+ < div className = "flex h-8 shrink-0 items-center justify-between border-b border-border/80 px-2" >
613+ < DialogTitle className = "text-xs font-medium leading-none" > Terminal</ DialogTitle >
614+ < button
615+ type = "button"
616+ className = "inline-flex size-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
617+ onClick = { closeTerminalWindow }
618+ aria-label = "Close terminal window"
619+ >
620+ < XIcon className = "size-3.5" />
621+ </ button >
622+ </ div >
623+ { drawer }
624+ </ DialogPopup >
625+ </ Dialog >
626+ ) ;
627+ }
628+
629+ return < div className = { visible ? undefined : "hidden" } > { drawer } </ div > ;
585630} ) ;
586631
587632export default function ChatView ( props : ChatViewProps ) {
@@ -3263,6 +3308,7 @@ export default function ChatView(props: ChatViewProps) {
32633308 availableEditors = { availableEditors }
32643309 terminalAvailable = { activeProject !== undefined }
32653310 terminalOpen = { terminalState . terminalOpen }
3311+ terminalLayout = { settings . terminalLayout }
32663312 terminalToggleShortcutLabel = { terminalToggleShortcutLabel }
32673313 diffToggleShortcutLabel = { diffPanelShortcutLabel }
32683314 gitCwd = { gitCwd }
@@ -3477,6 +3523,7 @@ export default function ChatView(props: ChatViewProps) {
34773523 newShortcutLabel = { newTerminalShortcutLabel ?? undefined }
34783524 closeShortcutLabel = { closeTerminalShortcutLabel ?? undefined }
34793525 keybindings = { keybindings }
3526+ terminalLayout = { settings . terminalLayout }
34803527 onAddTerminalContext = { addTerminalContextToDraft }
34813528 />
34823529 ) ) }
0 commit comments