Files
anomalyco_opencode/packages/plugin/src/tui.ts
T
Sebastian Herrlinger e89f5084d3 squashed
2026-03-26 21:36:58 +01:00

351 lines
8.0 KiB
TypeScript

import type { OpencodeClient, Event, LspStatus, McpStatus, Todo } from "@opencode-ai/sdk/v2"
import type { CliRenderer, ParsedKey } from "@opentui/core"
import type { JSX, SolidPlugin } from "@opentui/solid"
import type { Config, Plugin, PluginOptions } from "./index.js"
export type { CliRenderer, SlotMode } from "@opentui/core"
export type TuiRouteCurrent =
| {
name: "home"
}
| {
name: "session"
params: {
sessionID: string
initialPrompt?: unknown
}
}
| {
name: string
params?: Record<string, unknown>
}
export type TuiRouteDefinition = {
name: string
render: (input: { params?: Record<string, unknown> }) => JSX.Element
}
export type TuiCommand = {
title: string
value: string
description?: string
category?: string
keybind?: string
suggested?: boolean
hidden?: boolean
enabled?: boolean
slash?: {
name: string
aliases?: string[]
}
onSelect?: () => void
}
export type TuiKeybind = {
name: string
ctrl: boolean
meta: boolean
shift: boolean
super?: boolean
leader: boolean
}
export type TuiKeybindMap = Record<string, string>
export type TuiKeybindSet = {
readonly all: TuiKeybindMap
get: (name: string) => string
match: (name: string, evt: ParsedKey) => boolean
print: (name: string) => string
}
export type TuiDialogProps = {
size?: "medium" | "large"
onClose: () => void
children?: JSX.Element
}
export type TuiDialogStack = {
replace: (render: () => JSX.Element, onClose?: () => void) => void
clear: () => void
setSize: (size: "medium" | "large") => void
readonly size: "medium" | "large"
readonly depth: number
readonly open: boolean
}
export type TuiDialogAlertProps = {
title: string
message: string
onConfirm?: () => void
}
export type TuiDialogConfirmProps = {
title: string
message: string
onConfirm?: () => void
onCancel?: () => void
}
export type TuiDialogPromptProps = {
title: string
description?: () => JSX.Element
placeholder?: string
value?: string
onConfirm?: (value: string) => void
onCancel?: () => void
}
export type TuiDialogSelectOption<Value = unknown> = {
title: string
value: Value
description?: string
footer?: JSX.Element | string
category?: string
disabled?: boolean
onSelect?: () => void
}
export type TuiDialogSelectProps<Value = unknown> = {
title: string
placeholder?: string
options: TuiDialogSelectOption<Value>[]
flat?: boolean
onMove?: (option: TuiDialogSelectOption<Value>) => void
onFilter?: (query: string) => void
onSelect?: (option: TuiDialogSelectOption<Value>) => void
skipFilter?: boolean
current?: Value
}
export type TuiToast = {
variant?: "info" | "success" | "warning" | "error"
title?: string
message: string
duration?: number
}
export type TuiTheme = {
readonly current: Record<string, unknown>
readonly selected: string
has: (name: string) => boolean
set: (name: string) => boolean
install: (jsonPath: string) => Promise<void>
mode: () => "dark" | "light"
readonly ready: boolean
}
export type TuiKV = {
get: <Value = unknown>(key: string, fallback?: Value) => Value
set: (key: string, value: unknown) => void
readonly ready: boolean
}
export type TuiState = {
session: {
diff: (sessionID: string) => ReadonlyArray<TuiSidebarFileItem>
todo: (sessionID: string) => ReadonlyArray<TuiSidebarTodoItem>
}
lsp: () => ReadonlyArray<TuiSidebarLspItem>
mcp: () => ReadonlyArray<TuiSidebarMcpItem>
}
type TuiConfigView = Pick<Config, "$schema" | "theme" | "keybinds" | "plugin"> & NonNullable<Config["tui"]>
type Frozen<Value> = Value extends (...args: never[]) => unknown
? Value
: Value extends ReadonlyArray<infer Item>
? ReadonlyArray<Frozen<Item>>
: Value extends object
? { readonly [Key in keyof Value]: Frozen<Value[Key]> }
: Value
export type TuiApi = {
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"]
error?: string
}
export type TuiSidebarLspItem = Pick<LspStatus, "id" | "root" | "status">
export type TuiSidebarTodoItem = Pick<Todo, "content" | "status">
export type TuiSidebarFileItem = {
file: string
additions: number
deletions: number
}
export type TuiSlotMap = {
app: {}
home_logo: {}
home_tips: {
show_tips: boolean
tips_hidden: boolean
first_time_user: boolean
}
home_below_tips: {
show_tips: boolean
tips_hidden: boolean
first_time_user: boolean
}
sidebar_top: {
session_id: string
}
sidebar_title: {
session_id: string
title: string
share_url?: string
}
sidebar_context: {
session_id: string
tokens: number
percentage: number | null
cost: number
}
sidebar_mcp: {
session_id: string
items: TuiSidebarMcpItem[]
connected: number
errors: number
}
sidebar_lsp: {
session_id: string
items: TuiSidebarLspItem[]
disabled: boolean
}
sidebar_todo: {
session_id: string
items: TuiSidebarTodoItem[]
}
sidebar_files: {
session_id: string
items: TuiSidebarFileItem[]
}
sidebar_getting_started: {
session_id: string
show_getting_started: boolean
has_providers: boolean
dismissed: boolean
}
sidebar_directory: {
session_id: string
directory: string
directory_parent: string
directory_name: string
}
sidebar_version: {
session_id: string
version: string
}
sidebar_bottom: {
session_id: string
directory: string
directory_parent: string
directory_name: string
version: string
show_getting_started: boolean
has_providers: boolean
dismissed: boolean
}
}
export type TuiSlotContext = {
theme: TuiTheme
}
type SlotCore = SolidPlugin<TuiSlotMap, TuiSlotContext>
export type TuiSlotPlugin = Omit<SlotCore, "id"> & {
id?: never
}
export type TuiSlots = {
register: (plugin: TuiSlotPlugin) => string
}
export type TuiEventBus = {
on: <Type extends Event["type"]>(type: Type, handler: (event: Extract<Event, { type: Type }>) => void) => () => void
}
export type TuiDispose = () => void | Promise<void>
export type TuiLifecycle = {
readonly signal: AbortSignal
onDispose: (fn: TuiDispose) => () => void
}
export type TuiPluginState = "first" | "updated" | "same"
export type TuiPluginEntry = {
name: string
source: "file" | "npm" | "internal"
spec: string
target: string
requested?: string
version?: string
modified?: number
first_time: number
last_time: number
time_changed: number
load_count: number
fingerprint: string
}
export type TuiPluginMeta = TuiPluginEntry & {
state: TuiPluginState
}
export type TuiHostPluginApi<Renderer = CliRenderer> = TuiApi & {
client: OpencodeClient
event: TuiEventBus
renderer: Renderer
}
export type TuiPluginApi<Renderer = CliRenderer> = TuiHostPluginApi<Renderer> & {
slots: TuiSlots
lifecycle: TuiLifecycle
}
export type TuiPlugin<Renderer = CliRenderer> = (
api: TuiPluginApi<Renderer>,
options: PluginOptions | undefined,
meta: TuiPluginMeta,
) => Promise<void>
export type TuiPluginModule<Renderer = CliRenderer> = {
server?: Plugin
tui?: TuiPlugin<Renderer>
}