Explorar el Código

fix(tui): align session tab shortcut labels (#41308)

Kit Langton hace 3 días
padre
commit
bc51baa9a4

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

@@ -118,6 +118,7 @@ const sessionTabBindingCommands = [
   "session.tab.select.7",
   "session.tab.select.8",
   "session.tab.select.9",
+  "session.tab.select.10",
 ] as const
 
 const pinnedSessionBindingCommands = [
@@ -714,7 +715,7 @@ function App(props: { pair?: DialogPairCredentials }) {
         enabled: sessionTabs.enabled,
         run: () => sessionTabs.reopen(),
       },
-      ...Array.from({ length: 9 }, (_, i) => ({
+      ...Array.from({ length: 10 }, (_, i) => ({
         name: `session.tab.select.${i + 1}`,
         title: `Switch to tab ${i + 1}`,
         category: "Session",

+ 6 - 5
packages/tui/src/component/session-tabs.tsx

@@ -10,6 +10,7 @@ import {
   moveSessionTab,
   NEW_SESSION_TAB_TITLE,
   sessionTabComplete,
+  sessionTabShortcutLabel,
   seedSessionTabMotion,
   sessionTabOverflowWidth,
   type SessionTab,
@@ -140,7 +141,7 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
                 const value = session()
                 return value ? data.project.get(value.projectID) : undefined
               })
-              const numberWidth = () => String(index() + 1).length + 1
+              const numberWidth = () => 2
               const titleWidth = () => Math.max(1, width() - numberWidth() - 2 - (hovered() === tab.sessionID ? 1 : 0))
               const title = () => tab.title ?? "Untitled session"
               const visibleTitle = createMemo(() => Locale.takeWidth(title(), titleWidth()))
@@ -311,7 +312,7 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
                         selectable={false}
                         attributes={selected() ? TextAttributes.BOLD : undefined}
                       >
-                        {index() + 1}
+                        {sessionTabShortcutLabel(index())}
                       </text>
                       <text
                         width={titleWidth()}
@@ -555,8 +556,8 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
           const glows = () => !selected() && (status().attention || (!status().busy && status().unread !== undefined))
           const title = () => tab.title ?? "Untitled session"
           const tabNumber = createMemo(() => items().findIndex((item) => item.sessionID === tab.sessionID) + 1)
-          // The number cell keeps one trailing space, even for double-digit tabs.
-          const numberWidth = () => String(tabNumber()).length + 1
+          // Shortcut labels stay one cell wide: 1-9, 0 for ten, then a neutral dot.
+          const numberWidth = () => 2
           // Hovering reveals the close mark, so the title's right bound shifts left of it.
           const availableTitleWidth = () =>
             Math.max(1, width() - 1 - numberWidth() - (hovered() === tab.sessionID ? 2 : 0))
@@ -639,7 +640,7 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
                   {" "}
                 </text>
                 <text width={numberWidth()} fg={numberColor()} selectable={false} attributes={bold()}>
-                  {tabNumber()}
+                  {sessionTabShortcutLabel(tabNumber() - 1)}
                 </text>
                 <text
                   width={availableTitleWidth()}

+ 2 - 0
packages/tui/src/config/v1/keybind.ts

@@ -126,6 +126,7 @@ export const Definitions = {
   session_tab_select_7: keybind("<leader>7,ctrl+7", "Switch to tab 7"),
   session_tab_select_8: keybind("<leader>8,ctrl+8", "Switch to tab 8"),
   session_tab_select_9: keybind("<leader>9,ctrl+9", "Switch to tab 9"),
+  session_tab_select_10: keybind("<leader>0,ctrl+0", "Switch to tab 10"),
 
   stash_delete: keybind("ctrl+d", "Delete stash entry"),
   model_provider_list: keybind("ctrl+a", "Open provider list from model dialog"),
@@ -329,6 +330,7 @@ export const CommandMap = {
   session_tab_select_7: "session.tab.select.7",
   session_tab_select_8: "session.tab.select.8",
   session_tab_select_9: "session.tab.select.9",
+  session_tab_select_10: "session.tab.select.10",
   stash_delete: "stash.delete",
   model_provider_list: "model.dialog.provider",
   model_favorite_toggle: "model.dialog.favorite",

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

@@ -7,6 +7,12 @@ export type SessionTabUnread = "activity" | "error"
 
 export const NEW_SESSION_TAB_TITLE = "New session"
 
+export function sessionTabShortcutLabel(index: number) {
+  if (index >= 0 && index < 9) return String(index + 1)
+  if (index === 9) return "0"
+  return "·"
+}
+
 export type SessionTabHistory = {
   entries: readonly string[]
   index: number

+ 1 - 0
packages/tui/test/config.test.tsx

@@ -131,6 +131,7 @@ test("preserves pinned session bindings alongside tab bindings", () => {
   expect(config.keybinds.get("session.pin.toggle")).toMatchObject([{ key: "ctrl+f" }])
   expect(config.keybinds.get("session.quick_switch.1")).toMatchObject([{ key: "<leader>1" }])
   expect(config.keybinds.get("session.tab.select.1")).toMatchObject([{ key: "<leader>1,ctrl+1" }])
+  expect(config.keybinds.get("session.tab.select.10")).toMatchObject([{ key: "<leader>0,ctrl+0" }])
 })
 
 test("disables suspend and assigns ctrl+z to undo when unsupported", () => {

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

@@ -12,9 +12,27 @@ import {
   seedSessionTabMotion,
   sessionTabComplete,
   sessionTabOverflowWidth,
+  sessionTabShortcutLabel,
 } from "../../src/context/session-tabs-model"
 
 describe("session tabs", () => {
+  test("labels direct shortcut tabs and marks unbound tabs with a dot", () => {
+    expect(Array.from({ length: 12 }, (_, index) => sessionTabShortcutLabel(index))).toEqual([
+      "1",
+      "2",
+      "3",
+      "4",
+      "5",
+      "6",
+      "7",
+      "8",
+      "9",
+      "0",
+      "·",
+      "·",
+    ])
+  })
+
   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"])