mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-30 01:36:47 +00:00
tui plugins (#19347)
This commit is contained in:
334
packages/opencode/test/fixture/tui-plugin.ts
Normal file
334
packages/opencode/test/fixture/tui-plugin.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import { RGBA, type CliRenderer } from "@opentui/core"
|
||||
import { createPluginKeybind } from "../../src/cli/cmd/tui/context/plugin-keybinds"
|
||||
import type { HostPluginApi } from "../../src/cli/cmd/tui/plugin/slots"
|
||||
|
||||
type Count = {
|
||||
event_add: number
|
||||
event_drop: number
|
||||
route_add: number
|
||||
route_drop: number
|
||||
command_add: number
|
||||
command_drop: number
|
||||
}
|
||||
|
||||
function themeCurrent(): HostPluginApi["theme"]["current"] {
|
||||
const a = RGBA.fromInts(0, 120, 240)
|
||||
const b = RGBA.fromInts(120, 120, 120)
|
||||
const c = RGBA.fromInts(230, 230, 230)
|
||||
const d = RGBA.fromInts(120, 30, 30)
|
||||
const e = RGBA.fromInts(140, 100, 40)
|
||||
const f = RGBA.fromInts(20, 140, 80)
|
||||
const g = RGBA.fromInts(20, 80, 160)
|
||||
const h = RGBA.fromInts(40, 40, 40)
|
||||
const i = RGBA.fromInts(60, 60, 60)
|
||||
const j = RGBA.fromInts(80, 80, 80)
|
||||
return {
|
||||
primary: a,
|
||||
secondary: b,
|
||||
accent: a,
|
||||
error: d,
|
||||
warning: e,
|
||||
success: f,
|
||||
info: g,
|
||||
text: c,
|
||||
textMuted: b,
|
||||
selectedListItemText: h,
|
||||
background: h,
|
||||
backgroundPanel: h,
|
||||
backgroundElement: i,
|
||||
backgroundMenu: i,
|
||||
border: j,
|
||||
borderActive: c,
|
||||
borderSubtle: i,
|
||||
diffAdded: f,
|
||||
diffRemoved: d,
|
||||
diffContext: b,
|
||||
diffHunkHeader: b,
|
||||
diffHighlightAdded: f,
|
||||
diffHighlightRemoved: d,
|
||||
diffAddedBg: h,
|
||||
diffRemovedBg: h,
|
||||
diffContextBg: h,
|
||||
diffLineNumber: b,
|
||||
diffAddedLineNumberBg: h,
|
||||
diffRemovedLineNumberBg: h,
|
||||
markdownText: c,
|
||||
markdownHeading: c,
|
||||
markdownLink: a,
|
||||
markdownLinkText: g,
|
||||
markdownCode: f,
|
||||
markdownBlockQuote: e,
|
||||
markdownEmph: e,
|
||||
markdownStrong: c,
|
||||
markdownHorizontalRule: b,
|
||||
markdownListItem: a,
|
||||
markdownListEnumeration: g,
|
||||
markdownImage: a,
|
||||
markdownImageText: g,
|
||||
markdownCodeBlock: c,
|
||||
syntaxComment: b,
|
||||
syntaxKeyword: a,
|
||||
syntaxFunction: g,
|
||||
syntaxVariable: c,
|
||||
syntaxString: f,
|
||||
syntaxNumber: e,
|
||||
syntaxType: a,
|
||||
syntaxOperator: a,
|
||||
syntaxPunctuation: c,
|
||||
thinkingOpacity: 0.6,
|
||||
}
|
||||
}
|
||||
|
||||
type Opts = {
|
||||
client?: HostPluginApi["client"] | (() => HostPluginApi["client"])
|
||||
scopedClient?: HostPluginApi["scopedClient"]
|
||||
workspace?: Partial<HostPluginApi["workspace"]>
|
||||
renderer?: HostPluginApi["renderer"]
|
||||
count?: Count
|
||||
keybind?: Partial<HostPluginApi["keybind"]>
|
||||
tuiConfig?: HostPluginApi["tuiConfig"]
|
||||
app?: Partial<HostPluginApi["app"]>
|
||||
state?: {
|
||||
ready?: HostPluginApi["state"]["ready"]
|
||||
config?: HostPluginApi["state"]["config"]
|
||||
provider?: HostPluginApi["state"]["provider"]
|
||||
path?: HostPluginApi["state"]["path"]
|
||||
vcs?: HostPluginApi["state"]["vcs"]
|
||||
workspace?: Partial<HostPluginApi["state"]["workspace"]>
|
||||
session?: Partial<HostPluginApi["state"]["session"]>
|
||||
part?: HostPluginApi["state"]["part"]
|
||||
lsp?: HostPluginApi["state"]["lsp"]
|
||||
mcp?: HostPluginApi["state"]["mcp"]
|
||||
}
|
||||
theme?: {
|
||||
selected?: string
|
||||
has?: HostPluginApi["theme"]["has"]
|
||||
set?: HostPluginApi["theme"]["set"]
|
||||
install?: HostPluginApi["theme"]["install"]
|
||||
mode?: HostPluginApi["theme"]["mode"]
|
||||
ready?: boolean
|
||||
current?: HostPluginApi["theme"]["current"]
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
})
|
||||
const fallback = () => own
|
||||
const read =
|
||||
typeof opts.client === "function"
|
||||
? opts.client
|
||||
: opts.client
|
||||
? () => opts.client as HostPluginApi["client"]
|
||||
: fallback
|
||||
const client = () => read()
|
||||
const scopedClient = opts.scopedClient ?? ((_workspaceID?: string) => client())
|
||||
const workspace: HostPluginApi["workspace"] = {
|
||||
current: opts.workspace?.current ?? (() => undefined),
|
||||
set: opts.workspace?.set ?? (() => {}),
|
||||
}
|
||||
let depth = 0
|
||||
let size: "medium" | "large" | "xlarge" = "medium"
|
||||
const has = opts.theme?.has ?? (() => false)
|
||||
let selected = opts.theme?.selected ?? "opencode"
|
||||
const key = {
|
||||
match: opts.keybind?.match ?? (() => false),
|
||||
print: opts.keybind?.print ?? ((name: string) => name),
|
||||
}
|
||||
const set =
|
||||
opts.theme?.set ??
|
||||
((name: string) => {
|
||||
if (!has(name)) return false
|
||||
selected = name
|
||||
return true
|
||||
})
|
||||
const renderer: CliRenderer = opts.renderer ?? {
|
||||
...Object.create(null),
|
||||
once(this: CliRenderer) {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
function kvGet(name: string): unknown
|
||||
function kvGet<Value>(name: string, fallback: Value): Value
|
||||
function kvGet(name: string, fallback?: unknown) {
|
||||
const value = kv[name]
|
||||
if (value === undefined) return fallback
|
||||
return value
|
||||
}
|
||||
|
||||
return {
|
||||
app: {
|
||||
get version() {
|
||||
return opts.app?.version ?? "0.0.0-test"
|
||||
},
|
||||
},
|
||||
get client() {
|
||||
return client()
|
||||
},
|
||||
scopedClient,
|
||||
workspace,
|
||||
event: {
|
||||
on: () => {
|
||||
if (count) count.event_add += 1
|
||||
return () => {
|
||||
if (!count) return
|
||||
count.event_drop += 1
|
||||
}
|
||||
},
|
||||
},
|
||||
renderer,
|
||||
slots: {
|
||||
register: () => "fixture-slot",
|
||||
},
|
||||
plugins: {
|
||||
list: () => [],
|
||||
activate: async () => false,
|
||||
deactivate: async () => false,
|
||||
add: async () => false,
|
||||
install: async () => ({
|
||||
ok: false,
|
||||
message: "not implemented in fixture",
|
||||
}),
|
||||
},
|
||||
lifecycle: {
|
||||
signal: ctrl.signal,
|
||||
onDispose() {
|
||||
return () => {}
|
||||
},
|
||||
},
|
||||
command: {
|
||||
register: () => {
|
||||
if (count) count.command_add += 1
|
||||
return () => {
|
||||
if (!count) return
|
||||
count.command_drop += 1
|
||||
}
|
||||
},
|
||||
trigger: () => {},
|
||||
},
|
||||
route: {
|
||||
register: () => {
|
||||
if (count) count.route_add += 1
|
||||
return () => {
|
||||
if (!count) return
|
||||
count.route_drop += 1
|
||||
}
|
||||
},
|
||||
navigate: () => {},
|
||||
get current() {
|
||||
return { name: "home" }
|
||||
},
|
||||
},
|
||||
ui: {
|
||||
Dialog: () => null,
|
||||
DialogAlert: () => null,
|
||||
DialogConfirm: () => null,
|
||||
DialogPrompt: () => null,
|
||||
DialogSelect: () => null,
|
||||
toast: () => {},
|
||||
dialog: {
|
||||
replace: () => {
|
||||
depth = 1
|
||||
},
|
||||
clear: () => {
|
||||
depth = 0
|
||||
size = "medium"
|
||||
},
|
||||
setSize: (next) => {
|
||||
size = next
|
||||
},
|
||||
get size() {
|
||||
return size
|
||||
},
|
||||
get depth() {
|
||||
return depth
|
||||
},
|
||||
get open() {
|
||||
return depth > 0
|
||||
},
|
||||
},
|
||||
},
|
||||
keybind: {
|
||||
...key,
|
||||
create:
|
||||
opts.keybind?.create ??
|
||||
((defaults, over) => {
|
||||
return createPluginKeybind(key, defaults, over)
|
||||
}),
|
||||
},
|
||||
tuiConfig: opts.tuiConfig ?? {},
|
||||
kv: {
|
||||
get: kvGet,
|
||||
set(name, value) {
|
||||
kv[name] = value
|
||||
},
|
||||
get ready() {
|
||||
return true
|
||||
},
|
||||
},
|
||||
state: {
|
||||
get ready() {
|
||||
return opts.state?.ready ?? true
|
||||
},
|
||||
get config() {
|
||||
return opts.state?.config ?? {}
|
||||
},
|
||||
get provider() {
|
||||
return opts.state?.provider ?? []
|
||||
},
|
||||
get path() {
|
||||
return opts.state?.path ?? { state: "", config: "", worktree: "", directory: "" }
|
||||
},
|
||||
get vcs() {
|
||||
return opts.state?.vcs
|
||||
},
|
||||
workspace: {
|
||||
list: opts.state?.workspace?.list ?? (() => []),
|
||||
get: opts.state?.workspace?.get ?? (() => undefined),
|
||||
},
|
||||
session: {
|
||||
count: opts.state?.session?.count ?? (() => 0),
|
||||
diff: opts.state?.session?.diff ?? (() => []),
|
||||
todo: opts.state?.session?.todo ?? (() => []),
|
||||
messages: opts.state?.session?.messages ?? (() => []),
|
||||
status: opts.state?.session?.status ?? (() => undefined),
|
||||
permission: opts.state?.session?.permission ?? (() => []),
|
||||
question: opts.state?.session?.question ?? (() => []),
|
||||
},
|
||||
part: opts.state?.part ?? (() => []),
|
||||
lsp: opts.state?.lsp ?? (() => []),
|
||||
mcp: opts.state?.mcp ?? (() => []),
|
||||
},
|
||||
theme: {
|
||||
get current() {
|
||||
return opts.theme?.current ?? themeCurrent()
|
||||
},
|
||||
get selected() {
|
||||
return selected
|
||||
},
|
||||
has(name) {
|
||||
return has(name)
|
||||
},
|
||||
set(name) {
|
||||
return set(name)
|
||||
},
|
||||
async install(file) {
|
||||
if (opts.theme?.install) return opts.theme.install(file)
|
||||
throw new Error("base theme.install should not run")
|
||||
},
|
||||
mode() {
|
||||
if (opts.theme?.mode) return opts.theme.mode()
|
||||
return "dark"
|
||||
},
|
||||
get ready() {
|
||||
return opts.theme?.ready ?? true
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user