From 92c2aaeb6adf2ad658cd3e767da554a15a40ce55 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:40:40 -0700 Subject: [PATCH] fix structured completion focus scope --- .../src/hooks/ipc-events-test-harness.ts | 18 +++++- ...tructured-session-completion-focus.test.ts | 60 +++++++++++++++++++ src/renderer/src/hooks/useIpcEvents.ts | 3 + 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/hooks/structured-session-completion-focus.test.ts diff --git a/src/renderer/src/hooks/ipc-events-test-harness.ts b/src/renderer/src/hooks/ipc-events-test-harness.ts index e2226460a7b..ea2e9e2dc9f 100644 --- a/src/renderer/src/hooks/ipc-events-test-harness.ts +++ b/src/renderer/src/hooks/ipc-events-test-harness.ts @@ -121,6 +121,7 @@ export type IpcEventsHarness = { useIpcEvents: () => void createTerminal: (request: CreateTerminalRequest) => void requestTerminalCreate: (request: RequestTerminalCreateRequest) => void + focusEditorTab: (request: { tabId: string; worktreeId: string }) => void replyTerminalCreate: ReturnType /** Fire a main-process digit chord (zero-based index). */ jumpToWorktreeIndex: (index: number) => void @@ -136,8 +137,7 @@ export type IpcEventsHarnessOptions = { } /** - * Loads useIpcEvents against a stubbed preload API and returns a driver for the - * create-terminal IPC, so reveal/adoption behavior is asserted through the hook. + * Loads useIpcEvents against a stubbed preload API so IPC behavior is asserted through the hook. */ export async function loadIpcEventsHarness( storeState: HarnessStoreState, @@ -147,6 +147,8 @@ export async function loadIpcEventsHarness( const activateAndRevealWorkspace = vi.fn() let createTerminalListener: ((request: CreateTerminalRequest) => void) | null = null let requestTerminalCreateListener: ((request: RequestTerminalCreateRequest) => void) | null = null + let focusEditorTabListener: ((request: { tabId: string; worktreeId: string }) => void) | null = + null let navigationUpdateListener: | ((event: { browserPageId: string; url: string; title: string }) => void) | null = null @@ -209,6 +211,12 @@ export async function loadIpcEventsHarness( requestTerminalCreateListener = listener return () => {} }, + onFocusEditorTab: ( + listener: (request: { tabId: string; worktreeId: string }) => void + ) => { + focusEditorTabListener = listener + return () => {} + }, onJumpToWorktreeIndex: (listener: (index: number) => void) => { indexJumpListeners.set('worktree', listener) return () => {} @@ -279,6 +287,12 @@ export async function loadIpcEventsHarness( } requestTerminalCreateListener(request) }, + focusEditorTab: (request) => { + if (typeof focusEditorTabListener !== 'function') { + throw new Error('Expected the focus-editor-tab listener to be registered') + } + focusEditorTabListener(request) + }, replyTerminalCreate, jumpToWorktreeIndex: (index) => fireIndexJump(indexJumpListeners, 'worktree', index), jumpToTabIndex: (index) => fireIndexJump(indexJumpListeners, 'tab', index), diff --git a/src/renderer/src/hooks/structured-session-completion-focus.test.ts b/src/renderer/src/hooks/structured-session-completion-focus.test.ts new file mode 100644 index 00000000000..dfadee2c874 --- /dev/null +++ b/src/renderer/src/hooks/structured-session-completion-focus.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it, vi } from 'vitest' +import { + createHarnessStoreState, + loadIpcEventsHarness, + type HarnessStoreState +} from './ipc-events-test-harness' + +const SESSION_WORKSPACE_ID = 'repo-1::/session-workspace' +const OTHER_WORKSPACE_ID = 'repo-1::/other-workspace' +const TAB_ID = 'structured-agent-session-session-1' + +function createStoreState(activeWorktreeId: string): HarnessStoreState { + return createHarnessStoreState({ + tabsByWorktree: {}, + activeWorktreeId, + unifiedTabsByWorktree: { + [SESSION_WORKSPACE_ID]: [ + { + id: TAB_ID, + entityId: 'session-1', + groupId: 'group-1', + worktreeId: SESSION_WORKSPACE_ID, + contentType: 'agent-session' + } + ] + }, + focusGroup: vi.fn(), + activateTab: vi.fn() + }) +} + +describe('structured session completion focus', () => { + it('focuses the chat tab when its workspace is active', async () => { + const store = createStoreState(SESSION_WORKSPACE_ID) + const harness = await loadIpcEventsHarness(store) + harness.useIpcEvents() + + harness.focusEditorTab({ tabId: TAB_ID, worktreeId: SESSION_WORKSPACE_ID }) + + expect(store.focusGroup).toHaveBeenCalledWith(SESSION_WORKSPACE_ID, 'group-1') + expect(store.activateTab).toHaveBeenCalledWith(TAB_ID) + expect(store.setActiveTabType).toHaveBeenCalledWith('agent-session') + }) + + it('does not apply focus after the user moves to another workspace', async () => { + const store = createStoreState(OTHER_WORKSPACE_ID) + const harness = await loadIpcEventsHarness(store) + harness.useIpcEvents() + + harness.focusEditorTab({ tabId: TAB_ID, worktreeId: SESSION_WORKSPACE_ID }) + + expect(store.setActiveWorktree).not.toHaveBeenCalled() + expect(store.markWorktreeVisited).not.toHaveBeenCalled() + expect(store.setActiveView).not.toHaveBeenCalled() + expect(store.focusGroup).not.toHaveBeenCalled() + expect(store.activateTab).not.toHaveBeenCalled() + expect(store.setActiveTabType).not.toHaveBeenCalled() + expect(store.revealWorktreeInSidebar).not.toHaveBeenCalled() + }) +}) diff --git a/src/renderer/src/hooks/useIpcEvents.ts b/src/renderer/src/hooks/useIpcEvents.ts index ce6a027af40..4c2eccb57e5 100644 --- a/src/renderer/src/hooks/useIpcEvents.ts +++ b/src/renderer/src/hooks/useIpcEvents.ts @@ -1867,6 +1867,9 @@ export function useIpcEvents(): void { (item) => item.id === tabId ) const browserTarget = resolveBrowserSessionTabTarget(store, worktreeId, tabId) + if (tab?.contentType === 'agent-session' && store.activeWorktreeId !== worktreeId) { + return + } if (!tab) { if (browserTarget) { // Why: older/mobile fallback snapshots identify browser tabs by workspace id when no unified tab wrapper exists.