import { For, Show, createEffect, createMemo, on, onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" import { makeEventListener } from "@solid-primitives/event-listener" import { createMediaQuery } from "@solid-primitives/media" import { DragDropProvider, PointerSensor } from "@dnd-kit/solid" import { isSortable } from "@dnd-kit/solid/sortable" import { Accessibility, AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/dom" import { RestrictToHorizontalAxis } from "@dnd-kit/abstract/modifiers" import { RestrictToElement } from "@dnd-kit/dom/modifiers" 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 { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" import { SortableTerminalTabV2 } from "@/components/session/session-sortable-terminal-tab-v2" import { Terminal } from "@/components/terminal" import { useCommand } from "@/context/command" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" import { useSettings } from "@/context/settings" import { useTerminal } from "@/context/terminal" import { useSDK } from "@/context/sdk" 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 TerminalPanelV2(props: { stacked?: boolean } = {}) { const delays = [120, 240] const layout = useLayout() const terminal = useTerminal() const sdk = useSDK() const language = useLanguage() const command = useCommand() const settings = useSettings() const { workspaceKey, view } = useSessionLayout() const isDesktop = createMediaQuery("(min-width: 768px)") const newLayout = createMemo(() => settings.general.newLayoutDesigns()) const opened = createMemo(() => view().terminal.opened()) const size = createSizing() const height = createMemo(() => layout.terminal.height()) const close = () => view().terminal.close() let root: HTMLDivElement | undefined let tabList: HTMLDivElement | undefined const [store, setStore] = createStore({ autoCreated: false, recovered: {} as Record, view: typeof window === "undefined" ? 1000 : (window.visualViewport?.height ?? window.innerHeight), }) const max = () => store.view * 0.6 const pane = () => Math.min(height(), max()) const stacked = createMemo(() => isDesktop() && props.stacked) const panelHeight = createMemo(() => isDesktop() ? (stacked() ? `${pane()}px` : "100%") : opened() ? `${pane()}px` : "0px", ) const contentHeight = createMemo(() => (isDesktop() ? (stacked() ? `${pane()}px` : "100%") : `${pane()}px`)) const newTerminalKeybind = createMemo(() => command.keybindParts("terminal.new")) onMount(() => { if (typeof window === "undefined") return const sync = () => setStore("view", window.visualViewport?.height ?? window.innerHeight) const port = window.visualViewport sync() makeEventListener(window, "resize", sync) if (port) makeEventListener(port, "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 = delays.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 = sdk().directory if (!dir) return if (!terminal.ready()) return language.locale() setTerminalHandoff( workspaceKey(), 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 = sdk().directory if (!dir) return [] return getTerminalHandoff(workspaceKey()) ?? [] }) const all = terminal.all const ids = createMemo(() => all().map((pty) => pty.id)) const recoverTerminal = (key: string, id: string, clone: (id: string) => Promise) => { if (store.recovered[key]) return setStore("recovered", key, true) void clone(id) } const terminalRecoveryKey = (pty: { id: string; title: string; titleNumber: number }) => { return String(pty.titleNumber || pty.title || pty.id) } const markTerminalConnected = (key: string, id: string, trim: (id: string) => void) => { setStore("recovered", key, false) trim(id) } const handleTerminalDragEnd = () => { const activeId = terminal.active() if (!activeId) return requestAnimationFrame(() => { if (terminal.active() !== activeId) return focusTerminalById(activeId) }) } return ( ) }