mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
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:
@@ -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://', '')
|
||||
|
||||
Reference in New Issue
Block a user