collapse tui api types

This commit is contained in:
Sebastian Herrlinger
2026-03-25 20:20:23 +01:00
parent c0a4755d15
commit 1e711cda3f
6 changed files with 89 additions and 71 deletions
+5 -10
View File
@@ -59,7 +59,7 @@ import { PromptRefProvider, usePromptRef } from "./context/prompt"
import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
import { TuiConfig } from "@/config/tui"
import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin"
import type { TuiHostPluginApi } from "@opencode-ai/plugin/tui"
import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
import { FormatError, FormatUnknownError } from "@/cli/error"
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
@@ -278,7 +278,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
map.set(key, next)
return next
}
const workspace: TuiHostPluginApi["workspace"] = {
const workspace: TuiPluginApi["workspace"] = {
current() {
return sdk.workspaceID
},
@@ -310,19 +310,14 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
sync,
theme: themeState,
toast,
})
const host: TuiHostPluginApi = {
...api,
get client() {
return sdk.client
},
client: () => sdk.client,
scopedClient: scoped,
workspace,
event: sdk.event,
renderer,
}
})
const [ready, setReady] = createSignal(false)
TuiPluginRuntime.init(host)
TuiPluginRuntime.init(api)
.catch((error) => {
console.error("Failed to load TUI plugins", error)
})
@@ -1,5 +1,5 @@
import type { ParsedKey } from "@opentui/core"
import type { TuiApi, TuiDialogSelectOption, TuiRouteDefinition } from "@opencode-ai/plugin/tui"
import type { TuiDialogSelectOption, TuiPluginApi, TuiRouteDefinition } from "@opencode-ai/plugin/tui"
import type { useCommandDialog } from "@tui/component/dialog-command"
import type { useKeybind } from "@tui/context/keybind"
import type { useRoute } from "@tui/context/route"
@@ -35,6 +35,11 @@ type Input = {
sync: ReturnType<typeof useSync>
theme: ReturnType<typeof useTheme>
toast: ReturnType<typeof useToast>
client: () => TuiPluginApi["client"]
scopedClient: TuiPluginApi["scopedClient"]
workspace: TuiPluginApi["workspace"]
event: TuiPluginApi["event"]
renderer: TuiPluginApi["renderer"]
}
function routeRegister(routes: RouteMap, list: TuiRouteDefinition[], bump: () => void) {
@@ -77,7 +82,7 @@ function routeNavigate(route: ReturnType<typeof useRoute>, name: string, params?
route.navigate({ type: "plugin", id: name, data: params })
}
function routeCurrent(route: ReturnType<typeof useRoute>): TuiApi["route"]["current"] {
function routeCurrent(route: ReturnType<typeof useRoute>): TuiPluginApi["route"]["current"] {
if (route.data.type === "home") return { name: "home" }
if (route.data.type === "session") {
return {
@@ -118,7 +123,7 @@ function mapOptionCb<Value>(cb?: (item: TuiDialogSelectOption<Value>) => void) {
return (item: SelectOption<Value>) => cb(pickOption(item))
}
function stateApi(sync: ReturnType<typeof useSync>): TuiApi["state"] {
function stateApi(sync: ReturnType<typeof useSync>): TuiPluginApi["state"] {
return {
get ready() {
return sync.ready
@@ -187,7 +192,7 @@ function stateApi(sync: ReturnType<typeof useSync>): TuiApi["state"] {
}
}
function appApi(): TuiApi["app"] {
function appApi(): TuiPluginApi["app"] {
return {
get version() {
return Installation.VERSION
@@ -195,7 +200,14 @@ function appApi(): TuiApi["app"] {
}
}
export function createTuiApi(input: Input): TuiApi {
export function createTuiApi(input: Input): TuiPluginApi {
const lifecycle: TuiPluginApi["lifecycle"] = {
signal: new AbortController().signal,
onDispose() {
return () => {}
},
}
return {
app: appApi(),
command: {
@@ -304,6 +316,19 @@ export function createTuiApi(input: Input): TuiApi {
},
},
state: stateApi(input.sync),
get client() {
return input.client()
},
scopedClient: input.scopedClient,
workspace: input.workspace,
event: input.event,
renderer: input.renderer,
slots: {
register() {
throw new Error("slots.register is only available in plugin context")
},
},
lifecycle,
theme: {
get current() {
return input.theme.theme
@@ -7,7 +7,6 @@ import {
type TuiSlotPlugin,
type TuiTheme,
} from "@opencode-ai/plugin/tui"
import type { CliRenderer } from "@opentui/core"
import path from "path"
import { fileURLToPath } from "url"
@@ -42,7 +41,7 @@ type Deps = {
type Api = HostPluginApi
type Scope = {
lifecycle: TuiPluginApi<CliRenderer>["lifecycle"]
lifecycle: TuiPluginApi["lifecycle"]
wrap: (fn: (() => void) | undefined) => () => void
dispose: () => Promise<void>
}
@@ -87,7 +86,7 @@ function runCleanup(fn: () => unknown, ms: number): Promise<CleanupResult> {
})
}
function isTuiPlugin(value: unknown): value is TuiPlugin<CliRenderer> {
function isTuiPlugin(value: unknown): value is TuiPlugin {
return typeof value === "function"
}
@@ -310,7 +309,7 @@ function scope(load: Loaded, name: string) {
}
}
const lifecycle: TuiPluginApi<CliRenderer>["lifecycle"] = {
const lifecycle: TuiPluginApi["lifecycle"] = {
signal: ctrl.signal,
onDispose,
}
@@ -373,8 +372,8 @@ function plug(plugin: TuiSlotPlugin, id: string): HostSlotPlugin {
}
}
function pluginApi(api: Api, host: HostSlots, load: Loaded, state: Scope, base: string): TuiPluginApi<CliRenderer> {
const command: TuiPluginApi<CliRenderer>["command"] = {
function pluginApi(api: Api, host: HostSlots, load: Loaded, state: Scope, base: string): TuiPluginApi {
const command: TuiPluginApi["command"] = {
register(cb) {
return state.wrap(api.command.register(cb))
},
@@ -383,7 +382,7 @@ function pluginApi(api: Api, host: HostSlots, load: Loaded, state: Scope, base:
},
}
const route: TuiPluginApi<CliRenderer>["route"] = {
const route: TuiPluginApi["route"] = {
register(list) {
return state.wrap(api.route.register(list))
},
@@ -395,11 +394,11 @@ function pluginApi(api: Api, host: HostSlots, load: Loaded, state: Scope, base:
},
}
const theme: TuiPluginApi<CliRenderer>["theme"] = Object.assign(Object.create(api.theme), {
const theme: TuiPluginApi["theme"] = Object.assign(Object.create(api.theme), {
install: load.install,
})
const event: TuiPluginApi<CliRenderer>["event"] = {
const event: TuiPluginApi["event"] = {
on(type, handler) {
return state.wrap(api.event.on(type, handler))
},
@@ -407,7 +406,7 @@ function pluginApi(api: Api, host: HostSlots, load: Loaded, state: Scope, base:
let count = 0
const slots: TuiPluginApi<CliRenderer>["slots"] = {
const slots: TuiPluginApi["slots"] = {
register(plugin) {
const id = count ? `${base}:${count}` : base
count += 1
@@ -1,5 +1,4 @@
import type { CliRenderer } from "@opentui/core"
import { type SlotMode, type TuiHostPluginApi, type TuiSlotContext, type TuiSlotMap } from "@opencode-ai/plugin/tui"
import { type SlotMode, type TuiPluginApi, type TuiSlotContext, type TuiSlotMap } from "@opencode-ai/plugin/tui"
import { createSlot, createSolidSlotRegistry, type JSX, type SolidPlugin } from "@opentui/solid"
import { isRecord } from "@/util/record"
@@ -12,7 +11,7 @@ type SlotProps<K extends keyof TuiSlotMap> = {
type Slot = <K extends keyof TuiSlotMap>(props: SlotProps<K>) => JSX.Element | null
export type HostSlotPlugin = SolidPlugin<TuiSlotMap, TuiSlotContext>
export type HostPluginApi = TuiHostPluginApi<CliRenderer>
export type HostPluginApi = TuiPluginApi
export type HostSlots = {
register: (plugin: HostSlotPlugin) => () => void
}
@@ -115,6 +115,7 @@ type Opts = {
export function createTuiPluginApi(opts: Opts = {}): HostPluginApi {
const kv: Record<string, unknown> = {}
const count = opts.count
const ctrl = new AbortController()
const own = createOpencodeClient({
baseUrl: "http://localhost:4096",
})
@@ -182,6 +183,15 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi {
},
},
renderer,
slots: {
register: () => "fixture-slot",
},
lifecycle: {
signal: ctrl.signal,
onDispose() {
return () => {}
},
},
command: {
register: () => {
if (count) count.command_add += 1
+33 -43
View File
@@ -255,37 +255,6 @@ type Frozen<Value> = Value extends (...args: never[]) => unknown
? { readonly [Key in keyof Value]: Frozen<Value[Key]> }
: Value
export type TuiApi = {
app: TuiApp
command: {
register: (cb: () => TuiCommand[]) => () => void
trigger: (value: string) => void
}
route: {
register: (routes: TuiRouteDefinition[]) => () => void
navigate: (name: string, params?: Record<string, unknown>) => void
readonly current: TuiRouteCurrent
}
ui: {
Dialog: (props: TuiDialogProps) => JSX.Element
DialogAlert: (props: TuiDialogAlertProps) => JSX.Element
DialogConfirm: (props: TuiDialogConfirmProps) => JSX.Element
DialogPrompt: (props: TuiDialogPromptProps) => JSX.Element
DialogSelect: <Value = unknown>(props: TuiDialogSelectProps<Value>) => JSX.Element
toast: (input: TuiToast) => void
dialog: TuiDialogStack
}
keybind: {
match: (key: string, evt: ParsedKey) => boolean
print: (key: string) => string
create: (defaults: TuiKeybindMap, overrides?: Record<string, unknown>) => TuiKeybindSet
}
readonly tuiConfig: Frozen<TuiConfigView>
kv: TuiKV
state: TuiState
theme: TuiTheme
}
export type TuiSidebarMcpItem = {
name: string
status: McpStatus["status"]
@@ -370,26 +339,47 @@ export type TuiWorkspace = {
set: (workspaceID?: string) => void
}
export type TuiHostPluginApi<Renderer = CliRenderer> = TuiApi & {
export type TuiPluginApi = {
app: TuiApp
command: {
register: (cb: () => TuiCommand[]) => () => void
trigger: (value: string) => void
}
route: {
register: (routes: TuiRouteDefinition[]) => () => void
navigate: (name: string, params?: Record<string, unknown>) => void
readonly current: TuiRouteCurrent
}
ui: {
Dialog: (props: TuiDialogProps) => JSX.Element
DialogAlert: (props: TuiDialogAlertProps) => JSX.Element
DialogConfirm: (props: TuiDialogConfirmProps) => JSX.Element
DialogPrompt: (props: TuiDialogPromptProps) => JSX.Element
DialogSelect: <Value = unknown>(props: TuiDialogSelectProps<Value>) => JSX.Element
toast: (input: TuiToast) => void
dialog: TuiDialogStack
}
keybind: {
match: (key: string, evt: ParsedKey) => boolean
print: (key: string) => string
create: (defaults: TuiKeybindMap, overrides?: Record<string, unknown>) => TuiKeybindSet
}
readonly tuiConfig: Frozen<TuiConfigView>
kv: TuiKV
state: TuiState
theme: TuiTheme
client: OpencodeClient
scopedClient: (workspaceID?: string) => OpencodeClient
workspace: TuiWorkspace
event: TuiEventBus
renderer: Renderer
}
export type TuiPluginApi<Renderer = CliRenderer> = TuiHostPluginApi<Renderer> & {
renderer: CliRenderer
slots: TuiSlots
lifecycle: TuiLifecycle
}
export type TuiPlugin<Renderer = CliRenderer> = (
api: TuiPluginApi<Renderer>,
options: PluginOptions | undefined,
meta: TuiPluginMeta,
) => Promise<void>
export type TuiPlugin = (api: TuiPluginApi, options: PluginOptions | undefined, meta: TuiPluginMeta) => Promise<void>
export type TuiPluginModule<Renderer = CliRenderer> = {
export type TuiPluginModule = {
server?: Plugin
tui?: TuiPlugin<Renderer>
tui?: TuiPlugin
}