Skip to content
Merged
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
3 changes: 2 additions & 1 deletion packages/tui/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,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 { ThemeErrorToast } from "./component/theme-error-toast"
import { ThemeProvider, useTheme, useThemes } from "./context/theme"
import { Home } from "./routes/home"
Expand Down Expand Up @@ -519,7 +520,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?.vertical ?? false
const tabsVertical = () => (config.data.tabs?.vertical ?? false) && sessionTabsFitVertically(dimensions().width)
const tabsVisible = () =>
sessionTabs.enabled() && (sessionTabs.tabs().length > 0 || sessionTabs.newTab()) && route.data.type !== "plugin"

Expand Down
5 changes: 2 additions & 3 deletions packages/tui/src/component/session-tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
sessionTabComplete,
seedSessionTabMotion,
sessionTabOverflowWidth,
sessionTabVerticalWidth,
type SessionTab,
type SessionTabUnread,
} from "../context/session-tabs-model"
Expand All @@ -21,6 +20,7 @@ import { Locale } from "../util/locale"
import { stringWidth } from "../util/string-width"
import { TabPulse, unreadGlowIntensity } from "./tab-pulse"
import { tint } from "../theme/color"
import { SESSION_SIDEBAR_WIDTH } from "../ui/layout"
import { projectName } from "../util/project"

// A long title fades out over its last cells instead of cutting hard.
Expand Down Expand Up @@ -56,12 +56,11 @@ export function SessionTabs(
function VerticalSessionTabs(props: { controller?: SessionTabsController; animations?: boolean }) {
const tabs = props.controller ?? useSessionTabs()
const data = useData()
const dimensions = useTerminalDimensions()
const theme = useTheme("elevated")
const { mode } = useThemes()
const config = useConfig().data
const animations = () => props.animations ?? config.animations ?? true
const width = () => sessionTabVerticalWidth(dimensions().width)
const width = () => SESSION_SIDEBAR_WIDTH
const hueStep = () => (mode() === "light" ? 800 : 200)
const accent = () => theme.hue.accent[hueStep()]
const activeNumber = () => theme.hue.interactive[hueStep()]
Expand Down
5 changes: 0 additions & 5 deletions packages/tui/src/context/session-tabs-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ export function sessionTabComplete(unread: SessionTabUnread | undefined, busy: b
export const SESSION_TAB_WIDTH = 22
export const SESSION_TAB_MAX_WIDTH = 32
export const SESSION_TAB_MIN_WIDTH = 8
export const SESSION_TAB_VERTICAL_WIDTH = 30
export const SESSION_TAB_VERTICAL_MIN_WIDTH = 20
export function sessionTabVerticalWidth(total: number) {
return Math.min(SESSION_TAB_VERTICAL_WIDTH, Math.max(SESSION_TAB_VERTICAL_MIN_WIDTH, Math.floor(total * 0.3)))
}
// Overflow markers reserve one gap cell beside the arrow and count, e.g. "‹12 " and " 12›".
export const sessionTabOverflowWidth = (count: number) => String(count).length + 2

Expand Down
6 changes: 4 additions & 2 deletions packages/tui/src/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ import { errorMessage } from "../../util/error"
import { useToast } from "../../ui/toast"
import stripAnsi from "strip-ansi"
import { usePromptRef } from "../../context/prompt"
import { sessionTabVerticalWidth } from "../../context/session-tabs-model"
import { sessionTabsFitVertically, SESSION_SIDEBAR_WIDTH } from "../../ui/layout"
import { projectedPromptInput } from "../../prompt/codec"
import { useEpilogue } from "../../context/epilogue"
import { normalizePath } from "../../util/path"
Expand Down Expand Up @@ -201,7 +201,9 @@ export function Session() {
const groupExploration = createMemo(() => config.session?.grouping !== "none")

const tabRailWidth = createMemo(() =>
config.tabs?.enabled && config.tabs.vertical ? sessionTabVerticalWidth(dimensions().width) : 0,
config.tabs?.enabled && config.tabs.vertical && sessionTabsFitVertically(dimensions().width)
? SESSION_SIDEBAR_WIDTH
: 0,
)
const wide = createMemo(() => dimensions().width - tabRailWidth() > 120)
const sidebarVisible = createMemo(() => {
Expand Down
3 changes: 2 additions & 1 deletion packages/tui/src/routes/session/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { PluginSlot } from "../../plugin/render"
import { withTimestampedFallback } from "@opencode-ai/util/session-title-fallback"

import { getScrollAcceleration } from "../../util/scroll"
import { SESSION_SIDEBAR_WIDTH } from "../../ui/layout"

export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
const data = useData()
Expand All @@ -18,7 +19,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
<Show when={session()}>
<box
backgroundColor={theme.background.default}
width={42}
width={SESSION_SIDEBAR_WIDTH}
height="100%"
paddingTop={1}
paddingBottom={1}
Expand Down
6 changes: 6 additions & 0 deletions packages/tui/src/ui/layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
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
}
7 changes: 0 additions & 7 deletions packages/tui/test/context/session-tabs-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,9 @@ import {
seedSessionTabMotion,
sessionTabComplete,
sessionTabOverflowWidth,
sessionTabVerticalWidth,
} from "../../src/context/session-tabs-model"

describe("session tabs", () => {
test("keeps the vertical rail compact while preserving narrow-terminal content", () => {
expect(sessionTabVerticalWidth(140)).toBe(30)
expect(sessionTabVerticalWidth(90)).toBe(27)
expect(sessionTabVerticalWidth(60)).toBe(20)
})

test("moves a tab to a clamped index and returns the same tabs for no-ops", () => {
const tabs = ["a", "b", "c"].map((sessionID) => ({ sessionID }))
expect(moveSessionTab(tabs, "a", 2).map((tab) => tab.sessionID)).toEqual(["b", "c", "a"])
Expand Down
8 changes: 8 additions & 0 deletions packages/tui/test/ui/layout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { expect, test } from "bun:test"
import { sessionTabsFitVertically, SESSION_SIDEBAR_WIDTH } 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)
})
Loading