actual-tui-plugins
Sebastian Herrlinger 2026-03-04 23:41:34 +01:00
parent 3cc33e39e7
commit a7400a77ea
2 changed files with 17 additions and 21 deletions

View File

@ -18,7 +18,7 @@ import { resolvePluginTarget, uniqueModuleEntries } from "@/plugin/shared"
import { registerThemes } from "./context/theme"
type Slot = <K extends keyof TuiSlotMap>(props: { name: K } & TuiSlotMap[K]) => JSX.Element | null
type InitInput<Renderer> = Omit<TuiPluginInput<Renderer>, "slots">
type InitInput = Omit<TuiPluginInput<CliRenderer>, "slots">
function empty<K extends keyof TuiSlotMap>(_props: { name: K } & TuiSlotMap[K]) {
return null
@ -44,22 +44,16 @@ function getThemes(value: unknown) {
return value.themes
}
function isTuiPlugin<Renderer>(value: unknown): value is TuiPluginFn<Renderer> {
function isTuiPlugin(value: unknown): value is TuiPluginFn<CliRenderer> {
return typeof value === "function"
}
function getTuiPlugin<Renderer>(value: unknown) {
function getTuiPlugin(value: unknown) {
if (!isRecord(value) || !("tui" in value)) return
if (!isTuiPlugin<Renderer>(value.tui)) return
if (!isTuiPlugin(value.tui)) return
return value.tui
}
function isCliRenderer(value: unknown): value is CliRenderer {
if (!isRecord(value)) return false
if (!("once" in value)) return false
return typeof value.once === "function"
}
export namespace TuiPlugin {
const log = Log.create({ service: "tui.plugin" })
let loaded: Promise<void> | undefined
@ -67,11 +61,7 @@ export namespace TuiPlugin {
export const Slot: Slot = (props) => view(props)
function setupSlots(renderer: unknown): TuiSlots {
if (!isCliRenderer(renderer)) {
throw new TypeError("Invalid TUI renderer")
}
function setupSlots(renderer: CliRenderer): TuiSlots {
const reg = createSolidSlotRegistry<TuiSlotMap, TuiSlotContext>(
renderer,
{},
@ -98,7 +88,7 @@ export namespace TuiPlugin {
}
}
export async function init<Renderer>(input: InitInput<Renderer>) {
export async function init(input: InitInput) {
if (loaded) return loaded
loaded = load({
...input,
@ -107,7 +97,7 @@ export namespace TuiPlugin {
return loaded
}
async function load<Renderer>(input: TuiPluginInput<Renderer>) {
async function load(input: TuiPluginInput<CliRenderer>) {
const dir = process.cwd()
await Instance.provide({
@ -148,7 +138,7 @@ export namespace TuiPlugin {
const slotPlugin = getTuiSlotPlugin(entry)
if (slotPlugin) input.slots.register(slotPlugin)
const tuiPlugin = getTuiPlugin<Renderer>(entry)
const tuiPlugin = getTuiPlugin(entry)
if (!tuiPlugin) continue
await tuiPlugin(input, Config.pluginOptions(item))
}

View File

@ -3,6 +3,7 @@ import fs from "fs/promises"
import path from "path"
import { pathToFileURL } from "url"
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
import type { CliRenderer } from "@opentui/core"
import { tmpdir } from "../../fixture/fixture"
import { Log } from "../../../src/util/log"
@ -80,6 +81,13 @@ test("ignores function-only tui exports and loads object exports", async () => {
process.env.OPENCODE_TUI_CONFIG = tmp.extra.configPath
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
const renderer = {
...Object.create(null),
once(this: CliRenderer) {
return this
},
} satisfies CliRenderer
try {
await TuiPlugin.init({
client: createOpencodeClient({
@ -88,9 +96,7 @@ test("ignores function-only tui exports and loads object exports", async () => {
event: {
on: () => () => {},
},
renderer: {
once: () => undefined,
},
renderer,
})
expect(await fs.readFile(tmp.extra.objMarker, "utf8")).toBe("called")