import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" import { Clipboard } from "@tui/util/clipboard" import { TextAttributes } from "@opentui/core" import { RouteProvider, useRoute, type Route } from "@tui/context/route" import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal } from "solid-js" import { Installation } from "@/installation" import { Global } from "@/global" import { DialogProvider, useDialog } from "@tui/ui/dialog" import { SDKProvider, useSDK } from "@tui/context/sdk" import { SyncProvider, useSync } from "@tui/context/sync" import { LocalProvider, useLocal } from "@tui/context/local" import { DialogModel } from "@tui/component/dialog-model" import { DialogStatus } from "@tui/component/dialog-status" import { DialogThemeList } from "@tui/component/dialog-theme-list" import { DialogHelp } from "./ui/dialog-help" import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command" import { DialogAgent } from "@tui/component/dialog-agent" import { DialogSessionList } from "@tui/component/dialog-session-list" import { KeybindProvider } from "@tui/context/keybind" import { ThemeProvider, useTheme } from "@tui/context/theme" import { Home } from "@tui/routes/home" import { Session } from "@tui/routes/session" import { PromptHistoryProvider } from "./component/prompt/history" import { DialogAlert } from "./ui/dialog-alert" import { ToastProvider, useToast } from "./ui/toast" import { ExitProvider } from "./context/exit" import type { SessionRoute } from "./context/route" import { Session as SessionApi } from "@/session" import { TuiEvent } from "./event" import { KVProvider, useKV } from "./context/kv" export function tui(input: { url: string sessionID?: string model?: string agent?: string onExit?: () => Promise }) { // promise to prevent immediate exit return new Promise((resolve) => { const routeData: Route | undefined = input.sessionID ? { type: "session", sessionID: input.sessionID, } : undefined const onExit = async () => { await input.onExit?.() resolve() } render( () => { return ( }> ) }, { targetFps: 60, gatherStats: false, exitOnCtrlC: false, }, ) }) } function App() { const route = useRoute() const dimensions = useTerminalDimensions() const renderer = useRenderer() renderer.disableStdoutInterception() const dialog = useDialog() const local = useLocal() const kv = useKV() const command = useCommandDialog() const { event } = useSDK() const sync = useSync() const toast = useToast() const [sessionExists, setSessionExists] = createSignal(false) const { theme } = useTheme() useKeyboard(async (evt) => { if (evt.meta && evt.name === "t") { renderer.toggleDebugOverlay() return } if (evt.meta && evt.name === "d") { renderer.console.toggle() return } }) // Make sure session is valid, otherwise redirect to home createEffect(async () => { if (route.data.type === "session") { const data = route.data as SessionRoute await sync.session.sync(data.sessionID).catch(() => { toast.show({ message: `Session not found: ${data.sessionID}`, variant: "error", }) return route.navigate({ type: "home" }) }) setSessionExists(true) } }) createEffect(() => { console.log(JSON.stringify(route.data)) }) command.register(() => [ { title: "Switch session", value: "session.list", keybind: "session_list", category: "Session", onSelect: () => { dialog.replace(() => ) }, }, { title: "New session", value: "session.new", keybind: "session_new", category: "Session", onSelect: () => { route.navigate({ type: "home", }) dialog.clear() }, }, { title: "Switch model", value: "model.list", keybind: "model_list", category: "Agent", onSelect: () => { dialog.replace(() => ) }, }, { title: "Switch agent", value: "agent.list", keybind: "agent_list", category: "Agent", onSelect: () => { dialog.replace(() => ) }, }, { title: "Agent cycle", value: "agent.cycle", keybind: "agent_cycle", category: "Agent", disabled: true, onSelect: () => { local.agent.move(1) }, }, { title: "Agent cycle reverse", value: "agent.cycle.reverse", keybind: "agent_cycle_reverse", category: "Agent", disabled: true, onSelect: () => { local.agent.move(-1) }, }, { title: "View status", keybind: "status_view", value: "opencode.status", onSelect: () => { dialog.replace(() => ) }, category: "System", }, { title: "Switch theme", value: "theme.switch", onSelect: () => { dialog.replace(() => ) }, category: "System", }, { title: "Help", value: "help.show", onSelect: () => { dialog.replace(() => ) }, category: "System", }, ]) createEffect(() => { const providerID = local.model.current().providerID if (providerID === "openrouter" && !kv.data.openrouter_warning) { untrack(() => { DialogAlert.show( dialog, "Warning", "While openrouter is a convenient way to access LLMs your request will often be routed to subpar providers that do not work well in our testing.\n\nFor reliable access to models check out OpenCode Zen\nhttps://opencode.ai/zen", ).then(() => kv.set("openrouter_warning", true)) }) } }) event.on(TuiEvent.CommandExecute.type, (evt) => { command.trigger(evt.properties.command) }) event.on(TuiEvent.ToastShow.type, (evt) => { toast.show({ title: evt.properties.title, message: evt.properties.message, variant: evt.properties.variant, duration: evt.properties.duration, }) }) event.on(SessionApi.Event.Deleted.type, (evt) => { if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) { route.navigate({ type: "home" }) toast.show({ variant: "info", message: "The current session was deleted", }) } }) return ( { const text = renderer.getSelection()?.getSelectedText() if (text && text.length > 0) { const base64 = Buffer.from(text).toString("base64") const osc52 = `\x1b]52;c;${base64}\x07` const finalOsc52 = process.env["TMUX"] ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52 /* @ts-expect-error */ renderer.writeOut(finalOsc52) await Clipboard.copy(text) renderer.clearSelection() toast.show({ message: "Copied to clipboard", variant: "info" }) } }} > open code v{Installation.VERSION} {process.cwd().replace(Global.Path.home, "~")} tab {""} {local.agent.current().name.toUpperCase()} AGENT ) } function ErrorComponent(props: { error: Error; reset: () => void, onExit: () => Promise }) { const term = useTerminalDimensions() useKeyboard((evt) => { if (evt.ctrl && evt.name === "c") { props.onExit() } }) const [copied, setCopied] = createSignal(false) const issueURL = new URL("https://github.com/sst/opencode/issues/new?template=bug-report.yml") if (props.error.message) { issueURL.searchParams.set("title", `opentui: fatal: ${props.error.message}`) } if (props.error.stack) { issueURL.searchParams.set("description", "```\n" + props.error.stack.substring(0, 6000 - issueURL.toString().length) + "...\n```") } const copyIssueURL = () => { Clipboard.copy(issueURL.toString()).then(() => { setCopied(true) }) } return ( Please report an issue. Copy issue URL (exception info pre-filled) {copied() && Successfully copied} A fatal error occurred! Reset TUI Exit {props.error.stack} {props.error.message} ) }