mirror of
https://github.com/stablyai/orca.git
synced 2026-10-29 00:03:37 +00:00
Restore previously-active tab when closing a browser tab
Closing a browser tab now activates the most-recently-used (MRU) tab instead of the visual neighbor. This provides more predictable navigation behavior when switching between tabs.
This commit is contained in:
@@ -1774,9 +1774,12 @@ function Terminal(): React.JSX.Element | null {
|
||||
}
|
||||
const currentTabs = state.browserTabsByWorktree[owningWorktreeId] ?? []
|
||||
if (currentTabs.length <= 1) {
|
||||
const hasUnifiedEntry = Object.values(state.unifiedTabsByWorktree).some((tabs) =>
|
||||
tabs.some((tab) => tab.contentType === 'browser' && tab.entityId === tabId)
|
||||
)
|
||||
destroyWorkspaceWebviews(state.browserPagesByWorkspace, tabId)
|
||||
closeBrowserTab(tabId)
|
||||
if (state.activeWorktreeId === owningWorktreeId) {
|
||||
if (!hasUnifiedEntry && state.activeWorktreeId === owningWorktreeId) {
|
||||
const worktreeFile = state.openFiles.find((file) => file.worktreeId === owningWorktreeId)
|
||||
if (worktreeFile) {
|
||||
setActiveFile(worktreeFile.id)
|
||||
@@ -1793,24 +1796,10 @@ function Terminal(): React.JSX.Element | null {
|
||||
}
|
||||
return
|
||||
}
|
||||
if (state.activeWorktreeId === owningWorktreeId && tabId === state.activeBrowserTabId) {
|
||||
const idx = currentTabs.findIndex((tab) => tab.id === tabId)
|
||||
const nextTab = currentTabs[idx + 1] ?? currentTabs[idx - 1]
|
||||
if (nextTab) {
|
||||
setActiveBrowserTab(nextTab.id)
|
||||
}
|
||||
}
|
||||
destroyWorkspaceWebviews(state.browserPagesByWorkspace, tabId)
|
||||
closeBrowserTab(tabId)
|
||||
},
|
||||
[
|
||||
closeBrowserTab,
|
||||
setActiveBrowserTab,
|
||||
setActiveFile,
|
||||
setActiveTab,
|
||||
setActiveTabType,
|
||||
setActiveWorktree
|
||||
]
|
||||
[closeBrowserTab, setActiveFile, setActiveTab, setActiveTabType, setActiveWorktree]
|
||||
)
|
||||
|
||||
const handlePtyExit = useCallback(
|
||||
|
||||
@@ -23,13 +23,13 @@ import {
|
||||
normalizeBrowserHistoryEntries,
|
||||
normalizeBrowserHistoryUrl
|
||||
} from '../../../../shared/workspace-session-browser-history'
|
||||
import { pickNeighbor } from './tab-group-state'
|
||||
import { destroyWorkspaceWebviews } from './browser-webview-cleanup'
|
||||
import {
|
||||
getRecentlyClosedTabPosition,
|
||||
restoreRecentlyClosedTabPosition,
|
||||
pushRecentlyClosedTabKind
|
||||
} from './recently-closed-tabs'
|
||||
import { pickNeighbor } from './tab-group-state'
|
||||
import type { RecentlyClosedTabPosition } from './recently-closed-tabs'
|
||||
import { callRuntimeRpc, type RuntimeClientTarget } from '@/runtime/runtime-rpc-client'
|
||||
import { toRuntimeWorktreeSelector } from '@/runtime/runtime-worktree-selector'
|
||||
@@ -846,13 +846,14 @@ export const createBrowserSlice: StateCreator<AppState, [], [], BrowserSlice> =
|
||||
delete nextRemoteBrowserPageHandlesByPageId[page.id]
|
||||
}
|
||||
|
||||
const nextActiveBrowserTabIdByWorktree = { ...s.activeBrowserTabIdByWorktree }
|
||||
const remainingBrowserTabs = nextBrowserTabsByWorktree[owningWorktreeId] ?? []
|
||||
const tabBarOrder = s.tabBarOrderByWorktree[owningWorktreeId] ?? []
|
||||
const neighborTabId = pickNeighbor(tabBarOrder, tabId)
|
||||
const nextActiveBrowserTabIdByWorktree = { ...s.activeBrowserTabIdByWorktree }
|
||||
if (nextActiveBrowserTabIdByWorktree[owningWorktreeId] === tabId) {
|
||||
const neighborId = pickNeighbor(s.tabBarOrderByWorktree[owningWorktreeId] ?? [], tabId)
|
||||
nextActiveBrowserTabIdByWorktree[owningWorktreeId] =
|
||||
neighborTabId ?? remainingBrowserTabs[0]?.id ?? null
|
||||
(neighborId && remainingBrowserTabs.some((tab) => tab.id === neighborId)
|
||||
? neighborId
|
||||
: remainingBrowserTabs[0]?.id) ?? null
|
||||
}
|
||||
|
||||
const nextTabBarOrder = {
|
||||
@@ -916,7 +917,7 @@ export const createBrowserSlice: StateCreator<AppState, [], [], BrowserSlice> =
|
||||
browserPagesByWorkspace: nextBrowserPagesByWorkspace,
|
||||
activeBrowserTabId:
|
||||
s.activeBrowserTabId === tabId
|
||||
? (neighborTabId ?? remainingBrowserTabs[0]?.id ?? null)
|
||||
? (nextActiveBrowserTabIdByWorktree[owningWorktreeId] ?? null)
|
||||
: s.activeBrowserTabId,
|
||||
activeBrowserTabIdByWorktree: nextActiveBrowserTabIdByWorktree,
|
||||
tabBarOrderByWorktree: nextTabBarOrder,
|
||||
|
||||
@@ -424,6 +424,108 @@ describe('setActiveWorktree', () => {
|
||||
expect(s.activeBrowserTabIdByWorktree[backgroundWt]).toBe(browserTab.id)
|
||||
})
|
||||
|
||||
it('uses unified MRU selection when closing an active browser tab', () => {
|
||||
const store = createTestStore()
|
||||
const wt = 'repo1::/path/wt1'
|
||||
|
||||
seedStore(store, {
|
||||
worktreesByRepo: {
|
||||
repo1: [makeWorktree({ id: wt, repoId: 'repo1', path: '/path/wt1' })]
|
||||
},
|
||||
activeWorktreeId: wt
|
||||
})
|
||||
|
||||
const previous = store.getState().createBrowserTab(wt, 'https://previous.example.com')
|
||||
store.getState().createBrowserTab(wt, 'https://neighbor.example.com')
|
||||
const closing = store.getState().createBrowserTab(wt, 'https://closing.example.com')
|
||||
store.getState().setActiveBrowserTab(previous.id)
|
||||
store.getState().setActiveBrowserTab(closing.id)
|
||||
|
||||
store.getState().closeBrowserTab(closing.id)
|
||||
|
||||
expect(store.getState().activeBrowserTabId).toBe(previous.id)
|
||||
expect(store.getState().activeBrowserTabIdByWorktree[wt]).toBe(previous.id)
|
||||
})
|
||||
|
||||
it('keeps the unified MRU target when closing the last browser tab', () => {
|
||||
const store = createTestStore()
|
||||
const wt = 'repo1::/path/wt1'
|
||||
|
||||
seedStore(store, {
|
||||
worktreesByRepo: {
|
||||
repo1: [makeWorktree({ id: wt, repoId: 'repo1', path: '/path/wt1' })]
|
||||
},
|
||||
activeWorktreeId: wt
|
||||
})
|
||||
|
||||
store.getState().createTab(wt)
|
||||
const previous = store.getState().createTab(wt)
|
||||
const closing = store.getState().createBrowserTab(wt, 'https://closing.example.com')
|
||||
|
||||
store.getState().closeBrowserTab(closing.id)
|
||||
|
||||
expect(store.getState().activeTabId).toBe(previous.id)
|
||||
expect(store.getState().activeTabType).toBe('terminal')
|
||||
})
|
||||
|
||||
it('keeps a valid browser target for an inactive worktree after close', () => {
|
||||
const store = createTestStore()
|
||||
const activeWt = 'repo1::/path/active'
|
||||
const backgroundWt = 'repo1::/path/background'
|
||||
|
||||
seedStore(store, {
|
||||
worktreesByRepo: {
|
||||
repo1: [
|
||||
makeWorktree({ id: activeWt, repoId: 'repo1', path: '/path/active' }),
|
||||
makeWorktree({ id: backgroundWt, repoId: 'repo1', path: '/path/background' })
|
||||
]
|
||||
},
|
||||
activeWorktreeId: activeWt
|
||||
})
|
||||
|
||||
const previous = store
|
||||
.getState()
|
||||
.createBrowserTab(backgroundWt, 'https://previous.example.com', {
|
||||
activate: false
|
||||
})
|
||||
const closing = store.getState().createBrowserTab(backgroundWt, 'https://closing.example.com', {
|
||||
activate: false
|
||||
})
|
||||
store.setState({ activeBrowserTabIdByWorktree: { [backgroundWt]: closing.id } })
|
||||
|
||||
store.getState().closeBrowserTab(closing.id)
|
||||
|
||||
expect(store.getState().activeBrowserTabIdByWorktree[backgroundWt]).toBe(previous.id)
|
||||
})
|
||||
|
||||
it('keeps the global browser target when closing a legacy tab without a unified wrapper', () => {
|
||||
const store = createTestStore()
|
||||
const wt = 'repo1::/path/wt1'
|
||||
|
||||
seedStore(store, {
|
||||
worktreesByRepo: {
|
||||
repo1: [makeWorktree({ id: wt, repoId: 'repo1', path: '/path/wt1' })]
|
||||
},
|
||||
activeWorktreeId: wt
|
||||
})
|
||||
|
||||
const previous = store.getState().createBrowserTab(wt, 'https://previous.example.com', {
|
||||
activate: false
|
||||
})
|
||||
const closing = store.getState().createBrowserTab(wt, 'https://closing.example.com', {
|
||||
activate: false
|
||||
})
|
||||
store.setState({
|
||||
unifiedTabsByWorktree: {},
|
||||
activeBrowserTabId: closing.id,
|
||||
activeBrowserTabIdByWorktree: { [wt]: closing.id }
|
||||
})
|
||||
|
||||
store.getState().closeBrowserTab(closing.id)
|
||||
|
||||
expect(store.getState().activeBrowserTabId).toBe(previous.id)
|
||||
})
|
||||
|
||||
it('queues and consumes a one-shot address-bar focus request for a fresh blank browser tab', () => {
|
||||
const store = createTestStore()
|
||||
const wt = 'repo1::/path/wt1'
|
||||
|
||||
@@ -347,12 +347,12 @@ describe('TabsSlice', () => {
|
||||
expect(buildMobileSessionTabSnapshots(store.getState())[0]?.tabs ?? []).toEqual([])
|
||||
})
|
||||
|
||||
it('activates the previously-active tab (MRU) instead of the visual neighbor', () => {
|
||||
it('activates the previously-active tab across content types', () => {
|
||||
const t1 = store.getState().createUnifiedTab(WT, 'terminal')
|
||||
const t2 = store.getState().createUnifiedTab(WT, 'terminal')
|
||||
const t3 = store.getState().createUnifiedTab(WT, 'terminal')
|
||||
const t2 = store.getState().createUnifiedTab(WT, 'browser')
|
||||
const t3 = store.getState().createUnifiedTab(WT, 'editor')
|
||||
|
||||
// Visit order ...→t3→t1→t3; closing t3 should jump to t1 (MRU previous), not the visual neighbor t2.
|
||||
// Visit order ...→t3→t1→t3; closing t3 should jump to t1, not browser neighbor t2.
|
||||
store.getState().activateTab(t1.id)
|
||||
store.getState().activateTab(t3.id)
|
||||
store.getState().closeUnifiedTab(t3.id)
|
||||
|
||||
Reference in New Issue
Block a user