tui: ensure TUI plugins load with proper project context when multiple directories are open

Fixes potential plugin resolution issues when switching between projects by wrapping
plugin loading in Instance.provide(). This ensures each plugin resolves dependencies
relative to its correct project directory instead of inheriting context from whatever
instance happened to be active.

Also reorganizes config loading code into focused modules (command.ts, managed.ts,
plugin.ts) to make the codebase easier to maintain and test.
This commit is contained in:
Dax Raad
2026-04-16 12:40:24 -04:00
parent b28956f0db
commit bfffc3c2c6
8 changed files with 262 additions and 267 deletions
+14 -15
View File
@@ -1,7 +1,7 @@
import { test, expect, describe, mock, afterEach, beforeEach, spyOn } from "bun:test"
import { Deferred, Effect, Fiber, Layer, Option } from "effect"
import { test, expect, describe, mock, afterEach, beforeEach } from "bun:test"
import { Effect, Layer, Option } from "effect"
import { NodeFileSystem, NodePath } from "@effect/platform-node"
import { Config } from "../../src/config"
import { Config, ConfigManaged } from "../../src/config"
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
import { Instance } from "../../src/project/instance"
@@ -10,7 +10,7 @@ import { AccessToken, Account, AccountID, OrgID } from "../../src/account"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Env } from "../../src/env"
import { provideTmpdirInstance } from "../fixture/fixture"
import { tmpdir, tmpdirScoped } from "../fixture/fixture"
import { tmpdir } from "../fixture/fixture"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { testEffect } from "../lib/effect"
@@ -24,7 +24,6 @@ import { pathToFileURL } from "url"
import { Global } from "../../src/global"
import { ProjectID } from "../../src/project/schema"
import { Filesystem } from "../../src/util"
import * as Network from "../../src/util/network"
import { ConfigPlugin } from "@/config/plugin"
import { Npm } from "@opencode-ai/shared/npm"
@@ -1860,14 +1859,14 @@ describe("resolvePluginSpec", () => {
})
const file = path.join(tmp.path, "opencode.json")
const hit = await Config.resolvePluginSpec("./plugin", file)
expect(Config.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin", "index.ts")).href)
const hit = await ConfigPlugin.resolvePluginSpec("./plugin", file)
expect(ConfigPlugin.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin", "index.ts")).href)
})
})
describe("deduplicatePluginOrigins", () => {
const dedupe = (plugins: Config.PluginSpec[]) =>
Config.deduplicatePluginOrigins(
ConfigPlugin.deduplicatePluginOrigins(
plugins.map((spec) => ({
spec,
source: "",
@@ -1937,8 +1936,8 @@ describe("deduplicatePluginOrigins", () => {
const config = await load()
const plugins = config.plugin ?? []
expect(plugins.some((p) => Config.pluginSpecifier(p) === "my-plugin@1.0.0")).toBe(true)
expect(plugins.some((p) => Config.pluginSpecifier(p).startsWith("file://"))).toBe(true)
expect(plugins.some((p) => ConfigPlugin.pluginSpecifier(p) === "my-plugin@1.0.0")).toBe(true)
expect(plugins.some((p) => ConfigPlugin.pluginSpecifier(p).startsWith("file://"))).toBe(true)
},
})
})
@@ -2209,7 +2208,7 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => {
// parseManagedPlist unit tests — pure function, no OS interaction
test("parseManagedPlist strips MDM metadata keys", async () => {
const config = await Config.parseManagedPlist(
const config = await ConfigManaged.parseManagedPlist(
JSON.stringify({
PayloadDisplayName: "OpenCode Managed",
PayloadIdentifier: "ai.opencode.managed.test",
@@ -2231,7 +2230,7 @@ test("parseManagedPlist strips MDM metadata keys", async () => {
})
test("parseManagedPlist parses server settings", async () => {
const config = await Config.parseManagedPlist(
const config = await ConfigManaged.parseManagedPlist(
JSON.stringify({
$schema: "https://opencode.ai/config.json",
server: { hostname: "127.0.0.1", mdns: false },
@@ -2245,7 +2244,7 @@ test("parseManagedPlist parses server settings", async () => {
})
test("parseManagedPlist parses permission rules", async () => {
const config = await Config.parseManagedPlist(
const config = await ConfigManaged.parseManagedPlist(
JSON.stringify({
$schema: "https://opencode.ai/config.json",
permission: {
@@ -2269,7 +2268,7 @@ test("parseManagedPlist parses permission rules", async () => {
})
test("parseManagedPlist parses enabled_providers", async () => {
const config = await Config.parseManagedPlist(
const config = await ConfigManaged.parseManagedPlist(
JSON.stringify({
$schema: "https://opencode.ai/config.json",
enabled_providers: ["anthropic", "google"],
@@ -2280,7 +2279,7 @@ test("parseManagedPlist parses enabled_providers", async () => {
})
test("parseManagedPlist handles empty config", async () => {
const config = await Config.parseManagedPlist(
const config = await ConfigManaged.parseManagedPlist(
JSON.stringify({ $schema: "https://opencode.ai/config.json" }),
"test:mobileconfig",
)