Open linked reviews in Orca browser (#17360)

* feat(sidebar): open linked reviews in Orca browser

* test(e2e): match paired window reveal assertion

* feat(sidebar): focus linked browser tabs

* ci: retry checks after cancelled rerun
This commit is contained in:
Jinwoo Hong
2026-08-30 04:58:56 -04:00
committed by GitHub
parent 99b59064fd
commit d64097d109
12 changed files with 307 additions and 11 deletions
@@ -7,7 +7,7 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { CircleDot, Copy, Ellipsis, ExternalLink, MonitorUp, Pencil } from 'lucide-react'
import { CircleDot, Copy, Ellipsis, ExternalLink, Globe, MonitorUp, Pencil } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import {
WorktreeCardDetailSection,
@@ -22,6 +22,7 @@ type WorktreeCardIssueDetailSectionProps = {
issueMenuOpen: boolean
onIssueMenuOpenChange: (open: boolean) => void
onCopyIssueLink?: () => void
onOpenIssueInBrowser?: (url: string) => void
onEditIssue?: (event: React.MouseEvent) => void
onOpenGitHubIssueInOrca?: (event: React.MouseEvent) => void
}
@@ -31,6 +32,7 @@ export function WorktreeCardIssueDetailSection({
issueMenuOpen,
onIssueMenuOpenChange,
onCopyIssueLink,
onOpenIssueInBrowser,
onEditIssue,
onOpenGitHubIssueInOrca
}: WorktreeCardIssueDetailSectionProps): React.JSX.Element | null {
@@ -71,7 +73,7 @@ export function WorktreeCardIssueDetailSection({
)}
actions={
<>
{issue.url && onCopyIssueLink && (
{issue.url && (onCopyIssueLink || onOpenIssueInBrowser) && (
<DropdownMenu modal={false} open={issueMenuOpen} onOpenChange={onIssueMenuOpenChange}>
{issueMenuOpen ? (
moreActionsTrigger
@@ -84,10 +86,25 @@ export function WorktreeCardIssueDetailSection({
</Tooltip>
)}
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem onSelect={onCopyIssueLink}>
<Copy className="size-3.5" />
{translate('auto.components.sidebar.WorktreeCardMeta.copyLink', 'Copy link')}
</DropdownMenuItem>
{onOpenIssueInBrowser && (
<DropdownMenuItem
onSelect={() => {
onOpenIssueInBrowser(issue.url!)
}}
>
<Globe className="size-3.5" />
{translate(
'auto.components.sidebar.WorktreeCardMeta.openInOrcaBrowser',
'Open in Orca browser'
)}
</DropdownMenuItem>
)}
{onCopyIssueLink && (
<DropdownMenuItem onSelect={onCopyIssueLink}>
<Copy className="size-3.5" />
{translate('auto.components.sidebar.WorktreeCardMeta.copyLink', 'Copy link')}
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
@@ -116,7 +116,10 @@ describe('WorktreeCardDetailsHover interactions', () => {
writeClipboardText.mockResolvedValue(undefined)
})
function renderHover(onUnlinkReview = vi.fn()): ReturnType<typeof vi.fn> {
function renderHover(
onUnlinkReview = vi.fn(),
onOpenReviewInBrowser?: () => void
): ReturnType<typeof vi.fn> {
container = document.createElement('div')
root = createRoot(container)
act(() => {
@@ -130,6 +133,7 @@ describe('WorktreeCardDetailsHover interactions', () => {
onEditComment={vi.fn()}
onOpenReviewInOrca={vi.fn()}
onUnlinkReview={onUnlinkReview}
onOpenReviewInBrowser={onOpenReviewInBrowser}
>
<span>Linked PR</span>
</WorktreeCardDetailsHover>
@@ -288,6 +292,49 @@ describe('WorktreeCardDetailsHover interactions', () => {
).toBe('false')
})
it('opens the review URL in Orca browser and leaves existing actions independent', () => {
const onOpenReviewInBrowser = vi.fn()
const onUnlinkReview = renderHover(vi.fn(), onOpenReviewInBrowser)
act(() => {
interactionMocks.onHoverOpenChange?.(true)
interactionMocks.onReviewMenuOpenChange?.(true)
})
const browserButton = Array.from(container.querySelectorAll('button')).find((button) =>
button.textContent?.includes('Open in Orca browser')
)
act(() => {
browserButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
})
expect(onOpenReviewInBrowser).toHaveBeenCalledWith('https://github.com/acme/orca/pull/456')
expect(onUnlinkReview).not.toHaveBeenCalled()
expect(container.querySelector('[data-hover-open]')?.getAttribute('data-hover-open')).toBe(
'false'
)
})
it('preserves repeated-click behavior by forwarding each browser action', () => {
const onOpenReviewInBrowser = vi.fn()
renderHover(vi.fn(), onOpenReviewInBrowser)
act(() => {
interactionMocks.onReviewMenuOpenChange?.(true)
})
const browserButton = Array.from(container.querySelectorAll('button')).find((button) =>
button.textContent?.includes('Open in Orca browser')
)
act(() => {
browserButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
browserButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
})
expect(onOpenReviewInBrowser).toHaveBeenCalledTimes(2)
})
it('reports clipboard failures without unlinking the review', async () => {
writeClipboardText.mockRejectedValueOnce(new Error('clipboard unavailable'))
const onUnlinkReview = renderHover()
@@ -310,4 +357,43 @@ describe('WorktreeCardDetailsHover interactions', () => {
expect(onUnlinkReview).not.toHaveBeenCalled()
expect(toastMocks.error).toHaveBeenCalledWith('Failed to copy link')
})
it('passes a linked issue URL to the embedded-browser action', () => {
const onOpenIssueInBrowser = vi.fn()
container = document.createElement('div')
root = createRoot(container)
act(() => {
root.render(
<WorktreeCardDetailsHover
issue={{
number: 5518,
title: 'Agent monitor issue',
state: 'open',
url: 'https://github.com/acme/orca/issues/5518',
labels: []
}}
linearIssue={null}
review={null}
comment={null}
onOpenIssueInBrowser={onOpenIssueInBrowser}
>
<span>Linked issue</span>
</WorktreeCardDetailsHover>
)
})
act(() => {
interactionMocks.onHoverOpenChange?.(true)
interactionMocks.onReviewMenuOpenChange?.(true)
})
const browserButton = Array.from(container.querySelectorAll('button')).find((button) =>
button.textContent?.includes('Open in Orca browser')
)
act(() => {
browserButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
})
expect(onOpenIssueInBrowser).toHaveBeenCalledWith('https://github.com/acme/orca/issues/5518')
})
})
@@ -130,6 +130,7 @@ describe('WorktreeCardDetailsHover', () => {
onEditIssue={vi.fn()}
onEditComment={vi.fn()}
onOpenReviewInOrca={vi.fn()}
onOpenReviewInBrowser={vi.fn()}
onUnlinkReview={vi.fn()}
>
<span>Linked PR</span>
@@ -143,10 +144,12 @@ describe('WorktreeCardDetailsHover', () => {
expect(moreActionsIndex).toBeGreaterThan(-1)
expect(markup).toContain('More PR actions')
expect(markup).toContain('Copy link')
expect(markup).toContain('Open in Orca browser')
expect(markup).toContain('Unlink PR')
expect(moreActionsIndex).toBeLessThan(openInOrcaIndex)
expect(openInOrcaIndex).toBeLessThan(viewOnGitHubIndex)
expect(markup).not.toContain('aria-label="Unlink PR"')
expect(markup.indexOf('Open in Orca browser')).toBeLessThan(markup.indexOf('Copy link'))
expect(markup.indexOf('Copy link')).toBeLessThan(markup.indexOf('Unlink PR'))
})
@@ -166,6 +169,7 @@ describe('WorktreeCardDetailsHover', () => {
onEditIssue={vi.fn()}
onEditComment={vi.fn()}
onOpenGitHubIssueInOrca={vi.fn()}
onOpenIssueInBrowser={vi.fn()}
>
<span>Linked issue</span>
</WorktreeCardDetailsHover>
@@ -179,6 +183,8 @@ describe('WorktreeCardDetailsHover', () => {
expect(moreActionsIndex).toBeGreaterThan(-1)
expect(copyLinkIndex).toBeGreaterThan(-1)
expect(markup).toContain('Open in Orca browser')
expect(markup.indexOf('Open in Orca browser')).toBeLessThan(copyLinkIndex)
expect(editIssueIndex).toBeGreaterThan(-1)
expect(moreActionsIndex).toBeLessThan(editIssueIndex)
expect(copyLinkIndex).toBeLessThan(editIssueIndex)
@@ -203,6 +209,7 @@ describe('WorktreeCardDetailsHover', () => {
onEditIssue={vi.fn()}
onEditComment={vi.fn()}
onUnlinkReview={vi.fn()}
onOpenReviewInBrowser={vi.fn()}
>
<span>Linked MR</span>
</WorktreeCardDetailsHover>
@@ -211,6 +218,53 @@ describe('WorktreeCardDetailsHover', () => {
expect(markup).toContain('aria-label="More MR actions"')
expect(markup).toContain('Unlink MR')
expect(markup).toContain('View on GitLab')
expect(markup).toContain('Open in Orca browser')
})
it('hides the embedded-browser action when a linked review has no URL', () => {
const markup = renderToStaticMarkup(
<WorktreeCardDetailsHover
issue={null}
linearIssue={null}
review={{
provider: 'github',
number: 456,
title: 'Loading PR...',
state: 'open'
}}
comment={null}
onOpenReviewInBrowser={vi.fn()}
>
<span>Linked PR</span>
</WorktreeCardDetailsHover>
)
expect(markup).not.toContain('Open in Orca browser')
})
it('keeps the embedded-browser action provider-neutral for unsupported review URLs', () => {
const markup = renderToStaticMarkup(
<WorktreeCardDetailsHover
issue={null}
linearIssue={null}
review={{
provider: 'unsupported',
number: 12,
title: 'Review from an unsupported provider',
state: 'open',
url: 'https://code.example.test/reviews/12',
status: 'neutral',
updatedAt: '2026-05-17T00:00:00.000Z',
mergeable: 'UNKNOWN'
}}
comment={null}
onOpenReviewInBrowser={vi.fn()}
>
<span>Linked review</span>
</WorktreeCardDetailsHover>
)
expect(markup).toContain('Open in Orca browser')
})
it('displays Linear issue details with link', () => {
@@ -70,8 +70,10 @@ export function WorktreeCardDetailsHover({
onEditIssue,
onEditComment,
onOpenGitHubIssueInOrca,
onOpenIssueInBrowser,
onOpenLinearIssueInOrca,
onOpenReviewInOrca,
onOpenReviewInBrowser,
onUnlinkReview,
onOpenAutomation,
onOpenAutomationRun,
@@ -212,6 +214,14 @@ export function WorktreeCardDetailsHover({
onOpenGitHubIssueInOrca={
onOpenGitHubIssueInOrca ? dismissAndRun(onOpenGitHubIssueInOrca) : undefined
}
onOpenIssueInBrowser={
onOpenIssueInBrowser && issue?.url
? (url: string) => {
closeHover()
onOpenIssueInBrowser(url)
}
: undefined
}
/>
{linearIssue && (
@@ -305,6 +315,9 @@ export function WorktreeCardDetailsHover({
reviewMenuOpen={reviewMenuOpen}
onReviewMenuOpenChange={handleReviewMenuOpenChange}
onOpenReviewInOrca={onOpenReviewInOrca}
onOpenReviewInBrowser={
onOpenReviewInBrowser && review?.url ? onOpenReviewInBrowser : undefined
}
onCopyReviewLink={review?.url ? handleCopyReviewLink : undefined}
onUnlinkReview={onUnlinkReview}
closeHover={closeHover}
@@ -7,7 +7,7 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { Copy, Ellipsis, ExternalLink, MonitorUp, Unlink } from 'lucide-react'
import { Copy, Ellipsis, ExternalLink, Globe, MonitorUp, Unlink } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import {
WorktreeCardDetailSection,
@@ -24,6 +24,7 @@ type WorktreeCardReviewDetailSectionProps = {
onReviewMenuOpenChange: (open: boolean) => void
onOpenReviewInOrca?: (event: React.MouseEvent) => void
onCopyReviewLink?: () => void
onOpenReviewInBrowser?: (url: string) => void
onUnlinkReview?: () => void
closeHover: () => void
}
@@ -34,6 +35,7 @@ export function WorktreeCardReviewDetailSection({
onReviewMenuOpenChange,
onOpenReviewInOrca,
onCopyReviewLink,
onOpenReviewInBrowser,
onUnlinkReview,
closeHover
}: WorktreeCardReviewDetailSectionProps): React.JSX.Element | null {
@@ -78,7 +80,7 @@ export function WorktreeCardReviewDetailSection({
)}
actions={
<>
{(onCopyReviewLink || onUnlinkReview) && (
{(onCopyReviewLink || onOpenReviewInBrowser || onUnlinkReview) && (
<DropdownMenu
modal={false}
open={reviewMenuOpen}
@@ -95,6 +97,20 @@ export function WorktreeCardReviewDetailSection({
</Tooltip>
)}
<DropdownMenuContent align="end" className="w-40">
{onOpenReviewInBrowser && (
<DropdownMenuItem
onSelect={() => {
closeHover()
onOpenReviewInBrowser(review.url!)
}}
>
<Globe className="size-3.5" />
{translate(
'auto.components.sidebar.WorktreeCardMeta.openInOrcaBrowser',
'Open in Orca browser'
)}
</DropdownMenuItem>
)}
{onCopyReviewLink && (
<DropdownMenuItem
onSelect={() => {
@@ -1,6 +1,9 @@
import React, { useCallback } from 'react'
import { toast } from 'sonner'
import type { GitHubWorkItem } from '../../../../shared/github/work-item-types'
import { translate } from '@/i18n/i18n'
import { openWorkspaceBrowserTab } from '@/lib/workspace-browser-tab-open'
import { hasWorktreeCardDetails } from './WorktreeCardMeta'
import { usePromptCacheCountdownStartedAt } from './CacheTimer'
import { useWorktreeAgentRows } from './useWorktreeAgentRows'
@@ -141,6 +144,34 @@ export function useWorktreeCardSecondaryDetails({
},
[hoverReview, openTaskPage, repo]
)
const openLinkedUrlInBrowser = useCallback(
(url: string): void => {
void openWorkspaceBrowserTab({
workspaceId: worktree.id,
url,
intent: { kind: 'url' }
}).catch((error: unknown) => {
toast.error(
error instanceof Error
? error.message
: translate('auto.lib.workspace.browser.tab.open.urlFailed', 'Unable to open URL.')
)
})
},
[worktree.id]
)
const handleOpenIssueInBrowser = useCallback(
(url: string): void => {
openLinkedUrlInBrowser(url)
},
[openLinkedUrlInBrowser]
)
const handleOpenReviewInBrowser = useCallback(
(url: string): void => {
openLinkedUrlInBrowser(url)
},
[openLinkedUrlInBrowser]
)
const hoverReviewProvider = hoverReview?.provider
const hasExplicitLinkedReview =
(hoverReviewProvider === 'github' && worktree.linkedPR !== null) ||
@@ -214,7 +245,9 @@ export function useWorktreeCardSecondaryDetails({
showInlineAgentList,
compactInlineAgentRows,
handleOpenGitHubIssueInOrca,
handleOpenIssueInBrowser,
handleOpenReviewInOrca,
handleOpenReviewInBrowser,
hasExplicitLinkedReview,
handleUnlinkReview,
handleOpenLinearIssueInOrca,
@@ -57,8 +57,10 @@ export type WorktreeCardDetailsHoverProps = WorktreeCardMetaBadgesProps & {
onEditIssue?: (event: React.MouseEvent) => void
onEditComment?: (event: React.MouseEvent) => void
onOpenGitHubIssueInOrca?: (event: React.MouseEvent) => void
onOpenIssueInBrowser?: (url: string) => void
onOpenLinearIssueInOrca?: (event: React.MouseEvent) => void
onOpenReviewInOrca?: (event: React.MouseEvent) => void
onOpenReviewInBrowser?: (url: string) => void
onUnlinkReview?: () => void
onOpenAutomation?: (event: React.MouseEvent) => void
onOpenAutomationRun?: (event: React.MouseEvent) => void
@@ -44,9 +44,11 @@ export function WorktreeCardParentContent({
handleEditIssue,
handleEditComment,
handleOpenGitHubIssueInOrca,
handleOpenIssueInBrowser,
linearIssue,
handleOpenLinearIssueInOrca,
handleOpenReviewInOrca,
handleOpenReviewInBrowser,
handleOpenAutomation,
handleOpenAutomationRun,
hasExplicitLinkedReview,
@@ -98,10 +100,14 @@ export function WorktreeCardParentContent({
? handleOpenGitHubIssueInOrca
: undefined
}
onOpenIssueInBrowser={
hoverIssue && 'url' in hoverIssue && hoverIssue.url ? handleOpenIssueInBrowser : undefined
}
onOpenLinearIssueInOrca={linearIssue?.url ? handleOpenLinearIssueInOrca : undefined}
onOpenReviewInOrca={
hoverReview?.url && hoverReview.provider === 'github' ? handleOpenReviewInOrca : undefined
}
onOpenReviewInBrowser={hoverReview?.url ? handleOpenReviewInBrowser : undefined}
onOpenAutomation={affiliateListMode ? undefined : handleOpenAutomation}
onOpenAutomationRun={affiliateListMode ? undefined : handleOpenAutomationRun}
onUnlinkReview={
@@ -57,8 +57,10 @@ export function buildWorktreeCardPresentation(card: WorktreeCardController) {
handleEditIssue,
handleEditComment,
handleOpenGitHubIssueInOrca,
handleOpenIssueInBrowser,
handleOpenLinearIssueInOrca,
handleOpenReviewInOrca,
handleOpenReviewInBrowser,
handleOpenAutomation,
handleOpenAutomationRun,
hasExplicitLinkedReview,
@@ -167,12 +169,18 @@ export function buildWorktreeCardPresentation(card: WorktreeCardController) {
? handleOpenGitHubIssueInOrca
: undefined
}
onOpenIssueInBrowser={
metaIssue && 'url' in metaIssue && metaIssue.url
? handleOpenIssueInBrowser
: undefined
}
onOpenLinearIssueInOrca={linearIssue?.url ? handleOpenLinearIssueInOrca : undefined}
onOpenReviewInOrca={
metaReview?.url && metaReview.provider === 'github'
? handleOpenReviewInOrca
: undefined
}
onOpenReviewInBrowser={metaReview?.url ? handleOpenReviewInBrowser : undefined}
onOpenAutomation={affiliateListMode ? undefined : handleOpenAutomation}
onOpenAutomationRun={affiliateListMode ? undefined : handleOpenAutomationRun}
// Why: compact mode hides the metadata badge row, so title hover carries the explicit-link affordance.
@@ -231,10 +239,14 @@ export function buildWorktreeCardPresentation(card: WorktreeCardController) {
onOpenGitHubIssueInOrca={
metaIssue && 'url' in metaIssue && metaIssue.url ? handleOpenGitHubIssueInOrca : undefined
}
onOpenIssueInBrowser={
metaIssue && 'url' in metaIssue && metaIssue.url ? handleOpenIssueInBrowser : undefined
}
onOpenLinearIssueInOrca={linearIssue?.url ? handleOpenLinearIssueInOrca : undefined}
onOpenReviewInOrca={
metaReview?.url && metaReview.provider === 'github' ? handleOpenReviewInOrca : undefined
}
onOpenReviewInBrowser={metaReview?.url ? handleOpenReviewInBrowser : undefined}
onOpenAutomation={affiliateListMode ? undefined : handleOpenAutomation}
onOpenAutomationRun={affiliateListMode ? undefined : handleOpenAutomationRun}
// Why: branch lookup can surface a review without persisted metadata; only unlink when explicitly linked.
+1
View File
@@ -5415,6 +5415,7 @@
"eace1d2cf6": "neutral",
"dbe2d18972": "More {{value0}} actions",
"ae76907ca6": "Unlink {{value0}}",
"openInOrcaBrowser": "Open in Orca browser",
"ad25c3ff05": "View on {{value0}}",
"2c67730e07": "Open in Orca",
"e42941631a": "View on Linear",
@@ -104,6 +104,31 @@ describe('openWorkspaceBrowserTab', () => {
expect(mocks.createRemote).not.toHaveBeenCalled()
})
it('creates a client tab without activating it when requested', async () => {
const createBrowserTab = vi.fn()
const sshHost = toSshExecutionHostId('ssh-target')
mocks.state = {
...ownerState(sshHost),
createBrowserTab,
defaultBrowserSessionProfileId: 'focused-profile',
defaultBrowserSessionProfileIdByHostId: { [sshHost]: 'ssh-profile' }
}
await openWorkspaceBrowserTab({
workspaceId: WORKSPACE_ID,
url: 'https://github.com/acme/orca/pull/456',
intent: { kind: 'url' },
focusOnCreate: false,
selectWorktree: false
})
expect(createBrowserTab).toHaveBeenCalledWith(
WORKSPACE_ID,
'https://github.com/acme/orca/pull/456',
expect.objectContaining({ activate: false })
)
})
it('fails closed when the asserted SSH browser route is opted out or belongs to another host', async () => {
const sshHost = toSshExecutionHostId('ssh-target')
mocks.state = {
@@ -164,6 +189,32 @@ describe('openWorkspaceBrowserTab', () => {
expect(createBrowserTab).not.toHaveBeenCalled()
})
it('stages a runtime tab without selecting the worktree or focusing the browser', async () => {
mocks.state = {
...ownerState(toRuntimeExecutionHostId('hub-a')),
...browserCapableRuntime('hub-a'),
createBrowserTab: vi.fn(),
defaultBrowserSessionProfileId: 'client-profile',
defaultBrowserSessionProfileIdByHostId: {}
}
await openWorkspaceBrowserTab({
workspaceId: WORKSPACE_ID,
url: 'https://gitlab.com/acme/orca/-/merge_requests/77',
intent: { kind: 'url' },
focusOnCreate: false,
selectWorktree: false
})
expect(mocks.createRemote).toHaveBeenCalledWith(
expect.objectContaining({
focusOnCreate: false,
selectWorktree: false,
url: 'https://gitlab.com/acme/orca/-/merge_requests/77'
})
)
})
it('waits for host registration before reconciling an asserted runtime link', async () => {
const createBrowserTab = vi.fn()
mocks.state = {
@@ -25,6 +25,10 @@ export type OpenWorkspaceBrowserTabRequest = {
targetGroupId?: string
url: string
intent: WorkspaceBrowserTabIntent
/** Keep the caller's current terminal/task surface selected while creating the tab. */
focusOnCreate?: boolean
/** Keep the caller's current workspace selected while creating the tab. */
selectWorktree?: boolean
expectedRuntimeEnvironmentId?: string
expectedSshConnectionId?: string
}
@@ -180,7 +184,7 @@ function createClientBrowserTab(
): void {
try {
state.createBrowserTab(request.workspaceId, request.url, {
activate: true,
activate: request.focusOnCreate !== false,
browserRuntimeEnvironmentId: null,
focusAddressBar: false,
sessionProfileId:
@@ -284,7 +288,8 @@ export async function openWorkspaceBrowserTab(
...(expectedEnvironmentId !== null ? { waitForRegistration: true } : {}),
// Why: the tab is opened from this workspace's tab bar, so surface that
// workspace — otherwise a background worktree looks like nothing happened.
selectWorktree: true,
...(request.focusOnCreate !== undefined ? { focusOnCreate: request.focusOnCreate } : {}),
selectWorktree: request.selectWorktree !== false,
stagedTitle: presentation.title,
stagedFocusAddressBar: false,
failureLogMode: 'operation-only'