diff options
| author | Sebastian <[email protected]> | 2026-03-27 15:00:26 +0100 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-27 15:00:26 +0100 |
| commit | 6274b0677c1c65815c525b9b199f1ce5c6fb97fc (patch) | |
| tree | 399fdba174e350c2f86c2deba047ac8b17ce43bf /packages/plugin | |
| parent | d8ad8338f5311ac6692ebc362d28389e028f6aad (diff) | |
| download | opencode-6274b0677c1c65815c525b9b199f1ce5c6fb97fc.tar.gz opencode-6274b0677c1c65815c525b9b199f1ce5c6fb97fc.zip | |
tui plugins (#19347)
Diffstat (limited to 'packages/plugin')
| -rw-r--r-- | packages/plugin/package.json | 17 | ||||
| -rw-r--r-- | packages/plugin/src/index.ts | 22 | ||||
| -rw-r--r-- | packages/plugin/src/tui.ts | 419 |
3 files changed, 453 insertions, 5 deletions
diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 5004a3ee2..c0565a7a2 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -10,7 +10,8 @@ }, "exports": { ".": "./src/index.ts", - "./tool": "./src/tool.ts" + "./tool": "./src/tool.ts", + "./tui": "./src/tui.ts" }, "files": [ "dist" @@ -19,7 +20,21 @@ "@opencode-ai/sdk": "workspace:*", "zod": "catalog:" }, + "peerDependencies": { + "@opentui/core": ">=0.1.90", + "@opentui/solid": ">=0.1.90" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } + }, "devDependencies": { + "@opentui/core": "0.1.90", + "@opentui/solid": "0.1.90", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "typescript": "catalog:", diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 8bdb51a2a..d289689b9 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -9,7 +9,7 @@ import type { Message, Part, Auth, - Config, + Config as SDKConfig, } from "@opencode-ai/sdk" import type { BunShell } from "./shell.js" @@ -32,7 +32,18 @@ export type PluginInput = { $: BunShell } -export type Plugin = (input: PluginInput) => Promise<Hooks> +export type PluginOptions = Record<string, unknown> + +export type Config = Omit<SDKConfig, "plugin"> & { + plugin?: Array<string | [string, PluginOptions]> +} + +export type Plugin = (input: PluginInput, options?: PluginOptions) => Promise<Hooks> + +export type PluginModule = { + id?: string + server?: Plugin +} type Rule = { key: string @@ -72,7 +83,7 @@ export type AuthHook = { when?: Rule } > - authorize(inputs?: Record<string, string>): Promise<AuthOuathResult> + authorize(inputs?: Record<string, string>): Promise<AuthOAuthResult> } | { type: "api" @@ -116,7 +127,7 @@ export type AuthHook = { )[] } -export type AuthOuathResult = { url: string; instructions: string } & ( +export type AuthOAuthResult = { url: string; instructions: string } & ( | { method: "auto" callback(): Promise< @@ -161,6 +172,9 @@ export type AuthOuathResult = { url: string; instructions: string } & ( } ) +/** @deprecated Use AuthOAuthResult instead. */ +export type AuthOuathResult = AuthOAuthResult + export interface Hooks { event?: (input: { event: Event }) => Promise<void> config?: (input: Config) => Promise<void> diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts new file mode 100644 index 000000000..62747884f --- /dev/null +++ b/packages/plugin/src/tui.ts @@ -0,0 +1,419 @@ +import type { + OpencodeClient, + Event, + LspStatus, + McpStatus, + Todo, + Message, + Part, + Provider, + PermissionRequest, + QuestionRequest, + SessionStatus, + Workspace, + Config as SdkConfig, +} from "@opencode-ai/sdk/v2" +import type { CliRenderer, ParsedKey, RGBA } from "@opentui/core" +import type { JSX, SolidPlugin } from "@opentui/solid" +import type { Config as PluginConfig, Plugin, PluginModule, 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" | "xlarge" + onClose: () => void + children?: JSX.Element +} + +export type TuiDialogStack = { + replace: (render: () => JSX.Element, onClose?: () => void) => void + clear: () => void + setSize: (size: "medium" | "large" | "xlarge") => void + readonly size: "medium" | "large" | "xlarge" + 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 TuiThemeCurrent = { + readonly primary: RGBA + readonly secondary: RGBA + readonly accent: RGBA + readonly error: RGBA + readonly warning: RGBA + readonly success: RGBA + readonly info: RGBA + readonly text: RGBA + readonly textMuted: RGBA + readonly selectedListItemText: RGBA + readonly background: RGBA + readonly backgroundPanel: RGBA + readonly backgroundElement: RGBA + readonly backgroundMenu: RGBA + readonly border: RGBA + readonly borderActive: RGBA + readonly borderSubtle: RGBA + readonly diffAdded: RGBA + readonly diffRemoved: RGBA + readonly diffContext: RGBA + readonly diffHunkHeader: RGBA + readonly diffHighlightAdded: RGBA + readonly diffHighlightRemoved: RGBA + readonly diffAddedBg: RGBA + readonly diffRemovedBg: RGBA + readonly diffContextBg: RGBA + readonly diffLineNumber: RGBA + readonly diffAddedLineNumberBg: RGBA + readonly diffRemovedLineNumberBg: RGBA + readonly markdownText: RGBA + readonly markdownHeading: RGBA + readonly markdownLink: RGBA + readonly markdownLinkText: RGBA + readonly markdownCode: RGBA + readonly markdownBlockQuote: RGBA + readonly markdownEmph: RGBA + readonly markdownStrong: RGBA + readonly markdownHorizontalRule: RGBA + readonly markdownListItem: RGBA + readonly markdownListEnumeration: RGBA + readonly markdownImage: RGBA + readonly markdownImageText: RGBA + readonly markdownCodeBlock: RGBA + readonly syntaxComment: RGBA + readonly syntaxKeyword: RGBA + readonly syntaxFunction: RGBA + readonly syntaxVariable: RGBA + readonly syntaxString: RGBA + readonly syntaxNumber: RGBA + readonly syntaxType: RGBA + readonly syntaxOperator: RGBA + readonly syntaxPunctuation: RGBA + readonly thinkingOpacity: number +} + +export type TuiTheme = { + readonly current: TuiThemeCurrent + 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 = { + readonly ready: boolean + readonly config: SdkConfig + readonly provider: ReadonlyArray<Provider> + readonly path: { + state: string + config: string + worktree: string + directory: string + } + readonly vcs: { branch?: string } | undefined + readonly workspace: { + list: () => ReadonlyArray<Workspace> + get: (workspaceID: string) => Workspace | undefined + } + session: { + count: () => number + diff: (sessionID: string) => ReadonlyArray<TuiSidebarFileItem> + todo: (sessionID: string) => ReadonlyArray<TuiSidebarTodoItem> + messages: (sessionID: string) => ReadonlyArray<Message> + status: (sessionID: string) => SessionStatus | undefined + permission: (sessionID: string) => ReadonlyArray<PermissionRequest> + question: (sessionID: string) => ReadonlyArray<QuestionRequest> + } + part: (messageID: string) => ReadonlyArray<Part> + lsp: () => ReadonlyArray<TuiSidebarLspItem> + mcp: () => ReadonlyArray<TuiSidebarMcpItem> +} + +type TuiConfigView = Pick<PluginConfig, "$schema" | "theme" | "keybinds" | "plugin"> & + NonNullable<PluginConfig["tui"]> & { + plugin_enabled?: Record<string, boolean> + } + +export type TuiApp = { + readonly version: string +} + +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 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_bottom: {} + sidebar_title: { + session_id: string + title: string + share_url?: string + } + sidebar_content: { + session_id: string + } + sidebar_footer: { + session_id: string + } +} + +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 = { + id: 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 TuiPluginStatus = { + id: string + source: TuiPluginEntry["source"] + spec: string + target: string + enabled: boolean + active: boolean +} + +export type TuiPluginInstallOptions = { + global?: boolean +} + +export type TuiPluginInstallResult = + | { + ok: true + dir: string + tui: boolean + } + | { + ok: false + message: string + missing?: boolean + } + +export type TuiWorkspace = { + current: () => string | undefined + set: (workspaceID?: string) => void +} + +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: CliRenderer + slots: TuiSlots + plugins: { + list: () => ReadonlyArray<TuiPluginStatus> + activate: (id: string) => Promise<boolean> + deactivate: (id: string) => Promise<boolean> + add: (spec: string) => Promise<boolean> + install: (spec: string, options?: TuiPluginInstallOptions) => Promise<TuiPluginInstallResult> + } + lifecycle: TuiLifecycle +} + +export type TuiPlugin = (api: TuiPluginApi, options: PluginOptions | undefined, meta: TuiPluginMeta) => Promise<void> + +export type TuiPluginModule = PluginModule & { + tui?: TuiPlugin +} |
