Ver código fonte

feat(tui): streamline tab navigation shortcuts (#40551)

Kit Langton 1 semana atrás
pai
commit
ab20a7b4a3

+ 8 - 8
packages/tui/src/config/v1/keybind.ts

@@ -88,10 +88,10 @@ export const Definitions = {
   session_new: keybind("<leader>n", "Create a new session"),
   session_new: keybind("<leader>n", "Create a new session"),
   session_list: keybind("<leader>l", "List all sessions"),
   session_list: keybind("<leader>l", "List all sessions"),
   open_menu: keybind("ctrl+o", "Open recent sessions and projects"),
   open_menu: keybind("ctrl+o", "Open recent sessions and projects"),
-  session_tab_next: keybind("ctrl+tab,<leader>right,alt+shift+]", "Switch to next open tab"),
-  session_tab_previous: keybind("ctrl+shift+tab,<leader>left,alt+shift+[", "Switch to previous open tab"),
-  session_tab_next_unread: keybind("<leader>down", "Switch to next unread tab"),
-  session_tab_previous_unread: keybind("<leader>up", "Switch to previous unread tab"),
+  session_tab_next: keybind("ctrl+tab,alt+down", "Switch to next open tab"),
+  session_tab_previous: keybind("ctrl+shift+tab,alt+up", "Switch to previous open tab"),
+  session_tab_next_unread: keybind("alt+shift+down", "Switch to next unread tab"),
+  session_tab_previous_unread: keybind("alt+shift+up", "Switch to previous unread tab"),
   session_tab_close: keybind("<leader>w", "Close current tab"),
   session_tab_close: keybind("<leader>w", "Close current tab"),
   session_tab_reopen: keybind("ctrl+shift+t", "Reopen last closed tab"),
   session_tab_reopen: keybind("ctrl+shift+t", "Reopen last closed tab"),
   session_timeline: keybind("<leader>g", "Show session timeline"),
   session_timeline: keybind("<leader>g", "Show session timeline"),
@@ -150,10 +150,10 @@ export const Definitions = {
   messages_half_page_down: keybind("ctrl+alt+d", "Scroll messages down by half page"),
   messages_half_page_down: keybind("ctrl+alt+d", "Scroll messages down by half page"),
   messages_first: keybind("ctrl+g,home,alt+home", "Navigate to first message"),
   messages_first: keybind("ctrl+g,home,alt+home", "Navigate to first message"),
   messages_last: keybind("ctrl+alt+g,end", "Navigate to last message"),
   messages_last: keybind("ctrl+alt+g,end", "Navigate to last message"),
-  messages_next: keybind("alt+down", "Navigate to next message"),
-  messages_previous: keybind("alt+up", "Navigate to previous message"),
-  messages_next_user: keybind("alt+shift+down", "Navigate to next user message"),
-  messages_previous_user: keybind("alt+shift+up", "Navigate to previous user message"),
+  messages_next: keybind("none", "Navigate to next message"),
+  messages_previous: keybind("none", "Navigate to previous message"),
+  messages_next_user: keybind("none", "Navigate to next user message"),
+  messages_previous_user: keybind("none", "Navigate to previous user message"),
   messages_last_user: keybind("alt+end", "Navigate to last user message"),
   messages_last_user: keybind("alt+end", "Navigate to last user message"),
   messages_copy: keybind("<leader>y", "Copy message"),
   messages_copy: keybind("<leader>y", "Copy message"),
   messages_undo: keybind("<leader>u", "Undo message"),
   messages_undo: keybind("<leader>u", "Undo message"),

+ 10 - 2
packages/tui/src/context/session-tabs-model.ts

@@ -82,11 +82,19 @@ export function moveSessionTab(tabs: SessionTab[], sessionID: string, index: num
   return next
   return next
 }
 }
 
 
-export function cycleSessionTab(tabs: readonly SessionTab[], active: string | undefined, direction: 1 | -1) {
+export function cycleSessionTab(
+  tabs: readonly SessionTab[],
+  active: string | undefined,
+  direction: 1 | -1,
+  matches: (tab: SessionTab) => boolean = () => true,
+) {
   if (tabs.length === 0) return
   if (tabs.length === 0) return
   const index = tabs.findIndex((tab) => tab.sessionID === active)
   const index = tabs.findIndex((tab) => tab.sessionID === active)
   const start = index === -1 ? (direction === 1 ? -1 : 0) : index
   const start = index === -1 ? (direction === 1 ? -1 : 0) : index
-  return tabs[(start + direction + tabs.length) % tabs.length]
+  return Array.from(
+    { length: tabs.length },
+    (_, offset) => tabs[(start + direction * (offset + 1) + tabs.length * 2) % tabs.length],
+  ).find(matches)
 }
 }
 
 
 // In-memory navigation history is bounded so a long-lived TUI does not accumulate one entry per
 // In-memory navigation history is bounded so a long-lived TUI does not accumulate one entry per

+ 2 - 4
packages/tui/src/context/session-tabs.tsx

@@ -298,10 +298,8 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
       },
       },
       cycleUnread(direction: 1 | -1) {
       cycleUnread(direction: 1 | -1) {
         if (!enabled()) return
         if (!enabled()) return
-        const tab = cycleSessionTab(
-          state().tabs.filter((tab) => state().unread[tab.sessionID] || status(tab.sessionID).attention),
-          current(),
-          direction,
+        const tab = cycleSessionTab(state().tabs, current(), direction, (tab) =>
+          Boolean(state().unread[tab.sessionID] || status(tab.sessionID).attention),
         )
         )
         if (tab) route.navigate({ type: "session", sessionID: tab.sessionID })
         if (tab) route.navigate({ type: "session", sessionID: tab.sessionID })
       },
       },

