fix(desktop): scope branch-opens-primary to the currently selected session
forkBranch was unconditionally routing every branched session into the main pane via resumeSession, including sidebar/background branches of a session the user isn't currently viewing. That reintroduces the #69750 focus-stealing bug for that path: branching a different session from the sidebar yanked the active view away from whatever was open. Only take over the main pane when the branch's parent is the session already selected; otherwise keep opening it as its own tile.
This commit is contained in:
@@ -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<boolean>) | null = null
|
||||
render(
|
||||
<BranchHarness
|
||||
navigate={navigate}
|
||||
onReady={branch => (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<string, unknown> | undefined
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user