import { useEffect, useRef, useState } from 'react' import { Settings2, Plus, X, Columns2, Grid2x2, SquareSlash } from 'lucide-react' import { api, type Integration } from '../lib/api' import { useTerminalSessions } from '../lib/TerminalSessionContext' import { TERM_THEMES } from '../lib/terminalPrefs' const GOLD = '#C8A434' const TEXT_SECONDARY = '#7A7D85' const FONT_SIZES = [11, 12, 13, 14, 15, 16] // "Symbols Nerd Font Mono" is appended as a glyph-only fallback to every option, so // distro icons / git branch / etc. from prompts like Starship render instead of // showing as boxes — it has no letterforms of its own, so it never overrides the // chosen base font for normal text. const NERD_FALLBACK = '"Symbols Nerd Font Mono"' const FONT_FAMILIES = [ { name: 'Monospace', value: `ui-monospace, SFMono-Regular, Menlo, monospace, ${NERD_FALLBACK}` }, { name: 'Fira Code', value: `"Fira Code", ui-monospace, monospace, ${NERD_FALLBACK}` }, { name: 'JetBrains Mono', value: `"JetBrains Mono", ui-monospace, monospace, ${NERD_FALLBACK}` }, ] export default function Terminal() { const [hosts, setHosts] = useState([]) const [showPrefs, setShowPrefs] = useState(false) const { tabs, activeTabId, activePaneId, prefs, setActiveTabId, setActivePaneId, setPrefs, addTab, closeTab, setPaneCount, setPaneHost, } = useTerminalSessions() useEffect(() => { api.listIntegrations().then(({ integrations }) => { setHosts(integrations.filter((i) => i.type === 'ssh')) }) }, []) const activeTab = tabs.find((t) => t.id === activeTabId) ?? tabs[0] const paneGridClass = activeTab.panes.length === 1 ? 'grid-cols-1 grid-rows-1' : activeTab.panes.length === 2 ? 'grid-cols-2 grid-rows-1' : 'grid-cols-2 grid-rows-2' const activePaneHostId = activeTab.panes.find((p) => p.id === activePaneId)?.hostId ?? null return (

SSH Hosts

{hosts.length === 0 && (

No SSH integrations configured. Add one in Settings → Integrations.

)}
{hosts.map((h) => ( ))}

Click a pane, then a host to connect it. Sessions stay connected when you switch pages — close a tab or pane to disconnect.

{tabs.map((tab) => (
setActiveTabId(tab.id)} className="flex shrink-0 cursor-pointer items-center gap-2 rounded-md px-3 py-1.5 text-xs" style={{ background: tab.id === activeTabId ? 'rgba(200,164,52,0.15)' : 'rgba(255,255,255,0.04)', color: tab.id === activeTabId ? GOLD : '#E8E6E0', }} > {tab.name} { e.stopPropagation() closeTab(tab.id) }} />
))}
{showPrefs && (
)}
{activeTab.panes.map((pane) => ( setActivePaneId(pane.id)} /> ))}
) } function TerminalPane({ paneId, hostId, hosts, active, onFocus, }: { paneId: number hostId: number | null hosts: Integration[] active: boolean onFocus: () => void }) { const { attachPane, getPaneInfo, reconnectTmux, version } = useTerminalSessions() const cellRef = useRef(null) // Mount the persistent xterm DOM node owned by the provider into this cell; // on unmount (route change or layout change) the wrapper is moved back to the // hidden root rather than disposed, so the session keeps running. useEffect(() => { if (!cellRef.current) return const detach = attachPane(paneId, cellRef.current) return detach // eslint-disable-next-line react-hooks/exhaustive-deps }, [paneId]) // `version` is read so the header re-renders when live pane state changes. void version const info = getPaneInfo(paneId) const host = hosts.find((h) => h.id === hostId) return (
{host ? (info.connected ? `Connected — ${host.name}` : `Disconnected — ${host.name}`) : 'Select a host to connect'} {host && ( )}
) }