diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index 1608deb5bab..1f6ae642935 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -24,7 +24,7 @@ import { type SequenceBindingLike, } from "@opentui/keymap/extras" import type { JSX, SolidPlugin } from "@opentui/solid" -import type { PluginOptions } from "./index.js" +import type { Config as PluginConfig, PluginOptions } from "./index.js" export type { CliRenderer, KeyEvent, Renderable, SlotMode } from "@opentui/core" export { stringifyKeySequence, stringifyKeyStroke } from "@opentui/keymap" @@ -408,13 +408,16 @@ type TuiAttentionConfigView = { } type TuiConfigView = { - theme?: { - name?: string - mode?: "system" | "dark" | "light" - } + $schema?: string + theme?: string | { name?: string; mode?: "system" | "dark" | "light" } + plugin?: PluginConfig["plugin"] plugins?: ReadonlyArray }> - leader: { timeout: number } + plugin_enabled?: Record + leader?: { timeout: number } + leader_timeout?: number scroll?: { speed?: number; acceleration?: boolean } + scroll_speed?: number + scroll_acceleration?: { enabled: boolean } attention: TuiAttentionConfigView diffs?: { wrap?: "word" | "none" @@ -422,8 +425,11 @@ type TuiConfigView = { single?: boolean view?: "auto" | "split" | "unified" } + diff_style?: "auto" | "stacked" terminal?: { title?: boolean } - prompt?: { editor?: boolean; paste?: "compact" | "full" } + prompt?: + | { editor?: boolean; paste?: "compact" | "full" } + | { max_height?: number; max_width?: number | "auto" } session?: { sidebar?: "auto" | "hide" scrollbar?: boolean diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 5ce5a7b57dd..6c8abc1ea15 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -76,6 +76,7 @@ import { ArgsProvider, useArgs, type Args } from "./context/args" import open from "open" import { PromptRefProvider, usePromptRef } from "./context/prompt" import { TuiConfig, TuiConfigProvider, useTuiConfig } from "./config" +import { TuiConfigV1 } from "./config/v1" import { createTuiApiAdapters } from "./plugin/adapters" import { createTuiApi } from "./plugin/api" import { createPluginRuntime, PluginRuntimeProvider, usePluginRuntime, type TuiPluginHost } from "./plugin/runtime" @@ -153,7 +154,7 @@ export type TuiInput = { reload?: () => Promise } args: Args - config: TuiConfig.Interface + config: TuiConfig.Interface | TuiConfigV1.Resolved onSnapshot?: () => Promise pluginHost: TuiPluginHost terminalHandoff?: () => Promise< @@ -199,11 +200,43 @@ function isVersionGreater(left: string, right: string) { return a.prerelease.localeCompare(b.prerelease, undefined, { numeric: true }) > 0 } +function fromV1(config: TuiConfigV1.Resolved): TuiConfig.Info { + return { + theme: config.theme ? { name: config.theme } : undefined, + plugins: config.plugin?.map((plugin) => + typeof plugin === "string" ? plugin : { package: plugin[0], options: plugin[1] }, + ), + leader: { timeout: config.leader_timeout }, + scroll: + config.scroll_speed === undefined && config.scroll_acceleration === undefined + ? undefined + : { speed: config.scroll_speed, acceleration: config.scroll_acceleration?.enabled }, + attention: config.attention, + diffs: config.diff_style === undefined ? undefined : { view: config.diff_style === "stacked" ? "unified" : "auto" }, + mouse: config.mouse, + } +} + +function isConfigInterface(config: TuiConfig.Interface | TuiConfigV1.Resolved): config is TuiConfig.Interface { + return "get" in config && typeof config.get === "function" && "update" in config && typeof config.update === "function" +} + export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { const log = input.log ?? (() => {}) const global = yield* Global.Service - const configInfo = yield* Effect.tryPromise(() => input.config.get()) - const config = TuiConfig.resolve(configInfo, { terminalSuspend: process.platform !== "win32" }) + const configInput = input.config + const loaded = yield* Effect.gen(function* () { + if (isConfigInterface(configInput)) { + return { + service: configInput, + info: yield* Effect.tryPromise(() => configInput.get()), + legacy: undefined, + } + } + return { service: undefined, info: fromV1(configInput), legacy: configInput } + }) + const config = TuiConfig.resolve(loaded.info, { terminalSuspend: process.platform !== "win32" }) + if (loaded.legacy) config.keybinds = loaded.legacy.keybinds const options = { baseUrl: input.server.endpoint.url, headers: Service.headers(input.server.endpoint) } const api = OpenCode.make(options) const directory = yield* Effect.tryPromise(() => api.file.list({ location: { directory: process.cwd() } })).pipe( @@ -340,7 +373,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { @@ -378,6 +411,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { Promise; pluginHost: TuiPluginHost; pair?: DialogPairCredentials }) { +function App(props: { + onSnapshot?: () => Promise + pluginHost: TuiPluginHost + pluginConfig: any + pair?: DialogPairCredentials +}) { const log = useLog({ component: "app" }) const startup = useTuiStartup() const tuiConfig = useTuiConfig() @@ -512,7 +551,7 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi props.pluginHost .start({ api, - config: tuiConfig, + config: props.pluginConfig, runtime: pluginRuntime, dispose: () => attention.dispose(), }) diff --git a/packages/tui/src/plugin/runtime.tsx b/packages/tui/src/plugin/runtime.tsx index 4130ac9be7c..edd9571515c 100644 --- a/packages/tui/src/plugin/runtime.tsx +++ b/packages/tui/src/plugin/runtime.tsx @@ -4,7 +4,6 @@ import type { TuiPluginInstallResult, TuiPluginStatus, } from "@opencode-ai/plugin/tui" -import type { TuiConfig } from "../config" import { createContext, createSignal, useContext, type JSX, type ParentProps } from "solid-js" import { createPluginRoutes } from "./api" import { createSlots, type HostSlots } from "./slots" @@ -61,7 +60,7 @@ export type PluginRuntime = ReturnType export type TuiPluginHost = { start(input: { api: TuiPluginApi - config: TuiConfig.Resolved + config: any runtime: PluginRuntime dispose?: () => void }): Promise