From cd3b12c579ffc668c28f30f1acbbb17ec3ab7033 Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 28 Aug 2026 13:47:50 -0400 Subject: [PATCH] feat(tui): resize terminal panes (#45979) --- packages/tui/src/component/session-frame.tsx | 59 +++++++++++++++++--- packages/tui/src/component/terminal-pane.tsx | 5 ++ packages/tui/src/ui/layout.ts | 6 ++ packages/tui/src/ui/pane-resize-handle.tsx | 7 ++- 4 files changed, 67 insertions(+), 10 deletions(-) diff --git a/packages/tui/src/component/session-frame.tsx b/packages/tui/src/component/session-frame.tsx index 12391d8f81a..d62275ee60a 100644 --- a/packages/tui/src/component/session-frame.tsx +++ b/packages/tui/src/component/session-frame.tsx @@ -1,4 +1,4 @@ -import { RGBA } from "@opentui/core" +import { RGBA, type MouseEvent } from "@opentui/core" import { useRenderer, useTerminalDimensions } from "@opentui/solid" import { batch, createEffect, createMemo, createResource, createSignal, on, Show } from "solid-js" import { useConfig } from "../config" @@ -6,9 +6,12 @@ import { useData } from "../context/data" import { Keymap } from "../context/keymap" import { useSessionTerminals } from "../context/session-terminals" import { usePromptRef } from "../context/prompt" +import { useStorage } from "../context/storage" import { Session } from "../routes/session" import { Sidebar } from "../routes/session/sidebar" -import { SESSION_SIDEBAR_WIDTH } from "../ui/layout" +import { clampTerminalPaneWidth, SESSION_SIDEBAR_WIDTH } from "../ui/layout" +import { createPaneResize } from "../ui/pane-resize" +import { PaneResizeHandle } from "../ui/pane-resize-handle" import { useToast } from "../ui/toast" import { TerminalPane } from "./terminal-pane" @@ -20,6 +23,32 @@ export function SessionFrame(props: { sessionID: string; verticalTabsWidth: numb const toast = useToast() const renderer = useRenderer() const dimensions = useTerminalDimensions() + const availableWidth = () => Math.max(0, dimensions().width - props.verticalTabsWidth) + const defaultTerminalWidth = () => Math.max(1, Math.floor(dimensions().width / 2)) + const [layout, updateLayout] = useStorage().store<{ terminalWidth?: number }>("layout", { initial: {} }) + const terminalResize = createPaneResize({ + value: () => layout.terminalWidth ?? defaultTerminalWidth(), + defaultValue: defaultTerminalWidth, + clamp: (width) => clampTerminalPaneWidth(width, availableWidth()), + fromMouse: (event) => dimensions().width - event.x - 1, + contains: (event, width) => event.x >= dimensions().width - width - 1 && event.x <= dimensions().width - width, + onCommit: (width) => { + void updateLayout((draft) => { + draft.terminalWidth = width + }).catch((error) => console.error("Failed to persist TUI layout", error)) + }, + }) + let resizeRelease = false + const finishTerminalResize = (event: MouseEvent) => { + if (terminalResize.resizing()) { + // A captured drag-end can be followed by mouse-up on the focus overlay. + resizeRelease = true + queueMicrotask(() => { + resizeRelease = false + }) + } + terminalResize.onMouseUp(event) + } const [sidebarOpen, setSidebarOpen] = createSignal(false) const [sessionWidth, setSessionWidth] = createSignal() const [terminalFocused, setTerminalFocused] = createSignal(false) @@ -96,7 +125,16 @@ export function SessionFrame(props: { sessionID: string; verticalTabsWidth: numb })) return ( - + { + if (terminalResize.resizing() || resizeRelease) return + focusSession() + }} /> @@ -146,6 +183,7 @@ export function SessionFrame(props: { sessionID: string; verticalTabsWidth: numb {(ptyID) => ( { sessions.clearFocus(ptyID) @@ -163,6 +201,9 @@ export function SessionFrame(props: { sessionID: string; verticalTabsWidth: numb + = 3}> + + void onFocusRequest?: (focus: (() => void) | undefined) => void @@ -77,6 +78,10 @@ export function TerminalPane(props: { send(interactionFrame(size)) } + createEffect(() => { + if (props.resizing) interact() + }) + const sendInput = (data: Uint8Array) => { if (!restored) { pendingInput.push(data) diff --git a/packages/tui/src/ui/layout.ts b/packages/tui/src/ui/layout.ts index e1dad76fe7f..85aa92ca5e8 100644 --- a/packages/tui/src/ui/layout.ts +++ b/packages/tui/src/ui/layout.ts @@ -13,3 +13,9 @@ export function clampSessionTabsWidth(width: number, total: number) { Math.min(width, SESSION_SIDEBAR_MAX_WIDTH, total - SESSION_CONTENT_MIN_WIDTH), ) } + +export function clampTerminalPaneWidth(width: number, total: number) { + const half = Math.max(1, Math.floor(total / 2)) + // Preserve the equal split when there is not enough room for both pane minima. + return Math.max(Math.min(24, half), Math.min(width, Math.max(half, total - SESSION_CONTENT_MIN_WIDTH))) +} diff --git a/packages/tui/src/ui/pane-resize-handle.tsx b/packages/tui/src/ui/pane-resize-handle.tsx index 180ac92f226..edd1e04b75e 100644 --- a/packages/tui/src/ui/pane-resize-handle.tsx +++ b/packages/tui/src/ui/pane-resize-handle.tsx @@ -1,7 +1,11 @@ import { useTheme } from "../context/theme" import type { createPaneResize } from "./pane-resize" -export function PaneResizeHandle(props: { resize: ReturnType; left: number }) { +export function PaneResizeHandle(props: { + resize: ReturnType + left: number + highlight?: "left" | "right" +}) { const theme = useTheme("elevated") return ( @@ -19,6 +23,7 @@ export function PaneResizeHandle(props: { resize: ReturnType