diff --git a/src/main/ipc/repos-remote.test.ts b/src/main/ipc/repos-remote.test.ts index 2a9a66df94d..bd2541c7ebd 100644 --- a/src/main/ipc/repos-remote.test.ts +++ b/src/main/ipc/repos-remote.test.ts @@ -2413,13 +2413,13 @@ describe('repos:getBaseRefDefault envelope', () => { // independent of which Promise in the Promise.all resolves first. type ExecResponse = { stdout: string; stderr: string } type ExecRule = { - match: (argv: string[]) => boolean + matches: (argv: string[]) => boolean respond: () => Promise } const dispatchExec = (rules: ExecRule[]): ((argv: string[]) => Promise) => { return (argv: string[]) => { for (const rule of rules) { - if (rule.match(argv)) { + if (rule.matches(argv)) { return rule.respond() } } @@ -2438,17 +2438,17 @@ describe('repos:getBaseRefDefault envelope', () => { mockGitProvider.exec = vi.fn().mockImplementation( dispatchExec([ { - match: isSymbolicRef, + matches: isSymbolicRef, respond: () => Promise.resolve({ stdout: 'refs/remotes/origin/main\n', stderr: '' }) }, // The origin/HEAD target is verified before it is trusted, so the // symbolic-ref result must also resolve via rev-parse. { - match: isRevParseFor('refs/remotes/origin/main'), + matches: isRevParseFor('refs/remotes/origin/main'), respond: () => Promise.resolve({ stdout: '', stderr: '' }) }, { - match: isRemoteList, + matches: isRemoteList, respond: () => Promise.resolve({ stdout: 'origin\nupstream\n', stderr: '' }) } ]) @@ -2474,17 +2474,17 @@ describe('repos:getBaseRefDefault envelope', () => { mockGitProvider.exec = vi.fn().mockImplementation( dispatchExec([ { - match: isSymbolicRef, + matches: isSymbolicRef, respond: () => Promise.resolve({ stdout: 'refs/remotes/origin/main\n', stderr: '' }) }, // The origin/HEAD target is verified before it is trusted, so the // symbolic-ref result must also resolve via rev-parse. { - match: isRevParseFor('refs/remotes/origin/main'), + matches: isRevParseFor('refs/remotes/origin/main'), respond: () => Promise.resolve({ stdout: '', stderr: '' }) }, { - match: isRemoteList, + matches: isRemoteList, respond: () => Promise.reject(new Error('relay exec failed')) } ]) @@ -2512,18 +2512,21 @@ describe('repos:getBaseRefDefault envelope', () => { mockGitProvider.exec = vi.fn().mockImplementation( dispatchExec([ // symbolic-ref rejects (no origin/HEAD on the remote) - { match: isSymbolicRef, respond: () => Promise.reject(new Error('no symbolic-ref')) }, + { matches: isSymbolicRef, respond: () => Promise.reject(new Error('no symbolic-ref')) }, // probe 1: refs/remotes/origin/main — rejects { - match: isRevParseFor('refs/remotes/origin/main'), + matches: isRevParseFor('refs/remotes/origin/main'), respond: () => Promise.reject(new Error('missing')) }, // probe 2: refs/remotes/origin/master — succeeds { - match: isRevParseFor('refs/remotes/origin/master'), + matches: isRevParseFor('refs/remotes/origin/master'), respond: () => Promise.resolve({ stdout: 'abc123\n', stderr: '' }) }, - { match: isRemoteList, respond: () => Promise.resolve({ stdout: 'origin\n', stderr: '' }) } + { + matches: isRemoteList, + respond: () => Promise.resolve({ stdout: 'origin\n', stderr: '' }) + } ]) ) diff --git a/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx b/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx index 3fd7ae88809..8442e73f54e 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx @@ -355,6 +355,7 @@ export default function AiVaultPanel(): React.JSX.Element { sessionsCount={sessions.length} filteredSessionsCount={filteredSessions.length} error={error} + vaultScope={scope} buildResumeStartup={buildResumeStartup} getSessionResumeState={getSessionResumeState} getSessionResumeActions={getSessionResumeActions} diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx index d5a538b44fd..85f5b2a5759 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx @@ -3,13 +3,14 @@ import { FileJson, FolderGit2, MessageSquare, Play } from 'lucide-react' import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' -import type { AiVaultSession } from '../../../../shared/ai-vault-types' +import type { AiVaultScope, AiVaultSession } from '../../../../shared/ai-vault-types' import { translate } from '@/i18n/i18n' import { sessionDetailConversationTurns } from './ai-vault-session-display' import { aiVaultWorktreeCompactPath, aiVaultWorktreeStatusLabel, shouldShowAiVaultWorktreeStatusBadge, + shouldShowAiVaultSessionWorktreeLine, type AiVaultSessionWorktreeInfo } from './ai-vault-session-worktree' @@ -17,6 +18,7 @@ export function SessionInlineDetails({ id, session, worktreeInfo, + vaultScope, resumeActions, onResumeInWorktree, onResumeInNewTab, @@ -25,6 +27,7 @@ export function SessionInlineDetails({ id: string session: AiVaultSession worktreeInfo: AiVaultSessionWorktreeInfo | null + vaultScope: AiVaultScope resumeActions: { worktree: { worktreeId: string | null; disabled: boolean } newTab: { worktreeId: string | null; disabled: boolean } @@ -78,7 +81,7 @@ export function SessionInlineDetails({ )} - {worktreeDisplay ? ( + {shouldShowAiVaultSessionWorktreeLine(worktreeDisplay, { vaultScope }) ? ( } label={translate( @@ -86,7 +89,7 @@ export function SessionInlineDetails({ 'Worktree' )} > - + ) : null} @@ -202,9 +205,11 @@ function ConversationTurnCard({ } function WorktreeMetadataLines({ - worktreeInfo + worktreeInfo, + vaultScope }: { worktreeInfo: AiVaultSessionWorktreeInfo + vaultScope: AiVaultScope }): React.JSX.Element { const compactPath = aiVaultWorktreeCompactPath(worktreeInfo.path) const pathLine = @@ -214,7 +219,7 @@ function WorktreeMetadataLines({ return (
- {shouldShowAiVaultWorktreeStatusBadge(worktreeInfo.status) ? ( + {shouldShowAiVaultWorktreeStatusBadge(worktreeInfo.status, { vaultScope }) ? ( <> {aiVaultWorktreeStatusLabel(worktreeInfo.status)} diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx index 0826eed9509..b0d88016327 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx @@ -1,37 +1,36 @@ import { useCallback } from 'react' import type React from 'react' import { ContextMenu, ContextMenuContent, 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_END_EVENT, AI_VAULT_SESSION_DRAG_START_EVENT, writeAiVaultSessionDragData } from '@/lib/ai-vault-session-drag' -import type { AiVaultSession } from '../../../../shared/ai-vault-types' +import type { AiVaultScope, AiVaultSession } from '../../../../shared/ai-vault-types' import type { AiVaultResumeStartup } from '@/lib/ai-vault-resume-command' -import { agentLabel } from './ai-vault-session-filters' import { translate } from '@/i18n/i18n' -import { SessionInlineDetails, SessionTime } from './AiVaultSessionDetails' +import { SessionInlineDetails } from './AiVaultSessionDetails' import { latestSessionConversationTurn } from './ai-vault-session-display' import { SessionActionMenuItems } from './AiVaultSessionActionMenuItems' import { SessionRowTrailingActions } from './SessionRowTrailingActions' import type { AiVaultSessionResumeActions } from './ai-vault-session-resume' import { - aiVaultWorktreeStatusLabel, - shouldShowAiVaultWorktreeStatusBadge, + shouldShowAiVaultSessionWorktreeLine, type AiVaultSessionWorktreeInfo } from './ai-vault-session-worktree' +import { + conversationRoleLabel, + getSessionDetailsId, + SessionMetadata, + SessionWorktreeLine +} from './ai-vault-session-row-display' export function VaultSessionRow({ session, resumeStartup, worktreeInfo, + vaultScope, detailsExpanded, resumeDisabled, onToggleDetails, @@ -53,6 +52,7 @@ export function VaultSessionRow({ session: AiVaultSession resumeStartup: AiVaultResumeStartup worktreeInfo: AiVaultSessionWorktreeInfo | null + vaultScope: AiVaultScope detailsExpanded: boolean resumeDisabled: boolean onToggleDetails: () => void @@ -154,9 +154,9 @@ export function VaultSessionRow({ onOpenCwd={onOpenCwd} />
- {detailsExpanded && worktreeInfo ? ( + {detailsExpanded && shouldShowAiVaultSessionWorktreeLine(worktreeInfo, { vaultScope }) ? (
- +
) : null} {!detailsExpanded ? ( @@ -180,6 +180,7 @@ export function VaultSessionRow({ session={session} updatedAt={updatedAt} worktreeInfo={worktreeInfo} + vaultScope={vaultScope} /> ) : null} @@ -188,6 +189,7 @@ export function VaultSessionRow({ id={detailsId} session={session} worktreeInfo={worktreeInfo} + vaultScope={vaultScope} resumeActions={resumeActions} onResumeInWorktree={onResumeInWorktree} onResumeInNewTab={onResumeInNewTab} @@ -216,91 +218,3 @@ export function VaultSessionRow({ ) } - -function getSessionDetailsId(sessionId: string): string { - return `ai-vault-session-details-${sessionId.replace(/[^A-Za-z0-9_-]/g, '-')}` -} - -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 } - )} - - · - -
- {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') - } - if (role === 'assistant') { - return translate('auto.components.right.sidebar.AiVaultSessionRow.agentRole', 'Agent') - } - if (role === 'tool') { - return translate('auto.components.right.sidebar.AiVaultSessionRow.toolRole', 'Tool') - } - if (role === 'system') { - return translate('auto.components.right.sidebar.AiVaultSessionRow.systemRole', 'System') - } - return translate('auto.components.right.sidebar.AiVaultSessionRow.sessionRole', 'Session') -} diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx index 3d2bdde0384..a8de4d84313 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx @@ -1,6 +1,6 @@ import { useVirtualizer } from '@tanstack/react-virtual' import { useCallback, useMemo, useRef, useState } from 'react' -import type { AiVaultSession } from '../../../../shared/ai-vault-types' +import type { AiVaultScope, AiVaultSession } from '../../../../shared/ai-vault-types' import type { AiVaultResumeStartup } from '@/lib/ai-vault-resume-command' import { cn } from '@/lib/utils' import { translate } from '@/i18n/i18n' @@ -41,6 +41,7 @@ export function AiVaultSessionVirtualList({ sessionsCount, filteredSessionsCount, error, + vaultScope, buildResumeStartup, getOriginalPaneTarget, getWorktreeInfo, @@ -63,6 +64,7 @@ export function AiVaultSessionVirtualList({ sessionsCount: number filteredSessionsCount: number error: string | null + vaultScope: AiVaultScope buildResumeStartup: (session: AiVaultSession, worktreeId?: string | null) => AiVaultResumeStartup getOriginalPaneTarget: (session: AiVaultSession) => AiVaultOriginalPaneTarget | null getWorktreeInfo: (session: AiVaultSession) => AiVaultSessionWorktreeInfo | null @@ -185,6 +187,7 @@ export function AiVaultSessionVirtualList({ measureElement={virtualizer.measureElement} collapsedGroups={collapsedGroups} expandedSessionIds={expandedSessionIds} + vaultScope={vaultScope} buildResumeStartup={buildResumeStartup} getOriginalPaneTarget={getOriginalPaneTarget} getWorktreeInfo={getWorktreeInfo} @@ -217,6 +220,7 @@ function AiVaultVirtualRow({ measureElement, collapsedGroups, expandedSessionIds, + vaultScope, buildResumeStartup, getOriginalPaneTarget, getWorktreeInfo, @@ -241,6 +245,7 @@ function AiVaultVirtualRow({ measureElement: (node: Element | null) => void collapsedGroups: ReadonlySet expandedSessionIds: ReadonlySet + vaultScope: AiVaultScope buildResumeStartup: (session: AiVaultSession, worktreeId?: string | null) => AiVaultResumeStartup getOriginalPaneTarget: (session: AiVaultSession) => AiVaultOriginalPaneTarget | null getWorktreeInfo: (session: AiVaultSession) => AiVaultSessionWorktreeInfo | null @@ -266,8 +271,7 @@ function AiVaultVirtualRow({ const originalPaneTarget = row.type === 'session' ? getOriginalPaneTarget(row.session) : null const worktreeInfo = row.type === 'session' ? getWorktreeInfo(row.session) : null // Why: omit the jump affordance when the session already lives in the - // worktree on screen — jumping there is a no-op the "Current worktree" badge - // already conveys. + // worktree on screen — jumping there is a no-op. const showJumpToWorktree = !isAiVaultSessionInCurrentWorktree(worktreeInfo) const worktreeJumpId = showJumpToWorktree && canJumpToAiVaultSessionWorktree(worktreeInfo) @@ -300,6 +304,7 @@ function AiVaultVirtualRow({ session={row.session} resumeStartup={buildResumeStartup(row.session, resumeState?.worktreeId)} worktreeInfo={worktreeInfo} + vaultScope={vaultScope} detailsExpanded={expandedSessionIds.has(row.session.id)} resumeDisabled={resumeState?.blocked ?? true} resumeLabel={resumeLabel} diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-row-display.tsx b/src/renderer/src/components/right-sidebar/ai-vault-session-row-display.tsx new file mode 100644 index 00000000000..1d3972a6fac --- /dev/null +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-row-display.tsx @@ -0,0 +1,115 @@ +import type React from 'react' +import { Badge } from '@/components/ui/badge' +import RepoBadgeLabel from '@/components/repo/RepoBadgeLabel' +import { AgentIcon } from '@/lib/agent-catalog' +import { useRepoById } from '@/store/selectors' +import { resolveRepoBadgeColor } from '../../../../shared/repo-badge-color' +import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree-id' +import type { AiVaultScope, AiVaultSession } from '../../../../shared/ai-vault-types' +import { translate } from '@/i18n/i18n' +import { SessionTime } from './AiVaultSessionDetails' +import { agentLabel } from './ai-vault-session-filters' +import { + aiVaultWorktreeStatusLabel, + shouldShowAiVaultWorktreeStatusBadge, + shouldShowAiVaultSessionWorktreeLine, + type AiVaultSessionWorktreeInfo +} from './ai-vault-session-worktree' + +export function getSessionDetailsId(sessionId: string): string { + return `ai-vault-session-details-${sessionId.replace(/[^A-Za-z0-9_-]/g, '-')}` +} + +export function SessionMetadata({ + session, + updatedAt, + worktreeInfo, + vaultScope +}: { + session: AiVaultSession + updatedAt: string + worktreeInfo: AiVaultSessionWorktreeInfo | null + vaultScope: AiVaultScope +}) { + return ( +
+ + + +
+ {agentLabel(session.agent)} + + {translate( + 'auto.components.right.sidebar.AiVaultSessionRow.messageCount', + '{{value0}} msgs', + { value0: session.messageCount } + )} + + · + +
+ {shouldShowAiVaultSessionWorktreeLine(worktreeInfo, { vaultScope }) ? ( +
+ +
+ ) : null} +
+ ) +} + +export function SessionWorktreeLine({ + worktreeInfo, + vaultScope +}: { + worktreeInfo: AiVaultSessionWorktreeInfo + vaultScope: AiVaultScope +}): React.JSX.Element { + const repoId = worktreeInfo.worktreeId + ? (splitWorktreeIdForFilesystem(worktreeInfo.worktreeId)?.repoId ?? null) + : null + const repo = useRepoById(repoId) + + return ( +
+ {shouldShowAiVaultWorktreeStatusBadge(worktreeInfo.status, { vaultScope }) ? ( + + {worktreeStatusLabel(worktreeInfo.status)} + + ) : null} + + + +
+ ) +} + +function worktreeStatusLabel(status: AiVaultSessionWorktreeInfo['status']): string { + return aiVaultWorktreeStatusLabel(status) +} + +export function conversationRoleLabel( + role: AiVaultSession['previewMessages'][number]['role'] +): string { + if (role === 'user') { + return translate('auto.components.right.sidebar.AiVaultSessionRow.userRole', 'You') + } + if (role === 'assistant') { + return translate('auto.components.right.sidebar.AiVaultSessionRow.agentRole', 'Agent') + } + if (role === 'tool') { + return translate('auto.components.right.sidebar.AiVaultSessionRow.toolRole', 'Tool') + } + if (role === 'system') { + return translate('auto.components.right.sidebar.AiVaultSessionRow.systemRole', 'System') + } + return translate('auto.components.right.sidebar.AiVaultSessionRow.sessionRole', 'Session') +} diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-worktree-affordances.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree-affordances.ts new file mode 100644 index 00000000000..6551d961bd9 --- /dev/null +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree-affordances.ts @@ -0,0 +1,123 @@ +import { normalizeRuntimePathSeparators } from '../../../../shared/cross-platform-path' +import type { AiVaultScope } from '../../../../shared/ai-vault-types' +import { translate } from '@/i18n/i18n' +import type { + AiVaultSessionWorktreeInfo, + AiVaultSessionWorktreeStatus +} from './ai-vault-session-worktree' + +export function canJumpToAiVaultSessionWorktree( + worktreeInfo: AiVaultSessionWorktreeInfo | null +): boolean { + return Boolean( + worktreeInfo?.worktreeId && + worktreeInfo.status !== 'archived' && + worktreeInfo.status !== 'unavailable' + ) +} + +// Why: a session in the worktree you're already viewing has nowhere to jump, +// so we hide the affordance rather than offering a self-jump (the "Current +// worktree" badge already signals where it lives). +export function isAiVaultSessionInCurrentWorktree( + worktreeInfo: AiVaultSessionWorktreeInfo | null +): boolean { + return worktreeInfo?.status === 'current' +} + +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.' + ) +} + +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 shouldShowAiVaultSessionWorktreeLine( + worktreeInfo: AiVaultSessionWorktreeInfo | null, + options?: { vaultScope?: AiVaultScope } +): worktreeInfo is AiVaultSessionWorktreeInfo { + if (!worktreeInfo) { + return false + } + // Why: workspace scope already limits history to the active workspace; the + // worktree row adds no value when the session lives in the worktree on screen. + if (options?.vaultScope === 'workspace' && worktreeInfo.status === 'current') { + return false + } + return true +} + +export function shouldShowAiVaultWorktreeStatusBadge( + status: AiVaultSessionWorktreeStatus, + options?: { vaultScope?: AiVaultScope } +): boolean { + // Why: "active" repeats the branch label without adding scan value in dense rows. + if (status === 'active') { + return false + } + // Why: workspace scope already filters to the active workspace, so "Current + // worktree" is redundant in the default history view. + if (status === 'current' && options?.vaultScope === 'workspace') { + return false + } + return true +} + +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-session-worktree.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.test.ts index 58687977a33..56ba2f246f7 100644 --- 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 @@ -10,6 +10,7 @@ import { resolveAiVaultSessionWorktreeDisplay, resolveAiVaultSessionWorktreeInfo, shouldShowAiVaultWorktreeStatusBadge, + shouldShowAiVaultSessionWorktreeLine, type AiVaultSessionWorktreeInfo } from './ai-vault-session-worktree' @@ -269,6 +270,21 @@ describe('aiVaultWorktreeCompactPath', () => { }) }) +describe('shouldShowAiVaultSessionWorktreeLine', () => { + it('hides the worktree row for the current worktree in workspace scope', () => { + expect( + shouldShowAiVaultSessionWorktreeLine(makeWorktreeInfo('current'), { vaultScope: 'workspace' }) + ).toBe(false) + expect( + shouldShowAiVaultSessionWorktreeLine(makeWorktreeInfo('current'), { vaultScope: 'all' }) + ).toBe(true) + expect( + shouldShowAiVaultSessionWorktreeLine(makeWorktreeInfo('active'), { vaultScope: 'workspace' }) + ).toBe(true) + expect(shouldShowAiVaultSessionWorktreeLine(null, { vaultScope: 'workspace' })).toBe(false) + }) +}) + describe('shouldShowAiVaultWorktreeStatusBadge', () => { it('hides the generic active badge but keeps meaningful states', () => { expect(shouldShowAiVaultWorktreeStatusBadge('active')).toBe(false) @@ -276,6 +292,12 @@ describe('shouldShowAiVaultWorktreeStatusBadge', () => { expect(shouldShowAiVaultWorktreeStatusBadge('archived')).toBe(true) expect(shouldShowAiVaultWorktreeStatusBadge('unavailable')).toBe(true) }) + + it('hides the current badge in workspace scope', () => { + expect(shouldShowAiVaultWorktreeStatusBadge('current', { vaultScope: 'workspace' })).toBe(false) + expect(shouldShowAiVaultWorktreeStatusBadge('current', { vaultScope: 'all' })).toBe(true) + expect(shouldShowAiVaultWorktreeStatusBadge('archived', { vaultScope: 'workspace' })).toBe(true) + }) }) describe('aiVaultWorktreeJumpTooltip', () => { 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 index 31222bec1b5..670e2deecc1 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.ts @@ -10,12 +10,21 @@ import { import { isPathInsideOrEqual, isRuntimePathAbsolute, - normalizeRuntimePathForComparison, - normalizeRuntimePathSeparators + normalizeRuntimePathForComparison } from '../../../../shared/cross-platform-path' import type { AiVaultSession } from '../../../../shared/ai-vault-types' import type { Repo, Worktree } from '../../../../shared/types' -import { translate } from '@/i18n/i18n' +import { aiVaultWorktreeCompactPath } from './ai-vault-session-worktree-affordances' + +export { + aiVaultWorktreeCompactPath, + aiVaultWorktreeJumpTooltip, + aiVaultWorktreeStatusLabel, + canJumpToAiVaultSessionWorktree, + isAiVaultSessionInCurrentWorktree, + shouldShowAiVaultSessionWorktreeLine, + shouldShowAiVaultWorktreeStatusBadge +} from './ai-vault-session-worktree-affordances' export type AiVaultSessionWorktreeStatus = 'current' | 'active' | 'archived' | 'unavailable' @@ -155,58 +164,6 @@ export function useAiVaultSessionWorktreeMap({ ) } -export function canJumpToAiVaultSessionWorktree( - worktreeInfo: AiVaultSessionWorktreeInfo | null -): boolean { - return Boolean( - worktreeInfo?.worktreeId && - worktreeInfo.status !== 'archived' && - worktreeInfo.status !== 'unavailable' - ) -} - -// Why: a session in the worktree you're already viewing has nowhere to jump, -// so we hide the affordance rather than offering a self-jump (the "Current -// worktree" badge already signals where it lives). -export function isAiVaultSessionInCurrentWorktree( - worktreeInfo: AiVaultSessionWorktreeInfo | null -): boolean { - return worktreeInfo?.status === 'current' -} - -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[], repos: readonly Pick[] @@ -270,21 +227,6 @@ function compareWorktreeCandidates(left: WorktreeCandidate, right: WorktreeCandi 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', @@ -296,28 +238,3 @@ function unavailableWorktreeInfo(pathValue: string): AiVaultSessionWorktreeInfo 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' - ) -}