Compare commits

...

1 Commits

Author SHA1 Message Date
James Long 2513f29744 refactor(tui): break plugin registry cycle 2026-08-04 13:33:03 +00:00
6 changed files with 45 additions and 39 deletions
+2 -1
View File
@@ -83,7 +83,8 @@ import { ArgsProvider, useArgs, type Args } from "./context/args"
import open from "open"
import { PromptRefProvider, usePromptRef } from "./context/prompt"
import { Config, ConfigProvider, useConfig } from "./config"
import { PluginProvider, usePlugin, type PackageResolver } from "./plugin/context"
import { PluginProvider, type PackageResolver } from "./plugin/context"
import { usePlugin } from "./plugin/use-plugin"
import { tuiPluginDirectories } from "./plugin/discovery"
import { PluginRoute, PluginSlot } from "./plugin/render"
import { CommandPaletteDialog } from "./component/command-palette"
+1 -1
View File
@@ -13,7 +13,7 @@ import { useRoute } from "../context/route"
import { Keymap } from "../context/keymap"
import { useTheme, useThemes } from "../context/theme"
import { DevTools } from "../devtools"
import { usePlugin } from "../plugin/context"
import { usePlugin } from "../plugin/use-plugin"
import { errorMessage } from "../util/error"
const graphWidth = 23
@@ -1,6 +1,6 @@
import { Plugin } from "@opencode-ai/plugin/tui"
import { createMemo, createSignal } from "solid-js"
import { usePlugin } from "../../plugin/context"
import { usePlugin } from "../../plugin/use-plugin"
import { DialogSelect, type DialogSelectOption } from "../../ui/dialog-select"
const id = "opencode.plugins"
+7 -35
View File
@@ -1,9 +1,9 @@
import type { Plugin } from "@opencode-ai/plugin/tui"
import { batch, createContext, createEffect, on, onCleanup, onMount, useContext, type ParentProps } from "solid-js"
import { batch, createEffect, on, onCleanup, onMount, type ParentProps } from "solid-js"
import path from "path"
import { stat } from "fs/promises"
import { fileURLToPath, pathToFileURL } from "url"
import type { Page, Slot, SlotName } from "@opencode-ai/plugin/tui/context"
import type { Page, Slot } from "@opencode-ai/plugin/tui/context"
import { createStore, produce, reconcile as reconcileStore } from "solid-js/store"
import { isDeepEqual } from "remeda"
import "#runtime-plugin-support"
@@ -14,32 +14,12 @@ import { builtins } from "./builtins"
import { createPluginContext, usePluginHost, type Dispose } from "./api"
import { createSourceWatcher } from "./watch"
import { discoverTuiPlugins, freshSpecifier, localSource } from "./discovery"
import { PluginContext, type PluginState, type RegisteredPlugin } from "./use-plugin"
export interface PackageResolver {
readonly resolve: (spec: string) => Promise<string | undefined>
}
type State =
| { readonly target: string; readonly id: string; readonly status: "active" | "inactive" }
| { readonly target: string; readonly status: "unsupported" }
| { readonly target: string; readonly status: "failed"; readonly error: string }
type RegisteredPlugin = {
readonly id: string
readonly source: "builtin" | "external"
readonly active: boolean
}
type Value = {
readonly ready: () => boolean
readonly list: () => ReadonlyArray<State>
readonly registered: () => ReadonlyArray<RegisteredPlugin>
readonly route: (id: string, name: string) => Page["render"] | undefined
readonly slot: <Name extends SlotName>(name: Name) => ReadonlyArray<{ readonly id: string; readonly render: Slot<Name> }>
readonly activate: (id: string) => Promise<boolean>
readonly deactivate: (id: string) => Promise<boolean>
}
type Registration = {
plugin: Plugin.Definition
source: RegisteredPlugin["source"]
@@ -55,8 +35,6 @@ type Registration = {
// One entry of the desired plugin generation produced by the resolve phase.
type Desired = Pick<Registration, "plugin" | "source" | "target" | "version" | "options"> & { enabled: boolean }
const PluginContext = createContext<Value>()
export function PluginProvider(props: ParentProps<{ packages: PackageResolver; directories: string[] }>) {
const host = usePluginHost()
const config = useConfig()
@@ -64,7 +42,7 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver; d
const directory = config.path ? path.dirname(config.path) : process.cwd()
const [store, setStore] = createStore({
ready: false,
states: [] as ReadonlyArray<State>,
states: [] as ReadonlyArray<PluginState>,
registrations: {} as Record<string, Registration>,
})
@@ -194,7 +172,7 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver; d
// to import keeps its running previous version and only reports failure.
const desired = new Map<string, Desired>()
for (const plugin of builtins) desired.set(plugin.id, { plugin, source: "builtin", version: "builtin", enabled: true })
const failures: State[] = []
const failures: PluginState[] = []
for (const entry of entries) {
const target = typeof entry === "string" ? entry : entry.package
if (target.startsWith("-")) {
@@ -326,8 +304,8 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver; d
}
const failedTargets = new Set(failures.map((failure) => failure.target))
const states: State[] = [
...[...desired.values()].flatMap((item): State[] => {
const states: PluginState[] = [
...[...desired.values()].flatMap((item): PluginState[] => {
if (item.target === undefined) return []
// A failed reload keeps this item running; the failure entry covers it.
if (failedTargets.has(item.target)) return []
@@ -502,9 +480,3 @@ function isPlugin(value: unknown): value is Plugin.Definition {
typeof value.setup === "function"
)
}
export function usePlugin() {
const value = useContext(PluginContext)
if (!value) throw new Error("PluginProvider is missing")
return value
}
+1 -1
View File
@@ -3,7 +3,7 @@ import type { SlotMap, SlotName } from "@opencode-ai/plugin/tui/context"
import { useRoute } from "../context/route"
import { useToast } from "../ui/toast"
import { errorMessage } from "../util/error"
import { usePlugin } from "./context"
import { usePlugin } from "./use-plugin"
// Contain render-time plugin crashes: a throwing slot or route must not take
// down the app or the other plugins. The crash surfaces as one error toast.
+33
View File
@@ -0,0 +1,33 @@
import type { Page, Slot, SlotName } from "@opencode-ai/plugin/tui/context"
import { createContext, useContext } from "solid-js"
export type RegisteredPlugin = {
readonly id: string
readonly source: "builtin" | "external"
readonly active: boolean
}
export type PluginState =
| { readonly target: string; readonly id: string; readonly status: "active" | "inactive" }
| { readonly target: string; readonly status: "unsupported" }
| { readonly target: string; readonly status: "failed"; readonly error: string }
type Value = {
readonly ready: () => boolean
readonly list: () => ReadonlyArray<PluginState>
readonly registered: () => ReadonlyArray<RegisteredPlugin>
readonly route: (id: string, name: string) => Page["render"] | undefined
readonly slot: <Name extends SlotName>(
name: Name,
) => ReadonlyArray<{ readonly id: string; readonly render: Slot<Name> }>
readonly activate: (id: string) => Promise<boolean>
readonly deactivate: (id: string) => Promise<boolean>
}
export const PluginContext = createContext<Value>()
export function usePlugin() {
const value = useContext(PluginContext)
if (!value) throw new Error("PluginProvider is missing")
return value
}