Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions apps/desktop/src/app/session/hooks/use-session-actions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1612,6 +1612,55 @@ describe('branchStoredSession desktop source tagging', () => {
expect($sessionTiles.get().some(tile => tile.storedSessionId === 'branch-stored')).toBe(false)
})

it('routes the main workspace to the branch session (branch window renders without a manual away-and-back)', 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
})

setSessions([storedSession({ id: 'stored-parent', message_count: 1 })])
setSelectedStoredSessionId('stored-parent')
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-parent"
/>
)
await waitFor(() => expect(branchStoredSession).not.toBeNull())

await expect(branchStoredSession!('stored-parent')).resolves.toBe(true)

// resumeSession swaps the selection but never the URL. Without a navigate
// here the route stays on the PARENT while the selection is the CHILD, so
// isRouteSessionMismatch stays true: the transcript is suppressed and the
// routed loader never retires — the branched window stays blank until the
// user switches away and back. The URL must follow the selection.
expect(navigate).toHaveBeenCalledWith(sessionRoute('branch-stored'), { replace: true })
})

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') {
Expand Down
11 changes: 11 additions & 0 deletions apps/desktop/src/app/session/hooks/use-session-actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2022,6 +2022,16 @@ export function useSessionActions({
// (ensureSessionState/updateSessionState) instead of an extra resume RPC.
if (parentStoredId !== null && selectedStoredSessionIdRef.current === parentStoredId) {
await resumeSession(routedSessionId)
// resumeSession swaps the selection but never touches the URL — its
// `replaceRoute` parameter is accepted and ignored. The route still
// points at the PARENT, so isRouteSessionMismatch (route = parent,
// selection = child) stays true: ChatRuntimeBoundary keeps the
// transcript suppressed and the routed loader never retires, so the
// branched window shows an empty spinner until the user switches
// away and back (a sidebar click routes properly via openSession).
// Navigate here, same tick as the selection swap, so route and
// selection land atomically.
navigate(sessionRoute(routedSessionId), { replace: true })
} else {
openSessionTile(routedSessionId, 'center')
patchSessionTile(routedSessionId, { runtimeId: branched.session_id })
Expand All @@ -2045,6 +2055,7 @@ export function useSessionActions({
copy,
creatingSessionRef,
ensureSessionState,
navigate,
requestGateway,
resumeSession,
selectedStoredSessionIdRef,
Expand Down
Loading