import type { ParsedKey } from "@opentui/core" import type { TuiDialogSelectOption, TuiPluginApi, TuiRouteDefinition, TuiSlotProps } from "@opencode-ai/plugin/tui" import type { useCommandDialog } from "@tui/component/dialog-command" import type { useEvent } from "@tui/context/event" import type { useKeybind } from "@tui/context/keybind" 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 "@/config" import { createPluginKeybind } from "../context/plugin-keybinds" 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 { Installation } from "@/installation" type RouteEntry = { key: symbol render: TuiRouteDefinition["render"] } export type RouteMap = Map type Input = { command: ReturnType tuiConfig: TuiConfig.Info dialog: ReturnType keybind: 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, initialPrompt: route.data.initialPrompt, }, } } 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 }, diff(sessionID) { return sync.data.session_diff[sessionID] ?? [] }, 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 Installation.VERSION }, } } export function createTuiApi(input: Input): TuiPluginApi { const lifecycle: TuiPluginApi["lifecycle"] = { signal: new AbortController().signal, onDispose() { return () => {} }, } return { app: appApi(), command: { register(cb) { return input.command.register(() => cb()) }, trigger(value) { input.command.trigger(value) }, show() { input.command.show() }, }, 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 }, }, }, keybind: { match(key, evt: ParsedKey) { return input.keybind.match(key, evt) }, print(key) { return input.keybind.print(key) }, create(defaults, overrides) { return createPluginKeybind(input.keybind, defaults, overrides) }, }, 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 }, }, } }