Merge branch 'nwparker/omp-child-history-resume' into nwparker/omp-nested-history

This commit is contained in:
Neil
2026-09-14 14:57:22 -07:00
3 changed files with 85 additions and 4 deletions
@@ -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,
@@ -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<typeof resolveAiVaultSessionResumeState>[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
@@ -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 })
}
)
})