fix(desktop): leave the entered project when switching profile or backend
A project id names a row in one backend's projects.db, but $projectScope survived profile and connection switches. The fresh draft resolved its cwd from the stale scope against the still-loaded old project tree, so a new chat in profile B started in profile A's project folder. Fixes #54990 Co-authored-by: Robin Rademacher <robinradx@gmail.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { $sessionsLimit, resetSessionsLimit, SIDEBAR_SESSIONS_PAGE_SIZE } from '@/store/layout'
|
||||
import { $projectScope, ALL_PROJECTS } from '@/store/project-scope'
|
||||
import {
|
||||
$activeSessionId,
|
||||
$cronSessions,
|
||||
@@ -100,6 +101,14 @@ describe('wipeSessionListsForGatewaySwitch', () => {
|
||||
expect($currentBranch.get()).toBe('')
|
||||
})
|
||||
|
||||
it("leaves the outgoing backend's project scope so the next draft cannot start in it (#54990)", () => {
|
||||
$projectScope.set('p_old_backend')
|
||||
|
||||
wipeSessionListsForGatewaySwitch()
|
||||
|
||||
expect($projectScope.get()).toBe(ALL_PROJECTS)
|
||||
})
|
||||
|
||||
it("forgets the previous backend's in-memory paging state", () => {
|
||||
const page = {
|
||||
messages: Array.from({ length: 120 }, (_, index) => ({
|
||||
|
||||
@@ -8,6 +8,7 @@ import { invalidateCronJobsRequests, setCronJobs } from '@/store/cron'
|
||||
import { resetSessionsLimit } from '@/store/layout'
|
||||
import { resetLiveSync } from '@/store/live-sync'
|
||||
import { invalidateProfileListFetches } from '@/store/profile'
|
||||
import { exitProjectScope } from '@/store/project-scope'
|
||||
import {
|
||||
$unreadFinishedSessionIds,
|
||||
setActiveSessionId,
|
||||
@@ -195,6 +196,9 @@ export function wipeSessionListsForGatewaySwitch(): void {
|
||||
// has never seen them, so drop the "already pushed" bookkeeping and let the
|
||||
// next reconcile re-assert the whole set against the new backend.
|
||||
resetSessionPinMirror()
|
||||
// Project ids belong to the outgoing backend's projects.db; a scope left
|
||||
// entered would root the next draft's cwd in the old source's project.
|
||||
exitProjectScope()
|
||||
setSessions([])
|
||||
setSessionProfilesTruncated({})
|
||||
setSessionProfilesUsage({})
|
||||
|
||||
@@ -16,6 +16,7 @@ const resetStarmapGraph = vi.fn()
|
||||
|
||||
vi.mock('@/store/gateway', () => ({
|
||||
$gateway,
|
||||
activeGateway: () => null,
|
||||
activeGatewayConnectionId: () => null,
|
||||
// Activation now verifies the socket's route before publishing the profile.
|
||||
activeGatewayProfileKey: () => ensureGatewayForProfile.mock.lastCall?.[0] ?? $activeGatewayProfile.get(),
|
||||
@@ -44,10 +45,15 @@ const {
|
||||
$profiles,
|
||||
ensureGatewayProfile,
|
||||
invalidateProfileListFetches,
|
||||
newSessionInProfile,
|
||||
prewarmProfileBackend,
|
||||
refreshProfiles
|
||||
refreshProfiles,
|
||||
selectProfile
|
||||
} = await import('./profile')
|
||||
|
||||
const { $projectScope, ALL_PROJECTS } = await import('./project-scope')
|
||||
const { $projectTree, resolveNewSessionCwd } = await import('./projects')
|
||||
|
||||
const { $poolLimits } = await import('@/store/pool-limits')
|
||||
const { $connectionsRegistry } = await import('@/store/connection-registry-state')
|
||||
|
||||
@@ -375,3 +381,38 @@ describe('stale profile-list fetches across a backend switch (#85731)', () => {
|
||||
expect($profiles.get().map(profile => profile.name)).toEqual(['default', 'coder'])
|
||||
})
|
||||
})
|
||||
|
||||
describe("profile switch leaves the previous profile's project (#54990)", () => {
|
||||
const enterDefaultProfileProject = () => {
|
||||
$projectTree.set([{ id: 'p_app1', label: 'app1', path: '/work/app1', repos: [] } as never])
|
||||
$projectScope.set('p_app1')
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
$projectScope.set(ALL_PROJECTS)
|
||||
$projectTree.set([])
|
||||
})
|
||||
|
||||
it.each([
|
||||
['selectProfile', selectProfile],
|
||||
['newSessionInProfile', newSessionInProfile]
|
||||
])('%s to another profile does not root the fresh draft in the old project', (_name, open) => {
|
||||
enterDefaultProfileProject()
|
||||
expect(resolveNewSessionCwd()).toBe('/work/app1')
|
||||
|
||||
// The gateway swap is async: the old profile's project tree is still loaded
|
||||
// when the fresh draft resolves its cwd.
|
||||
open('sinan')
|
||||
|
||||
expect($projectScope.get()).toBe(ALL_PROJECTS)
|
||||
expect(resolveNewSessionCwd()).not.toBe('/work/app1')
|
||||
})
|
||||
|
||||
it('keeps the entered project when the draft stays on the active profile', () => {
|
||||
enterDefaultProfileProject()
|
||||
|
||||
newSessionInProfile('default')
|
||||
|
||||
expect(resolveNewSessionCwd()).toBe('/work/app1')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import { notifyError } from '@/store/notifications'
|
||||
import { $poolLimits } from '@/store/pool-limits'
|
||||
import { notifyRemoteOverrideAuthFailure } from '@/store/profile-remote-override'
|
||||
import { exitProjectScope } from '@/store/project-scope'
|
||||
import { $connection, clearComposerSelectionOwner, setComposerSelectionOwner, setConnection } from '@/store/session'
|
||||
import type { SessionOwnerRoute } from '@/store/session-request-router'
|
||||
import { resetStarmapGraph } from '@/store/starmap'
|
||||
@@ -955,6 +956,7 @@ export function selectProfile(name: string): void {
|
||||
captureNewChatSource(profilePickConnectionId(target))
|
||||
|
||||
if (switching) {
|
||||
leaveForeignProjectScope(target)
|
||||
requestFreshSession()
|
||||
}
|
||||
|
||||
@@ -1025,6 +1027,16 @@ function activateOnCurrentSource(target: string): Promise<void> {
|
||||
return connectionId ? ensureGatewayAgent(connectionId, target) : ensureGatewayProfile(target)
|
||||
}
|
||||
|
||||
// A project id names a row in ONE backend's projects.db. A draft headed for
|
||||
// another profile (or source) must not resolve its cwd from the scope entered on
|
||||
// the current one: the fresh draft runs before the gateway swap refreshes the
|
||||
// project tree, so it would start in the previous profile's project (#54990).
|
||||
function leaveForeignProjectScope(profile: string, connectionId: null | string = activeGatewayConnectionId()): void {
|
||||
if (profile !== normalizeProfileKey($activeGatewayProfile.get()) || connectionId !== activeGatewayConnectionId()) {
|
||||
exitProjectScope()
|
||||
}
|
||||
}
|
||||
|
||||
// Pin the next new chat to `name` (legacy profile-only door) so session.create
|
||||
// reads the profile the user clicked "+" under, not whatever
|
||||
// $activeGatewayProfile holds once an in-flight profile swap settles (#79005).
|
||||
@@ -1045,6 +1057,7 @@ export function pinNewChatProfile(name: string): string {
|
||||
// message lands in the right place.
|
||||
export function newSessionInProfile(name: string): void {
|
||||
const target = pinNewChatProfile(name)
|
||||
leaveForeignProjectScope(target)
|
||||
requestFreshSession()
|
||||
// #81094: surface the failed dial instead of failing silently.
|
||||
void activateOnCurrentSource(target).catch((error: unknown) => {
|
||||
@@ -1072,6 +1085,7 @@ export function newSessionInAgent(route: AgentProfileRoute): void {
|
||||
$newChatProfile.set(captured.profile)
|
||||
$newChatRoute.set(captured)
|
||||
captureNewChatSource(captured.connectionId)
|
||||
leaveForeignProjectScope(captured.profile, captured.connectionId)
|
||||
requestFreshSession()
|
||||
// #81094: surface the failed dial instead of failing silently.
|
||||
void ensureGatewayAgent(captured.connectionId, captured.profile).catch((error: unknown) => {
|
||||
|
||||
Reference in New Issue
Block a user