| import type { TuiPlugin, TuiPluginApi, TuiPluginStatus } from "@opencode-ai/plugin/tui" |
| import type { BuiltinTuiPlugin } from "../builtins" |
| import { useTerminalDimensions } from "@opentui/solid" |
| import { fileURLToPath } from "url" |
| import { DialogSelect, type DialogSelectOption } from "../../ui/dialog-select" |
| import { Show, createEffect, createMemo, createSignal } from "solid-js" |
| import { useBindings } from "../../keymap" |
|
|
| const id = "internal:plugin-manager" |
|
|
| function state(api: TuiPluginApi, item: TuiPluginStatus) { |
| if (!item.enabled) { |
| return <span style={{ fg: api.theme.current.textMuted }}>disabled</span> |
| } |
|
|
| return ( |
| <span style={{ fg: item.active ? api.theme.current.success : api.theme.current.error }}> |
| {item.active ? "active" : "inactive"} |
| </span> |
| ) |
| } |
|
|
| function source(spec: string) { |
| if (!spec.startsWith("file://")) return |
| return fileURLToPath(spec) |
| } |
|
|
| function meta(item: TuiPluginStatus, width: number) { |
| if (item.source === "internal") { |
| if (width >= 120) return "Built-in plugin" |
| return "Built-in" |
| } |
| const next = source(item.spec) |
| if (next) return next |
| return item.spec |
| } |
|
|
| function Install(props: { api: TuiPluginApi }) { |
| const [global, setGlobal] = createSignal(false) |
| const [busy, setBusy] = createSignal(false) |
|
|
| useBindings(() => ({ |
| enabled: !busy(), |
| bindings: [{ key: "tab", desc: "Toggle install scope", group: "Plugins", cmd: () => setGlobal((value) => !value) }], |
| })) |
|
|
| return ( |
| <props.api.ui.DialogPrompt |
| title="Install plugin" |
| placeholder="npm package name" |
| busy={busy()} |
| busyText="Installing plugin…" |
| description={() => ( |
| <box flexDirection="row" gap={1}> |
| <text fg={props.api.theme.current.textMuted}>scope:</text> |
| <text fg={busy() ? props.api.theme.current.textMuted : props.api.theme.current.text}> |
| {global() ? "global" : "local"} |
| </text> |
| <Show when={!busy()}> |
| <text fg={props.api.theme.current.textMuted}>(tab toggle)</text> |
| </Show> |
| </box> |
| )} |
| onConfirm={(raw) => { |
| if (busy()) return |
| const mod = raw.trim() |
| if (!mod) { |
| props.api.ui.toast({ |
| variant: "error", |
| message: "Plugin package name is required", |
| }) |
| return |
| } |
| |
| setBusy(true) |
| void props.api.plugins |
| .install(mod, { global: global() }) |
| .then((out) => { |
| if (!out.ok) { |
| props.api.ui.toast({ |
| variant: "error", |
| message: out.message, |
| }) |
| if (out.missing) { |
| props.api.ui.toast({ |
| variant: "info", |
| message: "Check npm registry/auth settings and try again.", |
| }) |
| } |
| show(props.api) |
| return |
| } |
| |
| props.api.ui.toast({ |
| variant: "success", |
| message: `Installed ${mod} (${global() ? "global" : "local"}: ${out.dir})`, |
| }) |
| if (!out.tui) { |
| props.api.ui.toast({ |
| variant: "info", |
| message: "Package has no TUI target to load in this app.", |
| }) |
| show(props.api) |
| return |
| } |
| |
| return props.api.plugins.add(mod).then((ok) => { |
| if (!ok) { |
| props.api.ui.toast({ |
| variant: "warning", |
| message: "Installed plugin, but runtime load failed. See console/logs; restart TUI to retry.", |
| }) |
| show(props.api) |
| return |
| } |
| |
| props.api.ui.toast({ |
| variant: "success", |
| message: `Loaded ${mod} in current session.`, |
| }) |
| show(props.api) |
| }) |
| }) |
| .finally(() => { |
| setBusy(false) |
| }) |
| }} |
| onCancel={() => { |
| show(props.api) |
| }} |
| /> |
| ) |
| } |
|
|
| function row(api: TuiPluginApi, item: TuiPluginStatus, width: number): DialogSelectOption<string> { |
| return { |
| title: item.id, |
| value: item.id, |
| category: item.source === "internal" ? "Internal" : "External", |
| description: meta(item, width), |
| footer: state(api, item), |
| disabled: item.id === id, |
| } |
| } |
|
|
| function showInstall(api: TuiPluginApi) { |
| api.ui.dialog.replace(() => <Install api={api} />) |
| } |
|
|
| function View(props: { api: TuiPluginApi }) { |
| const size = useTerminalDimensions() |
| const [list, setList] = createSignal(props.api.plugins.list()) |
| const [cur, setCur] = createSignal<string | undefined>() |
| const [lock, setLock] = createSignal(false) |
|
|
| createEffect(() => { |
| const width = size().width |
| if (width >= 128) { |
| props.api.ui.dialog.setSize("xlarge") |
| return |
| } |
| if (width >= 96) { |
| props.api.ui.dialog.setSize("large") |
| return |
| } |
| props.api.ui.dialog.setSize("medium") |
| }) |
|
|
| 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.id.localeCompare(b.id) |
| }) |
| .map((item) => row(props.api, item, size().width)), |
| ) |
|
|
| 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) |
| void task |
| .then((ok) => { |
| if (!ok) { |
| props.api.ui.toast({ |
| variant: "error", |
| message: `Failed to update plugin ${item.id}`, |
| }) |
| } |
| setList(props.api.plugins.list()) |
| }) |
| .finally(() => { |
| setLock(false) |
| }) |
| } |
|
|
| return ( |
| <DialogSelect |
| title="Plugins" |
| options={rows()} |
| current={cur()} |
| onMove={(item) => setCur(item.value)} |
| actions={[ |
| { |
| title: "toggle", |
| command: "plugins.toggle", |
| hidden: lock(), |
| onTrigger: (item) => { |
| setCur(item.value) |
| flip(item.value) |
| }, |
| }, |
| { |
| title: "install", |
| command: "dialog.plugins.install", |
| hidden: lock(), |
| onTrigger: () => { |
| showInstall(props.api) |
| }, |
| }, |
| ]} |
| onSelect={(item) => { |
| setCur(item.value) |
| flip(item.value) |
| }} |
| /> |
| ) |
| } |
|
|
| function show(api: TuiPluginApi) { |
| api.ui.dialog.replace(() => <View api={api} />) |
| } |
|
|
| const tui: TuiPlugin = async (api) => { |
| api.keymap.registerLayer({ |
| commands: [ |
| { |
| name: "plugins.list", |
| title: "Plugins", |
| category: "System", |
| namespace: "palette", |
| run() { |
| show(api) |
| }, |
| }, |
| { |
| name: "plugins.install", |
| title: "Install plugin", |
| category: "System", |
| namespace: "palette", |
| run() { |
| showInstall(api) |
| }, |
| }, |
| ], |
| bindings: api.tuiConfig.keybinds.gather("plugins.palette", ["plugins.list", "plugins.install"]), |
| }) |
| } |
|
|
| const plugin: BuiltinTuiPlugin = { |
| id, |
| tui, |
| } |
|
|
| export default plugin |
|
|