import { For, Show, createEffect, createMemo, on, onCleanup } from "solid-js" import { createStore } from "solid-js/store" import { useParams } from "@solidjs/router" 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, type LocalPTY } from "@/context/terminal" import { terminalTabLabel } from "@/pages/session/terminal-label" import { createSizing, focusTerminalById } from "@/pages/session/helpers" import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff" export function TerminalPanel() { const params = useParams() const layout = useLayout() const terminal = useTerminal() const language = useLanguage() const command = useCommand() const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const view = createMemo(() => layout.view(sessionKey)) 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()) createEffect(() => { 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 = createMemo(() => terminal.all()) const ids = createMemo(() => all().map((pty) => pty.id)) const byId = createMemo(() => new Map(all().map((pty) => [pty.id, { ...pty }]))) 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: LocalPTY) => t.id === draggable.id.toString()) const toIndex = terminals.findIndex((t: LocalPTY) => 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" > {(id) => ( {(pty) => } )}
{(id) => ( {(pty) => (
terminal.trim(id)} onCleanup={terminal.update} onConnectError={() => terminal.clone(id)} />
)}
)}
{(draggedId) => ( {(t) => (
{terminalTabLabel({ title: t().title, titleNumber: t().titleNumber, t: language.t as (key: string, vars?: Record) => string, })}
)}
)}
) }