@@ -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 ,
@@ -33,7 +34,7 @@ import {
3334import { projectScriptCwd , projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts" ;
3435import { truncate } from "@t3tools/shared/String" ;
3536import { Debouncer } from "@tanstack/react-pacer" ;
36- import { memo , useCallback , useEffect , useMemo , useRef , useState } from "react" ;
37+ import { memo , useCallback , useEffect , useId , useMemo , useRef , useState } from "react" ;
3738import { useNavigate , useSearch } from "@tanstack/react-router" ;
3839import { useShallow } from "zustand/react/shallow" ;
3940import { useGitStatus } from "~/lib/gitStatusState" ;
@@ -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" ;
@@ -418,6 +419,7 @@ interface PersistentThreadTerminalDrawerProps {
418419 newShortcutLabel : string | undefined ;
419420 closeShortcutLabel : string | undefined ;
420421 keybindings : ResolvedKeybindingsConfig ;
422+ terminalLayout : TerminalLayout ;
421423 onAddTerminalContext : ( selection : TerminalContextSelection ) => void ;
422424}
423425
@@ -431,6 +433,7 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
431433 newShortcutLabel,
432434 closeShortcutLabel,
433435 keybindings,
436+ terminalLayout,
434437 onAddTerminalContext,
435438} : PersistentThreadTerminalDrawerProps ) {
436439 const serverThread = useStore ( useMemo ( ( ) => createThreadSelectorByRef ( threadRef ) , [ threadRef ] ) ) ;
@@ -449,7 +452,9 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
449452 const storeNewTerminal = useTerminalStateStore ( ( state ) => state . newTerminal ) ;
450453 const storeSetActiveTerminal = useTerminalStateStore ( ( state ) => state . setActiveTerminal ) ;
451454 const storeCloseTerminal = useTerminalStateStore ( ( state ) => state . closeTerminal ) ;
455+ const storeSetTerminalOpen = useTerminalStateStore ( ( state ) => state . setTerminalOpen ) ;
452456 const [ localFocusRequestId , setLocalFocusRequestId ] = useState ( 0 ) ;
457+ const floatingTerminalTitleId = useId ( ) ;
453458 const worktreePath = serverThread ?. worktreePath ?? draftThread ?. worktreePath ?? null ;
454459 const effectiveWorktreePath = useMemo ( ( ) => {
455460 if ( launchContext !== null ) {
@@ -549,39 +554,81 @@ 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+ return (
595+ < div
596+ className = { cn (
597+ "fixed inset-0 z-50 bg-black/32 backdrop-blur-sm" ,
598+ visible ? "grid grid-rows-[1fr_auto_3fr] justify-items-center p-4" : "hidden" ,
599+ ) }
600+ onMouseDown = { ( event ) => {
601+ if ( event . target === event . currentTarget ) {
602+ closeTerminalWindow ( ) ;
603+ }
604+ } }
605+ >
606+ < div
607+ role = "dialog"
608+ aria-modal = "true"
609+ aria-labelledby = { floatingTerminalTitleId }
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+ < h2 id = { floatingTerminalTitleId } className = "text-xs font-medium leading-none" >
614+ Terminal
615+ </ h2 >
616+ < button
617+ type = "button"
618+ className = "inline-flex size-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
619+ onClick = { closeTerminalWindow }
620+ aria-label = "Close terminal window"
621+ >
622+ < XIcon className = "size-3.5" />
623+ </ button >
624+ </ div >
625+ { drawer }
626+ </ div >
627+ </ div >
628+ ) ;
629+ }
630+
631+ return < div className = { visible ? undefined : "hidden" } > { drawer } </ div > ;
585632} ) ;
586633
587634export default function ChatView ( props : ChatViewProps ) {
@@ -3263,6 +3310,7 @@ export default function ChatView(props: ChatViewProps) {
32633310 availableEditors = { availableEditors }
32643311 terminalAvailable = { activeProject !== undefined }
32653312 terminalOpen = { terminalState . terminalOpen }
3313+ terminalLayout = { settings . terminalLayout }
32663314 terminalToggleShortcutLabel = { terminalToggleShortcutLabel }
32673315 diffToggleShortcutLabel = { diffPanelShortcutLabel }
32683316 gitCwd = { gitCwd }
@@ -3477,6 +3525,7 @@ export default function ChatView(props: ChatViewProps) {
34773525 newShortcutLabel = { newTerminalShortcutLabel ?? undefined }
34783526 closeShortcutLabel = { closeTerminalShortcutLabel ?? undefined }
34793527 keybindings = { keybindings }
3528+ terminalLayout = { settings . terminalLayout }
34803529 onAddTerminalContext = { addTerminalContextToDraft }
34813530 />
34823531 ) ) }
0 commit comments