From f8f8cc95468e225d3ecacaf179d399c8f0e3f215 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 12 Jul 2026 17:12:19 -0400 Subject: [PATCH] refactor(tui): simplify config context --- packages/cli/src/commands/handlers/default.ts | 10 +++-- packages/cli/src/config/schema.ts | 4 +- packages/tui/src/app.tsx | 26 +++++------ packages/tui/src/attention.ts | 6 +-- .../tui/src/component/command-palette.tsx | 4 +- packages/tui/src/component/dialog-mcp.tsx | 6 +-- .../component/dialog-project-copy-name.tsx | 6 +-- .../dialog-workspace-file-changes.tsx | 6 +-- .../tui/src/component/prompt/autocomplete.tsx | 8 ++-- packages/tui/src/component/prompt/index.tsx | 14 +++--- packages/tui/src/config/index.tsx | 43 ++++++++----------- packages/tui/src/context/kv.tsx | 8 ++-- packages/tui/src/context/theme.tsx | 4 +- packages/tui/src/keymap.tsx | 4 +- packages/tui/src/plugin/adapters.tsx | 4 +- packages/tui/src/routes/session/form.tsx | 12 +++--- packages/tui/src/routes/session/index.tsx | 22 +++++----- .../tui/src/routes/session/permission.tsx | 14 +++--- packages/tui/src/routes/session/sidebar.tsx | 6 +-- packages/tui/src/ui/dialog-prompt.tsx | 6 +-- packages/tui/src/ui/dialog-select.tsx | 12 +++--- packages/tui/src/util/scroll.ts | 8 ++-- .../tui/test/cli/tui/dialog-prompt.test.tsx | 6 +-- .../cli/tui/diff-viewer-file-tree.test.tsx | 6 +-- .../tui/test/cli/tui/diff-viewer.test.tsx | 6 +-- packages/tui/test/cli/tui/form.test.tsx | 6 +-- packages/tui/test/config-v2.test.tsx | 20 ++++----- 27 files changed, 135 insertions(+), 142 deletions(-) diff --git a/packages/cli/src/commands/handlers/default.ts b/packages/cli/src/commands/handlers/default.ts index 63b4fb122c..474d2d184d 100644 --- a/packages/cli/src/commands/handlers/default.ts +++ b/packages/cli/src/commands/handlers/default.ts @@ -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) => { diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index 3b83db86d7..fc6b9724c6 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -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 diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 6c8abc1ea1..0fd4982767 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -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 } args: Args - config: TuiConfig.Interface | TuiConfigV1.Resolved + config: Config.Interface | TuiConfigV1.Resolved onSnapshot?: () => Promise 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) { - - + @@ -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 }) => { diff --git a/packages/tui/src/attention.ts b/packages/tui/src/attention.ts index b309edcd9d..948872a26b 100644 --- a/packages/tui/src/attention.ts +++ b/packages/tui/src/attention.ts @@ -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) { +function soundVolume(input: TuiAttentionNotifyInput, config: Pick) { 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 + config: Pick kv?: TuiKV audio?: Pick }): TuiAttentionHost { diff --git a/packages/tui/src/component/command-palette.tsx b/packages/tui/src/component/command-palette.tsx index 3dd6829c54..ff7b863678 100644 --- a/packages/tui/src/component/command-palette.tsx +++ b/packages/tui/src/component/command-palette.tsx @@ -8,7 +8,7 @@ import { useKeymapSelector, useOpencodeKeymap, } from "../keymap" -import { useTuiConfig } from "../config" +import { useConfig } from "../config" type PaletteCommandEntry = ReturnType[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 = { diff --git a/packages/tui/src/component/dialog-mcp.tsx b/packages/tui/src/component/dialog-mcp.tsx index 0ef74bd551..5d8b379d53 100644 --- a/packages/tui/src/component/dialog-mcp.tsx +++ b/packages/tui/src/component/dialog-mcp.tsx @@ -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)} > {error()} diff --git a/packages/tui/src/component/dialog-project-copy-name.tsx b/packages/tui/src/component/dialog-project-copy-name.tsx index 7ac357e1c8..3ae331da59 100644 --- a/packages/tui/src/component/dialog-project-copy-name.tsx +++ b/packages/tui/src/component/dialog-project-copy-name.tsx @@ -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() 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(() => { diff --git a/packages/tui/src/component/dialog-workspace-file-changes.tsx b/packages/tui/src/component/dialog-workspace-file-changes.tsx index bca389c368..415b70623a 100644 --- a/packages/tui/src/component/dialog-workspace-file-changes.tsx +++ b/packages/tui/src/component/dialog-workspace-file-changes.tsx @@ -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( diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index 91b4bbdb42..3f01827880 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -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 ( 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"), } }) diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index 2cfcd41359..74fb00f7d7 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -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 {props.children} + 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 {props.children} } -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) } diff --git a/packages/tui/src/context/kv.tsx b/packages/tui/src/context/kv.tsx index ae7face0cd..95c088f194 100644 --- a/packages/tui/src/context/kv.tsx +++ b/packages/tui/src/context/kv.tsx @@ -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 = {} if (config.theme?.name !== undefined) values.theme = config.theme.name if (config.theme?.mode !== undefined) { diff --git a/packages/tui/src/context/theme.tsx b/packages/tui/src/context/theme.tsx index 4a9008b2b5..5d72ea3bbc 100644 --- a/packages/tui/src/context/theme.tsx +++ b/packages/tui/src/context/theme.tsx @@ -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) => { diff --git a/packages/tui/src/keymap.tsx b/packages/tui/src/keymap.tsx index 20376c5e6e..71286dbe02 100644 --- a/packages/tui/src/keymap.tsx +++ b/packages/tui/src/keymap.tsx @@ -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 { } export function useCommandShortcut(command: string): Accessor { - const config = useTuiConfig() + const config = useConfig().data return useKeymapSelector((keymap: OpenTuiKeymap) => formatKeySequence( keymap.getCommandBindings({ visibility: "registered", commands: [command] }).get(command)?.[0]?.sequence, diff --git a/packages/tui/src/plugin/adapters.tsx b/packages/tui/src/plugin/adapters.tsx index b97f7b9b73..f7a440c029 100644 --- a/packages/tui/src/plugin/adapters.tsx +++ b/packages/tui/src/plugin/adapters.tsx @@ -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 keymap: ReturnType kv: ReturnType diff --git a/packages/tui/src/routes/session/form.tsx b/packages/tui/src/routes/session/form.tsx index cde48466a9..d09673d775 100644 --- a/packages/tui/src/routes/session/form.tsx +++ b/packages/tui/src/routes/session/form.tsx @@ -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"), ]), ], } diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 65a6896a6f..bf36219421 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -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 + config: ReturnType["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, }} > @@ -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" }) diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx index de592ecccf..a9382c85d3 100644 --- a/packages/tui/src/routes/session/permission.tsx +++ b/packages/tui/src/routes/session/permission.tsx @@ -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>(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>(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") : []), ], })) diff --git a/packages/tui/src/routes/session/sidebar.tsx b/packages/tui/src/routes/session/sidebar.tsx index d2ebcd2851..c32e47af6e 100644 --- a/packages/tui/src/routes/session/sidebar.tsx +++ b/packages/tui/src/routes/session/sidebar.tsx @@ -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 ( diff --git a/packages/tui/src/ui/dialog-prompt.tsx b/packages/tui/src/ui/dialog-prompt.tsx index f518fb2950..76cef90070 100644 --- a/packages/tui/src/ui/dialog-prompt.tsx +++ b/packages/tui/src/ui/dialog-prompt.tsx @@ -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() 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(() => { diff --git a/packages/tui/src/ui/dialog-select.tsx b/packages/tui/src/ui/dialog-select.tsx index 28235531e2..78563bcb86 100644 --- a/packages/tui/src/ui/dialog-select.tsx +++ b/packages/tui/src/ui/dialog-select.tsx @@ -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 { @@ -86,8 +86,8 @@ export function DialogSelect(props: DialogSelectProps) { 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(props: DialogSelectProps) { const labels = new Map() 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(props: DialogSelectProps) { })), ], 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(props: DialogSelectProps) { "dialog.select.end", "dialog.select.submit", ]), - ...visible.flatMap((item) => tuiConfig.keybinds.get(item.command)), + ...visible.flatMap((item) => config.keybinds.get(item.command)), ...(visible.length ? [ { diff --git a/packages/tui/src/util/scroll.ts b/packages/tui/src/util/scroll.ts index 758e18ae88..6d0d0ce080 100644 --- a/packages/tui/src/util/scroll.ts +++ b/packages/tui/src/util/scroll.ts @@ -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) diff --git a/packages/tui/test/cli/tui/dialog-prompt.test.tsx b/packages/tui/test/cli/tui/dialog-prompt.test.tsx index 4b185fdef1..b4b567b9e2 100644 --- a/packages/tui/test/cli/tui/dialog-prompt.test.tsx +++ b/packages/tui/test/cli/tui/dialog-prompt.test.tsx @@ -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: { }} > - + @@ -76,7 +76,7 @@ async function mountPrompt(input: { - + ) diff --git a/packages/tui/test/cli/tui/diff-viewer-file-tree.test.tsx b/packages/tui/test/cli/tui/diff-viewer-file-tree.test.tsx index 2a5a172f9c..30ec95565c 100644 --- a/packages/tui/test/cli/tui/diff-viewer-file-tree.test.tsx +++ b/packages/tui/test/cli/tui/diff-viewer-file-tree.test.tsx @@ -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>) function withTheme(component: () => JSX.Element) { return ( - + {component()} - + ) } diff --git a/packages/tui/test/cli/tui/diff-viewer.test.tsx b/packages/tui/test/cli/tui/diff-viewer.test.tsx index c882e67f14..36fff6f135 100644 --- a/packages/tui/test/cli/tui/diff-viewer.test.tsx +++ b/packages/tui/test/cli/tui/diff-viewer.test.tsx @@ -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?: - + {renderDiff?.({ params: "params" in current ? current.params : undefined })} - + diff --git a/packages/tui/test/cli/tui/form.test.tsx b/packages/tui/test/cli/tui/form.test.tsx index b447e3ab43..0a28ba8de3 100644 --- a/packages/tui/test/cli/tui/form.test.tsx +++ b/packages/tui/test/cli/tui/form.test.tsx @@ -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) { }} > - + Promise.resolve({}) }}> @@ -86,7 +86,7 @@ async function mountForm(root: string, width = 80) { - + diff --git a/packages/tui/test/config-v2.test.tsx b/packages/tui/test/config-v2.test.tsx index c2d4417e8e..c8c36f89ac 100644 --- a/packages/tui/test/config-v2.test.tsx +++ b/packages/tui/test/config-v2.test.tsx @@ -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 | undefined function Consumer() { - const config = useTuiConfig() - configService = useTuiConfigService() - return {`${config.mouse ? "mouse" : "none"} ${config.keybinds.get("leader")?.[0]?.key}`} + context = useConfig() + return {`${context.data.mouse ? "mouse" : "none"} ${context.data.keybinds.get("leader")?.[0]?.key}`} } const app = await testRender(() => ( - + - + )) 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" } })