plugin manager ui

This commit is contained in:
Sebastian Herrlinger
2026-03-25 22:36:08 +01:00
parent dde151a584
commit 0506b53f27
2 changed files with 117 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
import { Keybind } from "@/util/keybind"
import type { TuiPlugin, TuiPluginStatus } from "@opencode-ai/plugin/tui"
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
import { createMemo, createSignal } from "solid-js"
type Api = Parameters<TuiPlugin>[0]
const id = "internal:plugin-manager"
const key = Keybind.parse("space").at(0)
function state(api: Api, item: TuiPluginStatus) {
return (
<span style={{ fg: item.active ? api.theme.current.success : api.theme.current.error }}>
{item.active ? "active" : "inactive"}
</span>
)
}
function row(api: Api, item: TuiPluginStatus): DialogSelectOption<string> {
return {
title: item.name,
value: item.id,
category: item.source === "internal" ? "Internal" : "External",
description: item.source === "internal" ? "Built-in" : item.spec,
footer: state(api, item),
disabled: item.id === id,
}
}
function View(props: { api: Api }) {
const [list, setList] = createSignal(props.api.plugins.list())
const [cur, setCur] = createSignal<string | undefined>()
const [lock, setLock] = createSignal(false)
const rows = createMemo(() =>
[...list()]
.sort((a, b) => {
const x = a.source === "internal" ? 1 : 0
const y = b.source === "internal" ? 1 : 0
if (x !== y) return x - y
return a.name.localeCompare(b.name)
})
.map((item) => row(props.api, item)),
)
const flip = (x: string) => {
if (lock()) return
const item = list().find((entry) => entry.id === x)
if (!item) return
setLock(true)
const task = item.active ? props.api.plugins.deactivate(x) : props.api.plugins.activate(x)
task
.then((ok) => {
if (!ok) {
props.api.ui.toast({
variant: "error",
message: `Failed to update plugin ${item.name}`,
})
}
setList(props.api.plugins.list())
})
.finally(() => {
setLock(false)
})
}
return (
<DialogSelect
title="Plugins"
options={rows()}
current={cur()}
onMove={(item) => setCur(item.value)}
keybind={[
{
title: "toggle",
keybind: key,
disabled: lock(),
onTrigger: (item) => {
setCur(item.value)
flip(item.value)
},
},
]}
onSelect={(item) => {
setCur(item.value)
flip(item.value)
}}
/>
)
}
function show(api: Api) {
api.ui.dialog.replace(() => <View api={api} />)
}
const tui: TuiPlugin = async (api) => {
api.command.register(() => [
{
title: "Plugins",
value: "plugins.list",
keybind: "<leader>p",
category: "System",
onSelect() {
show(api)
},
},
])
}
export default {
id,
tui,
}

View File

@@ -6,6 +6,7 @@ import * as SidebarLsp from "../feature-plugins/sidebar/lsp"
import * as SidebarTodo from "../feature-plugins/sidebar/todo"
import * as SidebarFiles from "../feature-plugins/sidebar/files"
import * as SidebarFooter from "../feature-plugins/sidebar/footer"
import * as PluginManager from "../feature-plugins/system/plugins"
export type InternalTuiPlugin = {
name: string
@@ -46,4 +47,8 @@ export const INTERNAL_TUI_PLUGINS: InternalTuiPlugin[] = [
name: "sidebar-footer",
module: SidebarFooter,
},
{
name: "plugin-manager",
module: PluginManager,
},
]