diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 93ec2faafe5..c4837c9f20b 100644 --- a/packages/tui/src/app.tsx +++ b/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", diff --git a/packages/tui/src/component/session-tabs.tsx b/packages/tui/src/component/session-tabs.tsx index 678bd6bf9bd..e9c0977a76c 100644 --- a/packages/tui/src/component/session-tabs.tsx +++ b/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())} !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 {" "} - {tabNumber()} + {sessionTabShortcutLabel(tabNumber() - 1)} 7,ctrl+7", "Switch to tab 7"), session_tab_select_8: keybind("8,ctrl+8", "Switch to tab 8"), session_tab_select_9: keybind("9,ctrl+9", "Switch to tab 9"), + session_tab_select_10: keybind("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", diff --git a/packages/tui/src/context/session-tabs-model.ts b/packages/tui/src/context/session-tabs-model.ts index ef92a20c8c7..dd49fe790bd 100644 --- a/packages/tui/src/context/session-tabs-model.ts +++ b/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 diff --git a/packages/tui/test/config.test.tsx b/packages/tui/test/config.test.tsx index f9a45963e40..b1da09e22db 100644 --- a/packages/tui/test/config.test.tsx +++ b/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: "1" }]) expect(config.keybinds.get("session.tab.select.1")).toMatchObject([{ key: "1,ctrl+1" }]) + expect(config.keybinds.get("session.tab.select.10")).toMatchObject([{ key: "0,ctrl+0" }]) }) test("disables suspend and assigns ctrl+z to undo when unsupported", () => { diff --git a/packages/tui/test/context/session-tabs-model.test.ts b/packages/tui/test/context/session-tabs-model.test.ts index 9d4fd26b878..442df1abfc0 100644 --- a/packages/tui/test/context/session-tabs-model.test.ts +++ b/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"])