import { For, Show, createEffect, createMemo, on, onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" import { Tabs } from "@opencode-ai/ui/tabs" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { IconButton } from "@opencode-ai/ui/icon-button" import { TooltipKeybind } from "@opencode-ai/ui/tooltip" import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" import type { DragEvent } from "@thisbeyond/solid-dnd" import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" import { SortableTerminalTab } from "@/components/session" import { Terminal } from "@/components/terminal" import { useCommand } from "@/context/command" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" import { useTerminal } from "@/context/terminal" import { terminalTabLabel } from "@/pages/session/terminal-label" import { createSizing, focusTerminalById } from "@/pages/session/helpers" import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff" import { useSessionLayout } from "@/pages/session/session-layout" export function TerminalPanel() { const layout = useLayout() const terminal = useTerminal() const language = useLanguage() const command = useCommand() const { params, view } = useSessionLayout() const opened = createMemo(() => view().terminal.opened()) const size = createSizing() const height = createMemo(() => layout.terminal.height()) const close = () => view().terminal.close() let root: HTMLDivElement | undefined const [store, setStore] = createStore({ autoCreated: false, activeDraggable: undefined as string | undefined, view: typeof window === "undefined" ? 1000 : (window.visualViewport?.height ?? window.innerHeight), }) const max = () => store.view * 0.6 const pane = () => Math.min(height(), max()) onMount(() => { if (typeof window === "undefined") return const sync = () => setStore("view", window.visualViewport?.height ?? window.innerHeight) const port = window.visualViewport sync() window.addEventListener("resize", sync) port?.addEventListener("resize", sync) onCleanup(() => { window.removeEventListener("resize", sync) port?.removeEventListener("resize", sync) }) }) createEffect(() => { if (!opened()) { setStore("autoCreated", false) return } if (!terminal.ready() || terminal.all().length !== 0 || store.autoCreated) return terminal.new() setStore("autoCreated", true) }) createEffect( on( () => terminal.all().length, (count, prevCount) => { if (prevCount === undefined || prevCount <= 0 || count !== 0) return if (!opened()) return close() }, ), ) const focus = (id: string) => { focusTerminalById(id) const frame = requestAnimationFrame(() => { if (!opened()) return if (terminal.active() !== id) return focusTerminalById(id) }) const timers = [120, 240].map((ms) => window.setTimeout(() => { if (!opened()) return if (terminal.active() !== id) return focusTerminalById(id) }, ms), ) return () => { cancelAnimationFrame(frame) for (const timer of timers) clearTimeout(timer) } } createEffect( on( () => [opened(), terminal.active()] as const, ([next, id]) => { if (!next || !id) return const stop = focus(id) onCleanup(stop) }, ), ) createEffect(() => { if (opened()) return const active = document.activeElement if (!(active instanceof HTMLElement)) return if (!root?.contains(active)) return active.blur() }) createEffect(() => { const dir = params.dir if (!dir) return if (!terminal.ready()) return language.locale() setTerminalHandoff( dir, terminal.all().map((pty) => terminalTabLabel({ title: pty.title, titleNumber: pty.titleNumber, t: language.t as (key: string, vars?: Record) => string, }), ), ) }) const handoff = createMemo(() => { const dir = params.dir if (!dir) return [] return getTerminalHandoff(dir) ?? [] }) const all = terminal.all const ids = createMemo(() => all().map((pty) => pty.id)) const handleTerminalDragStart = (event: unknown) => { const id = getDraggableId(event) if (!id) return setStore("activeDraggable", id) } const handleTerminalDragOver = (event: DragEvent) => { const { draggable, droppable } = event if (!draggable || !droppable) return const terminals = terminal.all() const fromIndex = terminals.findIndex((t) => t.id === draggable.id.toString()) const toIndex = terminals.findIndex((t) => t.id === droppable.id.toString()) if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) { terminal.move(draggable.id.toString(), toIndex) } } const handleTerminalDragEnd = () => { setStore("activeDraggable", undefined) const activeId = terminal.active() if (!activeId) return requestAnimationFrame(() => { if (terminal.active() !== activeId) return focusTerminalById(activeId) }) } return (
{(title) => (
{title}
)}
{language.t("common.loading")} {language.t("common.loading.ellipsis")}
{language.t("terminal.loading")}
} >
terminal.open(id)} class="!h-auto !flex-none" > {(pty) => }
{(id) => ( pty.id === id)}> {(pty) => (
terminal.trim(id)} onCleanup={terminal.update} onConnectError={() => terminal.clone(id)} />
)}
)}
{(id) => ( pty.id === id)}> {(t) => (
{terminalTabLabel({ title: t().title, titleNumber: t().titleNumber, t: language.t as (key: string, vars?: Record) => string, })}
)}
)}
) }