summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-16 20:02:24 -0400
committerGitHub <[email protected]>2026-04-17 00:02:24 +0000
commit2704ad9110e6705dacd1b018a7245c1950a3ae80 (patch)
tree579dafb3fe90433644a157a176e2fb352a454c77 /packages
parent06d247c70982b9bba0bb25f4b990fb59e3374650 (diff)
downloadopencode-2704ad9110e6705dacd1b018a7245c1950a3ae80.tar.gz
opencode-2704ad9110e6705dacd1b018a7245c1950a3ae80.zip
refactor: unwrap TuiConfig namespace + self-reexport (#22952)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/cli/cmd/tui/config/tui.ts320
1 files changed, 160 insertions, 160 deletions
diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts
index e8eb9ff5d..d264273bc 100644
--- a/packages/opencode/src/cli/cmd/tui/config/tui.ts
+++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts
@@ -17,197 +17,197 @@ import { InstallationLocal, InstallationVersion } from "@/installation/version"
import { makeRuntime } from "@/cli/effect/runtime"
import { Filesystem, Log } from "@/util"
-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 Acc = {
+ result: Info
+}
- type State = {
- config: Info
- deps: Array<Fiber.Fiber<void, AppFileSystem.Error>>
- }
+type State = {
+ config: Info
+ deps: Array<Fiber.Fiber<void, AppFileSystem.Error>>
+}
- export type Info = z.output<typeof Info> & {
- // Internal resolved plugin list used by runtime loading.
- plugin_origins?: ConfigPlugin.Origin[]
- }
+export type Info = z.output<typeof Info> & {
+ // Internal resolved plugin list used by runtime loading.
+ plugin_origins?: ConfigPlugin.Origin[]
+}
- export interface Interface {
- readonly get: () => Effect.Effect<Info>
- readonly waitForDependencies: () => Effect.Effect<void>
- }
+export interface Interface {
+ readonly get: () => Effect.Effect<Info>
+ readonly waitForDependencies: () => Effect.Effect<void>
+}
+
+export class Service extends Context.Service<Service, Interface>()("@opencode/TuiConfig") {}
+
+function pluginScope(file: string, ctx: { directory: string }): ConfigPlugin.Scope {
+ if (Filesystem.contains(ctx.directory, file)) return "local"
+ // if (ctx.worktree !== "/" && Filesystem.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 }): ConfigPlugin.Scope {
- if (Filesystem.contains(ctx.directory, file)) return "local"
- // if (ctx.worktree !== "/" && Filesystem.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
+ delete data.tui
+ return {
+ ...tui,
+ ...data,
}
+}
- function normalize(raw: Record<string, unknown>) {
- const data = { ...raw }
- if (!("tui" in data)) return data
- if (!isRecord(data.tui)) {
- delete data.tui
- return data
- }
+async function resolvePlugins(config: Info, configFilepath: string) {
+ if (!config.plugin) return config
+ for (let i = 0; i < config.plugin.length; i++) {
+ config.plugin[i] = await ConfigPlugin.resolvePluginSpec(config.plugin[i], configFilepath)
+ }
+ return config
+}
- const tui = data.tui
- delete data.tui
- return {
- ...tui,
- ...data,
- }
+async function mergeFile(acc: Acc, file: string, ctx: { directory: string }) {
+ const data = await loadFile(file)
+ acc.result = mergeDeep(acc.result, data)
+ if (!data.plugin?.length) return
+
+ const scope = pluginScope(file, ctx)
+ const plugins = ConfigPlugin.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
+}
+
+async function loadState(ctx: { directory: string }) {
+ let projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("tui", ctx.directory)
+ const directories = await ConfigPaths.directories(ctx.directory)
+ const custom = customPath()
+ await migrateTuiConfig({ directories, custom, cwd: ctx.directory })
+ // 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)
+
+ const acc: Acc = {
+ result: {},
}
- async function resolvePlugins(config: Info, configFilepath: string) {
- if (!config.plugin) return config
- for (let i = 0; i < config.plugin.length; i++) {
- config.plugin[i] = await ConfigPlugin.resolvePluginSpec(config.plugin[i], configFilepath)
- }
- return config
+ 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 }) {
- const data = await loadFile(file)
- acc.result = mergeDeep(acc.result, data)
- if (!data.plugin?.length) return
-
- const scope = pluginScope(file, ctx)
- const plugins = ConfigPlugin.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 }) {
- let projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("tui", ctx.directory)
- const directories = await ConfigPaths.directories(ctx.directory)
- const custom = customPath()
- await migrateTuiConfig({ directories, custom, cwd: ctx.directory })
- // 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)
+ for (const file of projectFiles) {
+ await mergeFile(acc, file, ctx)
+ }
- const acc: Acc = {
- result: {},
- }
+ const dirs = unique(directories).filter((dir) => dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR)
- for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) {
+ 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)
}
+ }
- if (custom) {
- await mergeFile(acc, custom, ctx)
- log.debug("loaded custom tui config", { path: custom })
- }
+ 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",
+ ...ConfigKeybinds.Keybinds.shape.input_undo.parse(undefined).split(","),
+ ]).join(",")
+ }
+ acc.result.keybinds = ConfigKeybinds.Keybinds.parse(keybinds)
- for (const file of projectFiles) {
- await mergeFile(acc, file, ctx)
- }
+ return {
+ config: acc.result,
+ dirs: acc.result.plugin?.length ? dirs : [],
+ }
+}
- const dirs = unique(directories).filter((dir) => dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR)
+export const layer = Layer.effect(
+ Service,
+ Effect.gen(function* () {
+ const directory = yield* CurrentWorkingDirectory
+ const npm = yield* Npm.Service
+ const data = yield* Effect.promise(() => loadState({ directory }))
+ const deps = yield* Effect.forEach(
+ data.dirs,
+ (dir) =>
+ npm
+ .install(dir, {
+ add: ["@opencode-ai/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)],
+ })
+ .pipe(Effect.forkScoped),
+ {
+ concurrency: "unbounded",
+ },
+ )
- 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)
- }
- }
+ const get = Effect.fn("TuiConfig.get")(() => Effect.succeed(data.config))
- 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",
- ...ConfigKeybinds.Keybinds.shape.input_undo.parse(undefined).split(","),
- ]).join(",")
- }
- acc.result.keybinds = ConfigKeybinds.Keybinds.parse(keybinds)
+ const waitForDependencies = Effect.fn("TuiConfig.waitForDependencies")(() =>
+ Effect.forEach(deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.ignore(), Effect.asVoid),
+ )
+ return Service.of({ get, waitForDependencies })
+ }).pipe(Effect.withSpan("TuiConfig.layer")),
+)
- return {
- config: acc.result,
- dirs: acc.result.plugin?.length ? dirs : [],
- }
- }
+export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer))
- export const layer = Layer.effect(
- Service,
- Effect.gen(function* () {
- const directory = yield* CurrentWorkingDirectory
- const npm = yield* Npm.Service
- const data = yield* Effect.promise(() => loadState({ directory }))
- const deps = yield* Effect.forEach(
- data.dirs,
- (dir) =>
- npm
- .install(dir, {
- add: ["@opencode-ai/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)],
- })
- .pipe(Effect.forkScoped),
- {
- concurrency: "unbounded",
- },
- )
-
- const get = Effect.fn("TuiConfig.get")(() => Effect.succeed(data.config))
-
- const waitForDependencies = Effect.fn("TuiConfig.waitForDependencies")(() =>
- Effect.forEach(deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.ignore(), Effect.asVoid),
- )
- return Service.of({ get, waitForDependencies })
- }).pipe(Effect.withSpan("TuiConfig.layer")),
- )
-
- export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer))
-
- const { runPromise } = makeRuntime(Service, defaultLayer)
-
- export async function waitForDependencies() {
- await runPromise((svc) => svc.waitForDependencies())
- }
+const { runPromise } = makeRuntime(Service, defaultLayer)
- export async function get() {
- return runPromise((svc) => svc.get())
- }
+export async function waitForDependencies() {
+ await runPromise((svc) => svc.waitForDependencies())
+}
- 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 async function get() {
+ return runPromise((svc) => svc.get())
+}
- async function load(text: string, configFilepath: string): Promise<Info> {
- return ConfigParse.load(Info, text, {
- type: "path",
- path: configFilepath,
- missing: "empty",
- normalize: (data) => {
- if (!isRecord(data)) return {}
-
- // Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json
- // (mirroring the old opencode.json shape) still get their settings applied.
- return normalize(data)
- },
+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 {}
+ })
+}
+
+async function load(text: string, configFilepath: string): Promise<Info> {
+ return ConfigParse.load(Info, text, {
+ type: "path",
+ path: configFilepath,
+ missing: "empty",
+ normalize: (data) => {
+ if (!isRecord(data)) return {}
+
+ // Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json
+ // (mirroring the old opencode.json shape) still get their settings applied.
+ return normalize(data)
+ },
+ })
+ .then((data) => resolvePlugins(data, configFilepath))
+ .catch((error) => {
+ log.warn("invalid tui config", { path: configFilepath, error })
+ return {}
})
- .then((data) => resolvePlugins(data, configFilepath))
- .catch((error) => {
- log.warn("invalid tui config", { path: configFilepath, error })
- return {}
- })
- }
}
+
+export * as TuiConfig from "./tui"