fix(desktop): don't let a named session.info rehome a fresh draft
`session.info` claimed the cwd for whatever the selection happened to be, so a background tile's payload could re-point a fresh draft at the tile's workspace. Treat a nonempty `stored_session_id` as non-matching when no primary session is selected; only an ABSENT id uses the selected-session fallback (the backend omits it on a lazy session, and refusing there would leave the workspace un-owned for the rest of the conversation). Matching goes through the lineage rather than raw string equality: the backend id is the live session_key, which auto-compression rotates to the continuation tip, while a selection made from a pinned row holds the stable lineage root. Comparing those literally reads one conversation as two. Co-authored-by: ZHJay <ZHJay@users.noreply.github.com>
This commit is contained in:
parent
416e025c46
commit
9cdbeceda4
|
|
@ -47,6 +47,8 @@ import {
|
|||
$currentCwd,
|
||||
$currentModel,
|
||||
$currentProvider,
|
||||
$selectedStoredSessionId,
|
||||
$sessions,
|
||||
sessionMatchesStoredId,
|
||||
setCurrentBranch,
|
||||
setCurrentCwd,
|
||||
|
|
@ -58,6 +60,7 @@ import {
|
|||
setMessages,
|
||||
setSessions,
|
||||
setTurnStartedAt,
|
||||
setWorkspaceCwdOwner,
|
||||
setYoloActive
|
||||
} from '@/store/session'
|
||||
import { dropSessionState } from '@/store/session-states'
|
||||
|
|
@ -81,6 +84,42 @@ function firstBillingLine(text: string): string {
|
|||
return (text || '').split('\n')[0]?.trim() ?? ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*
|
||||
* 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
|
||||
* marked un-owned for the rest of the conversation. Matching goes through the
|
||||
* lineage (`sessionMatchesStoredId`) so a compression-rotated tip and the root
|
||||
* a pinned-row selection may hold still read as one conversation.
|
||||
*/
|
||||
function sessionInfoDescribesSelectedSession(storedSessionId: string | undefined): boolean {
|
||||
const infoStoredSessionId = storedSessionId?.trim() || null
|
||||
const selected = $selectedStoredSessionId.get() ?? null
|
||||
|
||||
if (!infoStoredSessionId) {
|
||||
return true
|
||||
}
|
||||
|
||||
// A named session cannot describe a fresh draft. Treating a null selection as
|
||||
// a wildcard let a background tile's `session.info` rehome the draft to the
|
||||
// tile's workspace.
|
||||
if (!selected) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (infoStoredSessionId === selected) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Either id may be the live tip or the lineage root, so ask whether ONE row
|
||||
// answers to both rather than assuming which side rotated.
|
||||
return $sessions
|
||||
.get()
|
||||
.some(session => sessionMatchesStoredId(session, infoStoredSessionId) && sessionMatchesStoredId(session, selected))
|
||||
}
|
||||
|
||||
/**
|
||||
* A turn failed on a billing wall (out of credits / payment required). The
|
||||
* gateway forwards the structured descriptor built by `agent/billing_links.py`;
|
||||
|
|
@ -396,7 +435,7 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
|
|||
// Active-session model/provider still flows through the session state
|
||||
// cache via updateSessionState → syncRuntimeMetadataToView below.
|
||||
|
||||
if (typeof payload?.cwd === 'string') {
|
||||
if (typeof payload?.cwd === 'string' && sessionInfoDescribesSelectedSession(payload.stored_session_id)) {
|
||||
// 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
|
||||
|
|
@ -408,6 +447,14 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
|
|||
lastCwdInfoSessionRef.current = sessionId
|
||||
setCurrentCwd(payload.cwd)
|
||||
|
||||
// The backend just confirmed the selected conversation's real
|
||||
// workspace, so it owns the path we wrote. Without the claim the
|
||||
// marker keeps naming whoever held it before — including the
|
||||
// released state a detached resume leaves behind — and the primary
|
||||
// workspace-derived surfaces stay hidden against a folder the
|
||||
// backend has confirmed (#71254).
|
||||
setWorkspaceCwdOwner($selectedStoredSessionId.get())
|
||||
|
||||
if (cwdMoved && sameSession) {
|
||||
void followActiveSessionCwd(payload.cwd)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue