Ver Fonte

fix(tui): make vertical tabs responsive (#40080)

Kit Langton há 2 semanas atrás
pai
commit
f158abd694

+ 2 - 1
packages/tui/src/app.tsx

@@ -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"
 

+ 2 - 3
packages/tui/src/component/session-tabs.tsx

@@ -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()]

+ 0 - 5
packages/tui/src/context/session-tabs-model.ts

@@ -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
 

+ 4 - 2
packages/tui/src/routes/session/index.tsx

@@ -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(() => {

+ 2 - 1
packages/tui/src/routes/session/sidebar.tsx

@@ -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 - 0
packages/tui/src/ui/layout.ts

@@ -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
+}

+ 0 - 7
packages/tui/test/context/session-tabs-model.test.ts

@@ -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 - 0
packages/tui/test/ui/layout.test.ts

@@ -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)
+})