Compare commits

...
Author SHA1 Message Date
Kit Langton 9a74b79834 fix(tui): persist plugin activation toggles 2026-08-13 17:03:50 -04:00
5 changed files with 55 additions and 6 deletions
+1
View File
@@ -1,4 +1,5 @@
export * as Config from "."
export * as ConfigPlugin from "./plugin"
import { createBindingLookup } from "@opentui/keymap/extras"
import { Schema } from "effect"
+5
View File
@@ -0,0 +1,5 @@
export function setEnabled(draft: { plugins?: unknown[] }, id: string, enabled: boolean) {
const plugins = Array.isArray(draft.plugins) ? draft.plugins : []
draft.plugins = plugins.filter((entry) => entry !== id && entry !== `-${id}`)
draft.plugins.push(enabled ? id : `-${id}`)
}
@@ -4,6 +4,7 @@ import { usePlugin } from "../../plugin/context"
import { DialogSelect, type DialogSelectOption } from "../../ui/dialog-select"
import { useDialog } from "../../ui/dialog"
import { DialogErrorDetails } from "../../component/dialog-error-details"
import { ConfigPlugin, useConfig } from "../../config"
const id = "opencode.plugins"
@@ -12,6 +13,7 @@ function View(props: { context: Plugin.Context; plugins: ReturnType<typeof usePl
const [focused, setFocused] = createSignal<string>()
const [detail, setDetail] = createSignal<{ title: string; error: string }>()
const dialog = useDialog()
const config = useConfig()
const options = createMemo(() => {
const builtins = props.plugins
.registered()
@@ -65,10 +67,9 @@ function View(props: { context: Plugin.Context; plugins: ReturnType<typeof usePl
const current = props.plugins.registered().find((item) => item.id === plugin.value)
if (!current) return
setLocked(true)
void (current.active ? props.plugins.deactivate(current.id) : props.plugins.activate(current.id))
.then((ok) => {
if (ok) return
props.context.ui.toast.show({ variant: "error", message: `Failed to update plugin ${current.id}` })
void config
.update((draft) => {
ConfigPlugin.setEnabled(draft, current.id, !current.active)
})
.catch((error) => {
props.context.ui.toast.show({
+31 -2
View File
@@ -30,7 +30,7 @@ async function until(read: () => Promise<string>, expected: (value: string | und
return value
}
async function bootApp(directory: string) {
async function bootApp(directory: string, config: Record<string, unknown> = {}) {
const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
const core = await import("@opentui/core")
mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer }))
@@ -53,7 +53,13 @@ async function bootApp(directory: string) {
run({
app: { name: "test", version: "test", channel: "test" },
server: { endpoint: { url: server.url.toString() } },
config: { get: async () => ({}), update: async () => ({}) },
config: {
get: async () => config,
update: async (update) => {
update(config)
return config
},
},
packages: { resolve: async () => undefined },
args: {},
log: () => {},
@@ -217,6 +223,29 @@ test("editing one plugin leaves others untouched and a broken save keeps the las
await app.task
})
test("editing one plugin does not reactivate a plugin disabled by configuration", async () => {
await using tmp = await tmpdir()
const directory = path.join(tmp.path, ".opencode", "plugins", "tui")
await mkdir(directory, { recursive: true })
const markerA = path.join(tmp.path, "a.txt")
const markerB = path.join(tmp.path, "b.txt")
await writeFile(path.join(directory, "a.ts"), lifecycleSource(markerA, "test.a", "a1"))
const sourceB = path.join(directory, "b.ts")
await writeFile(sourceB, lifecycleSource(markerB, "test.b", "b1"))
await using app = await bootApp(tmp.path, { plugins: ["-test.a"] })
const readB = () => readFile(markerB, "utf8")
expect(await until(readB, (value) => value === "b1:setup\n")).toBe("b1:setup\n")
expect(await readFile(markerA, "utf8").catch(() => undefined)).toBeUndefined()
await writeFile(sourceB, lifecycleSource(markerB, "test.b", "b2"))
expect(await until(readB, (value) => value?.includes("b2:setup") ?? false)).toBe("b1:setup\nb1:cleanup\nb2:setup\n")
expect(await readFile(markerA, "utf8").catch(() => undefined)).toBeUndefined()
process.emit("SIGHUP")
await app.task
})
test("a save whose setup throws restores the previous version", async () => {
await using tmp = await tmpdir()
const directory = path.join(tmp.path, ".opencode", "plugins", "tui")
+13
View File
@@ -0,0 +1,13 @@
import { expect, test } from "bun:test"
import { ConfigPlugin } from "../src/config"
test("plugin toggles replace exact directives without disturbing source declarations", () => {
const source = { package: "/tmp/recap.ts", options: { compact: true } }
const config = { plugins: [source, "kit.session-recap", "-kit.session-recap", "other"] }
ConfigPlugin.setEnabled(config, "kit.session-recap", false)
expect(config.plugins).toEqual([source, "other", "-kit.session-recap"])
ConfigPlugin.setEnabled(config, "kit.session-recap", true)
expect(config.plugins).toEqual([source, "other", "kit.session-recap"])
})