tui plugins (#19347)
This commit is contained in:
@@ -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:",
|
||||
|
||||
@@ -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>
|
||||
|
||||
419
packages/plugin/src/tui.ts
Normal file
419
packages/plugin/src/tui.ts
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user