From 3decc031222a54d850fb2ead53c5ece0c4408680 Mon Sep 17 00:00:00 2001 From: Neil Date: Mon, 14 Sep 2026 14:57:10 -0700 Subject: [PATCH] fix(ai-vault): resolve lazy child resumes in their own workspace --- .../components/right-sidebar/AiVaultPanel.tsx | 7 +-- .../right-sidebar/ai-vault-session-resume.ts | 27 +++++++++ .../ai-vault-session-worktree-map.test.tsx | 55 +++++++++++++++++++ 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx b/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx index 0e0fe1d7a78..a24ce03b448 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx @@ -27,7 +27,7 @@ import { } from './ai-vault-session-projects' import { resolveAiVaultSessionResumeActions, - resolveAiVaultSessionResumeState + resolveAiVaultHistorySessionResumeState } from './ai-vault-session-resume' import { useAiVaultSessionLaunchActions } from './ai-vault-session-launch-actions' import type { AiVaultResumeInChatEligibility } from './ai-vault-session-resume-in-chat' @@ -263,9 +263,8 @@ export default function AiVaultPanel(): React.JSX.Element { const getSessionResumeState = useCallback( (session: AiVaultSession) => - resolveAiVaultSessionResumeState({ - sessionFilePath: session.filePath, - sessionExecutionHostId: session.executionHostId, + resolveAiVaultHistorySessionResumeState({ + session, worktreeInfo: getSessionWorktreeInfo(session), activeWorktreeId: effectiveActiveWorktreeId, worktrees: allWorktrees, diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-resume.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-resume.ts index d021fc66b1f..1150f1ef2d6 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-resume.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-resume.ts @@ -15,6 +15,7 @@ import { translate } from '@/i18n/i18n' import { parseWorkspaceKey } from '../../../../shared/workspace-scope' import { canJumpToAiVaultSessionWorktree, + resolveAiVaultSessionWorktreeInfo, type AiVaultSessionWorktreeInfo } from './ai-vault-session-worktree' @@ -85,6 +86,32 @@ export function resolveAiVaultSessionResumeState(args: { } } +export function resolveAiVaultHistorySessionResumeState( + args: Omit< + Parameters[0], + 'sessionFilePath' | 'sessionExecutionHostId' + > & { + session: AiVaultSession + } +): AiVaultSessionResumeState { + const child = Boolean(args.session.subagent) + return resolveAiVaultSessionResumeState({ + ...args, + sessionFilePath: args.session.filePath, + sessionExecutionHostId: args.session.executionHostId, + worktreeInfo: child + ? resolveAiVaultSessionWorktreeInfo({ + session: args.session, + worktrees: args.worktrees, + repos: args.repos, + activeWorktreeId: args.activeWorktreeId + }) + : args.worktreeInfo, + // Lazy children are absent from the panel map; never resume them in an unrelated active workspace. + activeWorktreeId: child ? null : args.activeWorktreeId + }) +} + export function resolveAiVaultSessionResumeActions(args: { sessionFilePath: string | null sessionExecutionHostId?: AiVaultSession['executionHostId'] | null diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-worktree-map.test.tsx b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree-map.test.tsx index cc71cc9b55a..d54c2777841 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-worktree-map.test.tsx +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree-map.test.tsx @@ -235,3 +235,58 @@ describe('useAiVaultSessionWorktreeMap', () => { expect(elapsedMs).toBeLessThan(150) }) }) + +import { resolveAiVaultHistorySessionResumeState } from './ai-vault-session-resume' + +describe('lazy OMP child resume targets', () => { + it.each([worktreeA.id, worktreeB.id, null])( + 'keeps child and grandchild in their own worktree with active %s', + (activeWorktreeId) => { + const parent = makeSession({ agent: 'omp', id: 'omp:parent', cwd: worktreeA.path }) + const { result } = renderHook(() => + useAiVaultSessionWorktreeMap({ sessions: [parent], repos, worktrees }) + ) + for (const id of ['child', 'grandchild']) { + const session = makeSession({ + agent: 'omp', + id, + cwd: worktreeA.path, + subagent: { parentSessionId: parent.sessionId, agentType: null, status: null } + }) + expect(result.current.has(id)).toBe(false) + const target = resolveAiVaultHistorySessionResumeState({ + session, + worktreeInfo: result.current.get(id) ?? null, + activeWorktreeId, + worktrees, + repos + }) + expect(target).toEqual({ + blocked: false, + worktreeId: worktreeA.id, + usesSessionWorktree: true + }) + } + } + ) + it.each([{ cwd: null }, { cwd: '/missing' }, { executionHostId: 'ssh:other' as const }])( + 'refuses unmatched child %j instead of active-workspace fallback', + (overrides) => { + const session = makeSession({ + agent: 'omp', + cwd: worktreeA.path, + subagent: { parentSessionId: 'parent', agentType: null, status: null }, + ...overrides + }) + expect( + resolveAiVaultHistorySessionResumeState({ + session, + worktreeInfo: null, + activeWorktreeId: worktreeB.id, + worktrees, + repos + }) + ).toEqual({ blocked: true, worktreeId: null, usesSessionWorktree: false }) + } + ) +})