mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 16:02:15 +00:00
fix: extract AI vault resume domains
This commit is contained in:
@@ -98,6 +98,7 @@ beforeEach(async () => {
|
||||
afterEach(async () => {
|
||||
setStructuredAgentSessionHost(null)
|
||||
await host?.flushAllStreamedEvents()
|
||||
await host?.close(SESSION)
|
||||
await rm(root, { recursive: true, force: true })
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
@@ -29,21 +29,9 @@ import {
|
||||
resolveAiVaultSessionResumeActions,
|
||||
resolveAiVaultSessionResumeState
|
||||
} from './ai-vault-session-resume'
|
||||
import {
|
||||
resolveAiVaultTargetWorkspacePath,
|
||||
useAiVaultSessionLaunchActions
|
||||
} from './ai-vault-session-launch-actions'
|
||||
import {
|
||||
resolveAiVaultSessionResumeInChatEligibility,
|
||||
type AiVaultResumeInChatEligibility
|
||||
} from './ai-vault-session-resume-in-chat'
|
||||
import { structuredAgentLaunchSupported } from '@/lib/agent-launch-routing'
|
||||
import { isAgentSessionHandleProvider } from '../../../../shared/agent-session-provider-handle'
|
||||
import { STRUCTURED_AGENT_SESSION_RESUME_HISTORY_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
|
||||
import { readLocalRuntimeCapabilities } from '@/runtime/local-runtime-capabilities'
|
||||
import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner'
|
||||
import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-context'
|
||||
import { CLIENT_PLATFORM } from '@/lib/new-workspace'
|
||||
import { useAiVaultSessionLaunchActions } from './ai-vault-session-launch-actions'
|
||||
import type { AiVaultResumeInChatEligibility } from './ai-vault-session-resume-in-chat'
|
||||
import { resolveAiVaultSessionResumeInChatForWorkspace } from './ai-vault-session-resume-in-chat-workspace'
|
||||
import {
|
||||
useAiVaultSessionWorktreeMap,
|
||||
withAiVaultCurrentWorktreeStatus
|
||||
@@ -306,43 +294,14 @@ export default function AiVaultPanel(): React.JSX.Element {
|
||||
// would run it in". The workspace it targets is the session's own when that is open, because
|
||||
// Claude looks its transcript up under a directory derived from the launch cwd.
|
||||
const getSessionResumeInChat = useCallback(
|
||||
(session: AiVaultSession): AiVaultResumeInChatEligibility => {
|
||||
const resumeState = getSessionResumeState(session)
|
||||
const targetWorkspaceId = resumeState.usesSessionWorktree
|
||||
? resumeState.worktreeId
|
||||
: (resumeState.worktreeId ?? effectiveActiveWorktreeId)
|
||||
const targetWorkspacePath = targetWorkspaceId
|
||||
? resolveAiVaultTargetWorkspacePath(resumeTargetState, targetWorkspaceId)
|
||||
: null
|
||||
return resolveAiVaultSessionResumeInChatEligibility({
|
||||
(session: AiVaultSession): AiVaultResumeInChatEligibility =>
|
||||
resolveAiVaultSessionResumeInChatForWorkspace({
|
||||
session,
|
||||
targetWorkspaceId,
|
||||
targetWorkspacePath,
|
||||
structuredRouteAvailable:
|
||||
isAgentSessionHandleProvider(session.agent) &&
|
||||
Boolean(targetWorkspaceId) &&
|
||||
structuredAgentLaunchSupported({
|
||||
agent: session.agent,
|
||||
settings,
|
||||
executionHostId: getExecutionHostIdForWorktree(
|
||||
useAppStore.getState(),
|
||||
targetWorkspaceId as string
|
||||
),
|
||||
platform: CLIENT_PLATFORM,
|
||||
hostCapabilities: readLocalRuntimeCapabilities(),
|
||||
workspaceKind: (targetWorkspaceId as string).startsWith('folder:')
|
||||
? 'folder'
|
||||
: 'git-worktree',
|
||||
projectRuntime: getLocalProjectExecutionRuntimeContext(
|
||||
useAppStore.getState(),
|
||||
targetWorkspaceId as string
|
||||
)
|
||||
}) &&
|
||||
readLocalRuntimeCapabilities().includes(
|
||||
STRUCTURED_AGENT_SESSION_RESUME_HISTORY_RUNTIME_CAPABILITY
|
||||
)
|
||||
})
|
||||
},
|
||||
resumeState: getSessionResumeState(session),
|
||||
activeWorkspaceId: effectiveActiveWorktreeId,
|
||||
targetState: resumeTargetState,
|
||||
settings
|
||||
}),
|
||||
[effectiveActiveWorktreeId, getSessionResumeState, resumeTargetState, settings]
|
||||
)
|
||||
|
||||
|
||||
@@ -3,45 +3,28 @@ import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import type { AgentStatusState } from '../../../../shared/agent-status-types'
|
||||
import type { AiVaultScope, AiVaultSession } from '../../../../shared/ai-vault-types'
|
||||
import type { AiVaultResumeStartup } from '@/lib/ai-vault-resume-command'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { getActiveStickyHeaderIndexForScroll } from '../sidebar/worktree-list/viewport/virtual-rows'
|
||||
import { VaultGroupHeader } from './AiVaultPanelControls'
|
||||
import { EmptyState, SessionLoadingState } from './AiVaultSessionListStates'
|
||||
import { VaultSessionRow } from './AiVaultSessionRow'
|
||||
import type { AiVaultSessionGroup } from './ai-vault-session-filters'
|
||||
import type { AiVaultOriginalPaneTarget } from './ai-vault-original-pane'
|
||||
import {
|
||||
aiVaultSessionResumeLabel,
|
||||
aiVaultSessionRowResumeGating,
|
||||
type AiVaultSessionResumeActions,
|
||||
type AiVaultSessionResumeState
|
||||
import type {
|
||||
AiVaultSessionResumeActions,
|
||||
AiVaultSessionResumeState
|
||||
} from './ai-vault-session-resume'
|
||||
import {
|
||||
canJumpToAiVaultSessionWorktree,
|
||||
isAiVaultSessionInCurrentWorktree,
|
||||
type AiVaultSessionWorktreeInfo
|
||||
} from './ai-vault-session-worktree'
|
||||
import {
|
||||
canOpenAiVaultSessionLogInOrca,
|
||||
canUseLocalAiVaultSessionPathActions
|
||||
} from './ai-vault-session-path-actions'
|
||||
import type { AiVaultSessionWorktreeInfo } from './ai-vault-session-worktree'
|
||||
import {
|
||||
extractVaultVirtualRowIndexes,
|
||||
getVaultStickyHeaderIndexes,
|
||||
VAULT_GROUP_HEADER_ROW_HEIGHT,
|
||||
VAULT_SESSION_ROW_HEIGHT
|
||||
} from './ai-vault-virtual-rows'
|
||||
import { canContinueAiVaultSessionInNewSession } from './ai-vault-session-continuation'
|
||||
import type { AiVaultResumeInChatEligibility } from './ai-vault-session-resume-in-chat'
|
||||
import { AiVaultVirtualRow, type AiVaultListRow } from './AiVaultVirtualRow'
|
||||
|
||||
const VAULT_ROW_OVERSCAN = 8
|
||||
const VAULT_EXPANDED_SESSION_ROW_ESTIMATED_HEIGHT = 420
|
||||
|
||||
type AiVaultListRow =
|
||||
| { type: 'group'; group: AiVaultSessionGroup }
|
||||
| { type: 'session'; groupKey: string; session: AiVaultSession }
|
||||
|
||||
export function AiVaultSessionVirtualList({
|
||||
groups,
|
||||
collapsedGroups,
|
||||
@@ -246,186 +229,3 @@ export function AiVaultSessionVirtualList({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AiVaultVirtualRow({
|
||||
row,
|
||||
index,
|
||||
start,
|
||||
activeStickyHeaderIndex,
|
||||
measureElement,
|
||||
collapsedGroups,
|
||||
expandedSessionIds,
|
||||
vaultScope,
|
||||
buildResumeStartup,
|
||||
getOriginalPaneTarget,
|
||||
getSessionLiveState,
|
||||
getWorktreeInfo,
|
||||
getSessionResumeState,
|
||||
getSessionResumeActions,
|
||||
getSessionResumeInChat,
|
||||
onToggleGroup,
|
||||
onToggleSessionDetails,
|
||||
onJumpToOriginalPane,
|
||||
onJumpToWorktree,
|
||||
onResume,
|
||||
onContinueInNewSession,
|
||||
onResumeInNewChat,
|
||||
onCopyResume,
|
||||
onCopyId,
|
||||
onCopyPath,
|
||||
onOpenLog,
|
||||
onRevealLog,
|
||||
onOpenCwd,
|
||||
onRequestDelete
|
||||
}: {
|
||||
row: AiVaultListRow | undefined
|
||||
index: number
|
||||
start: number
|
||||
activeStickyHeaderIndex: number | null
|
||||
measureElement: (node: Element | null) => void
|
||||
collapsedGroups: ReadonlySet<string>
|
||||
expandedSessionIds: ReadonlySet<string>
|
||||
vaultScope: AiVaultScope
|
||||
buildResumeStartup: (session: AiVaultSession, worktreeId?: string | null) => AiVaultResumeStartup
|
||||
getOriginalPaneTarget: (session: AiVaultSession) => AiVaultOriginalPaneTarget | null
|
||||
getSessionLiveState: (session: AiVaultSession) => AgentStatusState | null
|
||||
getWorktreeInfo: (session: AiVaultSession) => AiVaultSessionWorktreeInfo | null
|
||||
getSessionResumeState: (session: AiVaultSession) => AiVaultSessionResumeState
|
||||
getSessionResumeActions: (session: AiVaultSession) => AiVaultSessionResumeActions
|
||||
getSessionResumeInChat: (session: AiVaultSession) => AiVaultResumeInChatEligibility
|
||||
onToggleGroup: (key: string) => void
|
||||
onToggleSessionDetails: (sessionId: string) => void
|
||||
onJumpToOriginalPane: (session: AiVaultSession) => void
|
||||
onJumpToWorktree: (worktreeId: string) => void
|
||||
onResume: (session: AiVaultSession, worktreeId: string) => void
|
||||
onContinueInNewSession: (session: AiVaultSession, worktreeId: string) => void
|
||||
onResumeInNewChat: (session: AiVaultSession, worktreeId: string) => void
|
||||
onCopyResume: (session: AiVaultSession, worktreeId?: string | null) => void
|
||||
onCopyId: (session: AiVaultSession) => void
|
||||
onCopyPath: (session: AiVaultSession) => void
|
||||
onOpenLog: (session: AiVaultSession) => void
|
||||
onRevealLog: (session: AiVaultSession) => void
|
||||
onOpenCwd: (session: AiVaultSession) => void
|
||||
onRequestDelete: (session: AiVaultSession) => void
|
||||
}): React.JSX.Element | null {
|
||||
if (!row) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isActiveStickyHeader = row.type === 'group' && activeStickyHeaderIndex === index
|
||||
const originalPaneTarget = row.type === 'session' ? getOriginalPaneTarget(row.session) : null
|
||||
const worktreeInfo = row.type === 'session' ? getWorktreeInfo(row.session) : null
|
||||
// Why: omit the jump affordance when the session already lives in the
|
||||
// worktree on screen — jumping there is a no-op.
|
||||
const showJumpToWorktree = !isAiVaultSessionInCurrentWorktree(worktreeInfo)
|
||||
const worktreeJumpId =
|
||||
showJumpToWorktree && canJumpToAiVaultSessionWorktree(worktreeInfo)
|
||||
? worktreeInfo?.worktreeId
|
||||
: null
|
||||
const resumeState = row.type === 'session' ? getSessionResumeState(row.session) : null
|
||||
const resumeActions = row.type === 'session' ? getSessionResumeActions(row.session) : null
|
||||
const resumeInChat = row.type === 'session' ? getSessionResumeInChat(row.session) : null
|
||||
const continuationWorktreeId =
|
||||
row.type === 'session' &&
|
||||
canContinueAiVaultSessionInNewSession(row.session, resumeState?.worktreeId)
|
||||
? resumeState?.worktreeId
|
||||
: null
|
||||
// Gate resume on real content: a zero-turn transcript would resume into an
|
||||
// empty conversation, so it is never offered as normally resumable.
|
||||
const resumeGating =
|
||||
row.type === 'session'
|
||||
? aiVaultSessionRowResumeGating(row.session, resumeState)
|
||||
: { resumeDisabled: true, canCopyResumeCommand: false }
|
||||
const resumeLabel = resumeState ? aiVaultSessionResumeLabel(resumeState) : ''
|
||||
const canOpenLocalSessionPaths =
|
||||
row.type === 'session' && canUseLocalAiVaultSessionPathActions(row.session.executionHostId)
|
||||
// Why: in-Orca View Log additionally withholds synthetic (SQLite/OpenCode)
|
||||
// identities that have no single file to open, while Reveal/CWD stay on the
|
||||
// existing local-path gate.
|
||||
const canOpenLogInOrca = row.type === 'session' && canOpenAiVaultSessionLogInOrca(row.session)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={measureElement}
|
||||
data-index={index}
|
||||
className={cn(
|
||||
'left-0 w-full',
|
||||
isActiveStickyHeader ? 'sticky top-0 z-10 bg-sidebar' : 'absolute top-0'
|
||||
)}
|
||||
style={isActiveStickyHeader ? undefined : { transform: `translateY(${start}px)` }}
|
||||
>
|
||||
{row.type === 'group' ? (
|
||||
<VaultGroupHeader
|
||||
group={row.group}
|
||||
collapsed={collapsedGroups.has(row.group.key)}
|
||||
onToggle={() => onToggleGroup(row.group.key)}
|
||||
/>
|
||||
) : (
|
||||
<VaultSessionRow
|
||||
session={row.session}
|
||||
liveState={getSessionLiveState(row.session)}
|
||||
resumeStartup={buildResumeStartup(row.session, resumeState?.worktreeId)}
|
||||
realHomeResumeStartup={buildResumeStartup(
|
||||
{ ...row.session, codexHome: null },
|
||||
resumeState?.worktreeId
|
||||
)}
|
||||
worktreeInfo={worktreeInfo}
|
||||
vaultScope={vaultScope}
|
||||
detailsExpanded={expandedSessionIds.has(row.session.id)}
|
||||
resumeDisabled={resumeGating.resumeDisabled}
|
||||
resumeLabel={resumeLabel}
|
||||
resumeActions={
|
||||
resumeActions ?? {
|
||||
worktree: { worktreeId: null, disabled: true },
|
||||
newTab: { worktreeId: null, disabled: true }
|
||||
}
|
||||
}
|
||||
onToggleDetails={() => onToggleSessionDetails(row.session.id)}
|
||||
onJumpToOriginalPane={
|
||||
originalPaneTarget ? () => onJumpToOriginalPane(row.session) : undefined
|
||||
}
|
||||
showJumpToWorktree={showJumpToWorktree}
|
||||
onJumpToWorktree={worktreeJumpId ? () => onJumpToWorktree(worktreeJumpId) : undefined}
|
||||
onResume={() => {
|
||||
if (resumeState?.worktreeId) {
|
||||
onResume(row.session, resumeState.worktreeId)
|
||||
}
|
||||
}}
|
||||
onContinueInNewSession={
|
||||
continuationWorktreeId
|
||||
? () => onContinueInNewSession(row.session, continuationWorktreeId)
|
||||
: undefined
|
||||
}
|
||||
onResumeInNewChat={
|
||||
resumeInChat?.available
|
||||
? () => onResumeInNewChat(row.session, resumeInChat.workspaceId)
|
||||
: undefined
|
||||
}
|
||||
onResumeInWorktree={() => {
|
||||
if (resumeActions?.worktree.worktreeId) {
|
||||
onResume(row.session, resumeActions.worktree.worktreeId)
|
||||
}
|
||||
}}
|
||||
onResumeInNewTab={() => {
|
||||
if (resumeActions?.newTab.worktreeId) {
|
||||
onResume(row.session, resumeActions.newTab.worktreeId)
|
||||
}
|
||||
}}
|
||||
onCopyResume={
|
||||
resumeGating.canCopyResumeCommand
|
||||
? () => onCopyResume(row.session, resumeState?.worktreeId)
|
||||
: undefined
|
||||
}
|
||||
onCopyId={() => onCopyId(row.session)}
|
||||
onCopyPath={() => onCopyPath(row.session)}
|
||||
onOpenLog={canOpenLogInOrca ? () => onOpenLog(row.session) : undefined}
|
||||
onRevealLog={canOpenLocalSessionPaths ? () => onRevealLog(row.session) : undefined}
|
||||
onOpenCwd={
|
||||
canOpenLocalSessionPaths && row.session.cwd ? () => onOpenCwd(row.session) : undefined
|
||||
}
|
||||
onRequestDelete={onRequestDelete}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import type { AgentStatusState } from '../../../../shared/agent-status-types'
|
||||
import type { AiVaultScope, AiVaultSession } from '../../../../shared/ai-vault-types'
|
||||
import type { AiVaultResumeStartup } from '@/lib/ai-vault-resume-command'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { VaultGroupHeader } from './AiVaultPanelControls'
|
||||
import { VaultSessionRow } from './AiVaultSessionRow'
|
||||
import type { AiVaultSessionGroup } from './ai-vault-session-filters'
|
||||
import type { AiVaultOriginalPaneTarget } from './ai-vault-original-pane'
|
||||
import {
|
||||
aiVaultSessionResumeLabel,
|
||||
aiVaultSessionRowResumeGating,
|
||||
type AiVaultSessionResumeActions,
|
||||
type AiVaultSessionResumeState
|
||||
} from './ai-vault-session-resume'
|
||||
import {
|
||||
canJumpToAiVaultSessionWorktree,
|
||||
isAiVaultSessionInCurrentWorktree,
|
||||
type AiVaultSessionWorktreeInfo
|
||||
} from './ai-vault-session-worktree'
|
||||
import {
|
||||
canOpenAiVaultSessionLogInOrca,
|
||||
canUseLocalAiVaultSessionPathActions
|
||||
} from './ai-vault-session-path-actions'
|
||||
import { canContinueAiVaultSessionInNewSession } from './ai-vault-session-continuation'
|
||||
import type { AiVaultResumeInChatEligibility } from './ai-vault-session-resume-in-chat'
|
||||
|
||||
export type AiVaultListRow =
|
||||
| { type: 'group'; group: AiVaultSessionGroup }
|
||||
| { type: 'session'; groupKey: string; session: AiVaultSession }
|
||||
|
||||
export function AiVaultVirtualRow({
|
||||
row,
|
||||
index,
|
||||
start,
|
||||
activeStickyHeaderIndex,
|
||||
measureElement,
|
||||
collapsedGroups,
|
||||
expandedSessionIds,
|
||||
vaultScope,
|
||||
buildResumeStartup,
|
||||
getOriginalPaneTarget,
|
||||
getSessionLiveState,
|
||||
getWorktreeInfo,
|
||||
getSessionResumeState,
|
||||
getSessionResumeActions,
|
||||
getSessionResumeInChat,
|
||||
onToggleGroup,
|
||||
onToggleSessionDetails,
|
||||
onJumpToOriginalPane,
|
||||
onJumpToWorktree,
|
||||
onResume,
|
||||
onContinueInNewSession,
|
||||
onResumeInNewChat,
|
||||
onCopyResume,
|
||||
onCopyId,
|
||||
onCopyPath,
|
||||
onOpenLog,
|
||||
onRevealLog,
|
||||
onOpenCwd,
|
||||
onRequestDelete
|
||||
}: {
|
||||
row: AiVaultListRow | undefined
|
||||
index: number
|
||||
start: number
|
||||
activeStickyHeaderIndex: number | null
|
||||
measureElement: (node: Element | null) => void
|
||||
collapsedGroups: ReadonlySet<string>
|
||||
expandedSessionIds: ReadonlySet<string>
|
||||
vaultScope: AiVaultScope
|
||||
buildResumeStartup: (session: AiVaultSession, worktreeId?: string | null) => AiVaultResumeStartup
|
||||
getOriginalPaneTarget: (session: AiVaultSession) => AiVaultOriginalPaneTarget | null
|
||||
getSessionLiveState: (session: AiVaultSession) => AgentStatusState | null
|
||||
getWorktreeInfo: (session: AiVaultSession) => AiVaultSessionWorktreeInfo | null
|
||||
getSessionResumeState: (session: AiVaultSession) => AiVaultSessionResumeState
|
||||
getSessionResumeActions: (session: AiVaultSession) => AiVaultSessionResumeActions
|
||||
getSessionResumeInChat: (session: AiVaultSession) => AiVaultResumeInChatEligibility
|
||||
onToggleGroup: (key: string) => void
|
||||
onToggleSessionDetails: (sessionId: string) => void
|
||||
onJumpToOriginalPane: (session: AiVaultSession) => void
|
||||
onJumpToWorktree: (worktreeId: string) => void
|
||||
onResume: (session: AiVaultSession, worktreeId: string) => void
|
||||
onContinueInNewSession: (session: AiVaultSession, worktreeId: string) => void
|
||||
onResumeInNewChat: (session: AiVaultSession, worktreeId: string) => void
|
||||
onCopyResume: (session: AiVaultSession, worktreeId?: string | null) => void
|
||||
onCopyId: (session: AiVaultSession) => void
|
||||
onCopyPath: (session: AiVaultSession) => void
|
||||
onOpenLog: (session: AiVaultSession) => void
|
||||
onRevealLog: (session: AiVaultSession) => void
|
||||
onOpenCwd: (session: AiVaultSession) => void
|
||||
onRequestDelete: (session: AiVaultSession) => void
|
||||
}): React.JSX.Element | null {
|
||||
if (!row) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isActiveStickyHeader = row.type === 'group' && activeStickyHeaderIndex === index
|
||||
const originalPaneTarget = row.type === 'session' ? getOriginalPaneTarget(row.session) : null
|
||||
const worktreeInfo = row.type === 'session' ? getWorktreeInfo(row.session) : null
|
||||
// Why: omit the jump affordance when the session already lives in the
|
||||
// worktree on screen — jumping there is a no-op.
|
||||
const showJumpToWorktree = !isAiVaultSessionInCurrentWorktree(worktreeInfo)
|
||||
const worktreeJumpId =
|
||||
showJumpToWorktree && canJumpToAiVaultSessionWorktree(worktreeInfo)
|
||||
? worktreeInfo?.worktreeId
|
||||
: null
|
||||
const resumeState = row.type === 'session' ? getSessionResumeState(row.session) : null
|
||||
const resumeActions = row.type === 'session' ? getSessionResumeActions(row.session) : null
|
||||
const resumeInChat = row.type === 'session' ? getSessionResumeInChat(row.session) : null
|
||||
const continuationWorktreeId =
|
||||
row.type === 'session' &&
|
||||
canContinueAiVaultSessionInNewSession(row.session, resumeState?.worktreeId)
|
||||
? resumeState?.worktreeId
|
||||
: null
|
||||
// Gate resume on real content: a zero-turn transcript would resume into an
|
||||
// empty conversation, so it is never offered as normally resumable.
|
||||
const resumeGating =
|
||||
row.type === 'session'
|
||||
? aiVaultSessionRowResumeGating(row.session, resumeState)
|
||||
: { resumeDisabled: true, canCopyResumeCommand: false }
|
||||
const resumeLabel = resumeState ? aiVaultSessionResumeLabel(resumeState) : ''
|
||||
const canOpenLocalSessionPaths =
|
||||
row.type === 'session' && canUseLocalAiVaultSessionPathActions(row.session.executionHostId)
|
||||
// Why: in-Orca View Log additionally withholds synthetic (SQLite/OpenCode)
|
||||
// identities that have no single file to open, while Reveal/CWD stay on the
|
||||
// existing local-path gate.
|
||||
const canOpenLogInOrca = row.type === 'session' && canOpenAiVaultSessionLogInOrca(row.session)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={measureElement}
|
||||
data-index={index}
|
||||
className={cn(
|
||||
'left-0 w-full',
|
||||
isActiveStickyHeader ? 'sticky top-0 z-10 bg-sidebar' : 'absolute top-0'
|
||||
)}
|
||||
style={isActiveStickyHeader ? undefined : { transform: `translateY(${start}px)` }}
|
||||
>
|
||||
{row.type === 'group' ? (
|
||||
<VaultGroupHeader
|
||||
group={row.group}
|
||||
collapsed={collapsedGroups.has(row.group.key)}
|
||||
onToggle={() => onToggleGroup(row.group.key)}
|
||||
/>
|
||||
) : (
|
||||
<VaultSessionRow
|
||||
session={row.session}
|
||||
liveState={getSessionLiveState(row.session)}
|
||||
resumeStartup={buildResumeStartup(row.session, resumeState?.worktreeId)}
|
||||
realHomeResumeStartup={buildResumeStartup(
|
||||
{ ...row.session, codexHome: null },
|
||||
resumeState?.worktreeId
|
||||
)}
|
||||
worktreeInfo={worktreeInfo}
|
||||
vaultScope={vaultScope}
|
||||
detailsExpanded={expandedSessionIds.has(row.session.id)}
|
||||
resumeDisabled={resumeGating.resumeDisabled}
|
||||
resumeLabel={resumeLabel}
|
||||
resumeActions={
|
||||
resumeActions ?? {
|
||||
worktree: { worktreeId: null, disabled: true },
|
||||
newTab: { worktreeId: null, disabled: true }
|
||||
}
|
||||
}
|
||||
onToggleDetails={() => onToggleSessionDetails(row.session.id)}
|
||||
onJumpToOriginalPane={
|
||||
originalPaneTarget ? () => onJumpToOriginalPane(row.session) : undefined
|
||||
}
|
||||
showJumpToWorktree={showJumpToWorktree}
|
||||
onJumpToWorktree={worktreeJumpId ? () => onJumpToWorktree(worktreeJumpId) : undefined}
|
||||
onResume={() => {
|
||||
if (resumeState?.worktreeId) {
|
||||
onResume(row.session, resumeState.worktreeId)
|
||||
}
|
||||
}}
|
||||
onContinueInNewSession={
|
||||
continuationWorktreeId
|
||||
? () => onContinueInNewSession(row.session, continuationWorktreeId)
|
||||
: undefined
|
||||
}
|
||||
onResumeInNewChat={
|
||||
resumeInChat?.available
|
||||
? () => onResumeInNewChat(row.session, resumeInChat.workspaceId)
|
||||
: undefined
|
||||
}
|
||||
onResumeInWorktree={() => {
|
||||
if (resumeActions?.worktree.worktreeId) {
|
||||
onResume(row.session, resumeActions.worktree.worktreeId)
|
||||
}
|
||||
}}
|
||||
onResumeInNewTab={() => {
|
||||
if (resumeActions?.newTab.worktreeId) {
|
||||
onResume(row.session, resumeActions.newTab.worktreeId)
|
||||
}
|
||||
}}
|
||||
onCopyResume={
|
||||
resumeGating.canCopyResumeCommand
|
||||
? () => onCopyResume(row.session, resumeState?.worktreeId)
|
||||
: undefined
|
||||
}
|
||||
onCopyId={() => onCopyId(row.session)}
|
||||
onCopyPath={() => onCopyPath(row.session)}
|
||||
onOpenLog={canOpenLogInOrca ? () => onOpenLog(row.session) : undefined}
|
||||
onRevealLog={canOpenLocalSessionPaths ? () => onRevealLog(row.session) : undefined}
|
||||
onOpenCwd={
|
||||
canOpenLocalSessionPaths && row.session.cwd ? () => onOpenCwd(row.session) : undefined
|
||||
}
|
||||
onRequestDelete={onRequestDelete}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -10,27 +10,23 @@ import {
|
||||
activateAndRevealWorktree
|
||||
} from '@/lib/worktree-activation'
|
||||
import { useAppStore } from '@/store'
|
||||
import {
|
||||
canResumeAiVaultSessionOnTarget,
|
||||
getAiVaultResumeWorkspaceExecutionHostId,
|
||||
getAiVaultResumeWorkspaceTargetStatus
|
||||
} from '@/lib/ai-vault-resume-target'
|
||||
import type { AiVaultAgent, AiVaultSession } from '../../../../shared/ai-vault-types'
|
||||
import { prepareAiVaultSessionForResume } from '@/lib/ai-vault-session-resume-preparation'
|
||||
import type { Worktree } from '../../../../shared/worktree/types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { agentLabel } from './ai-vault-session-filters'
|
||||
import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import {
|
||||
isKnownAiVaultResumeWorkspaceTarget,
|
||||
type AiVaultSessionResumeTargetState
|
||||
} from './ai-vault-session-resume'
|
||||
import type { AiVaultSessionResumeTargetState } from './ai-vault-session-resume'
|
||||
import { prepareAiVaultSessionContinuation } from './ai-vault-session-continuation'
|
||||
import type { AgentSessionContinuationRequest } from '@/lib/agent-session-continuation'
|
||||
import { findWorktreeById } from '@/store/slices/worktree-helpers'
|
||||
import { activateAiVaultStructuredSession } from '@/lib/activate-ai-vault-structured-session'
|
||||
import { startStructuredAgentLaunch } from '@/lib/structured-agent-session-launch'
|
||||
import { isAgentSessionHandleProvider } from '../../../../shared/agent-session-provider-handle'
|
||||
import {
|
||||
aiVaultResumeUnsupportedMessage,
|
||||
resolveAiVaultSessionLaunchTarget,
|
||||
resolveAiVaultTargetWorkspacePath
|
||||
} from './ai-vault-session-launch-target'
|
||||
|
||||
export function useAiVaultSessionLaunchActions({
|
||||
activeWorktree,
|
||||
@@ -283,66 +279,9 @@ function notifyAiVaultSessionPreparationFailure(error: unknown): void {
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveAiVaultTargetWorkspacePath(
|
||||
state: AiVaultSessionResumeTargetState,
|
||||
workspaceId: string
|
||||
): string | null {
|
||||
const scope = parseWorkspaceKey(workspaceId)
|
||||
if (scope?.type === 'folder') {
|
||||
return (
|
||||
state.folderWorkspaces.find((workspace) => workspace.id === scope.folderWorkspaceId)
|
||||
?.folderPath ?? null
|
||||
)
|
||||
}
|
||||
const worktreeId = scope?.type === 'worktree' ? scope.worktreeId : workspaceId
|
||||
return findWorktreeById(state.worktreesByRepo, worktreeId)?.path ?? null
|
||||
}
|
||||
|
||||
export type AiVaultSessionLaunchTarget =
|
||||
| { status: 'missing' }
|
||||
| {
|
||||
status: 'unsupported'
|
||||
targetStatus: ReturnType<typeof getAiVaultResumeWorkspaceTargetStatus>
|
||||
}
|
||||
| { status: 'ready'; worktreeId: string }
|
||||
|
||||
export function resolveAiVaultSessionLaunchTarget(args: {
|
||||
sessionFilePath: string | null
|
||||
sessionExecutionHostId?: AiVaultSession['executionHostId'] | null
|
||||
activeWorktreeId: string | null
|
||||
targetWorktreeId?: string
|
||||
targetState: AiVaultSessionResumeTargetState
|
||||
}): AiVaultSessionLaunchTarget {
|
||||
const targetWorktreeId = args.targetWorktreeId ?? args.activeWorktreeId
|
||||
if (
|
||||
!targetWorktreeId ||
|
||||
!isKnownAiVaultResumeWorkspaceTarget(args.targetState, targetWorktreeId)
|
||||
) {
|
||||
return { status: 'missing' }
|
||||
}
|
||||
|
||||
const targetStatus = getAiVaultResumeWorkspaceTargetStatus(args.targetState, targetWorktreeId)
|
||||
const targetExecutionHostId = getAiVaultResumeWorkspaceExecutionHostId(
|
||||
args.targetState,
|
||||
targetWorktreeId
|
||||
)
|
||||
if (
|
||||
!canResumeAiVaultSessionOnTarget({
|
||||
sessionFilePath: args.sessionFilePath,
|
||||
sessionExecutionHostId: args.sessionExecutionHostId,
|
||||
targetStatus,
|
||||
targetExecutionHostId
|
||||
})
|
||||
) {
|
||||
return { status: 'unsupported', targetStatus }
|
||||
}
|
||||
|
||||
return { status: 'ready', worktreeId: targetWorktreeId }
|
||||
}
|
||||
|
||||
function resolveAiVaultSessionLaunchTargetOrNotify(
|
||||
args: Parameters<typeof resolveAiVaultSessionLaunchTarget>[0]
|
||||
): Extract<AiVaultSessionLaunchTarget, { status: 'ready' }> | null {
|
||||
): Extract<ReturnType<typeof resolveAiVaultSessionLaunchTarget>, { status: 'ready' }> | null {
|
||||
const target = resolveAiVaultSessionLaunchTarget(args)
|
||||
if (target.status === 'missing') {
|
||||
toast.error(
|
||||
@@ -360,23 +299,6 @@ function resolveAiVaultSessionLaunchTargetOrNotify(
|
||||
return target
|
||||
}
|
||||
|
||||
function aiVaultResumeUnsupportedMessage(
|
||||
targetStatus: ReturnType<typeof getAiVaultResumeWorkspaceTargetStatus>
|
||||
): string {
|
||||
// Why: local and SSH targets can both be valid generally; this branch means
|
||||
// the session's recorded host does not match the selected workspace.
|
||||
if (targetStatus === 'ssh' || targetStatus === 'local' || targetStatus === 'runtime') {
|
||||
return translate(
|
||||
'auto.components.right.sidebar.AiVaultPanel.sessionHostMismatchUnsupported',
|
||||
'This session belongs to a different host. Open a workspace on the same host to resume it.'
|
||||
)
|
||||
}
|
||||
return translate(
|
||||
'auto.components.right.sidebar.AiVaultPanel.openSupportedWorkspace',
|
||||
'Open a workspace before resuming a session.'
|
||||
)
|
||||
}
|
||||
|
||||
function activateAiVaultResumeWorkspace(workspaceId: string): void {
|
||||
const workspaceScope = parseWorkspaceKey(workspaceId)
|
||||
if (workspaceScope?.type === 'folder') {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
canResumeAiVaultSessionOnTarget,
|
||||
getAiVaultResumeWorkspaceExecutionHostId,
|
||||
getAiVaultResumeWorkspaceTargetStatus
|
||||
} from '@/lib/ai-vault-resume-target'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { findWorktreeById } from '@/store/slices/worktree-helpers'
|
||||
import type { AiVaultSession } from '../../../../shared/ai-vault-types'
|
||||
import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import {
|
||||
isKnownAiVaultResumeWorkspaceTarget,
|
||||
type AiVaultSessionResumeTargetState
|
||||
} from './ai-vault-session-resume'
|
||||
|
||||
export function resolveAiVaultTargetWorkspacePath(
|
||||
state: AiVaultSessionResumeTargetState,
|
||||
workspaceId: string
|
||||
): string | null {
|
||||
const scope = parseWorkspaceKey(workspaceId)
|
||||
if (scope?.type === 'folder') {
|
||||
return (
|
||||
state.folderWorkspaces.find((workspace) => workspace.id === scope.folderWorkspaceId)
|
||||
?.folderPath ?? null
|
||||
)
|
||||
}
|
||||
const worktreeId = scope?.type === 'worktree' ? scope.worktreeId : workspaceId
|
||||
return findWorktreeById(state.worktreesByRepo, worktreeId)?.path ?? null
|
||||
}
|
||||
|
||||
export type AiVaultSessionLaunchTarget =
|
||||
| { status: 'missing' }
|
||||
| {
|
||||
status: 'unsupported'
|
||||
targetStatus: ReturnType<typeof getAiVaultResumeWorkspaceTargetStatus>
|
||||
}
|
||||
| { status: 'ready'; worktreeId: string }
|
||||
|
||||
export function resolveAiVaultSessionLaunchTarget(args: {
|
||||
sessionFilePath: string | null
|
||||
sessionExecutionHostId?: AiVaultSession['executionHostId'] | null
|
||||
activeWorktreeId: string | null
|
||||
targetWorktreeId?: string
|
||||
targetState: AiVaultSessionResumeTargetState
|
||||
}): AiVaultSessionLaunchTarget {
|
||||
const targetWorktreeId = args.targetWorktreeId ?? args.activeWorktreeId
|
||||
if (
|
||||
!targetWorktreeId ||
|
||||
!isKnownAiVaultResumeWorkspaceTarget(args.targetState, targetWorktreeId)
|
||||
) {
|
||||
return { status: 'missing' }
|
||||
}
|
||||
|
||||
const targetStatus = getAiVaultResumeWorkspaceTargetStatus(args.targetState, targetWorktreeId)
|
||||
const targetExecutionHostId = getAiVaultResumeWorkspaceExecutionHostId(
|
||||
args.targetState,
|
||||
targetWorktreeId
|
||||
)
|
||||
if (
|
||||
!canResumeAiVaultSessionOnTarget({
|
||||
sessionFilePath: args.sessionFilePath,
|
||||
sessionExecutionHostId: args.sessionExecutionHostId,
|
||||
targetStatus,
|
||||
targetExecutionHostId
|
||||
})
|
||||
) {
|
||||
return { status: 'unsupported', targetStatus }
|
||||
}
|
||||
|
||||
return { status: 'ready', worktreeId: targetWorktreeId }
|
||||
}
|
||||
|
||||
export function aiVaultResumeUnsupportedMessage(
|
||||
targetStatus: ReturnType<typeof getAiVaultResumeWorkspaceTargetStatus>
|
||||
): string {
|
||||
// Why: local and SSH targets can both be valid generally; this branch means
|
||||
// the session's recorded host does not match the selected workspace.
|
||||
if (targetStatus === 'ssh' || targetStatus === 'local' || targetStatus === 'runtime') {
|
||||
return translate(
|
||||
'auto.components.right.sidebar.AiVaultPanel.sessionHostMismatchUnsupported',
|
||||
'This session belongs to a different host. Open a workspace on the same host to resume it.'
|
||||
)
|
||||
}
|
||||
return translate(
|
||||
'auto.components.right.sidebar.AiVaultPanel.openSupportedWorkspace',
|
||||
'Open a workspace before resuming a session.'
|
||||
)
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
structuredAgentLaunchSupported,
|
||||
type AgentLaunchRoutingInput
|
||||
} from '@/lib/agent-launch-routing'
|
||||
import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-context'
|
||||
import { CLIENT_PLATFORM } from '@/lib/new-workspace'
|
||||
import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner'
|
||||
import { readLocalRuntimeCapabilities } from '@/runtime/local-runtime-capabilities'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { AiVaultSession } from '../../../../shared/ai-vault-types'
|
||||
import { isAgentSessionHandleProvider } from '../../../../shared/agent-session-provider-handle'
|
||||
import { STRUCTURED_AGENT_SESSION_RESUME_HISTORY_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
|
||||
import { resolveAiVaultTargetWorkspacePath } from './ai-vault-session-launch-target'
|
||||
import {
|
||||
resolveAiVaultSessionResumeInChatEligibility,
|
||||
type AiVaultResumeInChatEligibility
|
||||
} from './ai-vault-session-resume-in-chat'
|
||||
import type {
|
||||
AiVaultSessionResumeState,
|
||||
AiVaultSessionResumeTargetState
|
||||
} from './ai-vault-session-resume'
|
||||
|
||||
export function resolveAiVaultSessionResumeInChatForWorkspace(args: {
|
||||
session: AiVaultSession
|
||||
resumeState: AiVaultSessionResumeState
|
||||
activeWorkspaceId: string | null
|
||||
targetState: AiVaultSessionResumeTargetState
|
||||
settings: AgentLaunchRoutingInput['settings']
|
||||
}): AiVaultResumeInChatEligibility {
|
||||
const targetWorkspaceId = args.resumeState.usesSessionWorktree
|
||||
? args.resumeState.worktreeId
|
||||
: (args.resumeState.worktreeId ?? args.activeWorkspaceId)
|
||||
const targetWorkspacePath = targetWorkspaceId
|
||||
? resolveAiVaultTargetWorkspacePath(args.targetState, targetWorkspaceId)
|
||||
: null
|
||||
return resolveAiVaultSessionResumeInChatEligibility({
|
||||
session: args.session,
|
||||
targetWorkspaceId,
|
||||
targetWorkspacePath,
|
||||
structuredRouteAvailable:
|
||||
isAgentSessionHandleProvider(args.session.agent) &&
|
||||
Boolean(targetWorkspaceId) &&
|
||||
structuredAgentLaunchSupported({
|
||||
agent: args.session.agent,
|
||||
settings: args.settings,
|
||||
executionHostId: getExecutionHostIdForWorktree(
|
||||
useAppStore.getState(),
|
||||
targetWorkspaceId as string
|
||||
),
|
||||
platform: CLIENT_PLATFORM,
|
||||
hostCapabilities: readLocalRuntimeCapabilities(),
|
||||
workspaceKind: (targetWorkspaceId as string).startsWith('folder:')
|
||||
? 'folder'
|
||||
: 'git-worktree',
|
||||
projectRuntime: getLocalProjectExecutionRuntimeContext(
|
||||
useAppStore.getState(),
|
||||
targetWorkspaceId as string
|
||||
)
|
||||
}) &&
|
||||
readLocalRuntimeCapabilities().includes(
|
||||
STRUCTURED_AGENT_SESSION_RESUME_HISTORY_RUNTIME_CAPABILITY
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import type { Repo } from '../../../../shared/repo-types'
|
||||
import type { Worktree } from '../../../../shared/worktree/types'
|
||||
import type { AiVaultSessionWorktreeInfo } from './ai-vault-session-worktree'
|
||||
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import { resolveAiVaultSessionLaunchTarget } from './ai-vault-session-launch-actions'
|
||||
import { resolveAiVaultSessionLaunchTarget } from './ai-vault-session-launch-target'
|
||||
import {
|
||||
aiVaultSessionResumeLabel,
|
||||
aiVaultSessionRowResumeGating,
|
||||
|
||||
Reference in New Issue
Block a user