Files
orca/src/preload/api/browser-api.ts
T
Jinjing f811ee0740 Open open new link should not navigate away from current link (#18873)
* fix(browser): open modifier-click and middle-click links in background t

Links opened with modifier keys (Cmd/Ctrl+click) and middle-click now open
in background tabs, matching Chrome's behavior. Shift+middle-click continues
to open in the foreground tab. The routing system now tracks separate
foreground and background frame names, with an `activate` flag controlling
whether the new tab is brought to focus.

* fix(browser): don't navigate away when opening links in background tabs

When opening links via context menu or other mechanisms that create background tabs, keep focus on the source tab rather than automatically switching to the newly opened tab. Set `activate: false` on tab creation to prevent unwanted navigation away from the current page.

* fix(browser): silence popup notices for links opened in Orca tabs

Links that open in new Orca tabs are immediately visible to the user and
don't warrant a toast notification. Only external popup opens now show
notifications, reducing unnecessary clutter while still alerting the user
to unexpected external window opens.

* Replace loading dots with animated spinner icons

Replaces the small dot indicators with animated Loader2 icons that
appear in place of the favicon while tabs are loading. Provides
clearer, more prominent visual feedback during navigation.

* test(browser-tab): verify target=_blank links don't navigate source tab

Add a test case checking that plain main-frame target=_blank clicks open
in a new tab without navigating the source tab away. Extract
startBrowserLinkServer to a helper module and add the /blank-destination
endpoint to support the new test case.

* refactor(browser): localize clicked-link routing frame names

Remove the global clickedLinkFrameNamesByGuestId state map and generate
frame names locally within installGuestPopupPolicy, improving state
encapsulation and simplifying cleanup logic. Functionality unchanged.

* test(browser-tab): hold shift for middle-click gestures

* test(browser-tab): drop duplicate shift-middle gesture

* test(browser-favicon): verify spinner shown while favicon reloads

Updated test expectations to reflect that the favicon component shows a
loading spinner during reload instead of keeping the previous image
mounted.

* fix ci
2026-09-05 21:38:35 -07:00

188 lines
7.6 KiB
TypeScript

import type { BrowserSetAnnotationViewportBridgeArgs } from '../../shared/browser-annotation-viewport-bridge'
import type {
BrowserClientPageMetadataParams,
BrowserClientPageMetadataPublishOutcome
} from '../../shared/browser-client-page-metadata-protocol'
import type {
BrowserWebAuthnAccountRequest,
BrowserWebAuthnAccountResponse
} from '../../shared/browser-webauthn-account'
import type {
BrowserSetGrabModeArgs,
BrowserSetGrabModeResult,
BrowserAwaitGrabSelectionArgs,
BrowserGrabResult,
BrowserCancelGrabArgs,
BrowserCaptureSelectionScreenshotArgs,
BrowserCaptureSelectionScreenshotResult,
BrowserExtractHoverArgs,
BrowserExtractHoverResult
} from '../../shared/browser-grab-types'
import type {
BrowserContextMenuDismissedEvent,
BrowserContextMenuRequestedEvent,
BrowserDownloadFinishedEvent,
BrowserDownloadProgressEvent,
BrowserDownloadRequestedEvent,
BrowserPermissionDeniedEvent,
BrowserPopupEvent
} from '../../shared/browser-guest-events'
import type {
BrowserCertificateFailure,
BrowserCertificateProceedResult,
BrowserCookieImportResult,
BrowserLoadError,
BrowserSessionProfile,
BrowserSessionProfileCreateOptions,
BrowserSessionProfileScope,
BrowserSessionProfileSource,
BrowserViewportOverride,
BrowserViewportScrollState
} from '../../shared/browser-workspace-types'
import type {
BrowserClientPageRendererOutcome,
BrowserClientPageRendererRequest
} from '../../shared/browser-client-page-renderer-protocol'
export type BrowserApi = {
/** Absent wherever this client hosts no guests of its own, which is how the web client reads. */
readClientHostId?: () => string | null
onClientPageRendererRequest?: (
callback: (
request: BrowserClientPageRendererRequest
) => BrowserClientPageRendererOutcome | Promise<BrowserClientPageRendererOutcome>
) => () => void
registerGuest: (args: {
browserPageId: string
workspaceId: string
worktreeId: string
sessionProfileId?: string | null
webContentsId: number
}) => Promise<boolean>
isGuestRegistered: (args: { browserPageId: string; webContentsId: number }) => Promise<boolean>
repairGuestRegistration: (args: {
browserPageId: string
workspaceId: string
worktreeId: string
sessionProfileId?: string | null
webContentsId: number
}) => Promise<boolean>
unregisterGuest: (args: { browserPageId: string }) => Promise<void>
onWebAuthnAccountRequest: (
callback: (request: BrowserWebAuthnAccountRequest) => void
) => () => void
onWebAuthnAccountRequestClosed: (callback: (event: { requestId: string }) => void) => () => void
respondWebAuthnAccount: (response: BrowserWebAuthnAccountResponse) => Promise<boolean>
openDevTools: (args: { browserPageId: string }) => Promise<boolean>
setViewportOverride: (args: {
browserPageId: string
override: BrowserViewportOverride | null
}) => Promise<boolean>
reportViewportScrollState?: (args: {
browserPageId: string
state: BrowserViewportScrollState
}) => void
setAnnotationViewportBridge: (args: BrowserSetAnnotationViewportBridgeArgs) => Promise<boolean>
/** Publishes a client-hosted page's url/title to its runtime over that runtime's host lease. */
publishClientPageMetadata: (args: {
environmentId: string
params: BrowserClientPageMetadataParams
}) => Promise<BrowserClientPageMetadataPublishOutcome>
onGuestLoadFailed: (
callback: (args: { browserPageId: string; loadError: BrowserLoadError }) => void
) => () => void
onCertificateFailureChanged: (
callback: (event: { browserPageId: string; failure: BrowserCertificateFailure | null }) => void
) => () => void
proceedCertificate: (args: {
browserPageId: string
challengeId: string
}) => Promise<BrowserCertificateProceedResult>
onPermissionDenied: (callback: (event: BrowserPermissionDeniedEvent) => void) => () => void
onPopup: (callback: (event: BrowserPopupEvent) => void) => () => void
onDownloadRequested: (callback: (event: BrowserDownloadRequestedEvent) => void) => () => void
onDownloadProgress: (callback: (event: BrowserDownloadProgressEvent) => void) => () => void
onDownloadFinished: (callback: (event: BrowserDownloadFinishedEvent) => void) => () => void
onContextMenuRequested: (
callback: (event: BrowserContextMenuRequestedEvent) => void
) => () => void
onContextMenuDismissed: (
callback: (event: BrowserContextMenuDismissedEvent) => void
) => () => void
onNavigationUpdate: (
callback: (event: { browserPageId: string; url: string; title: string }) => void
) => () => void
onActivateView: (
callback: (data: { worktreeId?: string; browserPageId?: string }) => void
) => () => void
onPaneFocus: (
callback: (data: { worktreeId: string | null; browserPageId: string }) => void
) => () => void
onOpenLinkInOrcaTab: (
callback: (event: { browserPageId: string; url: string; activate?: boolean }) => void
) => () => void
cancelDownload: (args: { downloadId: string }) => Promise<boolean>
setGrabMode: (args: BrowserSetGrabModeArgs) => Promise<BrowserSetGrabModeResult>
awaitGrabSelection: (args: BrowserAwaitGrabSelectionArgs) => Promise<BrowserGrabResult>
cancelGrab: (args: BrowserCancelGrabArgs) => Promise<boolean>
captureSelectionScreenshot: (
args: BrowserCaptureSelectionScreenshotArgs
) => Promise<BrowserCaptureSelectionScreenshotResult>
extractHoverPayload: (args: BrowserExtractHoverArgs) => Promise<BrowserExtractHoverResult>
onGrabModeToggle: (callback: (browserPageId: string) => void) => () => void
onGrabActionShortcut: (
callback: (args: { browserPageId: string; key: 'c' | 's' }) => void
) => () => void
sessionListProfiles: () => Promise<BrowserSessionProfile[]>
/** Resolves once the SSH workspace's partition is bound and proxy-verified; the webview must wait for it. */
prepareSshWorkspacePartition: (args: {
targetId: string
browserProfileId?: string
skipProbe?: boolean
}) => Promise<{ partition: string }>
sessionCreateProfile: (
args: {
scope: BrowserSessionProfileScope
label: string
} & BrowserSessionProfileCreateOptions
) => Promise<BrowserSessionProfile | null>
sessionDeleteProfile: (args: { profileId: string }) => Promise<boolean>
sessionImportCookies: (args: { profileId: string }) => Promise<BrowserCookieImportResult>
sessionResolvePartition: (args: { profileId: string | null }) => Promise<string | null>
sessionDetectBrowsers: () => Promise<DetectedBrowserInfo[]>
/** Null when the environment's pages are not client-hosted on this desktop. */
sessionDetectBrowsersForClientHost: (args: {
environmentId: string
}) => Promise<DetectedBrowserInfo[] | null>
sessionImportFromBrowser: (args: {
profileId: string
browserFamily: string
browserProfile?: string
}) => Promise<BrowserCookieImportResult>
/** Null when the environment's pages are not client-hosted on this desktop. */
sessionImportFromBrowserForClientHost: (args: {
environmentId: string
profileId: string
browserFamily: string
browserProfile?: string
}) => Promise<BrowserCookieImportResult | null>
/** Import-source badges for one environment's client-hosted jars, keyed by profile id. */
sessionClientRouteImportSources: (args: {
environmentId: string
}) => Promise<Record<string, BrowserSessionProfileSource>>
sessionClearDefaultCookies: () => Promise<boolean>
notifyActiveTabChanged: (args: { browserPageId: string }) => Promise<boolean>
}
export type DetectedBrowserProfileInfo = {
name: string
directory: string
}
export type DetectedBrowserInfo = {
family: BrowserSessionProfileSource['browserFamily']
label: string
profiles: DetectedBrowserProfileInfo[]
selectedProfile: string
}