Keep an unplaceable row owner out of desktop Open in launches (#26797)

Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
OrcaWin
2026-10-09 06:02:49 -07:00
committed by GitHub
co-authored by m4air
parent c9c6a00449
commit 3f37f7ea6d
9 changed files with 399 additions and 180 deletions
@@ -329,4 +329,27 @@ describe('WorktreeOpenInMenu', () => {
metadata: 'Local only'
})
})
it('marks an unresolved owner local-only and never launches the desktop copy', async () => {
const editor = { id: 'zed', label: 'Zed', target: 'external-editor', command: 'zed' } as const
const fileManager = { id: 'file-manager', label: 'Finder', target: 'file-manager' } as const
for (const entry of [editor, fileManager]) {
expect(getOpenInEntryAvailability(entry, mockState.settings, null, null, true)).toEqual({
disabled: true,
metadata: 'Local only'
})
}
for (const target of ['external-editor', 'file-manager'] as const) {
await openWorktreePath({
target,
worktreePath: '/srv/worktree',
connectionId: 'ssh-1',
runtimeEnvironmentId: null,
ownerUnresolved: true,
command: 'code'
})
}
expect(openInExternalEditorMock).not.toHaveBeenCalled()
expect(openInFileManagerMock).not.toHaveBeenCalled()
})
})
@@ -1,6 +1,5 @@
import React, { useCallback } from 'react'
import { ExternalLink, FolderOpen } from 'lucide-react'
import { toast } from 'sonner'
import {
DropdownMenuItem,
DropdownMenuSeparator,
@@ -14,7 +13,7 @@ import { getLocalFileManagerLabel } from '@/lib/local-file-manager-label'
import { OpenInApplicationIcon } from '@/lib/open-in-app-catalog'
import { getExternalEditorOpenCapability } from '@/lib/external-editor-open-capability'
import { NO_OPEN_IN_APPLICATIONS } from '@/lib/open-in-application-selection'
import type { ShellOpenExternalEditorResult } from '../../../../shared/shell-open-types'
import { showOpenFailureToast } from './worktree-open-failure-toast'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { OpenInApplication } from '../../../../shared/ui-chrome-types'
import { translate } from '@/i18n/i18n'
@@ -25,6 +24,7 @@ type WorktreeOpenInMenuItemsProps = {
worktreePath: string
connectionId?: string | null
runtimeEnvironmentId?: string | null
ownerUnresolved?: boolean
disabled?: boolean
labelPrefix?: string
}
@@ -55,10 +55,15 @@ export function getOpenInEntryAvailability(
entry: OpenInMenuEntry,
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
connectionId?: string | null,
runtimeEnvironmentId?: string | null
runtimeEnvironmentId?: string | null,
ownerUnresolved?: boolean
): { disabled: boolean; metadata?: string } {
if (entry.target === 'file-manager') {
const disabled = isLocalPathOpenBlocked(settings, { connectionId, runtimeEnvironmentId })
const disabled = isLocalPathOpenBlocked(settings, {
connectionId,
runtimeEnvironmentId,
ownerUnresolved
})
return disabled
? {
disabled: true,
@@ -69,7 +74,8 @@ export function getOpenInEntryAvailability(
const capability = getExternalEditorOpenCapability(settings, {
connectionId,
command: entry.command,
runtimeEnvironmentId
runtimeEnvironmentId,
ownerUnresolved
})
if (!capability.allowed) {
return {
@@ -85,152 +91,6 @@ export function getOpenInEntryAvailability(
: { disabled: false }
}
function showOpenFailureToast(
result: Exclude<ShellOpenExternalEditorResult, { ok: true }>,
remote: boolean
): void {
if (result.reason === 'remote-runtime-unsupported') {
toast.error(
translate(
'auto.components.sidebar.WorktreeOpenInMenu.remoteRuntimeUnsupported',
'Opening this path in a local app is not available.'
),
{
description: translate(
'auto.components.sidebar.WorktreeOpenInMenu.remoteRuntimeUnsupportedDetail',
'Switch to a local or SSH workspace, then try again.'
)
}
)
return
}
if (result.reason === 'ssh-target-not-found') {
toast.error(
translate(
'auto.components.sidebar.WorktreeOpenInMenu.sshTargetNotFound',
'SSH host is no longer available.'
),
{
description: translate(
'auto.components.sidebar.WorktreeOpenInMenu.sshTargetNotFoundDetail',
'Refresh workspaces or reconnect the host, then try again.'
)
}
)
return
}
if (result.reason === 'ssh-target-invalid') {
toast.error(
translate(
'auto.components.sidebar.WorktreeOpenInMenu.sshTargetInvalid',
'SSH host configuration is incomplete.'
),
{
description: translate(
'auto.components.sidebar.WorktreeOpenInMenu.sshTargetInvalidDetail',
'Edit or reconnect the SSH host, then try again.'
)
}
)
return
}
if (result.reason === 'ssh-alias-required') {
toast.error(
translate(
'auto.components.sidebar.WorktreeOpenInMenu.sshAliasRequired',
'VS Code needs an SSH config alias for this host.'
),
{
description: translate(
'auto.components.sidebar.WorktreeOpenInMenu.sshAliasRequiredDetail',
'Add a Host alias for {{host}}:{{port}} to your local SSH config, reconnect the workspace, then try again.',
{ host: result.host, port: result.port }
)
}
)
return
}
if (result.reason === 'remote-editor-unsupported') {
toast.error(
translate(
'auto.components.sidebar.WorktreeOpenInMenu.remoteEditorUnsupported',
'This app cannot open SSH workspaces.'
),
{
description: translate(
'auto.components.sidebar.WorktreeOpenInMenu.remoteEditorUnsupportedDetail',
'Choose VS Code or use the app locally.'
)
}
)
return
}
if (result.reason === 'not-absolute') {
toast.error(
remote
? translate(
'auto.components.sidebar.WorktreeOpenInMenu.remotePathInvalid',
'Path is not valid for the SSH host.'
)
: translate(
'auto.components.sidebar.WorktreeOpenInMenu.f387af445b',
'Workspace path is not a valid local path.'
),
remote
? {
description: translate(
'auto.components.sidebar.WorktreeOpenInMenu.remotePathInvalidDetail',
'Refresh the workspace before trying again.'
)
}
: undefined
)
return
}
if (result.reason === 'not-found') {
toast.error(
translate(
'auto.components.sidebar.WorktreeOpenInMenu.3921d3d9a5',
'Workspace folder was not found.'
),
{
description: translate(
'auto.components.sidebar.WorktreeOpenInMenu.0bed8727db',
'It may have been moved or deleted. Refresh workspaces or remove it from Orca.'
)
}
)
return
}
if (remote) {
toast.error(
translate(
'auto.components.sidebar.WorktreeOpenInMenu.remoteLaunchFailed',
'Could not open the path in VS Code.'
),
{
description: translate(
'auto.components.sidebar.WorktreeOpenInMenu.remoteLaunchFailedDetail',
'Check the VS Code command configured on this machine.'
)
}
)
return
}
toast.error(
translate(
'auto.components.sidebar.WorktreeOpenInMenu.9a5381eb09',
'Could not open workspace folder.'
),
{
description: translate(
'auto.components.sidebar.WorktreeOpenInMenu.bd0e8159f8',
'Check the editor command or file manager configuration on this machine.'
)
}
)
}
function stopMenuPropagation(event: React.SyntheticEvent): void {
event.stopPropagation()
}
@@ -250,6 +110,7 @@ export async function openWorktreePath(args: {
worktreePath: string
connectionId?: string | null
runtimeEnvironmentId?: string | null
ownerUnresolved?: boolean
command?: string
}): Promise<void> {
const settings = useAppStore.getState().settings
@@ -257,7 +118,8 @@ export async function openWorktreePath(args: {
if (
isLocalPathOpenBlocked(settings, {
connectionId: args.connectionId ?? null,
runtimeEnvironmentId: args.runtimeEnvironmentId
runtimeEnvironmentId: args.runtimeEnvironmentId,
ownerUnresolved: args.ownerUnresolved
})
) {
showLocalPathOpenBlockedToast()
@@ -267,7 +129,8 @@ export async function openWorktreePath(args: {
const capability = getExternalEditorOpenCapability(settings, {
connectionId: args.connectionId,
command: args.command,
runtimeEnvironmentId: args.runtimeEnvironmentId
runtimeEnvironmentId: args.runtimeEnvironmentId,
ownerUnresolved: args.ownerUnresolved
})
if (!capability.allowed) {
if (capability.reason === 'remote-runtime') {
@@ -295,16 +158,24 @@ export async function openWorktreePath(args: {
function useOpenInWorktreePath({
worktreePath,
connectionId,
runtimeEnvironmentId
runtimeEnvironmentId,
ownerUnresolved
}: WorktreeOpenInMenuItemsProps): (
target: 'file-manager' | 'external-editor',
command?: string
) => Promise<void> {
return useCallback(
async (target, command) => {
await openWorktreePath({ target, worktreePath, connectionId, runtimeEnvironmentId, command })
await openWorktreePath({
target,
worktreePath,
connectionId,
runtimeEnvironmentId,
ownerUnresolved,
command
})
},
[connectionId, runtimeEnvironmentId, worktreePath]
[connectionId, ownerUnresolved, runtimeEnvironmentId, worktreePath]
)
}
@@ -312,13 +183,15 @@ export function WorktreeOpenInMenuItems({
worktreePath,
connectionId,
runtimeEnvironmentId,
ownerUnresolved,
disabled,
labelPrefix = ''
}: WorktreeOpenInMenuItemsProps): React.JSX.Element {
const openInWorktreePath = useOpenInWorktreePath({
worktreePath,
connectionId,
runtimeEnvironmentId
runtimeEnvironmentId,
ownerUnresolved
})
const openInApplications = useAppStore(
(s) => s.settings?.openInApplications ?? NO_OPEN_IN_APPLICATIONS
@@ -334,7 +207,8 @@ export function WorktreeOpenInMenuItems({
entry,
settings,
connectionId,
runtimeEnvironmentId
runtimeEnvironmentId,
ownerUnresolved
)
return (
<DropdownMenuItem
@@ -372,6 +246,7 @@ export function WorktreeOpenInSubMenu({
worktreePath,
connectionId,
runtimeEnvironmentId,
ownerUnresolved,
disabled
}: WorktreeOpenInMenuItemsProps): React.JSX.Element {
return (
@@ -389,6 +264,7 @@ export function WorktreeOpenInSubMenu({
worktreePath={worktreePath}
connectionId={connectionId}
runtimeEnvironmentId={runtimeEnvironmentId}
ownerUnresolved={ownerUnresolved}
disabled={disabled}
/>
<DropdownMenuSeparator />
@@ -1,7 +1,7 @@
import type React from 'react'
import { useShallow } from 'zustand/react/shallow'
import { useAppStore } from '@/store'
import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner'
import { resolveWorktreeOperationRouteForHost } from '@/lib/worktree-operation-route'
import { resolveWorktreeRowOpenInRuntimeOwner } from '@/lib/worktree-row-open-in-owner'
import type { Worktree } from '../../../../shared/worktree/types'
import { WorktreeOpenInSubMenu } from './WorktreeOpenInMenu'
@@ -11,23 +11,18 @@ export function WorktreeRowOpenInSubMenu({
connectionId,
disabled
}: {
worktree: Pick<Worktree, 'id' | 'path' | 'hostId'>
worktree: Pick<Worktree, 'id' | 'path' | 'hostId' | 'runtimeOwnerEnvironmentId'>
connectionId: string | null
disabled?: boolean
}): React.JSX.Element {
// Why: resolved only while the menu is open, not per row on every store update. An inactive
// row can name its host before the catalog has it, so the explicit host wins.
const runtimeEnvironmentId = useAppStore((s) =>
worktree.hostId
? (resolveWorktreeOperationRouteForHost(s, worktree.id, worktree.hostId)
?.runtimeEnvironmentId ?? null)
: getRuntimeEnvironmentIdForWorktree(s, worktree.id)
)
// Why: resolved only while the menu is open, not per row on every store update.
const owner = useAppStore(useShallow((s) => resolveWorktreeRowOpenInRuntimeOwner(s, worktree)))
return (
<WorktreeOpenInSubMenu
worktreePath={worktree.path}
connectionId={connectionId}
runtimeEnvironmentId={runtimeEnvironmentId}
runtimeEnvironmentId={owner.runtimeEnvironmentId}
ownerUnresolved={owner.ownerUnresolved}
disabled={disabled}
/>
)
@@ -0,0 +1,150 @@
import { toast } from 'sonner'
import { translate } from '@/i18n/i18n'
import type { ShellOpenExternalEditorResult } from '../../../../shared/shell-open-types'
/** Actionable toast for a refused or failed Open in launch. */
export function showOpenFailureToast(
result: Exclude<ShellOpenExternalEditorResult, { ok: true }>,
remote: boolean
): void {
if (result.reason === 'remote-runtime-unsupported') {
toast.error(
translate(
'auto.components.sidebar.WorktreeOpenInMenu.remoteRuntimeUnsupported',
'Opening this path in a local app is not available.'
),
{
description: translate(
'auto.components.sidebar.WorktreeOpenInMenu.remoteRuntimeUnsupportedDetail',
'Switch to a local or SSH workspace, then try again.'
)
}
)
return
}
if (result.reason === 'ssh-target-not-found') {
toast.error(
translate(
'auto.components.sidebar.WorktreeOpenInMenu.sshTargetNotFound',
'SSH host is no longer available.'
),
{
description: translate(
'auto.components.sidebar.WorktreeOpenInMenu.sshTargetNotFoundDetail',
'Refresh workspaces or reconnect the host, then try again.'
)
}
)
return
}
if (result.reason === 'ssh-target-invalid') {
toast.error(
translate(
'auto.components.sidebar.WorktreeOpenInMenu.sshTargetInvalid',
'SSH host configuration is incomplete.'
),
{
description: translate(
'auto.components.sidebar.WorktreeOpenInMenu.sshTargetInvalidDetail',
'Edit or reconnect the SSH host, then try again.'
)
}
)
return
}
if (result.reason === 'ssh-alias-required') {
toast.error(
translate(
'auto.components.sidebar.WorktreeOpenInMenu.sshAliasRequired',
'VS Code needs an SSH config alias for this host.'
),
{
description: translate(
'auto.components.sidebar.WorktreeOpenInMenu.sshAliasRequiredDetail',
'Add a Host alias for {{host}}:{{port}} to your local SSH config, reconnect the workspace, then try again.',
{ host: result.host, port: result.port }
)
}
)
return
}
if (result.reason === 'remote-editor-unsupported') {
toast.error(
translate(
'auto.components.sidebar.WorktreeOpenInMenu.remoteEditorUnsupported',
'This app cannot open SSH workspaces.'
),
{
description: translate(
'auto.components.sidebar.WorktreeOpenInMenu.remoteEditorUnsupportedDetail',
'Choose VS Code or use the app locally.'
)
}
)
return
}
if (result.reason === 'not-absolute') {
toast.error(
remote
? translate(
'auto.components.sidebar.WorktreeOpenInMenu.remotePathInvalid',
'Path is not valid for the SSH host.'
)
: translate(
'auto.components.sidebar.WorktreeOpenInMenu.f387af445b',
'Workspace path is not a valid local path.'
),
remote
? {
description: translate(
'auto.components.sidebar.WorktreeOpenInMenu.remotePathInvalidDetail',
'Refresh the workspace before trying again.'
)
}
: undefined
)
return
}
if (result.reason === 'not-found') {
toast.error(
translate(
'auto.components.sidebar.WorktreeOpenInMenu.3921d3d9a5',
'Workspace folder was not found.'
),
{
description: translate(
'auto.components.sidebar.WorktreeOpenInMenu.0bed8727db',
'It may have been moved or deleted. Refresh workspaces or remove it from Orca.'
)
}
)
return
}
if (remote) {
toast.error(
translate(
'auto.components.sidebar.WorktreeOpenInMenu.remoteLaunchFailed',
'Could not open the path in VS Code.'
),
{
description: translate(
'auto.components.sidebar.WorktreeOpenInMenu.remoteLaunchFailedDetail',
'Check the VS Code command configured on this machine.'
)
}
)
return
}
toast.error(
translate(
'auto.components.sidebar.WorktreeOpenInMenu.9a5381eb09',
'Could not open workspace folder.'
),
{
description: translate(
'auto.components.sidebar.WorktreeOpenInMenu.bd0e8159f8',
'Check the editor command or file manager configuration on this machine.'
)
}
)
}
@@ -7,9 +7,19 @@ export type ExternalEditorOpenCapability =
export function getExternalEditorOpenCapability(
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
context: { connectionId?: string | null; command?: string; runtimeEnvironmentId?: string | null }
context: {
connectionId?: string | null
command?: string
runtimeEnvironmentId?: string | null
/** The path's owner could not be placed; a null runtime then proves nothing about locality. */
ownerUnresolved?: boolean
}
): ExternalEditorOpenCapability {
if (settings?.activeRuntimeEnvironmentId?.trim() || context.runtimeEnvironmentId?.trim()) {
if (
context.ownerUnresolved ||
settings?.activeRuntimeEnvironmentId?.trim() ||
context.runtimeEnvironmentId?.trim()
) {
return { allowed: false, reason: 'remote-runtime' }
}
if (!context.connectionId?.trim()) {
@@ -4,9 +4,15 @@ import { translate } from '@/i18n/i18n'
export function isLocalPathOpenBlocked(
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
context?: { connectionId?: string | null; runtimeEnvironmentId?: string | null }
context?: {
connectionId?: string | null
runtimeEnvironmentId?: string | null
/** The path's owner could not be placed; a null runtime then proves nothing about locality. */
ownerUnresolved?: boolean
}
): boolean {
return Boolean(
context?.ownerUnresolved ||
settings?.activeRuntimeEnvironmentId?.trim() ||
context?.connectionId?.trim() ||
context?.runtimeEnvironmentId?.trim()
@@ -134,6 +134,45 @@ export function resolveWorktreeOperationRouteForHost(
return resolution.kind === 'resolved' ? resolution.route : null
}
/**
* Strict variant of the host-qualified resolver for callers that treat a null runtime as local:
* rival HUBs projecting the same host report `ambiguous` instead of a transport-less route.
*/
export function resolveStrictWorktreeOperationRouteResultForHost(
state: WorktreeOperationRouteState,
worktreeId: string,
executionHostId: ExecutionHostId
): WorktreeOperationRouteResolution {
const host = parseExecutionHostId(executionHostId)
if (!host) {
return { kind: 'missing' }
}
if (host.kind === 'ssh') {
const hubOwners = collectHubOwnerEnvironmentIds(state, worktreeId, host.id)
if (hubOwners.hasAmbiguousOwner || hubOwners.environmentIds.size > 1) {
return { kind: 'ambiguous' }
}
}
return { kind: 'resolved', route: resolveSelectedHostRoute(state, worktreeId, host) }
}
function collectHubOwnerEnvironmentIds(
state: WorktreeOperationRouteState,
worktreeId: string,
executionHostId: ExecutionHostId
): { environmentIds: Set<string>; hasAmbiguousOwner: boolean } {
const environmentIds = new Set<string>()
let hasAmbiguousOwner = false
for (const owner of ownerRecordsOnHost(state, worktreeId, executionHostId)) {
const resolution = resolveExactWorktreeRoute(state, owner)
hasAmbiguousOwner ||= resolution.kind === 'ambiguous'
if (resolution.kind === 'resolved' && resolution.route.runtimeEnvironmentId) {
environmentIds.add(resolution.route.runtimeEnvironmentId)
}
}
return { environmentIds, hasAmbiguousOwner }
}
/**
* An authoritative host selection already names the target, so only the transport has to be
* recovered — and only for `ssh:`, which a paired HUB can proxy. Rival HUBs projecting the same
@@ -151,13 +190,7 @@ function resolveSelectedHostRoute(
if (selectedHost.kind !== 'ssh') {
return { executionHostId: selectedHost.id, runtimeEnvironmentId: null }
}
const environmentIds = new Set<string>()
for (const owner of ownerRecordsOnHost(state, worktreeId, selectedHost.id)) {
const resolution = resolveExactWorktreeRoute(state, owner)
if (resolution.kind === 'resolved' && resolution.route.runtimeEnvironmentId) {
environmentIds.add(resolution.route.runtimeEnvironmentId)
}
}
const { environmentIds } = collectHubOwnerEnvironmentIds(state, worktreeId, selectedHost.id)
const environmentId = environmentIds.values().next().value
return {
executionHostId: selectedHost.id,
@@ -0,0 +1,82 @@
import { describe, expect, it } from 'vitest'
import type { Worktree } from '../../../shared/worktree/types'
import { resolveWorktreeRowOpenInRuntimeOwner } from './worktree-row-open-in-owner'
import { getExternalEditorOpenCapability } from './external-editor-open-capability'
import { isLocalPathOpenBlocked } from './local-path-open-guard'
const WORKTREE_ID = 'repo-1::/srv/worktree'
function worktree(hostId: Worktree['hostId'], runtimeOwnerEnvironmentId?: string): Worktree {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resolver reads only id/repoId/hostId/runtimeOwnerEnvironmentId.
return {
id: WORKTREE_ID,
repoId: 'repo-1',
path: '/srv/worktree',
hostId,
runtimeOwnerEnvironmentId
} as Worktree
}
// Two paired servers provisioned from one profile publish the same id and SSH host.
const rivalHubState = {
settings: { activeRuntimeEnvironmentId: null },
worktreesByRepo: {
'repo-1': [
worktree('ssh:same-private-target', 'hub-a'),
worktree('ssh:same-private-target', 'hub-b')
]
}
}
describe('resolveWorktreeRowOpenInRuntimeOwner', () => {
it('keeps the clicked row stamped owner when rival HUBs share its SSH host', () => {
expect(
resolveWorktreeRowOpenInRuntimeOwner(
rivalHubState,
worktree('ssh:same-private-target', 'hub-b')
)
).toEqual({ runtimeEnvironmentId: 'hub-b', ownerUnresolved: false })
})
it('reports an unstamped row on a rival-HUB host as unresolved, never as desktop-owned', () => {
const owner = resolveWorktreeRowOpenInRuntimeOwner(
rivalHubState,
worktree('ssh:same-private-target')
)
expect(owner).toEqual({ runtimeEnvironmentId: null, ownerUnresolved: true })
const context = { connectionId: null, command: 'code', ...owner }
expect(getExternalEditorOpenCapability(rivalHubState.settings, context)).toEqual({
allowed: false,
reason: 'remote-runtime'
})
expect(isLocalPathOpenBlocked(rivalHubState.settings, context)).toBe(true)
})
it('recovers a single HUB owner for an unstamped row on its SSH host', () => {
expect(
resolveWorktreeRowOpenInRuntimeOwner(
{ worktreesByRepo: { 'repo-1': [worktree('ssh:hub-private-target', 'hub-a')] } },
worktree('ssh:hub-private-target')
)
).toEqual({ runtimeEnvironmentId: 'hub-a', ownerUnresolved: false })
})
it('keeps a direct SSH row resolved with no runtime', () => {
expect(
resolveWorktreeRowOpenInRuntimeOwner(
{
repos: [{ id: 'repo-1', connectionId: 'ssh-1', executionHostId: 'ssh:ssh-1' }],
worktreesByRepo: { 'repo-1': [worktree('ssh:ssh-1')] }
},
worktree('ssh:ssh-1')
)
).toEqual({ runtimeEnvironmentId: null, ownerUnresolved: false })
})
it('keeps a local row resolved as desktop-owned', () => {
expect(resolveWorktreeRowOpenInRuntimeOwner({}, worktree('local'))).toEqual({
runtimeEnvironmentId: null,
ownerUnresolved: false
})
})
})
@@ -0,0 +1,44 @@
import type { Worktree } from '../../../shared/worktree/types'
import { getRuntimeEnvironmentIdForWorktree } from './worktree-runtime-owner'
import type { WorktreeRuntimeOwnerState } from './worktree-runtime-owner'
import {
resolveStrictWorktreeOperationRouteResultForHost,
type WorktreeOperationRouteState
} from './worktree-operation-route'
import { routeForOwner } from './worktree-owner-route'
export type WorktreeRowOpenInOwner = {
runtimeEnvironmentId: string | null
/** The store cannot place the row; a null runtime then must not read as desktop ownership. */
ownerUnresolved: boolean
}
const UNRESOLVED: WorktreeRowOpenInOwner = { runtimeEnvironmentId: null, ownerUnresolved: true }
function resolved(runtimeEnvironmentId: string | null): WorktreeRowOpenInOwner {
return { runtimeEnvironmentId, ownerUnresolved: false }
}
/** Runtime that owns one clicked sidebar row, for the desktop Open in launch guards. */
export function resolveWorktreeRowOpenInRuntimeOwner(
state: WorktreeOperationRouteState & WorktreeRuntimeOwnerState,
worktree: Pick<Worktree, 'id' | 'hostId' | 'runtimeOwnerEnvironmentId'>
): WorktreeRowOpenInOwner {
// Why: the row's stamped owner is exact; rival HUBs can share its host id, so never re-derive it.
if (worktree.runtimeOwnerEnvironmentId?.trim()) {
const runtimeEnvironmentId = routeForOwner(worktree)?.runtimeEnvironmentId
return runtimeEnvironmentId ? resolved(runtimeEnvironmentId) : UNRESOLVED
}
if (!worktree.hostId) {
return resolved(getRuntimeEnvironmentIdForWorktree(state, worktree.id))
}
// Why: an inactive row can name its host before the catalog has it, so the explicit host wins.
const resolution = resolveStrictWorktreeOperationRouteResultForHost(
state,
worktree.id,
worktree.hostId
)
return resolution.kind === 'resolved'
? resolved(resolution.route.runtimeEnvironmentId)
: UNRESOLVED
}