diff options
| author | Adam <[email protected]> | 2026-01-16 14:03:13 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-16 14:03:13 -0600 |
| commit | 71306cbd1f0b59f450fafaa6beccbcc161002692 (patch) | |
| tree | 0f520b651d7549a35800aeb81857f01bccd76b2c /packages/app/src/components | |
| parent | 086603494691fa832239d79bf844880f87f1299c (diff) | |
| download | opencode-71306cbd1f0b59f450fafaa6beccbcc161002692.tar.gz opencode-71306cbd1f0b59f450fafaa6beccbcc161002692.zip | |
Revert "feat(desktop): Terminal Splits (#8767)"
This reverts commit 88fd6a294b3ad88d50cb8e1853589ee4e68dc74e.
Diffstat (limited to 'packages/app/src/components')
| -rw-r--r-- | packages/app/src/components/session/session-sortable-terminal-tab.tsx | 4 | ||||
| -rw-r--r-- | packages/app/src/components/terminal-split.tsx | 322 | ||||
| -rw-r--r-- | packages/app/src/components/terminal.tsx | 21 |
3 files changed, 5 insertions, 342 deletions
diff --git a/packages/app/src/components/session/session-sortable-terminal-tab.tsx b/packages/app/src/components/session/session-sortable-terminal-tab.tsx index 654d1f9ab..d20f587f4 100644 --- a/packages/app/src/components/session/session-sortable-terminal-tab.tsx +++ b/packages/app/src/components/session/session-sortable-terminal-tab.tsx @@ -14,8 +14,8 @@ export function SortableTerminalTab(props: { terminal: LocalPTY }): JSX.Element <Tabs.Trigger value={props.terminal.id} closeButton={ - terminal.tabs().length > 1 && ( - <IconButton icon="close" variant="ghost" onClick={() => terminal.closeTab(props.terminal.tabId)} /> + terminal.all().length > 1 && ( + <IconButton icon="close" variant="ghost" onClick={() => terminal.close(props.terminal.id)} /> ) } > diff --git a/packages/app/src/components/terminal-split.tsx b/packages/app/src/components/terminal-split.tsx deleted file mode 100644 index 9a05ff22c..000000000 --- a/packages/app/src/components/terminal-split.tsx +++ /dev/null @@ -1,322 +0,0 @@ -import { For, Show, createMemo, createSignal, onCleanup } from "solid-js" -import { Terminal } from "./terminal" -import { useTerminal, type Panel } from "@/context/terminal" -import { IconButton } from "@opencode-ai/ui/icon-button" - -export interface TerminalSplitProps { - tabId: string -} - -function computeLayout( - panels: Record<string, Panel>, - panelId: string, - bounds: { top: number; left: number; width: number; height: number }, -): Map<string, { top: number; left: number; width: number; height: number }> { - const result = new Map<string, { top: number; left: number; width: number; height: number }>() - const panel = panels[panelId] - if (!panel) return result - - if (panel.ptyId) { - result.set(panel.ptyId, bounds) - } else if (panel.children && panel.children.length === 2) { - const [leftId, rightId] = panel.children - const sizes = panel.sizes ?? [50, 50] - - if (panel.direction === "horizontal") { - const topHeight = (bounds.height * sizes[0]) / 100 - const topBounds = { ...bounds, height: topHeight } - const bottomBounds = { ...bounds, top: bounds.top + topHeight, height: bounds.height - topHeight } - for (const [k, v] of computeLayout(panels, leftId, topBounds)) result.set(k, v) - for (const [k, v] of computeLayout(panels, rightId, bottomBounds)) result.set(k, v) - } else { - const leftWidth = (bounds.width * sizes[0]) / 100 - const leftBounds = { ...bounds, width: leftWidth } - const rightBounds = { ...bounds, left: bounds.left + leftWidth, width: bounds.width - leftWidth } - for (const [k, v] of computeLayout(panels, leftId, leftBounds)) result.set(k, v) - for (const [k, v] of computeLayout(panels, rightId, rightBounds)) result.set(k, v) - } - } - - return result -} - -function findPanelForPty(panels: Record<string, Panel>, ptyId: string): string | undefined { - for (const [id, panel] of Object.entries(panels)) { - if (panel.ptyId === ptyId) return id - } -} - -export function TerminalSplit(props: TerminalSplitProps) { - const terminal = useTerminal() - const pane = createMemo(() => terminal.pane(props.tabId)) - const terminals = createMemo(() => terminal.all().filter((t) => t.tabId === props.tabId)) - const [containerFocused, setContainerFocused] = createSignal(true) - - const layout = createMemo(() => { - const p = pane() - if (!p) { - const single = terminals()[0] - if (!single) return new Map() - return new Map([[single.id, { top: 0, left: 0, width: 100, height: 100 }]]) - } - return computeLayout(p.panels, p.root, { top: 0, left: 0, width: 100, height: 100 }) - }) - - const focused = createMemo(() => { - const p = pane() - if (!p) return props.tabId - const focusedPanel = p.panels[p.focused ?? ""] - return focusedPanel?.ptyId ?? props.tabId - }) - - const handleFocus = (ptyId: string) => { - const p = pane() - if (!p) return - const panelId = findPanelForPty(p.panels, ptyId) - if (panelId) terminal.focus(props.tabId, panelId) - } - - const handleClose = (ptyId: string) => { - const pty = terminal.all().find((t) => t.id === ptyId) - if (!pty) return - - const p = pane() - if (!p) { - if (pty.tabId === props.tabId) { - terminal.closeTab(props.tabId) - } - return - } - const panelId = findPanelForPty(p.panels, ptyId) - if (panelId) terminal.closeSplit(props.tabId, panelId) - } - - return ( - <div - class="relative size-full" - data-terminal-split-container - onFocusIn={() => setContainerFocused(true)} - onFocusOut={(e) => { - const related = e.relatedTarget as Node | null - if (!related || !e.currentTarget.contains(related)) { - setContainerFocused(false) - } - }} - > - <For each={terminals()}> - {(pty) => { - const bounds = createMemo(() => layout().get(pty.id) ?? { top: 0, left: 0, width: 100, height: 100 }) - const isFocused = createMemo(() => focused() === pty.id) - const hasSplits = createMemo(() => !!pane()) - - return ( - <div - class="absolute flex flex-col min-h-0" - classList={{ - "ring-1 ring-inset ring-border-strong-base": containerFocused() && isFocused(), - "border-l border-border-weak-base": bounds().left > 0, - "border-t border-border-weak-base": bounds().top > 0, - }} - style={{ - top: `${bounds().top}%`, - left: `${bounds().left}%`, - width: `${bounds().width}%`, - height: `${bounds().height}%`, - }} - onClick={() => handleFocus(pty.id)} - > - <Show when={pane()}> - <div class="absolute top-1 right-1 z-10 opacity-0 hover:opacity-100 transition-opacity"> - <IconButton - icon="close" - variant="ghost" - onClick={(e) => { - e.stopPropagation() - handleClose(pty.id) - }} - /> - </div> - </Show> - <div - class="flex-1 min-h-0" - classList={{ "opacity-50": !containerFocused() || (hasSplits() && !isFocused()) }} - > - <Terminal - pty={pty} - focused={isFocused()} - onCleanup={terminal.update} - onConnectError={() => terminal.clone(pty.id)} - onExit={() => handleClose(pty.id)} - class="size-full" - /> - </div> - </div> - ) - }} - </For> - <ResizeHandles tabId={props.tabId} /> - </div> - ) -} - -function ResizeHandles(props: { tabId: string }) { - const terminal = useTerminal() - const pane = createMemo(() => terminal.pane(props.tabId)) - - const splits = createMemo(() => { - const p = pane() - if (!p) return [] - return Object.values(p.panels).filter((panel) => panel.children && panel.children.length === 2) - }) - - return <For each={splits()}>{(panel) => <ResizeHandle tabId={props.tabId} panelId={panel.id} />}</For> -} - -function ResizeHandle(props: { tabId: string; panelId: string }) { - const terminal = useTerminal() - const pane = createMemo(() => terminal.pane(props.tabId)) - const panel = createMemo(() => pane()?.panels[props.panelId]) - - let cleanup: VoidFunction | undefined - - onCleanup(() => cleanup?.()) - - const position = createMemo(() => { - const p = pane() - if (!p) return null - const pan = panel() - if (!pan?.children || pan.children.length !== 2) return null - - const bounds = computePanelBounds(p.panels, p.root, props.panelId, { - top: 0, - left: 0, - width: 100, - height: 100, - }) - if (!bounds) return null - - const sizes = pan.sizes ?? [50, 50] - - if (pan.direction === "horizontal") { - return { - horizontal: true, - top: bounds.top + (bounds.height * sizes[0]) / 100, - left: bounds.left, - size: bounds.width, - } - } - return { - horizontal: false, - top: bounds.top, - left: bounds.left + (bounds.width * sizes[0]) / 100, - size: bounds.height, - } - }) - - const handleMouseDown = (e: MouseEvent) => { - e.preventDefault() - - const pos = position() - if (!pos) return - - const container = (e.target as HTMLElement).closest("[data-terminal-split-container]") as HTMLElement - if (!container) return - - const rect = container.getBoundingClientRect() - const pan = panel() - if (!pan) return - - const p = pane() - if (!p) return - const panelBounds = computePanelBounds(p.panels, p.root, props.panelId, { - top: 0, - left: 0, - width: 100, - height: 100, - }) - if (!panelBounds) return - - const handleMouseMove = (e: MouseEvent) => { - if (pan.direction === "horizontal") { - const totalPx = (rect.height * panelBounds.height) / 100 - const topPx = (rect.height * panelBounds.top) / 100 - const posPx = e.clientY - rect.top - topPx - const percent = Math.max(10, Math.min(90, (posPx / totalPx) * 100)) - terminal.resizeSplit(props.tabId, props.panelId, [percent, 100 - percent]) - } else { - const totalPx = (rect.width * panelBounds.width) / 100 - const leftPx = (rect.width * panelBounds.left) / 100 - const posPx = e.clientX - rect.left - leftPx - const percent = Math.max(10, Math.min(90, (posPx / totalPx) * 100)) - terminal.resizeSplit(props.tabId, props.panelId, [percent, 100 - percent]) - } - } - - const handleMouseUp = () => { - document.removeEventListener("mousemove", handleMouseMove) - document.removeEventListener("mouseup", handleMouseUp) - cleanup = undefined - } - - cleanup = handleMouseUp - document.addEventListener("mousemove", handleMouseMove) - document.addEventListener("mouseup", handleMouseUp) - } - - return ( - <Show when={position()}> - {(pos) => ( - <div - data-component="resize-handle" - data-direction={pos().horizontal ? "vertical" : "horizontal"} - class="absolute" - style={{ - top: `${pos().top}%`, - left: `${pos().left}%`, - width: pos().horizontal ? `${pos().size}%` : "8px", - height: pos().horizontal ? "8px" : `${pos().size}%`, - transform: pos().horizontal ? "translateY(-50%)" : "translateX(-50%)", - cursor: pos().horizontal ? "row-resize" : "col-resize", - }} - onMouseDown={handleMouseDown} - /> - )} - </Show> - ) -} - -function computePanelBounds( - panels: Record<string, Panel>, - currentId: string, - targetId: string, - bounds: { top: number; left: number; width: number; height: number }, -): { top: number; left: number; width: number; height: number } | null { - if (currentId === targetId) return bounds - - const panel = panels[currentId] - if (!panel?.children || panel.children.length !== 2) return null - - const [leftId, rightId] = panel.children - const sizes = panel.sizes ?? [50, 50] - const horizontal = panel.direction === "horizontal" - - if (horizontal) { - const topHeight = (bounds.height * sizes[0]) / 100 - const bottomHeight = bounds.height - topHeight - const topBounds = { ...bounds, height: topHeight } - const bottomBounds = { ...bounds, top: bounds.top + topHeight, height: bottomHeight } - return ( - computePanelBounds(panels, leftId, targetId, topBounds) ?? - computePanelBounds(panels, rightId, targetId, bottomBounds) - ) - } - - const leftWidth = (bounds.width * sizes[0]) / 100 - const rightWidth = bounds.width - leftWidth - const leftBounds = { ...bounds, width: leftWidth } - const rightBounds = { ...bounds, left: bounds.left + leftWidth, width: rightWidth } - return ( - computePanelBounds(panels, leftId, targetId, leftBounds) ?? - computePanelBounds(panels, rightId, targetId, rightBounds) - ) -} diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index a37a540f1..8001e2caa 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -7,11 +7,9 @@ import { resolveThemeVariant, useTheme, withAlpha, type HexColor } from "@openco export interface TerminalProps extends ComponentProps<"div"> { pty: LocalPTY - focused?: boolean onSubmit?: () => void onCleanup?: (pty: LocalPTY) => void onConnectError?: (error: unknown) => void - onExit?: () => void } type TerminalColors = { @@ -40,7 +38,7 @@ export const Terminal = (props: TerminalProps) => { const sdk = useSDK() const theme = useTheme() let container!: HTMLDivElement - const [local, others] = splitProps(props, ["pty", "focused", "class", "classList", "onConnectError"]) + const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"]) let ws: WebSocket | undefined let term: Term | undefined let ghostty: Ghostty @@ -51,7 +49,6 @@ export const Terminal = (props: TerminalProps) => { let handleTextareaBlur: () => void let reconnect: number | undefined let disposed = false - let cleaning = false const getTerminalColors = (): TerminalColors => { const mode = theme.mode() @@ -91,11 +88,6 @@ export const Terminal = (props: TerminalProps) => { t.focus() setTimeout(() => t.textarea?.focus(), 0) } - - createEffect(() => { - if (local.focused) focusTerminal() - }) - const handlePointerDown = () => { const activeElement = document.activeElement if (activeElement instanceof HTMLElement && activeElement !== container) { @@ -174,11 +166,6 @@ export const Terminal = (props: TerminalProps) => { return true } - // allow cmd+d and cmd+shift+d for terminal splitting - if (event.metaKey && key === "d") { - return true - } - return false }) @@ -244,6 +231,7 @@ export const Terminal = (props: TerminalProps) => { // console.log("Scroll position:", ydisp) // }) socket.addEventListener("open", () => { + console.log("WebSocket connected") sdk.client.pty .update({ ptyID: local.pty.id, @@ -262,9 +250,7 @@ export const Terminal = (props: TerminalProps) => { props.onConnectError?.(error) }) socket.addEventListener("close", () => { - if (!cleaning) { - props.onExit?.() - } + console.log("WebSocket disconnected") }) }) @@ -288,7 +274,6 @@ export const Terminal = (props: TerminalProps) => { }) } - cleaning = true ws?.close() t?.dispose() }) |
