summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-15 23:29:14 -0400
committerGitHub <[email protected]>2026-04-15 23:29:14 -0400
commit1ca257e356e404a659d6ee39d5e26a41e729ca54 (patch)
tree26f7d88aa5aa9e67762d7a3e7f32edaa490b5c6d
parentd4cfbd020da730ad8e9d72ffe61d6496d48ccf30 (diff)
downloadopencode-1ca257e356e404a659d6ee39d5e26a41e729ca54.tar.gz
opencode-1ca257e356e404a659d6ee39d5e26a41e729ca54.zip
feat: unwrap config namespaces to flat exports + barrel (#22746)
-rwxr-xr-xpackages/opencode/script/schema.ts2
-rw-r--r--packages/opencode/src/cli/cmd/plug.ts2
-rw-r--r--packages/opencode/src/cli/cmd/tui/app.tsx2
-rw-r--r--packages/opencode/src/cli/cmd/tui/attach.ts2
-rw-r--r--packages/opencode/src/cli/cmd/tui/context/keybind.tsx2
-rw-r--r--packages/opencode/src/cli/cmd/tui/context/tui-config.tsx2
-rw-r--r--packages/opencode/src/cli/cmd/tui/plugin/api.tsx2
-rw-r--r--packages/opencode/src/cli/cmd/tui/plugin/runtime.ts2
-rw-r--r--packages/opencode/src/cli/cmd/tui/thread.ts2
-rw-r--r--packages/opencode/src/cli/cmd/tui/util/scroll.ts2
-rw-r--r--packages/opencode/src/cli/error.ts2
-rw-r--r--packages/opencode/src/config/config.ts4
-rw-r--r--packages/opencode/src/config/index.ts3
-rw-r--r--packages/opencode/src/config/markdown.ts158
-rw-r--r--packages/opencode/src/config/paths.ts282
-rw-r--r--packages/opencode/src/config/tui-migrate.ts2
-rw-r--r--packages/opencode/src/config/tui.ts324
-rw-r--r--packages/opencode/src/plugin/install.ts2
-rw-r--r--packages/opencode/src/session/prompt.ts2
-rw-r--r--packages/opencode/src/skill/skill.ts2
-rw-r--r--packages/opencode/test/cli/tui/plugin-add.test.ts2
-rw-r--r--packages/opencode/test/cli/tui/plugin-install.test.ts2
-rw-r--r--packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts2
-rw-r--r--packages/opencode/test/cli/tui/plugin-loader-pure.test.ts2
-rw-r--r--packages/opencode/test/cli/tui/plugin-loader.test.ts2
-rw-r--r--packages/opencode/test/cli/tui/plugin-toggle.test.ts2
-rw-r--r--packages/opencode/test/cli/tui/thread.test.ts2
-rw-r--r--packages/opencode/test/config/markdown.test.ts2
-rw-r--r--packages/opencode/test/config/tui.test.ts2
-rw-r--r--packages/opencode/test/fixture/tui-runtime.ts2
30 files changed, 409 insertions, 412 deletions
diff --git a/packages/opencode/script/schema.ts b/packages/opencode/script/schema.ts
index 4ea68d9bb..4aa27423b 100755
--- a/packages/opencode/script/schema.ts
+++ b/packages/opencode/script/schema.ts
@@ -2,7 +2,7 @@
import { z } from "zod"
import { Config } from "../src/config"
-import { TuiConfig } from "../src/config/tui"
+import { TuiConfig } from "../src/config"
function generate(schema: z.ZodType) {
const result = z.toJSONSchema(schema, {
diff --git a/packages/opencode/src/cli/cmd/plug.ts b/packages/opencode/src/cli/cmd/plug.ts
index 42d06ff47..9dfda16d6 100644
--- a/packages/opencode/src/cli/cmd/plug.ts
+++ b/packages/opencode/src/cli/cmd/plug.ts
@@ -1,7 +1,7 @@
import { intro, log, outro, spinner } from "@clack/prompts"
import type { Argv } from "yargs"
-import { ConfigPaths } from "../../config/paths"
+import { ConfigPaths } from "../../config"
import { Global } from "../../global"
import { installPlugin, patchPluginConfig, readPluginManifest } from "../../plugin/install"
import { resolvePluginTarget } from "../../plugin/shared"
diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index e7e9fd9cd..9e96d5dcb 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -57,7 +57,7 @@ import { ArgsProvider, useArgs, type Args } from "./context/args"
import open from "open"
import { PromptRefProvider, usePromptRef } from "./context/prompt"
import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
-import { TuiConfig } from "@/config/tui"
+import { TuiConfig } from "@/config"
import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin"
import { FormatError, FormatUnknownError } from "@/cli/error"
diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts
index e892f9922..9fcbf4c1f 100644
--- a/packages/opencode/src/cli/cmd/tui/attach.ts
+++ b/packages/opencode/src/cli/cmd/tui/attach.ts
@@ -2,7 +2,7 @@ import { cmd } from "../cmd"
import { UI } from "@/cli/ui"
import { tui } from "./app"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
-import { TuiConfig } from "@/config/tui"
+import { TuiConfig } from "@/config"
import { Instance } from "@/project/instance"
import { existsSync } from "fs"
diff --git a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx
index 9c883aa20..b1dcdd780 100644
--- a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx
@@ -1,7 +1,7 @@
import { createMemo } from "solid-js"
import { Keybind } from "@/util"
import { pipe, mapValues } from "remeda"
-import type { TuiConfig } from "@/config/tui"
+import type { TuiConfig } from "@/config"
import type { ParsedKey, Renderable } from "@opentui/core"
import { createStore } from "solid-js/store"
import { useKeyboard, useRenderer } from "@opentui/solid"
diff --git a/packages/opencode/src/cli/cmd/tui/context/tui-config.tsx b/packages/opencode/src/cli/cmd/tui/context/tui-config.tsx
index 62dbf1ebd..cfe59ba80 100644
--- a/packages/opencode/src/cli/cmd/tui/context/tui-config.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/tui-config.tsx
@@ -1,4 +1,4 @@
-import { TuiConfig } from "@/config/tui"
+import { TuiConfig } from "@/config"
import { createSimpleContext } from "./helper"
export const { use: useTuiConfig, provider: TuiConfigProvider } = createSimpleContext({
diff --git a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx
index 3af70d8c2..42988fcb1 100644
--- a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx
+++ b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx
@@ -8,7 +8,7 @@ import type { useSDK } from "@tui/context/sdk"
import type { useSync } from "@tui/context/sync"
import type { useTheme } from "@tui/context/theme"
import { Dialog as DialogUI, type useDialog } from "@tui/ui/dialog"
-import type { TuiConfig } from "@/config/tui"
+import type { TuiConfig } from "@/config"
import { createPluginKeybind } from "../context/plugin-keybinds"
import type { useKV } from "../context/kv"
import { DialogAlert } from "../ui/dialog-alert"
diff --git a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts
index dd873b753..da003607c 100644
--- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts
+++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts
@@ -14,7 +14,7 @@ import path from "path"
import { fileURLToPath } from "url"
import { Config } from "@/config"
-import { TuiConfig } from "@/config/tui"
+import { TuiConfig } from "@/config"
import { Log } from "@/util"
import { errorData, errorMessage } from "@/util/error"
import { isRecord } from "@/util/record"
diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts
index 3aaa5a54f..89b32d166 100644
--- a/packages/opencode/src/cli/cmd/tui/thread.ts
+++ b/packages/opencode/src/cli/cmd/tui/thread.ts
@@ -13,7 +13,7 @@ import { Filesystem } from "@/util"
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
import type { EventSource } from "./context/sdk"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
-import { TuiConfig } from "@/config/tui"
+import { TuiConfig } from "@/config"
import { Instance } from "@/project/instance"
import { writeHeapSnapshot } from "v8"
diff --git a/packages/opencode/src/cli/cmd/tui/util/scroll.ts b/packages/opencode/src/cli/cmd/tui/util/scroll.ts
index 9b9398f30..d27bdb90c 100644
--- a/packages/opencode/src/cli/cmd/tui/util/scroll.ts
+++ b/packages/opencode/src/cli/cmd/tui/util/scroll.ts
@@ -1,5 +1,5 @@
import { MacOSScrollAccel, type ScrollAcceleration } from "@opentui/core"
-import type { TuiConfig } from "@/config/tui"
+import type { TuiConfig } from "@/config"
export class CustomSpeedScroll implements ScrollAcceleration {
constructor(private speed: number) {}
diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts
index 6ba110d34..735f1a721 100644
--- a/packages/opencode/src/cli/error.ts
+++ b/packages/opencode/src/cli/error.ts
@@ -1,5 +1,5 @@
import { AccountServiceError, AccountTransportError } from "@/account"
-import { ConfigMarkdown } from "@/config/markdown"
+import { ConfigMarkdown } from "@/config"
import { errorFormat } from "@/util/error"
import { Config } from "../config"
import { MCP } from "../mcp"
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 3da2dd6bd..04801098b 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -21,7 +21,7 @@ import {
import { Instance, type InstanceContext } from "../project/instance"
import { LSPServer } from "../lsp/server"
import { Installation } from "@/installation"
-import { ConfigMarkdown } from "./markdown"
+import { ConfigMarkdown } from "."
import { existsSync } from "fs"
import { Bus } from "@/bus"
import { GlobalBus } from "@/bus/global"
@@ -29,7 +29,7 @@ import { Event } from "../server/event"
import { Glob } from "@opencode-ai/shared/util/glob"
import { Account } from "@/account"
import { isRecord } from "@/util/record"
-import { ConfigPaths } from "./paths"
+import { ConfigPaths } from "."
import type { ConsoleState } from "./console-state"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { InstanceState } from "@/effect"
diff --git a/packages/opencode/src/config/index.ts b/packages/opencode/src/config/index.ts
index 60e39c316..d878fc99a 100644
--- a/packages/opencode/src/config/index.ts
+++ b/packages/opencode/src/config/index.ts
@@ -1 +1,4 @@
export * as Config from "./config"
+export * as ConfigMarkdown from "./markdown"
+export * as ConfigPaths from "./paths"
+export * as TuiConfig from "./tui"
diff --git a/packages/opencode/src/config/markdown.ts b/packages/opencode/src/config/markdown.ts
index 8b5392be5..7cad69266 100644
--- a/packages/opencode/src/config/markdown.ts
+++ b/packages/opencode/src/config/markdown.ts
@@ -3,97 +3,95 @@ import matter from "gray-matter"
import { z } from "zod"
import { Filesystem } from "../util"
-export namespace ConfigMarkdown {
- export const FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g
- export const SHELL_REGEX = /!`([^`]+)`/g
+export const FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g
+export const SHELL_REGEX = /!`([^`]+)`/g
- export function files(template: string) {
- return Array.from(template.matchAll(FILE_REGEX))
- }
+export function files(template: string) {
+ return Array.from(template.matchAll(FILE_REGEX))
+}
- export function shell(template: string) {
- return Array.from(template.matchAll(SHELL_REGEX))
- }
+export function shell(template: string) {
+ return Array.from(template.matchAll(SHELL_REGEX))
+}
+
+// other coding agents like claude code allow invalid yaml in their
+// frontmatter, we need to fallback to a more permissive parser for those cases
+export function fallbackSanitization(content: string): string {
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
+ if (!match) return content
+
+ const frontmatter = match[1]
+ const lines = frontmatter.split(/\r?\n/)
+ const result: string[] = []
- // other coding agents like claude code allow invalid yaml in their
- // frontmatter, we need to fallback to a more permissive parser for those cases
- export function fallbackSanitization(content: string): string {
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
- if (!match) return content
-
- const frontmatter = match[1]
- const lines = frontmatter.split(/\r?\n/)
- const result: string[] = []
-
- for (const line of lines) {
- // skip comments and empty lines
- if (line.trim().startsWith("#") || line.trim() === "") {
- result.push(line)
- continue
- }
-
- // skip lines that are continuations (indented)
- if (line.match(/^\s+/)) {
- result.push(line)
- continue
- }
-
- // match key: value pattern
- const kvMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/)
- if (!kvMatch) {
- result.push(line)
- continue
- }
-
- const key = kvMatch[1]
- const value = kvMatch[2].trim()
-
- // skip if value is empty, already quoted, or uses block scalar
- if (value === "" || value === ">" || value === "|" || value.startsWith('"') || value.startsWith("'")) {
- result.push(line)
- continue
- }
-
- // if value contains a colon, convert to block scalar
- if (value.includes(":")) {
- result.push(`${key}: |-`)
- result.push(` ${value}`)
- continue
- }
+ for (const line of lines) {
+ // skip comments and empty lines
+ if (line.trim().startsWith("#") || line.trim() === "") {
+ result.push(line)
+ continue
+ }
+
+ // skip lines that are continuations (indented)
+ if (line.match(/^\s+/)) {
+ result.push(line)
+ continue
+ }
+ // match key: value pattern
+ const kvMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/)
+ if (!kvMatch) {
result.push(line)
+ continue
}
- const processed = result.join("\n")
- return content.replace(frontmatter, () => processed)
+ const key = kvMatch[1]
+ const value = kvMatch[2].trim()
+
+ // skip if value is empty, already quoted, or uses block scalar
+ if (value === "" || value === ">" || value === "|" || value.startsWith('"') || value.startsWith("'")) {
+ result.push(line)
+ continue
+ }
+
+ // if value contains a colon, convert to block scalar
+ if (value.includes(":")) {
+ result.push(`${key}: |-`)
+ result.push(` ${value}`)
+ continue
+ }
+
+ result.push(line)
}
- export async function parse(filePath: string) {
- const template = await Filesystem.readText(filePath)
+ const processed = result.join("\n")
+ return content.replace(frontmatter, () => processed)
+}
+
+export async function parse(filePath: string) {
+ const template = await Filesystem.readText(filePath)
+ try {
+ const md = matter(template)
+ return md
+ } catch {
try {
- const md = matter(template)
- return md
- } catch {
- try {
- return matter(fallbackSanitization(template))
- } catch (err) {
- throw new FrontmatterError(
- {
- path: filePath,
- message: `${filePath}: Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
- },
- { cause: err },
- )
- }
+ return matter(fallbackSanitization(template))
+ } catch (err) {
+ throw new FrontmatterError(
+ {
+ path: filePath,
+ message: `${filePath}: Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
+ },
+ { cause: err },
+ )
}
}
-
- export const FrontmatterError = NamedError.create(
- "ConfigFrontmatterError",
- z.object({
- path: z.string(),
- message: z.string(),
- }),
- )
}
+
+export const FrontmatterError = NamedError.create(
+ "ConfigFrontmatterError",
+ z.object({
+ path: z.string(),
+ message: z.string(),
+ }),
+)
diff --git a/packages/opencode/src/config/paths.ts b/packages/opencode/src/config/paths.ts
index c5eb105c9..82dde2df9 100644
--- a/packages/opencode/src/config/paths.ts
+++ b/packages/opencode/src/config/paths.ts
@@ -7,161 +7,159 @@ import { Filesystem } from "@/util"
import { Flag } from "@/flag/flag"
import { Global } from "@/global"
-export namespace ConfigPaths {
- export async function projectFiles(name: string, directory: string, worktree: string) {
- return Filesystem.findUp([`${name}.json`, `${name}.jsonc`], directory, worktree, { rootFirst: true })
- }
-
- export async function directories(directory: string, worktree: string) {
- return [
- Global.Path.config,
- ...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG
- ? await Array.fromAsync(
- Filesystem.up({
- targets: [".opencode"],
- start: directory,
- stop: worktree,
- }),
- )
- : []),
- ...(await Array.fromAsync(
- Filesystem.up({
- targets: [".opencode"],
- start: Global.Path.home,
- stop: Global.Path.home,
- }),
- )),
- ...(Flag.OPENCODE_CONFIG_DIR ? [Flag.OPENCODE_CONFIG_DIR] : []),
- ]
- }
+export async function projectFiles(name: string, directory: string, worktree: string) {
+ return Filesystem.findUp([`${name}.json`, `${name}.jsonc`], directory, worktree, { rootFirst: true })
+}
- export function fileInDirectory(dir: string, name: string) {
- return [path.join(dir, `${name}.json`), path.join(dir, `${name}.jsonc`)]
- }
+export async function directories(directory: string, worktree: string) {
+ return [
+ Global.Path.config,
+ ...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG
+ ? await Array.fromAsync(
+ Filesystem.up({
+ targets: [".opencode"],
+ start: directory,
+ stop: worktree,
+ }),
+ )
+ : []),
+ ...(await Array.fromAsync(
+ Filesystem.up({
+ targets: [".opencode"],
+ start: Global.Path.home,
+ stop: Global.Path.home,
+ }),
+ )),
+ ...(Flag.OPENCODE_CONFIG_DIR ? [Flag.OPENCODE_CONFIG_DIR] : []),
+ ]
+}
- export const JsonError = NamedError.create(
- "ConfigJsonError",
- z.object({
- path: z.string(),
- message: z.string().optional(),
- }),
- )
-
- export const InvalidError = NamedError.create(
- "ConfigInvalidError",
- z.object({
- path: z.string(),
- issues: z.custom<z.core.$ZodIssue[]>().optional(),
- message: z.string().optional(),
- }),
- )
-
- /** Read a config file, returning undefined for missing files and throwing JsonError for other failures. */
- export async function readFile(filepath: string) {
- return Filesystem.readText(filepath).catch((err: NodeJS.ErrnoException) => {
- if (err.code === "ENOENT") return
- throw new JsonError({ path: filepath }, { cause: err })
- })
- }
+export function fileInDirectory(dir: string, name: string) {
+ return [path.join(dir, `${name}.json`), path.join(dir, `${name}.jsonc`)]
+}
- type ParseSource = string | { source: string; dir: string }
+export const JsonError = NamedError.create(
+ "ConfigJsonError",
+ z.object({
+ path: z.string(),
+ message: z.string().optional(),
+ }),
+)
+
+export const InvalidError = NamedError.create(
+ "ConfigInvalidError",
+ z.object({
+ path: z.string(),
+ issues: z.custom<z.core.$ZodIssue[]>().optional(),
+ message: z.string().optional(),
+ }),
+)
+
+/** Read a config file, returning undefined for missing files and throwing JsonError for other failures. */
+export async function readFile(filepath: string) {
+ return Filesystem.readText(filepath).catch((err: NodeJS.ErrnoException) => {
+ if (err.code === "ENOENT") return
+ throw new JsonError({ path: filepath }, { cause: err })
+ })
+}
- function source(input: ParseSource) {
- return typeof input === "string" ? input : input.source
- }
+type ParseSource = string | { source: string; dir: string }
- function dir(input: ParseSource) {
- return typeof input === "string" ? path.dirname(input) : input.dir
- }
+function source(input: ParseSource) {
+ return typeof input === "string" ? input : input.source
+}
- /** Apply {env:VAR} and {file:path} substitutions to config text. */
- async function substitute(text: string, input: ParseSource, missing: "error" | "empty" = "error") {
- text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
- return process.env[varName] || ""
- })
+function dir(input: ParseSource) {
+ return typeof input === "string" ? path.dirname(input) : input.dir
+}
- const fileMatches = Array.from(text.matchAll(/\{file:[^}]+\}/g))
- if (!fileMatches.length) return text
-
- const configDir = dir(input)
- const configSource = source(input)
- let out = ""
- let cursor = 0
-
- for (const match of fileMatches) {
- const token = match[0]
- const index = match.index!
- out += text.slice(cursor, index)
-
- const lineStart = text.lastIndexOf("\n", index - 1) + 1
- const prefix = text.slice(lineStart, index).trimStart()
- if (prefix.startsWith("//")) {
- out += token
- cursor = index + token.length
- continue
- }
-
- let filePath = token.replace(/^\{file:/, "").replace(/\}$/, "")
- if (filePath.startsWith("~/")) {
- filePath = path.join(os.homedir(), filePath.slice(2))
- }
-
- const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
- const fileContent = (
- await Filesystem.readText(resolvedPath).catch((error: NodeJS.ErrnoException) => {
- if (missing === "empty") return ""
-
- const errMsg = `bad file reference: "${token}"`
- if (error.code === "ENOENT") {
- throw new InvalidError(
- {
- path: configSource,
- message: errMsg + ` ${resolvedPath} does not exist`,
- },
- { cause: error },
- )
- }
- throw new InvalidError({ path: configSource, message: errMsg }, { cause: error })
- })
- ).trim()
-
- out += JSON.stringify(fileContent).slice(1, -1)
+/** Apply {env:VAR} and {file:path} substitutions to config text. */
+async function substitute(text: string, input: ParseSource, missing: "error" | "empty" = "error") {
+ text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
+ return process.env[varName] || ""
+ })
+
+ const fileMatches = Array.from(text.matchAll(/\{file:[^}]+\}/g))
+ if (!fileMatches.length) return text
+
+ const configDir = dir(input)
+ const configSource = source(input)
+ let out = ""
+ let cursor = 0
+
+ for (const match of fileMatches) {
+ const token = match[0]
+ const index = match.index!
+ out += text.slice(cursor, index)
+
+ const lineStart = text.lastIndexOf("\n", index - 1) + 1
+ const prefix = text.slice(lineStart, index).trimStart()
+ if (prefix.startsWith("//")) {
+ out += token
cursor = index + token.length
+ continue
}
- out += text.slice(cursor)
- return out
+ let filePath = token.replace(/^\{file:/, "").replace(/\}$/, "")
+ if (filePath.startsWith("~/")) {
+ filePath = path.join(os.homedir(), filePath.slice(2))
+ }
+
+ const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
+ const fileContent = (
+ await Filesystem.readText(resolvedPath).catch((error: NodeJS.ErrnoException) => {
+ if (missing === "empty") return ""
+
+ const errMsg = `bad file reference: "${token}"`
+ if (error.code === "ENOENT") {
+ throw new InvalidError(
+ {
+ path: configSource,
+ message: errMsg + ` ${resolvedPath} does not exist`,
+ },
+ { cause: error },
+ )
+ }
+ throw new InvalidError({ path: configSource, message: errMsg }, { cause: error })
+ })
+ ).trim()
+
+ out += JSON.stringify(fileContent).slice(1, -1)
+ cursor = index + token.length
}
- /** Substitute and parse JSONC text, throwing JsonError on syntax errors. */
- export async function parseText(text: string, input: ParseSource, missing: "error" | "empty" = "error") {
- const configSource = source(input)
- text = await substitute(text, input, missing)
-
- const errors: JsoncParseError[] = []
- const data = parseJsonc(text, errors, { allowTrailingComma: true })
- if (errors.length) {
- const lines = text.split("\n")
- const errorDetails = errors
- .map((e) => {
- const beforeOffset = text.substring(0, e.offset).split("\n")
- const line = beforeOffset.length
- const column = beforeOffset[beforeOffset.length - 1].length + 1
- const problemLine = lines[line - 1]
-
- const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}`
- if (!problemLine) return error
-
- return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^`
- })
- .join("\n")
-
- throw new JsonError({
- path: configSource,
- message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`,
+ out += text.slice(cursor)
+ return out
+}
+
+/** Substitute and parse JSONC text, throwing JsonError on syntax errors. */
+export async function parseText(text: string, input: ParseSource, missing: "error" | "empty" = "error") {
+ const configSource = source(input)
+ text = await substitute(text, input, missing)
+
+ const errors: JsoncParseError[] = []
+ const data = parseJsonc(text, errors, { allowTrailingComma: true })
+ if (errors.length) {
+ const lines = text.split("\n")
+ const errorDetails = errors
+ .map((e) => {
+ const beforeOffset = text.substring(0, e.offset).split("\n")
+ const line = beforeOffset.length
+ const column = beforeOffset[beforeOffset.length - 1].length + 1
+ const problemLine = lines[line - 1]
+
+ const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}`
+ if (!problemLine) return error
+
+ return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^`
})
- }
+ .join("\n")
- return data
+ throw new JsonError({
+ path: configSource,
+ message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`,
+ })
}
+
+ return data
}
diff --git a/packages/opencode/src/config/tui-migrate.ts b/packages/opencode/src/config/tui-migrate.ts
index f9d37e479..18cee554d 100644
--- a/packages/opencode/src/config/tui-migrate.ts
+++ b/packages/opencode/src/config/tui-migrate.ts
@@ -2,7 +2,7 @@ import path from "path"
import { type ParseError as JsoncParseError, applyEdits, modify, parse as parseJsonc } from "jsonc-parser"
import { unique } from "remeda"
import z from "zod"
-import { ConfigPaths } from "./paths"
+import { ConfigPaths } from "."
import { TuiInfo, TuiOptions } from "./tui-schema"
import { Instance } from "@/project/instance"
import { Flag } from "@/flag/flag"
diff --git a/packages/opencode/src/config/tui.ts b/packages/opencode/src/config/tui.ts
index c1e2b6e6b..43f1bce46 100644
--- a/packages/opencode/src/config/tui.ts
+++ b/packages/opencode/src/config/tui.ts
@@ -3,7 +3,7 @@ import z from "zod"
import { mergeDeep, unique } from "remeda"
import { Context, Effect, Fiber, Layer } from "effect"
import { Config } from "."
-import { ConfigPaths } from "./paths"
+import { ConfigPaths } from "."
import { migrateTuiConfig } from "./tui-migrate"
import { TuiInfo } from "./tui-schema"
import { Flag } from "@/flag/flag"
@@ -14,201 +14,199 @@ import { InstanceState } from "@/effect"
import { makeRuntime } from "@/effect/run-service"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
-export namespace TuiConfig {
- const log = Log.create({ service: "tui.config" })
+const log = Log.create({ service: "tui.config" })
- export const Info = TuiInfo
+export const Info = TuiInfo
- type Acc = {
- result: Info
- }
-
- type State = {
- config: Info
- deps: Array<Fiber.Fiber<void, AppFileSystem.Error>>
- }
+type Acc = {
+ result: Info
+}
- export type Info = z.output<typeof Info> & {
- // Internal resolved plugin list used by runtime loading.
- plugin_origins?: Config.PluginOrigin[]
- }
+type State = {
+ config: Info
+ deps: Array<Fiber.Fiber<void, AppFileSystem.Error>>
+}
- export interface Interface {
- readonly get: () => Effect.Effect<Info>
- readonly waitForDependencies: () => Effect.Effect<void, AppFileSystem.Error>
- }
+export type Info = z.output<typeof Info> & {
+ // Internal resolved plugin list used by runtime loading.
+ plugin_origins?: Config.PluginOrigin[]
+}
- export class Service extends Context.Service<Service, Interface>()("@opencode/TuiConfig") {}
+export interface Interface {
+ readonly get: () => Effect.Effect<Info>
+ readonly waitForDependencies: () => Effect.Effect<void, AppFileSystem.Error>
+}
- function pluginScope(file: string, ctx: { directory: string; worktree: string }): Config.PluginScope {
- if (AppFileSystem.contains(ctx.directory, file)) return "local"
- if (ctx.worktree !== "/" && AppFileSystem.contains(ctx.worktree, file)) return "local"
- return "global"
- }
+export class Service extends Context.Service<Service, Interface>()("@opencode/TuiConfig") {}
- function customPath() {
- return Flag.OPENCODE_TUI_CONFIG
- }
+function pluginScope(file: string, ctx: { directory: string; worktree: string }): Config.PluginScope {
+ if (AppFileSystem.contains(ctx.directory, file)) return "local"
+ if (ctx.worktree !== "/" && AppFileSystem.contains(ctx.worktree, file)) return "local"
+ return "global"
+}
- function normalize(raw: Record<string, unknown>) {
- const data = { ...raw }
- if (!("tui" in data)) return data
- if (!isRecord(data.tui)) {
- delete data.tui
- return data
- }
+function customPath() {
+ return Flag.OPENCODE_TUI_CONFIG
+}
- const tui = data.tui
+function normalize(raw: Record<string, unknown>) {
+ const data = { ...raw }
+ if (!("tui" in data)) return data
+ if (!isRecord(data.tui)) {
delete data.tui
- return {
- ...tui,
- ...data,
- }
+ return data
}
- async function mergeFile(acc: Acc, file: string, ctx: { directory: string; worktree: string }) {
- const data = await loadFile(file)
- acc.result = mergeDeep(acc.result, data)
- if (!data.plugin?.length) return
-
- const scope = pluginScope(file, ctx)
- const plugins = Config.deduplicatePluginOrigins([
- ...(acc.result.plugin_origins ?? []),
- ...data.plugin.map((spec) => ({ spec, scope, source: file })),
- ])
- acc.result.plugin = plugins.map((item) => item.spec)
- acc.result.plugin_origins = plugins
+ const tui = data.tui
+ delete data.tui
+ return {
+ ...tui,
+ ...data,
}
+}
- async function loadState(ctx: { directory: string; worktree: string }) {
- let projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG
- ? []
- : await ConfigPaths.projectFiles("tui", ctx.directory, ctx.worktree)
- const directories = await ConfigPaths.directories(ctx.directory, ctx.worktree)
- const custom = customPath()
- const managed = Config.managedConfigDir()
- await migrateTuiConfig({ directories, custom, managed })
- // Re-compute after migration since migrateTuiConfig may have created new tui.json files
- projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG
- ? []
- : await ConfigPaths.projectFiles("tui", ctx.directory, ctx.worktree)
-
- const acc: Acc = {
- result: {},
- }
-
- for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) {
- await mergeFile(acc, file, ctx)
- }
+async function mergeFile(acc: Acc, file: string, ctx: { directory: string; worktree: string }) {
+ const data = await loadFile(file)
+ acc.result = mergeDeep(acc.result, data)
+ if (!data.plugin?.length) return
+
+ const scope = pluginScope(file, ctx)
+ const plugins = Config.deduplicatePluginOrigins([
+ ...(acc.result.plugin_origins ?? []),
+ ...data.plugin.map((spec) => ({ spec, scope, source: file })),
+ ])
+ acc.result.plugin = plugins.map((item) => item.spec)
+ acc.result.plugin_origins = plugins
+}
- if (custom) {
- await mergeFile(acc, custom, ctx)
- log.debug("loaded custom tui config", { path: custom })
- }
+async function loadState(ctx: { directory: string; worktree: string }) {
+ let projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG
+ ? []
+ : await ConfigPaths.projectFiles("tui", ctx.directory, ctx.worktree)
+ const directories = await ConfigPaths.directories(ctx.directory, ctx.worktree)
+ const custom = customPath()
+ const managed = Config.managedConfigDir()
+ await migrateTuiConfig({ directories, custom, managed })
+ // Re-compute after migration since migrateTuiConfig may have created new tui.json files
+ projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG
+ ? []
+ : await ConfigPaths.projectFiles("tui", ctx.directory, ctx.worktree)
+
+ const acc: Acc = {
+ result: {},
+ }
- for (const file of projectFiles) {
- await mergeFile(acc, file, ctx)
- }
+ for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) {
+ await mergeFile(acc, file, ctx)
+ }
- const dirs = unique(directories).filter((dir) => dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR)
+ if (custom) {
+ await mergeFile(acc, custom, ctx)
+ log.debug("loaded custom tui config", { path: custom })
+ }
- for (const dir of dirs) {
- if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue
- for (const file of ConfigPaths.fileInDirectory(dir, "tui")) {
- await mergeFile(acc, file, ctx)
- }
- }
+ for (const file of projectFiles) {
+ await mergeFile(acc, file, ctx)
+ }
- if (existsSync(managed)) {
- for (const file of ConfigPaths.fileInDirectory(managed, "tui")) {
- await mergeFile(acc, file, ctx)
- }
- }
+ const dirs = unique(directories).filter((dir) => dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR)
- const keybinds = { ...acc.result.keybinds }
- if (process.platform === "win32") {
- // Native Windows terminals do not support POSIX suspend, so prefer prompt undo.
- keybinds.terminal_suspend = "none"
- keybinds.input_undo ??= unique(["ctrl+z", ...Config.Keybinds.shape.input_undo.parse(undefined).split(",")]).join(
- ",",
- )
+ for (const dir of dirs) {
+ if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue
+ for (const file of ConfigPaths.fileInDirectory(dir, "tui")) {
+ await mergeFile(acc, file, ctx)
}
- acc.result.keybinds = Config.Keybinds.parse(keybinds)
+ }
- return {
- config: acc.result,
- dirs: acc.result.plugin?.length ? dirs : [],
+ if (existsSync(managed)) {
+ for (const file of ConfigPaths.fileInDirectory(managed, "tui")) {
+ await mergeFile(acc, file, ctx)
}
}
- export const layer = Layer.effect(
- Service,
- Effect.gen(function* () {
- const cfg = yield* Config.Service
- const state = yield* InstanceState.make<State>(
- Effect.fn("TuiConfig.state")(function* (ctx) {
- const data = yield* Effect.promise(() => loadState(ctx))
- const deps = yield* Effect.forEach(data.dirs, (dir) => cfg.installDependencies(dir).pipe(Effect.forkScoped), {
- concurrency: "unbounded",
- })
- return { config: data.config, deps }
- }),
- )
-
- const get = Effect.fn("TuiConfig.get")(() => InstanceState.use(state, (s) => s.config))
-
- const waitForDependencies = Effect.fn("TuiConfig.waitForDependencies")(() =>
- InstanceState.useEffect(state, (s) =>
- Effect.forEach(s.deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.asVoid),
- ),
- )
-
- return Service.of({ get, waitForDependencies })
- }),
- )
-
- export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
-
- const { runPromise } = makeRuntime(Service, defaultLayer)
-
- export async function get() {
- return runPromise((svc) => svc.get())
+ const keybinds = { ...acc.result.keybinds }
+ if (process.platform === "win32") {
+ // Native Windows terminals do not support POSIX suspend, so prefer prompt undo.
+ keybinds.terminal_suspend = "none"
+ keybinds.input_undo ??= unique(["ctrl+z", ...Config.Keybinds.shape.input_undo.parse(undefined).split(",")]).join(
+ ",",
+ )
}
+ acc.result.keybinds = Config.Keybinds.parse(keybinds)
- export async function waitForDependencies() {
- await runPromise((svc) => svc.waitForDependencies())
+ return {
+ config: acc.result,
+ dirs: acc.result.plugin?.length ? dirs : [],
}
+}
- async function loadFile(filepath: string): Promise<Info> {
- const text = await ConfigPaths.readFile(filepath)
- if (!text) return {}
- return load(text, filepath).catch((error) => {
- log.warn("failed to load tui config", { path: filepath, error })
- return {}
- })
- }
+export const layer = Layer.effect(
+ Service,
+ Effect.gen(function* () {
+ const cfg = yield* Config.Service
+ const state = yield* InstanceState.make<State>(
+ Effect.fn("TuiConfig.state")(function* (ctx) {
+ const data = yield* Effect.promise(() => loadState(ctx))
+ const deps = yield* Effect.forEach(data.dirs, (dir) => cfg.installDependencies(dir).pipe(Effect.forkScoped), {
+ concurrency: "unbounded",
+ })
+ return { config: data.config, deps }
+ }),
+ )
+
+ const get = Effect.fn("TuiConfig.get")(() => InstanceState.use(state, (s) => s.config))
+
+ const waitForDependencies = Effect.fn("TuiConfig.waitForDependencies")(() =>
+ InstanceState.useEffect(state, (s) =>
+ Effect.forEach(s.deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.asVoid),
+ ),
+ )
+
+ return Service.of({ get, waitForDependencies })
+ }),
+)
+
+export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
+
+const { runPromise } = makeRuntime(Service, defaultLayer)
+
+export async function get() {
+ return runPromise((svc) => svc.get())
+}
- async function load(text: string, configFilepath: string): Promise<Info> {
- const raw = await ConfigPaths.parseText(text, configFilepath, "empty")
- if (!isRecord(raw)) return {}
+export async function waitForDependencies() {
+ await runPromise((svc) => svc.waitForDependencies())
+}
- // Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json
- // (mirroring the old opencode.json shape) still get their settings applied.
- const normalized = normalize(raw)
+async function loadFile(filepath: string): Promise<Info> {
+ const text = await ConfigPaths.readFile(filepath)
+ if (!text) return {}
+ return load(text, filepath).catch((error) => {
+ log.warn("failed to load tui config", { path: filepath, error })
+ return {}
+ })
+}
- const parsed = Info.safeParse(normalized)
- if (!parsed.success) {
- log.warn("invalid tui config", { path: configFilepath, issues: parsed.error.issues })
- return {}
- }
+async function load(text: string, configFilepath: string): Promise<Info> {
+ const raw = await ConfigPaths.parseText(text, configFilepath, "empty")
+ if (!isRecord(raw)) return {}
- const data = parsed.data
- if (data.plugin) {
- for (let i = 0; i < data.plugin.length; i++) {
- data.plugin[i] = await Config.resolvePluginSpec(data.plugin[i], configFilepath)
- }
- }
+ // Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json
+ // (mirroring the old opencode.json shape) still get their settings applied.
+ const normalized = normalize(raw)
- return data
+ const parsed = Info.safeParse(normalized)
+ if (!parsed.success) {
+ log.warn("invalid tui config", { path: configFilepath, issues: parsed.error.issues })
+ return {}
}
+
+ const data = parsed.data
+ if (data.plugin) {
+ for (let i = 0; i < data.plugin.length; i++) {
+ data.plugin[i] = await Config.resolvePluginSpec(data.plugin[i], configFilepath)
+ }
+ }
+
+ return data
}
diff --git a/packages/opencode/src/plugin/install.ts b/packages/opencode/src/plugin/install.ts
index 0a6256d6f..8b7e30c40 100644
--- a/packages/opencode/src/plugin/install.ts
+++ b/packages/opencode/src/plugin/install.ts
@@ -7,7 +7,7 @@ import {
printParseErrorCode,
} from "jsonc-parser"
-import { ConfigPaths } from "@/config/paths"
+import { ConfigPaths } from "@/config"
import { Global } from "@/global"
import { Filesystem } from "@/util"
import { Flock } from "@opencode-ai/shared/util/flock"
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 157533af0..1d4bb66bc 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -30,7 +30,7 @@ import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
import * as Stream from "effect/Stream"
import { Command } from "../command"
import { pathToFileURL, fileURLToPath } from "url"
-import { ConfigMarkdown } from "../config/markdown"
+import { ConfigMarkdown } from "../config"
import { SessionSummary } from "./summary"
import { NamedError } from "@opencode-ai/shared/util/error"
import { SessionProcessor } from "./processor"
diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts
index ef9f661cb..f8ff7b8f5 100644
--- a/packages/opencode/src/skill/skill.ts
+++ b/packages/opencode/src/skill/skill.ts
@@ -12,7 +12,7 @@ import { Global } from "@/global"
import { Permission } from "@/permission"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Config } from "../config"
-import { ConfigMarkdown } from "../config/markdown"
+import { ConfigMarkdown } from "../config"
import { Glob } from "@opencode-ai/shared/util/glob"
import { Log } from "../util"
import { Discovery } from "./discovery"
diff --git a/packages/opencode/test/cli/tui/plugin-add.test.ts b/packages/opencode/test/cli/tui/plugin-add.test.ts
index 748f29172..11865bedd 100644
--- a/packages/opencode/test/cli/tui/plugin-add.test.ts
+++ b/packages/opencode/test/cli/tui/plugin-add.test.ts
@@ -4,7 +4,7 @@ import path from "path"
import { pathToFileURL } from "url"
import { tmpdir } from "../../fixture/fixture"
import { createTuiPluginApi } from "../../fixture/tui-plugin"
-import { TuiConfig } from "../../../src/config/tui"
+import { TuiConfig } from "../../../src/config"
const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime")
diff --git a/packages/opencode/test/cli/tui/plugin-install.test.ts b/packages/opencode/test/cli/tui/plugin-install.test.ts
index 290a7eea1..bd490ac4f 100644
--- a/packages/opencode/test/cli/tui/plugin-install.test.ts
+++ b/packages/opencode/test/cli/tui/plugin-install.test.ts
@@ -4,7 +4,7 @@ import path from "path"
import { pathToFileURL } from "url"
import { tmpdir } from "../../fixture/fixture"
import { createTuiPluginApi } from "../../fixture/tui-plugin"
-import { TuiConfig } from "../../../src/config/tui"
+import { TuiConfig } from "../../../src/config"
const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime")
diff --git a/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts b/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts
index 68c3df447..7020ac742 100644
--- a/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts
+++ b/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts
@@ -4,7 +4,7 @@ import path from "path"
import { pathToFileURL } from "url"
import { tmpdir } from "../../fixture/fixture"
import { createTuiPluginApi } from "../../fixture/tui-plugin"
-import { TuiConfig } from "../../../src/config/tui"
+import { TuiConfig } from "../../../src/config"
import { Npm } from "../../../src/npm"
const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime")
diff --git a/packages/opencode/test/cli/tui/plugin-loader-pure.test.ts b/packages/opencode/test/cli/tui/plugin-loader-pure.test.ts
index f92d74292..25233adaa 100644
--- a/packages/opencode/test/cli/tui/plugin-loader-pure.test.ts
+++ b/packages/opencode/test/cli/tui/plugin-loader-pure.test.ts
@@ -4,7 +4,7 @@ import path from "path"
import { pathToFileURL } from "url"
import { tmpdir } from "../../fixture/fixture"
import { createTuiPluginApi } from "../../fixture/tui-plugin"
-import { TuiConfig } from "../../../src/config/tui"
+import { TuiConfig } from "../../../src/config"
const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime")
diff --git a/packages/opencode/test/cli/tui/plugin-loader.test.ts b/packages/opencode/test/cli/tui/plugin-loader.test.ts
index 8446570cc..4dc2aeccd 100644
--- a/packages/opencode/test/cli/tui/plugin-loader.test.ts
+++ b/packages/opencode/test/cli/tui/plugin-loader.test.ts
@@ -5,7 +5,7 @@ import { pathToFileURL } from "url"
import { tmpdir } from "../../fixture/fixture"
import { createTuiPluginApi } from "../../fixture/tui-plugin"
import { Global } from "../../../src/global"
-import { TuiConfig } from "../../../src/config/tui"
+import { TuiConfig } from "../../../src/config"
import { Filesystem } from "../../../src/util"
const { allThemes, addTheme } = await import("../../../src/cli/cmd/tui/context/theme")
diff --git a/packages/opencode/test/cli/tui/plugin-toggle.test.ts b/packages/opencode/test/cli/tui/plugin-toggle.test.ts
index 10ddfe8e1..3f04e3c6f 100644
--- a/packages/opencode/test/cli/tui/plugin-toggle.test.ts
+++ b/packages/opencode/test/cli/tui/plugin-toggle.test.ts
@@ -4,7 +4,7 @@ import path from "path"
import { pathToFileURL } from "url"
import { tmpdir } from "../../fixture/fixture"
import { createTuiPluginApi } from "../../fixture/tui-plugin"
-import { TuiConfig } from "../../../src/config/tui"
+import { TuiConfig } from "../../../src/config"
const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime")
diff --git a/packages/opencode/test/cli/tui/thread.test.ts b/packages/opencode/test/cli/tui/thread.test.ts
index 1c5c7e65e..7b781c49e 100644
--- a/packages/opencode/test/cli/tui/thread.test.ts
+++ b/packages/opencode/test/cli/tui/thread.test.ts
@@ -8,7 +8,7 @@ import { UI } from "../../../src/cli/ui"
import * as Timeout from "../../../src/util/timeout"
import * as Network from "../../../src/cli/network"
import * as Win32 from "../../../src/cli/cmd/tui/win32"
-import { TuiConfig } from "../../../src/config/tui"
+import { TuiConfig } from "../../../src/config"
import { Instance } from "../../../src/project/instance"
const stop = new Error("stop")
diff --git a/packages/opencode/test/config/markdown.test.ts b/packages/opencode/test/config/markdown.test.ts
index 865af2107..b807850c3 100644
--- a/packages/opencode/test/config/markdown.test.ts
+++ b/packages/opencode/test/config/markdown.test.ts
@@ -1,5 +1,5 @@
import { expect, test, describe } from "bun:test"
-import { ConfigMarkdown } from "../../src/config/markdown"
+import { ConfigMarkdown } from "../../src/config"
describe("ConfigMarkdown: normal template", () => {
const template = `This is a @valid/path/to/a/file and it should also match at
diff --git a/packages/opencode/test/config/tui.test.ts b/packages/opencode/test/config/tui.test.ts
index c80905cd1..62587d270 100644
--- a/packages/opencode/test/config/tui.test.ts
+++ b/packages/opencode/test/config/tui.test.ts
@@ -4,7 +4,7 @@ import fs from "fs/promises"
import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
import { Config } from "../../src/config"
-import { TuiConfig } from "../../src/config/tui"
+import { TuiConfig } from "../../src/config"
import { Global } from "../../src/global"
import { Filesystem } from "../../src/util"
import { AppRuntime } from "../../src/effect/app-runtime"
diff --git a/packages/opencode/test/fixture/tui-runtime.ts b/packages/opencode/test/fixture/tui-runtime.ts
index fdd3b6cff..493b23f7e 100644
--- a/packages/opencode/test/fixture/tui-runtime.ts
+++ b/packages/opencode/test/fixture/tui-runtime.ts
@@ -1,6 +1,6 @@
import { spyOn } from "bun:test"
import path from "path"
-import { TuiConfig } from "../../src/config/tui"
+import { TuiConfig } from "../../src/config"
type PluginSpec = string | [string, Record<string, unknown>]