diff --git a/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx b/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx index 093ef8feeb..3a46e8b62d 100644 --- a/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx @@ -1437,6 +1437,7 @@ describe('branchStoredSession desktop source tagging', () => { navigate={navigate} onReady={branch => (branchStoredSession = branch)} requestGateway={requestGateway} + selectedStoredSessionId="stored-parent" /> ) await waitFor(() => expect(branchStoredSession).not.toBeNull()) @@ -1451,6 +1452,56 @@ describe('branchStoredSession desktop source tagging', () => { expect($sessionTiles.get().some(tile => tile.storedSessionId === 'branch-stored')).toBe(false) }) + it('keeps the current view when branching a different session from the sidebar (does not reintroduce #69750)', async () => { + const requestGateway = vi.fn(async (method: string) => { + if (method === 'session.create') { + return { session_id: 'branch-runtime', stored_session_id: 'branch-stored' } as never + } + + if (method === 'session.resume') { + return { + info: {}, + message_count: 0, + messages: [], + resumed: 'branch-stored', + session_id: 'branch-runtime', + session_key: 'branch-stored' + } as never + } + + return {} as never + }) + + // The user is looking at "stored-other" and right-clicks a DIFFERENT, + // unrelated session ("stored-parent") in the sidebar to branch it. + setSessions([storedSession({ id: 'stored-parent', message_count: 1 })]) + setSelectedStoredSessionId('stored-other') + vi.mocked(getAllSessionMessages).mockResolvedValue({ + messages: [{ content: 'branch me', role: 'user', timestamp: 1 }], + session_id: 'stored-parent' + } as never) + + const navigate = vi.fn() + let branchStoredSession: ((storedSessionId: string) => Promise) | null = null + render( + (branchStoredSession = branch)} + requestGateway={requestGateway} + selectedStoredSessionId="stored-other" + /> + ) + await waitFor(() => expect(branchStoredSession).not.toBeNull()) + + await expect(branchStoredSession!('stored-parent')).resolves.toBe(true) + + // Branching a session that is not the one currently open must not steal + // the user's active view — "stored-other" stays selected. + expect($selectedStoredSessionId.get()).toBe('stored-other') + // The branch instead opens as its own tile. + expect($sessionTiles.get().some(tile => tile.storedSessionId === 'branch-stored')).toBe(true) + }) + it('tags desktop branch sessions as desktop sessions', async () => { let createParams: Record | undefined diff --git a/apps/desktop/src/app/session/hooks/use-session-actions/index.ts b/apps/desktop/src/app/session/hooks/use-session-actions/index.ts index 5001c3f6d3..cf913713f0 100644 --- a/apps/desktop/src/app/session/hooks/use-session-actions/index.ts +++ b/apps/desktop/src/app/session/hooks/use-session-actions/index.ts @@ -1656,11 +1656,20 @@ export function useSessionActions({ updateSessionState(branched.session_id, state => ({ ...state, ...runtimeInfo }), routedSessionId) } - // Load the branch as the primary session so it opens in the main - // workspace, not just a sidebar row. resumeSession reuses the runtime - // warm-cached above (ensureSessionState/updateSessionState) instead of - // an extra resume RPC. - await resumeSession(routedSessionId) + // Only take over the main pane when the chat being branched is the one + // already open there — branching a background/sidebar session must + // not yank the user's current view away from what they're looking at + // (the #69750 focus-stealing bug, reintroduced if this fires + // unconditionally). resumeSession reuses the runtime warm-cached above + // (ensureSessionState/updateSessionState) instead of an extra resume RPC. + if (parentStoredId !== null && selectedStoredSessionIdRef.current === parentStoredId) { + await resumeSession(routedSessionId) + } else { + openSessionTile(routedSessionId, 'center') + patchSessionTile(routedSessionId, { runtimeId: branched.session_id }) + revealTreePane(`session-tile:${routedSessionId}`) + } + broadcastSessionsChanged() return true @@ -1674,7 +1683,15 @@ export function useSessionActions({ }, 0) } }, - [copy, creatingSessionRef, ensureSessionState, requestGateway, resumeSession, updateSessionState] + [ + copy, + creatingSessionRef, + ensureSessionState, + requestGateway, + resumeSession, + selectedStoredSessionIdRef, + updateSessionState + ] ) // Branch the open chat — optionally from a specific message — off its live transcript.