fix(tui): make vertical tabs responsive (#40080)
This commit is contained in:
parent
0e3da4b4f1
commit
f158abd694
8 changed files with 24 additions and 19 deletions
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {
|
|||
sessionTabComplete,
|
||||
seedSessionTabMotion,
|
||||
sessionTabOverflowWidth,
|
||||
sessionTabVerticalWidth,
|
||||
type SessionTab,
|
||||
type SessionTabUnread,
|
||||
} from "../context/session-tabs-model"
|
||||
|
|
@ -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.
|
||||
|
|
@ -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()]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
6
packages/tui/src/ui/layout.ts
Normal file
6
packages/tui/src/ui/layout.ts
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"])
|
||||
|
|
|
|||
8
packages/tui/test/ui/layout.test.ts
Normal file
8
packages/tui/test/ui/layout.test.ts
Normal file
|
|
@ -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…
Add table
Add a link
Reference in a new issue