fix(desktop): pin the project '+' new chat to the profile its tree is shown under

workspace-session-target.ts never set $newChatProfile, so a new session
started from a project's "+" reached desktopSessionCreateParams with no
intent and fell back to $activeGatewayProfile — which an in-flight profile
swap can move between the click and Send, landing session.create on the
wrong backend (#79005 flaw 3, second path). Pin the tree's profile
(projectProfile()) via the same intent write newSessionInProfile uses.

Fixes #79005
This commit is contained in:
Teknium
2026-09-02 04:25:24 -07:00
parent 9189e42842
commit d9b051a9d6
4 changed files with 47 additions and 6 deletions

View File

@@ -1,5 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { $activeGatewayProfile, $newChatProfile } from '@/store/profile'
import { $projectScope, $projectTree, ALL_PROJECTS } from '@/store/projects'
import {
$currentBranch,
@@ -22,6 +23,8 @@ describe('startWorkspaceSession', () => {
setNewChatWorkspaceTarget(undefined)
$projectScope.set(ALL_PROJECTS)
$projectTree.set([])
$activeGatewayProfile.set('default')
$newChatProfile.set(null)
vi.restoreAllMocks()
})
@@ -107,4 +110,22 @@ describe('startWorkspaceSession', () => {
expect($newChatWorkspaceTarget.get()).toBeNull()
expect($currentCwd.get()).toBe('')
})
// #79005 flaw 3: the project "+" must pin the profile the tree is shown
// under; otherwise session.create reads $activeGatewayProfile after a swap.
it('pins the new chat to the profile the project tree is displayed under', () => {
$activeGatewayProfile.set('work')
$newChatProfile.set(null)
startWorkspaceSession({
activeSessionIdRef: { current: null },
path: '/workspace-work',
requestGateway: vi.fn(() => new Promise<never>(() => {})),
startFreshSessionDraft: vi.fn()
})
$activeGatewayProfile.set('personal')
expect($newChatProfile.get()).toBe('work')
})
})

View File

@@ -1,6 +1,7 @@
import type { MutableRefObject } from 'react'
import { followActiveSessionCwd, resolveNewSessionCwd } from '@/store/projects'
import { pinNewChatProfile } from '@/store/profile'
import { followActiveSessionCwd, projectProfile, resolveNewSessionCwd } from '@/store/projects'
import {
$newChatWorkspaceTargetGeneration,
type NewChatWorkspaceTarget,
@@ -26,6 +27,16 @@ export function startWorkspaceSession({
requestGateway,
startFreshSessionDraft
}: WorkspaceSessionOptions): void {
// The project tree is rendered under one profile; the "+" belongs to it.
// Pin that intent now — otherwise desktopSessionCreateParams falls back to
// $activeGatewayProfile, which a still-settling profile swap can move
// between this click and Send (#79005). All-profiles view has no owner.
const profile = projectProfile()
if (profile) {
pinNewChatProfile(profile)
}
// Home's "+" passes path=null on purpose ("no folder"). That must stay
// detached — do NOT fall through to resolveNewSessionCwd(), which can still
// return a default/remembered project folder and re-attach the last repo

View File

@@ -857,6 +857,18 @@ function activateOnCurrentSource(target: string): Promise<void> {
return connectionId ? ensureGatewayAgent(connectionId, target) : ensureGatewayProfile(target)
}
// 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).
export function pinNewChatProfile(name: string): string {
const target = normalizeProfileKey(name)
$newChatProfile.set(target)
$newChatRoute.set(null)
captureNewChatSource(profilePickConnectionId(target))
return target
}
// Start a fresh session in `name` WITHOUT collapsing the "All profiles" browse
// view. Unlike selectProfile, it leaves $showAllProfiles untouched, so the
// unified sidebar stays put — used by the per-profile "+" in the all-profiles
@@ -864,10 +876,7 @@ function activateOnCurrentSource(target: string): Promise<void> {
// is in. Points new chats at the profile and opens its backend so the next
// message lands in the right place.
export function newSessionInProfile(name: string): void {
const target = normalizeProfileKey(name)
$newChatProfile.set(target)
$newChatRoute.set(null)
captureNewChatSource(profilePickConnectionId(target))
const target = pinNewChatProfile(name)
requestFreshSession()
// #81094: surface the failed dial instead of failing silently.
void activateOnCurrentSource(target).catch((error: unknown) => {

View File

@@ -349,7 +349,7 @@ async function gatewayRequest<T>(method: string, params: Record<string, unknown>
return gateway.request<T>(method, params)
}
function projectProfile(): null | string {
export function projectProfile(): null | string {
const profile = normalizeProfileKey($activeGatewayProfile.get())
return $profileScope.get() === ALL_PROFILES || profile === ALL_PROFILES ? null : profile