diff --git a/packages/opencode/specs/effect/facades.md b/packages/opencode/specs/effect/facades.md index 8bf7d97bad..2b20e3f132 100644 --- a/packages/opencode/specs/effect/facades.md +++ b/packages/opencode/specs/effect/facades.md @@ -197,7 +197,7 @@ Most of the original facade-removal backlog is already done. The practical remai ## Checklist - [ ] `src/npm/index.ts` (`Npm`) - still exports runtime-backed async facade helpers on top of `Npm.Service` -- [ ] `src/cli/cmd/tui/config/tui.ts` (`TuiConfig`) - still exports runtime-backed async facade helpers on top of `TuiConfig.Service` +- [x] `src/cli/cmd/tui/config/tui.ts` (`TuiConfig`) - facades removed - [x] `src/session/session.ts` / `src/session/prompt.ts` / `src/session/revert.ts` / `src/session/summary.ts` - service-local facades removed - [x] `src/agent/agent.ts` (`Agent`) - service-local facades removed - [x] `src/permission/index.ts` (`Permission`) - service-local facades removed diff --git a/packages/opencode/src/cli/cmd/run/runtime.boot.ts b/packages/opencode/src/cli/cmd/run/runtime.boot.ts index 3ff9801c6a..50337c9121 100644 --- a/packages/opencode/src/cli/cmd/run/runtime.boot.ts +++ b/packages/opencode/src/cli/cmd/run/runtime.boot.ts @@ -9,6 +9,7 @@ import { Context, Effect, Layer } from "effect" import { stringifyKeyStroke } from "@opentui/keymap" import { TuiConfig } from "@/cli/cmd/tui/config/tui" import { TuiKeybind } from "@/cli/cmd/tui/config/keybind" +import { AppRuntime } from "@/effect/app-runtime" import { makeRuntime } from "@/effect/run-service" import { reusePendingTask } from "./runtime.shared" import { resolveSession, sessionHistory } from "./session.shared" @@ -40,7 +41,7 @@ export type SessionInfo = { variant: string | undefined } -type Config = Awaited> +type Config = TuiConfig.Resolved type BootService = { readonly resolveModelInfo: ( sdk: RunInput["sdk"], @@ -60,8 +61,14 @@ const configTask: { current?: Promise } = {} class Service extends Context.Service()("@opencode/RunBoot") {} +// Exposed on `TuiConfigLoader` so tests can mock without depending on the +// TuiConfig namespace; production calls TuiConfig.Service via AppRuntime. +export const TuiConfigLoader = { + get: () => AppRuntime.runPromise(TuiConfig.Service.use((svc) => svc.get()).pipe(Effect.provide(TuiConfig.layer))), +} + function loadConfig() { - return reusePendingTask(configTask, () => TuiConfig.get()) + return reusePendingTask(configTask, () => TuiConfigLoader.get()) } function emptyModelInfo(): ModelInfo { diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts index 2897a41caf..e322ba85c5 100644 --- a/packages/opencode/src/cli/cmd/tui/attach.ts +++ b/packages/opencode/src/cli/cmd/tui/attach.ts @@ -2,6 +2,8 @@ import { cmd } from "../cmd" import { UI } from "@/cli/ui" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" import { TuiConfig } from "@/cli/cmd/tui/config/tui" +import { AppRuntime } from "@/effect/app-runtime" +import { Effect } from "effect" import { errorMessage } from "@/util/error" import { validateSession } from "./validate-session" import { ServerAuth } from "@/server/auth" @@ -66,7 +68,9 @@ export const AttachCommand = cmd({ } })() const headers = ServerAuth.headers({ password: args.password, username: args.username }) - const config = await TuiConfig.get() + const config = await AppRuntime.runPromise( + TuiConfig.Service.use((svc) => svc.get()).pipe(Effect.provide(TuiConfig.layer)), + ) const { tui } = await import("./app") try { diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts index e53e20d343..47081d721e 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -16,7 +16,6 @@ import { CurrentWorkingDirectory } from "./cwd" import { ConfigPlugin } from "@/config/plugin" import { TuiKeybind } from "./keybind" import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version" -import { makeRuntime } from "@opencode-ai/core/effect/runtime" import { Filesystem } from "@/util/filesystem" import * as Log from "@opencode-ai/core/util/log" import { ConfigVariable } from "@/config/variable" @@ -245,13 +244,3 @@ export const layer = Layer.effect( ) export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer), Layer.provide(AppFileSystem.defaultLayer)) - -const { runPromise } = makeRuntime(Service, defaultLayer) - -export async function waitForDependencies() { - await runPromise((svc) => svc.waitForDependencies()) -} - -export async function get() { - return runPromise((svc) => svc.get()) -} diff --git a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts index dad4595e7f..70c2059a6e 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts @@ -14,6 +14,8 @@ import { import path from "path" import { fileURLToPath } from "url" import { TuiConfig } from "@/cli/cmd/tui/config/tui" +import { AppRuntime } from "@/effect/app-runtime" +import { Effect } from "effect" import * as Log from "@opencode-ai/core/util/log" import { errorData, errorMessage } from "@/util/error" import { isRecord } from "@/util/record" @@ -42,6 +44,14 @@ import { createCommandShim } from "./command-shim" ensureRuntimePluginSupport({ additional: keymapRuntimeModules }) +// Exposed as a separate function so tests can mock it without touching the +// TuiConfig service. Production callers wait on plugin install fibers via +// AppRuntime + TuiConfig.layer; tests replace this with a no-op. +export const waitForDependencies = () => + AppRuntime.runPromise( + TuiConfig.Service.use((svc) => svc.waitForDependencies()).pipe(Effect.provide(TuiConfig.layer)), + ) + type PluginLoad = { options: ConfigPlugin.Options | undefined spec: string @@ -837,7 +847,7 @@ async function addPluginBySpec(state: RuntimeState | undefined, raw: string) { state.pending.delete(spec) return true } - const ready = await resolveExternalPlugins([cfg], () => TuiConfig.waitForDependencies()).catch((error) => { + const ready = await resolveExternalPlugins([cfg], () => waitForDependencies()).catch((error) => { fail("failed to add tui plugin", { path: next, error }) return [] as PluginLoad[] }) @@ -1049,7 +1059,7 @@ async function load(input: { api: Api; config: TuiConfig.Resolved }) { }) } - const ready = await resolveExternalPlugins(records, () => TuiConfig.waitForDependencies()) + const ready = await resolveExternalPlugins(records, () => waitForDependencies()) await addExternalPluginEntries(next, ready) applyInitialPluginEnabledState(next, config) diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 7230dae16a..c46e2f2532 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -14,6 +14,8 @@ import type { EventSource } from "./context/sdk" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" import { writeHeapSnapshot } from "v8" import { TuiConfig } from "./config/tui" +import { AppRuntime } from "@/effect/app-runtime" +import { Effect } from "effect" import { OPENCODE_PROCESS_ROLE, OPENCODE_RUN_ID, @@ -187,7 +189,9 @@ export const TuiThreadCommand = cmd({ } const prompt = await input(args.prompt) - const config = await TuiConfig.get() + const config = await AppRuntime.runPromise( + TuiConfig.Service.use((svc) => svc.get()).pipe(Effect.provide(TuiConfig.layer)), + ) const network = resolveNetworkOptionsNoConfig(args) const external = diff --git a/packages/opencode/src/cli/cmd/uninstall.ts b/packages/opencode/src/cli/cmd/uninstall.ts index 0afdc51854..9dc35a9bd3 100644 --- a/packages/opencode/src/cli/cmd/uninstall.ts +++ b/packages/opencode/src/cli/cmd/uninstall.ts @@ -2,6 +2,7 @@ import type { Argv } from "yargs" import { UI } from "../ui" import * as prompts from "@clack/prompts" import { Installation } from "../../installation" +import { AppRuntime } from "../../effect/app-runtime" import { Global } from "@opencode-ai/core/global" import fs from "fs/promises" import path from "path" @@ -57,7 +58,7 @@ export const UninstallCommand = { UI.empty() prompts.intro("Uninstall OpenCode") - const method = await Installation.method() + const method = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method())) prompts.log.info(`Installation method: ${method}`) const targets = await collectRemovalTargets(args, method) diff --git a/packages/opencode/src/cli/cmd/upgrade.ts b/packages/opencode/src/cli/cmd/upgrade.ts index 3c1604a0b8..26b3ba1867 100644 --- a/packages/opencode/src/cli/cmd/upgrade.ts +++ b/packages/opencode/src/cli/cmd/upgrade.ts @@ -2,7 +2,9 @@ import type { Argv } from "yargs" import { UI } from "../ui" import * as prompts from "@clack/prompts" import { Installation } from "../../installation" +import { AppRuntime } from "../../effect/app-runtime" import { InstallationVersion } from "@opencode-ai/core/installation/version" +import { Effect } from "effect" export const UpgradeCommand = { command: "upgrade [target]", @@ -25,7 +27,7 @@ export const UpgradeCommand = { UI.println(UI.logo(" ")) UI.empty() prompts.intro("Upgrade") - const detectedMethod = await Installation.method() + const detectedMethod = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method())) const method = (args.method as Installation.Method) ?? detectedMethod if (method === "unknown") { prompts.log.error(`opencode is installed to ${process.execPath} and may be managed by a package manager`) @@ -43,7 +45,9 @@ export const UpgradeCommand = { } } prompts.log.info("Using method: " + method) - const target = args.target ? args.target.replace(/^v/, "") : await Installation.latest() + const target = args.target + ? args.target.replace(/^v/, "") + : await AppRuntime.runPromise(Installation.Service.use((svc) => svc.latest())) if (InstallationVersion === target) { prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`) @@ -54,7 +58,15 @@ export const UpgradeCommand = { prompts.log.info(`From ${InstallationVersion} → ${target}`) const spinner = prompts.spinner() spinner.start("Upgrading...") - const err = await Installation.upgrade(method, target).catch((err) => err) + const err = await AppRuntime.runPromise( + Effect.gen(function* () { + const installation = yield* Installation.Service + return yield* installation.upgrade(method, target).pipe( + Effect.map(() => undefined as Installation.UpgradeFailedError | Error | undefined), + Effect.catch((error) => Effect.succeed(error as Installation.UpgradeFailedError | Error | undefined)), + ) + }), + ).catch((err: Error) => err) if (err) { spinner.stop("Upgrade failed", 1) if (err instanceof Installation.UpgradeFailedError) { diff --git a/packages/opencode/src/cli/upgrade.ts b/packages/opencode/src/cli/upgrade.ts index 9f71fcc067..9d49e47c81 100644 --- a/packages/opencode/src/cli/upgrade.ts +++ b/packages/opencode/src/cli/upgrade.ts @@ -4,30 +4,40 @@ import { AppRuntime } from "@/effect/app-runtime" import { Flag } from "@opencode-ai/core/flag/flag" import { Installation } from "@/installation" import { InstallationVersion } from "@opencode-ai/core/installation/version" +import { Effect } from "effect" export async function upgrade() { - const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.getGlobal())) - if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return - const method = await Installation.method() - const latest = await Installation.latest(method).catch(() => {}) - if (!latest) return + await AppRuntime.runPromise( + Effect.gen(function* () { + const cfg = yield* Config.Service + const installation = yield* Installation.Service + const bus = yield* Bus.Service - if (Flag.OPENCODE_ALWAYS_NOTIFY_UPDATE) { - await Bus.publish(Installation.Event.UpdateAvailable, { version: latest }) - return - } + const config = yield* cfg.getGlobal() + if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return + const method = yield* installation.method() + const latest = yield* installation.latest(method).pipe(Effect.catch(() => Effect.succeed(undefined))) + if (!latest) return - if (InstallationVersion === latest) return + if (Flag.OPENCODE_ALWAYS_NOTIFY_UPDATE) { + yield* bus.publish(Installation.Event.UpdateAvailable, { version: latest }) + return + } - const kind = Installation.getReleaseType(InstallationVersion, latest) + if (InstallationVersion === latest) return - if (config.autoupdate === "notify" || kind !== "patch") { - await Bus.publish(Installation.Event.UpdateAvailable, { version: latest }) - return - } + const kind = Installation.getReleaseType(InstallationVersion, latest) - if (method === "unknown") return - await Installation.upgrade(method, latest) - .then(() => Bus.publish(Installation.Event.Updated, { version: latest })) - .catch(() => {}) + if (config.autoupdate === "notify" || kind !== "patch") { + yield* bus.publish(Installation.Event.UpdateAvailable, { version: latest }) + return + } + + if (method === "unknown") return + yield* installation.upgrade(method, latest).pipe( + Effect.flatMap(() => bus.publish(Installation.Event.Updated, { version: latest })), + Effect.catch(() => Effect.void), + ) + }), + ) } diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index e8c4342768..4a6bc3267c 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -7,7 +7,6 @@ import path from "path" import { BusEvent } from "@/bus/bus-event" import { Flag } from "@opencode-ai/core/flag/flag" import * as Log from "@opencode-ai/core/util/log" -import { makeRuntime } from "@opencode-ai/core/effect/runtime" import semver from "semver" import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version" import { NpmConfig } from "@opencode-ai/core/npm-config" @@ -325,10 +324,4 @@ export const defaultLayer = layer.pipe( Layer.provide(CrossSpawnSpawner.defaultLayer), ) -const { runPromise } = makeRuntime(Service, defaultLayer) - -export const latest = (...args: Parameters) => runPromise((s) => s.latest(...args)) -export const method = () => runPromise((s) => s.method()) -export const upgrade = (...args: Parameters) => runPromise((s) => s.upgrade(...args)) - export * as Installation from "." diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 3ca4f074f9..a6f7899723 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -16,7 +16,6 @@ import { Effect, Layer, Context, Schema } from "effect" import * as DateTime from "effect/DateTime" import { InstanceState } from "@/effect/instance-state" import { isOverflow as overflow, usable } from "./overflow" -import { makeRuntime } from "@/effect/run-service" import { serviceUse } from "@/effect/service-use" import { SyncEvent } from "@/sync" import { SessionEvent } from "@/v2/session-event" @@ -649,14 +648,4 @@ export const defaultLayer = Layer.suspend(() => ), ) -const { runPromise } = makeRuntime(Service, defaultLayer) - -export async function isOverflow(input: { tokens: MessageV2.Assistant["tokens"]; model: Provider.Model }) { - return runPromise((svc) => svc.isOverflow(input)) -} - -export async function prune(input: { sessionID: SessionID }) { - return runPromise((svc) => svc.prune(input)) -} - export * as SessionCompaction from "./compaction" diff --git a/packages/opencode/test/cli/run/runtime.boot.test.ts b/packages/opencode/test/cli/run/runtime.boot.test.ts index e2569b0ac6..6731659129 100644 --- a/packages/opencode/test/cli/run/runtime.boot.test.ts +++ b/packages/opencode/test/cli/run/runtime.boot.test.ts @@ -3,10 +3,15 @@ import type { KeyEvent, Renderable } from "@opentui/core" import type { Binding } from "@opentui/keymap" import { createBindingLookup } from "@opentui/keymap/extras" import { OpencodeClient, type Provider } from "@opencode-ai/sdk/v2" -import { TuiConfig, type Resolved } from "@/cli/cmd/tui/config/tui" +import { type Resolved } from "@/cli/cmd/tui/config/tui" import { formatBindings } from "@/cli/cmd/run/keymap.shared" import { TuiKeybind } from "@/cli/cmd/tui/config/keybind" -import { resolveDiffStyle, resolveFooterKeybinds, resolveModelInfo } from "@/cli/cmd/run/runtime.boot" +import { + TuiConfigLoader, + resolveDiffStyle, + resolveFooterKeybinds, + resolveModelInfo, +} from "@/cli/cmd/run/runtime.boot" type RunBinding = Binding @@ -108,7 +113,7 @@ describe("run runtime boot", () => { }) test("reads footer keybinds from resolved keybind config", async () => { - spyOn(TuiConfig, "get").mockResolvedValue( + spyOn(TuiConfigLoader, "get").mockResolvedValue( config({ leader: "ctrl+g", bindings: { @@ -139,7 +144,7 @@ describe("run runtime boot", () => { }) test("falls back to default keybinds when config load fails", async () => { - spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom")) + spyOn(TuiConfigLoader, "get").mockRejectedValue(new Error("boom")) const result = await resolveFooterKeybinds() @@ -156,11 +161,11 @@ describe("run runtime boot", () => { }) test("reads diff style and falls back to auto", async () => { - spyOn(TuiConfig, "get").mockResolvedValue(config({ diff_style: "stacked" })) + spyOn(TuiConfigLoader, "get").mockResolvedValue(config({ diff_style: "stacked" })) await expect(resolveDiffStyle()).resolves.toBe("stacked") mock.restore() - spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom")) + spyOn(TuiConfigLoader, "get").mockRejectedValue(new Error("boom")) await expect(resolveDiffStyle()).resolves.toBe("auto") }) diff --git a/packages/opencode/test/cli/tui/plugin-add.test.ts b/packages/opencode/test/cli/tui/plugin-add.test.ts index c54dbaacaa..625e887113 100644 --- a/packages/opencode/test/cli/tui/plugin-add.test.ts +++ b/packages/opencode/test/cli/tui/plugin-add.test.ts @@ -5,8 +5,6 @@ import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" import { createTuiResolvedConfig } from "../../fixture/tui-runtime" -import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui" - const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime") test("adds tui plugin at runtime from spec", async () => { @@ -35,7 +33,7 @@ test("adds tui plugin at runtime from spec", async () => { const config = createTuiResolvedConfig({ plugin: [], }) - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) try { @@ -77,7 +75,7 @@ test("retries runtime add for file plugins after dependency wait", async () => { const config = createTuiResolvedConfig({ plugin: [], }) - const wait = spyOn(TuiConfig, "waitForDependencies").mockImplementation(async () => { + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockImplementation(async () => { await Bun.write( path.join(tmp.extra.mod, "index.ts"), `export default { diff --git a/packages/opencode/test/cli/tui/plugin-install.test.ts b/packages/opencode/test/cli/tui/plugin-install.test.ts index 50ca4dbad2..bc8d4bb4c4 100644 --- a/packages/opencode/test/cli/tui/plugin-install.test.ts +++ b/packages/opencode/test/cli/tui/plugin-install.test.ts @@ -5,8 +5,6 @@ import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" import { createTuiResolvedConfig } from "../../fixture/tui-runtime" -import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui" - const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime") test("installs plugin without loading it", async () => { @@ -54,7 +52,7 @@ test("installs plugin without loading it", async () => { const config = createTuiResolvedConfig({ plugin: [], }) - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) const api = createTuiPluginApi({ state: { diff --git a/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts b/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts index 35df997e8b..19668d0b5c 100644 --- a/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts @@ -5,7 +5,6 @@ import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" import { createTuiResolvedConfig } from "../../fixture/tui-runtime" -import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui" import { Npm } from "@opencode-ai/core/npm" const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime") @@ -55,7 +54,7 @@ test("loads npm tui plugin from package ./tui export", async () => { }, ], }) - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) @@ -116,7 +115,7 @@ test("does not use npm package exports dot for tui entry", async () => { }, ], }) - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) @@ -178,7 +177,7 @@ test("rejects npm tui export that resolves outside plugin directory", async () = }, ], }) - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) @@ -240,7 +239,7 @@ test("rejects npm tui plugin that exports server and tui together", async () => }, ], }) - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) @@ -298,7 +297,7 @@ test("does not use npm package main for tui entry", async () => { }, ], }) - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) const warn = spyOn(console, "warn").mockImplementation(() => {}) @@ -363,7 +362,7 @@ test("does not use directory package main for tui entry", async () => { }, ], }) - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) try { @@ -410,7 +409,7 @@ test("uses directory index fallback for tui when package.json is missing", async }, ], }) - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) try { @@ -467,7 +466,7 @@ test("uses npm package name when tui plugin id is omitted", async () => { }, ], }) - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined }) diff --git a/packages/opencode/test/cli/tui/plugin-loader-pure.test.ts b/packages/opencode/test/cli/tui/plugin-loader-pure.test.ts index fb4a3bb57d..6177241308 100644 --- a/packages/opencode/test/cli/tui/plugin-loader-pure.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader-pure.test.ts @@ -5,8 +5,6 @@ import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" import { createTuiResolvedConfig } from "../../fixture/tui-runtime" -import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui" - const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime") test("skips external tui plugins in pure mode", async () => { @@ -48,7 +46,7 @@ test("skips external tui plugins in pure mode", async () => { }, ], }) - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) try { diff --git a/packages/opencode/test/cli/tui/plugin-loader.test.ts b/packages/opencode/test/cli/tui/plugin-loader.test.ts index 493520fc00..50f8c6ac01 100644 --- a/packages/opencode/test/cli/tui/plugin-loader.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader.test.ts @@ -7,7 +7,6 @@ import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" 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" const { allThemes, addTheme } = await import("../../../src/cli/cmd/tui/context/theme") @@ -339,7 +338,7 @@ export default { }, }) const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() try { expect(addTheme(tmp.extra.preloadedThemeName, { theme: { primary: "#303030" } })).toBe(true) @@ -539,7 +538,7 @@ test("continues loading when a plugin is missing config metadata", async () => { }, ], }) - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) try { @@ -784,7 +783,7 @@ test("auto-disposes plugin keymap layers", async () => { } }, } as NonNullable[0]>["keymap"] - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) try { @@ -830,7 +829,7 @@ test("plugin keymap proxy preserves real keymap receiver", async () => { }) const harness = createTestKeymap({ defaultKeys: true }) - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) try { @@ -893,7 +892,7 @@ test("auto-disposes plugin keymap transformers", async () => { prependCommandTransformer: track, appendCommandTransformer: track, } as unknown as NonNullable[0]>["keymap"] - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) try { @@ -950,7 +949,7 @@ test("manual onDispose for plugin keymap layers stays idempotent", async () => { } }, } as NonNullable[0]>["keymap"] - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) try { @@ -1021,7 +1020,7 @@ test("updates installed theme when plugin metadata changes", async () => { process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json") const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const mkApi = () => createTuiPluginApi({ diff --git a/packages/opencode/test/cli/tui/plugin-toggle.test.ts b/packages/opencode/test/cli/tui/plugin-toggle.test.ts index a3ee744bff..546c2cfec0 100644 --- a/packages/opencode/test/cli/tui/plugin-toggle.test.ts +++ b/packages/opencode/test/cli/tui/plugin-toggle.test.ts @@ -5,8 +5,6 @@ import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" import { createTuiResolvedConfig } from "../../fixture/tui-runtime" -import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui" - const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime") test("toggles plugin runtime state by exported id", async () => { @@ -53,7 +51,7 @@ test("toggles plugin runtime state by exported id", async () => { }, ], }) - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) const api = createTuiPluginApi() @@ -130,7 +128,7 @@ test("kv plugin_enabled overrides tui config on startup", async () => { }, ], }) - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) const api = createTuiPluginApi() api.kv.set("plugin_enabled", { @@ -160,7 +158,7 @@ test("kv plugin_enabled overrides tui config on startup", async () => { test("loads disabled-by-default internal plugin inactive and activates on demand", async () => { await using tmp = await tmpdir() const config = createTuiResolvedConfig() - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path) const api = createTuiPluginApi() diff --git a/packages/opencode/test/fixture/tui-runtime.ts b/packages/opencode/test/fixture/tui-runtime.ts index 64537b6c50..e22a0141c5 100644 --- a/packages/opencode/test/fixture/tui-runtime.ts +++ b/packages/opencode/test/fixture/tui-runtime.ts @@ -2,6 +2,7 @@ import { spyOn } from "bun:test" import path from "path" import { createBindingLookup } from "@opentui/keymap/extras" import { TuiConfig } from "../../src/cli/cmd/tui/config/tui" +import { TuiPluginRuntime } from "../../src/cli/cmd/tui/plugin/runtime" import { TuiKeybind } from "../../src/cli/cmd/tui/config/keybind" type PluginSpec = string | [string, Record] @@ -34,7 +35,7 @@ export function mockTuiRuntime(dir: string, plugin: PluginSpec[], opts?: { plugi scope: "local" as const, source: path.join(dir, "tui.json"), })) - const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue() + const wait = spyOn(TuiPluginRuntime, "waitForDependencies").mockResolvedValue() const cwd = spyOn(process, "cwd").mockImplementation(() => dir) const config = createTuiResolvedConfig({