Restore agent history after worktree renames (#6000)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
f524fdf8bb
commit
d517bb7250
|
|
@ -355,6 +355,7 @@ describe('mergeWorktree', () => {
|
|||
lastActivityAt: 1000,
|
||||
workspaceStatus: 'in-review',
|
||||
diffComments: [],
|
||||
priorWorktreeIds: ['repo1::/workspaces/old-feature'],
|
||||
automationProvenance: {
|
||||
kind: 'created-by-automation' as const,
|
||||
automationId: 'automation-1',
|
||||
|
|
@ -401,6 +402,7 @@ describe('mergeWorktree', () => {
|
|||
lastActivityAt: 1000,
|
||||
workspaceStatus: 'in-review',
|
||||
diffComments: [],
|
||||
priorWorktreeIds: ['repo1::/workspaces/old-feature'],
|
||||
automationProvenance: {
|
||||
kind: 'created-by-automation',
|
||||
automationId: 'automation-1',
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ export function mergeWorktree(
|
|||
: {}),
|
||||
...(meta?.baseRef !== undefined ? { baseRef: meta.baseRef } : {}),
|
||||
...(meta?.pushTarget !== undefined ? { pushTarget: meta.pushTarget } : {}),
|
||||
...(meta?.priorWorktreeIds !== undefined ? { priorWorktreeIds: meta.priorWorktreeIds } : {}),
|
||||
workspaceStatus: meta?.workspaceStatus ?? DEFAULT_WORKSPACE_STATUS_ID,
|
||||
// Why: diff comments are persisted on WorktreeMeta and forwarded verbatim
|
||||
// so the renderer store mirrors on-disk state.
|
||||
|
|
|
|||
|
|
@ -3795,6 +3795,15 @@ describe('registerWorktreeHandlers', () => {
|
|||
})
|
||||
|
||||
it('lists a synthetic worktree for folder-mode repos', async () => {
|
||||
const rootWorktreeId = 'repo-1::/workspace/folder'
|
||||
const priorWorktreeIds = ['repo-1::/workspace/old-folder']
|
||||
const rootMeta = makeWorktreeMeta({
|
||||
instanceId: 'folder-instance',
|
||||
projectId: 'repo:repo-1',
|
||||
hostId: 'local',
|
||||
projectHostSetupId: 'repo-1',
|
||||
priorWorktreeIds
|
||||
})
|
||||
store.getRepos.mockReturnValue([
|
||||
{
|
||||
id: 'repo-1',
|
||||
|
|
@ -3813,18 +3822,25 @@ describe('registerWorktreeHandlers', () => {
|
|||
addedAt: 0,
|
||||
kind: 'folder'
|
||||
})
|
||||
store.getAllWorktreeMeta.mockReturnValue({
|
||||
[rootWorktreeId]: rootMeta
|
||||
})
|
||||
store.getWorktreeMeta.mockImplementation((worktreeId: string) =>
|
||||
worktreeId === rootWorktreeId ? rootMeta : undefined
|
||||
)
|
||||
|
||||
const listed = await handlers['worktrees:list'](null, { repoId: 'repo-1' })
|
||||
|
||||
expect(listed).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'repo-1::/workspace/folder',
|
||||
id: rootWorktreeId,
|
||||
repoId: 'repo-1',
|
||||
path: '/workspace/folder',
|
||||
displayName: 'folder',
|
||||
branch: '',
|
||||
head: '',
|
||||
isMainWorktree: true
|
||||
isMainWorktree: true,
|
||||
priorWorktreeIds
|
||||
})
|
||||
])
|
||||
expect(listWorktreesMock).not.toHaveBeenCalled()
|
||||
|
|
|
|||
|
|
@ -640,6 +640,7 @@ function mergeFolderWorkspace(repo: Repo, worktreeId: string, meta: WorktreeMeta
|
|||
...(meta.automationProvenance !== undefined
|
||||
? { automationProvenance: meta.automationProvenance }
|
||||
: {}),
|
||||
...(meta.priorWorktreeIds !== undefined ? { priorWorktreeIds: meta.priorWorktreeIds } : {}),
|
||||
workspaceStatus: meta.workspaceStatus ?? DEFAULT_WORKSPACE_STATUS_ID,
|
||||
diffComments: meta.diffComments,
|
||||
mobileDiffReview: meta.mobileDiffReview
|
||||
|
|
|
|||
|
|
@ -1872,7 +1872,14 @@ describe('OrcaRuntimeService', () => {
|
|||
addedAt: 1,
|
||||
kind: 'folder' as const
|
||||
}
|
||||
const metaById: Record<string, WorktreeMeta> = {}
|
||||
const rootWorktreeId = 'folder-repo::/workspace/folder'
|
||||
const rootPriorWorktreeIds = ['folder-repo::/workspace/old-folder']
|
||||
const metaById: Record<string, WorktreeMeta> = {
|
||||
[rootWorktreeId]: makeWorktreeMeta({
|
||||
instanceId: 'root-instance',
|
||||
priorWorktreeIds: rootPriorWorktreeIds
|
||||
})
|
||||
}
|
||||
const runtimeStore = {
|
||||
...store,
|
||||
getRepos: () => [folderRepo],
|
||||
|
|
@ -1931,8 +1938,9 @@ describe('OrcaRuntimeService', () => {
|
|||
totalCount: 2,
|
||||
worktrees: [
|
||||
expect.objectContaining({
|
||||
id: 'folder-repo::/workspace/folder',
|
||||
isMainWorktree: true
|
||||
id: rootWorktreeId,
|
||||
isMainWorktree: true,
|
||||
priorWorktreeIds: rootPriorWorktreeIds
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: result.worktree.id,
|
||||
|
|
|
|||
|
|
@ -1226,6 +1226,7 @@ function mergeRuntimeFolderWorkspace(repo: Repo, worktreeId: string, meta: Workt
|
|||
...(meta.automationProvenance !== undefined
|
||||
? { automationProvenance: meta.automationProvenance }
|
||||
: {}),
|
||||
...(meta.priorWorktreeIds !== undefined ? { priorWorktreeIds: meta.priorWorktreeIds } : {}),
|
||||
workspaceStatus: meta.workspaceStatus ?? DEFAULT_WORKSPACE_STATUS_ID,
|
||||
diffComments: meta.diffComments,
|
||||
mobileDiffReview: meta.mobileDiffReview
|
||||
|
|
|
|||
|
|
@ -4,7 +4,12 @@ import { buildAiVaultResumeCommandForWorktree } from '@/lib/ai-vault-resume-comm
|
|||
import { launchAiVaultSessionInNewTab } from '@/lib/launch-ai-vault-session'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useActiveWorktree, useRepoById } from '@/store/selectors'
|
||||
import { agentLabel, filterAiVaultSessions, groupAiVaultSessions } from './ai-vault-session-filters'
|
||||
import {
|
||||
agentLabel,
|
||||
deriveAiVaultWorkspaceScopePaths,
|
||||
filterAiVaultSessions,
|
||||
groupAiVaultSessions
|
||||
} from './ai-vault-session-filters'
|
||||
import {
|
||||
AI_VAULT_AGENTS,
|
||||
type AiVaultAgent,
|
||||
|
|
@ -25,6 +30,7 @@ export default function AiVaultPanel(): React.JSX.Element {
|
|||
const activeWorktree = useActiveWorktree()
|
||||
const activeRepo = useRepoById(activeWorktree?.repoId ?? null)
|
||||
const agentCmdOverrides = useAppStore((s) => s.settings?.agentCmdOverrides ?? {})
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const [query, setQuery] = useState('')
|
||||
const [scope, setScope] = useState<AiVaultScope>('workspace')
|
||||
const [sort, setSort] = useState<AiVaultSort>('updated')
|
||||
|
|
@ -42,6 +48,12 @@ export default function AiVaultPanel(): React.JSX.Element {
|
|||
|
||||
const isRemoteWorktree = Boolean(activeRepo?.connectionId)
|
||||
const activeWorktreePath = activeWorktree?.path ?? null
|
||||
// Why: AI Vault ownership is cwd-based, so we must consider live worktrees across all repos.
|
||||
const liveWorktrees = useMemo(() => Object.values(worktreesByRepo).flat(), [worktreesByRepo])
|
||||
const activeWorktreePaths = useMemo(
|
||||
() => deriveAiVaultWorkspaceScopePaths(activeWorktree ?? null, liveWorktrees),
|
||||
[activeWorktree, liveWorktrees]
|
||||
)
|
||||
const hasAllAgentsSelected = agents.length === AI_VAULT_AGENTS.length
|
||||
const viewAdjustmentCount =
|
||||
(hasAllAgentsSelected ? 0 : 1) +
|
||||
|
|
@ -107,10 +119,10 @@ export default function AiVaultPanel(): React.JSX.Element {
|
|||
agents,
|
||||
scope,
|
||||
sort,
|
||||
activeWorktreePath,
|
||||
activeWorktreePaths,
|
||||
hideEmptySessions
|
||||
}),
|
||||
[activeWorktreePath, agents, hideEmptySessions, query, scope, sessions, sort]
|
||||
[activeWorktreePaths, agents, hideEmptySessions, query, scope, sessions, sort]
|
||||
)
|
||||
|
||||
const groups = useMemo(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
|||
import type { AiVaultSession } from '../../../../shared/ai-vault-types'
|
||||
import {
|
||||
AI_VAULT_SESSION_FILTER_QUERY_MAX_BYTES,
|
||||
deriveAiVaultWorkspaceScopePaths,
|
||||
filterAiVaultSessions,
|
||||
folderLabel,
|
||||
groupAiVaultSessions,
|
||||
|
|
@ -50,12 +51,54 @@ describe('filterAiVaultSessions', () => {
|
|||
agents: ['claude'],
|
||||
scope: 'workspace',
|
||||
sort: 'updated',
|
||||
activeWorktreePath: '/Users/ada/repo',
|
||||
activeWorktreePaths: ['/Users/ada/repo'],
|
||||
hideEmptySessions: true
|
||||
}).map((session) => session.id)
|
||||
).toEqual(['claude:1'])
|
||||
})
|
||||
|
||||
it('matches workspace sessions under prior renamed worktree paths', () => {
|
||||
const sessions: AiVaultSession[] = [
|
||||
{
|
||||
...baseSession,
|
||||
id: 'claude:old-path',
|
||||
cwd: '/Users/ada/workspaces/orca/bream/src'
|
||||
},
|
||||
{
|
||||
...baseSession,
|
||||
id: 'claude:other-path',
|
||||
cwd: '/Users/ada/workspaces/other/bream'
|
||||
}
|
||||
]
|
||||
|
||||
expect(
|
||||
filterAiVaultSessions(sessions, {
|
||||
query: '',
|
||||
agents: ['claude'],
|
||||
scope: 'workspace',
|
||||
sort: 'updated',
|
||||
activeWorktreePaths: [
|
||||
'/Users/ada/workspaces/orca/fix-agent-history',
|
||||
'/Users/ada/workspaces/orca/bream'
|
||||
],
|
||||
hideEmptySessions: true
|
||||
}).map((session) => session.id)
|
||||
).toEqual(['claude:old-path'])
|
||||
})
|
||||
|
||||
it('returns no workspace matches when active workspace paths are empty', () => {
|
||||
expect(
|
||||
filterAiVaultSessions([baseSession], {
|
||||
query: '',
|
||||
agents: ['claude'],
|
||||
scope: 'workspace',
|
||||
sort: 'updated',
|
||||
activeWorktreePaths: [],
|
||||
hideEmptySessions: true
|
||||
})
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('hides empty metadata-only sessions when requested', () => {
|
||||
const emptySession: AiVaultSession = {
|
||||
...baseSession,
|
||||
|
|
@ -71,7 +114,7 @@ describe('filterAiVaultSessions', () => {
|
|||
agents: ['claude'],
|
||||
scope: 'all',
|
||||
sort: 'updated',
|
||||
activeWorktreePath: null,
|
||||
activeWorktreePaths: [],
|
||||
hideEmptySessions: true
|
||||
}).map((session) => session.id)
|
||||
).toEqual(['claude:1'])
|
||||
|
|
@ -81,7 +124,7 @@ describe('filterAiVaultSessions', () => {
|
|||
agents: ['claude'],
|
||||
scope: 'all',
|
||||
sort: 'updated',
|
||||
activeWorktreePath: null,
|
||||
activeWorktreePaths: [],
|
||||
hideEmptySessions: false
|
||||
}).map((session) => session.id)
|
||||
|
||||
|
|
@ -108,7 +151,7 @@ describe('filterAiVaultSessions', () => {
|
|||
agents: ['claude'],
|
||||
scope: 'all',
|
||||
sort: 'updated',
|
||||
activeWorktreePath: null,
|
||||
activeWorktreePaths: [],
|
||||
hideEmptySessions: true
|
||||
}
|
||||
).map((session) => session.id)
|
||||
|
|
@ -140,7 +183,7 @@ describe('filterAiVaultSessions', () => {
|
|||
agents: ['claude'],
|
||||
scope: 'all',
|
||||
sort: 'updated',
|
||||
activeWorktreePath: null,
|
||||
activeWorktreePaths: [],
|
||||
hideEmptySessions: true
|
||||
}
|
||||
)
|
||||
|
|
@ -161,7 +204,7 @@ describe('filterAiVaultSessions', () => {
|
|||
agents: ['claude'],
|
||||
scope: 'workspace',
|
||||
sort: 'updated',
|
||||
activeWorktreePath: 'c:\\users\\ada\\repo',
|
||||
activeWorktreePaths: ['c:\\users\\ada\\repo'],
|
||||
hideEmptySessions: true
|
||||
}
|
||||
)
|
||||
|
|
@ -182,13 +225,119 @@ describe('filterAiVaultSessions', () => {
|
|||
agents: ['claude'],
|
||||
scope: 'all',
|
||||
sort: 'updated',
|
||||
activeWorktreePath: null,
|
||||
activeWorktreePaths: [],
|
||||
hideEmptySessions: false
|
||||
})
|
||||
).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('deriveAiVaultWorkspaceScopePaths', () => {
|
||||
it('includes current and same-repo prior filesystem paths', () => {
|
||||
expect(
|
||||
deriveAiVaultWorkspaceScopePaths({
|
||||
id: 'repo1::/Users/ada/workspaces/orca/fix-agent-history',
|
||||
repoId: 'repo1',
|
||||
path: '/Users/ada/workspaces/orca/fix-agent-history',
|
||||
priorWorktreeIds: ['repo1::/Users/ada/workspaces/orca/bream']
|
||||
})
|
||||
).toEqual(['/Users/ada/workspaces/orca/fix-agent-history', '/Users/ada/workspaces/orca/bream'])
|
||||
})
|
||||
|
||||
it('strips folder-workspace instance suffixes from prior ids', () => {
|
||||
expect(
|
||||
deriveAiVaultWorkspaceScopePaths({
|
||||
id: 'repo1::/Users/ada/folders/orca',
|
||||
repoId: 'repo1',
|
||||
path: '/Users/ada/folders/orca',
|
||||
priorWorktreeIds: [
|
||||
'repo1::/Users/ada/folders/old-orca::workspace:123e4567-e89b-12d3-a456-426614174000'
|
||||
]
|
||||
})
|
||||
).toEqual(['/Users/ada/folders/orca', '/Users/ada/folders/old-orca'])
|
||||
})
|
||||
|
||||
it('ignores malformed, different-repo, relative, empty, and duplicate aliases', () => {
|
||||
expect(
|
||||
deriveAiVaultWorkspaceScopePaths({
|
||||
id: 'repo1::C:\\Users\\Ada\\Repo',
|
||||
repoId: 'repo1',
|
||||
path: 'C:\\Users\\Ada\\Repo',
|
||||
priorWorktreeIds: [
|
||||
'not-a-worktree-id',
|
||||
'repo2::C:\\Users\\Ada\\OldRepo',
|
||||
'repo1::relative/path',
|
||||
'repo1::',
|
||||
'repo1::c:\\users\\ada\\repo',
|
||||
'repo1::C:\\Users\\Ada\\OldRepo'
|
||||
]
|
||||
})
|
||||
).toEqual(['C:\\Users\\Ada\\Repo', 'C:\\Users\\Ada\\OldRepo'])
|
||||
})
|
||||
|
||||
it('ignores prior paths claimed by another live worktree in the same repo', () => {
|
||||
expect(
|
||||
deriveAiVaultWorkspaceScopePaths(
|
||||
{
|
||||
id: 'repo1::/Users/ada/workspaces/orca/fix-agent-history',
|
||||
repoId: 'repo1',
|
||||
path: '/Users/ada/workspaces/orca/fix-agent-history',
|
||||
priorWorktreeIds: [
|
||||
'repo1::/Users/ada/workspaces/orca/bream',
|
||||
'repo1::/Users/ada/workspaces/orca/unclaimed-old-path'
|
||||
]
|
||||
},
|
||||
[
|
||||
{
|
||||
id: 'repo1::/Users/ada/workspaces/orca/fix-agent-history',
|
||||
repoId: 'repo1',
|
||||
path: '/Users/ada/workspaces/orca/fix-agent-history'
|
||||
},
|
||||
{
|
||||
id: 'repo1::/Users/ada/workspaces/orca/bream',
|
||||
repoId: 'repo1',
|
||||
path: '/Users/ada/workspaces/orca/bream'
|
||||
}
|
||||
]
|
||||
)
|
||||
).toEqual([
|
||||
'/Users/ada/workspaces/orca/fix-agent-history',
|
||||
'/Users/ada/workspaces/orca/unclaimed-old-path'
|
||||
])
|
||||
})
|
||||
|
||||
it('ignores prior paths claimed by another live worktree in a different repo', () => {
|
||||
expect(
|
||||
deriveAiVaultWorkspaceScopePaths(
|
||||
{
|
||||
id: 'repo1::/Users/ada/workspaces/orca/fix-agent-history',
|
||||
repoId: 'repo1',
|
||||
path: '/Users/ada/workspaces/orca/fix-agent-history',
|
||||
priorWorktreeIds: [
|
||||
'repo1::/Users/ada/workspaces/orca/bream',
|
||||
'repo1::/Users/ada/workspaces/orca/unclaimed-old-path'
|
||||
]
|
||||
},
|
||||
[
|
||||
{
|
||||
id: 'repo1::/Users/ada/workspaces/orca/fix-agent-history',
|
||||
repoId: 'repo1',
|
||||
path: '/Users/ada/workspaces/orca/fix-agent-history'
|
||||
},
|
||||
{
|
||||
id: 'repo2::/Users/ada/workspaces/orca/bream',
|
||||
repoId: 'repo2',
|
||||
path: '/Users/ada/workspaces/orca/bream'
|
||||
}
|
||||
]
|
||||
)
|
||||
).toEqual([
|
||||
'/Users/ada/workspaces/orca/fix-agent-history',
|
||||
'/Users/ada/workspaces/orca/unclaimed-old-path'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('isAiVaultSessionFilterQueryTooLarge', () => {
|
||||
it('counts UTF-8 bytes rather than UTF-16 code units', () => {
|
||||
expect(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import {
|
||||
isPathInsideOrEqual,
|
||||
isRuntimePathAbsolute,
|
||||
normalizeRuntimePathForComparison,
|
||||
normalizeRuntimePathSeparators
|
||||
} from '../../../../shared/cross-platform-path'
|
||||
import { isClipboardTextByteLengthOverLimit } from '../../../../shared/clipboard-text'
|
||||
|
|
@ -11,6 +13,8 @@ import type {
|
|||
AiVaultSort
|
||||
} from '../../../../shared/ai-vault-types'
|
||||
import { aiVaultAgentLabel } from '../../../../shared/ai-vault-types'
|
||||
import type { Worktree } from '../../../../shared/types'
|
||||
import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree-id'
|
||||
import { sessionPreviewSearchText } from './ai-vault-session-display'
|
||||
|
||||
export type AiVaultSessionFilterState = {
|
||||
|
|
@ -18,7 +22,7 @@ export type AiVaultSessionFilterState = {
|
|||
agents: readonly AiVaultAgent[]
|
||||
scope: AiVaultScope
|
||||
sort: AiVaultSort
|
||||
activeWorktreePath: string | null
|
||||
activeWorktreePaths: readonly string[]
|
||||
hideEmptySessions: boolean
|
||||
}
|
||||
|
||||
|
|
@ -62,18 +66,45 @@ export function filterAiVaultSessions(
|
|||
if (filters.hideEmptySessions && session.messageCount === 0) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
filters.scope === 'workspace' &&
|
||||
filters.activeWorktreePath &&
|
||||
(!session.cwd || !isPathInsideOrEqual(filters.activeWorktreePath, session.cwd))
|
||||
) {
|
||||
return false
|
||||
if (filters.scope === 'workspace') {
|
||||
const cwd = session.cwd
|
||||
if (
|
||||
!cwd ||
|
||||
!filters.activeWorktreePaths.some((pathValue) => isPathInsideOrEqual(pathValue, cwd))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return matchesQuery(session, parsedQuery)
|
||||
})
|
||||
.sort((left, right) => compareSessions(left, right, filters.sort))
|
||||
}
|
||||
|
||||
export function deriveAiVaultWorkspaceScopePaths(
|
||||
activeWorktree: Pick<Worktree, 'id' | 'path' | 'priorWorktreeIds' | 'repoId'> | null,
|
||||
liveWorktrees: readonly Pick<Worktree, 'id' | 'path' | 'repoId'>[] = []
|
||||
): string[] {
|
||||
if (!activeWorktree) {
|
||||
return []
|
||||
}
|
||||
|
||||
const paths: string[] = []
|
||||
addAiVaultWorkspaceScopePath(paths, activeWorktree.path)
|
||||
|
||||
for (const priorWorktreeId of activeWorktree.priorWorktreeIds ?? []) {
|
||||
const parsed = splitWorktreeIdForFilesystem(priorWorktreeId)
|
||||
if (!parsed || parsed.repoId !== activeWorktree.repoId) {
|
||||
continue
|
||||
}
|
||||
if (isAiVaultWorkspaceScopePathClaimed(parsed.worktreePath, activeWorktree, liveWorktrees)) {
|
||||
continue
|
||||
}
|
||||
addAiVaultWorkspaceScopePath(paths, parsed.worktreePath)
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
export function groupAiVaultSessions(
|
||||
sessions: readonly AiVaultSession[],
|
||||
group: AiVaultGroup
|
||||
|
|
@ -180,6 +211,38 @@ function getFolderGroupKey(pathValue: string | null): string {
|
|||
return pathValue ? normalizeRuntimePathSeparators(pathValue).toLowerCase() : 'unknown'
|
||||
}
|
||||
|
||||
function addAiVaultWorkspaceScopePath(paths: string[], pathValue: string): void {
|
||||
const trimmedPath = pathValue.trim()
|
||||
if (!trimmedPath || !isRuntimePathAbsolute(trimmedPath)) {
|
||||
return
|
||||
}
|
||||
const comparisonPath = normalizeRuntimePathForComparison(trimmedPath)
|
||||
if (
|
||||
paths.some((existingPath) => normalizeRuntimePathForComparison(existingPath) === comparisonPath)
|
||||
) {
|
||||
return
|
||||
}
|
||||
paths.push(trimmedPath)
|
||||
}
|
||||
|
||||
function isAiVaultWorkspaceScopePathClaimed(
|
||||
pathValue: string,
|
||||
activeWorktree: Pick<Worktree, 'id'>,
|
||||
liveWorktrees: readonly Pick<Worktree, 'id' | 'path'>[]
|
||||
): boolean {
|
||||
const trimmedPath = pathValue.trim()
|
||||
if (!trimmedPath || !isRuntimePathAbsolute(trimmedPath)) {
|
||||
return false
|
||||
}
|
||||
const comparisonPath = normalizeRuntimePathForComparison(trimmedPath)
|
||||
// AI Vault sessions are keyed by cwd only, so any live worktree now owning this path wins.
|
||||
return liveWorktrees.some(
|
||||
(worktree) =>
|
||||
worktree.id !== activeWorktree.id &&
|
||||
normalizeRuntimePathForComparison(worktree.path) === comparisonPath
|
||||
)
|
||||
}
|
||||
|
||||
function tokenizeQuery(query: string): string[] {
|
||||
const tokens: string[] = []
|
||||
const pattern = /"([^"]+)"|'([^']+)'|(\S+)/g
|
||||
|
|
|
|||
|
|
@ -394,6 +394,29 @@ describe('fetchWorktrees', () => {
|
|||
expect(store.getState().sortEpoch).toBe(8)
|
||||
})
|
||||
|
||||
it('updates the repo entry when only prior worktree id aliases change', async () => {
|
||||
const store = createTestStore()
|
||||
const existing = makeWorktree({
|
||||
id: 'repo1::/path/current-name',
|
||||
repoId: 'repo1',
|
||||
path: '/path/current-name'
|
||||
})
|
||||
const refreshed = makeWorktree({
|
||||
id: 'repo1::/path/current-name',
|
||||
repoId: 'repo1',
|
||||
path: '/path/current-name',
|
||||
priorWorktreeIds: ['repo1::/path/old-name']
|
||||
})
|
||||
|
||||
mockApi.worktrees.list.mockResolvedValue([refreshed])
|
||||
store.setState({ worktreesByRepo: { repo1: [existing] }, sortEpoch: 7 } as Partial<AppState>)
|
||||
|
||||
await store.getState().fetchWorktrees('repo1')
|
||||
|
||||
expect(store.getState().worktreesByRepo.repo1).toEqual([refreshed])
|
||||
expect(store.getState().sortEpoch).toBe(8)
|
||||
})
|
||||
|
||||
it('keeps the last known worktree list when a refresh transiently returns empty', async () => {
|
||||
const store = createTestStore()
|
||||
const existing = makeWorktree({ id: 'repo1::/path/wt1', repoId: 'repo1', path: '/path/wt1' })
|
||||
|
|
|
|||
|
|
@ -263,6 +263,7 @@ function areWorktreesEqual(current: Worktree[] | undefined, next: Worktree[]): b
|
|||
worktree.pushTarget?.remoteUrl === candidate.pushTarget?.remoteUrl &&
|
||||
worktree.sparseBaseRef === candidate.sparseBaseRef &&
|
||||
arraysShallowEqual(worktree.sparseDirectories, candidate.sparseDirectories) &&
|
||||
arraysShallowEqual(worktree.priorWorktreeIds, candidate.priorWorktreeIds) &&
|
||||
(worktree as WorktreeWithLineage).parentWorktreeId ===
|
||||
(candidate as WorktreeWithLineage).parentWorktreeId &&
|
||||
arraysShallowEqual(
|
||||
|
|
|
|||
|
|
@ -493,6 +493,8 @@ export type Worktree = {
|
|||
baseRef?: string
|
||||
/** Remote/branch Orca should publish review commits to when it created this worktree. */
|
||||
pushTarget?: GitPushTarget
|
||||
/** Path-derived worktree ids this worktree had before folder renames. */
|
||||
priorWorktreeIds?: string[]
|
||||
workspaceStatus?: WorkspaceStatus
|
||||
diffComments?: DiffComment[]
|
||||
mobileDiffReview?: MobileDiffReviewState
|
||||
|
|
|
|||
Loading…
Reference in New Issue