From d35ca49c31710ed212286740f543222cacdb8d6f Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 6 Aug 2026 02:27:38 -0400 Subject: [PATCH] feat(tui): enable cwd-scoped session tabs by default --- packages/tui/src/component/dialog-config.tsx | 4 ++-- packages/tui/src/config/index.tsx | 12 +++++++++++- packages/tui/src/context/session-tabs.tsx | 6 +++--- packages/tui/test/config-v2.test.tsx | 7 +++++++ packages/tui/test/context/session-tabs.test.tsx | 14 +++++++------- 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/packages/tui/src/component/dialog-config.tsx b/packages/tui/src/component/dialog-config.tsx index 93c0c65128c..98abf0a99bc 100644 --- a/packages/tui/src/component/dialog-config.tsx +++ b/packages/tui/src/component/dialog-config.tsx @@ -88,7 +88,7 @@ export const settings: Setting[] = [ title: "Enabled", category: "Tabs", path: ["tabs", "enabled"], - default: false, + default: true, values: [false, true], labels: ["off", "on"], }, @@ -96,7 +96,7 @@ export const settings: Setting[] = [ title: "Scope", category: "Tabs", path: ["tabs", "scope"], - default: "global", + default: "cwd", values: ["cwd", "global"], labels: ["current directory", "global"], }, diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index fa561c724ad..d94616bc9fb 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -179,7 +179,7 @@ export const Info = Schema.Struct({ }) export type Info = Schema.Schema.Type -export type Resolved = Omit & { +export type Resolved = Omit & { attention: { enabled: boolean notifications: boolean @@ -191,6 +191,11 @@ export type Resolved = Omit keybinds: TuiKeybind.BindingLookupView leader: { timeout: number } mouse: boolean + tabs: { + enabled: boolean + scope: "global" | "cwd" + vertical?: boolean + } } export function resolve(input: Info, options: { terminalSuspend: boolean }): Resolved { @@ -221,6 +226,11 @@ export function resolve(input: Info, options: { terminalSuspend: boolean }): Res }), leader: { timeout: input.leader?.timeout ?? 2000 }, mouse: input.mouse ?? true, + tabs: { + ...input.tabs, + enabled: input.tabs?.enabled ?? true, + scope: input.tabs?.scope ?? "cwd", + }, } } diff --git a/packages/tui/src/context/session-tabs.tsx b/packages/tui/src/context/session-tabs.tsx index 528e5b9bcb6..c980c4d93b1 100644 --- a/packages/tui/src/context/session-tabs.tsx +++ b/packages/tui/src/context/session-tabs.tsx @@ -49,7 +49,7 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp const event = useEvent() const config = useConfig().data const paths = useTuiPaths() - const enabled = () => config.tabs?.enabled ?? false + const enabled = () => config.tabs.enabled // Keyed reconcile keeps tab object identity across reorders, so strip rows move instead of // mutating in place, which per-row animations and drag state depend on. const [store, updateStore] = useStorage().store("tabs", { @@ -66,12 +66,12 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp let closedTabs: ClosedSessionTab[] = [] function state() { - if (config.tabs?.scope === "cwd") return store.cwd[paths.cwd] ?? fallback + if (config.tabs.scope === "cwd") return store.cwd[paths.cwd] ?? fallback return store.global } function update(mutation: (draft: TabsState) => void) { - const scope = config.tabs?.scope ?? "global" + const scope = config.tabs.scope void updateStore((draft) => mutation(scope === "cwd" ? (draft.cwd[paths.cwd] ??= empty()) : draft.global)).catch( // Failed writes lose only tab layout, but silence would hide tabs resetting every launch. (error) => console.error("Failed to persist session tabs", error), diff --git a/packages/tui/test/config-v2.test.tsx b/packages/tui/test/config-v2.test.tsx index 9a16675556c..19a1be0512a 100644 --- a/packages/tui/test/config-v2.test.tsx +++ b/packages/tui/test/config-v2.test.tsx @@ -3,6 +3,7 @@ import { testRender } from "@opentui/solid" import { expect, test } from "bun:test" import { Schema } from "effect" import { resolve, ConfigProvider, Info, useConfig, type Interface } from "../src/config" +import { settings } from "../src/component/dialog-config" test("validates mini replay settings", () => { const decode = Schema.decodeUnknownSync(Info) @@ -38,6 +39,12 @@ test("resolves nested config and keybind defaults", () => { expect(config.scroll).toEqual({ speed: 2, acceleration: true }) expect(config.diffs).toEqual({ view: "split" }) expect(config.debug).toEqual({ devtools: true }) + expect(config.tabs).toEqual({ enabled: true, scope: "cwd" }) +}) + +test("shows resolved tab defaults in settings", () => { + expect(settings.find((setting) => setting.path.join(".") === "tabs.enabled")?.default).toBe(true) + expect(settings.find((setting) => setting.path.join(".") === "tabs.scope")?.default).toBe("cwd") }) test("provides config and its host interface", async () => { diff --git a/packages/tui/test/context/session-tabs.test.tsx b/packages/tui/test/context/session-tabs.test.tsx index 471a3156d40..0739f04a5ea 100644 --- a/packages/tui/test/context/session-tabs.test.tsx +++ b/packages/tui/test/context/session-tabs.test.tsx @@ -60,8 +60,8 @@ async function renderSessionTabs( await Bun.write( file, JSON.stringify({ - global: { tabs: options.persisted.map((sessionID) => ({ sessionID })), unread: {} }, - cwd: {}, + global: { tabs: [], unread: {} }, + cwd: { [directory]: { tabs: options.persisted.map((sessionID) => ({ sessionID })), unread: {} } }, }), ) } @@ -153,15 +153,15 @@ test("loads persisted tab metadata concurrently on connect", async () => { } }) -test("stores session tabs globally by default", async () => { +test("stores session tabs for the current working directory by default", async () => { const setup = await renderSessionTabs("first") try { const file = path.join(setup.state, "test", "tui", "tabs.json") await wait(() => Bun.file(file).size > 0) expect(await Bun.file(file).json()).toEqual({ - global: { tabs: [{ sessionID: "first" }], unread: {} }, - cwd: {}, + global: { tabs: [], unread: {} }, + cwd: { [directory]: { tabs: [{ sessionID: "first" }], unread: {} } }, }) } finally { setup.destroy() @@ -180,7 +180,7 @@ test("concurrent TUIs do not alternate shared tab titles from divergent session await titled.data.session.sync("shared") await wait(async () => { if (!(await Bun.file(file).exists())) return false - return (await Bun.file(file).json()).global.tabs[0]?.title === "Generated title" + return (await Bun.file(file).json()).cwd[directory]?.tabs[0]?.title === "Generated title" }) const observed = ["Generated title"] const pending = new Set>() @@ -189,7 +189,7 @@ test("concurrent TUIs do not alternate shared tab titles from divergent session const read = Bun.file(file) .json() .then((value) => { - const title = value.global.tabs[0]?.title + const title = value.cwd[directory]?.tabs[0]?.title if (title && observed.at(-1) !== title) observed.push(title) }) .catch(() => undefined)