فهرست منبع

refactor(tui): simplify config context

Dax Raad 1 ماه پیش
والد
کامیت
f8f8cc9546

+ 6 - 4
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) => {

+ 2 - 2
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<typeof Info>

+ 13 - 13
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<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 }) => {

+ 3 - 3
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<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 {

+ 2 - 2
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<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 = {

+ 3 - 3
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)}
         >
           <text fg={theme.text} wrapMode="word">
             {error()}

+ 3 - 3
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<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(() => {

+ 3 - 3
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(

+ 4 - 4
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 (
     <box

+ 7 - 7
packages/tui/src/component/prompt/index.tsx

@@ -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"),
     }
   })
 

+ 18 - 25
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 <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
-}
-
-export function useTuiConfigOptional() {
-  return useContext(ConfigContext)?.config
+  if (!value) throw new Error("ConfigProvider is missing")
+  return value
 }
 
-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)
 }

+ 4 - 4
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<string, any> = {}
   if (config.theme?.name !== undefined) values.theme = config.theme.name
   if (config.theme?.mode !== undefined) {

+ 2 - 2
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) => {

+ 2 - 2
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<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,

+ 2 - 2
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<typeof useDialog>
   keymap: ReturnType<typeof useOpencodeKeymap>
   kv: ReturnType<typeof useKV>

+ 6 - 6
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"),
               ]),
       ],
     }

+ 11 - 11
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<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"
   })
 

+ 7 - 7
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<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") : []),
     ],
   }))
 

+ 3 - 3
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 (
     <Show when={session()}>

+ 3 - 3
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<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(() => {

+ 6 - 6
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<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
           ? [
               {

+ 4 - 4
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)

+ 3 - 3
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: {
         }}
       >
         <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>
     )

+ 3 - 3
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<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>
   )
 }

+ 3 - 3
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?:
       <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>

+ 3 - 3
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) {
           }}
         >
           <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>

+ 9 - 11
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<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" }
     })