diff --git a/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.focus.test.ts b/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.focus.test.ts index be8688751..01e772f4c 100644 --- a/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.focus.test.ts +++ b/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.focus.test.ts @@ -133,6 +133,7 @@ function resetStore(): void { reconcileWorktreeTabModel: vi.fn(() => ({ renderableTabCount: 0 })), settings: { activeRuntimeEnvironmentId: null }, tabsByWorktree: { 'wt-1': [terminalTab] }, + terminalLayoutsByTabId: {}, unifiedTabsByWorktree: { 'wt-1': [unifiedTab] }, activateTab: mocks.activateTab, closeBrowserTab: mocks.closeBrowserTab, @@ -186,7 +187,35 @@ describe('useTabGroupWorkspaceModel terminal activation focus', () => { expect(mocks.activateTab).toHaveBeenCalledWith('unified-terminal-1') expect(mocks.setActiveTab).toHaveBeenCalledWith('terminal-1') expect(mocks.setActiveTabType).toHaveBeenCalledWith('terminal') - expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith('terminal-1') + expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith('terminal-1', null) + }) + + it('returns keyboard focus to the active split pane leaf when a terminal tab is activated', async () => { + storeBox.state = { + ...storeBox.state, + terminalLayoutsByTabId: { + 'terminal-1': { + activeLeafId: 'right-leaf', + ptyIdsByLeafId: { + 'left-leaf': 'pty-left', + 'right-leaf': 'pty-right' + }, + root: { + type: 'split', + direction: 'horizontal', + first: { type: 'leaf', leafId: 'left-leaf' }, + second: { type: 'leaf', leafId: 'right-leaf' } + }, + expandedLeafId: null + } + } + } + const { useTabGroupWorkspaceModel } = await import('./useTabGroupWorkspaceModel') + const model = useTabGroupWorkspaceModel({ groupId: 'group-1', worktreeId: 'wt-1' }) + + model.commands.activateTerminal('terminal-1') + + expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith('terminal-1', 'right-leaf') }) it('toggles pane expansion from the split-group tab bar collapse button', async () => { diff --git a/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts b/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts index a723a4576..fe50561d2 100644 --- a/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts +++ b/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts @@ -44,6 +44,9 @@ const EMPTY_GROUPS: readonly TabGroup[] = [] const EMPTY_UNIFIED_TABS: readonly Tab[] = [] const EMPTY_BROWSER_TABS: readonly BrowserTabState[] = [] const EMPTY_TERMINAL_TABS: readonly TerminalTab[] = [] +const EMPTY_TERMINAL_LAYOUTS_BY_TAB_ID: NonNullable< + ReturnType['terminalLayoutsByTabId'] +> = {} type TerminalTabItem = TerminalTab & { unifiedTabId: string } @@ -67,6 +70,7 @@ export function useTabGroupWorkspaceModel({ openFiles: state.openFiles, browserTabs: state.browserTabsByWorktree[worktreeId] ?? EMPTY_BROWSER_TABS, expandedPaneByTabId: state.expandedPaneByTabId, + terminalLayoutsByTabId: state.terminalLayoutsByTabId ?? EMPTY_TERMINAL_LAYOUTS_BY_TAB_ID, generatedTabTitlesEnabled: state.settings?.tabAutoGenerateTitle === true, mobileEmulatorEnabled: state.settings?.mobileEmulatorEnabled !== false })) @@ -376,11 +380,21 @@ export function useTabGroupWorkspaceModel({ } setActiveTab(terminalId) setActiveTabType('terminal') - // Why: clicking the tab button gives the browser focus to the tab strip - // after pointerdown; explicitly return it to xterm on the next frames. - focusTerminalTabSurface(terminalId) + const activeLeafId = worktreeState.terminalLayoutsByTabId[terminalId]?.activeLeafId ?? null + // Why: split terminal tab activation must restore xterm focus to the + // store-active leaf so keyboard input cannot drift to a sibling pane. + focusTerminalTabSurface(terminalId, activeLeafId) }, - [activateTab, focusGroup, groupId, groupTabs, setActiveTab, setActiveTabType, worktreeId] + [ + activateTab, + focusGroup, + groupId, + groupTabs, + setActiveTab, + setActiveTabType, + worktreeState.terminalLayoutsByTabId, + worktreeId + ] ) const toggleTerminalPaneExpand = useCallback(