From 94a6ab2d0cd1f6906da15005bd64c2f93979d08b Mon Sep 17 00:00:00 2001 From: marcelarie Date: Mon, 5 Jan 2026 16:27:13 +0100 Subject: [PATCH 1/4] feat(tui): add config option for sidebar default state Adds a new `tui.sidebar` config option to set the sidebar's initial state. Accepts "auto" (default), "show", or "hide". - Config provides initial default before user interaction - User toggles via keybind are stored in KV and take precedence - Priority: KV store > config.tui.sidebar > "auto" Closes #3682 --- .../src/cli/cmd/tui/routes/session/index.tsx | 4 +- packages/opencode/src/config/config.ts | 4 + .../test/cli/tui/sidebar-config.test.ts | 48 ++++++++++++ packages/opencode/test/config/config.test.ts | 75 +++++++++++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 4 + packages/sdk/openapi.json | 5 ++ 6 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 packages/opencode/test/cli/tui/sidebar-config.test.ts diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 1503e37d99e..12a4c2c0237 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -130,7 +130,9 @@ export function Session() { }) const dimensions = useTerminalDimensions() - const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">(kv.get("sidebar", "auto")) + const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">( + kv.get("sidebar", sync.data.config.tui?.sidebar ?? "auto"), + ) const [conceal, setConceal] = createSignal(true) const [showThinking, setShowThinking] = createSignal(kv.get("thinking_visibility", true)) const [showTimestamps, setShowTimestamps] = createSignal(kv.get("timestamps", "hide") === "show") diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 8eafb92e815..4c1e39f6133 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -665,6 +665,10 @@ export namespace Config { .enum(["auto", "stacked"]) .optional() .describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"), + sidebar: z + .enum(["auto", "show", "hide"]) + .default("auto") + .describe("Default sidebar visibility: 'auto' shows on wide terminals, 'show' always visible, 'hide' always hidden"), }) export const Server = z diff --git a/packages/opencode/test/cli/tui/sidebar-config.test.ts b/packages/opencode/test/cli/tui/sidebar-config.test.ts new file mode 100644 index 00000000000..bc8946e62b2 --- /dev/null +++ b/packages/opencode/test/cli/tui/sidebar-config.test.ts @@ -0,0 +1,48 @@ +import { test, expect, describe } from "bun:test" + +// Tests for TUI sidebar config behavior +// The sidebar uses: kv.get("sidebar", sync.data.config.tui?.sidebar ?? "auto") +// Priority: KV store > config.tui.sidebar > "auto" + +describe("TUI Sidebar Config Behavior", () => { + test("config provides initial default before user interaction", () => { + // User opens OpenCode with tui.sidebar = "hide" and has never toggled sidebar + const kvValue = undefined + const configValue = "hide" + const defaultValue = "auto" + + const result = kvValue ?? configValue ?? defaultValue + + expect(result).toBe("hide") + }) + + test("KV store takes precedence over config after user toggles", () => { + // User toggled sidebar to "show" even though config says "hide" + const kvValue = "show" + const configValue = "hide" + const defaultValue = "auto" + + const result = kvValue ?? configValue ?? defaultValue + + expect(result).toBe("show") + }) + + test("default is used when neither KV nor config is set", () => { + const kvValue = undefined + const configValue = undefined + const defaultValue = "auto" + + const result = kvValue ?? configValue ?? defaultValue + + expect(result).toBe("auto") + }) + + test("all valid sidebar values are accepted", () => { + const validValues = ["auto", "show", "hide"] as const + + for (const value of validValues) { + const result = value + expect(["auto", "show", "hide"]).toContain(result) + } + }) +}) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index c35a391f838..c89a5c56bdd 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -868,3 +868,78 @@ test("merges legacy tools with existing permission config", async () => { }, }) }) + +// TUI sidebar config tests +// Note: The TUI uses kv.get("sidebar", config.tui?.sidebar ?? "auto") to initialize. +// This means the config provides the default, but user preferences in the KV store take precedence. +// The KV store is populated when users toggle the sidebar with the keybind. +// To test with a fresh state, clear ~/.local/state/opencode/kv.json or use OPENCODE_STATE_DIR. + +test("loads TUI sidebar config with default", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + tui: {}, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.tui?.sidebar).toBe("auto") + }, + }) +}) + +test("loads TUI sidebar config with custom value", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + tui: { + sidebar: "hide", + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.tui?.sidebar).toBe("hide") + }, + }) +}) + +test("TUI sidebar config accepts all valid values", async () => { + for (const value of ["auto", "show", "hide"] as const) { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + tui: { + sidebar: value, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.tui?.sidebar).toBe(value) + }, + }) + } +}) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 431135db3d1..e203ba50675 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1487,6 +1487,10 @@ export type Config = { * Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column */ diff_style?: "auto" | "stacked" + /** + * Default sidebar visibility: 'auto' shows on wide terminals, 'show' always visible, 'hide' always hidden + */ + sidebar?: "auto" | "show" | "hide" } server?: ServerConfig /** diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 3e7bd5e08da..f26d865cf9d 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -8736,6 +8736,11 @@ "description": "Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column", "type": "string", "enum": ["auto", "stacked"] + }, + "sidebar": { + "description": "Control sidebar visibility: 'show' always shows sidebar, 'hide' always hides it, 'auto' adapts to terminal width", + "type": "string", + "enum": ["show", "hide", "auto"] } } }, From 13b257380204b99589ce74991157b8908960bb43 Mon Sep 17 00:00:00 2001 From: marcelarie Date: Mon, 5 Jan 2026 16:41:58 +0100 Subject: [PATCH 2/4] fix(tui): correct sidebar config priority to enable per-workspace settings --- .../src/cli/cmd/tui/routes/session/index.tsx | 2 +- .../test/cli/tui/sidebar-config.test.ts | 21 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 12a4c2c0237..81ae607a1ec 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -131,7 +131,7 @@ export function Session() { const dimensions = useTerminalDimensions() const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">( - kv.get("sidebar", sync.data.config.tui?.sidebar ?? "auto"), + sync.data.config.tui?.sidebar ?? kv.get("sidebar", "auto"), ) const [conceal, setConceal] = createSignal(true) const [showThinking, setShowThinking] = createSignal(kv.get("thinking_visibility", true)) diff --git a/packages/opencode/test/cli/tui/sidebar-config.test.ts b/packages/opencode/test/cli/tui/sidebar-config.test.ts index bc8946e62b2..86dcd421ccb 100644 --- a/packages/opencode/test/cli/tui/sidebar-config.test.ts +++ b/packages/opencode/test/cli/tui/sidebar-config.test.ts @@ -1,28 +1,29 @@ import { test, expect, describe } from "bun:test" // Tests for TUI sidebar config behavior -// The sidebar uses: kv.get("sidebar", sync.data.config.tui?.sidebar ?? "auto") -// Priority: KV store > config.tui.sidebar > "auto" +// The sidebar uses: sync.data.config.tui?.sidebar ?? kv.get("sidebar", "auto") +// Priority: config.tui.sidebar > KV store > "auto" +// This allows per-workspace config to override global KV preferences describe("TUI Sidebar Config Behavior", () => { - test("config provides initial default before user interaction", () => { - // User opens OpenCode with tui.sidebar = "hide" and has never toggled sidebar - const kvValue = undefined + test("config takes precedence over KV store for per-workspace settings", () => { + // Workspace config set to "hide" overrides global KV preference of "show" const configValue = "hide" + const kvValue = "show" const defaultValue = "auto" - const result = kvValue ?? configValue ?? defaultValue + const result = configValue ?? kvValue ?? defaultValue expect(result).toBe("hide") }) - test("KV store takes precedence over config after user toggles", () => { - // User toggled sidebar to "show" even though config says "hide" + test("KV store is used when no workspace config is set", () => { + // No workspace config, use global KV preference + const configValue = undefined const kvValue = "show" - const configValue = "hide" const defaultValue = "auto" - const result = kvValue ?? configValue ?? defaultValue + const result = configValue ?? kvValue ?? defaultValue expect(result).toBe("show") }) From 4b9272607216da0adac4b46fb2b4df1686a1ccb3 Mon Sep 17 00:00:00 2001 From: marcelarie Date: Mon, 5 Jan 2026 16:45:50 +0100 Subject: [PATCH 3/4] fix(tui): changed from .default("auto") to .optional() to allow undefined values --- packages/opencode/src/config/config.ts | 4 ++-- packages/opencode/test/config/config.test.ts | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 4c1e39f6133..e476d21f7e6 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -667,8 +667,8 @@ export namespace Config { .describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"), sidebar: z .enum(["auto", "show", "hide"]) - .default("auto") - .describe("Default sidebar visibility: 'auto' shows on wide terminals, 'show' always visible, 'hide' always hidden"), + .optional() + .describe("Sidebar visibility: 'auto' shows on wide terminals, 'show' always visible, 'hide' always hidden"), }) export const Server = z diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index c89a5c56bdd..dda71b637b8 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -870,12 +870,11 @@ test("merges legacy tools with existing permission config", async () => { }) // TUI sidebar config tests -// Note: The TUI uses kv.get("sidebar", config.tui?.sidebar ?? "auto") to initialize. -// This means the config provides the default, but user preferences in the KV store take precedence. -// The KV store is populated when users toggle the sidebar with the keybind. -// To test with a fresh state, clear ~/.local/state/opencode/kv.json or use OPENCODE_STATE_DIR. +// The TUI uses: sync.data.config.tui?.sidebar ?? kv.get("sidebar", "auto") +// Priority: Config > KV store > "auto" +// Config provides per-workspace default, KV provides global fallback -test("loads TUI sidebar config with default", async () => { +test("sidebar is undefined when not set in config", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Bun.write( @@ -891,7 +890,7 @@ test("loads TUI sidebar config with default", async () => { directory: tmp.path, fn: async () => { const config = await Config.get() - expect(config.tui?.sidebar).toBe("auto") + expect(config.tui?.sidebar).toBeUndefined() }, }) }) From 0702b0f35884066c1d766d8ed88e2b366e91be26 Mon Sep 17 00:00:00 2001 From: marcelarie Date: Thu, 8 Jan 2026 20:39:32 +0100 Subject: [PATCH 4/4] fix(tui): improve sidebar config implementation - Fix KV persistence bug in sidebar toggle handler - Add runtime validation for config values - Add documentation for sidebar config optionk --- .../src/cli/cmd/tui/routes/session/index.tsx | 16 ++++++++-------- packages/web/src/content/docs/config.mdx | 10 +++++++++- packages/web/src/content/docs/tui.mdx | 4 +++- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 81ae607a1ec..695dc2264d1 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -130,9 +130,11 @@ export function Session() { }) const dimensions = useTerminalDimensions() - const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">( - sync.data.config.tui?.sidebar ?? kv.get("sidebar", "auto"), - ) + const validSidebar = (v: unknown): v is "show" | "hide" | "auto" => v === "show" || v === "hide" || v === "auto" + const initialSidebar = validSidebar(sync.data.config.tui?.sidebar) + ? sync.data.config.tui.sidebar + : kv.get("sidebar", "auto") + const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">(initialSidebar) const [conceal, setConceal] = createSignal(true) const [showThinking, setShowThinking] = createSignal(kv.get("thinking_visibility", true)) const [showTimestamps, setShowTimestamps] = createSignal(kv.get("timestamps", "hide") === "show") @@ -451,12 +453,10 @@ export function Session() { category: "Session", onSelect: (dialog) => { setSidebar((prev) => { - if (prev === "auto") return sidebarVisible() ? "hide" : "show" - if (prev === "show") return "hide" - return "show" + const next = prev === "auto" ? (sidebarVisible() ? "hide" : "show") : prev === "show" ? "hide" : "show" + kv.set("sidebar", next) + return next }) - if (sidebar() === "show") kv.set("sidebar", "auto") - if (sidebar() === "hide") kv.set("sidebar", "hide") dialog.clear() }, }, diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index 24b822cc423..c30d059a7f3 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -107,16 +107,24 @@ You can configure TUI-specific settings through the `tui` option. "scroll_acceleration": { "enabled": true }, - "diff_style": "auto" + "diff_style": "auto", + "sidebar": "hide" } } ``` +Set `sidebar` to control the initial state of the session sidebar: + +- `"auto"` (default) shows the sidebar when there is enough horizontal space. +- `"show"` always shows the sidebar on launch. +- `"hide"` keeps the sidebar hidden until toggled. + Available options: - `scroll_acceleration.enabled` - Enable macOS-style scroll acceleration. **Takes precedence over `scroll_speed`.** - `scroll_speed` - Custom scroll speed multiplier (default: `1`, minimum: `1`). Ignored if `scroll_acceleration.enabled` is `true`. - `diff_style` - Control diff rendering. `"auto"` adapts to terminal width, `"stacked"` always shows single column. +- `sidebar` - Initial sidebar visibility. `"auto"` shows on wide terminals, `"show"` always visible, `"hide"` always hidden. [Learn more about using the TUI here](/docs/tui). diff --git a/packages/web/src/content/docs/tui.mdx b/packages/web/src/content/docs/tui.mdx index a92da6a0226..7fd5d8d0363 100644 --- a/packages/web/src/content/docs/tui.mdx +++ b/packages/web/src/content/docs/tui.mdx @@ -350,7 +350,8 @@ You can customize TUI behavior through your OpenCode config file. "scroll_speed": 3, "scroll_acceleration": { "enabled": true - } + }, + "sidebar": "hide" } } ``` @@ -359,6 +360,7 @@ You can customize TUI behavior through your OpenCode config file. - `scroll_acceleration` - Enable macOS-style scroll acceleration for smooth, natural scrolling. When enabled, scroll speed increases with rapid scrolling gestures and stays precise for slower movements. **This setting takes precedence over `scroll_speed` and overrides it when enabled.** - `scroll_speed` - Controls how fast the TUI scrolls when using scroll commands (minimum: `1`). Defaults to `1` on Unix and `3` on Windows. **Note: This is ignored if `scroll_acceleration.enabled` is set to `true`.** +- `sidebar` - Sets the initial sidebar behavior (default: `"auto"` uses the responsive layout, `"show"` forces it visible, `"hide"` keeps it hidden until toggled) ---