diff --git a/packages/opencode/script/schema.ts b/packages/opencode/script/schema.ts index eaa678524d..9052645d54 100755 --- a/packages/opencode/script/schema.ts +++ b/packages/opencode/script/schema.ts @@ -3,7 +3,10 @@ import { z } from "zod" import { Config } from "@/config/config" import { zodObject } from "@opencode-ai/core/effect-zod" -import { TuiConfig } from "../src/cli/cmd/tui/config/tui" +import { TuiJsonSchema } from "../src/cli/cmd/tui/config/tui-json-schema" +import { Schema } from "effect" + +type JsonSchema = Record function generate(schema: z.ZodType) { const result = z.toJSONSchema(schema, { @@ -34,7 +37,7 @@ function generate(schema: z.ZodType) { schema.examples = [schema.default] } - schema.description = [schema.description || "", `default: \`${String(schema.default)}\``] + schema.description = [schema.description || "", `default: \`${formatDefault(schema.default)}\``] .filter(Boolean) .join("\n\n") .trim() @@ -52,6 +55,55 @@ function generate(schema: z.ZodType) { return result } +function formatDefault(value: unknown) { + if (typeof value !== "object" || value === null) return String(value) + return JSON.stringify(value) +} + +function generateEffect(schema: Schema.Top) { + const document = Schema.toJsonSchemaDocument(schema) + const normalized = normalize({ + $schema: "https://json-schema.org/draft/2020-12/schema", + ...document.schema, + $defs: document.definitions, + }) + if (!isRecord(normalized)) throw new Error("schema generator produced a non-object schema") + normalized.allowComments = true + normalized.allowTrailingCommas = true + return normalized +} + +function normalize(value: unknown): unknown { + if (Array.isArray(value)) return value.map(normalize) + if (!isRecord(value)) return value + + const schema = Object.fromEntries(Object.entries(value).map(([key, item]) => [key, normalize(item)])) + + if (Array.isArray(schema.anyOf)) { + const anyOf = schema.anyOf.filter((item) => !isRecord(item) || item.type !== "null") + if (anyOf.length !== schema.anyOf.length) { + const { anyOf: _, ...rest } = schema + if (anyOf.length === 1 && isRecord(anyOf[0])) return normalize({ ...anyOf[0], ...rest }) + return { ...rest, anyOf } + } + } + + if (Array.isArray(schema.allOf) && schema.allOf.length === 1 && isRecord(schema.allOf[0])) { + const { allOf: _, ...rest } = schema + return normalize({ ...schema.allOf[0], ...rest }) + } + + if (schema.type === "integer" && schema.maximum === undefined) { + return { ...schema, maximum: Number.MAX_SAFE_INTEGER } + } + + return schema +} + +function isRecord(value: unknown): value is JsonSchema { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + const configFile = process.argv[2] const tuiFile = process.argv[3] @@ -60,5 +112,5 @@ await Bun.write(configFile, JSON.stringify(generate(zodObject(Config.Info).stric if (tuiFile) { console.log(tuiFile) - await Bun.write(tuiFile, JSON.stringify(generate(TuiConfig.JsonSchemaInfo), null, 2)) + await Bun.write(tuiFile, JSON.stringify(generateEffect(TuiJsonSchema.Info), null, 2)) } diff --git a/packages/opencode/src/cli/cmd/tui/config/keybind.ts b/packages/opencode/src/cli/cmd/tui/config/keybind.ts index b20c87f30b..5e7fec4018 100644 --- a/packages/opencode/src/cli/cmd/tui/config/keybind.ts +++ b/packages/opencode/src/cli/cmd/tui/config/keybind.ts @@ -2,7 +2,7 @@ export * as TuiKeybind from "./keybind" import type { KeyEvent, Renderable } from "@opentui/core" import type { Binding } from "@opentui/keymap" -import type { BindingCommandMap, BindingConfig, BindingDefaults, BindingValue } from "@opentui/keymap/extras" +import type { BindingCommandMap, BindingConfig, BindingDefaults } from "@opentui/keymap/extras" import z from "zod" const KeyStroke = z @@ -38,7 +38,7 @@ export const LeaderDefault = "ctrl+x" const keybind = (value: Definition["default"], description: string): Definition => ({ default: value, description }) -const Definitions = { +export const Definitions = { leader: keybind(LeaderDefault, "Leader key for keybind combinations"), app_exit: keybind("ctrl+c,ctrl+d,q", "Exit the application"), diff --git a/packages/opencode/src/cli/cmd/tui/config/tui-json-schema.ts b/packages/opencode/src/cli/cmd/tui/config/tui-json-schema.ts new file mode 100644 index 0000000000..7784ce3ac6 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/config/tui-json-schema.ts @@ -0,0 +1,66 @@ +import { ConfigPlugin } from "@/config/plugin" +import { Schema } from "effect" +import { TuiKeybind } from "./keybind" + +const KeymapLeaderTimeout = Schema.Int.check(Schema.isGreaterThan(0)).annotate({ + description: "Leader key timeout in milliseconds", +}) + +const KeyStroke = Schema.Struct({ + name: Schema.String, + ctrl: Schema.optional(Schema.Boolean), + shift: Schema.optional(Schema.Boolean), + meta: Schema.optional(Schema.Boolean), + super: Schema.optional(Schema.Boolean), + hyper: Schema.optional(Schema.Boolean), +}) + +const BindingObject = Schema.StructWithRest( + Schema.Struct({ + key: Schema.Union([Schema.String, KeyStroke]), + event: Schema.optional(Schema.Literals(["press", "release"])), + preventDefault: Schema.optional(Schema.Boolean), + fallthrough: Schema.optional(Schema.Boolean), + }), + [Schema.Record(Schema.String, Schema.Unknown)], +) + +const BindingItem = Schema.Union([Schema.String, KeyStroke, BindingObject]) +const BindingValue = Schema.Union([ + Schema.Literal(false), + Schema.Literal("none"), + BindingItem, + Schema.Array(BindingItem), +]) + +const KeybindOverrides = Schema.Struct( + Object.fromEntries( + Object.entries(TuiKeybind.Definitions).map(([name, item]) => [ + name, + Schema.optional(BindingValue).annotate({ description: item.description }), + ]), + ), +).annotate({ description: "TUI keybinding overrides" }) + +export const Info = Schema.Struct({ + $schema: Schema.optional(Schema.String), + theme: Schema.optional(Schema.String), + keybinds: Schema.optional(KeybindOverrides), + plugin: Schema.optional(Schema.Array(ConfigPlugin.Spec)), + plugin_enabled: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)), + leader_timeout: Schema.optional(KeymapLeaderTimeout), + scroll_speed: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0.001))).annotate({ + description: "TUI scroll speed", + }), + scroll_acceleration: Schema.optional( + Schema.Struct({ + enabled: Schema.Boolean.annotate({ description: "Enable scroll acceleration" }), + }), + ).annotate({ description: "Scroll acceleration settings" }), + diff_style: Schema.optional(Schema.Literals(["auto", "stacked"])).annotate({ + description: "Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column", + }), + mouse: Schema.optional(Schema.Boolean).annotate({ description: "Enable or disable mouse capture (default: true)" }), +}) + +export * as TuiJsonSchema from "./tui-json-schema" diff --git a/packages/opencode/src/cli/cmd/tui/config/tui-schema.ts b/packages/opencode/src/cli/cmd/tui/config/tui-schema.ts index d08836e1dd..318a702464 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui-schema.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui-schema.ts @@ -31,5 +31,3 @@ export const TuiInfo = z }) .extend(TuiOptions.shape) .strict() - -export const TuiJsonSchemaInfo = TuiInfo diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts index d7409cd2db..572f50c4d1 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -8,7 +8,7 @@ import { ConfigParse } from "@/config/parse" import { InvalidError } from "@/config/error" import * as ConfigPaths from "@/config/paths" import { migrateTuiConfig } from "./tui-migrate" -import { KeymapLeaderTimeoutDefault, TuiInfo, TuiJsonSchemaInfo } from "./tui-schema" +import { KeymapLeaderTimeoutDefault, TuiInfo } from "./tui-schema" import { Flag } from "@opencode-ai/core/flag/flag" import { isRecord } from "@/util/record" import { Global } from "@opencode-ai/core/global" @@ -26,7 +26,6 @@ import { Npm } from "@opencode-ai/core/npm" const log = Log.create({ service: "tui.config" }) export const Info = TuiInfo -export const JsonSchemaInfo = TuiJsonSchemaInfo export type Info = z.output type Acc = {