import type { TuiDialogSelectOption, TuiPluginApi, TuiRouteDefinition, TuiSlotProps } from "@opencode-ai/plugin/tui" import type { useEvent } from "@tui/context/event" import type { useRoute } from "@tui/context/route" import type { useSDK } from "@tui/context/sdk" import type { useSync } from "@tui/context/sync" import type { useTheme } from "@tui/context/theme" import { Dialog as DialogUI, type useDialog } from "@tui/ui/dialog" import type { TuiConfig } from "@/cli/cmd/tui/config/tui" import type { useOpencodeKeymap } from "../keymap" import type { useKV } from "../context/kv" import { DialogAlert } from "../ui/dialog-alert" import { DialogConfirm } from "../ui/dialog-confirm" import { DialogPrompt } from "../ui/dialog-prompt" import { DialogSelect, type DialogSelectOption as SelectOption } from "../ui/dialog-select" import { Prompt } from "../component/prompt" import { Slot as HostSlot } from "./slots" import type { useToast } from "../ui/toast" import { InstallationVersion } from "@opencode-ai/core/installation/version" import * as Keymap from "../keymap" import { createCommandShim } from "./command-shim" type RouteEntry = { key: symbol render: TuiRouteDefinition["render"] } export type RouteMap = Map type Input = { tuiConfig: TuiConfig.Resolved dialog: ReturnType keymap: ReturnType kv: ReturnType route: ReturnType routes: RouteMap bump: () => void event: ReturnType sdk: ReturnType sync: ReturnType theme: ReturnType toast: ReturnType renderer: TuiPluginApi["renderer"] } function routeRegister(routes: RouteMap, list: TuiRouteDefinition[], bump: () => void) { const key = Symbol() for (const item of list) { const prev = routes.get(item.name) ?? [] prev.push({ key, render: item.render }) routes.set(item.name, prev) } bump() return () => { for (const item of list) { const prev = routes.get(item.name) if (!prev) continue const next = prev.filter((x) => x.key !== key) if (!next.length) { routes.delete(item.name) continue } routes.set(item.name, next) } bump() } } function routeNavigate(route: ReturnType, name: string, params?: Record) { if (name === "home") { route.navigate({ type: "home" }) return } if (name === "session") { const sessionID = params?.sessionID if (typeof sessionID !== "string") return route.navigate({ type: "session", sessionID }) return } route.navigate({ type: "plugin", id: name, data: params }) } function routeCurrent(route: ReturnType): TuiPluginApi["route"]["current"] { if (route.data.type === "home") return { name: "home" } if (route.data.type === "session") { return { name: "session", params: { sessionID: route.data.sessionID, prompt: route.data.prompt, }, } } return { name: route.data.id, params: route.data.data, } } function mapOption(item: TuiDialogSelectOption): SelectOption { return { ...item, onSelect: () => item.onSelect?.(), } } function pickOption(item: SelectOption): TuiDialogSelectOption { return { title: item.title, value: item.value, description: item.description, footer: item.footer, category: item.category, disabled: item.disabled, } } function mapOptionCb(cb?: (item: TuiDialogSelectOption) => void) { if (!cb) return return (item: SelectOption) => cb(pickOption(item)) } function stateApi(sync: ReturnType): TuiPluginApi["state"] { return { get ready() { return sync.ready }, get config() { return sync.data.config }, get provider() { return sync.data.provider }, get path() { return sync.path }, get vcs() { if (!sync.data.vcs) return return { branch: sync.data.vcs.branch, } }, session: { count() { return sync.data.session.length }, get(sessionID) { return sync.session.get(sessionID) }, diff(sessionID) { return (sync.data.session_diff[sessionID] ?? []).flatMap((item) => item.file === undefined ? [] : [{ ...item, file: item.file }], ) }, todo(sessionID) { return sync.data.todo[sessionID] ?? [] }, messages(sessionID) { return sync.data.message[sessionID] ?? [] }, status(sessionID) { return sync.data.session_status[sessionID] }, permission(sessionID) { return sync.data.permission[sessionID] ?? [] }, question(sessionID) { return sync.data.question[sessionID] ?? [] }, }, part(messageID) { return sync.data.part[messageID] ?? [] }, lsp() { return sync.data.lsp.map((item) => ({ id: item.id, root: item.root, status: item.status })) }, mcp() { return Object.entries(sync.data.mcp) .sort(([a], [b]) => a.localeCompare(b)) .map(([name, item]) => ({ name, status: item.status, error: item.status === "failed" ? item.error : undefined, })) }, } } function appApi(): TuiPluginApi["app"] { return { get version() { return InstallationVersion }, } } export function createTuiApi(input: Input): TuiPluginApi { const lifecycle: TuiPluginApi["lifecycle"] = { signal: new AbortController().signal, onDispose() { return () => {} }, } return { app: appApi(), // Keep deprecated `api.command` working for v1 plugins; remove in v2. command: createCommandShim(input.keymap, input.dialog, input.tuiConfig.keybinds), keys: { formatSequence(parts) { return Keymap.formatKeySequence(parts, input.tuiConfig) }, formatBindings(bindings) { return Keymap.formatKeyBindings(bindings, input.tuiConfig) }, }, keymap: input.keymap, route: { register(list) { return routeRegister(input.routes, list, input.bump) }, navigate(name, params) { routeNavigate(input.route, name, params) }, get current() { return routeCurrent(input.route) }, }, ui: { Dialog(props) { return ( {props.children} ) }, DialogAlert(props) { return }, DialogConfirm(props) { return }, DialogPrompt(props) { return }, DialogSelect(props) { return ( ) }, Slot(props: TuiSlotProps) { return }, Prompt(props) { return ( ) }, toast(inputToast) { input.toast.show({ title: inputToast.title, message: inputToast.message, variant: inputToast.variant ?? "info", duration: inputToast.duration, }) }, dialog: { replace(render, onClose) { input.dialog.replace(render, onClose) }, clear() { input.dialog.clear() }, setSize(size) { input.dialog.setSize(size) }, get size() { return input.dialog.size }, get depth() { return input.dialog.stack.length }, get open() { return input.dialog.stack.length > 0 }, }, }, get tuiConfig() { return input.tuiConfig }, kv: { get(key, fallback) { return input.kv.get(key, fallback) }, set(key, value) { input.kv.set(key, value) }, get ready() { return input.kv.ready }, }, state: stateApi(input.sync), get client() { return input.sdk.client }, event: input.event, renderer: input.renderer, slots: { register() { throw new Error("slots.register is only available in plugin context") }, }, plugins: { list() { return [] }, async activate() { return false }, async deactivate() { return false }, async add() { return false }, async install() { return { ok: false, message: "plugins.install is only available in plugin context", } }, }, lifecycle, theme: { get current() { return input.theme.theme }, get selected() { return input.theme.selected }, has(name) { return input.theme.has(name) }, set(name) { return input.theme.set(name) }, async install(_jsonPath) { throw new Error("theme.install is only available in plugin context") }, mode() { return input.theme.mode() }, get ready() { return input.theme.ready }, }, } }