diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index bde0bfb0718a..984ba350be60 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -19,6 +19,7 @@ export const PluginOptions = Schema.Record(Schema.String, Schema.Unknown) export const PluginSpec = Schema.Union([Schema.String, Schema.mutable(Schema.Tuple([Schema.String, PluginOptions]))]) export const LeaderTimeoutDefault = 2000 +export const SidebarWidthDefault = 42 export const LeaderTimeout = Schema.Int.check(Schema.isGreaterThan(0)).annotate({ description: "Leader key timeout in milliseconds", }) @@ -51,6 +52,7 @@ export const Attention = Schema.Struct({ }).annotate({ description: "Attention notification and sound settings" }) const PromptSize = Schema.Int.check(Schema.isGreaterThan(0)) +const SidebarWidth = Schema.Int.check(Schema.isGreaterThan(0)) export const Prompt = Schema.Struct({ max_height: Schema.optional(PromptSize).annotate({ description: "Prompt textarea max height" }), max_width: Schema.optional(Schema.Union([PromptSize, Schema.Literal("auto")])).annotate({ @@ -72,10 +74,11 @@ export const Info = Schema.Struct({ diff_style: Schema.optional(DiffStyle), cursor: Schema.optional(Cursor), mouse: Schema.optional(Schema.Boolean).annotate({ description: "Enable or disable mouse capture (default: true)" }), + sidebar_width: Schema.optional(SidebarWidth).annotate({ description: "Sidebar width in columns (default: 42)" }), }) export type Info = Schema.Schema.Type -export type Resolved = Omit & { +export type Resolved = Omit & { attention: { enabled: boolean notifications: boolean @@ -87,6 +90,7 @@ export type Resolved = Omit clampSidebarWidth(tuiConfig.sidebar_width, dimensions().width)) const showTimestamps = createMemo(() => timestamps() === "show") - const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4) + const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? sidebarWidth() : 0) - 4) const providers = createMemo(() => Model.index(sync.data.provider)) const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig)) @@ -1339,7 +1341,7 @@ export function Session() { - + - + diff --git a/packages/tui/src/routes/session/sidebar.tsx b/packages/tui/src/routes/session/sidebar.tsx index 0c5d2b313967..75cfc48f2df8 100644 --- a/packages/tui/src/routes/session/sidebar.tsx +++ b/packages/tui/src/routes/session/sidebar.tsx @@ -9,7 +9,7 @@ import { usePluginRuntime } from "../../plugin/runtime" import { getScrollAcceleration } from "../../util/scroll" import { WorkspaceLabel } from "../../component/workspace-label" -export function Sidebar(props: { sessionID: string; overlay?: boolean }) { +export function Sidebar(props: { sessionID: string; overlay?: boolean; width: number }) { const pluginRuntime = usePluginRuntime() const project = useProject() const sync = useSync() @@ -27,7 +27,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { { expect(() => decodeInfo({ prompt: { max_width: 0 } })).toThrow() expect(() => decodeInfo({ scroll_speed: 0 })).toThrow() expect(() => decodeInfo({ cursor: { style: "beam" } })).toThrow() + expect(() => decodeInfo({ sidebar_width: 42.7 })).toThrow() expect(decodeInfo({ attention: { sounds: { unknown: "sound.wav" } } })).toEqual({ attention: { sounds: {} } }) }) @@ -61,6 +63,7 @@ test("resolves host-neutral defaults", () => { }) expect(config.leader_timeout).toBe(LeaderTimeoutDefault) expect(config.mouse).toBe(true) + expect(config.sidebar_width).toBe(SidebarWidthDefault) expect(config.keybinds.has("terminal.suspend")).toBe(true) expect(config.keybinds.has("session.list")).toBe(true) expect(config.cursor).toBeUndefined() @@ -70,6 +73,7 @@ test("resolves overrides without mutating input", () => { const input: TuiConfigInfo = { theme: "custom", mouse: false, + sidebar_width: 56, leader_timeout: 750, attention: { enabled: true, @@ -87,6 +91,7 @@ test("resolves overrides without mutating input", () => { expect(config).toMatchObject({ theme: "custom", mouse: false, + sidebar_width: 56, leader_timeout: 750, attention: input.attention, cursor: { style: "block", blinking: false }, diff --git a/packages/tui/test/util/sidebar-width.test.ts b/packages/tui/test/util/sidebar-width.test.ts new file mode 100644 index 000000000000..5576cf1684cc --- /dev/null +++ b/packages/tui/test/util/sidebar-width.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, test } from "bun:test" +import { clampSidebarWidth } from "../../src/util/sidebar-width" + +describe("util.sidebar-width", () => { + test("passes through a configured width within bounds", () => { + expect(clampSidebarWidth(48, 120)).toBe(48) + }) + + test("clamps widths below the minimum to 20", () => { + expect(clampSidebarWidth(12, 120)).toBe(20) + }) + + test("clamps widths above the terminal content limit", () => { + expect(clampSidebarWidth(90, 120)).toBe(80) + }) + + test("clamps widths above the hard cap to 100", () => { + expect(clampSidebarWidth(120, 200)).toBe(100) + }) + + test("keeps the minimum when terminal bounds are inverted", () => { + expect(clampSidebarWidth(30, 55)).toBe(20) + }) + + test("defaults absent configuration to 42", () => { + expect(clampSidebarWidth(undefined, 120)).toBe(42) + }) +})