Let Cmd+J open hosting-provider domains such as github.io (#29989)

Typed hosts like github.io, vercel.app, or readthedocs.io were treated as bare public suffixes and sent to search. They are registrable domains, so they now navigate directly.

- Suffix check now uses ICANN-only rules. The private-section list made `foo.github.io` a domain, but it also let the bare suffix pass, so the classifier now treats the hosting-provider suffix itself as a navigable domain.
- Website navigation now outranks a name match only when the query equals a worktree, tab, or page name in full. Partial matches such as "project.dev server" no longer block the "Go to" row.
- Browser tab creation from a URL forwards the target group as `clientTargetGroupId`, so older hosts that publish the page before the create response still place the tab in the invoking split.
This commit is contained in:
Jinjing
2026-10-16 23:46:30 -07:00
committed by GitHub
parent 77c511b316
commit 05086a892e
10 changed files with 192 additions and 46 deletions
@@ -29,6 +29,13 @@ describe('Cmd+J website navigation', () => {
['brex.com/signup?source=orca#start', 'https://brex.com/signup?source=orca#start'],
['https://brex.com/login', 'https://brex.com/login'],
['docs.github.io', 'https://docs.github.io/'],
['github.io', 'https://github.io/'],
['blogspot.com', 'https://blogspot.com/'],
['readthedocs.io', 'https://readthedocs.io/'],
['vercel.app', 'https://vercel.app/'],
['netlify.app', 'https://netlify.app/'],
['pages.dev', 'https://pages.dev/'],
['workers.dev', 'https://workers.dev/'],
['http://my-app.local:5173', 'http://my-app.local:5173/'],
['localhost:3000', 'http://localhost:3000/'],
['127.0.0.1:5173/docs', 'http://127.0.0.1:5173/docs'],
@@ -53,6 +60,8 @@ describe('Cmd+J website navigation', () => {
'test.spec',
'Cargo.lock',
'my-app.local:5173',
'api.br',
'co.uk',
'/tmp/brex.com',
'javascript:alert(1)',
'file:///tmp/index.html',
@@ -16,7 +16,6 @@ describe('classifyHostUrl suffix gate', () => {
})
})
// The PRIVATE section has to stay in: these are navigable hosts, not search terms.
it('navigates for a private-section suffix carrying a path', () => {
expect(classifyHostUrl('foo.github.io/bar')).toEqual({
kind: 'host-url',
@@ -28,12 +27,25 @@ describe('classifyHostUrl suffix gate', () => {
})
})
it.each(['github.io', 'blogspot.com', 'readthedocs.io', 'vercel.app'])(
'navigates to the hosting-provider domain %s itself',
(host) => {
expect(classifyHostUrl(host, { requireListedDomain: true })).toEqual({
kind: 'host-url',
url: `https://${host}/`
})
expect(classifyHostUrl(`${host}/foo`)).toEqual({
kind: 'host-url',
url: `https://${host}/foo`
})
}
)
// Why this is the fix: psl's 2024 snapshot did not know `api.br`, so `isValid` called it a domain
// and a typed `api.br/x` navigated to a bare public suffix instead of searching.
it('refuses a bare public suffix carrying a path', () => {
expect(classifyHostUrl('api.br/foo')).toBeNull()
expect(classifyHostUrl('co.uk/foo')).toBeNull()
expect(classifyHostUrl('github.io/foo')).toBeNull()
})
it('keeps localhost and IPv4 on http without consulting the suffix list', () => {
@@ -59,11 +59,10 @@ function parseHttpUrl(query: string): ExplicitUrlClassification {
}
}
// Why: a bare public suffix (`api.br`) is not a navigable host, so the typed string stays a search.
// tldts' PRIVATE section is included so `foo.github.io` still reads as a domain.
// Hosting-provider suffixes such as `github.io` are themselves registrable ICANN domains.
function isListedDomain(host: string): boolean {
const parsed = parseDomain(host, { allowPrivateDomains: true })
return parsed.domain !== null && (parsed.isIcann === true || parsed.isPrivate === true)
const parsed = parseDomain(host, { allowPrivateDomains: false })
return parsed.domain !== null && parsed.isIcann === true
}
function splitHostCandidate(
@@ -51,7 +51,7 @@ export function useWorktreeJumpPaletteListEntries({
} = paletteSections
// Exact entity matches keep priority; issue/PR URLs keep their jump and create rows first.
const browserNavigationItem =
taskSourceUrl || middleLeadsSections
taskSourceUrl || !paletteSections.hasExactEntityDestination
? middleItems.find(
(item) =>
item.type === 'quick-action' && item.result.id === BROWSER_NAVIGATION_ACTION_ID
@@ -172,6 +172,10 @@ export function useWorktreeJumpPaletteSections({
})
: null
return {
// Whole-field equality outranks website navigation; exact words alone do not.
hasExactEntityDestination:
worktreeItems.some((item) => (item.match.rank?.destination ?? 3) <= 1) ||
openTabItems.some((item) => (item.result.rank?.destination ?? 3) <= 1),
visibleWorktreeItems: worktrees.visible as PaletteItem[],
worktreeOverflowCount: worktrees.overflowCount,
visibleProjectTargetItems: projectTargets.visible as PaletteItem[],
@@ -57,6 +57,7 @@ function listInput(query: string, overrides: Partial<ListInput> = {}): ListInput
recentTabShortcutIndexById: new Map(),
digitShortcutModifiers: [],
paletteSections: {
hasExactEntityDestination: false,
visibleWorktreeItems: [],
worktreeOverflowCount: 0,
visibleProjectTargetItems: [],
@@ -191,47 +192,57 @@ describe('Cmd+J website selection', () => {
}
)
it.each(['worktree', 'workspace-tab', 'browser-page'] as const)(
'keeps an exact %s name selected ahead of website creation',
async (type) => {
if (type === 'worktree') {
useAppStore.setState({
worktreesByRepo: {
'repo-1': [
makeWorktree('wt-alpha', 'Alpha workspace'),
makeWorktree('wt-beta', 'project.dev')
]
describe.each(['worktree', 'workspace-tab', 'browser-page'] as const)(
'%s name priority',
(type) => {
it.each(['project.dev', 'project.dev server'])(
'gives only a full name match priority over website creation (%s)',
async (name) => {
if (type === 'worktree') {
useAppStore.setState({
worktreesByRepo: {
'repo-1': [
makeWorktree('wt-alpha', 'Alpha workspace'),
makeWorktree('wt-beta', name)
]
}
})
} else if (type === 'workspace-tab') {
const state = useAppStore.getState()
useAppStore.setState({
tabsByWorktree: {
...state.tabsByWorktree,
'wt-beta': [makeTerminalTab('term-beta', 'wt-beta', name)]
},
unifiedTabsByWorktree: {
...state.unifiedTabsByWorktree,
'wt-beta': [makeUnifiedTab('tab-beta', 'wt-beta', 'term-beta', name)]
}
})
} else {
useAppStore.getState().createBrowserTab('wt-beta', 'https://example.com/', {
title: name
})
}
})
} else if (type === 'workspace-tab') {
const state = useAppStore.getState()
useAppStore.setState({
tabsByWorktree: {
...state.tabsByWorktree,
'wt-beta': [makeTerminalTab('term-beta', 'wt-beta', 'project.dev')]
},
unifiedTabsByWorktree: {
...state.unifiedTabsByWorktree,
'wt-beta': [makeUnifiedTab('tab-beta', 'wt-beta', 'term-beta', 'project.dev')]
const { result } = renderHook(usePalette)
await act(async () => result.current.handleQueryChange('project.dev'))
const selected = result.current.selectableItems.find(
(item) => item.id === result.current.commandSelectedItemId
)
expect(selected?.type).toBe(name === 'project.dev' ? type : 'quick-action')
if (name !== 'project.dev') {
expect(selected?.id).toBe('quick-action:navigate-to-url')
}
})
} else {
useAppStore.getState().createBrowserTab('wt-beta', 'https://project.dev/', {
title: 'project.dev'
})
}
const { result } = renderHook(usePalette)
await act(async () => result.current.handleQueryChange('project.dev'))
const selected = result.current.selectableItems.find(
(item) => item.id === result.current.commandSelectedItemId
expect(result.current.middleLeadsSections).toBe(false)
expect(
result.current.selectableItems.filter(
(item) => item.id === 'quick-action:navigate-to-url'
)
).toHaveLength(1)
}
)
expect(selected?.type).toBe(type)
expect(result.current.middleLeadsSections).toBe(false)
expect(
result.current.selectableItems.filter((item) => item.id === 'quick-action:navigate-to-url')
).toHaveLength(1)
}
)
@@ -0,0 +1,85 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as SnapshotModule from '../runtime/web-runtime-session-snapshot'
import { useAppStore } from '../store'
import { openWorkspaceBrowserTab } from './workspace-browser-tab-open'
import { resetWebSessionBrowserPlacementsForTests } from '../runtime/web-session-browser-placement'
import { resetWebSessionTabsSyncTestState, WT } from '../runtime/web-session-tabs-sync-test-harness'
import {
G1,
G2,
HOST,
groupOrder,
hostPages,
installRuntimeTransport,
pendingCreates,
publishHostSnapshot,
resetHostPages,
runtimeStatusesFor,
seedPairedWorktree,
wrapperForRemotePage
} from '../runtime/web-runtime-browser-creation-placement-test-rig'
import { makeRepo, makeWorktree } from '../components/worktree-jump-palette-test-fixtures'
vi.mock('../runtime/web-runtime-session-snapshot', async (importOriginal) => ({
...(await importOriginal<typeof SnapshotModule>()),
refreshWebRuntimeSessionTabsSnapshot: vi.fn(async () => publishHostSnapshot())
}))
const initialState = useAppStore.getInitialState()
describe('workspace URL tab placement', () => {
beforeEach(() => {
useAppStore.setState(initialState, true)
resetWebSessionTabsSyncTestState()
resetWebSessionBrowserPlacementsForTests()
resetHostPages()
seedPairedWorktree()
installRuntimeTransport()
useAppStore.setState({
repos: [{ ...makeRepo(), id: 'repo', executionHostId: HOST }],
worktreesByRepo: {
repo: [makeWorktree(WT, 'Workspace', { repoId: 'repo', hostId: HOST })]
}
})
})
afterEach(() => {
vi.unstubAllGlobals()
resetWebSessionTabsSyncTestState()
resetWebSessionBrowserPlacementsForTests()
useAppStore.setState(initialState, true)
})
it.each([true, false])(
'keeps the invoking split when a host snapshot precedes the create response (known page ID: %s)',
async (knownPageId) => {
useAppStore.setState({
runtimeStatusByEnvironmentId: runtimeStatusesFor([
'browser.screencast.v1',
...(knownPageId ? ['browser.tab-create-known-id.v1'] : [])
])
})
const creating = openWorkspaceBrowserTab({
workspaceId: WT,
targetGroupId: G1,
url: 'https://example.com/',
intent: { kind: 'url' },
placementPreference: 'server'
})
expect(pendingCreates).toHaveLength(1)
expect(groupOrder(G1)).toHaveLength(3)
useAppStore.getState().focusGroup(WT, G2)
const pageId = pendingCreates[0].params.page ?? 'host-minted-page'
hostPages.push(pageId)
publishHostSnapshot()
hostPages.pop()
pendingCreates[0].resolve(pageId)
await creating
expect(wrapperForRemotePage(pageId)?.groupId).toBe(G1)
expect(useAppStore.getState().activeGroupIdByWorktree[WT]).toBe(G2)
publishHostSnapshot()
expect(wrapperForRemotePage(pageId)?.groupId).toBe(G1)
}
)
})
@@ -176,6 +176,7 @@ describe('openWorkspaceBrowserTab', () => {
await openWorkspaceBrowserTab({
workspaceId: WORKSPACE_ID,
url: 'https://example.com/docs?token=secret',
targetGroupId: 'group-1',
intent: { kind: 'url' }
})
@@ -183,7 +184,8 @@ describe('openWorkspaceBrowserTab', () => {
worktreeId: WORKSPACE_ID,
environmentId: 'hub-a',
url: 'https://example.com/docs?token=secret',
targetGroupId: undefined,
targetGroupId: 'group-1',
clientTargetGroupId: 'group-1',
selectWorktree: true,
stagedTitle: 'example.com/docs',
stagedFocusAddressBar: false,
@@ -294,6 +294,8 @@ export async function openWorkspaceBrowserTab(
environmentId,
url: request.url,
targetGroupId: request.targetGroupId,
// Client placement survives older hosts publishing a page before the create response.
...(request.targetGroupId ? { clientTargetGroupId: request.targetGroupId } : {}),
...(request.afterTabId ? { clientAfterTabId: request.afterTabId } : {}),
// Owner-pinned links need the host tab published before client reconciliation.
...(expectedEnvironmentId !== null ? { waitForRegistration: true } : {}),
@@ -32,6 +32,28 @@ for (const theme of ['light', 'dark'] as const) {
await input.fill('brex login')
await expect(website).toHaveCount(0)
await input.fill('readthedocs.io')
await expect(website).toContainText('Go to readthedocs.io')
await expect(website).toHaveAttribute('data-selected', 'true')
await orcaPage.evaluate(() => {
window.__store!.setState((state) => ({
worktreesByRepo: Object.fromEntries(
Object.entries(state.worktreesByRepo).map(([repoId, worktrees]) => [
repoId,
worktrees.map((worktree) =>
worktree.id === state.activeWorktreeId
? { ...worktree, displayName: 'project.dev server' }
: worktree
)
])
)
}))
})
await input.fill('project.dev')
await expect(palette.locator('[cmdk-item]').first()).toContainText('Go to project.dev')
await expect(website).toHaveAttribute('data-selected', 'true')
const server = await startBrowserLinkServer()
try {
const address = server.sourceUrl.replace('http://', '')