mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 08:01:56 +00:00
Break browser history ties with URL for stable snapshot ordering
When browser history entries tie on tier, score, and recency, the sort order can become non-deterministic, especially when combining browser and document history that may be reordered in snapshots. Add normalizedUrl as the final tie-breaker to guarantee consistent ordering.
This commit is contained in:
+21
-19
@@ -76,28 +76,30 @@ export function buildBrowserAddressBarSuggestions({
|
||||
.sort((a, b) => b.lastVisitedAt - a.lastVisitedAt)
|
||||
.slice(0, MAX_BROWSER_ADDRESS_BAR_SUGGESTIONS)
|
||||
}
|
||||
const documentSuggestions = workspaceDocHistory.map(toWorkspaceDocSuggestion)
|
||||
const historyRows: { entry: BrowserHistoryEntry; row: BrowserAddressBarSuggestion }[] = [
|
||||
...browserUrlHistory.map((entry) => ({
|
||||
entry,
|
||||
row: { ...entry, subtitle: entry.url, isSearch: false }
|
||||
})),
|
||||
...documentSuggestions.map((row) => ({
|
||||
entry: {
|
||||
url: row.url,
|
||||
normalizedUrl: row.url,
|
||||
title: row.title,
|
||||
lastVisitedAt: row.lastVisitedAt,
|
||||
visitCount: row.visitCount
|
||||
},
|
||||
row
|
||||
}))
|
||||
]
|
||||
const rowByEntry = new Map(historyRows.map(({ entry, row }) => [entry, row]))
|
||||
const documentRows = workspaceDocHistory.map(toWorkspaceDocSuggestion)
|
||||
const documentEntries: BrowserHistoryEntry[] = documentRows.map((row) => ({
|
||||
url: row.url,
|
||||
normalizedUrl: row.url,
|
||||
title: row.title,
|
||||
lastVisitedAt: row.lastVisitedAt,
|
||||
visitCount: row.visitCount
|
||||
}))
|
||||
const rowByEntry = new Map<BrowserHistoryEntry, BrowserAddressBarSuggestion>()
|
||||
for (const entry of browserUrlHistory) {
|
||||
rowByEntry.set(entry, { ...entry, subtitle: entry.url, isSearch: false })
|
||||
}
|
||||
documentEntries.forEach((entry, index) => rowByEntry.set(entry, documentRows[index]))
|
||||
// Why prepare the caller's array as-is: it is the stable `browserUrlHistory`
|
||||
// identity, so the prepare cache hits instead of re-lowercasing every keystroke.
|
||||
const preparedHistory = prepareBrowserHistoryEntries(browserUrlHistory)
|
||||
const prepared =
|
||||
documentEntries.length === 0
|
||||
? preparedHistory
|
||||
: [...preparedHistory, ...prepareBrowserHistoryEntries(documentEntries)]
|
||||
// Why url-tail is kept here: the address bar is a navigation surface, so a
|
||||
// path-only recall is still a destination — it just never outranks a real one.
|
||||
const historySuggestions: BrowserAddressBarSuggestion[] = matchBrowserHistory({
|
||||
prepared: prepareBrowserHistoryEntries(historyRows.map(({ entry }) => entry)),
|
||||
prepared,
|
||||
query: trimmed,
|
||||
limit: MAX_BROWSER_ADDRESS_BAR_SUGGESTIONS - 1
|
||||
})
|
||||
|
||||
@@ -124,6 +124,17 @@ describe('browser history match', () => {
|
||||
expect(match(tied.toReversed(), 'acme')).toEqual(first)
|
||||
})
|
||||
|
||||
it('breaks a full tie on the url so a reordered snapshot keeps the same order', () => {
|
||||
const tied = [
|
||||
entry({ url: 'https://acme.dev/b', visitCount: 3, lastVisitedAt: NOW - 5 * HOUR }),
|
||||
entry({ url: 'https://acme.dev/a', visitCount: 3, lastVisitedAt: NOW - 5 * HOUR })
|
||||
]
|
||||
|
||||
const expected = ['https://acme.dev/a', 'https://acme.dev/b']
|
||||
expect(match(tied, 'acme').map((row) => row.url)).toEqual(expected)
|
||||
expect(match(tied.toReversed(), 'acme').map((row) => row.url)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('honours the limit and returns the shared empty array when nothing matches', () => {
|
||||
expect(match(GIT_CORPUS, 'git', { limit: 2 })).toHaveLength(2)
|
||||
const prepared = prepareBrowserHistoryEntries(GIT_CORPUS)
|
||||
|
||||
@@ -130,12 +130,14 @@ export function matchBrowserHistory({
|
||||
if (matches.length === 0) {
|
||||
return NO_MATCHES
|
||||
}
|
||||
// Why lastVisitedAt last: ordering must not wobble between renders on a tie.
|
||||
// Why the url last: ordering must not wobble between renders when a snapshot
|
||||
// reorders two entries that also tie on tier, score and recency.
|
||||
matches.sort(
|
||||
(a, b) =>
|
||||
TIER_RANK[a.tier] - TIER_RANK[b.tier] ||
|
||||
b.score - a.score ||
|
||||
b.entry.lastVisitedAt - a.entry.lastVisitedAt
|
||||
b.entry.lastVisitedAt - a.entry.lastVisitedAt ||
|
||||
a.entry.normalizedUrl.localeCompare(b.entry.normalizedUrl)
|
||||
)
|
||||
return matches.slice(0, limit)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user