From 9793b4f2e44569993bda11a8c5be716afe474859 Mon Sep 17 00:00:00 2001 From: adamt94 Date: Sat, 25 Apr 2026 14:35:44 +0100 Subject: [PATCH 1/2] Add floating terminal layout support - add terminal layout setting and contract schema - render the thread terminal as a floating dialog when enabled - switch package scripts to run under Bun --- apps/web/src/components/ChatView.tsx | 109 +++++++++++++----- .../src/components/ThreadTerminalDrawer.tsx | 9 +- apps/web/src/components/chat/ChatHeader.tsx | 11 +- .../components/settings/SettingsPanels.tsx | 63 ++++++++++ apps/web/src/localApi.test.ts | 2 + packages/contracts/src/settings.ts | 8 ++ 6 files changed, 168 insertions(+), 34 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index d66d2487ce3..59c6fc75a1c 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -12,6 +12,7 @@ import { type ServerProvider, type ResolvedKeybindingsConfig, type ScopedThreadRef, + type TerminalLayout, type ThreadId, type TurnId, type KeybindingCommand, @@ -35,7 +36,7 @@ import { import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts"; import { truncate } from "@t3tools/shared/String"; import { Debouncer } from "@tanstack/react-pacer"; -import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; import { useNavigate, useSearch } from "@tanstack/react-router"; import { useShallow } from "zustand/react/shallow"; import { useGitStatus } from "~/lib/gitStatusState"; @@ -104,7 +105,7 @@ import { BranchToolbar } from "./BranchToolbar"; import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; import PlanSidebar from "./PlanSidebar"; import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; -import { ChevronDownIcon, TriangleAlertIcon, WifiOffIcon } from "lucide-react"; +import { ChevronDownIcon, TriangleAlertIcon, WifiOffIcon, XIcon } from "lucide-react"; import { cn, randomUUID } from "~/lib/utils"; import { stackedThreadToast, toastManager } from "./ui/toast"; import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings"; @@ -438,6 +439,7 @@ interface PersistentThreadTerminalDrawerProps { newShortcutLabel: string | undefined; closeShortcutLabel: string | undefined; keybindings: ResolvedKeybindingsConfig; + terminalLayout: TerminalLayout; onAddTerminalContext: (selection: TerminalContextSelection) => void; } @@ -451,6 +453,7 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra newShortcutLabel, closeShortcutLabel, keybindings, + terminalLayout, onAddTerminalContext, }: PersistentThreadTerminalDrawerProps) { const serverThread = useStore(useMemo(() => createThreadSelectorByRef(threadRef), [threadRef])); @@ -469,7 +472,9 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra const storeNewTerminal = useTerminalStateStore((state) => state.newTerminal); const storeSetActiveTerminal = useTerminalStateStore((state) => state.setActiveTerminal); const storeCloseTerminal = useTerminalStateStore((state) => state.closeTerminal); + const storeSetTerminalOpen = useTerminalStateStore((state) => state.setTerminalOpen); const [localFocusRequestId, setLocalFocusRequestId] = useState(0); + const floatingTerminalTitleId = useId(); const worktreePath = serverThread?.worktreePath ?? draftThread?.worktreePath ?? null; const effectiveWorktreePath = useMemo(() => { if (launchContext !== null) { @@ -569,39 +574,81 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra }, [onAddTerminalContext, visible], ); + const closeTerminalWindow = useCallback(() => { + storeSetTerminalOpen(threadRef, false); + }, [storeSetTerminalOpen, threadRef]); if (!project || !terminalState.terminalOpen || !cwd) { return null; } - return ( -
- -
- ); + const drawer = ( + + ); + + if (terminalLayout === "floating") { + return ( +
{ + if (event.target === event.currentTarget) { + closeTerminalWindow(); + } + }} + > +
+
+

+ Terminal +

+ +
+ {drawer} +
+
+ ); + } + + return
{drawer}
; }); export default function ChatView(props: ChatViewProps) { @@ -3527,6 +3574,7 @@ export default function ChatView(props: ChatViewProps) { availableEditors={availableEditors} terminalAvailable={activeProject !== undefined} terminalOpen={terminalState.terminalOpen} + terminalLayout={settings.terminalLayout} terminalToggleShortcutLabel={terminalToggleShortcutLabel} diffToggleShortcutLabel={diffPanelShortcutLabel} gitCwd={gitCwd} @@ -3753,6 +3801,7 @@ export default function ChatView(props: ChatViewProps) { newShortcutLabel={newTerminalShortcutLabel ?? undefined} closeShortcutLabel={closeTerminalShortcutLabel ?? undefined} keybindings={keybindings} + terminalLayout={settings.terminalLayout} onAddTerminalContext={addTerminalContextToDraft} /> ))} diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index 6c71e5eb334..2167a47528f 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -3,6 +3,7 @@ import { Plus, SquareSplitHorizontal, TerminalSquare, Trash2, XIcon } from "luci import { type ResolvedKeybindingsConfig, type ScopedThreadRef, + type TerminalLayout, type TerminalEvent, type TerminalSessionSnapshot, type ThreadId, @@ -20,6 +21,7 @@ import { } from "react"; import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover"; import { type TerminalContextSelection } from "~/lib/terminalContext"; +import { cn } from "~/lib/utils"; import { openInPreferredEditor } from "../editorPreferences"; import { collectWrappedTerminalLinkLine, @@ -821,6 +823,7 @@ interface ThreadTerminalDrawerProps { onHeightChange: (height: number) => void; onAddTerminalContext: (selection: TerminalContextSelection) => void; keybindings: ResolvedKeybindingsConfig; + layout?: TerminalLayout; } interface TerminalActionButtonProps { @@ -875,6 +878,7 @@ export default function ThreadTerminalDrawer({ onHeightChange, onAddTerminalContext, keybindings, + layout = "docked", }: ThreadTerminalDrawerProps) { const [drawerHeight, setDrawerHeight] = useState(() => clampDrawerHeight(height)); const [resizeEpoch, setResizeEpoch] = useState(0); @@ -1111,7 +1115,10 @@ export default function ThreadTerminalDrawer({ return (