Revert "feat(tui): load external plugin exports"

This reverts commit fdd6498909.
This commit is contained in:
Kit Langton 2026-07-17 14:40:53 -04:00
commit 46148bbc6c
7 changed files with 55 additions and 129 deletions

View file

@ -34,7 +34,6 @@
"./prompt/content": "./src/prompt/content.ts",
"./prompt/display": "./src/prompt/display.ts",
"./plugin/runtime": "./src/plugin/runtime.tsx",
"./plugin/context": "./src/plugin/context.tsx",
"./plugin/slots": "./src/plugin/slots.tsx",
"./plugin/command-shim": "./src/plugin/command-shim.ts",
"./parsers-config": "./src/parsers-config.ts",

View file

@ -78,7 +78,7 @@ import open from "open"
import { PromptRefProvider, usePromptRef } from "./context/prompt"
import { Config, ConfigProvider, useConfig } from "./config"
import { createPluginRuntime, PluginRuntimeProvider, usePluginRuntime } from "./plugin/runtime"
import { PluginProvider, PluginRoute, PluginSlot, usePlugin, type PluginHost } from "./plugin/context"
import { PluginProvider, PluginRoute, PluginSlot, usePlugin, type PackageResolver } from "./plugin/context"
import { CommandPaletteDialog } from "./component/command-palette"
import { COMMAND_PALETTE_COMMAND, Keymap, type KeymapCommand } from "./context/keymap"
@ -149,7 +149,7 @@ export type TuiInput = {
}
args: Args
config: Config.Interface
pluginHost: PluginHost
packages: PackageResolver
terminalHandoff?: () => Promise<
| {
readonly renderer: CliRenderer
@ -346,7 +346,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
<PromptHistoryProvider>
<PromptRefProvider>
<EditorContextProvider>
<PluginProvider pluginHost={input.pluginHost}>
<PluginProvider packages={input.packages}>
<App
started={appStarted}
pair={

View file

@ -11,6 +11,8 @@ import {
type ParentProps,
} from "solid-js"
import path from "path"
import { stat } from "fs/promises"
import { fileURLToPath, pathToFileURL } from "url"
import type { Context, Page, Slot } from "@opencode-ai/plugin/v2/tui/context"
import { createStore, produce, reconcile as reconcileStore } from "solid-js/store"
import { useConfig } from "../config"
@ -22,8 +24,8 @@ import { useTuiLifecycle } from "../context/runtime"
import { useLocation } from "../context/location"
import { builtins } from "./builtins"
export interface PluginHost {
readonly load: (spec: string, directory: string) => Promise<Plugin.Definition | undefined>
export interface PackageResolver {
readonly resolve: (spec: string) => Promise<string | undefined>
}
type State =
@ -54,7 +56,7 @@ type Registration = {
const PluginContext = createContext<Value>()
export function PluginProvider(props: ParentProps<{ pluginHost: PluginHost }>) {
export function PluginProvider(props: ParentProps<{ packages: PackageResolver }>) {
const client = useClient()
const data = useData()
const route = useRoute()
@ -229,7 +231,7 @@ export function PluginProvider(props: ParentProps<{ pluginHost: PluginHost }>) {
const options = typeof entry === "string" ? undefined : entry.options
setStore("states", (items) => [...items, { target, status: "loading" }])
const plugin = await props.pluginHost.load(target, directory).catch((error) => {
const plugin = await loadPlugin(target, directory, props.packages).catch((error) => {
setStore("states", (items) =>
items.map((state) =>
state.target === target
@ -331,6 +333,46 @@ function matches(selector: string, id: string) {
return selector === "*" || selector === id || (selector.endsWith(".*") && id.startsWith(selector.slice(0, -1)))
}
async function loadPlugin(spec: string, directory: string, packages: PackageResolver) {
const local = spec.startsWith("file://")
? new URL(spec)
: spec.startsWith("./") || spec.startsWith("../") || path.isAbsolute(spec)
? pathToFileURL(path.resolve(directory, spec))
: undefined
const entrypoint = local ? await resolveLocal(local) : await packages.resolve(spec)
if (!entrypoint) return
const mod: { readonly default?: unknown } = await import(entrypoint)
if (!isPlugin(mod.default)) throw new Error(`Invalid V2 TUI plugin module: ${spec}`)
return mod.default
}
async function resolveLocal(url: URL) {
const info = await stat(url)
if (info.isFile()) return url.href
if (!info.isDirectory()) return
return resolve(pathToFileURL(path.join(fileURLToPath(url), "tui")).href)
}
function resolve(specifier: string) {
try {
return import.meta.resolve(specifier)
} catch {
return undefined
}
}
function isPlugin(value: unknown): value is Plugin.Definition {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof value.id === "string" &&
value.id.length > 0 &&
"setup" in value &&
typeof value.setup === "function"
)
}
export function usePlugin() {
const value = useContext(PluginContext)
if (!value) throw new Error("PluginProvider is missing")

View file

@ -30,7 +30,7 @@ test("SIGHUP clears title and disposes scoped resources once", async () => {
run({
server: { endpoint: { url: server.url.toString() } },
config: { get: async () => ({}), update: async () => ({}) },
pluginHost: { load: async () => undefined },
packages: { resolve: async () => undefined },
args: {},
log: () => {},
}).pipe(Effect.provide(AppNodeBuilder.build(Global.node)), Effect.provide(FileSystem.layerNoop({}))),
@ -102,7 +102,7 @@ test("session lifecycle updates the terminal title and prints the epilogue after
run({
server: { endpoint: { url: server.url.toString() } },
config: { get: async () => ({}), update: async () => ({}) },
pluginHost: { load: async () => undefined },
packages: { resolve: async () => undefined },
args: { sessionID: "dummy" },
log: () => {},
}).pipe(Effect.provide(AppNodeBuilder.build(Global.node)), Effect.provide(FileSystem.layerNoop({}))),