actual-tui-plugins
Sebastian Herrlinger 2026-03-04 21:15:45 +01:00
parent 557a92a1f3
commit 11c76c6d9a
4 changed files with 58 additions and 52 deletions

View File

@ -1,12 +1,4 @@
import {
createSlot,
createSolidSlotRegistry,
render,
useKeyboard,
useRenderer,
useTerminalDimensions,
type SolidPlugin,
} from "@opentui/solid"
import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
import { Clipboard } from "@tui/util/clipboard"
import { Selection } from "@tui/util/selection"
import { createCliRenderer, MouseButton, TextAttributes, type CliRendererConfig } from "@opentui/core"
@ -49,9 +41,7 @@ import { writeHeapSnapshot } from "v8"
import { PromptRefProvider, usePromptRef } from "./context/prompt"
import { TuiConfigProvider } from "./context/tui-config"
import { TuiConfig } from "@/config/tui"
import type { TuiSlotContext, TuiSlotMap, TuiSlots } from "@opencode-ai/plugin/tui"
type TuiSlot = <K extends keyof TuiSlotMap>(props: { name: K } & TuiSlotMap[K]) => unknown
import { TuiPlugin } from "./plugin"
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
// can't set raw mode if not a TTY
@ -160,29 +150,7 @@ export function tui(input: {
}
const renderer = await createCliRenderer(rendererConfig(input.config))
const registry = createSolidSlotRegistry<TuiSlotMap, TuiSlotContext>(
renderer,
{},
{
onPluginError(event) {
console.error("[tui.slot] plugin error", {
plugin: event.pluginId,
slot: event.slot,
phase: event.phase,
source: event.source,
message: event.error.message,
})
},
},
)
const Slot = createSlot<TuiSlotMap, TuiSlotContext>(registry)
const slot: TuiSlot = (props) => Slot(props)
const slots: TuiSlots = {
register(plugin) {
console.error("[tui.slot] register", plugin.id)
return registry.register(plugin as SolidPlugin<TuiSlotMap, TuiSlotContext>)
},
}
const slots = TuiPlugin.slots(renderer)
await render(() => {
return (
@ -214,7 +182,7 @@ export function tui(input: {
<FrecencyProvider>
<PromptHistoryProvider>
<PromptRefProvider>
<App slot={slot} />
<App />
</PromptRefProvider>
</PromptHistoryProvider>
</FrecencyProvider>
@ -238,7 +206,7 @@ export function tui(input: {
})
}
function App(props: { slot: TuiSlot }) {
function App() {
const route = useRoute()
const dimensions = useTerminalDimensions()
const renderer = useRenderer()
@ -805,10 +773,10 @@ function App(props: { slot: TuiSlot }) {
>
<Switch>
<Match when={route.data.type === "home"}>
<Home slot={props.slot} />
<Home />
</Match>
<Match when={route.data.type === "session"}>
<Session slot={props.slot} />
<Session />
</Match>
</Switch>
</box>

View File

@ -2,8 +2,13 @@ import {
type TuiPlugin as TuiPluginFn,
type TuiPluginInput,
type TuiPluginModule,
type TuiSlotContext,
type TuiSlotMap,
type TuiSlotPlugin,
type TuiSlots,
} from "@opencode-ai/plugin/tui"
import { createSlot, createSolidSlotRegistry, type SolidPlugin } from "@opentui/solid"
import type { CliRenderer } from "@opentui/core"
import type { JSX } from "solid-js"
import "@opentui/solid/preload"
@ -14,9 +19,46 @@ import { BunProc } from "@/bun"
import { Instance } from "@/project/instance"
import { registerThemes } from "./context/theme"
type Slot = <K extends keyof TuiSlotMap>(props: { name: K } & TuiSlotMap[K]) => unknown
function empty<K extends keyof TuiSlotMap>(_props: { name: K } & TuiSlotMap[K]) {
return null
}
export namespace TuiPlugin {
const log = Log.create({ service: "tui.plugin" })
let loaded: Promise<void> | undefined
let view: Slot = empty
export const Slot: Slot = (props) => view(props)
export function slots(renderer: CliRenderer): TuiSlots {
const reg = createSolidSlotRegistry<TuiSlotMap, TuiSlotContext>(
renderer,
{},
{
onPluginError(event) {
console.error("[tui.slot] plugin error", {
plugin: event.pluginId,
slot: event.slot,
phase: event.phase,
source: event.source,
message: event.error.message,
})
},
},
)
const slot = createSlot<TuiSlotMap, TuiSlotContext>(reg)
view = (props) => slot(props)
return {
register(plugin) {
console.error("[tui.slot] register", plugin.id)
return reg.register(plugin as SolidPlugin<TuiSlotMap, TuiSlotContext>)
},
}
}
export async function init(input: TuiPluginInput) {
if (loaded) return loaded
@ -32,7 +74,7 @@ export namespace TuiPlugin {
return BunProc.install(pkg, version)
}
function slot(entry: unknown) {
function pick(entry: unknown) {
if (!entry || typeof entry !== "object") return
if ("id" in entry && typeof entry.id === "string" && "slots" in entry && typeof entry.slots === "object") {
return entry as TuiSlotPlugin<JSX.Element>
@ -88,7 +130,7 @@ export namespace TuiPlugin {
registerThemes(pluginEntry.themes as Record<string, unknown>)
}
const plugin = slot(pluginEntry)
const plugin = pick(pluginEntry)
if (plugin) {
input.slots.register(plugin)
}

View File

@ -15,14 +15,12 @@ import { Installation } from "@/installation"
import { useKV } from "../context/kv"
import { useCommandDialog } from "../component/dialog-command"
import { useLocal } from "../context/local"
import type { TuiSlotMap } from "@opencode-ai/plugin/tui"
type Slot = <K extends "home_hint" | "home_footer">(props: { name: K } & TuiSlotMap[K]) => unknown
import { TuiPlugin } from "../plugin"
// TODO: what is the best way to do this?
let once = false
export function Home(props: { slot: Slot }) {
export function Home() {
const sync = useSync()
const kv = useKV()
const { theme } = useTheme()
@ -75,7 +73,7 @@ export function Home(props: { slot: Slot }) {
</Switch>
</text>
</Show>
{props.slot({ name: "home_hint" }) as never}
{TuiPlugin.Slot({ name: "home_hint" }) as never}
</box>
)
@ -154,7 +152,7 @@ export function Home(props: { slot: Slot }) {
</Show>
</box>
<box flexGrow={1} />
{props.slot({ name: "home_footer" }) as never}
{TuiPlugin.Slot({ name: "home_footer" }) as never}
<box flexShrink={0}>
<text fg={theme.textMuted}>{Installation.VERSION}</text>
</box>

View File

@ -80,12 +80,10 @@ import { DialogExportOptions } from "../../ui/dialog-export-options"
import { formatTranscript } from "../../util/transcript"
import { UI } from "@/cli/ui.ts"
import { useTuiConfig } from "../../context/tui-config"
import type { TuiSlotMap } from "@opencode-ai/plugin/tui"
import { TuiPlugin } from "../../plugin"
addDefaultParsers(parsers.parsers)
type Slot = (props: { name: "session_footer"; session_id: TuiSlotMap["session_footer"]["session_id"] }) => unknown
class CustomSpeedScroll implements ScrollAcceleration {
constructor(private speed: number) {}
@ -115,7 +113,7 @@ function use() {
return ctx
}
export function Session(props: { slot: Slot }) {
export function Session() {
const route = useRouteData("session")
const { navigate } = useRoute()
const sync = useSync()
@ -1180,7 +1178,7 @@ export function Session(props: { slot: Slot }) {
}}
sessionID={route.sessionID}
/>
{props.slot({ name: "session_footer", session_id: route.sessionID }) as never}
{TuiPlugin.Slot({ name: "session_footer", session_id: route.sessionID }) as never}
</box>
</Show>
<Toast />