mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 03:56:55 +03:00
feat(tui): resize terminal panes (#45979)
This commit is contained in:
committed by
GitHub
Unverified
parent
0d6232ffef
commit
cd3b12c579
@@ -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<number>()
|
||||
const [terminalFocused, setTerminalFocused] = createSignal(false)
|
||||
@@ -96,7 +125,16 @@ export function SessionFrame(props: { sessionID: string; verticalTabsWidth: numb
|
||||
}))
|
||||
|
||||
return (
|
||||
<box flexGrow={1} minWidth={0} minHeight={0} flexDirection="row" position="relative">
|
||||
<box
|
||||
flexGrow={1}
|
||||
minWidth={0}
|
||||
minHeight={0}
|
||||
flexDirection="row"
|
||||
position="relative"
|
||||
onMouseDrag={terminalResize.onMouseDrag}
|
||||
onMouseDragEnd={finishTerminalResize}
|
||||
onMouseUp={finishTerminalResize}
|
||||
>
|
||||
<box
|
||||
flexGrow={1}
|
||||
flexBasis={0}
|
||||
@@ -124,18 +162,17 @@ export function SessionFrame(props: { sessionID: string; verticalTabsWidth: numb
|
||||
height="100%"
|
||||
zIndex={1}
|
||||
// Consume the release before revealing permission buttons underneath.
|
||||
onMouseUp={focusSession}
|
||||
onMouseUp={() => {
|
||||
if (terminalResize.resizing() || resizeRelease) return
|
||||
focusSession()
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
</box>
|
||||
<Show when={rightPane() === "terminal" || (rightPane() === "sidebar" && wide())}>
|
||||
<box
|
||||
flexShrink={0}
|
||||
width={
|
||||
rightPane() === "terminal"
|
||||
? Math.max(1, Math.floor((dimensions().width - props.verticalTabsWidth) / 2))
|
||||
: SESSION_SIDEBAR_WIDTH
|
||||
}
|
||||
width={rightPane() === "terminal" ? terminalResize.size() : SESSION_SIDEBAR_WIDTH}
|
||||
minWidth={0}
|
||||
minHeight={0}
|
||||
>
|
||||
@@ -146,6 +183,7 @@ export function SessionFrame(props: { sessionID: string; verticalTabsWidth: numb
|
||||
{(ptyID) => (
|
||||
<TerminalPane
|
||||
ptyID={ptyID}
|
||||
resizing={terminalResize.resizing()}
|
||||
autoFocus={restoreTerminalFocus() || sessions.shouldFocus(ptyID)}
|
||||
onAutoFocus={() => {
|
||||
sessions.clearFocus(ptyID)
|
||||
@@ -163,6 +201,9 @@ export function SessionFrame(props: { sessionID: string; verticalTabsWidth: numb
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={rightPane() === "terminal" && availableWidth() >= 3}>
|
||||
<PaneResizeHandle resize={terminalResize} left={availableWidth() - terminalResize.size() - 1} highlight="right" />
|
||||
</Show>
|
||||
<Show when={rightPane() === "sidebar" && !wide()}>
|
||||
<box
|
||||
position="absolute"
|
||||
|
||||
@@ -23,6 +23,7 @@ type StreamItem =
|
||||
|
||||
export function TerminalPane(props: {
|
||||
ptyID: string
|
||||
resizing?: boolean
|
||||
autoFocus?: boolean
|
||||
onAutoFocus?: () => 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)
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useTheme } from "../context/theme"
|
||||
import type { createPaneResize } from "./pane-resize"
|
||||
|
||||
export function PaneResizeHandle(props: { resize: ReturnType<typeof createPaneResize>; left: number }) {
|
||||
export function PaneResizeHandle(props: {
|
||||
resize: ReturnType<typeof createPaneResize>
|
||||
left: number
|
||||
highlight?: "left" | "right"
|
||||
}) {
|
||||
const theme = useTheme("elevated")
|
||||
|
||||
return (
|
||||
@@ -19,6 +23,7 @@ export function PaneResizeHandle(props: { resize: ReturnType<typeof createPaneRe
|
||||
<box
|
||||
width={1}
|
||||
height="100%"
|
||||
marginLeft={props.highlight === "right" ? 1 : 0}
|
||||
backgroundColor={
|
||||
props.resize.hovered() || props.resize.resizing() ? theme.background.action.primary.hovered : undefined
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user