Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions packages/tui/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ import { DialogAgent } from "./component/dialog-agent"
import { DialogSessionList } from "./component/dialog-session-list"
import { DialogOpen } from "./component/dialog-open"
import { SessionTabs } from "./component/session-tabs"
import { sessionTabsFitVertically } from "./ui/layout"
import { effectiveSessionTabPosition } from "./ui/layout"
import { ThemeErrorToast } from "./component/theme-error-toast"
import { ThemeProvider, useTheme, useThemes } from "./context/theme"
import { Home } from "./routes/home"
Expand Down Expand Up @@ -513,7 +513,7 @@ function App(props: { pair?: DialogPairCredentials }) {
const terminalTitleEnabled = () => config.data.terminal?.title ?? true
const copyOnSelectEnabled = () => config.data.terminal?.copy_on_select ?? process.platform !== "win32"
const pasteSummaryEnabled = () => config.data.prompt?.paste !== "full"
const tabsVertical = () => config.data.tabs.layout === "vertical" && sessionTabsFitVertically(dimensions().width)
const tabPosition = () => effectiveSessionTabPosition(config.data.tabs.position, dimensions().width)
const tabsVisible = () =>
sessionTabs.enabled() && (sessionTabs.tabs().length > 0 || sessionTabs.newTab()) && route.data.type !== "plugin"

Expand Down Expand Up @@ -1198,13 +1198,13 @@ function App(props: { pair?: DialogPairCredentials }) {
onMouseUp={copyOnSelectEnabled() ? () => Selection.copy(renderer, toast, clipboard) : undefined}
>
<box flexGrow={1} minHeight={0} flexDirection="row">
<Show when={tabsVisible() && tabsVertical()}>
<Show when={tabsVisible() && tabPosition() === "left"}>
<SessionTabs orientation="vertical" />
</Show>
<box flexGrow={1} minWidth={0} flexDirection="column">
<Show when={plugins.ready()}>
<box flexGrow={1} minHeight={0} flexDirection="column">
<Show when={tabsVisible() && !tabsVertical()}>
<Show when={tabsVisible() && tabPosition() === "top"}>
<SessionTabs />
</Show>
<Switch>
Expand All @@ -1224,10 +1224,16 @@ function App(props: { pair?: DialogPairCredentials }) {
/>
</Match>
</Switch>
<Show when={tabsVisible() && tabPosition() === "bottom"}>
<SessionTabs />
</Show>
</box>
<PluginSlot name="app" input={{}} mode="all" />
</Show>
</box>
<Show when={tabsVisible() && tabPosition() === "right"}>
<SessionTabs orientation="vertical" />
</Show>
</box>
<Show when={devtools()}>
<DevToolsBar />
Expand Down
12 changes: 6 additions & 6 deletions packages/tui/src/component/dialog-config.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createMemo, createSignal } from "solid-js"
import { useConfig } from "../config"
import { TabPosition, useConfig } from "../config"
import { useThemes } from "../context/theme"
import { DialogSelect } from "../ui/dialog-select"
import { useToast } from "../ui/toast"
Expand Down Expand Up @@ -101,12 +101,12 @@ export const settings: Setting[] = [
labels: ["current directory", "global"],
},
{
title: "Layout",
title: "Position",
category: "Tabs",
path: ["tabs", "layout"],
default: "horizontal",
values: ["horizontal", "vertical"],
keywords: ["sidebar", "orientation", "left"],
path: ["tabs", "position"],
default: "top",
values: TabPosition.literals,
keywords: ["sidebar", "orientation", "layout"],
},
{
title: "Layout",
Expand Down
11 changes: 7 additions & 4 deletions packages/tui/src/config/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export const Cursor = Schema.Struct({
}),
}).annotate({ description: "Terminal cursor settings" })

export const TabPosition = Schema.Literals(["top", "bottom", "left", "right"])
export type TabPosition = Schema.Schema.Type<typeof TabPosition>

export const Info = Schema.Struct({
theme: Schema.optional(
Schema.Struct({
Expand Down Expand Up @@ -141,8 +144,8 @@ export const Info = Schema.Struct({
scope: Schema.optional(Schema.Literals(["global", "cwd"])).annotate({
description: "Share tabs globally or keep a separate set for each working directory",
}),
layout: Schema.optional(Schema.Literals(["horizontal", "vertical"])).annotate({
description: "Show tabs in a horizontal strip or vertical sidebar",
position: Schema.optional(TabPosition).annotate({
description: "Show tabs along the top, bottom, left, or right edge",
}),
}),
).annotate({ description: "Tab strip settings" }),
Expand Down Expand Up @@ -208,7 +211,7 @@ export type Resolved = Omit<Info, "attention" | "cursor" | "keybinds" | "leader"
tabs: {
enabled: boolean
scope: "global" | "cwd"
layout: "horizontal" | "vertical"
position: TabPosition
}
}

Expand Down Expand Up @@ -250,7 +253,7 @@ export function resolve(input: Info, options: { terminalSuspend: boolean }): Res
...input.tabs,
enabled: input.tabs?.enabled ?? true,
scope: input.tabs?.scope ?? "cwd",
layout: input.tabs?.layout ?? "horizontal",
position: input.tabs?.position ?? "top",
},
}
}
Expand Down
6 changes: 2 additions & 4 deletions packages/tui/src/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ import { errorMessage } from "../../util/error"
import { useToast } from "../../ui/toast"
import stripAnsi from "strip-ansi"
import { usePromptRef } from "../../context/prompt"
import { sessionTabsFitVertically, SESSION_SIDEBAR_WIDTH } from "../../ui/layout"
import { sessionTabSidebarWidth } from "../../ui/layout"
import { projectedPromptInput } from "../../prompt/codec"
import { useEpilogue } from "../../context/epilogue"
import { normalizePath } from "../../util/path"
Expand Down Expand Up @@ -225,9 +225,7 @@ export function Session() {
const availableWidth = createMemo(
() =>
dimensions().width -
(config.tabs?.enabled && config.tabs.layout === "vertical" && sessionTabsFitVertically(dimensions().width)
? SESSION_SIDEBAR_WIDTH
: 0),
(config.tabs?.enabled ? sessionTabSidebarWidth(config.tabs.position, dimensions().width) : 0),
)
const wide = createMemo(() => availableWidth() > 120)
const sidebarVisible = createMemo(() => {
Expand Down
13 changes: 13 additions & 0 deletions packages/tui/src/ui/layout.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import type { TabPosition } from "../config"

export const SESSION_SIDEBAR_WIDTH = 42
const SESSION_CONTENT_MIN_WIDTH = 44

export function sessionTabsFitVertically(total: number) {
return total >= SESSION_SIDEBAR_WIDTH + SESSION_CONTENT_MIN_WIDTH
}

export function effectiveSessionTabPosition(position: TabPosition, total: number): TabPosition {
if ((position === "left" || position === "right") && !sessionTabsFitVertically(total)) return "top"
return position
}

export function sessionTabSidebarWidth(position: TabPosition, total: number) {
const effective = effectiveSessionTabPosition(position, total)
if (effective === "left" || effective === "right") return SESSION_SIDEBAR_WIDTH
return 0
}
20 changes: 11 additions & 9 deletions packages/tui/test/config-v2.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
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"
import { resolve, ConfigProvider, Info, TabPosition, useConfig, type Interface } from "../src/config"
import { settingID, settings } from "../src/component/dialog-config"

test("validates mini replay settings", () => {
const decode = Schema.decodeUnknownSync(Info)
Expand All @@ -18,10 +18,10 @@ test("validates mini replay settings", () => {
test("validates the session tabs setting", () => {
const decode = Schema.decodeUnknownSync(Info)

expect(decode({ tabs: { enabled: true, layout: "vertical" } })).toEqual({
tabs: { enabled: true, layout: "vertical" },
expect(decode({ tabs: { enabled: true, position: "right" } })).toEqual({
tabs: { enabled: true, position: "right" },
})
expect(() => decode({ tabs: { layout: true } })).toThrow()
expect(() => decode({ tabs: { position: "vertical" } })).toThrow()
expect(() => decode({ tabs: { enabled: "on" } })).toThrow()
})

Expand All @@ -42,13 +42,15 @@ 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", layout: "horizontal" })
expect(config.tabs).toEqual({ enabled: true, scope: "cwd", position: "top" })
})

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")
expect(settings.find((setting) => setting.path.join(".") === "tabs.layout")?.default).toBe("horizontal")
expect(settings.find((setting) => settingID(setting) === "tabs.enabled")?.default).toBe(true)
expect(settings.find((setting) => settingID(setting) === "tabs.scope")?.default).toBe("cwd")
const position = settings.find((setting) => settingID(setting) === "tabs.position")
expect(position?.default).toBe("top")
expect(position?.values).toBe(TabPosition.literals)
})

test("provides config and its host interface", async () => {
Expand Down
21 changes: 20 additions & 1 deletion packages/tui/test/ui/layout.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
import { expect, test } from "bun:test"
import { sessionTabsFitVertically, SESSION_SIDEBAR_WIDTH } from "../../src/ui/layout"
import { TabPosition } from "../../src/config"
import {
effectiveSessionTabPosition,
SESSION_SIDEBAR_WIDTH,
sessionTabSidebarWidth,
sessionTabsFitVertically,
} from "../../src/ui/layout"

test("vertical tabs match the session sidebar and preserve compact content width", () => {
expect(SESSION_SIDEBAR_WIDTH).toBe(42)
expect(sessionTabsFitVertically(86)).toBe(true)
expect(sessionTabsFitVertically(85)).toBe(false)
})

test("preserves all tab positions when they fit", () => {
expect(TabPosition.literals.map((position) => effectiveSessionTabPosition(position, 120))).toEqual([
...TabPosition.literals,
])
})

test("falls side tabs back to the top strip when narrow", () => {
expect(effectiveSessionTabPosition("left", 85)).toBe("top")
expect(effectiveSessionTabPosition("right", 85)).toBe("top")
expect(sessionTabSidebarWidth("left", 85)).toBe(0)
expect(sessionTabSidebarWidth("right", 86)).toBe(SESSION_SIDEBAR_WIDTH)
})
Loading