diff --git a/src/renderer/src/components/tab-bar/TabBarCreateEntry.history.test.tsx b/src/renderer/src/components/tab-bar/TabBarCreateEntry.history.test.tsx index 8b08190fe81..a1ec5fad293 100644 --- a/src/renderer/src/components/tab-bar/TabBarCreateEntry.history.test.tsx +++ b/src/renderer/src/components/tab-bar/TabBarCreateEntry.history.test.tsx @@ -41,7 +41,7 @@ vi.mock('@/store', () => ({ browserDefaultSearchEngine: 'google', getKnownWorktreeById: () => ({ path: '/tmp/wt' }) }), - { getState: () => ({}) } + { getState: () => ({ browserUrlHistory: historyStoreMock.entries }) } ) })) vi.mock('react', async () => { diff --git a/src/renderer/src/components/tab-bar/use-omnibox-browser-history.ts b/src/renderer/src/components/tab-bar/use-omnibox-browser-history.ts index cb099d04db1..9b1d9947a08 100644 --- a/src/renderer/src/components/tab-bar/use-omnibox-browser-history.ts +++ b/src/renderer/src/components/tab-bar/use-omnibox-browser-history.ts @@ -85,9 +85,14 @@ export function useOmniboxBrowserHistory({ query: string tabResults: readonly OpenTabSearchResult[] }): readonly BrowserHistoryOmniboxRow[] { - // Why null while disabled: the tab bar is mounted for every workspace, and - // this array is replaced on every committed navigation in any browser tab. - const history = useAppStore((store) => (enabled ? store.browserUrlHistory : null)) + // Why read once: the tab bar is mounted for every workspace, and this array + // is replaced on every committed navigation in any browser tab. History is + // intentionally a per-open snapshot so background navigations cannot churn + // the omnibox or reshuffle a selection under the user's fingers. + const history = useMemo( + () => (enabled ? useAppStore.getState().browserUrlHistory : null), + [enabled] + ) const prepared = useMemo( () => (history ? prepareBrowserHistoryEntries(history) : null), // oxlint-disable-next-line react-hooks/exhaustive-deps -- Snapshot per menu open; a background navigation must not reshuffle rows mid-keystroke. diff --git a/src/renderer/src/lib/browser-history-match.performance.test.ts b/src/renderer/src/lib/browser-history-match.performance.test.ts index 4149272b4d6..7a99c2ad49d 100644 --- a/src/renderer/src/lib/browser-history-match.performance.test.ts +++ b/src/renderer/src/lib/browser-history-match.performance.test.ts @@ -26,7 +26,8 @@ const WORST_QUERY = 'observability rollout' function percentile95(samples: number[]): number { const sorted = [...samples].sort((a, b) => a - b) - return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))] + // Nearest-rank p95: ceil(n*0.95)-1, so 20 samples pick index 18 rather than the max. + return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * 0.95) - 1)] } describe('browser history match performance budget', () => { @@ -41,7 +42,9 @@ describe('browser history match performance budget', () => { const samples: number[] = [] for (let run = 0; run < 20; run += 1) { const start = performance.now() - prepareBrowserHistoryEntries(entries) + // Use a fresh array identity so this measures preparation rather than + // the identity cache used by live address bars and omniboxes. + prepareBrowserHistoryEntries(entries.slice()) samples.push(performance.now() - start) } expect(percentile95(samples)).toBeLessThan(prepareP95Ms) diff --git a/src/renderer/src/lib/browser-history-match.test.ts b/src/renderer/src/lib/browser-history-match.test.ts index 7235edc6c40..74d82f8bb57 100644 --- a/src/renderer/src/lib/browser-history-match.test.ts +++ b/src/renderer/src/lib/browser-history-match.test.ts @@ -56,6 +56,12 @@ describe('browser history match', () => { ]) }) + it('keeps fully-qualified URL prefixes in the top tier', () => { + expect(match([entry({ url: 'https://github.com/acme' })], 'https://github.com')).toEqual([ + { tier: 'host-prefix', url: 'https://github.com/acme' } + ]) + }) + it('leaves a subdomain match in host-substring rather than host-prefix', () => { expect(match([entry({ url: 'https://docs.github.com/actions' })], 'git')).toEqual([ { tier: 'host-substring', url: 'https://docs.github.com/actions' } @@ -131,6 +137,13 @@ describe('browser history match', () => { expect(matchBrowserHistory({ prepared, query: 'git', limit: 0, now: NOW })).toBe(missA) }) + it('reuses preparation for the same immutable history snapshot', () => { + const first = prepareBrowserHistoryEntries(GIT_CORPUS) + + expect(prepareBrowserHistoryEntries(GIT_CORPUS)).toBe(first) + expect(prepareBrowserHistoryEntries([...GIT_CORPUS])).not.toBe(first) + }) + it('survives an unparseable url by falling back to url and title matching', () => { const broken = entry({ url: 'not a url at all', title: 'Broken' }) diff --git a/src/renderer/src/lib/browser-history-match.ts b/src/renderer/src/lib/browser-history-match.ts index 9c7fb443d2e..88b760f48f9 100644 --- a/src/renderer/src/lib/browser-history-match.ts +++ b/src/renderer/src/lib/browser-history-match.ts @@ -34,6 +34,10 @@ export type BrowserHistoryMatch = { } const NO_MATCHES: readonly BrowserHistoryMatch[] = [] +const preparedHistoryCache = new WeakMap< + readonly BrowserHistoryEntry[], + readonly PreparedBrowserHistoryEntry[] +>() function historyHost(url: string): string { try { @@ -53,13 +57,19 @@ export function formatBrowserHistoryUrl(url: string): string { export function prepareBrowserHistoryEntries( entries: readonly BrowserHistoryEntry[] ): readonly PreparedBrowserHistoryEntry[] { - return entries.map((entry) => ({ + const cached = preparedHistoryCache.get(entries) + if (cached) { + return cached + } + const prepared = entries.map((entry) => ({ entry, lowerUrl: entry.url.toLowerCase(), lowerTitle: entry.title.toLowerCase(), lowerHost: historyHost(entry.url), frecencyBase: Math.min(entry.visitCount, MAX_VISIT_COUNT_BONUS) })) + preparedHistoryCache.set(entries, prepared) + return prepared } function matchTier( @@ -69,7 +79,9 @@ function matchTier( // A workspace-doc entry's url is a filesystem path, so it has no host: a path // prefix is as deliberate as a host prefix and earns the same top tier. const prefixTarget = prepared.lowerHost === '' ? prepared.lowerUrl : prepared.lowerHost - if (prefixTarget.startsWith(lowerQuery)) { + // Preserve the address-bar's long-standing behavior for fully-qualified + // input (e.g. `https://github.com`), while still ranking bare hosts by host. + if (prefixTarget.startsWith(lowerQuery) || prepared.lowerUrl.startsWith(lowerQuery)) { return 'host-prefix' } if (prepared.lowerHost.includes(lowerQuery)) {