mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-08 12:03:19 +00:00
Apply PR #37927: fix(app): navigate tabs by selection history
This commit is contained in:
commit
296247bacb
6 changed files with 137 additions and 0 deletions
|
|
@ -353,6 +353,22 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
|||
keybind: "mod+shift+t",
|
||||
onSelect: () => tabsStoreActions.reopenClosedTab(),
|
||||
},
|
||||
{
|
||||
id: `tab.prev`,
|
||||
category: "tab",
|
||||
title: "",
|
||||
keybind: `mod+option+ArrowLeft,ctrl+shift+tab`,
|
||||
hidden: true,
|
||||
onSelect: tabs.previous,
|
||||
},
|
||||
{
|
||||
id: `tab.next`,
|
||||
category: "tab",
|
||||
title: "",
|
||||
keybind: `mod+option+ArrowRight,ctrl+tab`,
|
||||
hidden: true,
|
||||
onSelect: tabs.next,
|
||||
},
|
||||
].filter((v) => v !== undefined)
|
||||
})
|
||||
|
||||
|
|
|
|||
43
packages/app/src/context/tab-history.test.ts
Normal file
43
packages/app/src/context/tab-history.test.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { nextTab, previousTab, rememberTab, type TabHistory } from "./tab-history"
|
||||
|
||||
function history(): TabHistory {
|
||||
return { stack: [], index: -1 }
|
||||
}
|
||||
|
||||
describe("tab history", () => {
|
||||
test("moves backward and forward through selected tabs", () => {
|
||||
const selected = ["a", "b", "c"].reduce(rememberTab, history())
|
||||
const available = new Set(selected.stack)
|
||||
|
||||
const previous = previousTab(selected, available)
|
||||
expect(previous?.key).toBe("b")
|
||||
|
||||
const first = previousTab(previous!.state, available)
|
||||
expect(first?.key).toBe("a")
|
||||
|
||||
const next = nextTab(first!.state, available)
|
||||
expect(next?.key).toBe("b")
|
||||
})
|
||||
|
||||
test("replaces forward history after a new selection", () => {
|
||||
const selected = ["a", "b", "c"].reduce(rememberTab, history())
|
||||
const previous = previousTab(selected, new Set(selected.stack))
|
||||
const next = rememberTab(previous!.state, "d")
|
||||
|
||||
expect(next).toEqual({ stack: ["a", "b", "d"], index: 2 })
|
||||
expect(nextTab(next, new Set(next.stack))).toBeUndefined()
|
||||
})
|
||||
|
||||
test("skips tabs that are no longer open", () => {
|
||||
const selected = ["a", "b", "c"].reduce(rememberTab, history())
|
||||
|
||||
expect(previousTab(selected, new Set(["a", "c"]))?.key).toBe("a")
|
||||
})
|
||||
|
||||
test("skips a repeated current tab after closing the previous selection", () => {
|
||||
const selected = ["a", "b", "c", "b"].reduce(rememberTab, history())
|
||||
|
||||
expect(previousTab(selected, new Set(["a", "b"]))?.key).toBe("a")
|
||||
})
|
||||
})
|
||||
28
packages/app/src/context/tab-history.ts
Normal file
28
packages/app/src/context/tab-history.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
const MAX_TAB_HISTORY = 100
|
||||
|
||||
export type TabHistory = {
|
||||
stack: string[]
|
||||
index: number
|
||||
}
|
||||
|
||||
export function rememberTab(state: TabHistory, key: string): TabHistory {
|
||||
if (state.stack[state.index] === key) return state
|
||||
const stack = state.stack.slice(0, state.index + 1).concat(key).slice(-MAX_TAB_HISTORY)
|
||||
return { stack, index: stack.length - 1 }
|
||||
}
|
||||
|
||||
export function previousTab(state: TabHistory, available: Set<string>) {
|
||||
return move(state, -1, available)
|
||||
}
|
||||
|
||||
export function nextTab(state: TabHistory, available: Set<string>) {
|
||||
return move(state, 1, available)
|
||||
}
|
||||
|
||||
function move(state: TabHistory, offset: -1 | 1, available: Set<string>) {
|
||||
const current = state.stack[state.index]
|
||||
for (let index = state.index + offset; index >= 0 && index < state.stack.length; index += offset) {
|
||||
const key = state.stack[index]
|
||||
if (key && key !== current && available.has(key)) return { state: { ...state, index }, key }
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import { createTabMemory } from "./tab-memory"
|
|||
import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs"
|
||||
import { createDraftPromptSession, type PromptModel } from "./prompt-state"
|
||||
import { migrateTabs } from "./tab-migration"
|
||||
import { nextTab, previousTab, rememberTab, type TabHistory } from "./tab-history"
|
||||
|
||||
export type SessionTab = {
|
||||
type: "session"
|
||||
|
|
@ -74,6 +75,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||
const memory = createTabMemory(getOwner())
|
||||
|
||||
const closing = new Set<string>()
|
||||
let history: TabHistory = { stack: [], index: -1 }
|
||||
let recentWrite = 0
|
||||
let recentValue: string | undefined
|
||||
|
||||
|
|
@ -148,10 +150,21 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||
|
||||
const navigateTab = (tab: Tab) => {
|
||||
const href = tabHref(tab)
|
||||
history = rememberTab(history, tabKey(tab))
|
||||
setRecentKey(tabKey(tab))
|
||||
navigate(href)
|
||||
}
|
||||
|
||||
const moveHistory = (direction: "previous" | "next") => {
|
||||
const available = new Set(store.map(tabKey))
|
||||
const result = direction === "previous" ? previousTab(history, available) : nextTab(history, available)
|
||||
if (!result) return
|
||||
const tab = store.find((item) => tabKey(item) === result.key)
|
||||
if (!tab) return
|
||||
history = result.state
|
||||
navigateTab(tab)
|
||||
}
|
||||
|
||||
const removeTab = (index: number) => {
|
||||
const tab = store[index]
|
||||
if (!tab) return
|
||||
|
|
@ -357,8 +370,11 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||
select: navigateTab,
|
||||
remember(tab: Tab) {
|
||||
const key = tabKey(tab)
|
||||
history = rememberTab(history, key)
|
||||
if (recentKey() !== key) setRecentKey(key)
|
||||
},
|
||||
previous: () => moveHistory("previous"),
|
||||
next: () => moveHistory("next"),
|
||||
toggleHome(input: { home: boolean; current?: Tab }) {
|
||||
if (input.home) {
|
||||
const tab = store.find((tab) => tabKey(tab) === recentKey())
|
||||
|
|
|
|||
|
|
@ -2,6 +2,27 @@ import { describe, expect, test } from "bun:test"
|
|||
import { DESKTOP_MENU } from "./desktop-menu"
|
||||
|
||||
describe("desktop menu", () => {
|
||||
test("navigates between tabs", () => {
|
||||
const items = DESKTOP_MENU.flatMap((menu) => menu.items ?? []).filter(
|
||||
(item) => item.type === "item" && (item.command === "tab.prev" || item.command === "tab.next"),
|
||||
)
|
||||
|
||||
expect(items).toEqual([
|
||||
{
|
||||
type: "item",
|
||||
labelKey: "desktop.menu.previousSession",
|
||||
command: "tab.prev",
|
||||
accelerator: { macos: "Option+Left" },
|
||||
},
|
||||
{
|
||||
type: "item",
|
||||
labelKey: "desktop.menu.nextSession",
|
||||
command: "tab.next",
|
||||
accelerator: { macos: "Option+Right" },
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("exports logs through the desktop command registry", () => {
|
||||
const items = DESKTOP_MENU.flatMap((menu) => menu.items ?? []).filter(
|
||||
(item) => item.type === "item" && item.labelKey === "desktop.menu.exportLogs",
|
||||
|
|
|
|||
|
|
@ -237,6 +237,19 @@ export const DESKTOP_MENU: DesktopMenu[] = [
|
|||
{ type: "item", labelKey: "desktop.menu.back", command: "common.goBack", accelerator: { macos: "Cmd+[" } },
|
||||
{ type: "item", labelKey: "desktop.menu.forward", command: "common.goForward", accelerator: { macos: "Cmd+]" } },
|
||||
{ type: "separator" },
|
||||
{
|
||||
type: "item",
|
||||
labelKey: "desktop.menu.previousSession",
|
||||
command: "tab.prev",
|
||||
accelerator: { macos: "Option+Left" },
|
||||
},
|
||||
{
|
||||
type: "item",
|
||||
labelKey: "desktop.menu.nextSession",
|
||||
command: "tab.next",
|
||||
accelerator: { macos: "Option+Right" },
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
type: "item",
|
||||
labelKey: "desktop.menu.previousSession",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue