fix(new-tab): Enter on a visited site's name opens the site, not a file (#28804)

* Rank browser history above fuzzy file matches in the omnibox

Scattered-character fuzzy file matches are weak evidence, so a remembered
page should appear above them. Exact and literal file matches and typed
URLs still rank above history. Adds a test for typing a host like
linear.app, which should rank the page above unrelated fuzzy files.

* Anchor omnibox history on the typed URL instead of demoting fuzzy files

The real bug: after visiting https://linear.app/, typing `linear.app`
made Enter open an unrelated file. The history row removes the typed-URL
row it duplicates, but was then placed below every file match, so the
page lost the top slot the URL row had.

History now sits with the typed URL row (in its slot when it replaced
it). Queries with no typed URL keep the old rule: below every file match.

This replaces the previous commit's rule of ranking history above
"fuzzy" files. That label covers every non-exact match for queries with
a slash, a dot suffix or a space, so a code-host page whose title holds
the file path took Enter away from local rows for path queries (or left
nothing selected when pasted). It also left `localhost` broken when a
file name contained it.
This commit is contained in:
Jinjing
2026-10-14 13:03:37 -07:00
committed by GitHub
parent 657a47c25c
commit 5976c4628d
5 changed files with 200 additions and 63 deletions
@@ -79,6 +79,18 @@ function press(key: string): void {
})
}
function rowTexts(): string[] {
return [...container.querySelectorAll('[role="option"]')].map((row) => row.textContent ?? '')
}
function seedBrowserHistory(url: string, title: string): void {
useAppStore.setState({
browserUrlHistory: [
{ url, normalizedUrl: url.replace(/\/$/, ''), title, lastVisitedAt: 1, visitCount: 1 }
]
})
}
beforeEach(() => {
fileListMock.current = { files: [], loading: false, loadError: null }
tabSearchMock.calls = []
@@ -134,6 +146,83 @@ describe('TabBarCreateEntry search behavior', () => {
)
})
it.each([false, true])(
'keeps a typed host and its pages above unrelated files (visited root: %s)',
(visitedRoot) => {
fileListMock.current.files = [
'mobile/rpc-foundation/goldens/matrix-transport.capability-probe-status.get-1.json',
'mobile/rpc-foundation/goldens/matrix-files.terminal-path-tap-files.open-1.json',
'mobile/rpc-foundation/goldens/matrix-files.terminal-path-tap-files.resolveterminalpath-1.json'
]
const url = visitedRoot ? 'https://linear.app/' : 'https://linear.app/acme/issues'
seedBrowserHistory(url, 'Linear')
const onOpenEntry = vi.fn().mockResolvedValue(undefined)
renderEntry({ onOpenEntry })
setQuery('linear.app')
const rows = rowTexts()
const pageIndex = rows.findIndex((row) => row.includes('Open page'))
expect(pageIndex).toBeGreaterThanOrEqual(0)
expect(rows.findIndex((row) => row.includes('Open file'))).toBeGreaterThan(pageIndex)
expect(container.querySelector('[aria-selected="true"]')?.textContent).toContain('linear.app')
submit()
expect(onOpenEntry).toHaveBeenCalledWith(
expect.objectContaining({
classification: visitedRoot
? { kind: 'explicit-url', url }
: { kind: 'host-url', url: 'https://linear.app/' }
})
)
}
)
it('still opens a visited host above a filename that contains it', () => {
fileListMock.current.files = ['config/localhost.ts']
seedBrowserHistory('http://localhost/', 'Local')
const onOpenEntry = vi.fn().mockResolvedValue(undefined)
renderEntry({ onOpenEntry })
setQuery('localhost')
submit()
expect(onOpenEntry).toHaveBeenCalledWith(
expect.objectContaining({
classification: { kind: 'explicit-url', url: 'http://localhost/' }
})
)
})
// A code-host page title carries the file path, so it matches path queries too.
it.each([false, true])(
'keeps a matching page below local rows for a path query (typed: %s)',
(typed) => {
fileListMock.current.files = [
'src/renderer/src/components/tab-bar/TabBarCreateEntry.tsx',
'src/renderer/src/components/tab-bar/TabBarCreateEntryRow.tsx'
]
seedBrowserHistory(
'https://github.com/acme/app/blob/main/src/renderer/src/components/tab-bar/TabBarCreateEntry.tsx',
'app/src/renderer/src/components/tab-bar/TabBarCreateEntry.tsx at main · acme/app'
)
const onOpenEntry = vi.fn().mockResolvedValue(undefined)
renderEntry({ onOpenEntry })
const query = 'components/tab-bar/TabBarCreate'
if (typed) {
for (let length = 1; length <= query.length; length += 1) {
setQuery(query.slice(0, length))
}
} else {
setQuery(query)
}
const rows = rowTexts()
const pageIndex = rows.findIndex((row) => row.includes('Open page'))
expect(pageIndex).toBeGreaterThan(rows.findLastIndex((row) => row.includes('Open file')))
submit()
expect(onOpenEntry).toHaveBeenCalledOnce()
expect(onOpenEntry.mock.calls[0]?.[0].classification.kind).not.toBe('explicit-url')
}
)
it.each(['a', 'ab', 'asciinema'])('waits for an exact filename before arming %s', (query) => {
fileListMock.current = { files: [], loading: true, loadError: null }
const onOpenEntry = vi.fn().mockResolvedValue(undefined)
@@ -16,7 +16,7 @@ import {
resultOptionDomId
} from './TabBarCreateEntryRow'
import { dropFileEntriesCoveredByTabResults } from './open-tab-entry-dedupe'
import { insertHistoryRowsBelowFileMatches } from './tab-create-entry-history-placement'
import { placeHistoryRowsInEntryOptions } from './tab-create-entry-history-placement'
import { useOmniboxBrowserHistory } from './use-omnibox-browser-history'
import { useTabEntryMenuReturnFocus } from './use-tab-entry-menu-return-focus'
import { activateOpenTabSearchResult } from './open-tab-selection-routing'
@@ -167,7 +167,7 @@ function TabBarCreateEntrySession({
kind: 'agent' as const,
option
})),
...insertHistoryRowsBelowFileMatches(options, historyRows)
...placeHistoryRowsInEntryOptions(options, historyRows)
]
const { activeSelectedIndex, selectedActiveOption } = useNetworkSafeTabEntrySelection({
activeOptions,
@@ -63,10 +63,10 @@ export function dropHistoryRowsCoveredByBrowserPages(
// The history row carries a real page title, so it beats the bare typed-URL row
// it duplicates — mirroring the address bar's synthetic-row fold.
export function dropUrlEntriesCoveredByHistoryRows(
options: readonly TabEntryOption[],
export function dropUrlEntriesCoveredByHistoryRows<Option extends TabEntryOption>(
options: readonly Option[],
rows: readonly BrowserHistoryOmniboxRow[]
): readonly TabEntryOption[] {
): readonly Option[] {
if (rows.length === 0) {
return options
}
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { insertHistoryRowsBelowFileMatches } from './tab-create-entry-history-placement'
import { placeHistoryRowsInEntryOptions } from './tab-create-entry-history-placement'
import type { BrowserHistoryOmniboxRow } from './tab-create-entry-active-option'
import type { TabEntryOption } from './tab-create-entry-action'
import type { ExistingFileMatch } from './tab-create-entry-file-matches'
@@ -22,6 +22,14 @@ const newFileOption: TabEntryOption = {
id: 'new-file:readme',
classification: { kind: 'new-file', relativePath: 'readme' }
}
const readmeHostOption: TabEntryOption = {
id: 'host-url:https://readme.io/docs',
classification: { kind: 'host-url', url: 'https://readme.io/docs' }
}
const exampleHostOption: TabEntryOption = {
id: 'host-url:https://example.com',
classification: { kind: 'host-url', url: 'https://example.com' }
}
function historyRow(url: string): BrowserHistoryOmniboxRow {
return {
@@ -38,7 +46,7 @@ function kinds(options: readonly { kind: string }[]): string[] {
describe('tab create entry history placement', () => {
it('lands history below every file match and above new-file and search', () => {
const placed = insertHistoryRowsBelowFileMatches(
const placed = placeHistoryRowsInEntryOptions(
[
fileOption('README.md', 'exact-basename'),
fileOption('docs/readme-notes.md', 'literal-basename'),
@@ -54,20 +62,23 @@ describe('tab create entry history placement', () => {
expect(placed[3]).toMatchObject({ option: { id: 'new-file:readme' } })
})
it.each(['literal-basename', 'fuzzy'] as const)('stays below a lone %s file row', (matchKind) => {
const placed = insertHistoryRowsBelowFileMatches(
[fileOption('docs/readme-notes.md', matchKind), searchOption],
rows
)
it.each(['exact-path', 'exact-basename', 'literal-basename', 'fuzzy'] as const)(
'stays below a lone %s file row',
(matchKind) => {
const placed = placeHistoryRowsInEntryOptions(
[fileOption('docs/readme-notes.md', matchKind), searchOption],
rows
)
expect(kinds(placed)).toEqual(['entry', 'history', 'entry'])
expect(placed[0]).toMatchObject({ option: { id: 'existing-file:docs/readme-notes.md' } })
})
expect(kinds(placed)).toEqual(['entry', 'history', 'entry'])
expect(placed[0]).toMatchObject({ option: { id: 'existing-file:docs/readme-notes.md' } })
}
)
// The phrase-query ordering: the classifier puts search first, and history
// still has to end up under the fuzzy files that follow it.
it('sinks past fuzzy files the classifier ranked below web search', () => {
const placed = insertHistoryRowsBelowFileMatches(
const placed = placeHistoryRowsInEntryOptions(
[searchOption, fileOption('docs/readme-notes.md', 'fuzzy'), newFileOption],
rows
)
@@ -77,43 +88,65 @@ describe('tab create entry history placement', () => {
})
it('leads the entry block when no file or typed url matched at all', () => {
const placed = insertHistoryRowsBelowFileMatches([searchOption, newFileOption], rows)
const placed = placeHistoryRowsInEntryOptions([searchOption, newFileOption], rows)
expect(kinds(placed)).toEqual(['history', 'entry', 'entry'])
})
it('drops a typed-url row the history row already covers', () => {
const placed = insertHistoryRowsBelowFileMatches(
[
{
id: 'host-url:https://readme.io/docs',
classification: { kind: 'host-url', url: 'https://readme.io/docs' }
},
searchOption
],
rows
)
const placed = placeHistoryRowsInEntryOptions([readmeHostOption, searchOption], rows)
expect(kinds(placed)).toEqual(['history', 'entry'])
expect(placed[1]).toMatchObject({ option: { id: 'search:readme' } })
})
it('keeps a typed-url row that points somewhere else, and stays under it', () => {
const placed = insertHistoryRowsBelowFileMatches(
[
{
id: 'host-url:https://example.com',
classification: { kind: 'host-url', url: 'https://example.com' }
}
],
rows
)
const placed = placeHistoryRowsInEntryOptions([exampleHostOption], rows)
expect(kinds(placed)).toEqual(['entry', 'history'])
})
// Visiting a site must not change what Enter does on its name.
it('takes the slot of a covered typed-url row above the files below it', () => {
const placed = placeHistoryRowsInEntryOptions(
[readmeHostOption, fileOption('docs/readme-notes.md', 'fuzzy')],
rows
)
expect(kinds(placed)).toEqual(['history', 'entry'])
expect(placed[1]).toMatchObject({ option: { id: 'existing-file:docs/readme-notes.md' } })
})
it('joins an uncovered typed-url row above the files below it', () => {
const placed = placeHistoryRowsInEntryOptions(
[exampleHostOption, fileOption('docs/readme-notes.md', 'fuzzy')],
rows
)
expect(kinds(placed)).toEqual(['entry', 'history', 'entry'])
expect(placed[0]).toMatchObject({ option: { id: 'host-url:https://example.com' } })
})
it.each([
{ relation: 'replaces', url: readmeHostOption, expected: ['entry', 'history', 'entry'] },
{ relation: 'joins', url: exampleHostOption, expected: ['entry', 'entry', 'history', 'entry'] }
])('keeps exact files above history that $relation the typed-url row', ({ url, expected }) => {
const placed = placeHistoryRowsInEntryOptions(
[
fileOption('README.md', 'exact-basename'),
url,
fileOption('docs/readme-notes.md', 'literal-basename')
],
rows
)
expect(kinds(placed)).toEqual(expected)
expect(placed[0]).toMatchObject({ option: { id: 'existing-file:README.md' } })
expect(placed.at(-1)).toMatchObject({ option: { id: 'existing-file:docs/readme-notes.md' } })
})
it('skips status rows, which are not actionable options', () => {
const placed = insertHistoryRowsBelowFileMatches(
const placed = placeHistoryRowsInEntryOptions(
[{ id: 'loading', classification: { kind: 'blocked', message: 'Loading files...' } }],
rows
)
@@ -122,7 +155,7 @@ describe('tab create entry history placement', () => {
})
it('returns only the mapped entry rows when there is no history to insert', () => {
const placed = insertHistoryRowsBelowFileMatches([searchOption], [])
const placed = placeHistoryRowsInEntryOptions([searchOption], [])
expect(kinds(placed)).toEqual(['entry'])
})
@@ -1,48 +1,63 @@
// Where the browser-history block lands inside the omnibox entry rows: below
// every file the query matched and below anything the user typed as a URL,
// above the new-file and web-search fallbacks.
// Where the browser-history block lands inside the omnibox entry rows: with the
// typed URL when the query is a web address, otherwise below every file the
// query matched and above the new-file and web-search fallbacks.
import { dropUrlEntriesCoveredByHistoryRows } from './open-tab-entry-dedupe'
import {
isActiveEntryOption,
type ActiveEntryOption,
type ActiveOption,
type BrowserHistoryOmniboxRow
} from './tab-create-entry-active-option'
import type { TabEntryOption } from './tab-create-entry-action'
// Leaving the workspace is the heavier action, so a local file outranks a
// remembered page even when the match was fuzzy.
function isFileOrUrlOption(option: TabEntryOption): boolean {
function isTypedUrlOption(option: TabEntryOption): boolean {
const { kind } = option.classification
return (
kind === 'existing-file' ||
kind === 'absolute-file' ||
kind === 'explicit-url' ||
kind === 'host-url'
)
return kind === 'explicit-url' || kind === 'host-url'
}
export function insertHistoryRowsBelowFileMatches(
entryOptions: readonly TabEntryOption[],
historyRows: readonly BrowserHistoryOmniboxRow[]
): readonly ActiveOption[] {
const options = dropUrlEntriesCoveredByHistoryRows(entryOptions, historyRows)
.filter(isActiveEntryOption)
.map((option) => ({ kind: 'entry' as const, option }))
if (historyRows.length === 0) {
return options
// Leaving the workspace is the heavier action, so a local file outranks a
// remembered page even when the match was fuzzy.
function isFileOption(option: TabEntryOption): boolean {
const { kind } = option.classification
return kind === 'existing-file' || kind === 'absolute-file'
}
function historyInsertIndex(
options: readonly ActiveEntryOption[],
keptOptions: readonly ActiveEntryOption[]
): number {
// History sits with the typed URL row (in its slot once it replaced it), so
// visiting a site never changes what Enter does on its name.
const urlIndex = options.findIndex(isTypedUrlOption)
if (urlIndex !== -1) {
return keptOptions.includes(options[urlIndex]) ? urlIndex + 1 : urlIndex
}
// Scanning for the last one, not the leading run: the classifier promotes web
// search above fuzzy files for phrase queries, and history belongs under both.
let insertAt = 0
for (const [index, option] of options.entries()) {
if (isFileOrUrlOption(option.option)) {
for (const [index, option] of keptOptions.entries()) {
if (isFileOption(option)) {
insertAt = index + 1
}
}
return insertAt
}
export function placeHistoryRowsInEntryOptions(
entryOptions: readonly TabEntryOption[],
historyRows: readonly BrowserHistoryOmniboxRow[]
): readonly ActiveOption[] {
const options = entryOptions.filter(isActiveEntryOption)
const keptOptions = dropUrlEntriesCoveredByHistoryRows(options, historyRows)
const rows = keptOptions.map((option) => ({ kind: 'entry' as const, option }))
if (historyRows.length === 0) {
return rows
}
const insertAt = historyInsertIndex(options, keptOptions)
return [
...options.slice(0, insertAt),
...rows.slice(0, insertAt),
...historyRows.map((option) => ({ kind: 'history' as const, option })),
...options.slice(insertAt)
...rows.slice(insertAt)
]
}