From f55154143bf0273219a70f31437af3652db99bb9 Mon Sep 17 00:00:00 2001
From: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Date: Thu, 25 Jun 2026 00:17:57 -0700
Subject: [PATCH 1/4] Update mobile Android APK links
Point mobile Android APK links at the published mobile-android-v0.0.16 release.
---
README.md | 4 ++--
docs/readme/README.es.md | 4 ++--
docs/readme/README.ja.md | 4 ++--
docs/readme/README.ko.md | 4 ++--
docs/readme/README.zh-CN.md | 4 ++--
src/renderer/src/components/mobile/mobile-platform-copy.ts | 2 +-
src/renderer/src/components/settings/MobileSettingsPane.tsx | 2 +-
7 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/README.md b/README.md
index e72cdfd1ee1..eea0b479af1 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@
Monitor and steer your agents from your phone — get notified when an agent finishes and send follow-ups from anywhere.
-[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.15/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
+[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.16/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
@@ -229,7 +229,7 @@ yay -S stably-orca-bin
Pair with your desktop app to monitor and steer your agents from your phone.
- **iOS:** [Download on the App Store](https://apps.apple.com/us/app/orca-ide/id6766130217)
-- **Android:** [Download the APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.15/app-release.apk)
+- **Android:** [Download the APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.16/app-release.apk)
---
diff --git a/docs/readme/README.es.md b/docs/readme/README.es.md
index 4ef58db2856..cb78aa38095 100644
--- a/docs/readme/README.es.md
+++ b/docs/readme/README.es.md
@@ -36,7 +36,7 @@
Supervisa y dirige a tus agentes desde el teléfono — recibe una notificación cuando un agente termine y envía instrucciones de seguimiento desde cualquier lugar.
-[App Store de iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [APK para Android](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.15/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
+[App Store de iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [APK para Android](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.16/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
@@ -227,7 +227,7 @@ yay -S stably-orca-bin
Vincúlala con tu app de escritorio para supervisar y dirigir a tus agentes desde el teléfono.
- **iOS:** [Descargar desde App Store](https://apps.apple.com/us/app/orca-ide/id6766130217)
-- **Android:** [Descargar el APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.15/app-release.apk)
+- **Android:** [Descargar el APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.16/app-release.apk)
---
diff --git a/docs/readme/README.ja.md b/docs/readme/README.ja.md
index 6f5e62ae72f..e2d93abd1f8 100644
--- a/docs/readme/README.ja.md
+++ b/docs/readme/README.ja.md
@@ -36,7 +36,7 @@
スマートフォンからエージェントを監視・操作 — エージェントの完了を通知で受け取り、どこからでもフォローアップを送信できます。
-[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.15/app-release.apk) · [ドキュメント →](https://www.onorca.dev/docs/mobile)
+[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.16/app-release.apk) · [ドキュメント →](https://www.onorca.dev/docs/mobile)
@@ -227,7 +227,7 @@ yay -S stably-orca-bin
デスクトップアプリとペアリングして、スマートフォンからエージェントを監視・操作できます。
- **iOS:** [App Store からダウンロード](https://apps.apple.com/us/app/orca-ide/id6766130217)
-- **Android:** [APK をダウンロード](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.15/app-release.apk)
+- **Android:** [APK をダウンロード](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.16/app-release.apk)
---
diff --git a/docs/readme/README.ko.md b/docs/readme/README.ko.md
index f4526d4d89c..4a63f512b9f 100644
--- a/docs/readme/README.ko.md
+++ b/docs/readme/README.ko.md
@@ -36,7 +36,7 @@
휴대폰에서 에이전트를 모니터링하고 조종하세요 — 에이전트가 완료되면 알림을 받고 어디서든 후속 지시를 보낼 수 있습니다.
-[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.15/app-release.apk) · [문서 →](https://www.onorca.dev/docs/mobile)
+[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.16/app-release.apk) · [문서 →](https://www.onorca.dev/docs/mobile)
@@ -227,7 +227,7 @@ yay -S stably-orca-bin
데스크톱 앱과 페어링해 휴대폰에서 에이전트를 모니터링하고 조종하세요.
- **iOS:** [App Store에서 다운로드](https://apps.apple.com/us/app/orca-ide/id6766130217)
-- **Android:** [APK 다운로드](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.15/app-release.apk)
+- **Android:** [APK 다운로드](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.16/app-release.apk)
---
diff --git a/docs/readme/README.zh-CN.md b/docs/readme/README.zh-CN.md
index 5babfc4b531..b3e97dd77cf 100644
--- a/docs/readme/README.zh-CN.md
+++ b/docs/readme/README.zh-CN.md
@@ -36,7 +36,7 @@
用手机监控并指挥你的智能体 — 智能体完成时收到通知,随时随地发送后续指令。
-[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.15/app-release.apk) · [文档 →](https://www.onorca.dev/docs/mobile)
+[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.16/app-release.apk) · [文档 →](https://www.onorca.dev/docs/mobile)
@@ -227,7 +227,7 @@ yay -S stably-orca-bin
与桌面应用配对,用手机监控并指挥你的智能体。
- **iOS:** [从 App Store 下载](https://apps.apple.com/us/app/orca-ide/id6766130217)
-- **Android:** [下载 APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.15/app-release.apk)
+- **Android:** [下载 APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.16/app-release.apk)
---
diff --git a/src/renderer/src/components/mobile/mobile-platform-copy.ts b/src/renderer/src/components/mobile/mobile-platform-copy.ts
index a5a3d5660ed..1539a590535 100644
--- a/src/renderer/src/components/mobile/mobile-platform-copy.ts
+++ b/src/renderer/src/components/mobile/mobile-platform-copy.ts
@@ -23,6 +23,6 @@ export const PLATFORM_COPY: Record<
)
},
ctaLabel: 'Download APK',
- url: 'https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.15/app-release.apk'
+ url: 'https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.16/app-release.apk'
}
}
diff --git a/src/renderer/src/components/settings/MobileSettingsPane.tsx b/src/renderer/src/components/settings/MobileSettingsPane.tsx
index b5a5d850c1a..016409893a8 100644
--- a/src/renderer/src/components/settings/MobileSettingsPane.tsx
+++ b/src/renderer/src/components/settings/MobileSettingsPane.tsx
@@ -9,7 +9,7 @@ export { getMobileSettingsPaneSearchEntries }
const ORCA_IOS_APP_STORE_URL = 'https://apps.apple.com/app/orca-ide/id6766130217'
const ORCA_ANDROID_APK_URL =
- 'https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.15/app-release.apk'
+ 'https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.16/app-release.apk'
export function MobileSettingsPane(): React.JSX.Element {
return (
From 9f0770da2b770392890a8553ad54d0abcc941499 Mon Sep 17 00:00:00 2001
From: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Date: Thu, 25 Jun 2026 00:28:13 -0700
Subject: [PATCH 2/4] Enhance AI Vault sessions with worktree resume and jump
actions (#6338)
- Add support for resuming agent sessions in their corresponding worktree
instead of defaulting exclusively to the active workspace.
- Implement "Jump to Original Pane" and "Jump to Worktree" actions to
quickly navigate to the session's workspace or terminal pane.
- Display worktree metadata and statuses (current, active, archived, or
unavailable) on session cards and inline details.
- Refactor launch actions, original pane lookup, and worktree mapping into
dedicated modules with comprehensive unit tests.
- Update internationalization files for all supported locales.
---
.../components/right-sidebar/AiVaultPanel.tsx | 127 +++-----
.../right-sidebar/AiVaultSessionDetails.tsx | 270 +++++++++++-----
.../right-sidebar/AiVaultSessionRow.tsx | 199 +++++++++---
.../AiVaultSessionVirtualList.tsx | 101 +++++-
.../SessionRowTrailingActions.tsx | 124 +++++++-
.../ai-vault-original-pane-actions.ts | 74 +++++
.../ai-vault-original-pane.test.ts | 213 +++++++++++++
.../right-sidebar/ai-vault-original-pane.ts | 262 ++++++++++++++++
.../ai-vault-session-display.test.ts | 19 ++
.../right-sidebar/ai-vault-session-display.ts | 52 ++++
.../ai-vault-session-launch-actions.ts | 113 +++++++
.../ai-vault-session-resume.test.ts | 175 +++++++++++
.../right-sidebar/ai-vault-session-resume.ts | 140 +++++++++
.../ai-vault-session-worktree.test.ts | 229 ++++++++++++++
.../ai-vault-session-worktree.ts | 289 ++++++++++++++++++
.../right-sidebar/ai-vault-virtual-rows.ts | 2 +-
src/renderer/src/i18n/locales/en.json | 26 +-
src/renderer/src/i18n/locales/es.json | 128 ++++----
src/renderer/src/i18n/locales/ja.json | 130 ++++----
src/renderer/src/i18n/locales/ko.json | 36 ++-
src/renderer/src/i18n/locales/zh.json | 44 ++-
.../lib/project-host-workspace-target.test.ts | 5 +-
22 files changed, 2395 insertions(+), 363 deletions(-)
create mode 100644 src/renderer/src/components/right-sidebar/ai-vault-original-pane-actions.ts
create mode 100644 src/renderer/src/components/right-sidebar/ai-vault-original-pane.test.ts
create mode 100644 src/renderer/src/components/right-sidebar/ai-vault-original-pane.ts
create mode 100644 src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts
create mode 100644 src/renderer/src/components/right-sidebar/ai-vault-session-resume.test.ts
create mode 100644 src/renderer/src/components/right-sidebar/ai-vault-session-resume.ts
create mode 100644 src/renderer/src/components/right-sidebar/ai-vault-session-worktree.test.ts
create mode 100644 src/renderer/src/components/right-sidebar/ai-vault-session-worktree.ts
diff --git a/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx b/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx
index 77036fb5eac..5b00eaace39 100644
--- a/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx
+++ b/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx
@@ -1,10 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { toast } from 'sonner'
-import {
- buildAiVaultResumeCommandForWorktree,
- buildAiVaultResumeStartupForWorktree
-} from '@/lib/ai-vault-resume-command'
-import { launchAiVaultSessionInNewTab } from '@/lib/launch-ai-vault-session'
import { useAppStore } from '@/store'
import {
useActiveRepo,
@@ -14,7 +9,7 @@ import {
useRepoById,
useRepos
} from '@/store/selectors'
-import { agentLabel, filterAiVaultSessions, groupAiVaultSessions } from './ai-vault-session-filters'
+import { filterAiVaultSessions, groupAiVaultSessions } from './ai-vault-session-filters'
import {
deriveAiVaultScopeSessionPaths,
deriveAiVaultWorkspaceScopePaths
@@ -25,6 +20,13 @@ import {
normalizeAiVaultScopeForContext
} from './ai-vault-scope-state'
import { buildAiVaultProjectContext } from './ai-vault-session-projects'
+import {
+ resolveAiVaultSessionResumeActions,
+ resolveAiVaultSessionResumeState
+} from './ai-vault-session-resume'
+import { useAiVaultSessionLaunchActions } from './ai-vault-session-launch-actions'
+import { useAiVaultSessionWorktreeMap } from './ai-vault-session-worktree'
+import { useAiVaultOriginalPaneActions } from './ai-vault-original-pane-actions'
import {
AI_VAULT_AGENTS,
type AiVaultAgent,
@@ -47,7 +49,9 @@ export default function AiVaultPanel(): React.JSX.Element {
const allWorktrees = useAllWorktrees()
const projectHostSetupProjection = useProjectHostSetupProjection()
const settings = useAppStore((s) => s.settings)
- const agentCmdOverrides = settings?.agentCmdOverrides ?? {}
+ const agentCmdOverrides = settings?.agentCmdOverrides
+ const { getOriginalPaneTarget, jumpToOriginalPane, jumpToWorktree } =
+ useAiVaultOriginalPaneActions()
const [query, setQuery] = useState('')
const [scope, setScope] = useState(DEFAULT_AI_VAULT_SCOPE)
const [sort, setSort] = useState('updated')
@@ -101,6 +105,17 @@ export default function AiVaultPanel(): React.JSX.Element {
}).sessionProjectById,
[activeRepo, activeWorktree, allWorktrees, projectHostSetupProjection, repos, sessions]
)
+ const sessionWorktreeById = useAiVaultSessionWorktreeMap({
+ sessions,
+ worktrees: allWorktrees,
+ activeWorktreeId: activeWorktree?.id ?? null
+ })
+ const { buildResumeStartup, copyResumeCommand, handleResume } = useAiVaultSessionLaunchActions({
+ activeWorktree: activeWorktree ?? null,
+ allWorktrees,
+ repos,
+ agentCmdOverrides
+ })
const hasAllAgentsSelected = agents.length === AI_VAULT_AGENTS.length
const viewAdjustmentCount =
(hasAllAgentsSelected ? 0 : 1) +
@@ -169,41 +184,6 @@ export default function AiVaultPanel(): React.JSX.Element {
[filteredSessions, group, projectLabelByKey, sessionProjectById]
)
- const buildResumeCommand = useCallback(
- (session: AiVaultSession): string =>
- buildAiVaultResumeCommandForWorktree({
- state: useAppStore.getState(),
- worktreeId: activeWorktree?.id ?? null,
- session,
- commandOverride: agentCmdOverrides[session.agent]
- }),
- [activeWorktree?.id, agentCmdOverrides, settings]
- )
-
- const buildResumeStartup = useCallback(
- (session: AiVaultSession) =>
- buildAiVaultResumeStartupForWorktree({
- state: useAppStore.getState(),
- worktreeId: activeWorktree?.id ?? null,
- session,
- commandOverride: agentCmdOverrides[session.agent]
- }),
- [activeWorktree?.id, agentCmdOverrides, settings]
- )
-
- const copyResumeCommand = useCallback(
- async (session: AiVaultSession): Promise => {
- await window.api.ui.writeClipboardText(buildResumeCommand(session))
- toast.success(
- translate(
- 'auto.components.right.sidebar.AiVaultPanel.resumeCommandCopied',
- 'Resume command copied'
- )
- )
- },
- [buildResumeCommand]
- )
-
const copyText = useCallback(async (text: string, label: string): Promise => {
await window.api.ui.writeClipboardText(text)
toast.success(
@@ -213,40 +193,26 @@ export default function AiVaultPanel(): React.JSX.Element {
)
}, [])
- const handleResume = useCallback(
- (session: AiVaultSession): void => {
- if (!activeWorktree) {
- toast.error(
- translate(
- 'auto.components.right.sidebar.AiVaultPanel.openWorkspaceBeforeResuming',
- 'Open a workspace before resuming a session.'
- )
- )
- return
- }
- if (isRemoteWorktree) {
- toast.error(
- translate(
- 'auto.components.right.sidebar.AiVaultPanel.localWorkspacesOnly',
- 'Resume from history is only available in local workspaces.'
- )
- )
- return
- }
- launchAiVaultSessionInNewTab({
- agent: session.agent,
- worktreeId: activeWorktree.id,
- ...buildResumeStartup(session)
- })
- toast.success(
- translate(
- 'auto.components.right.sidebar.AiVaultPanel.agentSessionQueued',
- '{{value0}} session queued',
- { value0: agentLabel(session.agent) }
- )
- )
- },
- [activeWorktree, buildResumeStartup, isRemoteWorktree]
+ const getSessionResumeState = useCallback(
+ (session: AiVaultSession) =>
+ resolveAiVaultSessionResumeState({
+ worktreeInfo: sessionWorktreeById.get(session.id) ?? null,
+ activeWorktreeId: activeWorktree?.id ?? null,
+ worktrees: allWorktrees,
+ repos
+ }),
+ [activeWorktree?.id, allWorktrees, repos, sessionWorktreeById]
+ )
+
+ const getSessionResumeActions = useCallback(
+ (session: AiVaultSession) =>
+ resolveAiVaultSessionResumeActions({
+ worktreeInfo: sessionWorktreeById.get(session.id) ?? null,
+ activeWorktreeId: activeWorktree?.id ?? null,
+ worktrees: allWorktrees,
+ repos
+ }),
+ [activeWorktree?.id, allWorktrees, repos, sessionWorktreeById]
)
const setAgentEnabled = useCallback((agent: AiVaultAgent, enabled: boolean) => {
@@ -343,11 +309,16 @@ export default function AiVaultPanel(): React.JSX.Element {
sessionsCount={sessions.length}
filteredSessionsCount={filteredSessions.length}
error={error}
- resumeDisabled={!activeWorktree || isRemoteWorktree}
buildResumeStartup={buildResumeStartup}
+ getSessionResumeState={getSessionResumeState}
+ getSessionResumeActions={getSessionResumeActions}
+ getOriginalPaneTarget={getOriginalPaneTarget}
+ getWorktreeInfo={(session) => sessionWorktreeById.get(session.id) ?? null}
onToggleGroup={toggleGroup}
+ onJumpToOriginalPane={jumpToOriginalPane}
+ onJumpToWorktree={jumpToWorktree}
onResume={handleResume}
- onCopyResume={(session) => void copyResumeCommand(session)}
+ onCopyResume={(session, worktreeId) => void copyResumeCommand(session, worktreeId)}
onCopyId={(session) =>
void copyText(
session.sessionId,
diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx
index 387bc877f7b..58923a53479 100644
--- a/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx
+++ b/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx
@@ -1,33 +1,47 @@
import type React from 'react'
-import { Copy, Play } from 'lucide-react'
+import { FileJson, FolderGit2, MessageSquare, Play } from 'lucide-react'
import { Button } from '@/components/ui/button'
-import { AgentIcon } from '@/lib/agent-catalog'
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import type { AiVaultSession } from '../../../../shared/ai-vault-types'
-import { agentLabel } from './ai-vault-session-filters'
import { translate } from '@/i18n/i18n'
-import { recentSessionConversationTurns } from './ai-vault-session-display'
+import { sessionDetailConversationTurns } from './ai-vault-session-display'
+import {
+ aiVaultWorktreeCompactPath,
+ aiVaultWorktreeStatusLabel,
+ shouldShowAiVaultWorktreeStatusBadge,
+ type AiVaultSessionWorktreeInfo
+} from './ai-vault-session-worktree'
export function SessionInlineDetails({
id,
session,
- onResume,
- onCopyResume,
- resumeDisabled
+ worktreeInfo,
+ resumeActions,
+ onResumeInWorktree,
+ onResumeInNewTab,
+ onOpenLog
}: {
id: string
session: AiVaultSession
- onResume: () => void
- onCopyResume: () => void
- resumeDisabled: boolean
+ worktreeInfo: AiVaultSessionWorktreeInfo | null
+ resumeActions: {
+ worktree: { worktreeId: string | null; disabled: boolean }
+ newTab: { worktreeId: string | null; disabled: boolean }
+ }
+ onResumeInWorktree: () => void
+ onResumeInNewTab: () => void
+ onOpenLog: () => void
}): React.JSX.Element {
- const updatedAt = session.updatedAt ?? session.modifiedAt
- const recentTurns = recentSessionConversationTurns(session, 3)
+ const showResumeInWorktree = Boolean(resumeActions.worktree.worktreeId)
+ const showResumeInNewTab = !showResumeInWorktree || Boolean(resumeActions.newTab.worktreeId)
+ const detailTurns = sessionDetailConversationTurns(session, 3)
+ const worktreeDisplay = worktreeInfo
return (
event.stopPropagation()}
onClick={(event) => event.stopPropagation()}
onDoubleClick={(event) => event.stopPropagation()}
@@ -36,83 +50,88 @@ export function SessionInlineDetails({
event.stopPropagation()
}}
>
-
-
-
- {session.title}
-
-
-
+
}
label={translate(
'auto.components.right.sidebar.AiVaultSessionDetails.latestTurns',
'Latest turns'
)}
>
- {recentTurns.length > 0 ? (
-
- {recentTurns.map((turn, index) => (
-
0 ? (
+
+ {detailTurns.map((turn, index) => (
+
- {conversationRoleLabel(turn.role)}
- {turn.text}
-
+ role={turn.role}
+ text={turn.text}
+ />
))}
) : (
-
- {translate(
+
+ />
)}
-
-
-
-
-
{agentLabel(session.agent)}
-
·
-
- {translate(
- 'auto.components.right.sidebar.AiVaultSessionDetails.messageCount',
- '{{value0}} msgs',
- { value0: session.messageCount }
+ {worktreeDisplay ? (
+ }
+ label={translate(
+ 'auto.components.right.sidebar.AiVaultSessionDetails.worktree',
+ 'Worktree'
)}
-
-
·
-
-
+ >
+
+
+ ) : null}
-
-
{
- event.stopPropagation()
- onResume()
- }}
- className="h-7 justify-start px-2 text-[11px]"
- >
-
- {translate(
- 'auto.components.right.sidebar.AiVaultSessionDetails.resumeInNewTab',
- 'Resume in New Tab'
- )}
-
+
+ {showResumeInWorktree ? (
+
{
+ event.stopPropagation()
+ onResumeInWorktree()
+ }}
+ className="h-7 shrink-0 px-2.5 text-[11px]"
+ >
+
+ {translate(
+ 'auto.components.right.sidebar.AiVaultSessionDetails.resumeInWorktree',
+ 'Resume in Worktree'
+ )}
+
+ ) : null}
+ {showResumeInNewTab ? (
+
{
+ event.stopPropagation()
+ onResumeInNewTab()
+ }}
+ className="h-7 shrink-0 px-2.5 text-[11px]"
+ >
+
+ {translate(
+ 'auto.components.right.sidebar.AiVaultSessionRow.resumeInNewTab',
+ 'Resume in New Tab'
+ )}
+
+ ) : null}
{
event.stopPropagation()
- onCopyResume()
+ onOpenLog()
}}
- className="h-7 justify-start px-2 text-[11px]"
+ className="h-7 shrink-0 px-2.5 text-[11px] text-muted-foreground"
>
-
- {translate(
- 'auto.components.right.sidebar.AiVaultSessionDetails.copyResumeCommand',
- 'Copy Resume Command'
- )}
+
+ {translate('auto.components.right.sidebar.AiVaultSessionDetails.viewLog', 'View Log')}
@@ -136,18 +152,110 @@ export function SessionInlineDetails({
}
function SessionReceiptSection({
+ icon,
label,
children
}: {
+ icon: React.ReactNode
label: string
children: React.ReactNode
}): React.JSX.Element {
return (
-
-
- {label}
+
+
+ {icon}
+ {label}
{children}
+
+ )
+}
+
+function ConversationTurnCard({
+ role,
+ text
+}: {
+ role: AiVaultSession['previewMessages'][number]['role']
+ text: string
+}): React.JSX.Element {
+ const isUserTurn = role === 'user'
+
+ return (
+
+
+ {conversationRoleLabel(role)}
+
+
+ {text}
+
+
+ )
+}
+
+function WorktreeMetadataLines({
+ worktreeInfo
+}: {
+ worktreeInfo: AiVaultSessionWorktreeInfo
+}): React.JSX.Element {
+ const compactPath = aiVaultWorktreeCompactPath(worktreeInfo.path)
+ const pathLine =
+ compactPath && compactPath !== worktreeInfo.label ? compactPath : worktreeInfo.path
+ const showPathLine = Boolean(pathLine) && pathLine !== worktreeInfo.label
+
+ return (
+
+
+ {shouldShowAiVaultWorktreeStatusBadge(worktreeInfo.status) ? (
+ <>
+
+ {aiVaultWorktreeStatusLabel(worktreeInfo.status)}
+
+ ·
+ >
+ ) : null}
+
+ {worktreeInfo.label}
+
+
+ {showPathLine ? (
+
+ ) : null}
+
+ )
+}
+
+function WorktreePathHint({
+ compactPath,
+ fullPath
+}: {
+ compactPath: string
+ fullPath: string
+}): React.JSX.Element {
+ return (
+
+
+
+ {compactPath}
+
+
+
+ {fullPath}
+
+
+ )
+}
+
+function SessionDetailEmptyState({ message }: { message: string }): React.JSX.Element {
+ return (
+
+ {message}
)
}
diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx
index e63479ee269..eab39c0f2d3 100644
--- a/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx
+++ b/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx
@@ -1,6 +1,6 @@
import { useCallback } from 'react'
import type React from 'react'
-import { Copy, FileJson, FolderOpen, Play } from 'lucide-react'
+import { Copy, FileJson, FolderOpen, LocateFixed, PanelTopOpen, Play } from 'lucide-react'
import { DropdownMenuItem, DropdownMenuSeparator } from '@/components/ui/dropdown-menu'
import {
ContextMenu,
@@ -9,8 +9,13 @@ import {
ContextMenuSeparator,
ContextMenuTrigger
} from '@/components/ui/context-menu'
+import { Badge } from '@/components/ui/badge'
+import RepoBadgeLabel from '@/components/repo/RepoBadgeLabel'
import { AgentIcon } from '@/lib/agent-catalog'
import { cn } from '@/lib/utils'
+import { useRepoById } from '@/store/selectors'
+import { resolveRepoBadgeColor } from '../../../../shared/repo-badge-color'
+import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree-id'
import {
AI_VAULT_SESSION_DRAG_START_EVENT,
writeAiVaultSessionDragData
@@ -22,14 +27,27 @@ import { translate } from '@/i18n/i18n'
import { SessionInlineDetails, SessionTime } from './AiVaultSessionDetails'
import { latestSessionConversationTurn } from './ai-vault-session-display'
import { SessionRowTrailingActions } from './SessionRowTrailingActions'
+import type { AiVaultSessionResumeActions } from './ai-vault-session-resume'
+import {
+ aiVaultWorktreeStatusLabel,
+ shouldShowAiVaultWorktreeStatusBadge,
+ type AiVaultSessionWorktreeInfo
+} from './ai-vault-session-worktree'
export function VaultSessionRow({
session,
resumeStartup,
+ worktreeInfo,
detailsExpanded,
resumeDisabled,
onToggleDetails,
+ onJumpToOriginalPane,
+ onJumpToWorktree,
onResume,
+ resumeLabel,
+ resumeActions,
+ onResumeInWorktree,
+ onResumeInNewTab,
onCopyResume,
onCopyId,
onCopyPath,
@@ -39,10 +57,17 @@ export function VaultSessionRow({
}: {
session: AiVaultSession
resumeStartup: AiVaultResumeStartup
+ worktreeInfo: AiVaultSessionWorktreeInfo | null
detailsExpanded: boolean
resumeDisabled: boolean
onToggleDetails: () => void
+ onJumpToOriginalPane?: () => void
+ onJumpToWorktree?: () => void
onResume: () => void
+ resumeLabel: string
+ resumeActions: AiVaultSessionResumeActions
+ onResumeInWorktree: () => void
+ onResumeInNewTab: () => void
onCopyResume: () => void
onCopyId: () => void
onCopyPath: () => void
@@ -80,7 +105,10 @@ export function VaultSessionRow({
{
onToggleDetails()
}}
@@ -89,7 +117,7 @@ export function VaultSessionRow({
{session.title}
@@ -100,7 +128,11 @@ export function VaultSessionRow({
detailsId={detailsId}
detailsTooltip={detailsTooltip}
resumeDisabled={resumeDisabled}
+ resumeLabel={resumeLabel}
+ worktreeInfo={worktreeInfo}
onToggleDetails={onToggleDetails}
+ onJumpToOriginalPane={onJumpToOriginalPane}
+ onJumpToWorktree={onJumpToWorktree}
onResume={onResume}
onCopyResume={onCopyResume}
onCopyId={onCopyId}
@@ -111,29 +143,44 @@ export function VaultSessionRow({
onStartResumeDrag={startResumeDrag}
/>
-
- {latestTurn ? (
- <>
-
- {conversationRoleLabel(latestTurn.role)}
-
- : {latestTurn.text}
- >
- ) : (
- translate(
- 'auto.components.right.sidebar.AiVaultSessionRow.noPreviewAvailable',
- 'No conversation preview available'
- )
- )}
-
-
+ {detailsExpanded && worktreeInfo ? (
+
+
+
+ ) : null}
+ {!detailsExpanded ? (
+ <>
+
+ {latestTurn ? (
+ <>
+
+ {conversationRoleLabel(latestTurn.role)}
+
+ : {latestTurn.text}
+ >
+ ) : (
+ translate(
+ 'auto.components.right.sidebar.AiVaultSessionRow.noPreviewAvailable',
+ 'No conversation preview available'
+ )
+ )}
+
+
+ >
+ ) : null}
{detailsExpanded ? (
) : null}
@@ -142,6 +189,9 @@ export function VaultSessionRow({
void
+ onJumpToOriginalPane?: () => void
+ onJumpToWorktree?: () => void
onCopyResume: () => void
onCopyId: () => void
onCopyPath: () => void
@@ -181,12 +237,25 @@ export function SessionActionMenuItems({
return (
<>
+ {onJumpToOriginalPane ? (
+ -
+
+ {translate(
+ 'auto.components.right.sidebar.AiVaultSessionRow.jumpToOriginalPane',
+ 'Jump to Original Pane'
+ )}
+
+ ) : null}
+ -
+
+ {translate(
+ 'auto.components.right.sidebar.AiVaultSessionRow.jumpToWorktree',
+ 'Jump to Worktree'
+ )}
+
-
- {translate(
- 'auto.components.right.sidebar.AiVaultSessionRow.resumeInNewTab',
- 'Resume in New Tab'
- )}
+ {resumeLabel}
-
@@ -231,26 +300,74 @@ function getSessionDetailsId(sessionId: string): string {
return `ai-vault-session-details-${sessionId.replace(/[^A-Za-z0-9_-]/g, '-')}`
}
-function SessionMetadata({ session, updatedAt }: { session: AiVaultSession; updatedAt: string }) {
+function SessionMetadata({
+ session,
+ updatedAt,
+ worktreeInfo
+}: {
+ session: AiVaultSession
+ updatedAt: string
+ worktreeInfo: AiVaultSessionWorktreeInfo | null
+}) {
return (
-
-
-
-
-
{agentLabel(session.agent)}
-
- {translate(
- 'auto.components.right.sidebar.AiVaultSessionRow.messageCount',
- '{{value0}} msgs',
- { value0: session.messageCount }
- )}
-
-
·
-
+
+
+
+
+
+
{agentLabel(session.agent)}
+
+ {translate(
+ 'auto.components.right.sidebar.AiVaultSessionRow.messageCount',
+ '{{value0}} msgs',
+ { value0: session.messageCount }
+ )}
+
+
·
+
+
+ {worktreeInfo ?
: null}
)
}
+function SessionWorktreeLine({
+ worktreeInfo
+}: {
+ worktreeInfo: AiVaultSessionWorktreeInfo
+}): React.JSX.Element {
+ const repoId = worktreeInfo.worktreeId
+ ? (splitWorktreeIdForFilesystem(worktreeInfo.worktreeId)?.repoId ?? null)
+ : null
+ const repo = useRepoById(repoId)
+
+ return (
+
+ {shouldShowAiVaultWorktreeStatusBadge(worktreeInfo.status) ? (
+
+ {worktreeStatusLabel(worktreeInfo.status)}
+
+ ) : null}
+
+
+
+
+ )
+}
+
+function worktreeStatusLabel(status: AiVaultSessionWorktreeInfo['status']): string {
+ return aiVaultWorktreeStatusLabel(status)
+}
+
function conversationRoleLabel(role: AiVaultSession['previewMessages'][number]['role']): string {
if (role === 'user') {
return translate('auto.components.right.sidebar.AiVaultSessionRow.userRole', 'You')
diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx
index 9a9cfdf7a9b..bac36ba8aa8 100644
--- a/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx
+++ b/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx
@@ -8,6 +8,16 @@ import { getActiveStickyHeaderIndexForScroll } from '../sidebar/worktree-list-vi
import { EmptyState, SessionLoadingState, 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,
+ type AiVaultSessionResumeActions,
+ type AiVaultSessionResumeState
+} from './ai-vault-session-resume'
+import {
+ canJumpToAiVaultSessionWorktree,
+ type AiVaultSessionWorktreeInfo
+} from './ai-vault-session-worktree'
import {
extractVaultVirtualRowIndexes,
getVaultStickyHeaderIndexes,
@@ -16,7 +26,7 @@ import {
} from './ai-vault-virtual-rows'
const VAULT_ROW_OVERSCAN = 8
-const VAULT_EXPANDED_SESSION_ROW_ESTIMATED_HEIGHT = 360
+const VAULT_EXPANDED_SESSION_ROW_ESTIMATED_HEIGHT = 420
type AiVaultListRow =
| { type: 'group'; group: AiVaultSessionGroup }
@@ -29,9 +39,14 @@ export function AiVaultSessionVirtualList({
sessionsCount,
filteredSessionsCount,
error,
- resumeDisabled,
buildResumeStartup,
+ getOriginalPaneTarget,
+ getWorktreeInfo,
+ getSessionResumeState,
+ getSessionResumeActions,
onToggleGroup,
+ onJumpToOriginalPane,
+ onJumpToWorktree,
onResume,
onCopyResume,
onCopyId,
@@ -46,11 +61,16 @@ export function AiVaultSessionVirtualList({
sessionsCount: number
filteredSessionsCount: number
error: string | null
- resumeDisabled: boolean
- buildResumeStartup: (session: AiVaultSession) => AiVaultResumeStartup
+ buildResumeStartup: (session: AiVaultSession, worktreeId?: string | null) => AiVaultResumeStartup
+ getOriginalPaneTarget: (session: AiVaultSession) => AiVaultOriginalPaneTarget | null
+ getWorktreeInfo: (session: AiVaultSession) => AiVaultSessionWorktreeInfo | null
+ getSessionResumeState: (session: AiVaultSession) => AiVaultSessionResumeState
+ getSessionResumeActions: (session: AiVaultSession) => AiVaultSessionResumeActions
onToggleGroup: (key: string) => void
- onResume: (session: AiVaultSession) => void
- onCopyResume: (session: AiVaultSession) => void
+ onJumpToOriginalPane: (session: AiVaultSession) => void
+ onJumpToWorktree: (worktreeId: string) => void
+ onResume: (session: AiVaultSession, worktreeId: string) => void
+ onCopyResume: (session: AiVaultSession, worktreeId?: string | null) => void
onCopyId: (session: AiVaultSession) => void
onCopyPath: (session: AiVaultSession) => void
onOpenLog: (session: AiVaultSession) => void
@@ -163,10 +183,15 @@ export function AiVaultSessionVirtualList({
measureElement={virtualizer.measureElement}
collapsedGroups={collapsedGroups}
expandedSessionIds={expandedSessionIds}
- resumeDisabled={resumeDisabled}
buildResumeStartup={buildResumeStartup}
+ getOriginalPaneTarget={getOriginalPaneTarget}
+ getWorktreeInfo={getWorktreeInfo}
+ getSessionResumeState={getSessionResumeState}
+ getSessionResumeActions={getSessionResumeActions}
onToggleGroup={onToggleGroup}
onToggleSessionDetails={toggleSessionDetails}
+ onJumpToOriginalPane={onJumpToOriginalPane}
+ onJumpToWorktree={onJumpToWorktree}
onResume={onResume}
onCopyResume={onCopyResume}
onCopyId={onCopyId}
@@ -190,10 +215,15 @@ function AiVaultVirtualRow({
measureElement,
collapsedGroups,
expandedSessionIds,
- resumeDisabled,
buildResumeStartup,
+ getOriginalPaneTarget,
+ getWorktreeInfo,
+ getSessionResumeState,
+ getSessionResumeActions,
onToggleGroup,
onToggleSessionDetails,
+ onJumpToOriginalPane,
+ onJumpToWorktree,
onResume,
onCopyResume,
onCopyId,
@@ -209,12 +239,17 @@ function AiVaultVirtualRow({
measureElement: (node: Element | null) => void
collapsedGroups: ReadonlySet
expandedSessionIds: ReadonlySet
- resumeDisabled: boolean
- buildResumeStartup: (session: AiVaultSession) => AiVaultResumeStartup
+ buildResumeStartup: (session: AiVaultSession, worktreeId?: string | null) => AiVaultResumeStartup
+ getOriginalPaneTarget: (session: AiVaultSession) => AiVaultOriginalPaneTarget | null
+ getWorktreeInfo: (session: AiVaultSession) => AiVaultSessionWorktreeInfo | null
+ getSessionResumeState: (session: AiVaultSession) => AiVaultSessionResumeState
+ getSessionResumeActions: (session: AiVaultSession) => AiVaultSessionResumeActions
onToggleGroup: (key: string) => void
onToggleSessionDetails: (sessionId: string) => void
- onResume: (session: AiVaultSession) => void
- onCopyResume: (session: AiVaultSession) => void
+ onJumpToOriginalPane: (session: AiVaultSession) => void
+ onJumpToWorktree: (worktreeId: string) => void
+ onResume: (session: AiVaultSession, worktreeId: string) => void
+ onCopyResume: (session: AiVaultSession, worktreeId?: string | null) => void
onCopyId: (session: AiVaultSession) => void
onCopyPath: (session: AiVaultSession) => void
onOpenLog: (session: AiVaultSession) => void
@@ -226,6 +261,14 @@ function AiVaultVirtualRow({
}
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
+ const worktreeJumpId = canJumpToAiVaultSessionWorktree(worktreeInfo)
+ ? worktreeInfo?.worktreeId
+ : null
+ const resumeState = row.type === 'session' ? getSessionResumeState(row.session) : null
+ const resumeActions = row.type === 'session' ? getSessionResumeActions(row.session) : null
+ const resumeLabel = resumeState ? aiVaultSessionResumeLabel(resumeState) : ''
return (
onToggleSessionDetails(row.session.id)}
- onResume={() => onResume(row.session)}
- onCopyResume={() => onCopyResume(row.session)}
+ onJumpToOriginalPane={
+ originalPaneTarget ? () => onJumpToOriginalPane(row.session) : undefined
+ }
+ onJumpToWorktree={worktreeJumpId ? () => onJumpToWorktree(worktreeJumpId) : undefined}
+ onResume={() => {
+ if (resumeState?.worktreeId) {
+ onResume(row.session, resumeState.worktreeId)
+ }
+ }}
+ onResumeInWorktree={() => {
+ if (resumeActions?.worktree.worktreeId) {
+ onResume(row.session, resumeActions.worktree.worktreeId)
+ }
+ }}
+ onResumeInNewTab={() => {
+ if (resumeActions?.newTab.worktreeId) {
+ onResume(row.session, resumeActions.newTab.worktreeId)
+ }
+ }}
+ onCopyResume={() => onCopyResume(row.session, resumeState?.worktreeId)}
onCopyId={() => onCopyId(row.session)}
onCopyPath={() => onCopyPath(row.session)}
onOpenLog={() => onOpenLog(row.session)}
diff --git a/src/renderer/src/components/right-sidebar/SessionRowTrailingActions.tsx b/src/renderer/src/components/right-sidebar/SessionRowTrailingActions.tsx
index 144f1a3d98f..f0552b3fab3 100644
--- a/src/renderer/src/components/right-sidebar/SessionRowTrailingActions.tsx
+++ b/src/renderer/src/components/right-sidebar/SessionRowTrailingActions.tsx
@@ -1,6 +1,13 @@
import { useCallback, useEffect, useRef } from 'react'
import type React from 'react'
-import { ChevronDown, GripVertical, MoreHorizontal, Play } from 'lucide-react'
+import {
+ ChevronDown,
+ GripVertical,
+ LocateFixed,
+ MoreHorizontal,
+ PanelTopOpen,
+ Play
+} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import {
@@ -14,6 +21,10 @@ import type { AiVaultSession } from '../../../../shared/ai-vault-types'
import { agentLabel } from './ai-vault-session-filters'
import { translate } from '@/i18n/i18n'
import { SessionActionMenuItems } from './AiVaultSessionRow'
+import {
+ aiVaultWorktreeJumpTooltip,
+ type AiVaultSessionWorktreeInfo
+} from './ai-vault-session-worktree'
// Why: hover-only actions live on the title row and collapse on hover-capable
// devices so the prompt keeps the full width until the row is hovered.
@@ -36,7 +47,11 @@ export function SessionRowTrailingActions({
detailsId,
detailsTooltip,
resumeDisabled,
+ resumeLabel,
+ worktreeInfo,
onToggleDetails,
+ onJumpToOriginalPane,
+ onJumpToWorktree,
onResume,
onCopyResume,
onCopyId,
@@ -51,7 +66,11 @@ export function SessionRowTrailingActions({
detailsId: string
detailsTooltip: string
resumeDisabled: boolean
+ resumeLabel: string
+ worktreeInfo: AiVaultSessionWorktreeInfo | null
onToggleDetails: () => void
+ onJumpToOriginalPane?: () => void
+ onJumpToWorktree?: () => void
onResume: () => void
onCopyResume: () => void
onCopyId: () => void
@@ -77,6 +96,8 @@ export function SessionRowTrailingActions({
window.dispatchEvent(new Event(AI_VAULT_SESSION_DRAG_END_EVENT))
}, [])
+ const jumpToWorktreeTooltip = aiVaultWorktreeJumpTooltip(worktreeInfo)
+
// Cleanup drag state on unmount if a drag was in progress
useEffect(() => {
return () => {
@@ -126,17 +147,53 @@ export function SessionRowTrailingActions({
)}
+ {onJumpToOriginalPane ? (
+
+
+ {
+ event.stopPropagation()
+ onJumpToOriginalPane()
+ }}
+ data-testid="ai-vault-session-jump-original-pane"
+ className="can-hover:pointer-events-none group-hover/session-row:pointer-events-auto group-focus-within/session-row:pointer-events-auto focus-visible:pointer-events-auto"
+ >
+
+
+
+
+ {translate(
+ 'auto.components.right.sidebar.AiVaultSessionRow.jumpToOriginalPane',
+ 'Jump to Original Pane'
+ )}
+
+
+ ) : null}
+
+
+
+ {jumpToWorktreeTooltip}
+
+
{
@@ -153,10 +210,7 @@ export function SessionRowTrailingActions({
- {translate(
- 'auto.components.right.sidebar.AiVaultSessionRow.resumeInNewTab',
- 'Resume in New Tab'
- )}
+ {resumeLabel}
@@ -219,7 +273,10 @@ export function SessionRowTrailingActions({
)
}
+
+function WorktreeJumpTooltipTrigger({
+ disabled,
+ ariaLabel,
+ onJumpToWorktree
+}: {
+ disabled: boolean
+ ariaLabel: string
+ onJumpToWorktree?: () => void
+}): React.JSX.Element {
+ const button = (
+ {
+ event.stopPropagation()
+ onJumpToWorktree?.()
+ }}
+ data-testid="ai-vault-session-jump-worktree"
+ className={cn(
+ !disabled &&
+ 'can-hover:pointer-events-none group-hover/session-row:pointer-events-auto group-focus-within/session-row:pointer-events-auto focus-visible:pointer-events-auto'
+ )}
+ >
+
+
+ )
+
+ if (!disabled) {
+ return {button}
+ }
+
+ return (
+
+ event.stopPropagation()}
+ >
+ {button}
+
+
+ )
+}
diff --git a/src/renderer/src/components/right-sidebar/ai-vault-original-pane-actions.ts b/src/renderer/src/components/right-sidebar/ai-vault-original-pane-actions.ts
new file mode 100644
index 00000000000..8e6ebf5f947
--- /dev/null
+++ b/src/renderer/src/components/right-sidebar/ai-vault-original-pane-actions.ts
@@ -0,0 +1,74 @@
+import { useCallback } from 'react'
+import { useShallow } from 'zustand/react/shallow'
+import { toast } from 'sonner'
+import { activateTabAndFocusPane } from '@/lib/activate-tab-and-focus-pane'
+import { activateAndRevealWorktree } from '@/lib/worktree-activation'
+import { useAppStore } from '@/store'
+import type { AiVaultSession } from '../../../../shared/ai-vault-types'
+import { translate } from '@/i18n/i18n'
+import { findOriginalAiVaultSessionPane } from './ai-vault-original-pane'
+
+export function useAiVaultOriginalPaneActions(): {
+ getOriginalPaneTarget: (
+ session: AiVaultSession
+ ) => ReturnType
+ jumpToOriginalPane: (session: AiVaultSession) => void
+ jumpToWorktree: (worktreeId: string) => void
+} {
+ const originalPaneLookupState = useAppStore(
+ useShallow((s) => ({
+ agentStatusByPaneKey: s.agentStatusByPaneKey,
+ retainedAgentsByPaneKey: s.retainedAgentsByPaneKey,
+ sleepingAgentSessionsByPaneKey: s.sleepingAgentSessionsByPaneKey,
+ tabsByWorktree: s.tabsByWorktree,
+ terminalLayoutsByTabId: s.terminalLayoutsByTabId
+ }))
+ )
+
+ const getOriginalPaneTarget = useCallback(
+ (session: AiVaultSession) => findOriginalAiVaultSessionPane(originalPaneLookupState, session),
+ [originalPaneLookupState]
+ )
+
+ const jumpToOriginalPane = useCallback((session: AiVaultSession): void => {
+ const target = findOriginalAiVaultSessionPane(useAppStore.getState(), session)
+ if (!target) {
+ toast.error(
+ translate(
+ 'auto.components.right.sidebar.AiVaultPanel.originalPaneUnavailable',
+ 'Original pane is no longer available.'
+ )
+ )
+ return
+ }
+
+ if (!activateAndRevealWorktree(target.worktreeId)) {
+ toast.error(
+ translate(
+ 'auto.components.right.sidebar.AiVaultPanel.worktreeUnavailable',
+ 'Worktree is no longer available.'
+ )
+ )
+ return
+ }
+ const state = useAppStore.getState()
+ state.setActiveTabType('terminal')
+ activateTabAndFocusPane(target.tabId, target.leafId, {
+ flashFocusedPane: true,
+ scrollToBottomIfOutputSinceLastView: true
+ })
+ }, [])
+
+ const jumpToWorktree = useCallback((worktreeId: string): void => {
+ if (!activateAndRevealWorktree(worktreeId)) {
+ toast.error(
+ translate(
+ 'auto.components.right.sidebar.AiVaultPanel.worktreeUnavailable',
+ 'Worktree is no longer available.'
+ )
+ )
+ }
+ }, [])
+
+ return { getOriginalPaneTarget, jumpToOriginalPane, jumpToWorktree }
+}
diff --git a/src/renderer/src/components/right-sidebar/ai-vault-original-pane.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-original-pane.test.ts
new file mode 100644
index 00000000000..cec8a30a270
--- /dev/null
+++ b/src/renderer/src/components/right-sidebar/ai-vault-original-pane.test.ts
@@ -0,0 +1,213 @@
+import { describe, expect, it } from 'vitest'
+import type { AiVaultSession } from '../../../../shared/ai-vault-types'
+import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
+import type { SleepingAgentSessionRecord } from '../../../../shared/agent-session-resume'
+import { makePaneKey } from '../../../../shared/stable-pane-id'
+import { findOriginalAiVaultSessionPane } from './ai-vault-original-pane'
+
+const LEAF_ID = '11111111-1111-4111-8111-111111111111'
+const OTHER_LEAF_ID = '22222222-2222-4222-8222-222222222222'
+
+const baseSession: AiVaultSession = {
+ id: 'codex:session-1',
+ agent: 'codex',
+ sessionId: 'session-1',
+ title: 'Fix the pane focus',
+ cwd: '/repo',
+ branch: null,
+ model: null,
+ filePath: '/home/ada/.codex/session-1.jsonl',
+ codexHome: null,
+ createdAt: null,
+ updatedAt: '2026-06-24T10:00:00.000Z',
+ modifiedAt: '2026-06-24T10:00:00.000Z',
+ messageCount: 2,
+ totalTokens: 42,
+ previewMessages: [],
+ resumeCommand: "codex resume 'session-1'"
+}
+
+function makeTab(id = 'tab-1', worktreeId = 'wt-1') {
+ return {
+ id,
+ ptyId: null,
+ worktreeId,
+ title: 'Agent',
+ customTitle: null,
+ color: null,
+ sortOrder: 0,
+ createdAt: 1
+ }
+}
+
+function makeLayout(leafId = LEAF_ID) {
+ return {
+ root: { type: 'leaf' as const, leafId },
+ activeLeafId: leafId,
+ expandedLeafId: null,
+ ptyIdsByLeafId: { [leafId]: 'pty-1' }
+ }
+}
+
+function makeState(overrides: Record = {}) {
+ return {
+ agentStatusByPaneKey: {},
+ retainedAgentsByPaneKey: {},
+ sleepingAgentSessionsByPaneKey: {},
+ tabsByWorktree: { 'wt-1': [makeTab()] },
+ terminalLayoutsByTabId: { 'tab-1': makeLayout() },
+ ...overrides
+ } as never
+}
+
+function makeEntry(overrides: Partial = {}): AgentStatusEntry {
+ const paneKey = makePaneKey('tab-1', LEAF_ID)
+ return {
+ state: 'working',
+ prompt: 'continue',
+ updatedAt: 1,
+ stateStartedAt: 1,
+ agentType: 'codex',
+ paneKey,
+ tabId: 'tab-1',
+ worktreeId: 'wt-1',
+ stateHistory: [],
+ providerSession: { key: 'session_id', id: 'session-1' },
+ ...overrides
+ }
+}
+
+function makeSleepingRecord(
+ overrides: Partial = {}
+): SleepingAgentSessionRecord {
+ return {
+ paneKey: makePaneKey('tab-1', LEAF_ID),
+ tabId: 'tab-1',
+ worktreeId: 'wt-1',
+ agent: 'codex',
+ providerSession: { key: 'session_id', id: 'session-1' },
+ prompt: 'continue',
+ state: 'working',
+ capturedAt: 1,
+ updatedAt: 1,
+ origin: 'live',
+ ...overrides
+ }
+}
+
+describe('findOriginalAiVaultSessionPane', () => {
+ it('finds a live pane with a matching provider session', () => {
+ const entry = makeEntry()
+
+ const target = findOriginalAiVaultSessionPane(
+ makeState({ agentStatusByPaneKey: { [entry.paneKey]: entry } }),
+ baseSession
+ )
+
+ expect(target).toEqual({
+ paneKey: entry.paneKey,
+ worktreeId: 'wt-1',
+ tabId: 'tab-1',
+ leafId: LEAF_ID
+ })
+ })
+
+ it('finds a unique live pane by prompt when provider session is not known yet', () => {
+ const entry = makeEntry({
+ providerSession: undefined,
+ prompt: 'Fix the pane focus'
+ })
+
+ const target = findOriginalAiVaultSessionPane(
+ makeState({ agentStatusByPaneKey: { [entry.paneKey]: entry } }),
+ baseSession
+ )
+
+ expect(target?.paneKey).toBe(entry.paneKey)
+ })
+
+ it('does not prompt-match when multiple panes are plausible', () => {
+ const firstPaneKey = makePaneKey('tab-1', LEAF_ID)
+ const secondPaneKey = makePaneKey('tab-2', OTHER_LEAF_ID)
+ const first = makeEntry({ paneKey: firstPaneKey, providerSession: undefined })
+ const second = makeEntry({
+ paneKey: secondPaneKey,
+ tabId: 'tab-2',
+ providerSession: undefined
+ })
+
+ const target = findOriginalAiVaultSessionPane(
+ makeState({
+ agentStatusByPaneKey: {
+ [first.paneKey]: first,
+ [second.paneKey]: second
+ },
+ tabsByWorktree: { 'wt-1': [makeTab('tab-1'), makeTab('tab-2')] },
+ terminalLayoutsByTabId: {
+ 'tab-1': makeLayout(LEAF_ID),
+ 'tab-2': makeLayout(OTHER_LEAF_ID)
+ }
+ }),
+ baseSession
+ )
+
+ expect(target).toBeNull()
+ })
+
+ it('does not return a target when the original leaf is gone', () => {
+ const entry = makeEntry()
+
+ const target = findOriginalAiVaultSessionPane(
+ makeState({
+ agentStatusByPaneKey: { [entry.paneKey]: entry },
+ terminalLayoutsByTabId: { 'tab-1': makeLayout(OTHER_LEAF_ID) }
+ }),
+ baseSession
+ )
+
+ expect(target).toBeNull()
+ })
+
+ it('finds a retained completed pane when the tab and layout still exist', () => {
+ const entry = makeEntry({ state: 'done' })
+
+ const target = findOriginalAiVaultSessionPane(
+ makeState({
+ retainedAgentsByPaneKey: {
+ [entry.paneKey]: {
+ entry,
+ worktreeId: 'wt-1',
+ tab: makeTab(),
+ agentType: 'codex',
+ startedAt: 1
+ }
+ }
+ }),
+ baseSession
+ )
+
+ expect(target?.leafId).toBe(LEAF_ID)
+ })
+
+ it('finds a preserved sleeping pane from a matching session record', () => {
+ const record = makeSleepingRecord()
+
+ const target = findOriginalAiVaultSessionPane(
+ makeState({ sleepingAgentSessionsByPaneKey: { [record.paneKey]: record } }),
+ baseSession
+ )
+
+ expect(target?.paneKey).toBe(record.paneKey)
+ })
+
+ it('resolves legacy numeric pane keys through the current tab layout', () => {
+ const record = makeSleepingRecord({ paneKey: 'tab-1:1' })
+
+ const target = findOriginalAiVaultSessionPane(
+ makeState({ sleepingAgentSessionsByPaneKey: { [record.paneKey]: record } }),
+ baseSession
+ )
+
+ expect(target?.leafId).toBe(LEAF_ID)
+ })
+})
diff --git a/src/renderer/src/components/right-sidebar/ai-vault-original-pane.ts b/src/renderer/src/components/right-sidebar/ai-vault-original-pane.ts
new file mode 100644
index 00000000000..a878789891c
--- /dev/null
+++ b/src/renderer/src/components/right-sidebar/ai-vault-original-pane.ts
@@ -0,0 +1,262 @@
+import type { AppState } from '@/store/types'
+import { resolveRuntimePaneTitleLeafId } from '@/lib/runtime-pane-title-leaf-id'
+import type { AiVaultSession } from '../../../../shared/ai-vault-types'
+import { parseLegacyNumericPaneKey, parsePaneKey } from '../../../../shared/stable-pane-id'
+import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode } from '../../../../shared/types'
+
+export type AiVaultOriginalPaneTarget = {
+ paneKey: string
+ worktreeId: string
+ tabId: string
+ leafId: string
+}
+
+type OriginalPaneState = Pick<
+ AppState,
+ | 'agentStatusByPaneKey'
+ | 'retainedAgentsByPaneKey'
+ | 'sleepingAgentSessionsByPaneKey'
+ | 'tabsByWorktree'
+ | 'terminalLayoutsByTabId'
+>
+
+function agentMatches(session: AiVaultSession, agent: string | undefined): boolean {
+ return agent === session.agent
+}
+
+function providerSessionMatches(session: AiVaultSession, providerSessionId: string | undefined) {
+ return providerSessionId === session.sessionId
+}
+
+function normalizeMatchText(value: string | null | undefined): string {
+ return value?.trim().replace(/\s+/g, ' ').toLowerCase() ?? ''
+}
+
+function longEnoughForPrefixMatch(value: string): boolean {
+ return value.length >= 24
+}
+
+function textMatchesSessionPrompt(sessionText: string, candidateText: string): boolean {
+ if (!sessionText || !candidateText) {
+ return false
+ }
+ if (sessionText === candidateText) {
+ return true
+ }
+ return (
+ longEnoughForPrefixMatch(sessionText) &&
+ longEnoughForPrefixMatch(candidateText) &&
+ (sessionText.startsWith(candidateText) || candidateText.startsWith(sessionText))
+ )
+}
+
+function sessionPromptCandidates(session: AiVaultSession): string[] {
+ const candidates = new Set()
+ const title = normalizeMatchText(session.title)
+ if (title) {
+ candidates.add(title)
+ }
+ for (const message of session.previewMessages) {
+ if (message.role !== 'user') {
+ continue
+ }
+ const text = normalizeMatchText(message.text)
+ if (text) {
+ candidates.add(text)
+ }
+ }
+ return [...candidates]
+}
+
+function entryPromptCandidates(entry: {
+ prompt?: string
+ stateHistory?: readonly { prompt: string }[]
+}): string[] {
+ const candidates = new Set()
+ const prompt = normalizeMatchText(entry.prompt)
+ if (prompt) {
+ candidates.add(prompt)
+ }
+ for (const historyEntry of entry.stateHistory ?? []) {
+ const text = normalizeMatchText(historyEntry.prompt)
+ if (text) {
+ candidates.add(text)
+ }
+ }
+ return [...candidates]
+}
+
+function promptsMatchSession(
+ session: AiVaultSession,
+ entry: Parameters[0]
+) {
+ const sessionCandidates = sessionPromptCandidates(session)
+ if (sessionCandidates.length === 0) {
+ return false
+ }
+ const entryCandidates = entryPromptCandidates(entry)
+ return sessionCandidates.some((sessionText) =>
+ entryCandidates.some((entryText) => textMatchesSessionPrompt(sessionText, entryText))
+ )
+}
+
+function layoutHasLeaf(node: TerminalPaneLayoutNode | null | undefined, leafId: string): boolean {
+ if (!node) {
+ return false
+ }
+ if (node.type === 'leaf') {
+ return node.leafId === leafId
+ }
+ return layoutHasLeaf(node.first, leafId) || layoutHasLeaf(node.second, leafId)
+}
+
+function hasAvailableLeaf(layout: TerminalLayoutSnapshot | undefined, leafId: string): boolean {
+ return layoutHasLeaf(layout?.root, leafId) || Boolean(layout?.ptyIdsByLeafId?.[leafId])
+}
+
+function getTabOwnerWorktreeId(
+ state: OriginalPaneState,
+ tabId: string,
+ worktreeIdHint?: string
+): string | null {
+ if (
+ worktreeIdHint &&
+ (state.tabsByWorktree[worktreeIdHint] ?? []).some((tab) => tab.id === tabId)
+ ) {
+ return worktreeIdHint
+ }
+ for (const [worktreeId, tabs] of Object.entries(state.tabsByWorktree)) {
+ if (tabs.some((tab) => tab.id === tabId)) {
+ return worktreeId
+ }
+ }
+ return null
+}
+
+function resolveOriginalPaneTarget(args: {
+ state: OriginalPaneState
+ paneKey: string
+ worktreeIdHint?: string
+ tabIdHint?: string
+}): AiVaultOriginalPaneTarget | null {
+ const { state, paneKey, worktreeIdHint, tabIdHint } = args
+ const stable = parsePaneKey(paneKey)
+ if (stable) {
+ if (tabIdHint && tabIdHint !== stable.tabId) {
+ return null
+ }
+ const worktreeId = getTabOwnerWorktreeId(state, stable.tabId, worktreeIdHint)
+ if (
+ !worktreeId ||
+ !hasAvailableLeaf(state.terminalLayoutsByTabId[stable.tabId], stable.leafId)
+ ) {
+ return null
+ }
+ return { paneKey, worktreeId, tabId: stable.tabId, leafId: stable.leafId }
+ }
+
+ const legacy = parseLegacyNumericPaneKey(paneKey)
+ if (!legacy || (tabIdHint && tabIdHint !== legacy.tabId)) {
+ return null
+ }
+ const worktreeId = getTabOwnerWorktreeId(state, legacy.tabId, worktreeIdHint)
+ if (!worktreeId) {
+ return null
+ }
+ const layout = state.terminalLayoutsByTabId[legacy.tabId]
+ const leafId = resolveRuntimePaneTitleLeafId(layout, legacy.numericPaneId)
+ if (!leafId || !hasAvailableLeaf(layout, leafId)) {
+ return null
+ }
+ return { paneKey, worktreeId, tabId: legacy.tabId, leafId }
+}
+
+export function findOriginalAiVaultSessionPane(
+ state: OriginalPaneState,
+ session: AiVaultSession
+): AiVaultOriginalPaneTarget | null {
+ const promptMatchedTargets: AiVaultOriginalPaneTarget[] = []
+
+ for (const entry of Object.values(state.agentStatusByPaneKey)) {
+ if (
+ agentMatches(session, entry.agentType) &&
+ providerSessionMatches(session, entry.providerSession?.id)
+ ) {
+ const target = resolveOriginalPaneTarget({
+ state,
+ paneKey: entry.paneKey,
+ worktreeIdHint: entry.worktreeId,
+ tabIdHint: entry.tabId
+ })
+ if (target) {
+ return target
+ }
+ }
+ if (
+ agentMatches(session, entry.agentType) &&
+ entry.providerSession === undefined &&
+ promptsMatchSession(session, entry)
+ ) {
+ const target = resolveOriginalPaneTarget({
+ state,
+ paneKey: entry.paneKey,
+ worktreeIdHint: entry.worktreeId,
+ tabIdHint: entry.tabId
+ })
+ if (target) {
+ promptMatchedTargets.push(target)
+ }
+ }
+ }
+
+ for (const retained of Object.values(state.retainedAgentsByPaneKey)) {
+ if (
+ agentMatches(session, retained.agentType) &&
+ providerSessionMatches(session, retained.entry.providerSession?.id)
+ ) {
+ const target = resolveOriginalPaneTarget({
+ state,
+ paneKey: retained.entry.paneKey,
+ worktreeIdHint: retained.worktreeId,
+ tabIdHint: retained.entry.tabId ?? retained.tab.id
+ })
+ if (target) {
+ return target
+ }
+ }
+ if (
+ agentMatches(session, retained.agentType) &&
+ retained.entry.providerSession === undefined &&
+ promptsMatchSession(session, retained.entry)
+ ) {
+ const target = resolveOriginalPaneTarget({
+ state,
+ paneKey: retained.entry.paneKey,
+ worktreeIdHint: retained.worktreeId,
+ tabIdHint: retained.entry.tabId ?? retained.tab.id
+ })
+ if (target) {
+ promptMatchedTargets.push(target)
+ }
+ }
+ }
+
+ for (const record of Object.values(state.sleepingAgentSessionsByPaneKey)) {
+ if (
+ agentMatches(session, record.agent) &&
+ providerSessionMatches(session, record.providerSession.id)
+ ) {
+ const target = resolveOriginalPaneTarget({
+ state,
+ paneKey: record.paneKey,
+ worktreeIdHint: record.worktreeId,
+ tabIdHint: record.tabId
+ })
+ if (target) {
+ return target
+ }
+ }
+ }
+
+ return promptMatchedTargets.length === 1 ? promptMatchedTargets[0] : null
+}
diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-display.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-display.test.ts
index 38d83f3efca..b341558c628 100644
--- a/src/renderer/src/components/right-sidebar/ai-vault-session-display.test.ts
+++ b/src/renderer/src/components/right-sidebar/ai-vault-session-display.test.ts
@@ -3,6 +3,7 @@ import type { AiVaultSession } from '../../../../shared/ai-vault-types'
import {
latestSessionConversationTurn,
recentSessionConversationTurns,
+ sessionDetailConversationTurns,
sessionPreviewSearchText
} from './ai-vault-session-display'
@@ -70,4 +71,22 @@ describe('ai vault session display', () => {
})
).toBe('tool-only transcript')
})
+
+ it('drops title-matching turns and adjacent duplicates from detail turns', () => {
+ const session: AiVaultSession = {
+ ...baseSession,
+ title: 'Fix the flaky golden tests',
+ previewMessages: [
+ { role: 'user', text: 'Fix the flaky golden tests', timestamp: null },
+ { role: 'assistant', text: 'I updated the fixture ordering', timestamp: null },
+ { role: 'assistant', text: 'I updated the fixture ordering', timestamp: null },
+ { role: 'assistant', text: 'Added a regression test', timestamp: null }
+ ]
+ }
+
+ expect(sessionDetailConversationTurns(session, 3).map((turn) => turn.text)).toEqual([
+ 'I updated the fixture ordering',
+ 'Added a regression test'
+ ])
+ })
})
diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-display.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-display.ts
index 4a4940543f1..a4078616810 100644
--- a/src/renderer/src/components/right-sidebar/ai-vault-session-display.ts
+++ b/src/renderer/src/components/right-sidebar/ai-vault-session-display.ts
@@ -28,6 +28,58 @@ export function recentSessionConversationTurns(
return displayableSessionPreviewMessages(session).slice(-limit).map(toDisplayTurn)
}
+export function sessionDetailConversationTurns(
+ session: AiVaultSession,
+ limit: number
+): AiVaultSessionDisplayTurn[] {
+ if (limit <= 0) {
+ return []
+ }
+
+ const turns = displayableSessionPreviewMessages(session)
+ .map(toDisplayTurn)
+ .filter((turn) => !turnTextMatchesSessionTitle(session.title, turn.text))
+
+ return dedupeAdjacentConversationTurns(turns).slice(-limit)
+}
+
+function turnTextMatchesSessionTitle(title: string, turnText: string): boolean {
+ const sessionText = normalizeSessionDisplayText(title)
+ const candidateText = normalizeSessionDisplayText(turnText)
+ if (!sessionText || !candidateText) {
+ return false
+ }
+ if (sessionText === candidateText) {
+ return true
+ }
+ const longEnough = sessionText.length >= 24 && candidateText.length >= 24
+ return (
+ longEnough && (sessionText.startsWith(candidateText) || candidateText.startsWith(sessionText))
+ )
+}
+
+function dedupeAdjacentConversationTurns(
+ turns: AiVaultSessionDisplayTurn[]
+): AiVaultSessionDisplayTurn[] {
+ const deduped: AiVaultSessionDisplayTurn[] = []
+ for (const turn of turns) {
+ const previous = deduped.at(-1)
+ if (
+ previous &&
+ previous.role === turn.role &&
+ normalizeSessionDisplayText(previous.text) === normalizeSessionDisplayText(turn.text)
+ ) {
+ continue
+ }
+ deduped.push(turn)
+ }
+ return deduped
+}
+
+function normalizeSessionDisplayText(value: string): string {
+ return value.trim().replace(/\s+/g, ' ').toLowerCase()
+}
+
export function sessionPreviewSearchText(session: AiVaultSession): string {
return displayableSessionPreviewMessages(session)
.map((message) => message.text)
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
new file mode 100644
index 00000000000..a2d074b6b34
--- /dev/null
+++ b/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts
@@ -0,0 +1,113 @@
+import { useCallback } from 'react'
+import { toast } from 'sonner'
+import {
+ buildAiVaultResumeCommandForWorktree,
+ buildAiVaultResumeStartupForWorktree,
+ type AiVaultResumeStartup
+} from '@/lib/ai-vault-resume-command'
+import { launchAiVaultSessionInNewTab } from '@/lib/launch-ai-vault-session'
+import { activateAndRevealWorktree } from '@/lib/worktree-activation'
+import { useAppStore } from '@/store'
+import type { AiVaultAgent, AiVaultSession } from '../../../../shared/ai-vault-types'
+import type { Repo, Worktree } from '../../../../shared/types'
+import { translate } from '@/i18n/i18n'
+import { agentLabel } from './ai-vault-session-filters'
+
+export function useAiVaultSessionLaunchActions({
+ activeWorktree,
+ allWorktrees,
+ repos,
+ agentCmdOverrides
+}: {
+ activeWorktree: Worktree | null
+ allWorktrees: readonly Worktree[]
+ repos: readonly Repo[]
+ agentCmdOverrides?: Partial>
+}): {
+ buildResumeStartup: (session: AiVaultSession, worktreeId?: string | null) => AiVaultResumeStartup
+ copyResumeCommand: (session: AiVaultSession, worktreeId?: string | null) => Promise
+ handleResume: (session: AiVaultSession, targetWorktreeId?: string) => void
+} {
+ const buildResumeCommand = useCallback(
+ (session: AiVaultSession, worktreeId?: string | null): string =>
+ buildAiVaultResumeCommandForWorktree({
+ state: useAppStore.getState(),
+ worktreeId: worktreeId ?? activeWorktree?.id ?? null,
+ session,
+ commandOverride: agentCmdOverrides?.[session.agent]
+ }),
+ [activeWorktree?.id, agentCmdOverrides]
+ )
+
+ const buildResumeStartup = useCallback(
+ (session: AiVaultSession, worktreeId?: string | null) =>
+ buildAiVaultResumeStartupForWorktree({
+ state: useAppStore.getState(),
+ worktreeId: worktreeId ?? activeWorktree?.id ?? null,
+ session,
+ commandOverride: agentCmdOverrides?.[session.agent]
+ }),
+ [activeWorktree?.id, agentCmdOverrides]
+ )
+
+ const copyResumeCommand = useCallback(
+ async (session: AiVaultSession, worktreeId?: string | null): Promise => {
+ await window.api.ui.writeClipboardText(buildResumeCommand(session, worktreeId))
+ toast.success(
+ translate(
+ 'auto.components.right.sidebar.AiVaultPanel.resumeCommandCopied',
+ 'Resume command copied'
+ )
+ )
+ },
+ [buildResumeCommand]
+ )
+
+ const handleResume = useCallback(
+ (session: AiVaultSession, targetWorktreeId?: string): void => {
+ const worktree =
+ (targetWorktreeId
+ ? allWorktrees.find((candidate) => candidate.id === targetWorktreeId)
+ : null) ?? activeWorktree
+ if (!worktree) {
+ toast.error(
+ translate(
+ 'auto.components.right.sidebar.AiVaultPanel.openWorkspaceBeforeResuming',
+ 'Open a workspace before resuming a session.'
+ )
+ )
+ return
+ }
+
+ const worktreeRepo = repos.find((repo) => repo.id === worktree.repoId)
+ if (worktreeRepo?.connectionId) {
+ toast.error(
+ translate(
+ 'auto.components.right.sidebar.AiVaultPanel.localWorkspacesOnly',
+ 'Resume from history is only available in local workspaces.'
+ )
+ )
+ return
+ }
+
+ launchAiVaultSessionInNewTab({
+ agent: session.agent,
+ worktreeId: worktree.id,
+ ...buildResumeStartup(session, worktree.id)
+ })
+ if (useAppStore.getState().activeWorktreeId !== worktree.id) {
+ activateAndRevealWorktree(worktree.id)
+ }
+ toast.success(
+ translate(
+ 'auto.components.right.sidebar.AiVaultPanel.agentSessionQueued',
+ '{{value0}} session queued',
+ { value0: agentLabel(session.agent) }
+ )
+ )
+ },
+ [activeWorktree, allWorktrees, buildResumeStartup, repos]
+ )
+
+ return { buildResumeStartup, copyResumeCommand, handleResume }
+}
diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-resume.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-resume.test.ts
new file mode 100644
index 00000000000..9c9e86b2990
--- /dev/null
+++ b/src/renderer/src/components/right-sidebar/ai-vault-session-resume.test.ts
@@ -0,0 +1,175 @@
+import { describe, expect, it } from 'vitest'
+import type { Repo, Worktree } from '../../../../shared/types'
+import type { AiVaultSessionWorktreeInfo } from './ai-vault-session-worktree'
+import {
+ aiVaultSessionResumeLabel,
+ resolveAiVaultSessionResumeActions,
+ resolveAiVaultSessionResumeState
+} from './ai-vault-session-resume'
+
+function makeWorktree(overrides: Partial = {}): Worktree {
+ return {
+ id: 'repo-1::/repo/orca',
+ repoId: 'repo-1',
+ displayName: 'orca',
+ path: '/repo/orca',
+ head: 'abc123',
+ branch: 'main',
+ isBare: false,
+ comment: '',
+ linkedIssue: null,
+ linkedPR: null,
+ linkedLinearIssue: null,
+ isArchived: false,
+ isUnread: false,
+ isPinned: false,
+ sortOrder: 0,
+ lastActivityAt: 1,
+ isMainWorktree: false,
+ ...overrides
+ }
+}
+
+function makeWorktreeInfo(
+ status: AiVaultSessionWorktreeInfo['status']
+): AiVaultSessionWorktreeInfo {
+ return {
+ status,
+ label: 'orca',
+ path: '/repo/orca',
+ ...(status === 'unavailable' ? {} : { worktreeId: 'repo-1::/repo/orca' })
+ }
+}
+
+describe('resolveAiVaultSessionResumeState', () => {
+ it('prefers the session worktree over the active workspace', () => {
+ expect(
+ resolveAiVaultSessionResumeState({
+ worktreeInfo: makeWorktreeInfo('active'),
+ activeWorktreeId: 'repo-1::/repo/other',
+ worktrees: [
+ makeWorktree(),
+ makeWorktree({ id: 'repo-1::/repo/other', path: '/repo/other' })
+ ],
+ repos: [{ id: 'repo-1' } as Repo]
+ })
+ ).toEqual({
+ blocked: false,
+ worktreeId: 'repo-1::/repo/orca',
+ usesSessionWorktree: true
+ })
+ })
+
+ it('falls back to the active workspace when the session worktree is unavailable', () => {
+ expect(
+ resolveAiVaultSessionResumeState({
+ worktreeInfo: makeWorktreeInfo('archived'),
+ activeWorktreeId: 'repo-1::/repo/orca',
+ worktrees: [makeWorktree()],
+ repos: [{ id: 'repo-1' } as Repo]
+ })
+ ).toEqual({
+ blocked: false,
+ worktreeId: 'repo-1::/repo/orca',
+ usesSessionWorktree: false
+ })
+ })
+
+ it('blocks remote targets and missing worktrees', () => {
+ expect(
+ resolveAiVaultSessionResumeState({
+ worktreeInfo: makeWorktreeInfo('active'),
+ activeWorktreeId: null,
+ worktrees: [],
+ repos: [{ id: 'repo-1', connectionId: 'ssh-1' } as Repo]
+ })
+ ).toEqual({
+ blocked: true,
+ worktreeId: null,
+ usesSessionWorktree: false
+ })
+ })
+
+ it('uses a local session worktree when the active workspace is remote', () => {
+ expect(
+ resolveAiVaultSessionResumeState({
+ worktreeInfo: makeWorktreeInfo('active'),
+ activeWorktreeId: 'repo-2::/remote/orca',
+ worktrees: [
+ makeWorktree(),
+ makeWorktree({
+ id: 'repo-2::/remote/orca',
+ repoId: 'repo-2',
+ path: '/remote/orca'
+ })
+ ],
+ repos: [{ id: 'repo-1' } as Repo, { id: 'repo-2', connectionId: 'ssh-1' } as Repo]
+ })
+ ).toEqual({
+ blocked: false,
+ worktreeId: 'repo-1::/repo/orca',
+ usesSessionWorktree: true
+ })
+ })
+})
+
+describe('resolveAiVaultSessionResumeActions', () => {
+ it('exposes separate session-worktree and active-workspace targets', () => {
+ expect(
+ resolveAiVaultSessionResumeActions({
+ worktreeInfo: makeWorktreeInfo('active'),
+ activeWorktreeId: 'repo-1::/repo/other',
+ worktrees: [
+ makeWorktree(),
+ makeWorktree({ id: 'repo-1::/repo/other', path: '/repo/other' })
+ ],
+ repos: [{ id: 'repo-1' } as Repo]
+ })
+ ).toEqual({
+ worktree: { worktreeId: 'repo-1::/repo/orca', disabled: false },
+ newTab: { worktreeId: 'repo-1::/repo/other', disabled: false }
+ })
+ })
+
+ it('disables only the remote active-workspace action when the session worktree is local', () => {
+ expect(
+ resolveAiVaultSessionResumeActions({
+ worktreeInfo: makeWorktreeInfo('active'),
+ activeWorktreeId: 'repo-2::/remote/orca',
+ worktrees: [
+ makeWorktree(),
+ makeWorktree({
+ id: 'repo-2::/remote/orca',
+ repoId: 'repo-2',
+ path: '/remote/orca'
+ })
+ ],
+ repos: [{ id: 'repo-1' } as Repo, { id: 'repo-2', connectionId: 'ssh-1' } as Repo]
+ })
+ ).toEqual({
+ worktree: { worktreeId: 'repo-1::/repo/orca', disabled: false },
+ newTab: { worktreeId: 'repo-2::/remote/orca', disabled: true }
+ })
+ })
+
+ it('does not expose the active workspace as a duplicate new-tab target', () => {
+ expect(
+ resolveAiVaultSessionResumeActions({
+ worktreeInfo: makeWorktreeInfo('current'),
+ activeWorktreeId: 'repo-1::/repo/orca',
+ worktrees: [makeWorktree()],
+ repos: [{ id: 'repo-1' } as Repo]
+ })
+ ).toEqual({
+ worktree: { worktreeId: 'repo-1::/repo/orca', disabled: false },
+ newTab: { worktreeId: null, disabled: true }
+ })
+ })
+})
+
+describe('aiVaultSessionResumeLabel', () => {
+ it('names the session worktree action distinctly from the active-workspace fallback', () => {
+ expect(aiVaultSessionResumeLabel({ usesSessionWorktree: true })).toBe('Resume in Worktree')
+ expect(aiVaultSessionResumeLabel({ usesSessionWorktree: false })).toBe('Resume in New Tab')
+ })
+})
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
new file mode 100644
index 00000000000..32481998af0
--- /dev/null
+++ b/src/renderer/src/components/right-sidebar/ai-vault-session-resume.ts
@@ -0,0 +1,140 @@
+import type { Repo, Worktree } from '../../../../shared/types'
+import { translate } from '@/i18n/i18n'
+import {
+ canJumpToAiVaultSessionWorktree,
+ type AiVaultSessionWorktreeInfo
+} from './ai-vault-session-worktree'
+
+export type AiVaultSessionResumeState = {
+ blocked: boolean
+ worktreeId: string | null
+ usesSessionWorktree: boolean
+}
+
+export type AiVaultSessionResumeAction = {
+ worktreeId: string | null
+ disabled: boolean
+}
+
+export type AiVaultSessionResumeActions = {
+ worktree: AiVaultSessionResumeAction
+ newTab: AiVaultSessionResumeAction
+}
+
+export function resolveAiVaultSessionResumeState(args: {
+ worktreeInfo: AiVaultSessionWorktreeInfo | null
+ activeWorktreeId: string | null
+ worktrees: readonly Worktree[]
+ repos: readonly Repo[]
+}): AiVaultSessionResumeState {
+ const sessionWorktreeId =
+ canJumpToAiVaultSessionWorktree(args.worktreeInfo) && args.worktreeInfo?.worktreeId
+ ? args.worktreeInfo.worktreeId
+ : null
+
+ const candidateWorktreeIds = [
+ sessionWorktreeId,
+ args.activeWorktreeId && args.activeWorktreeId !== sessionWorktreeId
+ ? args.activeWorktreeId
+ : null
+ ].filter((value): value is string => Boolean(value))
+
+ for (const worktreeId of candidateWorktreeIds) {
+ const worktree = args.worktrees.find((candidate) => candidate.id === worktreeId)
+ if (!worktree) {
+ continue
+ }
+ const repo = args.repos.find((candidate) => candidate.id === worktree.repoId)
+ if (repo?.connectionId) {
+ continue
+ }
+ return {
+ blocked: false,
+ worktreeId,
+ usesSessionWorktree: worktreeId === sessionWorktreeId
+ }
+ }
+
+ return {
+ blocked: true,
+ worktreeId: null,
+ usesSessionWorktree: false
+ }
+}
+
+export function resolveAiVaultSessionResumeActions(args: {
+ worktreeInfo: AiVaultSessionWorktreeInfo | null
+ activeWorktreeId: string | null
+ worktrees: readonly Worktree[]
+ repos: readonly Repo[]
+}): AiVaultSessionResumeActions {
+ const sessionWorktreeId =
+ canJumpToAiVaultSessionWorktree(args.worktreeInfo) && args.worktreeInfo?.worktreeId
+ ? args.worktreeInfo.worktreeId
+ : null
+
+ const sessionTargetId = resolveLocalResumeWorktreeId({
+ worktreeId: sessionWorktreeId,
+ worktrees: args.worktrees,
+ repos: args.repos
+ })
+ const activeTargetId = resolveLocalResumeWorktreeId({
+ worktreeId:
+ args.activeWorktreeId && args.activeWorktreeId !== sessionWorktreeId
+ ? args.activeWorktreeId
+ : null,
+ worktrees: args.worktrees,
+ repos: args.repos
+ })
+
+ return {
+ worktree: {
+ worktreeId: sessionWorktreeId,
+ disabled: !sessionTargetId
+ },
+ newTab: {
+ worktreeId:
+ args.activeWorktreeId && args.activeWorktreeId !== sessionWorktreeId
+ ? args.activeWorktreeId
+ : null,
+ disabled: !activeTargetId
+ }
+ }
+}
+
+function resolveLocalResumeWorktreeId(args: {
+ worktreeId: string | null
+ worktrees: readonly Worktree[]
+ repos: readonly Repo[]
+}): string | null {
+ if (!args.worktreeId) {
+ return null
+ }
+
+ const worktree = args.worktrees.find((candidate) => candidate.id === args.worktreeId)
+ if (!worktree) {
+ return null
+ }
+
+ const repo = args.repos.find((candidate) => candidate.id === worktree.repoId)
+ if (repo?.connectionId) {
+ return null
+ }
+
+ return args.worktreeId
+}
+
+export function aiVaultSessionResumeLabel(
+ state: Pick
+): string {
+ if (state.usesSessionWorktree) {
+ return translate(
+ 'auto.components.right.sidebar.AiVaultSessionDetails.resumeInWorktree',
+ 'Resume in Worktree'
+ )
+ }
+ return translate(
+ 'auto.components.right.sidebar.AiVaultSessionRow.resumeInNewTab',
+ 'Resume in New Tab'
+ )
+}
diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.test.ts
new file mode 100644
index 00000000000..d1adbbb9a94
--- /dev/null
+++ b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.test.ts
@@ -0,0 +1,229 @@
+import { describe, expect, it } from 'vitest'
+import type { AiVaultSession } from '../../../../shared/ai-vault-types'
+import type { Worktree } from '../../../../shared/types'
+import {
+ aiVaultWorktreeCompactPath,
+ aiVaultWorktreeJumpTooltip,
+ canJumpToAiVaultSessionWorktree,
+ extractWorktreePathFromSessionTitle,
+ resolveAiVaultSessionWorktreeDisplay,
+ resolveAiVaultSessionWorktreeInfo,
+ shouldShowAiVaultWorktreeStatusBadge,
+ type AiVaultSessionWorktreeInfo
+} from './ai-vault-session-worktree'
+
+const baseSession: AiVaultSession = {
+ id: 'codex:session-1',
+ agent: 'codex',
+ sessionId: 'session-1',
+ title: 'Find the pane',
+ cwd: '/repo/orca/src',
+ branch: null,
+ model: null,
+ filePath: '/home/ada/.codex/session-1.jsonl',
+ codexHome: null,
+ createdAt: null,
+ updatedAt: '2026-06-24T10:00:00.000Z',
+ modifiedAt: '2026-06-24T10:00:00.000Z',
+ messageCount: 2,
+ totalTokens: 42,
+ previewMessages: [],
+ resumeCommand: "codex resume 'session-1'"
+}
+
+function makeWorktree(overrides: Partial = {}): Worktree {
+ const worktree: Worktree = {
+ id: 'repo-1::/repo/orca',
+ repoId: 'repo-1',
+ displayName: 'orca',
+ path: '/repo/orca',
+ head: 'abc123',
+ branch: 'main',
+ isBare: false,
+ comment: '',
+ linkedIssue: null,
+ linkedPR: null,
+ linkedLinearIssue: null,
+ isArchived: false,
+ isUnread: false,
+ isPinned: false,
+ sortOrder: 0,
+ lastActivityAt: 1,
+ isMainWorktree: false
+ }
+ return { ...worktree, ...overrides }
+}
+
+describe('resolveAiVaultSessionWorktreeInfo', () => {
+ it('marks the selected owning worktree as current', () => {
+ const worktree = makeWorktree()
+
+ expect(
+ resolveAiVaultSessionWorktreeInfo({
+ session: baseSession,
+ worktrees: [worktree],
+ activeWorktreeId: worktree.id
+ })
+ ).toMatchObject({
+ status: 'current',
+ label: 'orca',
+ path: '/repo/orca'
+ })
+ })
+
+ it('marks a known non-selected worktree as active', () => {
+ const worktree = makeWorktree()
+
+ expect(
+ resolveAiVaultSessionWorktreeInfo({
+ session: baseSession,
+ worktrees: [worktree],
+ activeWorktreeId: 'other'
+ })?.status
+ ).toBe('active')
+ })
+
+ it('uses prior worktree paths to identify renamed active worktrees', () => {
+ const worktree = makeWorktree({
+ id: 'repo-1::/repo/orca-renamed',
+ path: '/repo/orca-renamed',
+ priorWorktreeIds: ['repo-1::/repo/orca']
+ })
+
+ expect(
+ resolveAiVaultSessionWorktreeInfo({
+ session: baseSession,
+ worktrees: [worktree],
+ activeWorktreeId: null
+ })
+ ).toMatchObject({
+ status: 'active',
+ label: 'orca',
+ path: '/repo/orca'
+ })
+ })
+
+ it('falls back to unavailable when no known worktree owns the transcript cwd', () => {
+ expect(
+ resolveAiVaultSessionWorktreeInfo({
+ session: baseSession,
+ worktrees: [],
+ activeWorktreeId: null
+ })
+ ).toMatchObject({
+ status: 'unavailable',
+ label: 'orca/src',
+ path: '/repo/orca/src'
+ })
+ })
+
+ it('matches WSL UNC worktree paths to Linux transcript cwd values', () => {
+ const worktree = makeWorktree({
+ path: '\\\\wsl.localhost\\Ubuntu\\home\\ada\\orca'
+ })
+
+ expect(
+ resolveAiVaultSessionWorktreeInfo({
+ session: { ...baseSession, cwd: '/home/ada/orca/src' },
+ worktrees: [worktree],
+ activeWorktreeId: null
+ })
+ ).toMatchObject({
+ status: 'active',
+ label: 'orca',
+ path: '\\\\wsl.localhost\\Ubuntu\\home\\ada\\orca'
+ })
+ })
+})
+
+describe('canJumpToAiVaultSessionWorktree', () => {
+ it('allows current and active worktree targets', () => {
+ expect(canJumpToAiVaultSessionWorktree(makeWorktreeInfo('current'))).toBe(true)
+ expect(canJumpToAiVaultSessionWorktree(makeWorktreeInfo('active'))).toBe(true)
+ })
+
+ it('disables jump targets that are not active worktrees', () => {
+ expect(canJumpToAiVaultSessionWorktree(makeWorktreeInfo('archived'))).toBe(false)
+ expect(canJumpToAiVaultSessionWorktree(makeWorktreeInfo('unavailable'))).toBe(false)
+ expect(canJumpToAiVaultSessionWorktree(null)).toBe(false)
+ })
+})
+
+describe('extractWorktreePathFromSessionTitle', () => {
+ it('reads worktree paths embedded in session titles', () => {
+ expect(
+ extractWorktreePathFromSessionTitle(
+ 'Inspect PR #6229 - Worktree: /Users/ada/projects/orca/fix-tabs'
+ )
+ ).toBe('/Users/ada/projects/orca/fix-tabs')
+ expect(extractWorktreePathFromSessionTitle('Worktree: /tmp/orca-worker')).toBe(
+ '/tmp/orca-worker'
+ )
+ })
+})
+
+describe('resolveAiVaultSessionWorktreeDisplay', () => {
+ it('falls back to title and branch when cwd is missing', () => {
+ expect(
+ resolveAiVaultSessionWorktreeDisplay({
+ session: {
+ ...baseSession,
+ cwd: null,
+ branch: null,
+ title: 'Fix tabs - Worktree: /Users/ada/projects/orca/fix-tabs'
+ },
+ worktrees: [makeWorktree()],
+ activeWorktreeId: null
+ })?.path
+ ).toBe('/Users/ada/projects/orca/fix-tabs')
+
+ expect(
+ resolveAiVaultSessionWorktreeDisplay({
+ session: { ...baseSession, cwd: null, branch: 'chinese-translation-improvement' },
+ worktrees: [makeWorktree()],
+ activeWorktreeId: null
+ })?.label
+ ).toBe('chinese-translation-improvement')
+ })
+})
+
+describe('aiVaultWorktreeCompactPath', () => {
+ it('keeps the last two path segments for dense detail rows', () => {
+ expect(aiVaultWorktreeCompactPath('/Users/ada/projects/orca/improve-agent-session')).toBe(
+ 'orca/improve-agent-session'
+ )
+ })
+})
+
+describe('shouldShowAiVaultWorktreeStatusBadge', () => {
+ it('hides the generic active badge but keeps meaningful states', () => {
+ expect(shouldShowAiVaultWorktreeStatusBadge('active')).toBe(false)
+ expect(shouldShowAiVaultWorktreeStatusBadge('current')).toBe(true)
+ expect(shouldShowAiVaultWorktreeStatusBadge('archived')).toBe(true)
+ expect(shouldShowAiVaultWorktreeStatusBadge('unavailable')).toBe(true)
+ })
+})
+
+describe('aiVaultWorktreeJumpTooltip', () => {
+ it('explains active jump targets and disabled states', () => {
+ expect(aiVaultWorktreeJumpTooltip(makeWorktreeInfo('active'))).toBe('Jump to Worktree')
+ expect(aiVaultWorktreeJumpTooltip(makeWorktreeInfo('archived'))).toBe(
+ 'This session is in an archived worktree.'
+ )
+ expect(aiVaultWorktreeJumpTooltip(makeWorktreeInfo('unavailable'))).toBe(
+ 'No active worktree matches this session.'
+ )
+ expect(aiVaultWorktreeJumpTooltip(null)).toBe('No worktree was recorded for this session.')
+ })
+})
+
+function makeWorktreeInfo(
+ status: AiVaultSessionWorktreeInfo['status']
+): AiVaultSessionWorktreeInfo {
+ return {
+ status,
+ label: 'orca',
+ path: '/repo/orca',
+ ...(status === 'unavailable' ? {} : { worktreeId: 'repo-1::/repo/orca' })
+ }
+}
diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.ts
new file mode 100644
index 00000000000..142cfe164d6
--- /dev/null
+++ b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.ts
@@ -0,0 +1,289 @@
+import { useMemo } from 'react'
+import { parseWslUncPath } from '../../../../shared/wsl-paths'
+import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree-id'
+import {
+ isPathInsideOrEqual,
+ isRuntimePathAbsolute,
+ normalizeRuntimePathForComparison,
+ normalizeRuntimePathSeparators
+} from '../../../../shared/cross-platform-path'
+import type { AiVaultSession } from '../../../../shared/ai-vault-types'
+import type { Worktree } from '../../../../shared/types'
+import { translate } from '@/i18n/i18n'
+
+export type AiVaultSessionWorktreeStatus = 'current' | 'active' | 'archived' | 'unavailable'
+
+export type AiVaultSessionWorktreeInfo = {
+ status: AiVaultSessionWorktreeStatus
+ label: string
+ path: string
+ worktreeId?: string
+}
+
+type WorktreeCandidate = {
+ worktree: Worktree
+ path: string
+ status: Exclude
+ source: 'current-path' | 'prior-path'
+}
+
+export function resolveAiVaultSessionWorktreeInfo({
+ session,
+ worktrees,
+ activeWorktreeId
+}: {
+ session: AiVaultSession
+ worktrees: readonly Worktree[]
+ activeWorktreeId: string | null
+}): AiVaultSessionWorktreeInfo | null {
+ if (!session.cwd) {
+ return null
+ }
+
+ const candidates = buildWorktreeCandidates(worktrees)
+ .filter((candidate) => isSessionInWorktreePath(candidate.path, session.cwd!))
+ .sort(compareWorktreeCandidates)
+
+ const best = candidates[0]
+ if (!best) {
+ return {
+ status: 'unavailable',
+ label: compactPathLabel(session.cwd),
+ path: session.cwd
+ }
+ }
+
+ const status =
+ best.worktree.id === activeWorktreeId
+ ? 'current'
+ : best.worktree.isArchived
+ ? 'archived'
+ : best.status
+
+ return {
+ status,
+ label: best.worktree.displayName || compactPathLabel(best.path),
+ path: best.path,
+ worktreeId: best.worktree.id
+ }
+}
+
+export function extractWorktreePathFromSessionTitle(title: string): string | null {
+ const trimmed = title.trim()
+ if (!trimmed) {
+ return null
+ }
+
+ const suffixMatch = trimmed.match(/\s-\s*Worktree:\s*(.+)$/i)
+ if (suffixMatch?.[1]) {
+ return suffixMatch[1].trim()
+ }
+
+ const inlineMatch = trimmed.match(/\bWorktree:\s*(.+)$/i)
+ return inlineMatch?.[1]?.trim() ?? null
+}
+
+export function resolveAiVaultSessionWorktreeDisplay(args: {
+ session: AiVaultSession
+ worktrees: readonly Worktree[]
+ activeWorktreeId: string | null
+}): AiVaultSessionWorktreeInfo | null {
+ const resolved = resolveAiVaultSessionWorktreeInfo(args)
+ if (resolved) {
+ return resolved
+ }
+
+ const cwd = args.session.cwd?.trim()
+ if (cwd) {
+ return unavailableWorktreeInfo(cwd)
+ }
+
+ const titlePath = extractWorktreePathFromSessionTitle(args.session.title)
+ if (titlePath) {
+ return unavailableWorktreeInfo(titlePath)
+ }
+
+ const branch = args.session.branch?.trim()
+ if (branch) {
+ return {
+ status: 'unavailable',
+ label: branch,
+ path: branch
+ }
+ }
+
+ return null
+}
+
+export function useAiVaultSessionWorktreeMap({
+ sessions,
+ worktrees,
+ activeWorktreeId
+}: {
+ sessions: readonly AiVaultSession[]
+ worktrees: readonly Worktree[]
+ activeWorktreeId: string | null
+}): ReadonlyMap {
+ return useMemo(
+ () =>
+ new Map(
+ sessions.flatMap((session) => {
+ const worktreeInfo = resolveAiVaultSessionWorktreeDisplay({
+ session,
+ worktrees,
+ activeWorktreeId
+ })
+ return worktreeInfo ? [[session.id, worktreeInfo] as const] : []
+ })
+ ),
+ [activeWorktreeId, sessions, worktrees]
+ )
+}
+
+export function canJumpToAiVaultSessionWorktree(
+ worktreeInfo: AiVaultSessionWorktreeInfo | null
+): boolean {
+ return Boolean(
+ worktreeInfo?.worktreeId &&
+ worktreeInfo.status !== 'archived' &&
+ worktreeInfo.status !== 'unavailable'
+ )
+}
+
+export function aiVaultWorktreeJumpTooltip(
+ worktreeInfo: AiVaultSessionWorktreeInfo | null
+): string {
+ if (canJumpToAiVaultSessionWorktree(worktreeInfo)) {
+ return translate(
+ 'auto.components.right.sidebar.AiVaultSessionWorktree.jumpToWorktree',
+ 'Jump to Worktree'
+ )
+ }
+ if (!worktreeInfo) {
+ return translate(
+ 'auto.components.right.sidebar.AiVaultSessionWorktree.noRecordedWorktree',
+ 'No worktree was recorded for this session.'
+ )
+ }
+ if (worktreeInfo.status === 'archived') {
+ return translate(
+ 'auto.components.right.sidebar.AiVaultSessionWorktree.archivedJumpUnavailable',
+ 'This session is in an archived worktree.'
+ )
+ }
+ if (worktreeInfo.status === 'unavailable') {
+ return translate(
+ 'auto.components.right.sidebar.AiVaultSessionWorktree.noActiveWorktreeMatch',
+ 'No active worktree matches this session.'
+ )
+ }
+ return translate(
+ 'auto.components.right.sidebar.AiVaultSessionWorktree.noActiveWorktreeTarget',
+ 'No active worktree is available.'
+ )
+}
+
+function buildWorktreeCandidates(worktrees: readonly Worktree[]): WorktreeCandidate[] {
+ const candidates: WorktreeCandidate[] = []
+ for (const worktree of worktrees) {
+ if (hasUsablePath(worktree.path)) {
+ candidates.push({
+ worktree,
+ path: worktree.path,
+ status: worktree.isArchived ? 'archived' : 'active',
+ source: 'current-path'
+ })
+ }
+ for (const priorWorktreeId of worktree.priorWorktreeIds ?? []) {
+ const parsed = splitWorktreeIdForFilesystem(priorWorktreeId)
+ if (!parsed || parsed.repoId !== worktree.repoId || !hasUsablePath(parsed.worktreePath)) {
+ continue
+ }
+ candidates.push({
+ worktree,
+ path: parsed.worktreePath,
+ status: worktree.isArchived ? 'archived' : 'active',
+ source: 'prior-path'
+ })
+ }
+ }
+ return candidates
+}
+
+function hasUsablePath(pathValue: string): boolean {
+ const trimmed = pathValue.trim()
+ return Boolean(trimmed && isRuntimePathAbsolute(trimmed))
+}
+
+function isSessionInWorktreePath(worktreePath: string, sessionCwd: string): boolean {
+ if (isPathInsideOrEqual(worktreePath, sessionCwd)) {
+ return true
+ }
+ const wslPath = parseWslUncPath(worktreePath)
+ return wslPath ? isPathInsideOrEqual(wslPath.linuxPath, sessionCwd) : false
+}
+
+function compareWorktreeCandidates(left: WorktreeCandidate, right: WorktreeCandidate): number {
+ const lengthDifference =
+ normalizeRuntimePathForComparison(right.path).length -
+ normalizeRuntimePathForComparison(left.path).length
+ if (lengthDifference !== 0) {
+ return lengthDifference
+ }
+ if (left.source === right.source) {
+ return 0
+ }
+ return left.source === 'current-path' ? -1 : 1
+}
+
+export function aiVaultWorktreeCompactPath(pathValue: string): string {
+ const parts = normalizeRuntimePathSeparators(pathValue).split('/').filter(Boolean)
+ if (parts.length >= 2) {
+ return parts.slice(-2).join('/')
+ }
+ return parts[0] ?? pathValue
+}
+
+export function shouldShowAiVaultWorktreeStatusBadge(
+ status: AiVaultSessionWorktreeStatus
+): boolean {
+ // Why: "active" repeats the branch label without adding scan value in dense rows.
+ return status !== 'active'
+}
+
+function unavailableWorktreeInfo(pathValue: string): AiVaultSessionWorktreeInfo {
+ return {
+ status: 'unavailable',
+ label: compactPathLabel(pathValue),
+ path: pathValue
+ }
+}
+
+function compactPathLabel(pathValue: string): string {
+ return aiVaultWorktreeCompactPath(pathValue)
+}
+
+export function aiVaultWorktreeStatusLabel(status: AiVaultSessionWorktreeStatus): string {
+ if (status === 'current') {
+ return translate(
+ 'auto.components.right.sidebar.AiVaultSessionWorktree.currentWorktree',
+ 'Current worktree'
+ )
+ }
+ if (status === 'active') {
+ return translate(
+ 'auto.components.right.sidebar.AiVaultSessionWorktree.activeWorktree',
+ 'Active worktree'
+ )
+ }
+ if (status === 'archived') {
+ return translate(
+ 'auto.components.right.sidebar.AiVaultSessionWorktree.archivedWorktree',
+ 'Archived worktree'
+ )
+ }
+ return translate(
+ 'auto.components.right.sidebar.AiVaultSessionWorktree.unavailableWorktree',
+ 'Unavailable worktree'
+ )
+}
diff --git a/src/renderer/src/components/right-sidebar/ai-vault-virtual-rows.ts b/src/renderer/src/components/right-sidebar/ai-vault-virtual-rows.ts
index 32b9d9bb47e..c92e3f1e64e 100644
--- a/src/renderer/src/components/right-sidebar/ai-vault-virtual-rows.ts
+++ b/src/renderer/src/components/right-sidebar/ai-vault-virtual-rows.ts
@@ -6,7 +6,7 @@ import {
} from '../sidebar/worktree-list-virtual-rows'
export const VAULT_GROUP_HEADER_ROW_HEIGHT = 32
-export const VAULT_SESSION_ROW_HEIGHT = 82
+export const VAULT_SESSION_ROW_HEIGHT = 98
export type VaultVirtualRow = { type: 'group' | 'session' }
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index 66aed969e3b..7dcc1c33231 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -9170,7 +9170,9 @@
"noAgentSessionsFound": "No agent sessions found",
"noSessionsMatchFilters": "No sessions match the current filters",
"sessionId": "Session ID",
- "logPath": "Log path"
+ "logPath": "Log path",
+ "originalPaneUnavailable": "Original pane is no longer available.",
+ "worktreeUnavailable": "Worktree is no longer available."
},
"AiVaultPanelControls": {
"scanningSessions": "Scanning sessions",
@@ -9223,6 +9225,8 @@
"resumeCommand": "Resume command",
"sessionActions": "{{value0}} session actions",
"resumeInNewTab": "Resume in New Tab",
+ "resumeInWorktree": "Resume in Worktree",
+ "viewLog": "View Log",
"copyResumeCommand": "Copy Resume Command",
"openLog": "Open Log",
"revealLog": "Reveal Log",
@@ -9246,7 +9250,10 @@
"agentRole": "Agent",
"toolRole": "Tool",
"systemRole": "System",
- "sessionRole": "Session"
+ "sessionRole": "Session",
+ "jumpToOriginalPane": "Jump to Original Pane",
+ "worktree": "Worktree",
+ "jumpToWorktree": "Jump to Worktree"
},
"AiVaultSessionRow": {
"noPreviewAvailable": "No conversation preview available",
@@ -9270,7 +9277,9 @@
"agentRole": "Agent",
"toolRole": "Tool",
"systemRole": "System",
- "sessionRole": "Session"
+ "sessionRole": "Session",
+ "jumpToOriginalPane": "Jump to Original Pane",
+ "jumpToWorktree": "Jump to Worktree"
},
"FileExplorerNameFilter": {
"26fb73c6e3": "Find files",
@@ -9314,6 +9323,17 @@
"copyCommand": "Copy command"
}
}
+ },
+ "AiVaultSessionWorktree": {
+ "currentWorktree": "Current worktree",
+ "activeWorktree": "Active worktree",
+ "archivedWorktree": "Archived worktree",
+ "unavailableWorktree": "Unavailable worktree",
+ "jumpToWorktree": "Jump to Worktree",
+ "noRecordedWorktree": "No worktree was recorded for this session.",
+ "archivedJumpUnavailable": "This session is in an archived worktree.",
+ "noActiveWorktreeMatch": "No active worktree matches this session.",
+ "noActiveWorktreeTarget": "No active worktree is available."
}
}
},
diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json
index e91a3329eb8..232615452b7 100644
--- a/src/renderer/src/i18n/locales/es.json
+++ b/src/renderer/src/i18n/locales/es.json
@@ -9170,7 +9170,9 @@
"sessionId": "ID de sesión",
"logPath": "Ruta del registro",
"agents": "Agentes",
- "sessionsShownCompact": "{{value0}} mostradas"
+ "sessionsShownCompact": "{{value0}} mostradas",
+ "originalPaneUnavailable": "Original pane is no longer available.",
+ "worktreeUnavailable": "Worktree is no longer available."
},
"AiVaultPanelControls": {
"scanningSessions": "Scanning sessions",
@@ -9202,41 +9204,43 @@
"project": "Proyecto"
},
"AiVaultSessionDetails": {
- "updated": "Updated",
- "created": "Created",
- "workingDir": "Working dir",
- "unknownLocation": "Unknown location",
+ "updated": "Actualizado",
+ "created": "Creado",
+ "workingDir": "Dir. de trabajo",
+ "unknownLocation": "Ubicación desconocida",
"branch": "Rama",
"model": "Modelo",
- "usage": "Usage",
- "usageValue": "{{value0}} msgs{{value1}}",
+ "usage": "Uso",
+ "usageValue": "{{value0}} msjs{{value1}}",
"tokenSuffix": " · {{value0}} tok",
- "session": "Session",
- "copyDetailValue": "Copy {{value0}}",
- "latestLog": "Latest log",
- "noReadablePreview": "No readable message preview in this transcript.",
- "resumeCommand": "Resume command",
- "sessionActions": "{{value0}} session actions",
- "resumeInNewTab": "Resume in New Tab",
- "copyResumeCommand": "Copy Resume Command",
- "openLog": "Open Log",
- "revealLog": "Reveal Log",
- "openWorkingDirectory": "Open Working Directory",
- "copySessionId": "Copy Session ID",
- "copyLogPath": "Copy Log Path",
- "unknownTime": "Unknown time",
+ "session": "Sesión",
+ "copyDetailValue": "Copiar {{value0}}",
+ "latestLog": "Último registro",
+ "noReadablePreview": "No hay vista previa de mensajes legible en esta transcripción.",
+ "resumeCommand": "Comando para reanudar",
+ "sessionActions": "Acciones de sesión {{value0}}",
+ "resumeInNewTab": "Reanudar en nueva pestaña",
+ "resumeInWorktree": "Reanudar en árbol de trabajo",
+ "viewLog": "Ver registro",
+ "copyResumeCommand": "Copiar comando para reanudar",
+ "openLog": "Abrir registro",
+ "revealLog": "Mostrar registro",
+ "openWorkingDirectory": "Abrir directorio de trabajo",
+ "copySessionId": "Copiar ID de sesión",
+ "copyLogPath": "Copiar ruta de registro",
+ "unknownTime": "Hora desconocida",
"unknown": "Desconocido",
- "user": "User",
- "assistant": "Assistant",
- "tool": "Tool",
- "system": "System",
- "log": "Log",
- "justNow": "Just now",
- "minutesAgo": "{{value0}}m ago",
- "hoursAgo": "{{value0}}h ago",
- "daysAgo": "{{value0}}d ago",
- "monthsAgo": "{{value0}}mo ago",
- "yearsAgo": "{{value0}}y ago",
+ "user": "Usuario",
+ "assistant": "Asistente",
+ "tool": "Herramienta",
+ "system": "Sistema",
+ "log": "Registro",
+ "justNow": "Ahora mismo",
+ "minutesAgo": "hace {{value0}}m",
+ "hoursAgo": "hace {{value0}}h",
+ "daysAgo": "hace {{value0}}d",
+ "monthsAgo": "hace {{value0}}mo",
+ "yearsAgo": "hace {{value0}}a",
"sessionId": "ID de sesión",
"originalAsk": "Solicitud original",
"latestTurns": "Últimos turnos",
@@ -9246,18 +9250,21 @@
"agentRole": "Agente",
"toolRole": "Herramienta",
"systemRole": "Sistema",
- "sessionRole": "Sesión"
+ "sessionRole": "Sesión",
+ "jumpToOriginalPane": "Saltar al panel original",
+ "worktree": "Árbol de trabajo",
+ "jumpToWorktree": "Saltar al árbol de trabajo"
},
"AiVaultSessionRow": {
- "resumeAgentSession": "Resume {{value0}} session",
- "resumeInNewTab": "Resume in New Tab",
- "copyResumeCommand": "Copy Resume Command",
- "openLog": "Open Log",
- "revealLog": "Reveal Log",
- "openWorkingDirectory": "Open Working Directory",
- "copySessionId": "Copy Session ID",
- "copyLogPath": "Copy Log Path",
- "messageCount": "{{value0}} msgs",
+ "resumeAgentSession": "Reanudar sesión {{value0}}",
+ "resumeInNewTab": "Reanudar en nueva pestaña",
+ "copyResumeCommand": "Copiar comando para reanudar",
+ "openLog": "Abrir registro",
+ "revealLog": "Mostrar registro",
+ "openWorkingDirectory": "Abrir directorio de trabajo",
+ "copySessionId": "Copiar ID de sesión",
+ "copyLogPath": "Copiar ruta de registro",
+ "messageCount": "{{value0}} msjs",
"tokenCount": "{{value0}} tok",
"toggleSessionDetails": "Detalles de sesión de {{value0}}",
"hideDetails": "Ocultar detalles",
@@ -9270,7 +9277,9 @@
"agentRole": "Agente",
"toolRole": "Herramienta",
"systemRole": "Sistema",
- "sessionRole": "Sesión"
+ "sessionRole": "Sesión",
+ "jumpToOriginalPane": "Saltar al panel original",
+ "jumpToWorktree": "Saltar al árbol de trabajo"
},
"FileExplorerNameFilter": {
"26fb73c6e3": "Buscar archivos",
@@ -9300,20 +9309,31 @@
"pull": {
"policy": {
"notice": {
- "merge": "Merge",
- "mergeDescription": "Create a merge commit when local and remote both changed.",
- "rebase": "Rebase",
- "rebaseDescription": "Replay local commits on top of the remote branch.",
- "fastForwardOnly": "Fast-forward only",
- "fastForwardOnlyDescription": "Only pull when no merge or rebase is needed.",
- "title": "Pull needs a policy",
- "diverged": "Diverged",
- "body": "This branch has local and remote commits. Run one command in this worktree or on the SSH host, then try Pull or Sync again.",
- "copyAria": "Copy {{value0}} pull policy command",
- "copied": "Copied",
+ "merge": "Fusionar (Merge)",
+ "mergeDescription": "Crear un commit de fusión cuando tanto el repositorio local como el remoto hayan cambiado.",
+ "rebase": "Reorganizar (Rebase)",
+ "rebaseDescription": "Volver a aplicar los commits locales sobre la rama remota.",
+ "fastForwardOnly": "Solo avance rápido (Fast-forward)",
+ "fastForwardOnlyDescription": "Solo realizar pull cuando no se requiera fusión ni rebase.",
+ "title": "El pull requiere una política",
+ "diverged": "Divergido",
+ "body": "Esta rama tiene commits locales y remotos. Ejecute un comando en este árbol de trabajo o en el host SSH, luego intente Pull o Sincronizar de nuevo.",
+ "copyAria": "Copiar el comando de política de pull {{value0}}",
+ "copied": "Copiado",
"copyCommand": "Copiar comando"
}
}
+ },
+ "AiVaultSessionWorktree": {
+ "currentWorktree": "Árbol de trabajo actual",
+ "activeWorktree": "Árbol de trabajo activo",
+ "archivedWorktree": "Árbol de trabajo archivado",
+ "unavailableWorktree": "Árbol de trabajo no disponible",
+ "jumpToWorktree": "Saltar al árbol de trabajo",
+ "noRecordedWorktree": "No se registró ningún árbol de trabajo para esta sesión.",
+ "archivedJumpUnavailable": "Esta sesión se encuentra en un árbol de trabajo archivado.",
+ "noActiveWorktreeMatch": "Ningún árbol de trabajo activo coincide con esta sesión.",
+ "noActiveWorktreeTarget": "No hay ningún árbol de trabajo activo disponible."
}
}
},
diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json
index 6e9e5e0ffd4..b992fa00f60 100644
--- a/src/renderer/src/i18n/locales/ja.json
+++ b/src/renderer/src/i18n/locales/ja.json
@@ -9170,7 +9170,9 @@
"sessionId": "セッション ID",
"logPath": "ログパス",
"agents": "エージェント",
- "sessionsShownCompact": "{{value0}} 件表示"
+ "sessionsShownCompact": "{{value0}} 件表示",
+ "originalPaneUnavailable": "元のペインは利用できなくなりました。",
+ "worktreeUnavailable": "ワークツリーは利用できなくなりました。"
},
"AiVaultPanelControls": {
"scanningSessions": "Scanning sessions",
@@ -9202,41 +9204,43 @@
"project": "プロジェクト"
},
"AiVaultSessionDetails": {
- "updated": "Updated",
- "created": "Created",
- "workingDir": "Working dir",
- "unknownLocation": "Unknown location",
+ "updated": "更新日時",
+ "created": "作成日時",
+ "workingDir": "作業ディレクトリ",
+ "unknownLocation": "不明な場所",
"branch": "ブランチ",
"model": "モデル",
- "usage": "Usage",
- "usageValue": "{{value0}} msgs{{value1}}",
+ "usage": "使用量",
+ "usageValue": "{{value0}} 件のメッセージ{{value1}}",
"tokenSuffix": " · {{value0}} tok",
- "session": "Session",
- "copyDetailValue": "Copy {{value0}}",
- "latestLog": "Latest log",
- "noReadablePreview": "No readable message preview in this transcript.",
- "resumeCommand": "Resume command",
- "sessionActions": "{{value0}} session actions",
- "resumeInNewTab": "Resume in New Tab",
- "copyResumeCommand": "Copy Resume Command",
- "openLog": "Open Log",
- "revealLog": "Reveal Log",
- "openWorkingDirectory": "Open Working Directory",
- "copySessionId": "Copy Session ID",
- "copyLogPath": "Copy Log Path",
- "unknownTime": "Unknown time",
+ "session": "セッション",
+ "copyDetailValue": "{{value0}}をコピー",
+ "latestLog": "最新のログ",
+ "noReadablePreview": "この履歴には読み取り可能なメッセージプレビューがありません。",
+ "resumeCommand": "再開コマンド",
+ "sessionActions": "{{value0}} 件のセッション操作",
+ "resumeInNewTab": "新しいタブで再開",
+ "resumeInWorktree": "ワークツリーで再開",
+ "viewLog": "ログを表示",
+ "copyResumeCommand": "再開コマンドをコピー",
+ "openLog": "ログを開く",
+ "revealLog": "ログを表示",
+ "openWorkingDirectory": "作業ディレクトリを開く",
+ "copySessionId": "セッション ID をコピー",
+ "copyLogPath": "ログパスをコピー",
+ "unknownTime": "不明な時間",
"unknown": "不明",
- "user": "User",
- "assistant": "Assistant",
- "tool": "Tool",
- "system": "System",
- "log": "Log",
- "justNow": "Just now",
- "minutesAgo": "{{value0}}m ago",
- "hoursAgo": "{{value0}}h ago",
- "daysAgo": "{{value0}}d ago",
- "monthsAgo": "{{value0}}mo ago",
- "yearsAgo": "{{value0}}y ago",
+ "user": "ユーザー",
+ "assistant": "アシスタント",
+ "tool": "ツール",
+ "system": "システム",
+ "log": "ログ",
+ "justNow": "たった今",
+ "minutesAgo": "{{value0}} 分前",
+ "hoursAgo": "{{value0}} 時間前",
+ "daysAgo": "{{value0}} 日前",
+ "monthsAgo": "{{value0}} か月前",
+ "yearsAgo": "{{value0}} 年前",
"sessionId": "セッション ID",
"originalAsk": "最初の依頼",
"latestTurns": "最新のやり取り",
@@ -9246,19 +9250,22 @@
"agentRole": "エージェント",
"toolRole": "ツール",
"systemRole": "システム",
- "sessionRole": "セッション"
+ "sessionRole": "セッション",
+ "jumpToOriginalPane": "元のペインにジャンプ",
+ "worktree": "ワークツリー",
+ "jumpToWorktree": "ワークツリーにジャンプ"
},
"AiVaultSessionRow": {
- "resumeAgentSession": "Resume {{value0}} session",
- "resumeInNewTab": "Resume in New Tab",
- "copyResumeCommand": "Copy Resume Command",
- "openLog": "Open Log",
- "revealLog": "Reveal Log",
- "openWorkingDirectory": "Open Working Directory",
- "copySessionId": "Copy Session ID",
- "copyLogPath": "Copy Log Path",
- "messageCount": "{{value0}} msgs",
- "tokenCount": "{{value0}} tok",
+ "resumeAgentSession": "{{value0}} セッションを再開",
+ "resumeInNewTab": "新しいタブで再開",
+ "copyResumeCommand": "再開コマンドをコピー",
+ "openLog": "ログを開く",
+ "revealLog": "ログを表示",
+ "openWorkingDirectory": "作業ディレクトリを開く",
+ "copySessionId": "セッション ID をコピー",
+ "copyLogPath": "ログパスをコピー",
+ "messageCount": "{{value0}} 件のメッセージ",
+ "tokenCount": "{{value0}} トークン",
"toggleSessionDetails": "{{value0}} のセッション詳細",
"hideDetails": "詳細を非表示",
"showDetails": "詳細を表示",
@@ -9270,7 +9277,9 @@
"agentRole": "エージェント",
"toolRole": "ツール",
"systemRole": "システム",
- "sessionRole": "セッション"
+ "sessionRole": "セッション",
+ "jumpToOriginalPane": "元のペインにジャンプ",
+ "jumpToWorktree": "ワークツリーにジャンプ"
},
"FileExplorerNameFilter": {
"26fb73c6e3": "ファイルを検索",
@@ -9300,20 +9309,31 @@
"pull": {
"policy": {
"notice": {
- "merge": "Merge",
- "mergeDescription": "Create a merge commit when local and remote both changed.",
- "rebase": "Rebase",
- "rebaseDescription": "Replay local commits on top of the remote branch.",
- "fastForwardOnly": "Fast-forward only",
- "fastForwardOnlyDescription": "Only pull when no merge or rebase is needed.",
- "title": "Pull needs a policy",
- "diverged": "Diverged",
- "body": "This branch has local and remote commits. Run one command in this worktree or on the SSH host, then try Pull or Sync again.",
- "copyAria": "Copy {{value0}} pull policy command",
- "copied": "Copied",
+ "merge": "マージ (Merge)",
+ "mergeDescription": "ローカルとリモートの両方が変更された場合にマージコミットを作成します。",
+ "rebase": "リベース (Rebase)",
+ "rebaseDescription": "ローカルのコミットをリモートブランチの上に再適用します。",
+ "fastForwardOnly": "ファストフォワードのみ",
+ "fastForwardOnlyDescription": "マージやリベースが必要ない場合のみプルします。",
+ "title": "プルのポリシー設定が必要です",
+ "diverged": "分岐が分かれています",
+ "body": "このブランチにはローカルとリモートのコミットがあります。このワークツリーまたは SSH ホストでコマンドを 1 つ実行してから、プルまたは同期を再試行してください。",
+ "copyAria": "{{value0}} のプルポリシーコマンドをコピー",
+ "copied": "コピーしました",
"copyCommand": "コマンドをコピー"
}
}
+ },
+ "AiVaultSessionWorktree": {
+ "currentWorktree": "現在のワークツリー",
+ "activeWorktree": "アクティブなワークツリー",
+ "archivedWorktree": "アーカイブされたワークツリー",
+ "unavailableWorktree": "利用不可なワークツリー",
+ "jumpToWorktree": "ワークツリーにジャンプ",
+ "noRecordedWorktree": "このセッションに記録されたワークツリーはありません。",
+ "archivedJumpUnavailable": "このセッションはアーカイブされたワークツリーにあります。",
+ "noActiveWorktreeMatch": "このセッションに一致するアクティブなワークツリーはありません。",
+ "noActiveWorktreeTarget": "利用可能なアクティブなワークツリーはありません。"
}
}
},
diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json
index e18f3c89a00..8181b7c7f23 100644
--- a/src/renderer/src/i18n/locales/ko.json
+++ b/src/renderer/src/i18n/locales/ko.json
@@ -9170,7 +9170,9 @@
"sessionId": "세션 ID",
"logPath": "로그 경로",
"agents": "에이전트",
- "sessionsShownCompact": "{{value0}} 표시됨"
+ "sessionsShownCompact": "{{value0}} 표시됨",
+ "originalPaneUnavailable": "원래 창은 더 이상 사용할 수 없습니다.",
+ "worktreeUnavailable": "워크트리는 더 이상 사용할 수 없습니다."
},
"AiVaultPanelControls": {
"scanningSessions": "세션 스캔 중",
@@ -9218,6 +9220,8 @@
"resumeCommand": "재개 명령",
"sessionActions": "{{value0}} 세션 작업",
"resumeInNewTab": "새 탭에서 재개",
+ "resumeInWorktree": "워크트리에서 재개",
+ "viewLog": "로그 보기",
"copyResumeCommand": "재개 명령 복사",
"openLog": "로그 열기",
"revealLog": "로그 위치 열기",
@@ -9226,10 +9230,10 @@
"copyLogPath": "로그 경로 복사",
"unknownTime": "알 수 없는 시간",
"unknown": "알 수 없음",
- "user": "User",
- "assistant": "Assistant",
- "tool": "Tool",
- "system": "System",
+ "user": "사용자",
+ "assistant": "어시스턴트",
+ "tool": "도구",
+ "system": "시스템",
"log": "로그",
"justNow": "방금 전",
"minutesAgo": "{{value0}}분 전",
@@ -9246,7 +9250,10 @@
"agentRole": "에이전트",
"toolRole": "도구",
"systemRole": "시스템",
- "sessionRole": "세션"
+ "sessionRole": "세션",
+ "jumpToOriginalPane": "원래 창으로 이동",
+ "worktree": "작업 트리",
+ "jumpToWorktree": "작업 트리로 이동"
},
"AiVaultSessionRow": {
"resumeAgentSession": "{{value0}} 세션 재개",
@@ -9270,7 +9277,9 @@
"agentRole": "에이전트",
"toolRole": "도구",
"systemRole": "시스템",
- "sessionRole": "세션"
+ "sessionRole": "세션",
+ "jumpToOriginalPane": "원래 창으로 이동",
+ "jumpToWorktree": "작업 트리로 이동"
},
"FileExplorerNameFilter": {
"26fb73c6e3": "파일 찾기",
@@ -9310,10 +9319,21 @@
"diverged": "Diverged",
"body": "이 브랜치에는 로컬 commits과 원격 commits이 모두 있습니다. 이 워크트리 또는 SSH 호스트에서 명령 하나를 실행한 다음 풀 또는 동기화를 다시 시도하세요.",
"copyAria": "{{value0}} pull 정책 명령 복사",
- "copied": "Copied",
+ "copied": "복사됨",
"copyCommand": "명령 복사"
}
}
+ },
+ "AiVaultSessionWorktree": {
+ "currentWorktree": "현재 작업 트리",
+ "activeWorktree": "활성 작업 트리",
+ "archivedWorktree": "보관된 작업 트리",
+ "unavailableWorktree": "사용할 수 없는 작업 트리",
+ "jumpToWorktree": "작업 트리로 이동",
+ "noRecordedWorktree": "이 세션에 기록된 작업 트리가 없습니다.",
+ "archivedJumpUnavailable": "이 세션은 보관된 작업 트리에 있습니다.",
+ "noActiveWorktreeMatch": "이 세션과 일치하는 활성 작업 트리가 없습니다.",
+ "noActiveWorktreeTarget": "사용 가능한 활성 작업 트리가 없습니다."
}
}
},
diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json
index 81bceae9d7d..00f7b5bc129 100644
--- a/src/renderer/src/i18n/locales/zh.json
+++ b/src/renderer/src/i18n/locales/zh.json
@@ -9170,7 +9170,9 @@
"sessionId": "会话 ID",
"logPath": "日志路径",
"agents": "智能体",
- "sessionsShownCompact": "已显示 {{value0}} 个"
+ "sessionsShownCompact": "已显示 {{value0}} 个",
+ "originalPaneUnavailable": "Original pane is no longer available.",
+ "worktreeUnavailable": "Worktree is no longer available."
},
"AiVaultPanelControls": {
"scanningSessions": "正在扫描会话",
@@ -9218,6 +9220,8 @@
"resumeCommand": "恢复命令",
"sessionActions": "{{value0}} 会话操作",
"resumeInNewTab": "在新标签页中恢复",
+ "resumeInWorktree": "在工作树中恢复",
+ "viewLog": "查看日志",
"copyResumeCommand": "复制恢复命令",
"openLog": "打开日志",
"revealLog": "显示日志",
@@ -9246,7 +9250,10 @@
"agentRole": "智能体",
"toolRole": "工具",
"systemRole": "系统",
- "sessionRole": "会话"
+ "sessionRole": "会话",
+ "jumpToOriginalPane": "跳转到原始窗格",
+ "worktree": "工作树",
+ "jumpToWorktree": "跳转到工作树"
},
"AiVaultSessionRow": {
"resumeAgentSession": "恢复 {{value0}} 会话",
@@ -9270,7 +9277,9 @@
"agentRole": "智能体",
"toolRole": "工具",
"systemRole": "系统",
- "sessionRole": "会话"
+ "sessionRole": "会话",
+ "jumpToOriginalPane": "跳转到原始窗格",
+ "jumpToWorktree": "跳转到工作树"
},
"FileExplorerNameFilter": {
"26fb73c6e3": "查找文件",
@@ -9300,20 +9309,31 @@
"pull": {
"policy": {
"notice": {
- "merge": "合并",
- "mergeDescription": "当本地和远程都有更改时创建合并提交。",
- "rebase": "变基",
- "rebaseDescription": "在远程分支之上重放本地提交。",
- "fastForwardOnly": "仅快进",
- "fastForwardOnlyDescription": "仅在不需要合并或变基时拉取。",
+ "merge": "合并 (Merge)",
+ "mergeDescription": "当本地和远程都发生更改时,创建一个合并提交。",
+ "rebase": "变基 (Rebase)",
+ "rebaseDescription": "在远程分支之上重新应用本地提交。",
+ "fastForwardOnly": "仅快进 (Fast-forward)",
+ "fastForwardOnlyDescription": "仅在不需要合并或变基时进行拉取。",
"title": "拉取需要策略",
- "diverged": "已分叉",
- "body": "此分支同时存在本地和远程提交。请在此工作树或 SSH 主机中运行一个命令,然后再次尝试拉取或同步。",
- "copyAria": "Copy {{value0}} pull policy command",
+ "diverged": "分支已分叉",
+ "body": "此分支具有本地和远程提交。在此工作树或 SSH 主机上运行一个命令,然后重新尝试拉取或同步。",
+ "copyAria": "复制 {{value0}} 拉取策略命令",
"copied": "已复制",
"copyCommand": "复制命令"
}
}
+ },
+ "AiVaultSessionWorktree": {
+ "currentWorktree": "当前工作树",
+ "activeWorktree": "活动工作树",
+ "archivedWorktree": "已归档工作树",
+ "unavailableWorktree": "不可用的工作树",
+ "jumpToWorktree": "跳转到工作树",
+ "noRecordedWorktree": "此会话没有记录工作树。",
+ "archivedJumpUnavailable": "此会话位于已归档的工作树中。",
+ "noActiveWorktreeMatch": "没有与此会话匹配的活动工作树。",
+ "noActiveWorktreeTarget": "没有可用的活动工作树。"
}
}
},
diff --git a/src/renderer/src/lib/project-host-workspace-target.test.ts b/src/renderer/src/lib/project-host-workspace-target.test.ts
index 778b2ae30d8..e26eb4d4108 100644
--- a/src/renderer/src/lib/project-host-workspace-target.test.ts
+++ b/src/renderer/src/lib/project-host-workspace-target.test.ts
@@ -207,10 +207,7 @@ describe('project-host workspace target resolution', () => {
// current host is given only as a preference must resolve to that project's
// own host instead of returning '' (the silent no-op the dropdown showed).
describe('cross-host project selection', () => {
- const repos = [
- makeRepo('local-repo'),
- makeRepo('remote-repo', { connectionId: 'remote-1' })
- ]
+ const repos = [makeRepo('local-repo'), makeRepo('remote-repo', { connectionId: 'remote-1' })]
const projects = [
makeProject('repo:local-repo', ['local-repo']),
makeProject('repo:remote-repo', ['remote-repo'])
From f864fc5bc68afb2a8d213ea74df55480b145a2d6 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 25 Jun 2026 07:30:08 +0000
Subject: [PATCH 3/4] release: v1.4.98-rc.0
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 8a40788458a..0375d9786ed 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "orca",
- "version": "1.4.97",
+ "version": "1.4.98-rc.0",
"description": "Next-gen IDE for parallel agentic development",
"homepage": "https://github.com/stablyai/orca",
"author": "stablyai",
From 35c3f86c82edee4a419d1dc7be7cec05c8a954ef Mon Sep 17 00:00:00 2001
From: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Date: Thu, 25 Jun 2026 01:18:31 -0700
Subject: [PATCH 4/4] Style commit text area to match input tokens and fix
disabled state (#6339)
* Style commit text area to match input tokens and fix disabled state
- Update borders and shadows to match standard input tokens.
- Style the disabled state explicitly to prevent Chromium's user-agent
styles from washing out the field outline.
- Add dark mode background styling overrides.
* Improve timeline items assertions in work-item-details test
Extract timelineItems to a local variable and add an explicit null check to prevent TypeScript compilation errors when calling the .at method.
---
src/main/github/work-item-details.test.ts | 10 +++++++---
.../src/components/right-sidebar/SourceControl.tsx | 4 +++-
2 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/src/main/github/work-item-details.test.ts b/src/main/github/work-item-details.test.ts
index 5343c00813f..a36ac97e0fa 100644
--- a/src/main/github/work-item-details.test.ts
+++ b/src/main/github/work-item-details.test.ts
@@ -283,9 +283,13 @@ describe('getWorkItemDetails', () => {
call[0].includes('repos/acme/widgets/issues/923/timeline?per_page=100&page=7')
)
).toBe(false)
- expect(details?.timelineItems).toHaveLength(300)
- expect(details?.timelineItems.at(0)).toMatchObject({ assignee: 'assignee-3-0' })
- expect(details?.timelineItems.at(-1)).toMatchObject({ assignee: 'assignee-6-89' })
+ const timelineItems = details?.timelineItems
+ expect(timelineItems).toHaveLength(300)
+ if (!timelineItems) {
+ throw new Error('Expected timeline items to be present')
+ }
+ expect(timelineItems.at(0)).toMatchObject({ assignee: 'assignee-3-0' })
+ expect(timelineItems.at(-1)).toMatchObject({ assignee: 'assignee-6-89' })
})
it('falls back to REST + GraphQL when the collapsed issue query fails', async () => {
diff --git a/src/renderer/src/components/right-sidebar/SourceControl.tsx b/src/renderer/src/components/right-sidebar/SourceControl.tsx
index 45da0012ddd..74fa804f48b 100644
--- a/src/renderer/src/components/right-sidebar/SourceControl.tsx
+++ b/src/renderer/src/components/right-sidebar/SourceControl.tsx
@@ -6501,7 +6501,9 @@ export function CommitArea({
aria-describedby={describedBy || undefined}
// Why: reserve right padding so typed text does not slide under the
// absolute-positioned Generate icon in the top-right corner.
- className={`mt-0.5 min-h-14 w-full resize-none rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground outline-none placeholder:text-muted-foreground/70 focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 ${
+ // Why: match Input surface tokens and pin disabled:border-input so
+ // Chromium's UA disabled styles don't wash out the field outline.
+ className={`mt-0.5 min-h-14 w-full resize-none appearance-none rounded-md border border-input bg-background shadow-xs px-2 py-1.5 text-xs text-foreground outline-none placeholder:text-muted-foreground/70 focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:border-input disabled:bg-background disabled:text-foreground disabled:shadow-xs dark:bg-input/30 dark:disabled:bg-input/30 ${
showGenerate ? 'pr-8' : ''
}`}
/>