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 1 && ( - terminal.closeTab(props.terminal.tabId)} /> + terminal.all().length > 1 && ( + 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, - panelId: string, - bounds: { top: number; left: number; width: number; height: number }, -): Map { - const result = new Map() - 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, 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 ( -
setContainerFocused(true)} - onFocusOut={(e) => { - const related = e.relatedTarget as Node | null - if (!related || !e.currentTarget.contains(related)) { - setContainerFocused(false) - } - }} - > - - {(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 ( -
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)} - > - -
- { - e.stopPropagation() - handleClose(pty.id) - }} - /> -
-
-
- terminal.clone(pty.id)} - onExit={() => handleClose(pty.id)} - class="size-full" - /> -
-
- ) - }} -
- -
- ) -} - -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 {(panel) => } -} - -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 ( - - {(pos) => ( -
- )} - - ) -} - -function computePanelBounds( - panels: Record, - 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() }) diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index e1492c8da..a7753069c 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -9,31 +9,12 @@ export type LocalPTY = { id: string title: string titleNumber: number - tabId: string rows?: number cols?: number buffer?: string scrollY?: number } -export type SplitDirection = "horizontal" | "vertical" - -export type Panel = { - id: string - parentId?: string - ptyId?: string - direction?: SplitDirection - children?: [string, string] - sizes?: [number, number] -} - -export type TabPane = { - id: string - root: string - panels: Record - focused?: string -} - const WORKSPACE_KEY = "__workspace__" const MAX_TERMINAL_SESSIONS = 20 @@ -44,10 +25,6 @@ type TerminalCacheEntry = { dispose: VoidFunction } -function generateId() { - return Math.random().toString(36).slice(2, 10) -} - function createTerminalSession(sdk: ReturnType, dir: string, id: string | undefined) { const legacy = `${dir}/terminal${id ? "/" + id : ""}.v1` @@ -56,102 +33,47 @@ function createTerminalSession(sdk: ReturnType, dir: string, id: createStore<{ active?: string all: LocalPTY[] - panes: Record }>({ all: [], - panes: {}, }), ) - const getNextTitleNumber = () => { - const existing = new Set(store.all.filter((p) => p.tabId === p.id).map((pty) => pty.titleNumber)) - let next = 1 - while (existing.has(next)) next++ - return next - } - - const createPty = async (tabId?: string): Promise => { - const tab = tabId ? store.all.find((p) => p.id === tabId) : undefined - const num = tab?.titleNumber ?? getNextTitleNumber() - const title = tab?.title ?? `Terminal ${num}` - const pty = await sdk.client.pty.create({ title }).catch((e) => { - console.error("Failed to create terminal", e) - return undefined - }) - if (!pty?.data?.id) return undefined - return { - id: pty.data.id, - title, - titleNumber: num, - tabId: tabId ?? pty.data.id, - } - } - - const getAllPtyIds = (pane: TabPane, panelId: string): string[] => { - const panel = pane.panels[panelId] - if (!panel) return [] - if (panel.ptyId) return [panel.ptyId] - if (panel.children && panel.children.length === 2) { - return [...getAllPtyIds(pane, panel.children[0]), ...getAllPtyIds(pane, panel.children[1])] - } - return [] - } - - const getFirstLeaf = (pane: TabPane, panelId: string): string | undefined => { - const panel = pane.panels[panelId] - if (!panel) return undefined - if (panel.ptyId) return panelId - if (panel.children?.[0]) return getFirstLeaf(pane, panel.children[0]) - return undefined - } - - const migrate = (terminals: LocalPTY[]) => - terminals.map((p) => ((p as { tabId?: string }).tabId ? p : { ...p, tabId: p.id })) - - const tabCache = new Map() - const tabs = createMemo(() => { - const migrated = migrate(store.all) - const seen = new Set() - const result: LocalPTY[] = [] - for (const p of migrated) { - if (!seen.has(p.tabId)) { - seen.add(p.tabId) - const cached = tabCache.get(p.tabId) - if (cached) { - cached.title = p.title - cached.titleNumber = p.titleNumber - result.push(cached) - } else { - const tab = { ...p, id: p.tabId } - tabCache.set(p.tabId, tab) - result.push(tab) - } - } - } - for (const key of tabCache.keys()) { - if (!seen.has(key)) tabCache.delete(key) - } - return result - }) - const all = createMemo(() => migrate(store.all)) - return { ready, - tabs, - all, - active: () => store.active, - panes: () => store.panes, - pane: (tabId: string) => store.panes[tabId], - panel: (tabId: string, panelId: string) => store.panes[tabId]?.panels[panelId], - focused: (tabId: string) => store.panes[tabId]?.focused, + all: createMemo(() => Object.values(store.all)), + active: createMemo(() => store.active), + new() { + const existingTitleNumbers = new Set( + store.all.map((pty) => { + const match = pty.titleNumber + return match + }), + ) - async new() { - const pty = await createPty() - if (!pty) return - setStore("all", [...store.all, pty]) - setStore("active", pty.tabId) + let nextNumber = 1 + while (existingTitleNumbers.has(nextNumber)) { + nextNumber++ + } + + sdk.client.pty + .create({ title: `Terminal ${nextNumber}` }) + .then((pty) => { + const id = pty.data?.id + if (!id) return + setStore("all", [ + ...store.all, + { + id, + title: pty.data?.title ?? "Terminal", + titleNumber: nextNumber, + }, + ]) + setStore("active", id) + }) + .catch((e) => { + console.error("Failed to create terminal", e) + }) }, - update(pty: Partial & { id: string }) { setStore("all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x))) sdk.client.pty @@ -164,82 +86,46 @@ function createTerminalSession(sdk: ReturnType, dir: string, id: console.error("Failed to update terminal", e) }) }, - async clone(id: string) { const index = store.all.findIndex((x) => x.id === id) const pty = store.all[index] if (!pty) return - const clone = await sdk.client.pty.create({ title: pty.title }).catch((e) => { - console.error("Failed to clone terminal", e) - return undefined - }) + const clone = await sdk.client.pty + .create({ + title: pty.title, + }) + .catch((e) => { + console.error("Failed to clone terminal", e) + return undefined + }) if (!clone?.data) return - setStore("all", index, { ...pty, ...clone.data }) - if (store.active === pty.tabId) { - setStore("active", pty.tabId) + setStore("all", index, { + ...pty, + ...clone.data, + }) + if (store.active === pty.id) { + setStore("active", clone.data.id) } }, - open(id: string) { setStore("active", id) }, - async close(id: string) { - const pty = store.all.find((x) => x.id === id) - if (!pty) return - - const pane = store.panes[pty.tabId] - if (pane) { - const panelId = Object.keys(pane.panels).find((key) => pane.panels[key].ptyId === id) - if (panelId) { - await this.closeSplit(pty.tabId, panelId) - return + batch(() => { + setStore( + "all", + store.all.filter((x) => x.id !== id), + ) + if (store.active === id) { + const index = store.all.findIndex((f) => f.id === id) + const previous = store.all[Math.max(0, index - 1)] + setStore("active", previous?.id) } - } - - if (store.active === pty.tabId) { - const remaining = store.all.filter((p) => p.tabId === p.id && p.id !== id) - setStore("active", remaining[0]?.tabId) - } - - setStore( - "all", - store.all.filter((x) => x.id !== id), - ) - + }) await sdk.client.pty.remove({ ptyID: id }).catch((e) => { console.error("Failed to close terminal", e) }) }, - - async closeTab(tabId: string) { - const pane = store.panes[tabId] - const terminalsInTab = store.all.filter((p) => p.tabId === tabId) - const ptyIds = pane ? getAllPtyIds(pane, pane.root) : terminalsInTab.map((p) => p.id) - - const remainingTabs = store.all.filter((p) => p.tabId !== tabId) - const uniqueTabIds = [...new Set(remainingTabs.map((p) => p.tabId))] - - setStore( - "all", - store.all.filter((x) => !ptyIds.includes(x.id)), - ) - setStore( - "panes", - produce((panes) => { - delete panes[tabId] - }), - ) - if (store.active === tabId) { - setStore("active", uniqueTabIds[0]) - } - for (const ptyId of ptyIds) { - await sdk.client.pty.remove({ ptyID: ptyId }).catch((e) => { - console.error("Failed to close terminal", e) - }) - } - }, - move(id: string, to: number) { const index = store.all.findIndex((f) => f.id === id) if (index === -1) return @@ -250,159 +136,6 @@ function createTerminalSession(sdk: ReturnType, dir: string, id: }), ) }, - - async split(tabId: string, direction: SplitDirection) { - const pane = store.panes[tabId] - const newPty = await createPty(tabId) - if (!newPty) return - - setStore("all", [...store.all, newPty]) - - if (!pane) { - const rootId = generateId() - const leftId = generateId() - const rightId = generateId() - - setStore("panes", tabId, { - id: tabId, - root: rootId, - panels: { - [rootId]: { - id: rootId, - direction, - children: [leftId, rightId], - sizes: [50, 50], - }, - [leftId]: { - id: leftId, - parentId: rootId, - ptyId: tabId, - }, - [rightId]: { - id: rightId, - parentId: rootId, - ptyId: newPty.id, - }, - }, - focused: rightId, - }) - } else { - const focusedPanelId = pane.focused - if (!focusedPanelId) return - - const focusedPanel = pane.panels[focusedPanelId] - if (!focusedPanel?.ptyId) return - - const oldPtyId = focusedPanel.ptyId - const newSplitId = generateId() - const newTerminalId = generateId() - - setStore("panes", tabId, "panels", newSplitId, { - id: newSplitId, - parentId: focusedPanelId, - ptyId: oldPtyId, - }) - setStore("panes", tabId, "panels", newTerminalId, { - id: newTerminalId, - parentId: focusedPanelId, - ptyId: newPty.id, - }) - setStore("panes", tabId, "panels", focusedPanelId, "ptyId", undefined) - setStore("panes", tabId, "panels", focusedPanelId, "direction", direction) - setStore("panes", tabId, "panels", focusedPanelId, "children", [newSplitId, newTerminalId]) - setStore("panes", tabId, "panels", focusedPanelId, "sizes", [50, 50]) - setStore("panes", tabId, "focused", newTerminalId) - } - }, - - focus(tabId: string, panelId: string) { - if (store.panes[tabId]) { - setStore("panes", tabId, "focused", panelId) - } - }, - - async closeSplit(tabId: string, panelId: string) { - const pane = store.panes[tabId] - if (!pane) return - - const panel = pane.panels[panelId] - if (!panel) return - - const ptyId = panel.ptyId - if (!ptyId) return - - if (!panel.parentId) { - await this.closeTab(tabId) - return - } - - const parentPanel = pane.panels[panel.parentId] - if (!parentPanel?.children || parentPanel.children.length !== 2) return - - const siblingId = parentPanel.children[0] === panelId ? parentPanel.children[1] : parentPanel.children[0] - const sibling = pane.panels[siblingId] - if (!sibling) return - - const newFocused = sibling.ptyId ? panel.parentId! : (getFirstLeaf(pane, sibling.children![0]) ?? panel.parentId!) - - batch(() => { - setStore( - "panes", - tabId, - "panels", - produce((panels) => { - const parent = panels[panel.parentId!] - if (!parent) return - - if (sibling.ptyId) { - parent.ptyId = sibling.ptyId - parent.direction = undefined - parent.children = undefined - parent.sizes = undefined - } else if (sibling.children && sibling.children.length === 2) { - parent.ptyId = undefined - parent.direction = sibling.direction - parent.children = sibling.children - parent.sizes = sibling.sizes - panels[sibling.children[0]].parentId = panel.parentId! - panels[sibling.children[1]].parentId = panel.parentId! - } - - delete panels[panelId] - delete panels[siblingId] - }), - ) - - setStore("panes", tabId, "focused", newFocused) - - setStore( - "all", - store.all.filter((x) => x.id !== ptyId), - ) - }) - - const remainingPanels = Object.values(store.panes[tabId]?.panels ?? {}) - const shouldCleanupPane = remainingPanels.length === 1 && remainingPanels[0]?.ptyId - - if (shouldCleanupPane) { - setStore( - "panes", - produce((panes) => { - delete panes[tabId] - }), - ) - } - - await sdk.client.pty.remove({ ptyID: ptyId }).catch((e) => { - console.error("Failed to close terminal", e) - }) - }, - - resizeSplit(tabId: string, panelId: string, sizes: [number, number]) { - if (store.panes[tabId]?.panels[panelId]) { - setStore("panes", tabId, "panels", panelId, "sizes", sizes) - } - }, } } @@ -456,25 +189,14 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont return { ready: () => session().ready(), - tabs: () => session().tabs(), all: () => session().all(), active: () => session().active(), - panes: () => session().panes(), - pane: (tabId: string) => session().pane(tabId), - panel: (tabId: string, panelId: string) => session().panel(tabId, panelId), - focused: (tabId: string) => session().focused(tabId), new: () => session().new(), update: (pty: Partial & { id: string }) => session().update(pty), clone: (id: string) => session().clone(id), open: (id: string) => session().open(id), close: (id: string) => session().close(id), - closeTab: (tabId: string) => session().closeTab(tabId), move: (id: string, to: number) => session().move(id, to), - split: (tabId: string, direction: SplitDirection) => session().split(tabId, direction), - focus: (tabId: string, panelId: string) => session().focus(tabId, panelId), - closeSplit: (tabId: string, panelId: string) => session().closeSplit(tabId, panelId), - resizeSplit: (tabId: string, panelId: string, sizes: [number, number]) => - session().resizeSplit(tabId, panelId, sizes), } }, }) diff --git a/packages/app/src/index.css b/packages/app/src/index.css index 2326bbb11..d9d51aa8f 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -9,16 +9,3 @@ *[data-tauri-drag-region] { app-region: drag; } - -/* Terminal split resize handles */ -[data-terminal-split-container] [data-component="resize-handle"] { - inset: unset; - - &[data-direction="horizontal"] { - height: 100%; - } - - &[data-direction="vertical"] { - width: 100%; - } -} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 585156afa..ca5e73a9b 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -26,7 +26,6 @@ import { useSync } from "@/context/sync" import { useTerminal, type LocalPTY } from "@/context/terminal" import { useLayout } from "@/context/layout" import { Terminal } from "@/components/terminal" -import { TerminalSplit } from "@/components/terminal-split" import { checksum, base64Encode, base64Decode } from "@opencode-ai/util/encode" import { useDialog } from "@opencode-ai/ui/context/dialog" import { DialogSelectFile } from "@/components/dialog-select-file" @@ -171,7 +170,6 @@ export default function Page() { const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey())) const view = createMemo(() => layout.view(sessionKey())) - const activeTerminal = createMemo(() => terminal.active()) if (import.meta.env.DEV) { createEffect( @@ -382,7 +380,7 @@ export default function Page() { createEffect(() => { if (!view().terminal.opened()) return if (!terminal.ready()) return - if (terminal.tabs().length !== 0) return + if (terminal.all().length !== 0) return terminal.new() }) @@ -461,30 +459,6 @@ export default function Page() { keybind: "ctrl+shift+`", onSelect: () => terminal.new(), }, - { - id: "terminal.split.vertical", - title: "Split terminal right", - description: "Split the current terminal vertically", - category: "Terminal", - keybind: "mod+d", - disabled: !terminal.active(), - onSelect: () => { - const active = terminal.active() - if (active) terminal.split(active, "vertical") - }, - }, - { - id: "terminal.split.horizontal", - title: "Split terminal down", - description: "Split the current terminal horizontally", - category: "Terminal", - keybind: "mod+shift+d", - disabled: !terminal.active(), - onSelect: () => { - const active = terminal.active() - if (active) terminal.split(active, "horizontal") - }, - }, { id: "steps.toggle", title: "Toggle steps", @@ -733,7 +707,7 @@ export default function Page() { const handleTerminalDragOver = (event: DragEvent) => { const { draggable, droppable } = event if (draggable && droppable) { - const terminals = terminal.tabs() + const terminals = terminal.all() const fromIndex = terminals.findIndex((t: LocalPTY) => t.id === draggable.id.toString()) const toIndex = terminals.findIndex((t: LocalPTY) => t.id === droppable.id.toString()) if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) { @@ -1035,7 +1009,7 @@ export default function Page() { createEffect(() => { if (!terminal.ready()) return - handoff.terminals = terminal.tabs().map((t) => t.title) + handoff.terminals = terminal.all().map((t) => t.title) }) createEffect(() => { @@ -1692,10 +1666,10 @@ export default function Page() { > - + - t.id)}> - {(pty) => } + t.id)}> + {(pty) => }
- + {(pty) => ( - - + + terminal.clone(pty.id)} /> )} @@ -1718,7 +1692,7 @@ export default function Page() { {(draggedId) => { - const pty = createMemo(() => terminal.tabs().find((t: LocalPTY) => t.id === draggedId())) + const pty = createMemo(() => terminal.all().find((t: LocalPTY) => t.id === draggedId())) return ( {(t) => ( diff --git a/packages/opencode/src/pty/index.ts b/packages/opencode/src/pty/index.ts index b76160d50..39ccebf96 100644 --- a/packages/opencode/src/pty/index.ts +++ b/packages/opencode/src/pty/index.ts @@ -146,10 +146,6 @@ export namespace Pty { ptyProcess.onExit(({ exitCode }) => { log.info("session exited", { id, exitCode }) session.info.status = "exited" - for (const ws of session.subscribers) { - ws.close() - } - session.subscribers.clear() Bus.publish(Event.Exited, { id, exitCode }) state().delete(id) })