refactor(tui): simplify config context

This commit is contained in:
Dax Raad 2026-07-12 17:12:19 -04:00
commit f8f8cc9546
27 changed files with 135 additions and 142 deletions

View file

@ -35,15 +35,17 @@ export default Runtime.handler(Commands, (input) =>
),
)
preflight.loading()
const configService = yield* Config.Service
const config = yield* Config.Service
let disposeSlots: (() => void) | undefined
const runFork = Effect.runForkWith(yield* Effect.context())
const context = yield* Effect.context()
const runFork = Effect.runForkWith(context)
const runPromise = Effect.runPromiseWith(context)
yield* run({
server,
args: { continue: input.continue, sessionID: Option.getOrUndefined(input.session) },
config: {
get: () => Effect.runPromise(configService.get()),
update: (update) => Effect.runPromise(configService.update(update)),
get: () => runPromise(config.get()),
update: (update) => runPromise(config.update(update)),
},
terminalHandoff: () => preflight.finish(),
log: (level, message, tags) => {

View file

@ -1,5 +1,5 @@
import { TuiConfig } from "@opencode-ai/tui/config"
import { Config } from "@opencode-ai/tui/config"
import { Schema } from "effect"
export const Info = Schema.Struct({ ...TuiConfig.Info.fields })
export const Info = Schema.Struct({ ...Config.Info.fields })
export type Info = Schema.Schema.Type<typeof Info>

View file

@ -75,7 +75,7 @@ import * as Model from "./util/model"
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 { Config, ConfigProvider, useConfig } from "./config"
import { TuiConfigV1 } from "./config/v1"
import { createTuiApiAdapters } from "./plugin/adapters"
import { createTuiApi } from "./plugin/api"
@ -154,7 +154,7 @@ export type TuiInput = {
reload?: () => Promise<void>
}
args: Args
config: TuiConfig.Interface | TuiConfigV1.Resolved
config: Config.Interface | TuiConfigV1.Resolved
onSnapshot?: () => Promise<string[]>
pluginHost: TuiPluginHost
terminalHandoff?: () => Promise<
@ -200,7 +200,7 @@ function isVersionGreater(left: string, right: string) {
return a.prerelease.localeCompare(b.prerelease, undefined, { numeric: true }) > 0
}
function fromV1(config: TuiConfigV1.Resolved): TuiConfig.Info {
function fromV1(config: TuiConfigV1.Resolved): Config.Info {
return {
theme: config.theme ? { name: config.theme } : undefined,
plugins: config.plugin?.map((plugin) =>
@ -217,7 +217,7 @@ function fromV1(config: TuiConfigV1.Resolved): TuiConfig.Info {
}
}
function isConfigInterface(config: TuiConfig.Interface | TuiConfigV1.Resolved): config is TuiConfig.Interface {
function isConfigInterface(config: Config.Interface | TuiConfigV1.Resolved): config is Config.Interface {
return "get" in config && typeof config.get === "function" && "update" in config && typeof config.update === "function"
}
@ -235,7 +235,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
}
return { service: undefined, info: fromV1(configInput), legacy: configInput }
})
const config = TuiConfig.resolve(loaded.info, { terminalSuspend: process.platform !== "win32" })
const config = Config.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)
@ -371,7 +371,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
<ClipboardProvider>
<OpencodeKeymapProvider keymap={keymap}>
<ArgsProvider {...input.args}>
<TuiConfigProvider
<ConfigProvider
config={config}
service={loaded.service}
options={{ terminalSuspend: process.platform !== "win32" }}
@ -439,7 +439,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
</RouteProvider>
</ToastProvider>
</KVProvider>
</TuiConfigProvider>
</ConfigProvider>
</ArgsProvider>
</OpencodeKeymapProvider>
</ClipboardProvider>
@ -477,7 +477,7 @@ function App(props: {
}) {
const log = useLog({ component: "app" })
const startup = useTuiStartup()
const tuiConfig = useTuiConfig()
const config = useConfig().data
const route = useRoute()
const dimensions = useTerminalDimensions()
const renderer = useRenderer()
@ -496,7 +496,7 @@ function App(props: {
const exit = useExit()
const promptRef = usePromptRef()
const pluginRuntime = usePluginRuntime()
const attention = createTuiAttention({ renderer, config: tuiConfig, kv })
const attention = createTuiAttention({ renderer, config, kv })
const clipboard = useClipboard()
// Toast once when an MCP server enters a failed or needs-auth state so the user knows to act,
@ -530,7 +530,7 @@ function App(props: {
const api = createTuiApi(
createTuiApiAdapters({
version: InstallationVersion,
tuiConfig,
tuiConfig: config,
dialog,
keymap,
kv,
@ -1070,11 +1070,11 @@ function App(props: {
useBindings(() => ({
mode: OPENCODE_BASE_MODE,
bindings: tuiConfig.keybinds.gather("app", appBindingCommands),
bindings: config.keybinds.gather("app", appBindingCommands),
}))
useBindings(() => ({
bindings: tuiConfig.keybinds.gather("app.global", appGlobalBindingCommands),
bindings: config.keybinds.gather("app.global", appGlobalBindingCommands),
}))
useBindings(() => ({
@ -1084,7 +1084,7 @@ function App(props: {
if (!current?.focused) return true
return current.current.text === ""
},
bindings: tuiConfig.keybinds.gather("app_exit", ["app.exit"]),
bindings: config.keybinds.gather("app_exit", ["app.exit"]),
}))
event.on("tui.command.execute", (evt, { workspace }) => {

View file

@ -10,7 +10,7 @@ import type {
TuiAttentionSoundPack,
TuiAttentionSoundPackInfo,
} from "@opencode-ai/plugin/tui"
import { AttentionSoundName, type TuiConfig } from "./config"
import { AttentionSoundName, type Config } from "./config"
import { Schema } from "effect"
import stripAnsi from "strip-ansi"
import * as TuiAudio from "./audio"
@ -80,7 +80,7 @@ function clampVolume(volume: number) {
return Math.min(1, Math.max(0, volume))
}
function soundVolume(input: TuiAttentionNotifyInput, config: Pick<TuiConfig.Resolved, "attention">) {
function soundVolume(input: TuiAttentionNotifyInput, config: Pick<Config.Resolved, "attention">) {
if (!config.attention.sound) return
if (input.sound === false) return
if (input.sound === undefined) return clampVolume(config.attention.volume)
@ -113,7 +113,7 @@ function focusSkip(when: TuiAttentionWhen, focus: FocusState) {
export function createTuiAttention(input: {
renderer: AttentionRenderer
config: Pick<TuiConfig.Resolved, "attention">
config: Pick<Config.Resolved, "attention">
kv?: TuiKV
audio?: Pick<typeof TuiAudio, "loadSoundFile" | "play">
}): TuiAttentionHost {

View file

@ -8,7 +8,7 @@ import {
useKeymapSelector,
useOpencodeKeymap,
} from "../keymap"
import { useTuiConfig } from "../config"
import { useConfig } from "../config"
type PaletteCommandEntry = ReturnType<OpenTuiKeymap["getCommandEntries"]>[number]
@ -24,7 +24,7 @@ function isSuggestedPaletteCommand(entry: PaletteCommandEntry) {
}
export function CommandPaletteDialog() {
const config = useTuiConfig()
const config = useConfig().data
const keymap = useOpencodeKeymap()
const entries = useKeymapSelector((keymap: OpenTuiKeymap) => {
const query = {

View file

@ -9,7 +9,7 @@ import type { McpServer } from "@opencode-ai/client"
import { useClipboard } from "../context/clipboard"
import { useToast } from "../ui/toast"
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
import { useTuiConfig } from "../config"
import { useConfig } from "../config"
import { getScrollAcceleration } from "../util/scroll"
import { useBindings } from "../keymap"
@ -118,7 +118,7 @@ function DialogMcpError(props: { server: McpServer; onBack: () => void }) {
const toast = useToast()
const { theme } = useTheme()
const dimensions = useTerminalDimensions()
const tuiConfig = useTuiConfig()
const config = useConfig().data
const [copied, setCopied] = createSignal(false)
const error = () => statusMeta(props.server.status, theme).error ?? "Unknown MCP connection error"
const height = createMemo(() => Math.max(3, Math.floor(dimensions().height / 2) - 5))
@ -164,7 +164,7 @@ function DialogMcpError(props: { server: McpServer; onBack: () => void }) {
ref={(element: ScrollBoxRenderable) => (scroll = element)}
height={height()}
scrollbarOptions={{ visible: false }}
scrollAcceleration={getScrollAcceleration(tuiConfig)}
scrollAcceleration={getScrollAcceleration(config)}
>
<text fg={theme.text} wrapMode="word">
{error()}

View file

@ -1,7 +1,7 @@
import { InputRenderable, TextAttributes } from "@opentui/core"
import { Slug } from "@opencode-ai/core/util/slug"
import { createSignal, onMount } from "solid-js"
import { useTuiConfig } from "../config"
import { useConfig } from "../config"
import { useTheme } from "../context/theme"
import { useBindings, useCommandShortcut } from "../keymap"
import { useDialog, type DialogContext } from "../ui/dialog"
@ -9,7 +9,7 @@ import { useDialog, type DialogContext } from "../ui/dialog"
export function DialogProjectCopyName(props: { onConfirm: (name: string) => void }) {
const dialog = useDialog()
const { theme } = useTheme()
const tuiConfig = useTuiConfig()
const config = useConfig().data
const generateShortcut = useCommandShortcut("dialog.project_copy.generate")
const [inputTarget, setInputTarget] = createSignal<InputRenderable>()
let input: InputRenderable
@ -35,7 +35,7 @@ export function DialogProjectCopyName(props: { onConfirm: (name: string) => void
run: generate,
},
],
bindings: tuiConfig.keybinds.get("dialog.project_copy.generate"),
bindings: config.keybinds.get("dialog.project_copy.generate"),
}))
onMount(() => {

View file

@ -5,7 +5,7 @@ import { createMemo, For } from "solid-js"
import { createStore } from "solid-js/store"
import { FilePath } from "../ui/file-path"
import { useTheme } from "../context/theme"
import { useTuiConfig } from "../config"
import { useConfig } from "../config"
import { useDialog, type DialogContext } from "../ui/dialog"
import { getScrollAcceleration } from "../util/scroll"
@ -32,9 +32,9 @@ export function DialogWorkspaceFileChanges(props: {
}) {
const dialog = useDialog()
const { theme } = useTheme()
const tuiConfig = useTuiConfig()
const config = useConfig().data
const dimensions = useTerminalDimensions()
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
const [store, setStore] = createStore({ active: "yes" as WorkspaceFileChangesChoice })
const height = createMemo(() => Math.min(props.files.length, 8))
const fileNameWidth = createMemo(

View file

@ -11,7 +11,7 @@ import { useSDK } from "../../context/sdk"
import { useData } from "../../context/data"
import { getScrollAcceleration } from "../../util/scroll"
import { useTuiPaths } from "../../context/runtime"
import { useTuiConfig } from "../../config"
import { useConfig } from "../../config"
import { useLocation } from "../../context/location"
import { useTheme, selectedForeground } from "../../context/theme"
import { SplitBorder } from "../../ui/border"
@ -92,7 +92,7 @@ export function Autocomplete(props: {
const { theme } = useTheme()
const dimensions = useTerminalDimensions()
const frecency = useFrecency()
const tuiConfig = useTuiConfig()
const config = useConfig().data
const paths = useTuiPaths()
const location = useLocation()
const [store, setStore] = createStore({
@ -628,7 +628,7 @@ export function Autocomplete(props: {
},
},
],
bindings: tuiConfig.keybinds.gather("prompt.autocomplete", [
bindings: config.keybinds.gather("prompt.autocomplete", [
"prompt.autocomplete.prev",
"prompt.autocomplete.next",
"prompt.autocomplete.hide",
@ -714,7 +714,7 @@ export function Autocomplete(props: {
})
let scroll: ScrollBoxRenderable
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
return (
<box

View file

@ -48,7 +48,7 @@ import { createFadeIn } from "../../util/signal"
import { DialogSkill } from "../dialog-skill"
import { useArgs } from "../../context/args"
import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut, useLeaderActive, useOpencodeKeymap } from "../../keymap"
import { useTuiConfig } from "../../config"
import { useConfig } from "../../config"
import { usePromptMove } from "./move"
import { readLocalAttachment } from "./local-attachment"
import { useData } from "../../context/data"
@ -154,7 +154,7 @@ export function Prompt(props: PromptProps) {
const project = useProject()
const data = useData()
const currentLocation = useLocation()
const tuiConfig = useTuiConfig()
const config = useConfig().data
const dialog = useDialog()
const toast = useToast()
const status = createMemo(() => data.session.status(props.sessionID ?? ""))
@ -538,7 +538,7 @@ export function Prompt(props: PromptProps) {
useBindings(() => ({
mode: OPENCODE_BASE_MODE,
bindings: tuiConfig.keybinds.gather("prompt.palette", [
bindings: config.keybinds.gather("prompt.palette", [
"prompt.submit",
"prompt.editor",
"prompt.editor_context.clear",
@ -779,7 +779,7 @@ export function Prompt(props: PromptProps) {
return {
target: inputTarget,
enabled: inputTarget() !== undefined && !props.disabled,
bindings: tuiConfig.keybinds.get("prompt.paste"),
bindings: config.keybinds.get("prompt.paste"),
}
})
@ -787,7 +787,7 @@ export function Prompt(props: PromptProps) {
return {
target: inputTarget,
enabled: inputTarget() !== undefined && !props.disabled && store.prompt.text !== "",
bindings: tuiConfig.keybinds.get("prompt.clear"),
bindings: config.keybinds.get("prompt.clear"),
}
})
@ -870,7 +870,7 @@ export function Prompt(props: PromptProps) {
},
},
],
bindings: tuiConfig.keybinds.get("prompt.history.previous"),
bindings: config.keybinds.get("prompt.history.previous"),
}
})
@ -910,7 +910,7 @@ export function Prompt(props: PromptProps) {
},
},
],
bindings: tuiConfig.keybinds.get("prompt.history.next"),
bindings: config.keybinds.get("prompt.history.next"),
}
})

View file

@ -1,4 +1,4 @@
export * as TuiConfig from "."
export * as Config from "."
import { createBindingLookup } from "@opentui/keymap/extras"
import { Schema } from "effect"
@ -176,9 +176,12 @@ export function resolve(input: Info, options: { terminalSuspend: boolean }): Res
}
}
const ConfigContext = createContext<{ config: Resolved; service?: Interface }>()
const ConfigContext = createContext<{
data: Resolved
update: Interface["update"]
}>()
export function TuiConfigProvider(props: {
export function ConfigProvider(props: {
config: Resolved
service?: Interface
options?: { terminalSuspend: boolean }
@ -186,31 +189,21 @@ export function TuiConfigProvider(props: {
}) {
const [config, setConfig] = createStore(props.config)
const host = props.service
const service = host
? {
get: host.get,
update: async (update: (draft: any) => void) => {
const info = await host.update(update)
setConfig(reconcile(resolve(info, props.options ?? { terminalSuspend: true })))
return info
},
}
: undefined
return <ConfigContext.Provider value={{ config, service }}>{props.children}</ConfigContext.Provider>
const update = async (update: (draft: any) => void) => {
if (!host) throw new Error("Config updates are not available")
const info = await host.update(update)
setConfig(reconcile(resolve(info, props.options ?? { terminalSuspend: true })))
return info
}
return <ConfigContext.Provider value={{ data: config, update }}>{props.children}</ConfigContext.Provider>
}
export function useTuiConfig() {
export function useConfig() {
const value = useContext(ConfigContext)
if (!value) throw new Error("TuiConfigProvider is missing")
return value.config
if (!value) throw new Error("ConfigProvider is missing")
return value
}
export function useTuiConfigOptional() {
return useContext(ConfigContext)?.config
}
export function useTuiConfigService() {
const value = useContext(ConfigContext)
if (!value?.service) throw new Error("TuiConfig service is missing")
return value.service
export function useConfigOptional() {
return useContext(ConfigContext)
}

View file

@ -6,12 +6,12 @@ import { Global } from "@opencode-ai/core/global"
import { readJson, writeJsonAtomic } from "../util/persistence"
import { useTuiPaths } from "./runtime"
import path from "path"
import { useTuiConfigOptional, type TuiConfig } from "../config"
import { useConfigOptional, type Config } from "../config"
export const { use: useKV, provider: KVProvider } = createSimpleContext({
name: "KV",
init: (props: { config?: TuiConfig.Info }) => {
const config = props.config ?? useTuiConfigOptional()
init: (props: { config?: Config.Info }) => {
const config = props.config ?? useConfigOptional()?.data
const paths = useTuiPaths()
void Global.Path.state
const file = path.join(paths.state, "kv.json")
@ -80,7 +80,7 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
},
})
function configValues(config: TuiConfig.Info) {
function configValues(config: Config.Info) {
const values: Record<string, any> = {}
if (config.theme?.name !== undefined) values.theme = config.theme.name
if (config.theme?.mode !== undefined) {

View file

@ -23,7 +23,7 @@ import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { createSimpleContext } from "./helper"
import { useKV } from "./kv"
import { useTuiConfig } from "../config"
import { useConfig } from "../config"
import { Global } from "@opencode-ai/core/global"
import { Glob } from "@opencode-ai/core/util/glob"
import { readFile } from "node:fs/promises"
@ -103,7 +103,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
name: "Theme",
init: (props: { mode: "dark" | "light"; source?: ThemeSource }) => {
const renderer = useRenderer()
const config = useTuiConfig()
const config = useConfig().data
const kv = useKV()
const themes = props.source ?? themeSource
const pick = (value: unknown) => {

View file

@ -14,7 +14,7 @@ import {
} from "@opentui/keymap/extras"
import { KeymapProvider, useKeymap, useKeymapSelector, useBindings } from "@opentui/keymap/solid"
import { createMemo, type Accessor } from "solid-js"
import { useTuiConfig } from "./config"
import { useConfig } from "./config"
import { TuiKeybind } from "./config/keybind"
export const LEADER_TOKEN = "leader"
@ -252,7 +252,7 @@ export function useLeaderActive(): Accessor<boolean> {
}
export function useCommandShortcut(command: string): Accessor<string> {
const config = useTuiConfig()
const config = useConfig().data
return useKeymapSelector((keymap: OpenTuiKeymap) =>
formatKeySequence(
keymap.getCommandBindings({ visibility: "registered", commands: [command] }).get(command)?.[0]?.sequence,

View file

@ -1,5 +1,5 @@
import type { TuiDialogSelectOption, TuiPluginApi, TuiSlotProps } from "@opencode-ai/plugin/tui"
import type { TuiConfig } from "../config"
import type { Config } from "../config"
import type { useEvent } from "../context/event"
import type { useRoute } from "../context/route"
import type { useSDK } from "../context/sdk"
@ -23,7 +23,7 @@ export { createPluginRoutes, createTuiApi } from "./api"
type Input = {
version: string
tuiConfig: TuiConfig.Resolved
tuiConfig: Config.Resolved
dialog: ReturnType<typeof useDialog>
keymap: ReturnType<typeof useOpencodeKeymap>
kv: ReturnType<typeof useKV>

View file

@ -10,7 +10,7 @@ import { useSDK } from "../../context/sdk"
import { useClipboard } from "../../context/clipboard"
import { SplitBorder } from "../../ui/border"
import { useToast } from "../../ui/toast"
import { useTuiConfig } from "../../config"
import { useConfig } from "../../config"
import { useBindings, useOpencodeModeStack } from "../../keymap"
const FORM_MODE = "form"
@ -149,7 +149,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
const { theme } = useTheme()
const renderer = useRenderer()
const dimensions = useTerminalDimensions()
const tuiConfig = useTuiConfig()
const config = useConfig().data
const modeStack = useOpencodeModeStack()
const clipboard = useClipboard()
const toast = useToast()
@ -590,7 +590,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
setStore("editing", false)
},
},
...tuiConfig.keybinds.get("prompt.clear"),
...config.keybinds.get("prompt.clear"),
{
key: "tab",
desc: "Next field",
@ -691,7 +691,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
},
{ key: "c", desc: "Copy link", group: "Form", cmd: copyExternal },
{ key: "escape", desc: "Dismiss form", group: "Form", cmd: cancel },
...tuiConfig.keybinds.get("app.exit"),
...config.keybinds.get("app.exit"),
]
: confirm()
? [
@ -711,7 +711,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
{ key: "k", desc: "Scroll review", group: "Form", cmd: () => review?.scrollBy(-1) },
{ key: "down", desc: "Scroll review", group: "Form", cmd: () => review?.scrollBy(1) },
{ key: "j", desc: "Scroll review", group: "Form", cmd: () => review?.scrollBy(1) },
...tuiConfig.keybinds.get("app.exit"),
...config.keybinds.get("app.exit"),
]
: [
...Array.from({ length: max }, (_, index) => ({
@ -754,7 +754,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
group: "Form",
cmd: cancel,
},
...tuiConfig.keybinds.get("app.exit"),
...config.keybinds.get("app.exit"),
]),
],
}

View file

@ -64,7 +64,7 @@ import { FormPrompt } from "./form"
import { DialogExportOptions } from "../../ui/dialog-export-options"
import { DialogExportResult } from "../../ui/dialog-export-result"
import { sessionEpilogue } from "../../util/presentation"
import { useTuiConfig } from "../../config"
import { useConfig } from "../../config"
import { useClipboard } from "../../context/clipboard"
import { nextThinkingMode, reasoningSummary, useThinkingMode, type ThinkingMode } from "../../context/thinking"
import { getScrollAcceleration } from "../../util/scroll"
@ -127,7 +127,7 @@ const context = createContext<{
groupExploration: () => boolean
diffWrapMode: () => "word" | "none"
models: () => ModelInfo[]
tui: ReturnType<typeof useTuiConfig>
config: ReturnType<typeof useConfig>["data"]
}>()
function use() {
@ -149,7 +149,7 @@ export function Session() {
const data = useData()
const project = useProject()
const paths = useTuiPaths()
const tuiConfig = useTuiConfig()
const config = useConfig().data
const kv = useKV()
const { theme } = useTheme()
const promptRef = usePromptRef()
@ -217,7 +217,7 @@ export function Session() {
const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)
const models = createMemo(() => data.location.model.list(location()) ?? [])
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
const toast = useToast()
const sdk = useSDK()
const editor = useEditorContext()
@ -835,17 +835,17 @@ export function Session() {
}))
useBindings(() => ({
bindings: tuiConfig.keybinds.gather("session.global", sessionGlobalBindingCommands),
bindings: config.keybinds.gather("session.global", sessionGlobalBindingCommands),
}))
useBindings(() => ({
enabled: () => renderer.currentFocusedEditor === null,
bindings: tuiConfig.keybinds.gather("session.global.unfocused", sessionGlobalUnfocusedBindingCommands),
bindings: config.keybinds.gather("session.global.unfocused", sessionGlobalUnfocusedBindingCommands),
}))
useBindings(() => ({
mode: OPENCODE_BASE_MODE,
bindings: tuiConfig.keybinds.gather("session", sessionBindingCommands),
bindings: config.keybinds.gather("session", sessionBindingCommands),
}))
// snap to bottom when session changes
@ -871,7 +871,7 @@ export function Session() {
groupExploration,
diffWrapMode,
models,
tui: tuiConfig,
config,
}}
>
<box flexDirection="row" flexGrow={1} minHeight={0}>
@ -2465,7 +2465,7 @@ function Edit(props: ToolProps) {
const pathFormatter = usePathFormatter()
const view = createMemo(() => {
const diffView = ctx.tui.diffs?.view
const diffView = ctx.config.diffs?.view
if (diffView === "unified") return "unified"
if (diffView === "split") return "split"
// Default to "auto" behavior
@ -2542,8 +2542,8 @@ function ApplyPatch(props: ToolProps) {
})
})
const view = createMemo(() => {
if (ctx.tui.diffs?.view === "unified") return "unified"
if (ctx.tui.diffs?.view === "split") return "split"
if (ctx.config.diffs?.view === "unified") return "unified"
if (ctx.config.diffs?.view === "split") return "split"
return ctx.width > 120 ? "split" : "unified"
})

View file

@ -12,7 +12,7 @@ import { filetype } from "../../util/filetype"
import { Locale } from "../../util/locale"
import { webSearchProviderLabel } from "../../util/tool-display"
import { getScrollAcceleration } from "../../util/scroll"
import { useTuiConfig } from "../../config"
import { useConfig } from "../../config"
import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut } from "../../keymap"
import { usePathFormatter } from "../../context/path-format"
@ -22,7 +22,7 @@ function EditBody(props: { request: PermissionV2Request; patch?: string }) {
const themeState = useTheme()
const theme = themeState.theme
const syntax = themeState.syntax
const config = useTuiConfig()
const config = useConfig().data
const dimensions = useTerminalDimensions()
const filepath = createMemo(() => {
@ -470,7 +470,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: () => void }) {
let input: TextareaRenderable
const { theme } = useTheme()
const tuiConfig = useTuiConfig()
const config = useConfig().data
const dimensions = useTerminalDimensions()
const narrow = createMemo(() => dimensions().width < 80)
useBindings(() => ({
@ -487,7 +487,7 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: (
],
bindings: [
{ key: "escape", desc: "Cancel permission rejection", group: "Permission", cmd: () => props.onCancel() },
...tuiConfig.keybinds.get("app.exit"),
...config.keybinds.get("app.exit"),
{
key: "return",
desc: "Confirm permission rejection",
@ -558,7 +558,7 @@ function Prompt<const T extends Record<string, string>>(props: {
onSelect: (option: keyof T) => void
}) {
const { theme } = useTheme()
const tuiConfig = useTuiConfig()
const config = useConfig().data
const dimensions = useTerminalDimensions()
const keys = Object.keys(props.options) as (keyof T)[]
const [store, setStore] = createStore({
@ -647,8 +647,8 @@ function Prompt<const T extends Record<string, string>>(props: {
},
]
: []),
...(props.escapeKey ? tuiConfig.keybinds.get("app.exit") : []),
...(props.fullscreen ? tuiConfig.keybinds.get("permission.prompt.fullscreen") : []),
...(props.escapeKey ? config.keybinds.get("app.exit") : []),
...(props.fullscreen ? config.keybinds.get("permission.prompt.fullscreen") : []),
],
}))

View file

@ -1,7 +1,7 @@
import { useData } from "../../context/data"
import { createMemo, Show } from "solid-js"
import { useTheme } from "../../context/theme"
import { useTuiConfig } from "../../config"
import { useConfig } from "../../config"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { usePluginRuntime } from "../../plugin/runtime"
@ -11,9 +11,9 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
const pluginRuntime = usePluginRuntime()
const data = useData()
const { theme } = useTheme()
const tuiConfig = useTuiConfig()
const config = useConfig().data
const session = createMemo(() => data.session.get(props.sessionID))
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
return (
<Show when={session()}>

View file

@ -3,7 +3,7 @@ import { useTheme } from "../context/theme"
import { useDialog, type DialogContext } from "./dialog"
import { Show, createEffect, createSignal, onMount, type JSX } from "solid-js"
import { Spinner } from "../component/spinner"
import { useTuiConfig } from "../config"
import { useConfig } from "../config"
import { useBindings, useCommandShortcut } from "../keymap"
export type DialogPromptProps = {
@ -20,7 +20,7 @@ export type DialogPromptProps = {
export function DialogPrompt(props: DialogPromptProps) {
const dialog = useDialog()
const { theme } = useTheme()
const tuiConfig = useTuiConfig()
const config = useConfig().data
const submitShortcut = useCommandShortcut("dialog.prompt.submit")
const [textareaTarget, setTextareaTarget] = createSignal<TextareaRenderable>()
let textarea: TextareaRenderable
@ -43,7 +43,7 @@ export function DialogPrompt(props: DialogPromptProps) {
run: confirm,
},
],
bindings: tuiConfig.keybinds.gather("dialog.prompt", ["dialog.prompt.submit"]),
bindings: config.keybinds.gather("dialog.prompt", ["dialog.prompt.submit"]),
}))
onMount(() => {

View file

@ -17,7 +17,7 @@ import { isDeepEqual } from "remeda"
import { useDialog, type DialogContext } from "./dialog"
import { Locale } from "../util/locale"
import { getScrollAcceleration } from "../util/scroll"
import { useTuiConfig } from "../config"
import { useConfig } from "../config"
import { formatKeyBindings, useBindings, useKeymapSelector } from "../keymap"
export interface DialogSelectProps<T> {
@ -86,8 +86,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
const dialog = useDialog()
const { theme } = useTheme()
const tuiConfig = useTuiConfig()
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
const config = useConfig().data
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
const [store, setStore] = createStore({
selected: 0,
@ -130,7 +130,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
const labels = new Map<string, string>()
for (const action of shownActions()) {
const label = formatKeyBindings(actionBindings().get(action.command), tuiConfig)
const label = formatKeyBindings(actionBindings().get(action.command), config)
if (label) labels.set(action.command, label)
}
@ -450,7 +450,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
})),
],
bindings: [
...tuiConfig.keybinds.gather("dialog.select", [
...config.keybinds.gather("dialog.select", [
"dialog.select.prev",
"dialog.select.next",
"dialog.select.page_up",
@ -459,7 +459,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
"dialog.select.end",
"dialog.select.submit",
]),
...visible.flatMap((item) => tuiConfig.keybinds.get(item.command)),
...visible.flatMap((item) => config.keybinds.get(item.command)),
...(visible.length
? [
{

View file

@ -17,12 +17,12 @@ export class CustomSpeedScroll implements ScrollAcceleration {
reset(): void {}
}
export function getScrollAcceleration(tuiConfig?: ScrollConfig): ScrollAcceleration {
if (tuiConfig?.scroll?.acceleration) {
export function getScrollAcceleration(config?: ScrollConfig): ScrollAcceleration {
if (config?.scroll?.acceleration) {
return new MacOSScrollAccel()
}
if (tuiConfig?.scroll?.speed !== undefined) {
return new CustomSpeedScroll(tuiConfig.scroll.speed)
if (config?.scroll?.speed !== undefined) {
return new CustomSpeedScroll(config.scroll.speed)
}
return new CustomSpeedScroll(3)

View file

@ -33,7 +33,7 @@ async function mountPrompt(input: {
{ DialogPrompt },
{ KVProvider },
{ ThemeProvider },
{ TuiConfigProvider },
{ ConfigProvider },
{ ToastProvider },
{ OpencodeKeymapProvider, registerOpencodeKeymap },
] = await Promise.all([
@ -66,7 +66,7 @@ async function mountPrompt(input: {
}}
>
<OpencodeKeymapProvider keymap={keymap}>
<TuiConfigProvider config={resolvedConfig}>
<ConfigProvider config={resolvedConfig}>
<KVProvider>
<ThemeProvider mode="dark">
<ToastProvider>
@ -76,7 +76,7 @@ async function mountPrompt(input: {
</ToastProvider>
</ThemeProvider>
</KVProvider>
</TuiConfigProvider>
</ConfigProvider>
</OpencodeKeymapProvider>
</TestTuiContexts>
)

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"
import { ConfigProvider } from "../../../src/config"
import { DiffViewerFileTree } from "../../../src/feature-plugins/system/diff-viewer-file-tree"
import { TestTuiContexts } from "../../fixture/tui-environment"
import {
@ -181,11 +181,11 @@ async function captureSettledFrame(app: Awaited<ReturnType<typeof testRender>>)
function withTheme(component: () => JSX.Element) {
return (
<TestTuiContexts>
<TuiConfigProvider config={createTuiResolvedConfig()}>
<ConfigProvider config={createTuiResolvedConfig()}>
<KVProvider>
<ThemeProvider mode="dark">{component()}</ThemeProvider>
</KVProvider>
</TuiConfigProvider>
</ConfigProvider>
</TestTuiContexts>
)
}

View file

@ -7,7 +7,7 @@ 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"
import { ConfigProvider } from "../../../src/config"
import { SDKProvider } from "../../../src/context/sdk"
import { TuiKeybind } from "../../../src/config/keybind"
import { OpencodeKeymapProvider } from "../../../src/keymap"
@ -173,13 +173,13 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?:
<TestTuiContexts>
<SDKProvider client={createClient(transport.fetch)} api={createApi(transport.fetch)}>
<OpencodeKeymapProvider keymap={keymap}>
<TuiConfigProvider config={config}>
<ConfigProvider config={config}>
<KVProvider>
<ThemeProvider mode="dark">
{renderDiff?.({ params: "params" in current ? current.params : undefined })}
</ThemeProvider>
</KVProvider>
</TuiConfigProvider>
</ConfigProvider>
</OpencodeKeymapProvider>
</SDKProvider>
</TestTuiContexts>

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"
import { ConfigProvider } from "../../../src/config"
import { OpencodeKeymapProvider, registerOpencodeKeymap } from "../../../src/keymap"
import { ToastProvider } from "../../../src/ui/toast"
import { tmpdir } from "../../fixture/fixture"
@ -76,7 +76,7 @@ async function mountForm(root: string, width = 80) {
}}
>
<OpencodeKeymapProvider keymap={keymap}>
<TuiConfigProvider config={config}>
<ConfigProvider config={config}>
<SDKProvider client={createClient(transport.fetch)} api={createApi(transport.fetch)}>
<KVProvider>
<ThemeProvider mode="dark" source={{ discover: () => Promise.resolve({}) }}>
@ -86,7 +86,7 @@ async function mountForm(root: string, width = 80) {
</ThemeProvider>
</KVProvider>
</SDKProvider>
</TuiConfigProvider>
</ConfigProvider>
</OpencodeKeymapProvider>
</ClipboardProvider>
</TestTuiContexts>

View file

@ -3,9 +3,8 @@ import { testRender } from "@opentui/solid"
import { expect, test } from "bun:test"
import {
resolve,
TuiConfigProvider,
useTuiConfig,
useTuiConfigService,
ConfigProvider,
useConfig,
type Interface,
} from "../src/config"
@ -38,24 +37,23 @@ test("provides config and its host interface", async () => {
return draft
},
}
let configService: Interface | undefined
let context: ReturnType<typeof useConfig> | undefined
function Consumer() {
const config = useTuiConfig()
configService = useTuiConfigService()
return <text>{`${config.mouse ? "mouse" : "none"} ${config.keybinds.get("leader")?.[0]?.key}`}</text>
context = useConfig()
return <text>{`${context.data.mouse ? "mouse" : "none"} ${context.data.keybinds.get("leader")?.[0]?.key}`}</text>
}
const app = await testRender(() => (
<TuiConfigProvider config={config} service={service}>
<ConfigProvider config={config} service={service}>
<Consumer />
</TuiConfigProvider>
</ConfigProvider>
))
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) => {
if (!context) throw new Error("Config context was not provided")
await context.update((draft) => {
draft.mouse = false
draft.keybinds = { leader: "ctrl+o" }
})