fix(desktop): gate the session.info branch write on the selected session
0401e08884 made the session.info cwd write require the event to describe
the selected stored session, but the branch write a few lines below
stayed unconditional. A background Kanban worker's runtime update that
reached the pane's active-runtime path could still flip the composer's
coding-rail branch to the worker's PR worktree while the default chat
stayed selected. cwd and branch now share one identity check.
Co-authored-by: ClintonEmok <54935030+ClintonEmok@users.noreply.github.com>
This commit is contained in:
@@ -4,10 +4,12 @@ import type { ClientSessionState } from '@/app/types'
|
||||
import { createClientSessionState } from '@/lib/chat-runtime'
|
||||
import {
|
||||
$activeSessionId,
|
||||
$currentBranch,
|
||||
$currentCwd,
|
||||
$selectedStoredSessionId,
|
||||
$workspaceCwdOwner,
|
||||
releaseWorkspaceCwdOwner,
|
||||
setCurrentBranch,
|
||||
setCurrentCwd
|
||||
} from '@/store/session'
|
||||
|
||||
@@ -19,11 +21,13 @@ import type { GatewayEventContext } from './types'
|
||||
// still carries a real cwd.
|
||||
function sessionInfoEvent({
|
||||
activeSessionId,
|
||||
branch,
|
||||
cwd,
|
||||
explicitSid = '',
|
||||
storedSessionId = ''
|
||||
}: {
|
||||
activeSessionId: null | string
|
||||
branch?: string
|
||||
cwd: string
|
||||
explicitSid?: string
|
||||
storedSessionId?: string
|
||||
@@ -49,7 +53,7 @@ function sessionInfoEvent({
|
||||
fromActiveSource: () => true,
|
||||
isActiveEvent: !!sessionId && sessionId === activeSessionId,
|
||||
occurredAt: Date.now() / 1000,
|
||||
payload: { cwd, stored_session_id: storedSessionId },
|
||||
payload: { branch, cwd, stored_session_id: storedSessionId },
|
||||
scheduleConfigRefresh: vi.fn(),
|
||||
sessionId
|
||||
} as unknown as GatewayEventContext
|
||||
@@ -60,12 +64,14 @@ describe('handleSessionInfoEvent workspace ownership', () => {
|
||||
$selectedStoredSessionId.set(null)
|
||||
$workspaceCwdOwner.set(null)
|
||||
setCurrentCwd('')
|
||||
setCurrentBranch('')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
$selectedStoredSessionId.set(null)
|
||||
$workspaceCwdOwner.set(null)
|
||||
setCurrentCwd('')
|
||||
setCurrentBranch('')
|
||||
})
|
||||
|
||||
// #55831 / the "workspace pane visible with no agent selected" report: with
|
||||
@@ -109,6 +115,43 @@ describe('handleSessionInfoEvent workspace ownership', () => {
|
||||
expect($workspaceCwdOwner.get()).toBe('selected-session')
|
||||
})
|
||||
|
||||
// #92888: a background Kanban worker's runtime update reaches the pane's
|
||||
// active-runtime path while the default Bot Chat stays selected. It names the
|
||||
// worker's own stored session and its PR worktree; neither the path nor the
|
||||
// branch may move onto the composer, while the selected chat's own update
|
||||
// still publishes both.
|
||||
it("keeps another session's worktree cwd and branch off the selected chat's composer", () => {
|
||||
$selectedStoredSessionId.set('default-bot-chat')
|
||||
setCurrentCwd('/repo/main-checkout')
|
||||
setCurrentBranch('main')
|
||||
|
||||
handleSessionInfoEvent(
|
||||
sessionInfoEvent({
|
||||
activeSessionId: 'runtime-1',
|
||||
branch: 'kanban/pr-42',
|
||||
cwd: '/repo/.worktrees/pr-42',
|
||||
explicitSid: 'runtime-1',
|
||||
storedSessionId: 'kanban-worker'
|
||||
})
|
||||
)
|
||||
|
||||
expect($currentCwd.get()).toBe('/repo/main-checkout')
|
||||
expect($currentBranch.get()).toBe('main')
|
||||
|
||||
handleSessionInfoEvent(
|
||||
sessionInfoEvent({
|
||||
activeSessionId: 'runtime-1',
|
||||
branch: 'feature/mine',
|
||||
cwd: '/repo/main-checkout',
|
||||
explicitSid: 'runtime-1',
|
||||
storedSessionId: 'default-bot-chat'
|
||||
})
|
||||
)
|
||||
|
||||
expect($currentBranch.get()).toBe('feature/mine')
|
||||
expect($workspaceCwdOwner.get()).toBe('default-bot-chat')
|
||||
})
|
||||
|
||||
it('keeps runtime state identity when a heartbeat only restates cached fields', () => {
|
||||
const original = {
|
||||
...createClientSessionState('stored-1'),
|
||||
|
||||
@@ -40,7 +40,8 @@ import type { GatewayEventContext } from './types'
|
||||
|
||||
/**
|
||||
* Whether a `session.info` payload's `stored_session_id` may be treated as the
|
||||
* selected conversation's, so its cwd can be claimed for it (#71254).
|
||||
* selected conversation's, so its cwd and branch can be claimed for it
|
||||
* (#71254, #92888).
|
||||
*
|
||||
* Absent is not the same as different: the backend omits the id on a
|
||||
* not-yet-built (`lazy`) session, and refusing there would leave the workspace
|
||||
@@ -199,10 +200,16 @@ export function handleSessionInfoEvent(ctx: GatewayEventContext): boolean {
|
||||
// Active-session model/provider still flows through the session state
|
||||
// cache via updateSessionState → syncRuntimeMetadataToView below.
|
||||
|
||||
if (
|
||||
typeof payload?.cwd === 'string' &&
|
||||
sessionInfoDescribesSelectedSession(payload.stored_session_id, isActiveEvent || rebound)
|
||||
) {
|
||||
// cwd and branch together name the workspace, so both need the event to
|
||||
// be the selected conversation's. A background Kanban worker's update
|
||||
// otherwise repointed the composer's branch at its PR worktree while
|
||||
// the default chat stayed selected (#92888).
|
||||
const describesSelectedWorkspace = sessionInfoDescribesSelectedSession(
|
||||
payload?.stored_session_id,
|
||||
isActiveEvent || rebound
|
||||
)
|
||||
|
||||
if (typeof payload?.cwd === 'string' && describesSelectedWorkspace) {
|
||||
// The active session's agent can relocate itself (new repo/worktree
|
||||
// via the terminal). When the SAME active session's cwd actually
|
||||
// moves, follow it — refresh the project tree + scope so the sidebar
|
||||
@@ -227,7 +234,7 @@ export function handleSessionInfoEvent(ctx: GatewayEventContext): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof payload?.branch === 'string') {
|
||||
if (typeof payload?.branch === 'string' && describesSelectedWorkspace) {
|
||||
setCurrentBranch(payload.branch)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user