fix(tui): preserve legacy host compatibility

This commit is contained in:
Dax Raad 2026-07-12 16:59:09 -04:00
commit b2bdab24e6
3 changed files with 59 additions and 15 deletions

View file

@ -24,7 +24,7 @@ import {
type SequenceBindingLike, type SequenceBindingLike,
} from "@opentui/keymap/extras" } from "@opentui/keymap/extras"
import type { JSX, SolidPlugin } from "@opentui/solid" 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 type { CliRenderer, KeyEvent, Renderable, SlotMode } from "@opentui/core"
export { stringifyKeySequence, stringifyKeyStroke } from "@opentui/keymap" export { stringifyKeySequence, stringifyKeyStroke } from "@opentui/keymap"
@ -408,13 +408,16 @@ type TuiAttentionConfigView = {
} }
type TuiConfigView = { type TuiConfigView = {
theme?: { $schema?: string
name?: string theme?: string | { name?: string; mode?: "system" | "dark" | "light" }
mode?: "system" | "dark" | "light" plugin?: PluginConfig["plugin"]
}
plugins?: ReadonlyArray<string | { package: string; options?: Record<string, any> }> plugins?: ReadonlyArray<string | { package: string; options?: Record<string, any> }>
leader: { timeout: number } plugin_enabled?: Record<string, boolean>
leader?: { timeout: number }
leader_timeout?: number
scroll?: { speed?: number; acceleration?: boolean } scroll?: { speed?: number; acceleration?: boolean }
scroll_speed?: number
scroll_acceleration?: { enabled: boolean }
attention: TuiAttentionConfigView attention: TuiAttentionConfigView
diffs?: { diffs?: {
wrap?: "word" | "none" wrap?: "word" | "none"
@ -422,8 +425,11 @@ type TuiConfigView = {
single?: boolean single?: boolean
view?: "auto" | "split" | "unified" view?: "auto" | "split" | "unified"
} }
diff_style?: "auto" | "stacked"
terminal?: { title?: boolean } terminal?: { title?: boolean }
prompt?: { editor?: boolean; paste?: "compact" | "full" } prompt?:
| { editor?: boolean; paste?: "compact" | "full" }
| { max_height?: number; max_width?: number | "auto" }
session?: { session?: {
sidebar?: "auto" | "hide" sidebar?: "auto" | "hide"
scrollbar?: boolean scrollbar?: boolean

View file

@ -76,6 +76,7 @@ import { ArgsProvider, useArgs, type Args } from "./context/args"
import open from "open" import open from "open"
import { PromptRefProvider, usePromptRef } from "./context/prompt" import { PromptRefProvider, usePromptRef } from "./context/prompt"
import { TuiConfig, TuiConfigProvider, useTuiConfig } from "./config" import { TuiConfig, TuiConfigProvider, useTuiConfig } from "./config"
import { TuiConfigV1 } from "./config/v1"
import { createTuiApiAdapters } from "./plugin/adapters" import { createTuiApiAdapters } from "./plugin/adapters"
import { createTuiApi } from "./plugin/api" import { createTuiApi } from "./plugin/api"
import { createPluginRuntime, PluginRuntimeProvider, usePluginRuntime, type TuiPluginHost } from "./plugin/runtime" import { createPluginRuntime, PluginRuntimeProvider, usePluginRuntime, type TuiPluginHost } from "./plugin/runtime"
@ -153,7 +154,7 @@ export type TuiInput = {
reload?: () => Promise<void> reload?: () => Promise<void>
} }
args: Args args: Args
config: TuiConfig.Interface config: TuiConfig.Interface | TuiConfigV1.Resolved
onSnapshot?: () => Promise<string[]> onSnapshot?: () => Promise<string[]>
pluginHost: TuiPluginHost pluginHost: TuiPluginHost
terminalHandoff?: () => Promise< terminalHandoff?: () => Promise<
@ -199,11 +200,43 @@ function isVersionGreater(left: string, right: string) {
return a.prerelease.localeCompare(b.prerelease, undefined, { numeric: true }) > 0 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) { export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
const log = input.log ?? (() => {}) const log = input.log ?? (() => {})
const global = yield* Global.Service const global = yield* Global.Service
const configInfo = yield* Effect.tryPromise(() => input.config.get()) const configInput = input.config
const config = TuiConfig.resolve(configInfo, { terminalSuspend: process.platform !== "win32" }) 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 options = { baseUrl: input.server.endpoint.url, headers: Service.headers(input.server.endpoint) }
const api = OpenCode.make(options) const api = OpenCode.make(options)
const directory = yield* Effect.tryPromise(() => api.file.list({ location: { directory: process.cwd() } })).pipe( 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) {
<ArgsProvider {...input.args}> <ArgsProvider {...input.args}>
<TuiConfigProvider <TuiConfigProvider
config={config} config={config}
service={input.config} service={loaded.service}
options={{ terminalSuspend: process.platform !== "win32" }} options={{ terminalSuspend: process.platform !== "win32" }}
> >
<KVProvider> <KVProvider>
@ -378,6 +411,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
<App <App
onSnapshot={input.onSnapshot} onSnapshot={input.onSnapshot}
pluginHost={input.pluginHost} pluginHost={input.pluginHost}
pluginConfig={loaded.legacy ?? config}
pair={ pair={
input.server.endpoint.auth input.server.endpoint.auth
? input.server.endpoint.auth ? input.server.endpoint.auth
@ -435,7 +469,12 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
}) })
}) })
function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPluginHost; pair?: DialogPairCredentials }) { function App(props: {
onSnapshot?: () => Promise<string[]>
pluginHost: TuiPluginHost
pluginConfig: any
pair?: DialogPairCredentials
}) {
const log = useLog({ component: "app" }) const log = useLog({ component: "app" })
const startup = useTuiStartup() const startup = useTuiStartup()
const tuiConfig = useTuiConfig() const tuiConfig = useTuiConfig()
@ -512,7 +551,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
props.pluginHost props.pluginHost
.start({ .start({
api, api,
config: tuiConfig, config: props.pluginConfig,
runtime: pluginRuntime, runtime: pluginRuntime,
dispose: () => attention.dispose(), dispose: () => attention.dispose(),
}) })

View file

@ -4,7 +4,6 @@ import type {
TuiPluginInstallResult, TuiPluginInstallResult,
TuiPluginStatus, TuiPluginStatus,
} from "@opencode-ai/plugin/tui" } from "@opencode-ai/plugin/tui"
import type { TuiConfig } from "../config"
import { createContext, createSignal, useContext, type JSX, type ParentProps } from "solid-js" import { createContext, createSignal, useContext, type JSX, type ParentProps } from "solid-js"
import { createPluginRoutes } from "./api" import { createPluginRoutes } from "./api"
import { createSlots, type HostSlots } from "./slots" import { createSlots, type HostSlots } from "./slots"
@ -61,7 +60,7 @@ export type PluginRuntime = ReturnType<typeof createPluginRuntime>
export type TuiPluginHost = { export type TuiPluginHost = {
start(input: { start(input: {
api: TuiPluginApi api: TuiPluginApi
config: TuiConfig.Resolved config: any
runtime: PluginRuntime runtime: PluginRuntime
dispose?: () => void dispose?: () => void
}): Promise<void> }): Promise<void>