diff --git a/src/shared/workspace-doc-history.test.ts b/src/shared/workspace-doc-history.test.ts index 15ea1c2d3d7..8d69b22d712 100644 --- a/src/shared/workspace-doc-history.test.ts +++ b/src/shared/workspace-doc-history.test.ts @@ -53,4 +53,36 @@ describe('normalizeWorkspaceDocHistoryEntries', () => { expect(normalizeWorkspaceDocHistoryTitle('', DOC)).toBe('a.html') expect(normalizeWorkspaceDocHistoryTitle('Report', DOC)).toBe('Report') }) + it('indexes document identity while deduplicating a large legacy history', () => { + let reads = 0 + const entries = Array.from({ length: 10_000 }, (_, i) => + entry({ + docLocation: { + kind: 'workspace-doc', + get worktreeId() { + reads++ + return 'wt-1' + }, + filePath: `/repo/${i % 99}.html` + }, + lastVisitedAt: i + }) + ) + const result = normalizeWorkspaceDocHistoryEntries(entries) + expect(reads).toBeLessThanOrEqual(20_000) + expect(result).toHaveLength(99) + expect(result.map((row) => row.lastVisitedAt)).toEqual( + Array.from({ length: 99 }, (_, i) => 9999 - i) + ) + }) + + it('keeps workspace and file identity separate, including separator-like text', () => { + const locations = [ + { kind: 'workspace-doc' as const, worktreeId: 'a::b', filePath: 'c' }, + { kind: 'workspace-doc' as const, worktreeId: 'a', filePath: 'b::c' }, + { kind: 'workspace-doc' as const, worktreeId: 'a', filePath: 'B::c' } + ] + const entries = locations.map((docLocation) => entry({ docLocation })) + expect(normalizeWorkspaceDocHistoryEntries(entries)).toEqual(entries) + }) }) diff --git a/src/shared/workspace-doc-history.ts b/src/shared/workspace-doc-history.ts index bf9e111dc8f..24e3a5c2311 100644 --- a/src/shared/workspace-doc-history.ts +++ b/src/shared/workspace-doc-history.ts @@ -1,4 +1,3 @@ -import { browserPageDocLocationsEqual } from './browser-page-doc-location' import type { BrowserPageDocLocation } from './browser-workspace-types' import { isDocPreviewUrl } from './doc-preview-scheme' @@ -47,6 +46,7 @@ export function normalizeWorkspaceDocHistoryEntries( entries: readonly WorkspaceDocHistoryEntry[] ): WorkspaceDocHistoryEntry[] { const normalized: WorkspaceDocHistoryEntry[] = [] + const seenPathsByWorktree = new Map>() const candidates = [...entries].sort((a, b) => b.lastVisitedAt - a.lastVisitedAt) for (const entry of candidates) { if ( @@ -56,11 +56,19 @@ export function normalizeWorkspaceDocHistoryEntries( ) { continue } - if ( - normalized.some((kept) => browserPageDocLocationsEqual(kept.docLocation, entry.docLocation)) - ) { + // Nested, not a joined key: any separator would collide with worktree ids or paths that + // contain it. Must stay equivalent to `browserPageDocLocationsEqual`, which the store's + // doc-history dedupe still uses — divergence would show up as duplicate dropdown rows. + const { worktreeId, filePath } = entry.docLocation + const seenPaths = seenPathsByWorktree.get(worktreeId) + if (seenPaths?.has(filePath)) { continue } + if (seenPaths) { + seenPaths.add(filePath) + } else { + seenPathsByWorktree.set(worktreeId, new Set([filePath])) + } normalized.push({ ...entry, title: normalizeWorkspaceDocHistoryTitle(entry.title, entry.docLocation)