Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 2 additions & 20 deletions packages/app/src/components/titlebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -419,33 +419,15 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
title: "",
keybind: `mod+option+ArrowLeft,ctrl+shift+tab`,
hidden: true,
onSelect: () => {
let index = tabsStore.findIndex((tab) => tab === currentTab())
if (index === -1) return

index -= 1
if (index === -1) index = tabsStore.length - 1

const next = tabsStore[index]
if (next) tabs.select(next)
},
onSelect: tabs.previous,
},
{
id: `tab.next`,
category: "tab",
title: "",
keybind: `mod+option+ArrowRight,ctrl+tab`,
hidden: true,
onSelect: () => {
let index = tabsStore.findIndex((tab) => tab === currentTab())
if (index === -1) return

index += 1
if (index === tabsStore.length) index = 0

const next = tabsStore[index]
if (next) tabs.select(next)
},
onSelect: tabs.next,
},
].filter((v) => v !== undefined)
})
Expand Down
43 changes: 43 additions & 0 deletions packages/app/src/context/tab-history.test.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 28 additions & 0 deletions packages/app/src/context/tab-history.ts
Original file line number Diff line number Diff line change
@@ -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 }
}
}
16 changes: 16 additions & 0 deletions packages/app/src/context/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { sessionHref } from "@/utils/session-route"
import { createTabMemory } from "./tab-memory"
import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs"
import { createDraftPromptSession, type PromptModel } from "./prompt-state"
import { nextTab, previousTab, rememberTab, type TabHistory } from "./tab-history"

export type SessionTab = {
type: "session"
Expand Down Expand Up @@ -79,6 +80,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

Expand Down Expand Up @@ -150,10 +152,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
Expand Down Expand Up @@ -359,8 +372,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())
Expand Down
11 changes: 11 additions & 0 deletions packages/app/src/desktop-menu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ 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.label === "Previous Tab" || item.label === "Next Tab"),
)

expect(items).toEqual([
{ type: "item", label: "Previous Tab", command: "tab.prev", accelerator: { macos: "Option+Up" } },
{ type: "item", label: "Next Tab", command: "tab.next", accelerator: { macos: "Option+Down" } },
])
})

test("exports logs through the desktop command registry", () => {
const items = DESKTOP_MENU.flatMap((menu) => menu.items ?? []).filter(
(item) => item.type === "item" && item.label === "Export Logs...",
Expand Down
4 changes: 2 additions & 2 deletions packages/app/src/desktop-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ export const DESKTOP_MENU: DesktopMenu[] = [
{ type: "item", label: "Back", command: "common.goBack", accelerator: { macos: "Cmd+[" } },
{ type: "item", label: "Forward", command: "common.goForward", accelerator: { macos: "Cmd+]" } },
{ type: "separator" },
{ type: "item", label: "Previous Session", command: "session.previous", accelerator: { macos: "Option+Up" } },
{ type: "item", label: "Next Session", command: "session.next", accelerator: { macos: "Option+Down" } },
{ type: "item", label: "Previous Tab", command: "tab.prev", accelerator: { macos: "Option+Up" } },
{ type: "item", label: "Next Tab", command: "tab.next", accelerator: { macos: "Option+Down" } },
{ type: "separator" },
{
type: "item",
Expand Down
Loading