From a5c35bf1822f14a40f7aa788996aa6defb0dfbb7 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 11 May 2026 17:32:16 -0400 Subject: [PATCH] Avoid bootstrapping server plugins from TUI plugin runtime (#26938) --- .../src/cli/cmd/tui/plugin/runtime.ts | 67 ++++++++----------- .../test/cli/tui/plugin-loader.test.ts | 32 ++++++++- 2 files changed, 60 insertions(+), 39 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts index 64961b20f7..dad4595e7f 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts @@ -17,7 +17,6 @@ import { TuiConfig } from "@/cli/cmd/tui/config/tui" import * as Log from "@opencode-ai/core/util/log" import { errorData, errorMessage } from "@/util/error" import { isRecord } from "@/util/record" -import { WithInstance } from "@/project/with-instance" import { readPackageThemes, readPluginId, @@ -838,10 +837,7 @@ async function addPluginBySpec(state: RuntimeState | undefined, raw: string) { state.pending.delete(spec) return true } - const ready = await WithInstance.provide({ - directory: state.directory, - fn: () => resolveExternalPlugins([cfg], () => TuiConfig.waitForDependencies()), - }).catch((error) => { + const ready = await resolveExternalPlugins([cfg], () => TuiConfig.waitForDependencies()).catch((error) => { fail("failed to add tui plugin", { path: next, error }) return [] as PluginLoad[] }) @@ -1034,42 +1030,37 @@ async function load(input: { api: Api; config: TuiConfig.Resolved }) { } runtime = next try { - await WithInstance.provide({ - directory: cwd, - fn: async () => { - const records = Flag.OPENCODE_PURE ? [] : (config.plugin_origins ?? []) - if (Flag.OPENCODE_PURE && config.plugin_origins?.length) { - log.info("skipping external tui plugins in pure mode", { count: config.plugin_origins.length }) - } + const records = Flag.OPENCODE_PURE ? [] : (config.plugin_origins ?? []) + if (Flag.OPENCODE_PURE && config.plugin_origins?.length) { + log.info("skipping external tui plugins in pure mode", { count: config.plugin_origins.length }) + } - for (const item of INTERNAL_TUI_PLUGINS) { - log.info("loading internal tui plugin", { id: item.id }) - const entry = loadInternalPlugin(item) - const meta = createMeta(entry.source, entry.spec, entry.target, undefined, entry.id) - addPluginEntry(next, { - id: entry.id, - load: entry, - meta, - themes: {}, - plugin: entry.module.tui, - enabled: item.enabled ?? true, - }) - } + for (const item of INTERNAL_TUI_PLUGINS) { + log.info("loading internal tui plugin", { id: item.id }) + const entry = loadInternalPlugin(item) + const meta = createMeta(entry.source, entry.spec, entry.target, undefined, entry.id) + addPluginEntry(next, { + id: entry.id, + load: entry, + meta, + themes: {}, + plugin: entry.module.tui, + enabled: item.enabled ?? true, + }) + } - const ready = await resolveExternalPlugins(records, () => TuiConfig.waitForDependencies()) - await addExternalPluginEntries(next, ready) + const ready = await resolveExternalPlugins(records, () => TuiConfig.waitForDependencies()) + await addExternalPluginEntries(next, ready) - applyInitialPluginEnabledState(next, config) - for (const plugin of next.plugins) { - if (!plugin.enabled) continue - // Keep plugin execution sequential for deterministic side effects: - // command registration order affects keybind/command precedence, - // route registration is last-wins when ids collide, - // and hook chains rely on stable plugin ordering. - await activatePluginEntry(next, plugin, false) - } - }, - }) + applyInitialPluginEnabledState(next, config) + for (const plugin of next.plugins) { + if (!plugin.enabled) continue + // Keep plugin execution sequential for deterministic side effects: + // command registration order affects keybind/command precedence, + // route registration is last-wins when ids collide, + // and hook chains rely on stable plugin ordering. + await activatePluginEntry(next, plugin, false) + } } catch (error) { fail("failed to load tui plugins", { directory: cwd, error }) } diff --git a/packages/opencode/test/cli/tui/plugin-loader.test.ts b/packages/opencode/test/cli/tui/plugin-loader.test.ts index d62bc19bfe..493520fc00 100644 --- a/packages/opencode/test/cli/tui/plugin-loader.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader.test.ts @@ -5,7 +5,7 @@ import { pathToFileURL } from "url" import { createTestKeymap } from "@opentui/keymap/testing" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" -import { createTuiResolvedConfig } from "../../fixture/tui-runtime" +import { createTuiResolvedConfig, mockTuiRuntime } from "../../fixture/tui-runtime" import { Global } from "@opencode-ai/core/global" import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui" import { Filesystem } from "@/util/filesystem" @@ -647,6 +647,36 @@ export default { } }) +test("does not bootstrap server plugins while initializing tui plugins", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + const marker = path.join(dir, "server-plugin-called.txt") + const plugin = path.join(dir, "server-plugin.ts") + await Bun.write( + plugin, + [ + "export default async () => {", + ` await Bun.write(${JSON.stringify(marker)}, "called")`, + " return {}", + "}", + "", + ].join("\n"), + ) + await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ plugin: [pathToFileURL(plugin).href] })) + return { marker } + }, + }) + + const mock = mockTuiRuntime(tmp.path, []) + try { + await TuiPluginRuntime.init({ api: createTuiPluginApi(), config: mock.config }) + await expect(fs.stat(tmp.extra.marker)).rejects.toThrow() + } finally { + await TuiPluginRuntime.dispose() + mock.restore() + } +}) + describe("tui.plugin.loader", () => { let data: Data