+ 9 - 11
packages/tui/test/config.test.tsx

@@ -90,10 +90,10 @@ test("resolves message navigation defaults", () => {
   const config = resolve({}, { terminalSuspend: true })
   const config = resolve({}, { terminalSuspend: true })
 
 
   expect(config.keybinds.get("session.first")).toMatchObject([{ key: "ctrl+g,home,alt+home" }])
   expect(config.keybinds.get("session.first")).toMatchObject([{ key: "ctrl+g,home,alt+home" }])
-  expect(config.keybinds.get("session.message.previous")).toMatchObject([{ key: "alt+up" }])
-  expect(config.keybinds.get("session.message.next")).toMatchObject([{ key: "alt+down" }])
-  expect(config.keybinds.get("session.message.user.previous")).toMatchObject([{ key: "alt+shift+up" }])
-  expect(config.keybinds.get("session.message.user.next")).toMatchObject([{ key: "alt+shift+down" }])
+  expect(config.keybinds.get("session.message.previous")).toEqual([])
+  expect(config.keybinds.get("session.message.next")).toEqual([])
+  expect(config.keybinds.get("session.message.user.previous")).toEqual([])
+  expect(config.keybinds.get("session.message.user.next")).toEqual([])
   expect(config.keybinds.get("session.messages_last_user")).toMatchObject([{ key: "alt+end" }])
   expect(config.keybinds.get("session.messages_last_user")).toMatchObject([{ key: "alt+end" }])
 })
 })
 
 
@@ -116,15 +116,13 @@ test("opens the subagent picker with down", () => {
   expect(config.keybinds.get("session.child.first")).toMatchObject([{ key: "down" }])
   expect(config.keybinds.get("session.child.first")).toMatchObject([{ key: "down" }])
 })
 })
 
 
-test("navigates session tabs with leader arrows", () => {
+test("navigates session tabs with option arrows", () => {
   const config = resolve({}, { terminalSuspend: true })
   const config = resolve({}, { terminalSuspend: true })
 
 
-  expect(config.keybinds.get("session.tab.next")).toMatchObject([{ key: "ctrl+tab,<leader>right,alt+shift+]" }])
-  expect(config.keybinds.get("session.tab.previous")).toMatchObject([
-    { key: "ctrl+shift+tab,<leader>left,alt+shift+[" },
-  ])
-  expect(config.keybinds.get("session.tab.next_unread")).toMatchObject([{ key: "<leader>down" }])
-  expect(config.keybinds.get("session.tab.previous_unread")).toMatchObject([{ key: "<leader>up" }])
+  expect(config.keybinds.get("session.tab.next")).toMatchObject([{ key: "ctrl+tab,alt+down" }])
+  expect(config.keybinds.get("session.tab.previous")).toMatchObject([{ key: "ctrl+shift+tab,alt+up" }])
+  expect(config.keybinds.get("session.tab.next_unread")).toMatchObject([{ key: "alt+shift+down" }])
+  expect(config.keybinds.get("session.tab.previous_unread")).toMatchObject([{ key: "alt+shift+up" }])
 })
 })
 
 
 test("preserves pinned session bindings alongside tab bindings", () => {
 test("preserves pinned session bindings alongside tab bindings", () => {

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

@@ -93,6 +93,17 @@ describe("session tabs", () => {
     expect(cycleSessionTab(tabs, "b", 1)?.sessionID).toBe("a")
     expect(cycleSessionTab(tabs, "b", 1)?.sessionID).toBe("a")
   })
   })
 
 
+  test("cycles to the nearest matching tab from an unmatched active tab", () => {
+    const tabs = ["a", "b", "c", "d", "e"].map((sessionID) => ({ sessionID }))
+    const unread = new Set(["a", "d"])
+    const matches = (tab: { sessionID: string }) => unread.has(tab.sessionID)
+
+    expect(cycleSessionTab(tabs, "c", 1, matches)?.sessionID).toBe("d")
+    expect(cycleSessionTab(tabs, "c", -1, matches)?.sessionID).toBe("a")
+    expect(cycleSessionTab(tabs, "e", 1, matches)?.sessionID).toBe("a")
+    expect(cycleSessionTab(tabs, "a", -1, matches)?.sessionID).toBe("d")
+  })
+
   test("moves backward and forward through selection history", () => {
   test("moves backward and forward through selection history", () => {
     const tabs = ["a", "b", "c", "d"].map((sessionID) => ({ sessionID }))
     const tabs = ["a", "b", "c", "d"].map((sessionID) => ({ sessionID }))
     const history = ["a", "b", "c", "d"].reduce(recordSessionTabHistory, { entries: [], index: -1 })
     const history = ["a", "b", "c", "d"].reduce(recordSessionTabHistory, { entries: [], index: -1 })