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"])