-
diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx
index d9ea283a0a5..29b21ff071a 100644
--- a/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx
+++ b/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx
@@ -1,5 +1,5 @@
import type React from 'react'
-import { FileJson, FolderGit2, MessageSquare, Play } from 'lucide-react'
+import { FileJson, FolderGit2, MessageSquare, MessageSquarePlus, Play } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
@@ -28,6 +28,7 @@ export function SessionInlineDetails({
resumeActions,
onResumeInWorktree,
onResumeInNewTab,
+ onContinueInNewSession,
onOpenLog
}: {
id: string
@@ -40,6 +41,7 @@ export function SessionInlineDetails({
}
onResumeInWorktree: () => void
onResumeInNewTab: () => void
+ onContinueInNewSession?: () => void
onOpenLog?: () => void
}): React.JSX.Element {
// A zero-turn transcript would resume into an empty conversation, so the plain
@@ -115,8 +117,27 @@ export function SessionInlineDetails({
) : null}
- {showResumeInWorktree || showResumeInNewTab || onOpenLog ? (
+ {showResumeInWorktree || showResumeInNewTab || onContinueInNewSession || onOpenLog ? (
+ {onContinueInNewSession ? (
+
+ ) : null}
{showResumeInWorktree ? (
@@ -211,6 +250,7 @@ export function SessionRowTrailingActions({
resumeDisabled={resumeDisabled}
resumeLabel={resumeLabel}
onResume={onResume}
+ onContinueInNewSession={onContinueInNewSession}
onJumpToOriginalPane={onJumpToOriginalPane}
showJumpToWorktree={showJumpToWorktree}
onJumpToWorktree={onJumpToWorktree}
diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-continuation.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-continuation.test.ts
new file mode 100644
index 00000000000..28ccfbe056f
--- /dev/null
+++ b/src/renderer/src/components/right-sidebar/ai-vault-session-continuation.test.ts
@@ -0,0 +1,81 @@
+import { describe, expect, it } from 'vitest'
+import type { AiVaultSession } from '../../../../shared/ai-vault-types'
+import {
+ canContinueAiVaultSessionInNewSession,
+ prepareAiVaultSessionContinuation
+} from './ai-vault-session-continuation'
+
+function session(agent: AiVaultSession['agent'] = 'claude'): AiVaultSession {
+ return {
+ id: 'session-row-1',
+ executionHostId: 'local',
+ executionHostPlatform: 'darwin',
+ agent,
+ sessionId: `${agent}-session-1`,
+ title: 'Finish the editor refactor',
+ cwd: '/Users/ada/Desktop/Client App',
+ branch: 'main',
+ model: null,
+ filePath: `/Users/ada/.${agent}/projects/client/session.jsonl`,
+ codexHome: null,
+ createdAt: null,
+ updatedAt: null,
+ modifiedAt: '2026-07-15T02:00:00.000Z',
+ messageCount: 3,
+ totalTokens: 1200,
+ lastUserPrompt: 'Finish the editor refactor',
+ previewMessages: [
+ { role: 'user', text: 'Finish the editor refactor', timestamp: null },
+ { role: 'assistant', text: 'The component tests still need work.', timestamp: null },
+ { role: 'user', text: 'Tool output that is not a user request', timestamp: null }
+ ],
+ queuedMessageCount: 0,
+ subagentTranscriptCount: 0,
+ resumeCommand: `${agent} --resume session-1`,
+ subagent: null
+ }
+}
+
+describe('AI Vault session continuation', () => {
+ it('supports both cross-Agent and same-Agent continuation', () => {
+ expect(canContinueAiVaultSessionInNewSession(session('claude'), 'worktree-1')).toBe(true)
+ expect(canContinueAiVaultSessionInNewSession(session('codex'), 'worktree-1')).toBe(true)
+ expect(canContinueAiVaultSessionInNewSession(session(), null)).toBe(false)
+ })
+
+ it('preserves the transcript, stopping point, and historical cwd', () => {
+ const request = prepareAiVaultSessionContinuation({
+ session: session(),
+ targetWorktreeId: 'worktree-1',
+ targetWorkspacePath: '/Users/ada/Desktop/current-worktree'
+ })
+
+ expect(request).toMatchObject({
+ worktreeId: 'worktree-1',
+ workspacePath: '/Users/ada/Desktop/current-worktree',
+ initialCwd: '/Users/ada/Desktop/Client App',
+ launchSource: 'sidebar',
+ source: {
+ sourceAgent: 'claude',
+ lastPrompt: 'Finish the editor refactor',
+ lastAssistantMessage: 'The component tests still need work.'
+ }
+ })
+ expect(request.source.transcriptPath).toContain('session.jsonl')
+ expect(request.source.capturedText).toContain('assistant: The component tests still need work.')
+ })
+
+ it('never treats a preview tool result as the user prompt', () => {
+ const sourceSession = session()
+ sourceSession.lastUserPrompt = null
+
+ const request = prepareAiVaultSessionContinuation({
+ session: sourceSession,
+ targetWorktreeId: 'worktree-1',
+ targetWorkspacePath: '/Users/ada/Desktop/current-worktree'
+ })
+
+ expect(request.source.lastPrompt).toBeNull()
+ expect(request.source.lastAssistantMessage).toBe('The component tests still need work.')
+ })
+})
diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-continuation.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-continuation.ts
new file mode 100644
index 00000000000..3baff2bf272
--- /dev/null
+++ b/src/renderer/src/components/right-sidebar/ai-vault-session-continuation.ts
@@ -0,0 +1,48 @@
+import type { AgentSessionContinuationRequest } from '@/lib/agent-session-continuation'
+import type { AiVaultSession } from '../../../../shared/ai-vault-types'
+
+export function canContinueAiVaultSessionInNewSession(
+ session: AiVaultSession,
+ targetWorktreeId: string | null | undefined
+): boolean {
+ return Boolean(
+ targetWorktreeId &&
+ (session.filePath.trim() || session.previewMessages.some((message) => message.text.trim()))
+ )
+}
+
+export function prepareAiVaultSessionContinuation(args: {
+ session: AiVaultSession
+ targetWorktreeId: string
+ targetWorkspacePath: string
+}): AgentSessionContinuationRequest {
+ const { session, targetWorktreeId, targetWorkspacePath } = args
+ return {
+ source: {
+ capturedText: previewTranscript(session),
+ sourceAgent: session.agent,
+ sourceTitle: session.title,
+ sourceWorkingDirectory: session.cwd,
+ transcriptPath: session.filePath.trim() || null,
+ // Why: preview user entries can be tool results or injected skill text; only provider-authenticated prompts are safe hints.
+ lastPrompt: session.lastUserPrompt ?? null,
+ lastAssistantMessage: latestAssistantPreview(session)
+ },
+ worktreeId: targetWorktreeId,
+ workspacePath: targetWorkspacePath,
+ // Why: sessions can outlive their worktree selection, but continuation should preserve their recorded cwd.
+ initialCwd: session.cwd || targetWorkspacePath,
+ launchSource: 'sidebar'
+ }
+}
+
+function latestAssistantPreview(session: AiVaultSession): string | null {
+ return session.previewMessages.findLast((message) => message.role === 'assistant')?.text ?? null
+}
+
+function previewTranscript(session: AiVaultSession): string {
+ return session.previewMessages
+ .filter((message) => message.text.trim())
+ .map((message) => `${message.role}: ${message.text.trim()}`)
+ .join('\n\n')
+}
diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts
index ef0bba73b3f..5540b8f3059 100644
--- a/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts
+++ b/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts
@@ -1,4 +1,4 @@
-import { useCallback } from 'react'
+import { useCallback, useState } from 'react'
import { toast } from 'sonner'
import {
buildAiVaultResumeCopyCommandForWorktree,
@@ -26,6 +26,9 @@ import {
isKnownAiVaultResumeWorkspaceTarget,
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'
export function useAiVaultSessionLaunchActions({
activeWorktree,
@@ -41,7 +44,13 @@ export function useAiVaultSessionLaunchActions({
buildResumeStartup: (session: AiVaultSession, worktreeId?: string | null) => AiVaultResumeStartup
copyResumeCommand: (session: AiVaultSession, worktreeId?: string | null) => Promise
handleResume: (session: AiVaultSession, targetWorktreeId?: string) => void
+ handleContinueInNewSession: (session: AiVaultSession, targetWorktreeId: string) => void
+ continuationRequest: AgentSessionContinuationRequest | null
+ handleContinuationDialogOpenChange: (open: boolean) => void
} {
+ const [continuationRequest, setContinuationRequest] =
+ useState(null)
+
const buildResumeCommand = useCallback(
(session: AiVaultSession, worktreeId?: string | null): string =>
buildAiVaultResumeCopyCommandForWorktree({
@@ -86,25 +95,14 @@ export function useAiVaultSessionLaunchActions({
const handleResume = useCallback(
(session: AiVaultSession, targetWorktreeId?: string): void => {
- const targetId = resolveAiVaultSessionLaunchTarget({
+ const targetId = resolveAiVaultSessionLaunchTargetOrNotify({
sessionFilePath: session.filePath,
sessionExecutionHostId: session.executionHostId,
activeWorktreeId: activeWorktreeId ?? activeWorktree?.id ?? null,
targetWorktreeId,
targetState
})
- if (targetId.status === 'missing') {
- toast.error(
- translate(
- 'auto.components.right.sidebar.AiVaultPanel.openWorkspaceBeforeResuming',
- 'Open a workspace before resuming a session.'
- )
- )
- return
- }
-
- if (targetId.status === 'unsupported') {
- toast.error(aiVaultResumeUnsupportedMessage(targetId.targetStatus))
+ if (!targetId) {
return
}
@@ -154,7 +152,72 @@ export function useAiVaultSessionLaunchActions({
[activeWorktree?.id, activeWorktreeId, buildResumeStartup, targetState]
)
- return { buildResumeStartup, copyResumeCommand, handleResume }
+ const handleContinueInNewSession = useCallback(
+ (session: AiVaultSession, targetWorktreeId: string): void => {
+ const targetId = resolveAiVaultSessionLaunchTargetOrNotify({
+ sessionFilePath: session.filePath,
+ sessionExecutionHostId: session.executionHostId,
+ activeWorktreeId: activeWorktreeId ?? activeWorktree?.id ?? null,
+ targetWorktreeId,
+ targetState
+ })
+ if (!targetId) {
+ return
+ }
+
+ const targetWorkspacePath = resolveAiVaultTargetWorkspacePath(
+ targetState,
+ targetId.worktreeId
+ )
+ if (!targetWorkspacePath) {
+ toast.error(
+ translate(
+ 'auto.components.right.sidebar.AiVaultPanel.openWorkspaceBeforeResuming',
+ 'Open a workspace before resuming a session.'
+ )
+ )
+ return
+ }
+ setContinuationRequest(
+ prepareAiVaultSessionContinuation({
+ session,
+ targetWorktreeId: targetId.worktreeId,
+ targetWorkspacePath
+ })
+ )
+ },
+ [activeWorktree?.id, activeWorktreeId, targetState]
+ )
+
+ const handleContinuationDialogOpenChange = useCallback((open: boolean): void => {
+ if (!open) {
+ setContinuationRequest(null)
+ }
+ }, [])
+
+ return {
+ buildResumeStartup,
+ copyResumeCommand,
+ handleResume,
+ handleContinueInNewSession,
+ continuationRequest,
+ handleContinuationDialogOpenChange
+ }
+}
+
+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 =
@@ -199,6 +262,26 @@ export function resolveAiVaultSessionLaunchTarget(args: {
return { status: 'ready', worktreeId: targetWorktreeId }
}
+function resolveAiVaultSessionLaunchTargetOrNotify(
+ args: Parameters[0]
+): Extract | null {
+ const target = resolveAiVaultSessionLaunchTarget(args)
+ if (target.status === 'missing') {
+ toast.error(
+ translate(
+ 'auto.components.right.sidebar.AiVaultPanel.openWorkspaceBeforeResuming',
+ 'Open a workspace before resuming a session.'
+ )
+ )
+ return null
+ }
+ if (target.status === 'unsupported') {
+ toast.error(aiVaultResumeUnsupportedMessage(target.targetStatus))
+ return null
+ }
+ return target
+}
+
function aiVaultResumeUnsupportedMessage(
targetStatus: ReturnType
): string {
diff --git a/src/renderer/src/components/terminal-pane/AgentSessionContinuationMenuItem.tsx b/src/renderer/src/components/terminal-pane/AgentSessionContinuationMenuItem.tsx
new file mode 100644
index 00000000000..ae8340016bf
--- /dev/null
+++ b/src/renderer/src/components/terminal-pane/AgentSessionContinuationMenuItem.tsx
@@ -0,0 +1,21 @@
+import { MessageSquarePlus } from 'lucide-react'
+import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
+import { translate } from '@/i18n/i18n'
+
+type AgentSessionContinuationMenuItemProps = {
+ onSelect: () => void
+}
+
+export function AgentSessionContinuationMenuItem({
+ onSelect
+}: AgentSessionContinuationMenuItemProps): React.JSX.Element {
+ return (
+
+
+ {translate(
+ 'components.agentSessionContinuation.continueInNewSession',
+ 'Continue in New Session…'
+ )}
+
+ )
+}
diff --git a/src/renderer/src/components/terminal-pane/TerminalContextMenu.test.tsx b/src/renderer/src/components/terminal-pane/TerminalContextMenu.test.tsx
index 0cfa3f1619f..33672553d17 100644
--- a/src/renderer/src/components/terminal-pane/TerminalContextMenu.test.tsx
+++ b/src/renderer/src/components/terminal-pane/TerminalContextMenu.test.tsx
@@ -66,6 +66,8 @@ function renderMenu(overrides: Record = {}): void {
onEqualizePaneSizes: vi.fn(),
onClosePane: vi.fn(),
onClearScreen: vi.fn(),
+ canContinueAgentSessionInNewSession: false,
+ onContinueAgentSessionInNewSession: vi.fn(),
onForkAgentSession: vi.fn(),
canToggleNativeChat: false,
isNativeChatView: false,
@@ -114,6 +116,22 @@ describe('TerminalContextMenu', () => {
expect(onForkAgentSession).not.toHaveBeenCalled()
})
+ it('shows new-session continuation only for eligible agent panes', () => {
+ const onContinueAgentSessionInNewSession = vi.fn()
+ renderMenu({
+ canContinueAgentSessionInNewSession: true,
+ onContinueAgentSessionInNewSession
+ })
+
+ const handoffItem = items.list.find(
+ (item) => childrenText(item.children) === 'Continue in New Session…'
+ )
+ expect(handoffItem).toBeDefined()
+
+ handoffItem?.onSelect?.()
+ expect(onContinueAgentSessionInNewSession).toHaveBeenCalledTimes(1)
+ })
+
it('shows one shortcut per terminal menu action on Windows', () => {
vi.stubGlobal('navigator', {
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
diff --git a/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx b/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx
index 749b1e98a28..353887352ce 100644
--- a/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx
+++ b/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx
@@ -37,6 +37,7 @@ import { AgentIcon } from '@/lib/agent-catalog'
import type { KeybindingOverrides } from '../../../../shared/keybindings'
import { translate } from '@/i18n/i18n'
import { isMacPlatform, nativeChatToggleShortcutLabel } from '../native-chat/native-chat-shortcut'
+import { AgentSessionContinuationMenuItem } from './AgentSessionContinuationMenuItem'
type TerminalContextMenuProps = {
open: boolean
@@ -55,6 +56,8 @@ type TerminalContextMenuProps = {
onEqualizePaneSizes: () => void
onClosePane: () => void
onClearScreen: () => void
+ canContinueAgentSessionInNewSession: boolean
+ onContinueAgentSessionInNewSession: () => void
onForkAgentSession: () => void
canToggleNativeChat: boolean
isNativeChatView: boolean
@@ -90,6 +93,8 @@ export default function TerminalContextMenu({
onEqualizePaneSizes,
onClosePane,
onClearScreen,
+ canContinueAgentSessionInNewSession,
+ onContinueAgentSessionInNewSession,
onForkAgentSession,
canToggleNativeChat,
isNativeChatView,
@@ -263,6 +268,9 @@ export default function TerminalContextMenu({