mirror of
https://github.com/stablyai/orca.git
synced 2026-09-27 00:02:37 +00:00
fix(floating-workspace): keep agent launches from moving the main window's tab (#22603)
* fix(floating-workspace): keep agent launches from moving the main window's tab
Launching an agent from the floating workspace's "+" menu switched the main
window off whatever chat or editor tab it was showing and onto its terminals.
The main window's selection is supposed to move only for the worktree it is
showing: browser and editor tab creation, splits, moves and drops all check
`activeWorktreeId === worktreeId` before touching it. Two places did not:
- `launchAgentInNewTab` called `setActiveTabType('terminal')` without a
worktree, which targets the active worktree whatever worktree the launch
landed in.
- terminal `createTab` wrote the global `activeTabId` for a tab in any
worktree.
Both now follow the store rule. The launch still selects its tab within its
own worktree, which is what the floating panel renders.
The floating titlebar button had side-stepped this with an `activate: false`
opt-out plus manual selection. That opt-out had no other caller and is removed;
the button now launches and focuses like every other entry point.
* fix(tabs): scope the remaining launch surface writes to the launch's worktree
Three more launch paths create a terminal tab and then call
`setActiveTabType('terminal')` without a worktree, which targets whatever
worktree is active when the call runs rather than the one the tab landed in:
- the paired-host agent launch, after the host's asynchronous create
- Session History resume, which can target a worktree the user is not viewing
and activates it only afterwards
- sleeping-agent resume, which the activation gate runs after asynchronous
readiness checks, by which time the user may have moved to another worktree
Each now names its worktree, like the local agent launch. The new tab still
lands selected when the user switches to that worktree.
* test(tabs): pin the paired-host launch scope in its existing web-runtime test
* test(tabs): type the left-worktree resume fixture instead of casting it
* refactor(tabs): require the worktree that setActiveTabType applies to
`setActiveTabType(type, worktreeId?)` quietly fell back to the active
worktree when the caller left the worktree out. A caller acting on a tab in
another worktree (the floating workspace, a background launch, a reveal that
lands after an async step) therefore retyped whatever the main window was
showing. The launch paths fixed earlier in this branch were instances of that;
54 other callers still relied on the fallback.
The worktree is now a required argument (nullable only for the no-active-
worktree case), so every caller states which worktree it means and a new
unscoped call fails to compile. Each call site passes the worktree of the tab
it acts on; where that is by construction the active worktree (shortcuts,
palette, tab strip), the result is unchanged. `activateTabAndFocusPane`
resolves the tab's owning worktree the same way `setActiveTab` does.
End-to-end helpers that drive the store directly pass the active worktree,
which keeps their previous behaviour.
* fix(floating-workspace): let the floating New Terminal activate its own tab
The floating "+" New Terminal created its tab with `activate: false` and then
selected it with `activateTab`, because creating an active tab used to write
the main window's selected tab even for another worktree. `createTab` now
activates a tab only within its own worktree's group unless that worktree is
the one on screen, so the workaround is no longer needed.
Creating the tab active also moves the floating workspace's remembered tab to
the new one; before, it stayed on the previously selected floating tab, which
auto-acknowledge reads to decide which floating agent the user is looking at.
* refactor(floating-workspace): route every floating New Terminal through one creator
The floating "+" New Terminal had stopped deferring activation, but Cmd+T with the
floating panel focused still went through a separate creator that created the tab
inactive and activated it by hand, which left the floating workspace's remembered tab
on the previous tab. Both now call createFloatingWorkspaceTerminalTab, which creates
the tab active in its own group and focuses it.
* docs(tabs): say why an unowned tab id keeps the on-screen worktree scope
This commit is contained in:
@@ -176,7 +176,7 @@ export async function setupTerminal(page, repoPath, logPhase) {
|
||||
})
|
||||
state = store.getState()
|
||||
state.setActiveTab(tab.id)
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', worktree.id)
|
||||
state = store.getState()
|
||||
if (
|
||||
state.activeWorktreeId !== worktree.id ||
|
||||
|
||||
@@ -97,11 +97,11 @@ export function TerminalTitlebarTabs({
|
||||
const unifiedTab = unifiedTabs.find((tab) => tab.id === fileId)
|
||||
if (unifiedTab?.contentType === 'simulator') {
|
||||
setActiveTab(fileId)
|
||||
setActiveTabType('simulator')
|
||||
setActiveTabType('simulator', renderedActiveWorktreeId)
|
||||
return
|
||||
}
|
||||
setActiveFile(fileId)
|
||||
setActiveTabType('editor')
|
||||
setActiveTabType('editor', renderedActiveWorktreeId)
|
||||
}}
|
||||
onCloseFile={handleCloseFile}
|
||||
onActivateBrowserTab={handleActivateBrowserTab}
|
||||
|
||||
@@ -105,7 +105,7 @@ export function createActivityThreadActions({
|
||||
// no pane to focus and focusing a sibling would be worse than focusing nothing.
|
||||
return
|
||||
}
|
||||
activated.setActiveTabType('terminal')
|
||||
activated.setActiveTabType('terminal', thread.worktree.id)
|
||||
const parsed = parsePaneKey(thread.paneKey)
|
||||
activateTabAndFocusPane(
|
||||
thread.tab.id,
|
||||
|
||||
@@ -51,7 +51,7 @@ export function createAutomationRunWorkspaceAction({ store, list }: AutomationsP
|
||||
)
|
||||
if (activateAndRevealWorktree(run.workspaceId)) {
|
||||
appStore.setActiveTab(terminalTarget.tabId)
|
||||
appStore.setActiveTabType('terminal')
|
||||
appStore.setActiveTabType('terminal', run.workspaceId)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ export function navigateBrowserPageToUrl({
|
||||
}
|
||||
|
||||
// Why: file:// notebooks in the browser are otherwise rendered as raw JSON by Chromium.
|
||||
store.setActiveTabType('editor')
|
||||
store.setActiveTabType('editor', worktreeId)
|
||||
store.openFile(
|
||||
{
|
||||
filePath: notebookPath,
|
||||
|
||||
+6
-6
@@ -202,10 +202,10 @@ describe('FloatingTerminalPanel close behavior', () => {
|
||||
expect(mocks.createTab).toHaveBeenCalledWith(
|
||||
FLOATING_TERMINAL_WORKTREE_ID,
|
||||
'floating-group',
|
||||
undefined,
|
||||
{ activate: false }
|
||||
undefined
|
||||
)
|
||||
expect(mocks.activateTab).toHaveBeenCalledWith('created-tab')
|
||||
// Why: createTab itself activates the new tab within the floating group.
|
||||
expect(mocks.activateTab).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('routes titlebar Cmd+Shift+O to the floating markdown picker', async () => {
|
||||
@@ -299,10 +299,10 @@ describe('FloatingTerminalPanel close behavior', () => {
|
||||
expect(mocks.createTab).toHaveBeenCalledWith(
|
||||
FLOATING_TERMINAL_WORKTREE_ID,
|
||||
'floating-group',
|
||||
undefined,
|
||||
{ activate: false }
|
||||
undefined
|
||||
)
|
||||
expect(mocks.activateTab).toHaveBeenCalledWith('created-tab')
|
||||
// Why: createTab itself activates the new tab within the floating group.
|
||||
expect(mocks.activateTab).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('resets focused floating terminal double-tap detection on window blur', async () => {
|
||||
|
||||
+7
-7
@@ -164,7 +164,7 @@ describe('FloatingTerminalPanel close behavior', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
it('creates new floating terminal tabs without globally activating createTab', async () => {
|
||||
it('creates new floating terminal tabs active in the floating group', async () => {
|
||||
setFloatingTabs([makeTab({ id: 'tab-1' })])
|
||||
|
||||
const element = await renderPanel(true)
|
||||
@@ -175,10 +175,10 @@ describe('FloatingTerminalPanel close behavior', () => {
|
||||
expect(mocks.createTab).toHaveBeenCalledWith(
|
||||
FLOATING_TERMINAL_WORKTREE_ID,
|
||||
'floating-group',
|
||||
undefined,
|
||||
{ activate: false }
|
||||
undefined
|
||||
)
|
||||
expect(mocks.activateTab).toHaveBeenCalledWith('created-tab')
|
||||
// Why: createTab itself activates the new tab within the floating group.
|
||||
expect(mocks.activateTab).not.toHaveBeenCalled()
|
||||
expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith('created-tab')
|
||||
})
|
||||
|
||||
@@ -439,10 +439,10 @@ describe('FloatingTerminalPanel close behavior', () => {
|
||||
expect(mocks.createTab).toHaveBeenCalledWith(
|
||||
FLOATING_TERMINAL_WORKTREE_ID,
|
||||
'floating-group',
|
||||
undefined,
|
||||
{ activate: false }
|
||||
undefined
|
||||
)
|
||||
expect(mocks.activateTab).toHaveBeenCalledWith('created-tab')
|
||||
// Why: createTab itself activates the new tab within the floating group.
|
||||
expect(mocks.activateTab).not.toHaveBeenCalled()
|
||||
expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith('created-tab')
|
||||
|
||||
;(tabBar.props.onClose as (tabId: string) => void)('tab-1')
|
||||
|
||||
+3
-17
@@ -15,9 +15,7 @@ const storeBox = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
activateTab: vi.fn(),
|
||||
createTab: vi.fn(),
|
||||
setActiveTabForWorktree: vi.fn(),
|
||||
setTabBarOrder: vi.fn(),
|
||||
queueTabStartupCommand: vi.fn(),
|
||||
focusTerminalTabSurface: vi.fn(),
|
||||
@@ -151,8 +149,6 @@ beforeEach(() => {
|
||||
agentDefaultEnv: {}
|
||||
},
|
||||
createTab: mocks.createTab,
|
||||
activateTab: mocks.activateTab,
|
||||
setActiveTabForWorktree: mocks.setActiveTabForWorktree,
|
||||
setTabBarOrder: mocks.setTabBarOrder,
|
||||
queueTabStartupCommand: mocks.queueTabStartupCommand,
|
||||
tabsByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: [{ id: EXISTING_TAB_ID }] },
|
||||
@@ -180,8 +176,7 @@ describe('FloatingTerminalWindowControls default-agent launch', () => {
|
||||
expect(mocks.launchAgentInNewTab).toHaveBeenCalledExactlyOnceWith({
|
||||
agent: 'claude',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
launchSource: 'shortcut',
|
||||
activate: false
|
||||
launchSource: 'shortcut'
|
||||
})
|
||||
// Why: the whole point of the migration. The shared launcher owns the startup plan and the
|
||||
// tab it lands in, so this button must not reach past it into the tab store.
|
||||
@@ -190,18 +185,9 @@ describe('FloatingTerminalWindowControls default-agent launch', () => {
|
||||
expect(mocks.setTabBarOrder).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('activates the launched terminal tab so the floating panel selects and focuses it', () => {
|
||||
it('focuses the launched terminal tab', () => {
|
||||
clickLaunch()
|
||||
|
||||
// Why: the floating panel renders its visible tab from the unified group's
|
||||
// activeTabId, which only activateTab writes. setActiveTabForWorktree updates
|
||||
// the complementary legacy per-worktree map. Without activateTab the new agent
|
||||
// tab would be appended but never selected/focused.
|
||||
expect(mocks.setActiveTabForWorktree).toHaveBeenCalledWith(
|
||||
FLOATING_TERMINAL_WORKTREE_ID,
|
||||
NEW_AGENT_TAB_ID
|
||||
)
|
||||
expect(mocks.activateTab).toHaveBeenCalledWith(NEW_AGENT_TAB_ID)
|
||||
expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith(NEW_AGENT_TAB_ID)
|
||||
})
|
||||
|
||||
@@ -211,7 +197,7 @@ describe('FloatingTerminalWindowControls default-agent launch', () => {
|
||||
clickLaunch()
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('Could not build launch command for Claude.')
|
||||
expect(mocks.activateTab).not.toHaveBeenCalled()
|
||||
expect(mocks.focusTerminalTabSurface).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Why: a floating window has nowhere to keep a structured session, so the launch must resolve a
|
||||
|
||||
@@ -38,8 +38,6 @@ export function FloatingTerminalWindowControls({
|
||||
onMinimize
|
||||
}: FloatingTerminalWindowControlsProps): React.JSX.Element {
|
||||
const defaultTuiAgent = useAppStore((s) => s.settings?.defaultTuiAgent ?? null)
|
||||
const setActiveTabForWorktree = useAppStore((s) => s.setActiveTabForWorktree)
|
||||
const activateTab = useAppStore((s) => s.activateTab)
|
||||
const maximizeShortcutLabel = useOptionalShortcutLabel('floatingWorkspace.maximize')
|
||||
const minimizeShortcutLabel = useOptionalShortcutLabel('floatingWorkspace.minimize')
|
||||
|
||||
@@ -71,11 +69,7 @@ export function FloatingTerminalWindowControls({
|
||||
const result = launchAgentInNewTab({
|
||||
agent: defaultAgent,
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
launchSource: 'shortcut',
|
||||
// Why: `agent-auto-ack-targets` relies on the floating panel's active tab never becoming the
|
||||
// global `activeTabId`; activating here would also flip the main view off an open editor.
|
||||
// This selects within the floating group below instead.
|
||||
activate: false
|
||||
launchSource: 'shortcut'
|
||||
})
|
||||
if (!result) {
|
||||
toast.error(
|
||||
@@ -90,14 +84,8 @@ export function FloatingTerminalWindowControls({
|
||||
if (result.surface.kind !== 'local-terminal') {
|
||||
return
|
||||
}
|
||||
// Why: the floating panel renders its visible tab from the unified group's
|
||||
// activeTabId. setActiveTabForWorktree only writes activeTabIdByWorktree, so
|
||||
// the new agent tab would be appended but never selected/focused. activateTab
|
||||
// selects it within the group, matching the empty-state tab creators.
|
||||
setActiveTabForWorktree(FLOATING_TERMINAL_WORKTREE_ID, result.surface.tabId)
|
||||
activateTab(result.surface.tabId)
|
||||
focusTerminalTabSurface(result.surface.tabId)
|
||||
}, [activateTab, defaultAgent, defaultAgentLabel, setActiveTabForWorktree])
|
||||
}, [defaultAgent, defaultAgentLabel])
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 px-2" data-floating-terminal-no-drag>
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
// Real-store coverage: the floating New Terminal ("+" menu and Cmd+T) selects its tab in the floating panel only.
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID, getDefaultSettings } from '../../../../shared/constants'
|
||||
import { createTestStore, makeWorktree, seedStore } from '../../store/slices/store-test-helpers'
|
||||
import { createStoreCascadesMockApi } from '../../store/slices/store-cascades-test-harness'
|
||||
|
||||
const storeBox = vi.hoisted(() => {
|
||||
const box: { store: unknown } = { store: null }
|
||||
return box
|
||||
})
|
||||
const focusTerminalTabSurface = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('react', async () => {
|
||||
const actual = await vi.importActual<typeof import('react')>('react') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import()
|
||||
return { ...actual, useCallback: <T>(callback: T) => callback }
|
||||
})
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: { info: vi.fn(), success: vi.fn(), error: vi.fn(), warning: vi.fn(), message: vi.fn() }
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
get useAppStore() {
|
||||
return storeBox.store
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/focus-terminal-tab-surface', () => ({ focusTerminalTabSurface }))
|
||||
|
||||
createStoreCascadesMockApi()
|
||||
|
||||
const MAIN_WORKTREE_ID = 'repo1::/path/wt1'
|
||||
|
||||
describe('floating "+" New Terminal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('selects the new tab in the floating panel without moving the main window', async () => {
|
||||
const store = createTestStore()
|
||||
storeBox.store = store
|
||||
seedStore(store, {
|
||||
settings: getDefaultSettings('/tmp'),
|
||||
worktreesByRepo: {
|
||||
repo1: [makeWorktree({ id: MAIN_WORKTREE_ID, repoId: 'repo1', path: '/path/wt1' })]
|
||||
},
|
||||
activeWorktreeId: MAIN_WORKTREE_ID
|
||||
})
|
||||
const mainTerminal = store.getState().createTab(MAIN_WORKTREE_ID)
|
||||
store.getState().setActiveTabType('editor', MAIN_WORKTREE_ID)
|
||||
const previousFloatingTab = store.getState().createTab(FLOATING_TERMINAL_WORKTREE_ID)
|
||||
const { useFloatingTerminalCreateActions } =
|
||||
await import('./use-floating-terminal-create-actions')
|
||||
const seeded = store.getState()
|
||||
const floatingGroup = seeded.groupsByWorktree[FLOATING_TERMINAL_WORKTREE_ID]?.[0]
|
||||
expect(floatingGroup?.activeTabId).toBe(previousFloatingTab.id)
|
||||
|
||||
const { createFloatingTerminalTab } = useFloatingTerminalCreateActions({
|
||||
activateTab: seeded.activateTab,
|
||||
setActiveTab: seeded.setActiveTab,
|
||||
createBrowserTab: seeded.createBrowserTab,
|
||||
browserDefaultUrl: seeded.browserDefaultUrl,
|
||||
openFile: seeded.openFile,
|
||||
activeGroup: floatingGroup,
|
||||
groupTabs: seeded.unifiedTabsByWorktree[FLOATING_TERMINAL_WORKTREE_ID] ?? [],
|
||||
markdownCwd: null
|
||||
})
|
||||
createFloatingTerminalTab()
|
||||
|
||||
const state = store.getState()
|
||||
const newTab = state.tabsByWorktree[FLOATING_TERMINAL_WORKTREE_ID]?.find(
|
||||
(tab) => tab.id !== previousFloatingTab.id
|
||||
)
|
||||
expect(newTab).toBeDefined()
|
||||
expect(state.activeTabId).toBe(mainTerminal.id)
|
||||
expect(state.activeTabType).toBe('editor')
|
||||
expect(state.activeTabTypeByWorktree[MAIN_WORKTREE_ID]).toBe('editor')
|
||||
expect(state.groupsByWorktree[FLOATING_TERMINAL_WORKTREE_ID]?.[0]?.activeTabId).toBe(newTab?.id)
|
||||
// Why: auto-acknowledge reads this map; it must follow the tab the floating panel shows.
|
||||
expect(state.activeTabIdByWorktree[FLOATING_TERMINAL_WORKTREE_ID]).toBe(newTab?.id)
|
||||
expect(focusTerminalTabSurface).toHaveBeenCalledExactlyOnceWith(newTab?.id)
|
||||
})
|
||||
})
|
||||
+5
-17
@@ -7,6 +7,7 @@ import { ensureClientCreationActionAllowed } from '@/lib/client-creation-action-
|
||||
import { openMarkdownDocumentInFloatingWorkspace } from '@/lib/open-markdown-in-floating-workspace'
|
||||
import { extractIpcErrorMessage } from '@/lib/ipc-error'
|
||||
import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface'
|
||||
import { createFloatingWorkspaceTerminalTab } from '@/lib/floating-workspace-tab-creation'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useAppStore } from '@/store'
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
|
||||
@@ -18,12 +19,7 @@ const LOCAL_RUNTIME_SETTINGS = { activeRuntimeEnvironmentId: null } as const
|
||||
|
||||
type FloatingTerminalCreateActionsInput = Pick<
|
||||
FloatingTerminalPanelStoreState,
|
||||
| 'activateTab'
|
||||
| 'setActiveTab'
|
||||
| 'createTab'
|
||||
| 'createBrowserTab'
|
||||
| 'browserDefaultUrl'
|
||||
| 'openFile'
|
||||
'activateTab' | 'setActiveTab' | 'createBrowserTab' | 'browserDefaultUrl' | 'openFile'
|
||||
> &
|
||||
Pick<FloatingTerminalPanelItems, 'activeGroup' | 'groupTabs'> &
|
||||
Pick<FloatingTerminalPanelLocalState, 'markdownCwd'>
|
||||
@@ -31,7 +27,6 @@ type FloatingTerminalCreateActionsInput = Pick<
|
||||
export function useFloatingTerminalCreateActions({
|
||||
activateTab,
|
||||
setActiveTab,
|
||||
createTab,
|
||||
createBrowserTab,
|
||||
browserDefaultUrl,
|
||||
openFile,
|
||||
@@ -63,16 +58,9 @@ export function useFloatingTerminalCreateActions({
|
||||
[activateTab, groupTabs, setActiveTab]
|
||||
)
|
||||
|
||||
const createFloatingTerminalTab = useCallback(
|
||||
(shellOverride?: string) => {
|
||||
const tab = createTab(FLOATING_TERMINAL_WORKTREE_ID, activeGroup?.id, shellOverride, {
|
||||
activate: false
|
||||
})
|
||||
activateTab(tab.id)
|
||||
focusTerminalTabSurface(tab.id)
|
||||
},
|
||||
[activateTab, activeGroup, createTab]
|
||||
)
|
||||
const createFloatingTerminalTab = useCallback((shellOverride?: string) => {
|
||||
void createFloatingWorkspaceTerminalTab(useAppStore.getState(), shellOverride)
|
||||
}, [])
|
||||
|
||||
const createFloatingBrowserTab = useCallback(() => {
|
||||
if (!ensureClientCreationActionAllowed(FLOATING_TERMINAL_WORKTREE_ID, 'managed-browser')) {
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ import { selectFloatingTerminalPanelInputs } from './floating-terminal-panel-inp
|
||||
export function useFloatingTerminalPanelStoreState() {
|
||||
const { tabs, browserTabs, groups, unifiedTabs, floatingFiles, expandedPaneByTabId } =
|
||||
useAppStore(selectFloatingTerminalPanelInputs)
|
||||
const createTab = useAppStore((state) => state.createTab)
|
||||
const createBrowserTab = useAppStore((state) => state.createBrowserTab)
|
||||
const closeTab = useAppStore((state) => state.closeTab)
|
||||
const closeBrowserTab = useAppStore((state) => state.closeBrowserTab)
|
||||
@@ -39,7 +38,6 @@ export function useFloatingTerminalPanelStoreState() {
|
||||
unifiedTabs,
|
||||
floatingFiles,
|
||||
expandedPaneByTabId,
|
||||
createTab,
|
||||
createBrowserTab,
|
||||
closeTab,
|
||||
closeBrowserTab,
|
||||
|
||||
@@ -96,7 +96,7 @@ export function useAiVaultOriginalPaneActions(): {
|
||||
return
|
||||
}
|
||||
const state = useAppStore.getState()
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', target.worktreeId)
|
||||
activateTabAndFocusPane(target.tabId, target.leafId, {
|
||||
flashFocusedPane: true,
|
||||
scrollToBottomIfOutputSinceLastView: true
|
||||
|
||||
@@ -52,7 +52,7 @@ export function useTabGroupActivationCommands({
|
||||
})
|
||||
}
|
||||
setActiveTab(terminalId)
|
||||
setActiveTabType('terminal')
|
||||
setActiveTabType('terminal', worktreeId)
|
||||
const activeLeafId = worktreeState.terminalLayoutsByTabId[terminalId]?.activeLeafId ?? null
|
||||
// Why: restore xterm focus to the store-active leaf so keyboard input can't drift to a sibling pane.
|
||||
focusTerminalTabSurface(terminalId, activeLeafId)
|
||||
@@ -99,11 +99,11 @@ export function useTabGroupActivationCommands({
|
||||
focusGroup(worktreeId, groupId)
|
||||
activateTab(item.id)
|
||||
if (item.contentType === 'simulator') {
|
||||
setActiveTabType('simulator')
|
||||
setActiveTabType('simulator', worktreeId)
|
||||
// simulator has no editor file entity
|
||||
} else {
|
||||
setActiveFile(item.entityId)
|
||||
setActiveTabType('editor')
|
||||
setActiveTabType('editor', worktreeId)
|
||||
}
|
||||
},
|
||||
[activateTab, focusGroup, groupId, groupTabs, setActiveFile, setActiveTabType, worktreeId]
|
||||
@@ -134,7 +134,7 @@ export function useTabGroupActivationCommands({
|
||||
})
|
||||
}
|
||||
setActiveBrowserTab(browserTabId)
|
||||
setActiveTabType('browser')
|
||||
setActiveTabType('browser', worktreeId)
|
||||
},
|
||||
[activateTab, focusGroup, groupId, groupTabs, setActiveBrowserTab, setActiveTabType, worktreeId]
|
||||
)
|
||||
|
||||
@@ -60,7 +60,7 @@ export function useTabGroupCreationCommands({
|
||||
const terminal = createTab(worktreeId, newGroupId)
|
||||
recordTerminalTabGroupSplit(terminal)
|
||||
setActiveTab(terminal.id)
|
||||
setActiveTabType('terminal')
|
||||
setActiveTabType('terminal', worktreeId)
|
||||
},
|
||||
[
|
||||
createEmptySplitGroup,
|
||||
@@ -161,7 +161,7 @@ export function useTabGroupCreationCommands({
|
||||
}
|
||||
const terminal = createTab(worktreeId, groupId, shellOverride)
|
||||
setActiveTab(terminal.id)
|
||||
setActiveTabType('terminal')
|
||||
setActiveTabType('terminal', worktreeId)
|
||||
focusTerminalTabSurface(terminal.id)
|
||||
})()
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ describe('useTabGroupWorkspaceModel terminal activation focus', () => {
|
||||
expect(mocks.focusGroup).toHaveBeenCalledWith('wt-1', 'group-1')
|
||||
expect(mocks.activateTab).toHaveBeenCalledWith('unified-terminal-1')
|
||||
expect(mocks.setActiveTab).toHaveBeenCalledWith('terminal-1')
|
||||
expect(mocks.setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(mocks.setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1')
|
||||
expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith('terminal-1', null)
|
||||
})
|
||||
|
||||
@@ -302,7 +302,7 @@ describe('useTabGroupWorkspaceModel terminal activation focus', () => {
|
||||
expect(mocks.focusGroup).toHaveBeenCalledWith('wt-1', 'group-1')
|
||||
expect(mocks.activateTab).toHaveBeenCalledWith('unified-terminal-1')
|
||||
expect(mocks.setActiveTab).toHaveBeenCalledWith('terminal-1')
|
||||
expect(mocks.setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(mocks.setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1')
|
||||
const event = mocks.dispatchEvent.mock.calls[0]?.[0] as CustomEvent<{ tabId: string }>
|
||||
expect(event.type).toBe(TOGGLE_TERMINAL_PANE_EXPAND_EVENT)
|
||||
expect(event.detail).toEqual({ tabId: 'terminal-1' })
|
||||
@@ -388,7 +388,7 @@ describe('useTabGroupWorkspaceModel terminal activation focus', () => {
|
||||
expect(mocks.dropUnifiedTab).not.toHaveBeenCalled()
|
||||
expect(mocks.recordFeatureInteraction).toHaveBeenCalledWith('terminal-pane-split')
|
||||
expect(mocks.setActiveTab).toHaveBeenCalledWith('terminal-2')
|
||||
expect(mocks.setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(mocks.setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1')
|
||||
})
|
||||
|
||||
it('seeds a new terminal instead of moving the active tab when the group has multiple tabs', async () => {
|
||||
|
||||
@@ -241,7 +241,7 @@ describe('focusRendererTerminalHandle', () => {
|
||||
|
||||
expect(mocks.storeState.setActiveWorktree).toHaveBeenCalledWith('wt-1')
|
||||
expect(mocks.storeState.setActiveView).toHaveBeenCalledWith('terminal')
|
||||
expect(mocks.storeState.setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(mocks.storeState.setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1')
|
||||
expect(mocks.storeState.setActiveTab).toHaveBeenCalledWith('tab-1')
|
||||
expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith('tab-1')
|
||||
})
|
||||
@@ -471,7 +471,7 @@ describe('createTerminalHandleLinkProvider', () => {
|
||||
expect(mocks.storeState.setActiveWorktree).toHaveBeenCalledWith('wt-1')
|
||||
expect(mocks.storeState.markWorktreeVisited).toHaveBeenCalledWith('wt-1')
|
||||
expect(mocks.storeState.setActiveView).toHaveBeenCalledWith('terminal')
|
||||
expect(mocks.storeState.setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(mocks.storeState.setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1')
|
||||
expect(mocks.storeState.revealWorktreeInSidebar).toHaveBeenCalledWith('wt-1')
|
||||
expect(mocks.storeState.setActiveTab).toHaveBeenCalledWith('tab-1')
|
||||
expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith('tab-1')
|
||||
|
||||
@@ -144,7 +144,7 @@ export function focusRendererTerminalHandle(
|
||||
store.setActiveWorktree(target.worktreeId)
|
||||
store.markWorktreeVisited(target.worktreeId)
|
||||
store.setActiveView('terminal')
|
||||
store.setActiveTabType('terminal')
|
||||
store.setActiveTabType('terminal', target.worktreeId)
|
||||
store.revealWorktreeInSidebar(target.worktreeId)
|
||||
if (target.leafId) {
|
||||
activateTabAndFocusPane(target.tabId, target.leafId)
|
||||
|
||||
@@ -330,7 +330,7 @@ describe('detachTerminalPaneToTab', () => {
|
||||
targetTabId: 'tab-detached'
|
||||
})
|
||||
expect(store.setActiveTab).toHaveBeenCalledWith('tab-detached')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('terminal', WORKTREE_ID)
|
||||
expect(persistLayoutSnapshot).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ export function detachTerminalPaneToTab(args: {
|
||||
targetTabId: tab.id
|
||||
})
|
||||
afterCreateStore.setActiveTab(tab.id)
|
||||
afterCreateStore.setActiveTabType('terminal')
|
||||
afterCreateStore.setActiveTabType('terminal', args.worktreeId)
|
||||
|
||||
return { tab, leafId: sourceLeafId, ptyId }
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ describe('closeTerminalTab kill-all routing', () => {
|
||||
|
||||
expect(state.closeTab).toHaveBeenCalledWith('terminal-1')
|
||||
expect(state.setActiveFile).toHaveBeenCalledWith('editor-1')
|
||||
expect(state.setActiveTabType).toHaveBeenCalledWith('editor')
|
||||
expect(state.setActiveTabType).toHaveBeenCalledWith('editor', 'wt')
|
||||
expect(state.closeFile).not.toHaveBeenCalled()
|
||||
expect(state.closeBrowserTab).not.toHaveBeenCalled()
|
||||
expect(state.setActiveWorktree).not.toHaveBeenCalled()
|
||||
@@ -115,7 +115,7 @@ describe('closeTerminalTab kill-all routing', () => {
|
||||
closeTerminalTab('terminal-1', { force: true })
|
||||
|
||||
expect(state.setActiveBrowserTab).toHaveBeenCalledWith('browser-1')
|
||||
expect(state.setActiveTabType).toHaveBeenCalledWith('browser')
|
||||
expect(state.setActiveTabType).toHaveBeenCalledWith('browser', 'wt')
|
||||
expect(state.closeBrowserTab).not.toHaveBeenCalled()
|
||||
expect(state.setActiveWorktree).not.toHaveBeenCalled()
|
||||
expect(state.createTab).not.toHaveBeenCalled()
|
||||
|
||||
@@ -77,7 +77,7 @@ describe('createNewTerminalTab', () => {
|
||||
createNewTerminalTab('wt-1', 'zsh')
|
||||
|
||||
expect(createTab).toHaveBeenCalledWith('wt-1', undefined, 'zsh', undefined)
|
||||
expect(setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1')
|
||||
expect(setTabBarOrder).toHaveBeenCalledWith('wt-1', ['tab-1'])
|
||||
expect(createWebRuntimeSessionTerminalMock).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -150,7 +150,7 @@ describe('createNewTerminalTab', () => {
|
||||
expect(createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, {
|
||||
startupCwd: '/repo/packages/app'
|
||||
})
|
||||
expect(setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1')
|
||||
})
|
||||
|
||||
it('delegates requested startup cwd to host runtime terminals', () => {
|
||||
|
||||
@@ -243,12 +243,12 @@ export function closeTerminalTab(
|
||||
const worktreeFile = current.openFiles.find((f) => f.worktreeId === owningWorktreeId)
|
||||
if (worktreeFile) {
|
||||
current.setActiveFile(worktreeFile.id)
|
||||
current.setActiveTabType('editor')
|
||||
current.setActiveTabType('editor', owningWorktreeId)
|
||||
} else {
|
||||
const browserTab = (current.browserTabsByWorktree?.[owningWorktreeId] ?? [])[0]
|
||||
if (browserTab) {
|
||||
current.setActiveBrowserTab(browserTab.id)
|
||||
current.setActiveTabType('browser')
|
||||
current.setActiveTabType('browser', owningWorktreeId)
|
||||
} else {
|
||||
current.setActiveWorktree(null)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export function createNewTerminalTab(
|
||||
shellOverride,
|
||||
options?.startupCwd ? { startupCwd: options.startupCwd } : undefined
|
||||
)
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', activeWorktreeId)
|
||||
// Why: persist the tab bar order with the new terminal at the end of the
|
||||
// current visual order. Without this, reconcileTabOrder falls back to
|
||||
// terminals-first when tabBarOrderByWorktree is unset, causing a new
|
||||
|
||||
@@ -22,7 +22,7 @@ export function useTerminalActivationActions(controller: TerminalBulkCloseContro
|
||||
})
|
||||
}
|
||||
setActiveTab(tabId)
|
||||
setActiveTabType('terminal')
|
||||
setActiveTabType('terminal', activeWorktreeId)
|
||||
},
|
||||
[activeWorktreeId, setActiveTab, setActiveTabType]
|
||||
)
|
||||
@@ -55,7 +55,7 @@ export function useTerminalActivationActions(controller: TerminalBulkCloseContro
|
||||
})
|
||||
}
|
||||
setActiveBrowserTab(tabId)
|
||||
setActiveTabType('browser')
|
||||
setActiveTabType('browser', activeWorktreeId)
|
||||
},
|
||||
[activeWorktreeId, setActiveBrowserTab, setActiveTabType]
|
||||
)
|
||||
|
||||
@@ -54,7 +54,7 @@ export function useTerminalCreateActions(controller: TerminalColdActivationContr
|
||||
return
|
||||
}
|
||||
const newTab = createTab(activeWorktreeId, undefined, shellOverride)
|
||||
setActiveTabType('terminal')
|
||||
setActiveTabType('terminal', activeWorktreeId)
|
||||
const state = useAppStore.getState()
|
||||
const currentTerminals = state.tabsByWorktree[activeWorktreeId] ?? []
|
||||
const currentEditors = state.openFiles.filter((file) => file.worktreeId === activeWorktreeId)
|
||||
|
||||
@@ -73,7 +73,7 @@ export function useTerminalEditorCloseQueue(controller: TerminalEditorCloseFound
|
||||
setActiveWorktree(file.worktreeId)
|
||||
}
|
||||
setActiveFile(nextFileId)
|
||||
setActiveTabType('editor')
|
||||
setActiveTabType('editor', file?.worktreeId ?? state.activeWorktreeId)
|
||||
setSaveDialogFileId(nextFileId)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ export function useTerminalWindowLifecycle(controller: TerminalActivationControl
|
||||
if (fallbackBrowserTab) {
|
||||
setActiveBrowserTab(fallbackBrowserTab.id)
|
||||
} else {
|
||||
setActiveTabType('terminal')
|
||||
setActiveTabType('terminal', renderedActiveWorktreeId)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
|
||||
@@ -223,7 +223,10 @@ export async function handleAutomationDispatchRequest({
|
||||
if (focusBeforeDispatch.activeTabId) {
|
||||
currentState.setActiveTab(focusBeforeDispatch.activeTabId)
|
||||
}
|
||||
currentState.setActiveTabType(focusBeforeDispatch.activeTabType)
|
||||
currentState.setActiveTabType(
|
||||
focusBeforeDispatch.activeTabType,
|
||||
focusBeforeDispatch.activeWorktreeId
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
releaseTerminalOwnership()
|
||||
|
||||
@@ -28,7 +28,7 @@ export function registerMobileAndTerminalCloseIpcBridge(
|
||||
runtimeEnvironmentId,
|
||||
mode: 'edit'
|
||||
})
|
||||
store.setActiveTabType('editor')
|
||||
store.setActiveTabType('editor', worktreeId)
|
||||
store.revealWorktreeInSidebar(worktreeId)
|
||||
}
|
||||
)
|
||||
@@ -46,7 +46,7 @@ export function registerMobileAndTerminalCloseIpcBridge(
|
||||
store.openDiff(worktreeId, filePath, relativePath, language, staged, {
|
||||
runtimeEnvironmentId
|
||||
})
|
||||
store.setActiveTabType('editor')
|
||||
store.setActiveTabType('editor', worktreeId)
|
||||
store.revealWorktreeInSidebar(worktreeId)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@ export function registerTabLifecycleIpcBridge(unsubs: (() => void)[]): void {
|
||||
return
|
||||
}
|
||||
const newTab = store.createTab(worktreeId)
|
||||
store.setActiveTabType('terminal')
|
||||
store.setActiveTabType('terminal', worktreeId)
|
||||
// Why: mirror Terminal.tsx handleNewTab so a new tab appends at the end, not index 0, when tabBarOrder is unset.
|
||||
const freshStore = useAppStore.getState()
|
||||
const currentTerminals = freshStore.tabsByWorktree[worktreeId] ?? []
|
||||
|
||||
@@ -126,7 +126,7 @@ export function registerTerminalPresentationIpcBridge(unsubs: (() => void)[]): v
|
||||
)
|
||||
}
|
||||
if (shouldActivate) {
|
||||
store.setActiveTabType('terminal')
|
||||
store.setActiveTabType('terminal', worktreeId)
|
||||
store.setActiveTab(tab.id)
|
||||
}
|
||||
if (shouldSurfaceOwner) {
|
||||
|
||||
@@ -92,7 +92,7 @@ export function registerTerminalRequestIpcBridge(unsubs: (() => void)[]): void {
|
||||
}
|
||||
}
|
||||
if (shouldActivate) {
|
||||
store.setActiveTabType('terminal')
|
||||
store.setActiveTabType('terminal', worktreeId)
|
||||
store.setActiveTab(tab.id)
|
||||
}
|
||||
if (shouldSurfaceOwner) {
|
||||
|
||||
@@ -167,7 +167,7 @@ export function registerTerminalUiRoutingIpcBridge(unsubs: (() => void)[]): void
|
||||
store.markWorktreeVisited(worktreeId)
|
||||
store.setActiveView('terminal')
|
||||
store.setActiveBrowserTab(browserTarget.workspaceId)
|
||||
store.setActiveTabType('browser')
|
||||
store.setActiveTabType('browser', worktreeId)
|
||||
store.revealWorktreeInSidebar(worktreeId)
|
||||
}
|
||||
return
|
||||
@@ -178,14 +178,14 @@ export function registerTerminalUiRoutingIpcBridge(unsubs: (() => void)[]): void
|
||||
store.focusGroup(worktreeId, tab.groupId)
|
||||
store.activateTab(tab.id)
|
||||
if (tab.contentType === 'agent-session') {
|
||||
store.setActiveTabType('agent-session')
|
||||
store.setActiveTabType('agent-session', worktreeId)
|
||||
} else if (browserTarget) {
|
||||
// Why: browser tabs need their own active-page state, not the editor file activation path.
|
||||
store.setActiveBrowserTab(browserTarget.workspaceId)
|
||||
store.setActiveTabType('browser')
|
||||
store.setActiveTabType('browser', worktreeId)
|
||||
} else {
|
||||
store.setActiveFile(tab.entityId)
|
||||
store.setActiveTabType('editor')
|
||||
store.setActiveTabType('editor', worktreeId)
|
||||
}
|
||||
store.revealWorktreeInSidebar(worktreeId)
|
||||
})
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('handleSwitchTerminalTab with a structured chat tab active', () => {
|
||||
// Stepping from the stale 'term-2' would land on 'term-3'.
|
||||
expect(mocks.store.setActiveTab).toHaveBeenCalledWith('term-1')
|
||||
expect(mocks.store.setActiveTab).not.toHaveBeenCalledWith('term-3')
|
||||
expect(mocks.store.setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(mocks.store.setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1')
|
||||
})
|
||||
|
||||
it('still reaches the sole terminal rather than reading as already focused', () => {
|
||||
|
||||
@@ -106,7 +106,7 @@ describe('handleSwitchTerminalTab', () => {
|
||||
|
||||
expect(handleSwitchTerminalTab(1)).toBe(true)
|
||||
expect(store.setActiveTab).toHaveBeenCalledWith('term-3')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1')
|
||||
})
|
||||
|
||||
it('wraps from the last terminal to the first terminal', () => {
|
||||
@@ -123,7 +123,7 @@ describe('handleSwitchTerminalTab', () => {
|
||||
|
||||
expect(handleSwitchTerminalTab(1)).toBe(true)
|
||||
expect(store.setActiveTab).toHaveBeenCalledWith('term-1')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1')
|
||||
})
|
||||
|
||||
it('returns false when no terminal tabs exist', () => {
|
||||
@@ -163,7 +163,7 @@ describe('handleSwitchTerminalTab', () => {
|
||||
|
||||
expect(handleSwitchTerminalTab(1)).toBe(true)
|
||||
expect(store.setActiveTab).toHaveBeenCalledWith('term-1')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1')
|
||||
})
|
||||
|
||||
it('jumps from an editor to the only terminal when one terminal exists', () => {
|
||||
@@ -178,7 +178,7 @@ describe('handleSwitchTerminalTab', () => {
|
||||
|
||||
expect(handleSwitchTerminalTab(1)).toBe(true)
|
||||
expect(store.setActiveTab).toHaveBeenCalledWith('term-1')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1')
|
||||
})
|
||||
|
||||
it('returns false when the only terminal is already active', () => {
|
||||
@@ -207,7 +207,7 @@ describe('handleSwitchTerminalTab', () => {
|
||||
|
||||
expect(handleSwitchTerminalTab(1)).toBe(true)
|
||||
expect(store.setActiveTab).toHaveBeenCalledWith('term-2')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1')
|
||||
})
|
||||
|
||||
it('falls back when one stale group terminal hides the remaining worktree terminal', () => {
|
||||
@@ -309,7 +309,7 @@ describe('handleSwitchTab', () => {
|
||||
expect(store.activateTab).toHaveBeenCalledWith('tab-terminal-2')
|
||||
expect(store.setActiveFile).not.toHaveBeenCalled()
|
||||
expect(store.setActiveBrowserTab).not.toHaveBeenCalled()
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1')
|
||||
})
|
||||
|
||||
it('cycles editor tabs using the active group tab id', () => {
|
||||
@@ -329,7 +329,7 @@ describe('handleSwitchTab', () => {
|
||||
expect(handleSwitchTab(1)).toBe(true)
|
||||
expect(store.setActiveFile).toHaveBeenCalledWith('file-c')
|
||||
expect(store.activateTab).toHaveBeenCalledWith('tab-c')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('editor')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('editor', 'wt-1')
|
||||
})
|
||||
|
||||
it('cycles browser tabs without jumping to other tab types', () => {
|
||||
@@ -348,7 +348,7 @@ describe('handleSwitchTab', () => {
|
||||
expect(store.setActiveBrowserTab).toHaveBeenCalledWith('browser-2')
|
||||
expect(store.setActiveTab).not.toHaveBeenCalled()
|
||||
expect(store.setActiveFile).not.toHaveBeenCalled()
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('browser')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('browser', 'wt-1')
|
||||
})
|
||||
|
||||
it('returns false when the active type has only one tab', () => {
|
||||
@@ -380,7 +380,7 @@ describe('handleSwitchTab', () => {
|
||||
expect(() => handleSwitchTab(1)).not.toThrow()
|
||||
expect(store.setActiveFile).toHaveBeenCalledWith('file-2')
|
||||
expect(store.activateTab).not.toHaveBeenCalled()
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('editor')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('editor', 'wt-1')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -402,7 +402,7 @@ describe('handleSwitchTabAcrossAllTypes', () => {
|
||||
expect(handleSwitchTabAcrossAllTypes(1)).toBe(true)
|
||||
expect(store.setActiveFile).toHaveBeenCalledWith('file-1')
|
||||
expect(store.activateTab).toHaveBeenCalledWith('tab-file-1')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('editor')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('editor', 'wt-1')
|
||||
})
|
||||
|
||||
it('wraps around across types', () => {
|
||||
@@ -419,7 +419,7 @@ describe('handleSwitchTabAcrossAllTypes', () => {
|
||||
|
||||
expect(handleSwitchTabAcrossAllTypes(1)).toBe(true)
|
||||
expect(store.setActiveTab).toHaveBeenCalledWith('term-1')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1')
|
||||
})
|
||||
|
||||
it('returns false when only one tab exists total', () => {
|
||||
@@ -461,7 +461,7 @@ describe('handleSwitchRecentTab', () => {
|
||||
expect(handleSwitchRecentTab()).toBe(true)
|
||||
expect(store.setActiveBrowserTab).toHaveBeenCalledWith('browser-b')
|
||||
expect(store.activateTab).toHaveBeenCalledWith('tab-b')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('browser')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('browser', 'wt-1')
|
||||
})
|
||||
|
||||
it('returns false when the MRU stack has no previous visible tab', () => {
|
||||
|
||||
@@ -59,6 +59,7 @@ function resolveCycleContext(): CycleContext | null {
|
||||
* correct tab instance is focused.
|
||||
*/
|
||||
export function activateCyclableTab(store: AppStoreState, next: TypeCyclableTab): void {
|
||||
const worktreeId = store.activeWorktreeId
|
||||
if (next.type === 'terminal') {
|
||||
store.setActiveTab(next.id)
|
||||
// Terminal entities can be open in multiple split groups. setActiveTab uses the legacy
|
||||
@@ -66,24 +67,24 @@ export function activateCyclableTab(store: AppStoreState, next: TypeCyclableTab)
|
||||
if (next.tabId) {
|
||||
store.activateTab?.(next.tabId)
|
||||
}
|
||||
store.setActiveTabType('terminal')
|
||||
store.setActiveTabType('terminal', worktreeId)
|
||||
} else if (next.type === 'browser') {
|
||||
store.setActiveBrowserTab(next.id)
|
||||
if (next.tabId) {
|
||||
store.activateTab?.(next.tabId)
|
||||
}
|
||||
store.setActiveTabType('browser')
|
||||
store.setActiveTabType('browser', worktreeId)
|
||||
} else if (next.type === 'simulator') {
|
||||
store.setActiveTab(next.tabId ?? next.id)
|
||||
if (next.tabId) {
|
||||
store.activateTab?.(next.tabId)
|
||||
}
|
||||
store.setActiveTabType('simulator')
|
||||
store.setActiveTabType('simulator', worktreeId)
|
||||
} else if (next.type === 'agent-session') {
|
||||
if (next.tabId) {
|
||||
store.activateTab?.(next.tabId)
|
||||
}
|
||||
store.setActiveTabType('agent-session')
|
||||
store.setActiveTabType('agent-session', worktreeId)
|
||||
} else {
|
||||
// Why: `setActiveFile` targets the file entity (its implicit activateTab
|
||||
// picks the first matching tab in the active group); `activateTab(tabId)`
|
||||
@@ -92,7 +93,7 @@ export function activateCyclableTab(store: AppStoreState, next: TypeCyclableTab)
|
||||
if (next.tabId) {
|
||||
store.activateTab?.(next.tabId)
|
||||
}
|
||||
store.setActiveTabType('editor')
|
||||
store.setActiveTabType('editor', worktreeId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,6 +375,6 @@ export function handleSwitchTerminalTab(direction: number): boolean {
|
||||
return false
|
||||
}
|
||||
store.setActiveTab(next.id)
|
||||
store.setActiveTabType('terminal')
|
||||
store.setActiveTabType('terminal', worktreeId)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('structured session completion focus', () => {
|
||||
|
||||
expect(store.focusGroup).toHaveBeenCalledWith(SESSION_WORKSPACE_ID, 'group-1')
|
||||
expect(store.activateTab).toHaveBeenCalledWith(TAB_ID)
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('agent-session')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('agent-session', SESSION_WORKSPACE_ID)
|
||||
})
|
||||
|
||||
it('does not apply focus after the user moves to another workspace', async () => {
|
||||
@@ -74,6 +74,6 @@ describe('structured session completion focus', () => {
|
||||
|
||||
expect(store.focusGroup).toHaveBeenCalledWith(SESSION_WORKSPACE_ID, 'group-1')
|
||||
expect(store.activateTab).toHaveBeenCalledWith(TAB_ID)
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('agent-session')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('agent-session', SESSION_WORKSPACE_ID)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -120,7 +120,7 @@ export function useGlobalFileDrop(): void {
|
||||
continue
|
||||
}
|
||||
const maybeRelative = toWorktreeRelativePath(result.destPath, worktreePath)
|
||||
store.setActiveTabType('editor')
|
||||
store.setActiveTabType('editor', activeWorktreeId)
|
||||
store.openFile(
|
||||
{
|
||||
filePath: result.destPath,
|
||||
@@ -181,7 +181,7 @@ export function useGlobalFileDrop(): void {
|
||||
// tab-strip editor target. Keeping the editor-open path centralized
|
||||
// here avoids the regression where CLI drops were all coerced into
|
||||
// editor tabs once the renderer lost the original drop surface.
|
||||
store.setActiveTabType('editor')
|
||||
store.setActiveTabType('editor', activeWorktreeId)
|
||||
store.openFile({
|
||||
filePath,
|
||||
relativePath,
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('useIpcEvents updater integration', () => {
|
||||
activate: true
|
||||
})
|
||||
expect(createTab).toHaveBeenCalledWith('wt-1')
|
||||
expect(setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1')
|
||||
|
||||
// Exact regression sequence: Local default -> connect/navigate Windows 2 ->
|
||||
// reveal a local terminal -> restart. Connection and navigation are transient.
|
||||
@@ -139,7 +139,7 @@ describe('useIpcEvents updater integration', () => {
|
||||
expect(markWorktreeVisited).toHaveBeenCalledWith('wt-2')
|
||||
expect(recordWorktreeVisit).toHaveBeenCalledWith('wt-2')
|
||||
expect(createTab).toHaveBeenCalledWith('wt-2', undefined, undefined, undefined)
|
||||
expect(setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-2')
|
||||
expect(setActiveTab).toHaveBeenCalledWith('tab-new')
|
||||
expect(revealWorktreeInSidebar).toHaveBeenCalledWith('wt-2')
|
||||
expect(focusRuntimeTerminalSurface).toHaveBeenCalledWith('tab-new', undefined, 'wt-2')
|
||||
|
||||
@@ -3,18 +3,29 @@ import { activateTabAndFocusPane } from './activate-tab-and-focus-pane'
|
||||
|
||||
const setActiveTab = vi.hoisted(() => vi.fn())
|
||||
const setActiveTabType = vi.hoisted(() => vi.fn())
|
||||
const worktreeState = vi.hoisted(() => {
|
||||
const state: {
|
||||
activeWorktreeId: string | null
|
||||
tabsByWorktree: Record<string, { id: string }[]>
|
||||
} = { activeWorktreeId: 'wt-1', tabsByWorktree: {} }
|
||||
return state
|
||||
})
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: {
|
||||
getState: () => ({
|
||||
setActiveTab,
|
||||
setActiveTabType
|
||||
setActiveTabType,
|
||||
activeWorktreeId: worktreeState.activeWorktreeId,
|
||||
tabsByWorktree: worktreeState.tabsByWorktree
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
describe('activateTabAndFocusPane', () => {
|
||||
beforeEach(() => {
|
||||
worktreeState.activeWorktreeId = 'wt-1'
|
||||
worktreeState.tabsByWorktree = { 'wt-1': [{ id: 'tab-1' }], 'wt-2': [{ id: 'tab-2' }] }
|
||||
setActiveTab.mockImplementation(() => undefined)
|
||||
setActiveTabType.mockImplementation(() => undefined)
|
||||
})
|
||||
@@ -76,9 +87,19 @@ describe('activateTabAndFocusPane', () => {
|
||||
|
||||
activateTabAndFocusPane('tab-1', null)
|
||||
|
||||
expect(setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1')
|
||||
expect(setActiveTab).toHaveBeenCalledWith('tab-1')
|
||||
expect(requestAnimationFrame).not.toHaveBeenCalled()
|
||||
expect(dispatchEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("scopes the terminal reveal to the tab's own worktree, not the one the main window shows", () => {
|
||||
vi.stubGlobal('requestAnimationFrame', vi.fn())
|
||||
vi.stubGlobal('cancelAnimationFrame', vi.fn())
|
||||
vi.stubGlobal('window', { dispatchEvent: vi.fn() })
|
||||
|
||||
activateTabAndFocusPane('tab-2', null)
|
||||
|
||||
expect(setActiveTabType).toHaveBeenCalledExactlyOnceWith('terminal', 'wt-2')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useAppStore } from '@/store'
|
||||
import { resolveActiveTabOwnerWorktreeId } from '@/store/slices/active-tab-owner-worktree'
|
||||
import { FOCUS_TERMINAL_PANE_EVENT, type FocusTerminalPaneDetail } from '@/constants/terminal'
|
||||
|
||||
let pendingFocusPaneFrameId: number | null = null
|
||||
@@ -19,10 +20,16 @@ export function activateTabAndFocusPane(
|
||||
scrollToBottomIfOutputSinceLastView?: boolean
|
||||
}
|
||||
): void {
|
||||
const { setActiveTab, setActiveTabType } = useAppStore.getState()
|
||||
const { setActiveTab, setActiveTabType, tabsByWorktree, activeWorktreeId } =
|
||||
useAppStore.getState()
|
||||
// Why: selecting a terminal tab is independent from the visible surface;
|
||||
// force Terminal first so tab-only activation reveals the full log.
|
||||
setActiveTabType('terminal')
|
||||
// Scoped to the tab's owner: a reveal that lands after a worktree switch must not retype another worktree.
|
||||
// An id no worktree owns yet keeps the on-screen worktree, as before scoping.
|
||||
setActiveTabType(
|
||||
'terminal',
|
||||
resolveActiveTabOwnerWorktreeId(tabsByWorktree, activeWorktreeId, tabId) ?? activeWorktreeId
|
||||
)
|
||||
setActiveTab(tabId)
|
||||
cancelPendingFocusPaneFrame()
|
||||
if (leafId === null) {
|
||||
|
||||
@@ -73,22 +73,23 @@ describe('agent launch caller placement and telemetry', () => {
|
||||
expect(createdTabOptions(store)).toMatchObject({ launchAgent: profile.args.agent })
|
||||
})
|
||||
|
||||
it.each(cases)('decides whether %s takes the global selection', async (_id, profile) => {
|
||||
it.each(cases)('shows terminals only in the worktree %s launched into', async (_id, profile) => {
|
||||
await launch(profile)
|
||||
|
||||
const takesSelection = profile.args.activate !== false
|
||||
expect(createdTabOptions(store)?.activate).toBe(takesSelection ? undefined : false)
|
||||
expect(store.setActiveTabType.mock.calls.length).toBe(takesSelection ? 1 : 0)
|
||||
if (takesSelection) {
|
||||
expect(store.setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
}
|
||||
// Why: the store moves the main window only when that worktree is the active one, so a floating
|
||||
// or background launch cannot drop the main window off its editor or chat tab.
|
||||
expect(createdTabOptions(store)).not.toHaveProperty('activate')
|
||||
expect(store.setActiveTabType).toHaveBeenCalledExactlyOnceWith(
|
||||
'terminal',
|
||||
profile.args.worktreeId
|
||||
)
|
||||
})
|
||||
|
||||
it.each(cases)('persists the tab-bar order after %s launches', async (_id, profile) => {
|
||||
await launch(profile)
|
||||
|
||||
// Why: without this the stored order falls back to terminals-first and the new tab jumps to
|
||||
// index 0. It runs for every call site, including the one that does not take the selection.
|
||||
// index 0. It runs for every call site, including the floating workspace.
|
||||
expect(store.setTabBarOrder).toHaveBeenCalledTimes(1)
|
||||
expect(store.setTabBarOrder.mock.calls[0]?.[0]).toBe(profile.args.worktreeId)
|
||||
expect(store.setTabBarOrder.mock.calls[0]?.[1]).toContain('tab-1')
|
||||
|
||||
@@ -61,16 +61,11 @@ export const AGENT_LAUNCH_CALLER_PROFILES: readonly AgentLaunchCallerProfile[] =
|
||||
{
|
||||
id: 'floating-default-agent',
|
||||
caller: 'src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx',
|
||||
sourceMarkers: [
|
||||
'worktreeId: FLOATING_TERMINAL_WORKTREE_ID',
|
||||
"launchSource: 'shortcut'",
|
||||
'activate: false'
|
||||
],
|
||||
sourceMarkers: ['worktreeId: FLOATING_TERMINAL_WORKTREE_ID', "launchSource: 'shortcut'"],
|
||||
args: {
|
||||
agent: 'codex',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
launchSource: 'shortcut',
|
||||
activate: false
|
||||
launchSource: 'shortcut'
|
||||
},
|
||||
passesBeforeSurfaceOpen: false,
|
||||
passesLaunchPlan: false,
|
||||
|
||||
@@ -80,7 +80,7 @@ describe('ensureSimulatorTab', () => {
|
||||
expect(mockStoreState.activateTab).toHaveBeenCalledWith('sim-1')
|
||||
expect(mockStoreState.setActiveTab).not.toHaveBeenCalled()
|
||||
expect(mockStoreState.focusGroup).toHaveBeenCalledWith('wt-1', 'group-1')
|
||||
expect(mockStoreState.setActiveTabType).toHaveBeenCalledWith('simulator')
|
||||
expect(mockStoreState.setActiveTabType).toHaveBeenCalledWith('simulator', 'wt-1')
|
||||
})
|
||||
|
||||
it('does not reuse a simulator owned by a same-id sibling host', async () => {
|
||||
@@ -266,7 +266,7 @@ describe('ensureSimulatorTab', () => {
|
||||
})
|
||||
expect(mockStoreState.activateTab).toHaveBeenCalledWith('sim-2')
|
||||
expect(mockStoreState.focusGroup).toHaveBeenCalledWith('wt-1', 'group-2')
|
||||
expect(mockStoreState.setActiveTabType).toHaveBeenCalledWith('simulator')
|
||||
expect(mockStoreState.setActiveTabType).toHaveBeenCalledWith('simulator', 'wt-1')
|
||||
})
|
||||
|
||||
it('falls back to the source group when atomic right split creation fails', async () => {
|
||||
|
||||
@@ -115,7 +115,7 @@ export function ensureSimulatorTab(
|
||||
if (shouldSurface && store.activeWorktreeId === worktreeId) {
|
||||
store.activateTab(existing.id)
|
||||
store.focusGroup(worktreeId, existing.groupId)
|
||||
store.setActiveTabType('simulator')
|
||||
store.setActiveTabType('simulator', worktreeId)
|
||||
}
|
||||
return existing.id
|
||||
}
|
||||
@@ -133,7 +133,7 @@ export function ensureSimulatorTab(
|
||||
...(executionHostId ? { executionHostId } : {})
|
||||
})
|
||||
store.activateTab(tab.id)
|
||||
store.setActiveTabType('simulator')
|
||||
store.setActiveTabType('simulator', worktreeId)
|
||||
store.focusGroup(worktreeId, tab.groupId)
|
||||
return tab.id
|
||||
}
|
||||
@@ -166,7 +166,7 @@ export function ensureSimulatorTab(
|
||||
})
|
||||
if (shouldSurface) {
|
||||
store.activateTab(tab.id)
|
||||
store.setActiveTabType('simulator')
|
||||
store.setActiveTabType('simulator', worktreeId)
|
||||
store.focusGroup(worktreeId, tab.groupId)
|
||||
}
|
||||
return tab.id
|
||||
|
||||
@@ -9,10 +9,7 @@ import { focusTerminalTabSurface } from './focus-terminal-tab-surface'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { assertClientCreationActionAvailable } from './client-creation-action-policy'
|
||||
|
||||
type FloatingWorkspaceTerminalStore = Pick<
|
||||
AppState,
|
||||
'activeGroupIdByWorktree' | 'createTab' | 'activateTab'
|
||||
>
|
||||
type FloatingWorkspaceTerminalStore = Pick<AppState, 'activeGroupIdByWorktree' | 'createTab'>
|
||||
|
||||
type FloatingWorkspaceBrowserStore = Pick<
|
||||
AppState,
|
||||
@@ -29,10 +26,7 @@ export async function createFloatingWorkspaceTerminalTab(
|
||||
|
||||
// Why: the floating workspace is a local scratchpad; a focused remote runtime
|
||||
// must not own its SSH/tmux terminals or prune them via session snapshots.
|
||||
const tab = store.createTab(FLOATING_TERMINAL_WORKTREE_ID, targetGroupId, shellOverride, {
|
||||
activate: false
|
||||
})
|
||||
store.activateTab(tab.id)
|
||||
const tab = store.createTab(FLOATING_TERMINAL_WORKTREE_ID, targetGroupId, shellOverride)
|
||||
focusTerminalTabSurface(tab.id)
|
||||
return tab
|
||||
}
|
||||
|
||||
@@ -458,10 +458,10 @@ describe('createFloatingWorkspaceTerminalTab', () => {
|
||||
expect(store.createTab).toHaveBeenCalledWith(
|
||||
FLOATING_TERMINAL_WORKTREE_ID,
|
||||
'floating-group',
|
||||
undefined,
|
||||
{ activate: false }
|
||||
undefined
|
||||
)
|
||||
expect(store.activateTab).toHaveBeenCalledWith('floating-tab-1')
|
||||
// Why: createTab itself activates the new tab within the floating group.
|
||||
expect(store.activateTab).not.toHaveBeenCalled()
|
||||
expect(focusTerminalTabSurfaceMock).toHaveBeenCalledWith('floating-tab-1')
|
||||
})
|
||||
|
||||
@@ -481,10 +481,10 @@ describe('createFloatingWorkspaceTerminalTab', () => {
|
||||
expect(store.createTab).toHaveBeenCalledWith(
|
||||
FLOATING_TERMINAL_WORKTREE_ID,
|
||||
'floating-group',
|
||||
'pwsh',
|
||||
{ activate: false }
|
||||
'pwsh'
|
||||
)
|
||||
expect(store.activateTab).toHaveBeenCalledWith('floating-tab-runtime')
|
||||
// Why: createTab itself activates the new tab within the floating group.
|
||||
expect(store.activateTab).not.toHaveBeenCalled()
|
||||
expect(focusTerminalTabSurfaceMock).toHaveBeenCalledWith('floating-tab-runtime')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// Real-store coverage: a launch into the floating workspace must leave the main window's tab alone.
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID, getDefaultSettings } from '../../../shared/constants'
|
||||
import { createTestStore, makeWorktree, seedStore } from '../store/slices/store-test-helpers'
|
||||
import { createStoreCascadesMockApi } from '../store/slices/store-cascades-test-harness'
|
||||
|
||||
const storeBox = vi.hoisted(() => {
|
||||
const box: { store: unknown } = { store: null }
|
||||
return box
|
||||
})
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: { info: vi.fn(), success: vi.fn(), error: vi.fn(), warning: vi.fn(), message: vi.fn() }
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
get useAppStore() {
|
||||
return storeBox.store
|
||||
}
|
||||
}))
|
||||
|
||||
createStoreCascadesMockApi()
|
||||
|
||||
const MAIN_WORKTREE_ID = 'repo1::/path/wt1'
|
||||
|
||||
function seedMainWindowOnEditor(): ReturnType<typeof createTestStore> {
|
||||
const store = createTestStore()
|
||||
storeBox.store = store
|
||||
seedStore(store, {
|
||||
settings: getDefaultSettings('/tmp'),
|
||||
worktreesByRepo: {
|
||||
repo1: [makeWorktree({ id: MAIN_WORKTREE_ID, repoId: 'repo1', path: '/path/wt1' })]
|
||||
},
|
||||
activeWorktreeId: MAIN_WORKTREE_ID
|
||||
})
|
||||
const mainTerminal = store.getState().createTab(MAIN_WORKTREE_ID)
|
||||
// The main window is showing a non-terminal tab, as in the report.
|
||||
store.getState().setActiveTabType('editor', MAIN_WORKTREE_ID)
|
||||
expect(store.getState().activeTabId).toBe(mainTerminal.id)
|
||||
return store
|
||||
}
|
||||
|
||||
describe('launchAgentInNewTab main-window surface', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('selects a floating launch in the floating panel without moving the main window', async () => {
|
||||
const store = seedMainWindowOnEditor()
|
||||
const before = store.getState()
|
||||
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
|
||||
|
||||
const result = launchAgentInNewTab({
|
||||
agent: 'opencode',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID
|
||||
})
|
||||
|
||||
expect(result?.surface.kind).toBe('local-terminal')
|
||||
const tabId = result?.surface.kind === 'local-terminal' ? result.surface.tabId : null
|
||||
const state = store.getState()
|
||||
expect(state.activeTabType).toBe('editor')
|
||||
expect(state.activeTabId).toBe(before.activeTabId)
|
||||
expect(state.activeTabTypeByWorktree[MAIN_WORKTREE_ID]).toBe('editor')
|
||||
// Why: the floating panel renders the group's active tab, so the launch still lands selected there.
|
||||
const floatingGroup = state.groupsByWorktree[FLOATING_TERMINAL_WORKTREE_ID]?.[0]
|
||||
expect(floatingGroup?.activeTabId).toBe(tabId)
|
||||
expect(state.activeTabIdByWorktree[FLOATING_TERMINAL_WORKTREE_ID]).toBe(tabId)
|
||||
expect(state.activeTabTypeByWorktree[FLOATING_TERMINAL_WORKTREE_ID]).toBe('terminal')
|
||||
})
|
||||
|
||||
it('still brings a launch in the active worktree to the front', async () => {
|
||||
const store = seedMainWindowOnEditor()
|
||||
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
|
||||
|
||||
const result = launchAgentInNewTab({ agent: 'opencode', worktreeId: MAIN_WORKTREE_ID })
|
||||
|
||||
const tabId = result?.surface.kind === 'local-terminal' ? result.surface.tabId : null
|
||||
expect(tabId).not.toBeNull()
|
||||
expect(store.getState().activeTabType).toBe('terminal')
|
||||
expect(store.getState().activeTabId).toBe(tabId)
|
||||
})
|
||||
})
|
||||
@@ -91,16 +91,18 @@ describe('launchAgentInNewTab terminal tab activation', () => {
|
||||
mockCreateTab.mockReturnValue({ id: 'tab-1' })
|
||||
})
|
||||
|
||||
it('takes the global selection by default', async () => {
|
||||
it('shows terminals in the worktree it launched into', async () => {
|
||||
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
|
||||
|
||||
launchAgentInNewTab({ agent: 'codex', worktreeId: 'wt-1' })
|
||||
|
||||
expect(mockCreateTab.mock.calls[0]?.[3]).not.toHaveProperty('activate')
|
||||
expect(mockSetActiveTabType).toHaveBeenCalledExactlyOnceWith('terminal')
|
||||
// Why: an unscoped call targets the active worktree — the main window — whatever worktree the
|
||||
// launch landed in, which is how a floating launch dropped the main window off its editor.
|
||||
expect(mockSetActiveTabType).toHaveBeenCalledExactlyOnceWith('terminal', 'wt-1')
|
||||
})
|
||||
|
||||
it('honours the chat default in a floating launch while keeping it out of the global selection', async () => {
|
||||
it('honours the chat default in a floating launch and scopes its surface to the floating workspace', async () => {
|
||||
store.settings = placementSettings({
|
||||
experimentalNativeChat: true,
|
||||
experimentalStructuredNativeChat: true,
|
||||
@@ -116,23 +118,22 @@ describe('launchAgentInNewTab terminal tab activation', () => {
|
||||
|
||||
launchAgentInNewTab({
|
||||
agent: 'codex',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
activate: false
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID
|
||||
})
|
||||
|
||||
// Why: the floating workspace selects within its own group; activating here would move the
|
||||
// main window's active tab to a tab it does not show.
|
||||
expect(mockCreateTab).toHaveBeenCalledWith(
|
||||
FLOATING_TERMINAL_WORKTREE_ID,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
launchAgent: 'codex',
|
||||
activate: false,
|
||||
viewMode: 'chat'
|
||||
}
|
||||
)
|
||||
expect(mockSetActiveTabType).not.toHaveBeenCalled()
|
||||
expect(mockSetActiveTabType).toHaveBeenCalledExactlyOnceWith(
|
||||
'terminal',
|
||||
FLOATING_TERMINAL_WORKTREE_ID
|
||||
)
|
||||
// Why: the panel hosts the chat pane itself, so the launch carries the user's model/effort
|
||||
// preferences the same way a main-window launch does.
|
||||
expect(mockSeedNativeChatAppliedSessionOptions).toHaveBeenCalledWith('tab-1', 'codex', {
|
||||
|
||||
@@ -102,7 +102,8 @@ describe('launchAgentInNewTab paired web runtime', () => {
|
||||
})
|
||||
expect(mocks.createTab).not.toHaveBeenCalled()
|
||||
await Promise.resolve()
|
||||
expect(mocks.setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
// Why: host creation is async, so the user may be viewing another worktree by the time it lands.
|
||||
expect(mocks.setActiveTabType).toHaveBeenCalledExactlyOnceWith('terminal', 'wt-1')
|
||||
expect(mocks.closeTab).toHaveBeenCalledWith('stale-agent-tab', { reason: 'cleanup' })
|
||||
})
|
||||
|
||||
|
||||
@@ -58,13 +58,6 @@ export type LaunchAgentInNewTabArgs = {
|
||||
* terminal route, whose readiness signal the client watches itself.
|
||||
*/
|
||||
onPromptDeliveryUnconfirmed?: () => void
|
||||
/**
|
||||
* Whether the new terminal tab takes the global selection. The floating workspace passes `false`
|
||||
* and selects within its own group instead, so launching there does not move the main window's
|
||||
* active tab. Terminal surface only — the structured and host-published routes own their own
|
||||
* activation.
|
||||
*/
|
||||
activate?: boolean
|
||||
/** Keeps a preflighted route authoritative across workspace creation. */
|
||||
agentSessionLaunchPlan?: AgentSessionLaunchPlan
|
||||
/** Lets a workspace reveal itself before the selected surface opens. */
|
||||
@@ -121,8 +114,7 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent
|
||||
onPromptDelivered,
|
||||
onPromptDeliveryUnconfirmed,
|
||||
agentSessionLaunchPlan,
|
||||
beforeSurfaceOpen,
|
||||
activate
|
||||
beforeSurfaceOpen
|
||||
} = args
|
||||
const store = useAppStore.getState()
|
||||
const { worktreeSshConnectionId, resolvedLaunchPlatform, isRemote, queuedShell } =
|
||||
@@ -253,7 +245,6 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent
|
||||
const tab = store.createTab(worktreeId, groupId, undefined, {
|
||||
launchAgent: agent,
|
||||
quickCommandLabel,
|
||||
...(activate === false ? { activate: false } : {}),
|
||||
...initialViewModeProps
|
||||
})
|
||||
seedNativeChatAppliedSessionOptions(tab.id, agent, startupPlan.sessionOptions)
|
||||
@@ -325,9 +316,8 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent
|
||||
}
|
||||
|
||||
// Why: without setActiveTabType('terminal') an activated launch can stay hidden behind an editor.
|
||||
if (activate !== false) {
|
||||
store.setActiveTabType('terminal')
|
||||
}
|
||||
// Scoped to the launch's worktree so a floating or background launch leaves the main window's tab alone.
|
||||
store.setActiveTabType('terminal', worktreeId)
|
||||
|
||||
// Why: persist tab-bar order so reconcileTabOrder doesn't fall back to terminals-first and jump the new tab to index 0.
|
||||
persistAgentLaunchTabOrder(worktreeId, tab.id)
|
||||
|
||||
@@ -115,7 +115,7 @@ export function launchAgentInWebHostTab(args: {
|
||||
)
|
||||
return { delivered: false, failureNotified: true }
|
||||
}
|
||||
useAppStore.getState().setActiveTabType('terminal')
|
||||
useAppStore.getState().setActiveTabType('terminal', worktreeId)
|
||||
if (hasPrompt && promptDelivered) {
|
||||
onPromptDelivered?.()
|
||||
}
|
||||
|
||||
@@ -88,7 +88,8 @@ describe('launchAiVaultSessionInNewTab', () => {
|
||||
request_kind: 'resume'
|
||||
}
|
||||
})
|
||||
expect(mockSetActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
// Why: a resume can target a worktree the user is not viewing (it is activated afterwards).
|
||||
expect(mockSetActiveTabType).toHaveBeenCalledExactlyOnceWith('terminal', 'wt-1')
|
||||
expect(mockSetTabBarOrder).toHaveBeenCalledWith('wt-1', ['tab-1'])
|
||||
expect(result).toEqual({ tabId: 'tab-1', groupId: 'group-1' })
|
||||
})
|
||||
@@ -188,6 +189,6 @@ describe('launchAiVaultSessionInNewTab', () => {
|
||||
if (result.tabId === null) {
|
||||
await expect(result.runtimeLaunch).resolves.toEqual({ status: 'created' })
|
||||
}
|
||||
expect(mockSetActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(mockSetActiveTabType).toHaveBeenCalledExactlyOnceWith('terminal', 'wt-1')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -51,7 +51,7 @@ export function launchAiVaultSessionInNewTab(args: {
|
||||
})
|
||||
const observedRuntimeLaunch = runtimeLaunch.then((outcome) => {
|
||||
if (outcome.status === 'created') {
|
||||
useAppStore.getState().setActiveTabType('terminal')
|
||||
useAppStore.getState().setActiveTabType('terminal', args.worktreeId)
|
||||
}
|
||||
return outcome
|
||||
})
|
||||
@@ -83,7 +83,7 @@ export function launchAiVaultSessionInNewTab(args: {
|
||||
request_kind: 'resume'
|
||||
}
|
||||
})
|
||||
store.setActiveTabType('terminal')
|
||||
store.setActiveTabType('terminal', args.worktreeId)
|
||||
|
||||
const fresh = useAppStore.getState()
|
||||
const termIds = (fresh.tabsByWorktree[args.worktreeId] ?? []).map((t) => t.id)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { SleepingAgentSessionRecord } from '../../../shared/agent-session-resume'
|
||||
import type { TerminalTab } from '../../../shared/terminal-tab-types'
|
||||
import { useAppStore } from '@/store'
|
||||
import { resumeSleepingAgentSessionsForWorktree } from './resume-sleeping-agent-session'
|
||||
import { getProviderSessionClaimKey } from './sleeping-agent-pane-ownership'
|
||||
@@ -137,4 +138,40 @@ describe('resumeSleepingAgentSessionsForWorktree navigation suppression', () =>
|
||||
|
||||
expect(useAppStore.getState().activeTabType).toBe('terminal')
|
||||
})
|
||||
|
||||
it('leaves the current view alone when the user has left the resumed worktree', () => {
|
||||
// Why: the activation gate resumes after async readiness checks, by which time the user may
|
||||
// be viewing another worktree; the resume still lands selected when they return.
|
||||
const record = makeRecord({ origin: 'quit' })
|
||||
const sleptTab: TerminalTab = {
|
||||
id: 'tab-1',
|
||||
ptyId: null,
|
||||
worktreeId: 'wt-1',
|
||||
title: 'shell',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1
|
||||
}
|
||||
useAppStore.setState({
|
||||
activeWorktreeId: 'wt-other',
|
||||
activeTabId: 'other-tab',
|
||||
activeTabType: 'browser',
|
||||
activeTabIdByWorktree: { 'wt-other': 'other-tab' },
|
||||
activeTabTypeByWorktree: { 'wt-other': 'browser' },
|
||||
tabsByWorktree: { 'wt-1': [sleptTab] },
|
||||
sleepingAgentSessionsByPaneKey: { [record.paneKey]: record }
|
||||
})
|
||||
|
||||
resumeSleepingAgentSessionsForWorktree('wt-1')
|
||||
|
||||
const state = useAppStore.getState()
|
||||
const resumedTab = state.tabsByWorktree['wt-1']?.find((tab) => tab.id !== 'tab-1')
|
||||
expect(resumedTab).toBeDefined()
|
||||
expect(state.activeTabId).toBe('other-tab')
|
||||
expect(state.activeTabType).toBe('browser')
|
||||
expect(state.activeTabTypeByWorktree['wt-other']).toBe('browser')
|
||||
expect(state.activeTabIdByWorktree['wt-1']).toBe(resumedTab?.id)
|
||||
expect(state.activeTabTypeByWorktree['wt-1']).toBe('terminal')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -114,7 +114,7 @@ export function runQuickCommandInNewTab({
|
||||
// Why: match `+` button's createNewTerminalTab — without this, a worktree
|
||||
// currently showing an editor file keeps rendering the editor and the new
|
||||
// terminal tab stays invisible.
|
||||
store.setActiveTabType('terminal')
|
||||
store.setActiveTabType('terminal', worktreeId)
|
||||
|
||||
// Why: persist tab-bar order with the new terminal appended. Without this,
|
||||
// reconcileTabOrder falls back to terminals-first when the stored order is
|
||||
|
||||
@@ -60,6 +60,6 @@ export function activateSimulatorTabPaletteResult({
|
||||
state.focusGroup(worktreeId, tab.groupId)
|
||||
state.activateTab(tab.id, { worktreeId })
|
||||
state.setActiveTab(tab.id)
|
||||
state.setActiveTabType('simulator')
|
||||
state.setActiveTabType('simulator', worktreeId)
|
||||
return { status: 'activated', tabId: tab.id }
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ export function launchSleepingAgentSession(
|
||||
})
|
||||
state.clearSleepingAgentSession(record.paneKey)
|
||||
if (!options?.suppressNavigation) {
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', record.worktreeId)
|
||||
}
|
||||
appendTabToWorktreeOrder(record.worktreeId, tab.id)
|
||||
options?.onSessionLaunched?.(tab.id)
|
||||
|
||||
@@ -75,7 +75,7 @@ export function activateTabNumberShortcut(index: number): boolean {
|
||||
})
|
||||
}
|
||||
store.setActiveTab(target.entityId)
|
||||
store.setActiveTabType('terminal')
|
||||
store.setActiveTabType('terminal', worktreeId)
|
||||
focusTerminalTabSurface(target.entityId)
|
||||
return true
|
||||
}
|
||||
@@ -89,17 +89,17 @@ export function activateTabNumberShortcut(index: number): boolean {
|
||||
})
|
||||
}
|
||||
store.setActiveBrowserTab(target.entityId)
|
||||
store.setActiveTabType('browser')
|
||||
store.setActiveTabType('browser', worktreeId)
|
||||
return true
|
||||
}
|
||||
|
||||
if (target.contentType === 'simulator') {
|
||||
store.setActiveTab(target.id)
|
||||
store.setActiveTabType('simulator')
|
||||
store.setActiveTabType('simulator', worktreeId)
|
||||
return true
|
||||
}
|
||||
|
||||
store.setActiveFile(target.entityId)
|
||||
store.setActiveTabType('editor')
|
||||
store.setActiveTabType('editor', worktreeId)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ describe('activateWorkspaceTabPaletteResult', () => {
|
||||
worktreeId: 'wt-1'
|
||||
})
|
||||
expect(mocks.store.setActiveTab).toHaveBeenCalledWith('terminal-1')
|
||||
expect(mocks.store.setActiveTabType).toHaveBeenCalledWith('terminal')
|
||||
expect(mocks.store.setActiveTabType).toHaveBeenCalledWith('terminal', 'wt-1')
|
||||
expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith('terminal-1')
|
||||
})
|
||||
|
||||
@@ -293,7 +293,7 @@ describe('activateWorkspaceTabPaletteResult', () => {
|
||||
expect(mocks.store.focusGroup).toHaveBeenCalledWith('wt-1', 'group-2')
|
||||
expect(mocks.store.setActiveFile).toHaveBeenCalledWith('/tmp/wt-1/src/app.ts')
|
||||
expect(mocks.store.activateTab).toHaveBeenLastCalledWith('diff-tab-1', { worktreeId: 'wt-1' })
|
||||
expect(mocks.store.setActiveTabType).toHaveBeenCalledWith('editor')
|
||||
expect(mocks.store.setActiveTabType).toHaveBeenCalledWith('editor', 'wt-1')
|
||||
expect(mocks.focusTerminalTabSurface).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -344,7 +344,7 @@ describe('activateWorkspaceTabPaletteResult', () => {
|
||||
expect(mocks.store.focusGroup).toHaveBeenCalledWith('wt-1', 'group-2')
|
||||
expect(mocks.store.setActiveFile).toHaveBeenCalledWith(entityId)
|
||||
expect(mocks.store.activateTab).toHaveBeenLastCalledWith(tabId, { worktreeId: 'wt-1' })
|
||||
expect(mocks.store.setActiveTabType).toHaveBeenCalledWith('editor')
|
||||
expect(mocks.store.setActiveTabType).toHaveBeenCalledWith('editor', 'wt-1')
|
||||
})
|
||||
|
||||
it('returns stale failures before focusing a removed group or tab', () => {
|
||||
|
||||
@@ -133,7 +133,7 @@ export function activateWorkspaceTabPaletteResult(
|
||||
})
|
||||
}
|
||||
state.setActiveTab(result.entityId)
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', result.worktreeId)
|
||||
focusTerminalTabSurface(result.entityId)
|
||||
return { status: 'activated' }
|
||||
}
|
||||
@@ -141,6 +141,6 @@ export function activateWorkspaceTabPaletteResult(
|
||||
state.setActiveFile(result.entityId)
|
||||
// setActiveFile may pick an editor tab for the same entity instead of this diff.
|
||||
state.activateTab(result.tabId, { worktreeId: result.worktreeId })
|
||||
state.setActiveTabType('editor')
|
||||
state.setActiveTabType('editor', result.worktreeId)
|
||||
return { status: 'activated' }
|
||||
}
|
||||
|
||||
@@ -21,15 +21,14 @@ export function createOpenFileState(
|
||||
activeTabTypeByWorktree: {},
|
||||
activeTabType: 'terminal',
|
||||
recentlyClosedEditorTabsByWorktree: {},
|
||||
setActiveTabType: (type, targetWorktreeId) =>
|
||||
set((s) => {
|
||||
const worktreeId = targetWorktreeId ?? s.activeWorktreeId
|
||||
return {
|
||||
...(worktreeId === s.activeWorktreeId ? { activeTabType: type } : {}),
|
||||
activeTabTypeByWorktree: worktreeId
|
||||
? { ...s.activeTabTypeByWorktree, [worktreeId]: type }
|
||||
: s.activeTabTypeByWorktree
|
||||
}
|
||||
})
|
||||
// Why the worktree is required: an implicit "active worktree" default let callers retype the
|
||||
// main window while acting on a tab that lives elsewhere (e.g. the floating workspace).
|
||||
setActiveTabType: (type, worktreeId) =>
|
||||
set((s) => ({
|
||||
...(worktreeId === s.activeWorktreeId ? { activeTabType: type } : {}),
|
||||
activeTabTypeByWorktree: worktreeId
|
||||
? { ...s.activeTabTypeByWorktree, [worktreeId]: type }
|
||||
: s.activeTabTypeByWorktree
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export type EditorFilesSlice = {
|
||||
activeFileIdByWorktree: Record<string, string | null> // worktreeId -> last active file
|
||||
activeTabTypeByWorktree: Record<string, WorkspaceVisibleTabType> // worktreeId -> last active tab type
|
||||
activeTabType: WorkspaceVisibleTabType
|
||||
setActiveTabType: (type: WorkspaceVisibleTabType, worktreeId?: string) => void
|
||||
setActiveTabType: (type: WorkspaceVisibleTabType, worktreeId: string | null) => void
|
||||
openFile: (
|
||||
file: Omit<OpenFile, 'id' | 'isDirty'>,
|
||||
options?: {
|
||||
|
||||
@@ -161,7 +161,7 @@ export const createRecentlyClosedTabsSlice: StateCreator<
|
||||
if (snapshot.color) {
|
||||
get().setTabColor(tab.id, snapshot.color)
|
||||
}
|
||||
get().setActiveTabType('terminal')
|
||||
get().setActiveTabType('terminal', worktreeId)
|
||||
restoreRecentlyClosedTabPosition(get, worktreeId, tab.id, snapshot.position)
|
||||
return true
|
||||
},
|
||||
|
||||
@@ -43,7 +43,7 @@ export function createActiveWorkspaceTerminalActions(
|
||||
}
|
||||
const terminal = get().createTab(worktreeId, groupId)
|
||||
get().setActiveTab(terminal.id)
|
||||
get().setActiveTabType('terminal')
|
||||
get().setActiveTabType('terminal', worktreeId)
|
||||
const latest = get()
|
||||
const currentTerminals = latest.tabsByWorktree[worktreeId] ?? []
|
||||
const currentEditors = latest.openFiles.filter((file) => file.worktreeId === worktreeId)
|
||||
|
||||
@@ -243,7 +243,11 @@ export function createTerminalTabCreationActions(
|
||||
...s.layoutByWorktree,
|
||||
[worktreeId]: s.layoutByWorktree[worktreeId] ?? { type: 'leaf', groupId: group.id }
|
||||
},
|
||||
activeTabId: shouldActivate ? tab.id : orphanCleanupPatch.activeTabId,
|
||||
// Why: the global selection is the main window's; a tab in another worktree (or the floating workspace) activates only within its own group.
|
||||
activeTabId:
|
||||
shouldActivate && s.activeWorktreeId === worktreeId
|
||||
? tab.id
|
||||
: orphanCleanupPatch.activeTabId,
|
||||
activeTabIdByWorktree: {
|
||||
...orphanCleanupPatch.activeTabIdByWorktree,
|
||||
[worktreeId]: nextActiveTabIdForWorktree
|
||||
|
||||
@@ -207,7 +207,7 @@ async function createTerminalInNewSplitGroup(page: Page): Promise<SplitGroupTerm
|
||||
const tab = state.createTab(worktreeId, groupId, undefined, { activate: true })
|
||||
state.focusGroup(worktreeId, groupId)
|
||||
state.setActiveTab(tab.id)
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', window.__store?.getState().activeWorktreeId ?? null)
|
||||
return { sourceGroupId, groupId, tabId: tab.id }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ test.describe('Automation hidden terminal first mount', () => {
|
||||
}
|
||||
const state = store.getState()
|
||||
state.setActiveTab(tabId)
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', store.getState().activeWorktreeId)
|
||||
}, hiddenTabId)
|
||||
|
||||
await ensureTerminalVisible(orcaPage)
|
||||
|
||||
@@ -54,7 +54,7 @@ async function switchToBrowserTab(page: Page, worktreeId: string, browserTabId:
|
||||
return
|
||||
}
|
||||
state.setActiveBrowserTab(targetBrowserTabId)
|
||||
state.setActiveTabType('browser')
|
||||
state.setActiveTabType('browser', window.__store?.getState().activeWorktreeId ?? null)
|
||||
},
|
||||
{ targetWorktreeId: worktreeId, targetBrowserTabId: browserTabId }
|
||||
)
|
||||
|
||||
@@ -69,7 +69,7 @@ async function switchToTerminalTab(
|
||||
if (terminalTab) {
|
||||
state.setActiveTab(terminalTab.id)
|
||||
}
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', store.getState().activeWorktreeId)
|
||||
}, worktreeId)
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ async function switchToTerminal(
|
||||
if (terminalTab) {
|
||||
state.setActiveTab(terminalTab.id)
|
||||
}
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', store.getState().activeWorktreeId)
|
||||
}, worktreeId)
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ async function switchToEditor(
|
||||
const state = store.getState()
|
||||
if (state.openFiles.some((file) => file.id === targetFileId)) {
|
||||
state.setActiveFile(targetFileId)
|
||||
state.setActiveTabType('editor')
|
||||
state.setActiveTabType('editor', store.getState().activeWorktreeId)
|
||||
}
|
||||
}, fileId)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export async function createRemoteTerminalTab(page: Page, worktreeId: string): P
|
||||
}
|
||||
const tab = state.createTab(id, undefined, undefined, { activate: true })
|
||||
state.setActiveTab(tab.id)
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', window.__store?.getState().activeWorktreeId ?? null)
|
||||
return tab.id
|
||||
}, worktreeId)
|
||||
await expect
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function createAndActivateDockerSshRelayWorktree(
|
||||
if ((store.getState().tabsByWorktree[id] ?? []).length === 0) {
|
||||
store.getState().createTab(id)
|
||||
}
|
||||
store.getState().setActiveTabType('terminal')
|
||||
store.getState().setActiveTabType('terminal', store.getState().activeWorktreeId)
|
||||
}, worktreeId)
|
||||
return { worktreeId }
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ export async function createHostRendererTerminalTab(
|
||||
store.getState().setActiveWorktree(id)
|
||||
const tab = store.getState().createTab(id)
|
||||
store.getState().setActiveTab(tab.id)
|
||||
store.getState().setActiveTabType('terminal')
|
||||
store.getState().setActiveTabType('terminal', store.getState().activeWorktreeId)
|
||||
return tab.id
|
||||
}, worktreeId)
|
||||
await expect
|
||||
|
||||
@@ -99,7 +99,7 @@ export async function openPairedClientTab(
|
||||
state?.setActiveView('terminal')
|
||||
state?.setActiveWorktree(worktreeId)
|
||||
state?.setActiveTab(webTabId)
|
||||
state?.setActiveTabType('terminal')
|
||||
state?.setActiveTabType('terminal', window.__store?.getState().activeWorktreeId ?? null)
|
||||
},
|
||||
{ webTabId, worktreeId }
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ async function assertCreatedFileRendered(
|
||||
throw new Error(`Paired web editor did not open ${filePath}`)
|
||||
}
|
||||
state.setActiveFile(file.id)
|
||||
state.setActiveTabType('editor')
|
||||
state.setActiveTabType('editor', window.__store?.getState().activeWorktreeId ?? null)
|
||||
return file.id
|
||||
},
|
||||
{ fileName, filePath, worktreeId }
|
||||
@@ -47,7 +47,7 @@ async function assertCreatedFileRendered(
|
||||
await page.evaluate((id) => {
|
||||
const state = window.__store?.getState()
|
||||
state?.closeFile(id)
|
||||
state?.setActiveTabType('terminal')
|
||||
state?.setActiveTabType('terminal', window.__store?.getState().activeWorktreeId ?? null)
|
||||
}, fileId)
|
||||
await expect(page.locator('.editor-header-path').filter({ hasText: fileName })).toHaveCount(0)
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ export async function connectSshTestTarget(
|
||||
if (seedInitialTab && (store.getState().tabsByWorktree[worktree.id] ?? []).length === 0) {
|
||||
store.getState().createTab(worktree.id)
|
||||
}
|
||||
store.getState().setActiveTabType('terminal')
|
||||
store.getState().setActiveTabType('terminal', store.getState().activeWorktreeId)
|
||||
return {
|
||||
targetId: createdTarget.id,
|
||||
repoId: result.repo.id,
|
||||
|
||||
@@ -301,7 +301,7 @@ export async function ensureTerminalVisible(page: Page, timeoutMs = 10_000): Pro
|
||||
state.createTab(worktreeId)
|
||||
state.setActiveTab(activeTab.id)
|
||||
if (state.activeTabType !== 'terminal') {
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', window.__store?.getState().activeWorktreeId ?? null)
|
||||
}
|
||||
|
||||
state = store.getState()
|
||||
|
||||
@@ -34,7 +34,7 @@ async function createActiveTerminalTab(page: Page, worktreeId: string): Promise<
|
||||
const state = store.getState()
|
||||
const tab = state.createTab(worktreeId, undefined, undefined, { activate: true })
|
||||
state.setActiveTab(tab.id)
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', store.getState().activeWorktreeId)
|
||||
return tab.id
|
||||
}, worktreeId)
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ export async function focusActiveTerminalInput(page: Page): Promise<void> {
|
||||
throw new Error('No active terminal pane to focus')
|
||||
}
|
||||
state.setActiveTab(tabId)
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', window.__store?.getState().activeWorktreeId ?? null)
|
||||
pane.terminal.focus()
|
||||
const textarea = pane.container.querySelector(
|
||||
'.xterm-helper-textarea'
|
||||
|
||||
@@ -206,7 +206,7 @@ test('a cold-parked host pane keeps serving its paired remote viewer', async ({
|
||||
state?.setActiveView('terminal')
|
||||
state?.setActiveWorktree(worktreeId)
|
||||
state?.setActiveTab(tabId)
|
||||
state?.setActiveTabType('terminal')
|
||||
state?.setActiveTabType('terminal', window.__store?.getState().activeWorktreeId ?? null)
|
||||
},
|
||||
{ worktreeId, tabId: hostTabId }
|
||||
)
|
||||
@@ -234,7 +234,7 @@ test('a cold-parked host pane keeps serving its paired remote viewer', async ({
|
||||
state?.setActiveView('terminal')
|
||||
state?.setActiveWorktree(worktreeId)
|
||||
state?.setActiveTab(webTabId)
|
||||
state?.setActiveTabType('terminal')
|
||||
state?.setActiveTabType('terminal', window.__store?.getState().activeWorktreeId ?? null)
|
||||
},
|
||||
{ webTabId, worktreeId }
|
||||
)
|
||||
@@ -278,7 +278,7 @@ test('a cold-parked host pane keeps serving its paired remote viewer', async ({
|
||||
const tab = state?.createTab(id, undefined, undefined, { activate: true })
|
||||
if (tab) {
|
||||
state?.setActiveTab(tab.id)
|
||||
state?.setActiveTabType('terminal')
|
||||
state?.setActiveTabType('terminal', window.__store?.getState().activeWorktreeId ?? null)
|
||||
}
|
||||
}, worktreeId)
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ async function activateNewTerminalTab(page: Page, worktreeId: string): Promise<v
|
||||
const state = store.getState()
|
||||
const tab = state.createTab(id, undefined, undefined, { activate: true })
|
||||
state.setActiveTab(tab.id)
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', store.getState().activeWorktreeId)
|
||||
}, worktreeId)
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ async function activateTerminalTab(page: Page, tabId: string): Promise<void> {
|
||||
}
|
||||
const state = store.getState()
|
||||
state.setActiveTab(id)
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', store.getState().activeWorktreeId)
|
||||
}, tabId)
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ async function activateHubRepoTerminal(page: Page, repoId: string): Promise<stri
|
||||
if ((store.getState().tabsByWorktree[worktree.id] ?? []).length === 0) {
|
||||
store.getState().createTab(worktree.id)
|
||||
}
|
||||
store.getState().setActiveTabType('terminal')
|
||||
store.getState().setActiveTabType('terminal', worktree.id)
|
||||
return worktree.id
|
||||
}, repoId)
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@ async function activateTerminalTab(page: Page, tabId: string): Promise<void> {
|
||||
throw new Error('activateTerminalTab: window.__store is unavailable')
|
||||
}
|
||||
const state = store.getState()
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', state.activeWorktreeId)
|
||||
state.setActiveTab(targetTabId)
|
||||
}, tabId)
|
||||
await expect.poll(() => getActiveTabId(page), { timeout: 5_000 }).toBe(tabId)
|
||||
|
||||
@@ -299,7 +299,7 @@ async function run(args: {
|
||||
await page.evaluate(
|
||||
({ pageId, worktreeId }) => {
|
||||
const state = window.__store?.getState()
|
||||
state?.setActiveTabType('terminal')
|
||||
state?.setActiveTabType('terminal', window.__store?.getState().activeWorktreeId ?? null)
|
||||
state?.focusBrowserTabInWorktree(worktreeId, pageId, { surfacePane: true })
|
||||
},
|
||||
{ pageId, worktreeId }
|
||||
|
||||
@@ -54,7 +54,7 @@ async function openClientTab(page: Page, worktreeId: string, tabId: string): Pro
|
||||
state?.setActiveView('terminal')
|
||||
state?.setActiveWorktree(worktreeId)
|
||||
state?.setActiveTab(tabId)
|
||||
state?.setActiveTabType('terminal')
|
||||
state?.setActiveTabType('terminal', worktreeId)
|
||||
},
|
||||
{ tabId, worktreeId }
|
||||
)
|
||||
|
||||
@@ -186,7 +186,7 @@ async function selectClientTab(page: Page, worktreeId: string, webTabId: string)
|
||||
state?.setActiveView('terminal')
|
||||
state?.setActiveWorktree(worktreeId)
|
||||
state?.setActiveTab(webTabId)
|
||||
state?.setActiveTabType('terminal')
|
||||
state?.setActiveTabType('terminal', window.__store?.getState().activeWorktreeId ?? null)
|
||||
},
|
||||
{ webTabId, worktreeId }
|
||||
)
|
||||
|
||||
@@ -225,7 +225,7 @@ async function openClientTab(page: Page, worktreeId: string, webTabId: string):
|
||||
state?.setActiveView('terminal')
|
||||
state?.setActiveWorktree(worktreeId)
|
||||
state?.setActiveTab(webTabId)
|
||||
state?.setActiveTabType('terminal')
|
||||
state?.setActiveTabType('terminal', window.__store?.getState().activeWorktreeId ?? null)
|
||||
},
|
||||
{ worktreeId, webTabId }
|
||||
)
|
||||
|
||||
@@ -172,7 +172,7 @@ test('paints a nonempty lossy initial snapshot on a paired Electron client @head
|
||||
state?.setActiveView('terminal')
|
||||
state?.setActiveWorktree(worktreeId)
|
||||
state?.setActiveTab(tabId)
|
||||
state?.setActiveTabType('terminal')
|
||||
state?.setActiveTabType('terminal', window.__store?.getState().activeWorktreeId ?? null)
|
||||
},
|
||||
{ tabId: webTabId, worktreeId }
|
||||
)
|
||||
|
||||
@@ -171,7 +171,7 @@ async function openClientTab(page: Page, worktreeId: string, webTabId: string):
|
||||
state?.setActiveView('terminal')
|
||||
state?.setActiveWorktree(worktreeId)
|
||||
state?.setActiveTab(webTabId)
|
||||
state?.setActiveTabType('terminal')
|
||||
state?.setActiveTabType('terminal', worktreeId)
|
||||
},
|
||||
{ webTabId, worktreeId }
|
||||
)
|
||||
|
||||
@@ -83,7 +83,7 @@ async function openAgentTab(
|
||||
launchAgent
|
||||
})
|
||||
state.setActiveTab(tab.id)
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', store.getState().activeWorktreeId)
|
||||
return tab.id
|
||||
},
|
||||
{ worktreeId, launchAgent }
|
||||
|
||||
@@ -104,7 +104,7 @@ export async function connectDockerRemote(
|
||||
if ((store.getState().tabsByWorktree[worktree.id] ?? []).length === 0) {
|
||||
store.getState().createTab(worktree.id)
|
||||
}
|
||||
store.getState().setActiveTabType('terminal')
|
||||
store.getState().setActiveTabType('terminal', store.getState().activeWorktreeId)
|
||||
return worktree.id
|
||||
}, remote.repoId)
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ test.describe('SSH terminal hidden view parking', () => {
|
||||
await orcaPage.evaluate((tabId) => {
|
||||
const state = window.__store?.getState()
|
||||
state?.setActiveTab(tabId)
|
||||
state?.setActiveTabType('terminal')
|
||||
state?.setActiveTabType('terminal', window.__store?.getState().activeWorktreeId ?? null)
|
||||
}, sshTabId)
|
||||
await waitForActiveTerminalManager(orcaPage, 60_000)
|
||||
await expect
|
||||
|
||||
@@ -98,7 +98,7 @@ async function setActiveFile(
|
||||
|
||||
const state = store.getState()
|
||||
state.setActiveFile(id)
|
||||
state.setActiveTabType('editor')
|
||||
state.setActiveTabType('editor', state.activeWorktreeId)
|
||||
}, fileId)
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ async function activateTerminalTab(page: Page, tabId: string): Promise<void> {
|
||||
throw new Error('activateTerminalTab: window.__store is unavailable')
|
||||
}
|
||||
const state = store.getState()
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', state.activeWorktreeId)
|
||||
state.setActiveTab(targetTabId)
|
||||
}, tabId)
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ test.describe('Codex hidden startup composer background', () => {
|
||||
}
|
||||
const state = store.getState()
|
||||
state.setActiveTab(tabId)
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', store.getState().activeWorktreeId)
|
||||
}, hiddenTabId)
|
||||
await ensureTerminalVisible(orcaPage)
|
||||
await waitForActiveTerminalManager(orcaPage, 30_000)
|
||||
|
||||
@@ -61,7 +61,7 @@ test.describe('Terminal Codex runtime home', () => {
|
||||
})
|
||||
const tab = state.createTab(state.activeWorktreeId!)
|
||||
state.setActiveTab(tab.id)
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', window.__store?.getState().activeWorktreeId ?? null)
|
||||
},
|
||||
{ accountId, managedHomePath }
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@ async function createActiveTerminalTab(page: Page, worktreeId: string): Promise<
|
||||
const state = store.getState()
|
||||
const tab = state.createTab(id, undefined, undefined, { activate: true })
|
||||
state.setActiveTab(tab.id)
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', store.getState().activeWorktreeId)
|
||||
return tab.id
|
||||
}, worktreeId)
|
||||
await expect
|
||||
@@ -198,7 +198,7 @@ test.describe('cold worktree activation deferral', () => {
|
||||
await page.evaluate((tabId) => {
|
||||
const state = window.__store?.getState()
|
||||
state?.setActiveTab(tabId)
|
||||
state?.setActiveTabType('terminal')
|
||||
state?.setActiveTabType('terminal', window.__store?.getState().activeWorktreeId ?? null)
|
||||
}, deferredTabId)
|
||||
await expect
|
||||
.poll(async () => (await getMountedTabIds(page, [deferredTabId])).length, {
|
||||
|
||||
@@ -24,7 +24,7 @@ async function activateTerminalTab(page: Page, tabId: string): Promise<void> {
|
||||
await page.evaluate((id) => {
|
||||
const state = window.__store?.getState()
|
||||
state?.setActiveTab(id)
|
||||
state?.setActiveTabType('terminal')
|
||||
state?.setActiveTabType('terminal', state.activeWorktreeId)
|
||||
}, tabId)
|
||||
await expect
|
||||
.poll(() =>
|
||||
@@ -70,7 +70,7 @@ for (const exitMode of ['normal', 'sigkill'] as const) {
|
||||
const tab = state.createTab(worktreeId)
|
||||
state.queueTabStartupCommand(tab.id, { command })
|
||||
state.setActiveTab(tab.id)
|
||||
state.setActiveTabType('terminal')
|
||||
state.setActiveTabType('terminal', window.__store?.getState().activeWorktreeId ?? null)
|
||||
return tab.id
|
||||
},
|
||||
{ command }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user