Skip to content

Commit 9d29d69

Browse files
authored
split tui/server config (#13968)
1 parent 1172fa4 commit 9d29d69

27 files changed

Lines changed: 1284 additions & 706 deletions

File tree

packages/console/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"typecheck": "tsgo --noEmit",
88
"dev": "vite dev --host 0.0.0.0",
99
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51RtuLNE7fOCwHSD4mewwzFejyytjdGoSDK7CAvhbffwaZnPbNb2rwJICw6LTOXCmWO320fSNXvb5NzI08RZVkAxd00syfqrW7t bun sst shell --stage=dev bun dev",
10-
"build": "bun ./script/generate-sitemap.ts && vite build && bun ../../opencode/script/schema.ts ./.output/public/config.json",
10+
"build": "bun ./script/generate-sitemap.ts && vite build && bun ../../opencode/script/schema.ts ./.output/public/config.json ./.output/public/tui.json",
1111
"start": "vite start"
1212
},
1313
"dependencies": {

packages/opencode/script/schema.ts

Lines changed: 53 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,62 @@
22

33
import { z } from "zod"
44
import { Config } from "../src/config/config"
5+
import { TuiConfig } from "../src/config/tui"
6+
7+
function generate(schema: z.ZodType) {
8+
const result = z.toJSONSchema(schema, {
9+
io: "input", // Generate input shape (treats optional().default() as not required)
10+
/**
11+
* We'll use the `default` values of the field as the only value in `examples`.
12+
* This will ensure no docs are needed to be read, as the configuration is
13+
* self-documenting.
14+
*
15+
* See https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.9.5
16+
*/
17+
override(ctx) {
18+
const schema = ctx.jsonSchema
19+
20+
// Preserve strictness: set additionalProperties: false for objects
21+
if (
22+
schema &&
23+
typeof schema === "object" &&
24+
schema.type === "object" &&
25+
schema.additionalProperties === undefined
26+
) {
27+
schema.additionalProperties = false
28+
}
29+
30+
// Add examples and default descriptions for string fields with defaults
31+
if (schema && typeof schema === "object" && "type" in schema && schema.type === "string" && schema?.default) {
32+
if (!schema.examples) {
33+
schema.examples = [schema.default]
34+
}
535

6-
const file = process.argv[2]
7-
console.log(file)
8-
9-
const result = z.toJSONSchema(Config.Info, {
10-
io: "input", // Generate input shape (treats optional().default() as not required)
11-
/**
12-
* We'll use the `default` values of the field as the only value in `examples`.
13-
* This will ensure no docs are needed to be read, as the configuration is
14-
* self-documenting.
15-
*
16-
* See https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.9.5
17-
*/
18-
override(ctx) {
19-
const schema = ctx.jsonSchema
20-
21-
// Preserve strictness: set additionalProperties: false for objects
22-
if (schema && typeof schema === "object" && schema.type === "object" && schema.additionalProperties === undefined) {
23-
schema.additionalProperties = false
24-
}
25-
26-
// Add examples and default descriptions for string fields with defaults
27-
if (schema && typeof schema === "object" && "type" in schema && schema.type === "string" && schema?.default) {
28-
if (!schema.examples) {
29-
schema.examples = [schema.default]
36+
schema.description = [schema.description || "", `default: \`${schema.default}\``]
37+
.filter(Boolean)
38+
.join("\n\n")
39+
.trim()
3040
}
41+
},
42+
}) as Record<string, unknown> & {
43+
allowComments?: boolean
44+
allowTrailingCommas?: boolean
45+
}
46+
47+
// used for json lsps since config supports jsonc
48+
result.allowComments = true
49+
result.allowTrailingCommas = true
3150

32-
schema.description = [schema.description || "", `default: \`${schema.default}\``]
33-
.filter(Boolean)
34-
.join("\n\n")
35-
.trim()
36-
}
37-
},
38-
}) as Record<string, unknown> & {
39-
allowComments?: boolean
40-
allowTrailingCommas?: boolean
51+
return result
4152
}
4253

43-
// used for json lsps since config supports jsonc
44-
result.allowComments = true
45-
result.allowTrailingCommas = true
54+
const configFile = process.argv[2]
55+
const tuiFile = process.argv[3]
4656

47-
await Bun.write(file, JSON.stringify(result, null, 2))
57+
console.log(configFile)
58+
await Bun.write(configFile, JSON.stringify(generate(Config.Info), null, 2))
59+
60+
if (tuiFile) {
61+
console.log(tuiFile)
62+
await Bun.write(tuiFile, JSON.stringify(generate(TuiConfig.Info), null, 2))
63+
}

packages/opencode/src/cli/cmd/tui/app.tsx

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import { ArgsProvider, useArgs, type Args } from "./context/args"
3838
import open from "open"
3939
import { writeHeapSnapshot } from "v8"
4040
import { PromptRefProvider, usePromptRef } from "./context/prompt"
41+
import { TuiConfigProvider } from "./context/tui-config"
42+
import { TuiConfig } from "@/config/tui"
4143

4244
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
4345
// can't set raw mode if not a TTY
@@ -104,6 +106,7 @@ import type { EventSource } from "./context/sdk"
104106
export function tui(input: {
105107
url: string
106108
args: Args
109+
config: TuiConfig.Info
107110
directory?: string
108111
fetch?: typeof fetch
109112
headers?: RequestInit["headers"]
@@ -138,35 +141,37 @@ export function tui(input: {
138141
<KVProvider>
139142
<ToastProvider>
140143
<RouteProvider>
141-
<SDKProvider
142-
url={input.url}
143-
directory={input.directory}
144-
fetch={input.fetch}
145-
headers={input.headers}
146-
events={input.events}
147-
>
148-
<SyncProvider>
149-
<ThemeProvider mode={mode}>
150-
<LocalProvider>
151-
<KeybindProvider>
152-
<PromptStashProvider>
153-
<DialogProvider>
154-
<CommandProvider>
155-
<FrecencyProvider>
156-
<PromptHistoryProvider>
157-
<PromptRefProvider>
158-
<App />
159-
</PromptRefProvider>
160-
</PromptHistoryProvider>
161-
</FrecencyProvider>
162-
</CommandProvider>
163-
</DialogProvider>
164-
</PromptStashProvider>
165-
</KeybindProvider>
166-
</LocalProvider>
167-
</ThemeProvider>
168-
</SyncProvider>
169-
</SDKProvider>
144+
<TuiConfigProvider config={input.config}>
145+
<SDKProvider
146+
url={input.url}
147+
directory={input.directory}
148+
fetch={input.fetch}
149+
headers={input.headers}
150+
events={input.events}
151+
>
152+
<SyncProvider>
153+
<ThemeProvider mode={mode}>
154+
<LocalProvider>
155+
<KeybindProvider>
156+
<PromptStashProvider>
157+
<DialogProvider>
158+
<CommandProvider>
159+
<FrecencyProvider>
160+
<PromptHistoryProvider>
161+
<PromptRefProvider>
162+
<App />
163+
</PromptRefProvider>
164+
</PromptHistoryProvider>
165+
</FrecencyProvider>
166+
</CommandProvider>
167+
</DialogProvider>
168+
</PromptStashProvider>
169+
</KeybindProvider>
170+
</LocalProvider>
171+
</ThemeProvider>
172+
</SyncProvider>
173+
</SDKProvider>
174+
</TuiConfigProvider>
170175
</RouteProvider>
171176
</ToastProvider>
172177
</KVProvider>

packages/opencode/src/cli/cmd/tui/attach.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { cmd } from "../cmd"
22
import { UI } from "@/cli/ui"
33
import { tui } from "./app"
44
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
5+
import { TuiConfig } from "@/config/tui"
6+
import { Instance } from "@/project/instance"
7+
import { existsSync } from "fs"
58

69
export const AttachCommand = cmd({
710
command: "attach <url>",
@@ -63,8 +66,13 @@ export const AttachCommand = cmd({
6366
const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`
6467
return { Authorization: auth }
6568
})()
69+
const config = await Instance.provide({
70+
directory: directory && existsSync(directory) ? directory : process.cwd(),
71+
fn: () => TuiConfig.get(),
72+
})
6673
await tui({
6774
url: args.url,
75+
config,
6876
args: {
6977
continue: args.continue,
7078
sessionID: args.session,

packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import {
1010
type ParentProps,
1111
} from "solid-js"
1212
import { useKeyboard } from "@opentui/solid"
13-
import { useKeybind } from "@tui/context/keybind"
14-
import type { KeybindsConfig } from "@opencode-ai/sdk/v2"
13+
import { type KeybindKey, useKeybind } from "@tui/context/keybind"
1514

1615
type Context = ReturnType<typeof init>
1716
const ctx = createContext<Context>()
@@ -22,7 +21,7 @@ export type Slash = {
2221
}
2322

2423
export type CommandOption = DialogSelectOption<string> & {
25-
keybind?: keyof KeybindsConfig
24+
keybind?: KeybindKey
2625
suggested?: boolean
2726
slash?: Slash
2827
hidden?: boolean

packages/opencode/src/cli/cmd/tui/component/tips.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ const TIPS = [
8080
"Switch to {highlight}Plan{/highlight} agent to get suggestions without making actual changes",
8181
"Use {highlight}@agent-name{/highlight} in prompts to invoke specialized subagents",
8282
"Press {highlight}Ctrl+X Right/Left{/highlight} to cycle through parent and child sessions",
83-
"Create {highlight}opencode.json{/highlight} in project root for project-specific settings",
84-
"Place settings in {highlight}~/.config/opencode/opencode.json{/highlight} for global config",
83+
"Create {highlight}opencode.json{/highlight} for server settings and {highlight}tui.json{/highlight} for TUI settings",
84+
"Place TUI settings in {highlight}~/.config/opencode/tui.json{/highlight} for global config",
8585
"Add {highlight}$schema{/highlight} to your config for autocomplete in your editor",
8686
"Configure {highlight}model{/highlight} in config to set your default model",
87-
"Override any keybind in config via the {highlight}keybinds{/highlight} section",
87+
"Override any keybind in {highlight}tui.json{/highlight} via the {highlight}keybinds{/highlight} section",
8888
"Set any keybind to {highlight}none{/highlight} to disable it completely",
8989
"Configure local or remote MCP servers in the {highlight}mcp{/highlight} config section",
9090
"OpenCode auto-handles OAuth for remote MCP servers requiring auth",
@@ -140,7 +140,7 @@ const TIPS = [
140140
"Press {highlight}Ctrl+X G{/highlight} or {highlight}/timeline{/highlight} to jump to specific messages",
141141
"Press {highlight}Ctrl+X H{/highlight} to toggle code block visibility in messages",
142142
"Press {highlight}Ctrl+X S{/highlight} or {highlight}/status{/highlight} to see system status info",
143-
"Enable {highlight}tui.scroll_acceleration{/highlight} for smooth macOS-style scrolling",
143+
"Enable {highlight}scroll_acceleration{/highlight} in {highlight}tui.json{/highlight} for smooth macOS-style scrolling",
144144
"Toggle username display in chat via command palette ({highlight}Ctrl+P{/highlight})",
145145
"Run {highlight}docker run -it --rm ghcr.io/anomalyco/opencode{/highlight} for containerized use",
146146
"Use {highlight}/connect{/highlight} with OpenCode Zen for curated, tested models",

packages/opencode/src/cli/cmd/tui/context/keybind.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import { createMemo } from "solid-js"
2-
import { useSync } from "@tui/context/sync"
32
import { Keybind } from "@/util/keybind"
43
import { pipe, mapValues } from "remeda"
5-
import type { KeybindsConfig } from "@opencode-ai/sdk/v2"
4+
import type { TuiConfig } from "@/config/tui"
65
import type { ParsedKey, Renderable } from "@opentui/core"
76
import { createStore } from "solid-js/store"
87
import { useKeyboard, useRenderer } from "@opentui/solid"
98
import { createSimpleContext } from "./helper"
9+
import { useTuiConfig } from "./tui-config"
10+
11+
export type KeybindKey = keyof NonNullable<TuiConfig.Info["keybinds"]> & string
1012

1113
export const { use: useKeybind, provider: KeybindProvider } = createSimpleContext({
1214
name: "Keybind",
1315
init: () => {
14-
const sync = useSync()
15-
const keybinds = createMemo(() => {
16+
const config = useTuiConfig()
17+
const keybinds = createMemo<Record<string, Keybind.Info[]>>(() => {
1618
return pipe(
17-
sync.data.config.keybinds ?? {},
19+
(config.keybinds ?? {}) as Record<string, string>,
1820
mapValues((value) => Keybind.parse(value)),
1921
)
2022
})
@@ -78,7 +80,7 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex
7880
}
7981
return Keybind.fromParsedKey(evt, store.leader)
8082
},
81-
match(key: keyof KeybindsConfig, evt: ParsedKey) {
83+
match(key: KeybindKey, evt: ParsedKey) {
8284
const keybind = keybinds()[key]
8385
if (!keybind) return false
8486
const parsed: Keybind.Info = result.parse(evt)
@@ -88,7 +90,7 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex
8890
}
8991
}
9092
},
91-
print(key: keyof KeybindsConfig) {
93+
print(key: KeybindKey) {
9294
const first = keybinds()[key]?.at(0)
9395
if (!first) return ""
9496
const result = Keybind.toString(first)

packages/opencode/src/cli/cmd/tui/context/theme.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { SyntaxStyle, RGBA, type TerminalColors } from "@opentui/core"
22
import path from "path"
33
import { createEffect, createMemo, onMount } from "solid-js"
4-
import { useSync } from "@tui/context/sync"
54
import { createSimpleContext } from "./helper"
65
import { Glob } from "../../../../util/glob"
76
import aura from "./theme/aura.json" with { type: "json" }
@@ -42,6 +41,7 @@ import { useRenderer } from "@opentui/solid"
4241
import { createStore, produce } from "solid-js/store"
4342
import { Global } from "@/global"
4443
import { Filesystem } from "@/util/filesystem"
44+
import { useTuiConfig } from "./tui-config"
4545

4646
type ThemeColors = {
4747
primary: RGBA
@@ -280,17 +280,17 @@ function ansiToRgba(code: number): RGBA {
280280
export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
281281
name: "Theme",
282282
init: (props: { mode: "dark" | "light" }) => {
283-
const sync = useSync()
283+
const config = useTuiConfig()
284284
const kv = useKV()
285285
const [store, setStore] = createStore({
286286
themes: DEFAULT_THEMES,
287287
mode: kv.get("theme_mode", props.mode),
288-
active: (sync.data.config.theme ?? kv.get("theme", "opencode")) as string,
288+
active: (config.theme ?? kv.get("theme", "opencode")) as string,
289289
ready: false,
290290
})
291291

292292
createEffect(() => {
293-
const theme = sync.data.config.theme
293+
const theme = config.theme
294294
if (theme) setStore("active", theme)
295295
})
296296

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { TuiConfig } from "@/config/tui"
2+
import { createSimpleContext } from "./helper"
3+
4+
export const { use: useTuiConfig, provider: TuiConfigProvider } = createSimpleContext({
5+
name: "TuiConfig",
6+
init: (props: { config: TuiConfig.Info }) => {
7+
return props.config
8+
},
9+
})

0 commit comments

Comments
 (0)