Procházet zdrojové kódy

Revert "feat(desktop): Terminal Splits (#8767)"

This reverts commit 88fd6a294b3ad88d50cb8e1853589ee4e68dc74e.
Adam před 2 měsíci
rodič
revize
71306cbd1f

+ 2 - 2
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)} />
             )
           }
         >

+ 0 - 322
packages/app/src/components/terminal-split.tsx

@@ -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)
-  )
-}

+ 3 - 18
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()
   })

+ 56 - 334
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<string, Panel>
-  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<typeof useSDK>, dir: string, id: string | undefined) {
   const legacy = `${dir}/terminal${id ? "/" + id : ""}.v1`
 
@@ -56,102 +33,47 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, id:
     createStore<{
       active?: string
       all: LocalPTY[]
-      panes: Record<string, TabPane>
     }>({
       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<LocalPTY | undefined> => {
-    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<string, LocalPTY>()
-  const tabs = createMemo(() => {
-    const migrated = migrate(store.all)
-    const seen = new Set<string>()
-    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<LocalPTY> & { 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<typeof useSDK>, 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<typeof useSDK>, 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<LocalPTY> & { 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),
     }
   },
 })

+ 0 - 13
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%;
-  }
-}

+ 10 - 36
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() {
             >
               <DragDropSensors />
               <ConstrainDragYAxis />
-              <Tabs variant="alt" value={activeTerminal()} onChange={terminal.open}>
+              <Tabs variant="alt" value={terminal.active()} onChange={terminal.open}>
                 <Tabs.List class="h-10">
-                  <SortableProvider ids={terminal.tabs().map((t: LocalPTY) => t.id)}>
-                    <For each={terminal.tabs()}>{(pty) => <SortableTerminalTab terminal={pty} />}</For>
+                  <SortableProvider ids={terminal.all().map((t: LocalPTY) => t.id)}>
+                    <For each={terminal.all()}>{(pty) => <SortableTerminalTab terminal={pty} />}</For>
                   </SortableProvider>
                   <div class="h-full flex items-center justify-center">
                     <TooltipKeybind
@@ -1707,10 +1681,10 @@ export default function Page() {
                     </TooltipKeybind>
                   </div>
                 </Tabs.List>
-                <For each={terminal.tabs()}>
+                <For each={terminal.all()}>
                   {(pty) => (
-                    <Tabs.Content value={pty.id} class="h-[calc(100%-2.5rem)]">
-                      <TerminalSplit tabId={pty.id} />
+                    <Tabs.Content value={pty.id}>
+                      <Terminal pty={pty} onCleanup={terminal.update} onConnectError={() => terminal.clone(pty.id)} />
                     </Tabs.Content>
                   )}
                 </For>
@@ -1718,7 +1692,7 @@ export default function Page() {
               <DragOverlay>
                 <Show when={store.activeTerminalDraggable}>
                   {(draggedId) => {
-                    const pty = createMemo(() => terminal.tabs().find((t: LocalPTY) => t.id === draggedId()))
+                    const pty = createMemo(() => terminal.all().find((t: LocalPTY) => t.id === draggedId()))
                     return (
                       <Show when={pty()}>
                         {(t) => (

+ 0 - 4
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)
     })