feat(tui): add managed config interface

This commit is contained in:
Dax Raad 2026-07-12 16:54:08 -04:00
commit 3568dd1b99
52 changed files with 848 additions and 266 deletions

View file

@ -4,7 +4,6 @@ import { createTestRenderer } from "@opentui/core/testing"
import { Effect } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Global } from "@opencode-ai/core/global"
import { createTuiResolvedConfig } from "./fixture/tui-runtime"
import { createEventStream, createFetch, directory, json } from "./fixture/tui-sdk"
test("SIGHUP clears title and disposes scoped resources once", async () => {
@ -32,7 +31,7 @@ test("SIGHUP clears title and disposes scoped resources once", async () => {
const task = Effect.runPromise(
run({
server: { endpoint: { url: server.url.toString() } },
config: createTuiResolvedConfig({ plugin_enabled: {} }),
config: { get: async () => ({}), update: async () => ({}) },
args: {},
log: () => {},
pluginHost: {
@ -118,7 +117,7 @@ test("session lifecycle updates the terminal title and prints the epilogue after
const task = Effect.runPromise(
run({
server: { endpoint: { url: server.url.toString() } },
config: createTuiResolvedConfig({ plugin_enabled: {} }),
config: { get: async () => ({}), update: async () => ({}) },
args: { sessionID: "dummy" },
log: () => {},
pluginHost: {

View file

@ -8,7 +8,7 @@ import path from "node:path"
import { onCleanup } from "solid-js"
import { tmpdir } from "../../fixture/fixture"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
import type { TuiKeybind } from "../../../src/config/v1/keybind"
import type { TuiKeybind } from "../../../src/config/keybind"
import { TestTuiContexts } from "../../fixture/tui-environment"
async function wait(fn: () => boolean, timeout = 2000) {
@ -41,7 +41,7 @@ async function mountPrompt(input: {
import("../../../src/ui/dialog-prompt"),
import("../../../src/context/kv"),
import("../../../src/context/theme"),
import("../../../src/config/v1"),
import("../../../src/config"),
import("../../../src/ui/toast"),
import("../../../src/keymap"),
])
@ -51,7 +51,7 @@ async function mountPrompt(input: {
const keymap = createDefaultOpenTuiKeymap(renderer)
const resolvedConfig = createTuiResolvedConfig({
keybinds: input.keybinds,
leader_timeout: 1000,
leader: { timeout: 1000 },
})
const off = registerOpencodeKeymap(keymap, renderer, resolvedConfig)
onCleanup(off)

View file

@ -6,7 +6,7 @@ import type { JSX } from "solid-js"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
import { KVProvider } from "../../../src/context/kv"
import { ThemeProvider } from "../../../src/context/theme"
import { TuiConfigProvider } from "../../../src/config/v1"
import { TuiConfigProvider } from "../../../src/config"
import { DiffViewerFileTree } from "../../../src/feature-plugins/system/diff-viewer-file-tree"
import { TestTuiContexts } from "../../fixture/tui-environment"
import {

View file

@ -7,9 +7,9 @@ import type { TuiPluginApi, TuiPluginMeta, TuiRouteCurrent, TuiRouteDefinition }
import type { Session } from "@opencode-ai/sdk/v2"
import { KVProvider } from "../../../src/context/kv"
import { ThemeProvider } from "../../../src/context/theme"
import { TuiConfigProvider } from "../../../src/config/v1"
import { TuiConfigProvider } from "../../../src/config"
import { SDKProvider } from "../../../src/context/sdk"
import { TuiKeybind } from "../../../src/config/v1/keybind"
import { TuiKeybind } from "../../../src/config/keybind"
import { OpencodeKeymapProvider } from "../../../src/keymap"
import diffViewerPlugin from "../../../src/feature-plugins/system/diff-viewer"
import { createTuiPluginApi } from "../../fixture/tui-plugin"

View file

@ -10,7 +10,7 @@ import type { FormWithLocation } from "../../../src/context/data"
import { KVProvider } from "../../../src/context/kv"
import { SDKProvider } from "../../../src/context/sdk"
import { ThemeProvider } from "../../../src/context/theme"
import { TuiConfigProvider } from "../../../src/config/v1"
import { TuiConfigProvider } from "../../../src/config"
import { OpencodeKeymapProvider, registerOpencodeKeymap } from "../../../src/keymap"
import { ToastProvider } from "../../../src/ui/toast"
import { tmpdir } from "../../fixture/fixture"

View file

@ -0,0 +1,67 @@
/** @jsxImportSource @opentui/solid */
import { testRender } from "@opentui/solid"
import { expect, test } from "bun:test"
import {
resolve,
TuiConfigProvider,
useTuiConfig,
useTuiConfigService,
type Interface,
} from "../src/config"
test("resolves nested config and keybind defaults", () => {
const config = resolve(
{
keybinds: { leader: "ctrl+o" },
leader: { timeout: 500 },
scroll: { speed: 2, acceleration: true },
diffs: { view: "split" },
},
{ terminalSuspend: true },
)
expect(config.leader.timeout).toBe(500)
expect(config.keybinds.get("leader")?.[0]?.key).toBe("ctrl+o")
expect(config.scroll).toEqual({ speed: 2, acceleration: true })
expect(config.diffs).toEqual({ view: "split" })
})
test("provides config and its host interface", async () => {
const config = resolve({}, { terminalSuspend: true })
let current = {}
const service: Interface = {
get: async () => current,
update: async (update) => {
const draft: Record<string, any> = { ...current }
update(draft)
current = draft
return draft
},
}
let configService: Interface | undefined
function Consumer() {
const config = useTuiConfig()
configService = useTuiConfigService()
return <text>{`${config.mouse ? "mouse" : "none"} ${config.keybinds.get("leader")?.[0]?.key}`}</text>
}
const app = await testRender(() => (
<TuiConfigProvider config={config} service={service}>
<Consumer />
</TuiConfigProvider>
))
try {
await app.renderOnce()
expect(app.captureCharFrame()).toContain("mouse ctrl+x")
if (!configService) throw new Error("Config service was not provided")
await configService.update((draft) => {
draft.mouse = false
draft.keybinds = { leader: "ctrl+o" }
})
await app.renderOnce()
expect(app.captureCharFrame()).toContain("none ctrl+o")
} finally {
app.renderer.destroy()
}
})

View file

@ -1,10 +1,10 @@
import { resolve, type Info, type Resolved } from "../../src/config/v1"
import { TuiKeybind } from "../../src/config/v1/keybind"
import { resolve, type Info, type Resolved } from "../../src/config"
import { TuiKeybind } from "../../src/config/keybind"
type ResolvedInput = Omit<Info, "attention" | "keybinds" | "leader_timeout"> & {
type ResolvedInput = Omit<Info, "attention" | "keybinds" | "leader"> & {
attention?: Partial<Resolved["attention"]>
keybinds?: Partial<TuiKeybind.Keybinds>
leader_timeout?: number
leader?: { timeout?: number }
}
export function createTuiResolvedConfig(input: ResolvedInput = {}) {

View file

@ -4,7 +4,7 @@ import { createBindingLookup } from "@opentui/keymap/extras"
import { testRender, useRenderer } from "@opentui/solid"
import { expect, test } from "bun:test"
import { onCleanup } from "solid-js"
import { TuiKeybind } from "../src/config/v1/keybind"
import { TuiKeybind } from "../src/config/keybind"
import {
formatKeySequence,
getOpencodeModeStack,