refactor(renderer): one twin check, one worktree-copy scan, drop stale resolver mocks (#30579)

- isTwinWorkspace replaces four inline resolveOwner(...).kind === 'ambiguous' checks
- someWorktreeCopy shared by isUnprovenServerSshRoute and hasHostEvidence
- file-explorer owner: drop the unreachable second unresolved guard and the stale comment
- native chat owner: drop a null ternary the helper already handles
- tests: delete stale getRuntimeEnvironmentIdForWorktree mocks (29 suites), one shared
  local+ssh twin fixture, file:// open-target tests next to their module, reset mocks.twin
This commit is contained in:
Neil
2026-10-19 00:10:41 -07:00
committed by GitHub
parent 21af18af35
commit d03d731ea6
42 changed files with 190 additions and 240 deletions
@@ -40,8 +40,6 @@ vi.mock('@/store', async () => {
})
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: (state: { testRuntimeOwner?: string | null }) =>
state.testRuntimeOwner ?? null,
getExecutionHostIdForWorktree: (state: { testRuntimeOwner?: string | null }) =>
state.testRuntimeOwner ? `runtime:${state.testRuntimeOwner}` : 'local'
}))
@@ -35,7 +35,6 @@ vi.mock('@/store', async () => {
}
})
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: () => null,
runtimeEnvironmentIdWithLegacyFallbackUnlessTwin: () => null,
getExecutionHostIdForWorktree: () => mocks.executionHostId
}))
@@ -63,26 +63,14 @@ vi.mock('@/store', () => ({
},
getKnownWorktreeById: () =>
mocks.worktreePath === null ? undefined : { id: 'wt-1', path: mocks.worktreePath },
...(mocks.twin
? {
repos: [
{ id: 'repo', executionHostId: 'local' },
{ id: 'repo', connectionId: 'box', executionHostId: 'ssh:box' }
],
worktreesByRepo: {
repo: [
{ id: 'repo::/srv/repo', repoId: 'repo', hostId: 'local' },
{ id: 'repo::/srv/repo', repoId: 'repo', hostId: 'ssh:box' }
]
}
}
: {}),
...(mocks.twin ? localAndSshTwinRows('repo', 'repo::/srv/repo') : {}),
openFile: mocks.openFile
})
}
}))
import { openDocPreviewExternally, openDocPreviewSource } from './doc-preview-document-actions'
import { localAndSshTwinRows } from '@/lib/workspace-twin-rows.test-fixture'
const REMOTE_DOCUMENT = {
filePath: '/root/demo/report/index.html',
@@ -3,7 +3,7 @@ import { toast } from 'sonner'
import { translate } from '@/i18n/i18n'
import { getConnectionIdForFileFromState } from '@/lib/connection-owner-resolution'
import { runtimeEnvironmentIdWithLegacyFallbackUnlessTwin } from '@/lib/worktree-runtime-owner'
import { resolveOwner } from '@/lib/resolve-owner'
import { isTwinWorkspace } from '@/lib/resolve-owner'
import { detectLanguage } from '@/lib/language-detect'
import {
buildWorkspaceFileContextForFile,
@@ -81,7 +81,7 @@ export function openDocPreviewExternally(document: DocPreviewDocument): void {
runtimeEnvironmentId === null &&
worktreeRoot !== null &&
// Why: a twin reads as "no server", which is not proof this machine holds the file.
resolveOwner(state, { workspaceId: document.worktreeId }).kind !== 'ambiguous'
!isTwinWorkspace(state, document.worktreeId)
if (ownedByThisMachine && canClientOsOpenWorkspaceFile(fileContext)) {
void window.api.shell.openFilePath(document.filePath, LOCAL_EXECUTION_HOST_ID)
return
@@ -46,8 +46,6 @@ vi.mock('@/store', async () => {
})
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: (state: { testRuntimeOwner?: string | null }) =>
state.testRuntimeOwner ?? null,
getExecutionHostIdForWorktree: (state: { testRuntimeOwner?: string | null }) =>
state.testRuntimeOwner ? `runtime:${state.testRuntimeOwner}` : 'local'
}))
@@ -26,14 +26,18 @@ export function selectNativeChatRuntimeEnvironmentId(
state: NativeChatRuntimeOwnerState,
terminalTabId: string
): string | null {
const worktreeId = findTerminalTabWorktreeId(state.tabsByWorktree, terminalTabId)
return worktreeId ? runtimeEnvironmentIdWithLegacyFallbackUnlessTwin(state, worktreeId) : null
return runtimeEnvironmentIdWithLegacyFallbackUnlessTwin(
state,
findTerminalTabWorktreeId(state.tabsByWorktree, terminalTabId)
)
}
export function createNativeChatRuntimeSelector(terminalTabId: string) {
const selectOwner = createNativeChatTabOwnerSelector(terminalTabId)
return (state: NativeChatRuntimeOwnerState): string | null => {
const worktreeId = selectOwner({ tabsByWorktree: state.tabsByWorktree })
return worktreeId ? runtimeEnvironmentIdWithLegacyFallbackUnlessTwin(state, worktreeId) : null
return runtimeEnvironmentIdWithLegacyFallbackUnlessTwin(
state,
selectOwner({ tabsByWorktree: state.tabsByWorktree })
)
}
}
@@ -38,8 +38,6 @@ vi.mock('@/store', async () => {
})
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: (state: { testRuntimeOwner?: string | null }) =>
state.testRuntimeOwner ?? null,
getExecutionHostIdForWorktree: (state: { testRuntimeOwner?: string | null }) =>
state.testRuntimeOwner ? `runtime:${state.testRuntimeOwner}` : 'local'
}))
@@ -52,7 +52,6 @@ vi.mock('@/store', async () => {
vi.mock('sonner', () => ({ toast: { error: vi.fn(), message: vi.fn() } }))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: () => null,
getExecutionHostIdForWorktree: () => 'local'
}))
@@ -35,8 +35,6 @@ vi.mock('@/store', async () => {
})
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: (state: { testRuntimeOwner?: string | null }) =>
state.testRuntimeOwner ?? null,
getExecutionHostIdForWorktree: (state: { testRuntimeOwner?: string | null }) =>
state.testRuntimeOwner ? `runtime:${state.testRuntimeOwner}` : 'local'
}))
@@ -53,7 +53,6 @@ vi.mock('@/store', async () => {
vi.mock('sonner', () => ({ toast: { error: vi.fn(), message: vi.fn() } }))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: () => null,
getExecutionHostIdForWorktree: () => 'local'
}))
@@ -23,7 +23,6 @@ import type { TerminalTab } from '../../../../shared/terminal-tab-types'
import { isExplicitAgentStatusFresh } from '@/lib/pane-agent-evidence'
import type { AppState } from '@/store/types'
import type * as RuntimeRpcClientModule from '@/runtime/runtime-rpc-client'
import type * as WorktreeRuntimeOwnerModule from '@/lib/worktree-runtime-owner'
type TestStore = {
getState: () => AppState
@@ -52,12 +51,6 @@ vi.mock('@/store', async () => {
return { useAppStore }
})
// Partial: the status projection also resolves each chat's owner from the worktree.
vi.mock('@/lib/worktree-runtime-owner', async (importOriginal) => ({
...(await importOriginal<typeof WorktreeRuntimeOwnerModule>()),
getRuntimeEnvironmentIdForWorktree: () => null
}))
vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) => ({
...(await importOriginal<typeof RuntimeRpcClientModule>()),
runtimeEnvironmentSupportsCapability: vi.fn(async () => true)
@@ -80,16 +80,7 @@ export function getFileExplorerOperationOwnerFromState(
if (explicitOwner?.kind === 'ambiguous') {
return { kind: 'unresolved' }
}
const connectionId = getConnectionIdFromState(state, worktreeId ?? null)
const explicitRuntimeEnvironmentId = explicitOwner?.environmentId ?? null
// Why: global runtime focus is not ownership evidence while SSH/local
// metadata is unresolved; destructive actions must wait for explicit provenance.
if (connectionId === undefined && explicitRuntimeEnvironmentId === null) {
return { kind: 'unresolved' }
}
// Why: inferred SSH ownership outranks a projected runtime owner, but an explicit
// workspace runtime still owns its files.
const runtimeEnvironmentId = explicitRuntimeEnvironmentId?.trim()
const runtimeEnvironmentId = explicitOwner?.environmentId?.trim()
if (runtimeEnvironmentId) {
return {
kind: 'runtime',
@@ -97,6 +88,9 @@ export function getFileExplorerOperationOwnerFromState(
executionHostId: toRuntimeExecutionHostId(runtimeEnvironmentId)
}
}
const connectionId = getConnectionIdFromState(state, worktreeId ?? null)
// Why: global runtime focus is not ownership evidence while SSH/local
// metadata is unresolved; destructive actions must wait for explicit provenance.
if (connectionId === undefined) {
return { kind: 'unresolved' }
}
@@ -4,6 +4,7 @@ import { act, renderHook } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { SearchResult } from '../../../../shared/code-search-types'
import { useFileSearchRunner } from './useFileSearchRunner'
import { localAndSshTwinRows } from '@/lib/workspace-twin-rows.test-fixture'
const mocks = vi.hoisted(() => ({
getConnectionId: vi.fn(),
@@ -105,16 +106,7 @@ describe('useFileSearchRunner result ownership', () => {
const worktreeId = 'twin-repo::/repo'
const state = {
settings: { activeRuntimeEnvironmentId: 'ambient-runtime-b' },
repos: [
{ id: 'twin-repo', executionHostId: 'local' },
{ id: 'twin-repo', connectionId: 'box', executionHostId: 'ssh:box' }
],
worktreesByRepo: {
'twin-repo': [
{ id: worktreeId, repoId: 'twin-repo', hostId: 'local' },
{ id: worktreeId, repoId: 'twin-repo', hostId: 'ssh:box' }
]
},
...localAndSshTwinRows('twin-repo', worktreeId),
fileSearchStateByWorktree: { [worktreeId]: {} }
}
const { hook, updates } = renderSearchRunner(state, worktreeId)
@@ -16,7 +16,7 @@ import {
getExecutionHostIdForWorktree,
runtimeEnvironmentIdWithLegacyFallbackUnlessTwin
} from '@/lib/worktree-runtime-owner'
import { resolveOwner } from '@/lib/resolve-owner'
import { isTwinWorkspace } from '@/lib/resolve-owner'
import type { SearchResult } from '../../../../shared/code-search-types'
import { runtimeTargetForOwnerEnvironment } from '@/runtime/runtime-client-target'
@@ -51,11 +51,7 @@ export function useFileSearchRunner({
getExecutionHostIdForWorktree(state, activeWorktreeId)
)
// Why: a workspace two hosts publish names no owner, so either host's results would mislead.
const ownerIsTwin = useAppStore(
(state) =>
!!activeWorktreeId &&
resolveOwner(state, { workspaceId: activeWorktreeId }).kind === 'ambiguous'
)
const ownerIsTwin = useAppStore((state) => isTwinWorkspace(state, activeWorktreeId))
const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
// Why: runtime searches can finish out of order; ids keep stale results
// from overwriting the newest query state.
@@ -63,8 +63,6 @@ vi.mock('../../runtime/web-runtime-session-environment', () => ({
isWebRuntimeSessionActive: vi.fn(() => false)
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({ getRuntimeEnvironmentIdForWorktree: () => null }))
vi.mock('@/runtime/remote-browser-tab-ownership', () => ({
browserWorkspaceHasRemoteOwner: () => false
}))
@@ -62,7 +62,6 @@ vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: mocks.callRunti
vi.mock('@/lib/structured-agent-session-launch', () => ({
cancelStructuredAgentLaunch: mocks.cancelStructuredAgentLaunch
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({ getRuntimeEnvironmentIdForWorktree: () => null }))
vi.mock('@/runtime/runtime-worktree-selector', () => ({
toRuntimeWorktreeSelector: (id: string) => `id:${id}`
}))
+10 -71
View File
@@ -6,11 +6,8 @@ import {
openFileInBrowserTab,
openFilePreviewToSide
} from './file-preview'
import {
getWorkspaceFileBrowserOpenTarget,
REMOTE_FILE_BROWSER_UNSUPPORTED_MESSAGE,
TWIN_FILE_BROWSER_UNSUPPORTED_MESSAGE
} from './workspace-file-browser-open-target'
import { TWIN_FILE_BROWSER_UNSUPPORTED_MESSAGE } from './workspace-file-browser-open-target'
import { localAndSshTwinRows } from './workspace-twin-rows.test-fixture'
import { folderWorkspaceKey } from '../../../shared/workspace-scope'
import { useAppStore } from '@/store'
@@ -76,16 +73,12 @@ vi.mock('@/store', () => ({
getKnownWorktreeById: () => ({ id: 'wt-1', path: '/srv/repo' }),
groupsByWorktree: {},
layoutByWorktree: mocks.layoutByWorktree,
repos: [
{ id: 'repo-1', connectionId: mocks.connectionId },
...(mocks.twin ? [{ id: 'repo-1', connectionId: 'box', executionHostId: 'ssh:box' }] : [])
],
worktreesByRepo: {
'repo-1': [
{ id: 'wt-1', repoId: 'repo-1', ...(mocks.twin ? { hostId: 'local' } : {}) },
...(mocks.twin ? [{ id: 'wt-1', repoId: 'repo-1', hostId: 'ssh:box' }] : [])
]
}
...(mocks.twin
? localAndSshTwinRows('repo-1', 'wt-1')
: {
repos: [{ id: 'repo-1', connectionId: mocks.connectionId }],
worktreesByRepo: { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] }
})
})
}
}))
@@ -100,6 +93,7 @@ beforeEach(() => {
mocks.browserPagesByWorkspace = {}
mocks.unifiedTabsByWorktree = {}
mocks.activeWorktreeId = 'wt-1'
mocks.twin = false
})
/**
@@ -484,47 +478,7 @@ describe('canShowWorkspaceFileBrowserAction', () => {
})
})
describe('getWorkspaceFileBrowserOpenTarget', () => {
it('returns a reusable browser navigation target for local files', () => {
expect(
getWorkspaceFileBrowserOpenTarget({
filePath: 'C:\\repo\\demo page.html',
worktreeId: 'wt-1'
})
).toEqual({
status: 'ready',
url: 'file:///C:/repo/demo%20page.html',
title: 'demo page.html'
})
})
it('still refuses remote files, which have no local file URL', () => {
mocks.connectionId = 'ssh-1'
expect(
getWorkspaceFileBrowserOpenTarget({
filePath: '/home/alice/report.html',
worktreeId: 'wt-1'
})
).toEqual({
status: 'unsupported',
reason: 'remote-worktree',
message: REMOTE_FILE_BROWSER_UNSUPPORTED_MESSAGE
})
})
it('refuses a file:// URL for a project two hosts publish, naming why', () => {
mocks.twin = true
expect(
getWorkspaceFileBrowserOpenTarget({ filePath: '/srv/repo/report.html', worktreeId: 'wt-1' })
).toEqual({
status: 'unsupported',
reason: 'remote-worktree',
message: TWIN_FILE_BROWSER_UNSUPPORTED_MESSAGE
})
mocks.twin = false
})
describe('getWorkspaceFilePreviewPlan', () => {
it('plans no local browser tab for a project two hosts publish', () => {
mocks.twin = true
expect(
@@ -534,21 +488,6 @@ describe('getWorkspaceFileBrowserOpenTarget', () => {
message: TWIN_FILE_BROWSER_UNSUPPORTED_MESSAGE,
reason: 'no-channel'
})
mocks.twin = false
})
it('refuses paired files even when their connection ID is null', () => {
mocks.environmentId = 'managed-host'
expect(
getWorkspaceFileBrowserOpenTarget({
filePath: '/srv/repo/report.html',
worktreeId: 'wt-1'
})
).toEqual({
status: 'unsupported',
reason: 'remote-worktree',
message: REMOTE_FILE_BROWSER_UNSUPPORTED_MESSAGE
})
})
})
+2 -2
View File
@@ -8,7 +8,7 @@ import { basename, getRelativePathInsideRoot } from '@/lib/path'
import { getConnectionIdForFileFromState } from '@/lib/connection-owner-resolution'
import { activateBrowserWorkspaceTab } from '@/lib/browser-workspace-tab-activation'
import { runtimeEnvironmentIdWithLegacyFallbackUnlessTwin } from '@/lib/worktree-runtime-owner'
import { resolveOwner } from '@/lib/resolve-owner'
import { isTwinWorkspace } from '@/lib/resolve-owner'
import {
REMOTE_FILE_BROWSER_UNSUPPORTED_MESSAGE,
TWIN_FILE_BROWSER_UNSUPPORTED_MESSAGE
@@ -56,7 +56,7 @@ export function getWorkspaceFilePreviewPlan(
filePath: string
): WorkspaceFilePreviewPlan {
// Why: a twin reads as "no server" below, which would open this machine's copy of the path.
if (resolveOwner(state, { workspaceId: worktreeId }).kind === 'ambiguous') {
if (isTwinWorkspace(state, worktreeId)) {
return {
status: 'unsupported',
message: TWIN_FILE_BROWSER_UNSUPPORTED_MESSAGE,
@@ -178,7 +178,6 @@ vi.mock('@/runtime/local-runtime-capabilities', () => ({
vi.mock('@/lib/worktree-runtime-owner', () => ({
getExecutionHostIdForWorktree: () =>
store.repos[0]?.connectionId ? `ssh:${store.repos[0].connectionId}` : 'local',
getRuntimeEnvironmentIdForWorktree: () => null,
runtimeEnvironmentIdWithLegacyFallbackUnlessTwin: () => null
}))
vi.mock('@/runtime/structured-agent-session-client', () => ({
+30 -8
View File
@@ -178,6 +178,14 @@ function focusFreeRouteState(state: WorktreeOperationRouteState): {
}
}
/** Two hosts publish this workspace (or contest it), so no host-routed action may pick one. */
export function isTwinWorkspace(
state: WorktreeOperationRouteState,
workspaceId: string | null | undefined
): boolean {
return !!workspaceId && resolveOwner(state, { workspaceId }).kind === 'ambiguous'
}
/** Transport to the workspace's owner, or `null` when the rows name none or disagree. */
export function runtimeTargetForWorkspaceOwner(
state: WorktreeOperationRouteState,
@@ -270,13 +278,27 @@ export function isUnprovenServerSshRoute(
) {
return false
}
const repoId = getRepoIdFromWorktreeId(workspaceId)
const namesServer = (row: { id: string; hostId?: string; runtimeOwnerEnvironmentId?: string }) =>
row.id === workspaceId &&
parseExecutionHostId(row.hostId)?.id === at &&
row.runtimeOwnerEnvironmentId?.trim() === endpoint.environmentId
return !(
state.worktreesByRepo?.[repoId]?.some(namesServer) ||
state.detectedWorktreesByRepo?.[repoId]?.worktrees.some(namesServer)
return !someWorktreeCopy(
state,
workspaceId,
(row) =>
parseExecutionHostId(row.hostId)?.id === at &&
row.runtimeOwnerEnvironmentId?.trim() === endpoint.environmentId
)
}
type WorktreeCopy = { hostId?: string; runtimeOwnerEnvironmentId?: string }
/** Whether a listed or detected row with this worktree id satisfies `matches`. */
export function someWorktreeCopy(
state: Pick<WorktreeOperationRouteState, 'worktreesByRepo' | 'detectedWorktreesByRepo'>,
worktreeId: string,
matches: (row: WorktreeCopy) => boolean
): boolean {
const repoId = getRepoIdFromWorktreeId(worktreeId)
const copyMatches = (row: WorktreeCopy & { id: string }) => row.id === worktreeId && matches(row)
return Boolean(
state.worktreesByRepo?.[repoId]?.some(copyMatches) ||
state.detectedWorktreesByRepo?.[repoId]?.worktrees.some(copyMatches)
)
}
@@ -22,8 +22,6 @@ vi.mock('@/store', () => ({
}
}))
vi.mock('./worktree-runtime-owner', () => ({ getRuntimeEnvironmentIdForWorktree: () => null }))
vi.mock('./agent-draft-readiness', () => ({
waitForAgentDraftInputReady: async () => true
}))
@@ -0,0 +1,90 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
getWorkspaceFileBrowserOpenTarget,
REMOTE_FILE_BROWSER_UNSUPPORTED_MESSAGE,
TWIN_FILE_BROWSER_UNSUPPORTED_MESSAGE
} from './workspace-file-browser-open-target'
import { localAndSshTwinRows } from './workspace-twin-rows.test-fixture'
const mocks = vi.hoisted(() => ({
environmentId: null as string | null,
connectionId: null as string | null,
twin: false
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
runtimeEnvironmentIdWithLegacyFallbackUnlessTwin: () => mocks.environmentId
}))
vi.mock('@/store', () => ({
useAppStore: {
getState: () =>
mocks.twin
? localAndSshTwinRows('repo-1', 'wt-1')
: {
repos: [{ id: 'repo-1', connectionId: mocks.connectionId }],
worktreesByRepo: { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] }
}
}
}))
beforeEach(() => {
mocks.environmentId = null
mocks.connectionId = null
mocks.twin = false
})
describe('getWorkspaceFileBrowserOpenTarget', () => {
it('returns a reusable browser navigation target for local files', () => {
expect(
getWorkspaceFileBrowserOpenTarget({
filePath: 'C:\\repo\\demo page.html',
worktreeId: 'wt-1'
})
).toEqual({
status: 'ready',
url: 'file:///C:/repo/demo%20page.html',
title: 'demo page.html'
})
})
it('still refuses remote files, which have no local file URL', () => {
mocks.connectionId = 'ssh-1'
expect(
getWorkspaceFileBrowserOpenTarget({
filePath: '/home/alice/report.html',
worktreeId: 'wt-1'
})
).toEqual({
status: 'unsupported',
reason: 'remote-worktree',
message: REMOTE_FILE_BROWSER_UNSUPPORTED_MESSAGE
})
})
it('refuses a file:// URL for a project two hosts publish, naming why', () => {
mocks.twin = true
expect(
getWorkspaceFileBrowserOpenTarget({ filePath: '/srv/repo/report.html', worktreeId: 'wt-1' })
).toEqual({
status: 'unsupported',
reason: 'remote-worktree',
message: TWIN_FILE_BROWSER_UNSUPPORTED_MESSAGE
})
})
it('refuses paired files even when their connection ID is null', () => {
mocks.environmentId = 'managed-host'
expect(
getWorkspaceFileBrowserOpenTarget({
filePath: '/srv/repo/report.html',
worktreeId: 'wt-1'
})
).toEqual({
status: 'unsupported',
reason: 'remote-worktree',
message: REMOTE_FILE_BROWSER_UNSUPPORTED_MESSAGE
})
})
})
@@ -1,7 +1,7 @@
import { absolutePathToFileUri } from '@/components/editor/markdown-internal-links'
import { getConnectionIdForFile } from '@/lib/connection-context'
import { basename } from '@/lib/path'
import { resolveOwner } from '@/lib/resolve-owner'
import { isTwinWorkspace } from '@/lib/resolve-owner'
import { runtimeEnvironmentIdWithLegacyFallbackUnlessTwin } from '@/lib/worktree-runtime-owner'
import { useAppStore } from '@/store'
@@ -30,7 +30,7 @@ export function getWorkspaceFileBrowserOpenTarget(params: {
}): WorkspaceFileBrowserOpenTarget {
const state = useAppStore.getState()
// Why: a twin reads as "no server", which must not let a file:// URL open this machine's copy.
if (resolveOwner(state, { workspaceId: params.worktreeId }).kind === 'ambiguous') {
if (isTwinWorkspace(state, params.worktreeId)) {
return {
status: 'unsupported',
reason: 'remote-worktree',
@@ -0,0 +1,23 @@
import type { ExecutionHostId } from '../../../shared/execution-host'
type TwinRepoRow = { id: string; connectionId?: string; executionHostId: ExecutionHostId }
type TwinWorktreeRow = { id: string; repoId: string; hostId: ExecutionHostId }
/** Repo and worktree rows for one project that this app and the SSH target `box` both publish. */
export function localAndSshTwinRows(
repoId: string,
worktreeId: string
): { repos: TwinRepoRow[]; worktreesByRepo: Record<string, TwinWorktreeRow[]> } {
return {
repos: [
{ id: repoId, executionHostId: 'local' },
{ id: repoId, connectionId: 'box', executionHostId: 'ssh:box' }
],
worktreesByRepo: {
[repoId]: [
{ id: worktreeId, repoId, hostId: 'local' },
{ id: worktreeId, repoId, hostId: 'ssh:box' }
]
}
}
}
+10 -11
View File
@@ -22,7 +22,12 @@ import {
getExecutionHostIdForFolderWorkspace,
getRuntimeEnvironmentIdForFolderWorkspace
} from './folder-workspace-runtime-owner'
import { namedHostForOwnerRef, resolveOwner, type WorkspaceOwnerRef } from './resolve-owner'
import {
namedHostForOwnerRef,
resolveOwner,
someWorktreeCopy,
type WorkspaceOwnerRef
} from './resolve-owner'
import { SELF_LOCAL_AUTHORITY, type HostAuthority } from '../../../shared/host-authority'
import type { ExecutionHostOwnerMatch } from '../../../shared/worktree-execution-host-resolution'
import {
@@ -249,16 +254,10 @@ function hasHostEvidence(state: WorktreeRuntimeOwnerState, workspaceId: string):
)
}
const repoId = getRepoIdFromWorktreeId(workspaceId)
const isStampedCopy = (row: {
id: string
hostId?: string
runtimeOwnerEnvironmentId?: string
}) =>
row.id === workspaceId && Boolean(row.hostId?.trim() || row.runtimeOwnerEnvironmentId?.trim())
return Boolean(
state.worktreesByRepo?.[repoId]?.some(isStampedCopy) ||
state.detectedWorktreesByRepo?.[repoId]?.worktrees.some(isStampedCopy) ||
state.repos?.some((repo) => repo.id === repoId && hasRowHost(repo))
return (
someWorktreeCopy(state, workspaceId, (row) =>
Boolean(row.hostId?.trim() || row.runtimeOwnerEnvironmentId?.trim())
) || Boolean(state.repos?.some((repo) => repo.id === repoId && hasRowHost(repo)))
)
}
@@ -40,8 +40,6 @@ vi.mock('@/lib/feature-education-telemetry', () => ({
trackTerminalPaneSplit: vi.fn()
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({ getRuntimeEnvironmentIdForWorktree: vi.fn() }))
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn()
@@ -36,7 +36,6 @@ const mocks = vi.hoisted(() => ({
trackTerminalPaneSplit: vi.fn(),
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
getRuntimeEnvironmentIdForWorktree: vi.fn(),
hasMaterializedWebRuntimeBrowserPage: vi.fn()
}))
@@ -70,9 +69,6 @@ vi.mock('./web-session-tabs-sync/tracking-mappings', () => ({
vi.mock('@/lib/feature-education-telemetry', () => ({
trackTerminalPaneSplit: mocks.trackTerminalPaneSplit
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree
}))
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({
deliverLaunchPromptToAgentTab: mocks.deliverLaunchPromptToAgentTab,
seedNativeChatLaunchDraftForAgentTab: mocks.seedNativeChatLaunchDraftForAgentTab
@@ -29,7 +29,6 @@ const mocks = vi.hoisted(() => ({
trackTerminalPaneSplit: vi.fn(),
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
getRuntimeEnvironmentIdForWorktree: vi.fn(),
hasMaterializedWebRuntimeBrowserPage: vi.fn()
}))
@@ -66,10 +65,6 @@ vi.mock('@/lib/feature-education-telemetry', () => ({
trackTerminalPaneSplit: mocks.trackTerminalPaneSplit
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree
}))
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({
deliverLaunchPromptToAgentTab: mocks.deliverLaunchPromptToAgentTab,
seedNativeChatLaunchDraftForAgentTab: mocks.seedNativeChatLaunchDraftForAgentTab
@@ -31,7 +31,6 @@ const mocks = vi.hoisted(() => ({
trackTerminalPaneSplit: vi.fn(),
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
getRuntimeEnvironmentIdForWorktree: vi.fn(),
hasMaterializedWebRuntimeBrowserPage: vi.fn()
}))
@@ -68,10 +67,6 @@ vi.mock('@/lib/feature-education-telemetry', () => ({
trackTerminalPaneSplit: mocks.trackTerminalPaneSplit
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree
}))
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({
deliverLaunchPromptToAgentTab: mocks.deliverLaunchPromptToAgentTab,
seedNativeChatLaunchDraftForAgentTab: mocks.seedNativeChatLaunchDraftForAgentTab
@@ -32,7 +32,6 @@ const mocks = vi.hoisted(() => ({
trackTerminalPaneSplit: vi.fn(),
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
getRuntimeEnvironmentIdForWorktree: vi.fn(),
hasMaterializedWebRuntimeBrowserPage: vi.fn()
}))
@@ -69,10 +68,6 @@ vi.mock('@/lib/feature-education-telemetry', () => ({
trackTerminalPaneSplit: mocks.trackTerminalPaneSplit
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree
}))
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({
deliverLaunchPromptToAgentTab: mocks.deliverLaunchPromptToAgentTab,
seedNativeChatLaunchDraftForAgentTab: mocks.seedNativeChatLaunchDraftForAgentTab
@@ -37,7 +37,6 @@ const mocks = vi.hoisted(() => ({
trackTerminalPaneSplit: vi.fn(),
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
getRuntimeEnvironmentIdForWorktree: vi.fn(),
hasMaterializedWebRuntimeBrowserPage: vi.fn()
}))
@@ -71,9 +70,6 @@ vi.mock('./web-session-tabs-sync/tracking-mappings', () => ({
vi.mock('@/lib/feature-education-telemetry', () => ({
trackTerminalPaneSplit: mocks.trackTerminalPaneSplit
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree
}))
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({
deliverLaunchPromptToAgentTab: mocks.deliverLaunchPromptToAgentTab,
seedNativeChatLaunchDraftForAgentTab: mocks.seedNativeChatLaunchDraftForAgentTab
@@ -37,7 +37,6 @@ const mocks = vi.hoisted(() => ({
trackTerminalPaneSplit: vi.fn(),
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
getRuntimeEnvironmentIdForWorktree: vi.fn(),
hasMaterializedWebRuntimeBrowserPage: vi.fn()
}))
@@ -71,9 +70,6 @@ vi.mock('./web-session-tabs-sync/tracking-mappings', () => ({
vi.mock('@/lib/feature-education-telemetry', () => ({
trackTerminalPaneSplit: mocks.trackTerminalPaneSplit
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree
}))
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({
deliverLaunchPromptToAgentTab: mocks.deliverLaunchPromptToAgentTab,
seedNativeChatLaunchDraftForAgentTab: mocks.seedNativeChatLaunchDraftForAgentTab
@@ -32,7 +32,6 @@ const mocks = vi.hoisted(() => ({
trackTerminalPaneSplit: vi.fn(),
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
getRuntimeEnvironmentIdForWorktree: vi.fn(),
hasMaterializedWebRuntimeBrowserPage: vi.fn()
}))
@@ -69,10 +68,6 @@ vi.mock('@/lib/feature-education-telemetry', () => ({
trackTerminalPaneSplit: mocks.trackTerminalPaneSplit
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree
}))
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({
deliverLaunchPromptToAgentTab: mocks.deliverLaunchPromptToAgentTab,
seedNativeChatLaunchDraftForAgentTab: mocks.seedNativeChatLaunchDraftForAgentTab
@@ -36,7 +36,6 @@ const mocks = vi.hoisted(() => ({
trackTerminalPaneSplit: vi.fn(),
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
getRuntimeEnvironmentIdForWorktree: vi.fn(),
hasMaterializedWebRuntimeBrowserPage: vi.fn()
}))
@@ -69,9 +68,6 @@ vi.mock('./web-session-tabs-sync/tracking-mappings', () => ({
vi.mock('@/lib/feature-education-telemetry', () => ({
trackTerminalPaneSplit: mocks.trackTerminalPaneSplit
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree
}))
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({
deliverLaunchPromptToAgentTab: mocks.deliverLaunchPromptToAgentTab,
seedNativeChatLaunchDraftForAgentTab: mocks.seedNativeChatLaunchDraftForAgentTab
@@ -42,7 +42,6 @@ const mocks = vi.hoisted(() => ({
trackTerminalPaneSplit: vi.fn(),
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
getRuntimeEnvironmentIdForWorktree: vi.fn(),
hasMaterializedWebRuntimeBrowserPage: vi.fn()
}))
@@ -80,10 +79,6 @@ vi.mock('@/lib/feature-education-telemetry', () => ({
trackTerminalPaneSplit: mocks.trackTerminalPaneSplit
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree
}))
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({
deliverLaunchPromptToAgentTab: mocks.deliverLaunchPromptToAgentTab,
seedNativeChatLaunchDraftForAgentTab: mocks.seedNativeChatLaunchDraftForAgentTab
@@ -31,7 +31,6 @@ const mocks = vi.hoisted(() => ({
trackTerminalPaneSplit: vi.fn(),
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
getRuntimeEnvironmentIdForWorktree: vi.fn(),
hasMaterializedWebRuntimeBrowserPage: vi.fn()
}))
@@ -69,10 +68,6 @@ vi.mock('@/lib/feature-education-telemetry', () => ({
trackTerminalPaneSplit: mocks.trackTerminalPaneSplit
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree
}))
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({
deliverLaunchPromptToAgentTab: mocks.deliverLaunchPromptToAgentTab,
seedNativeChatLaunchDraftForAgentTab: mocks.seedNativeChatLaunchDraftForAgentTab
@@ -26,7 +26,6 @@ const mocks = vi.hoisted(() => ({
trackTerminalPaneSplit: vi.fn(),
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
getRuntimeEnvironmentIdForWorktree: vi.fn(),
hasMaterializedWebRuntimeBrowserPage: vi.fn()
}))
@@ -64,10 +63,6 @@ vi.mock('@/lib/feature-education-telemetry', () => ({
trackTerminalPaneSplit: mocks.trackTerminalPaneSplit
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree
}))
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({
deliverLaunchPromptToAgentTab: mocks.deliverLaunchPromptToAgentTab,
seedNativeChatLaunchDraftForAgentTab: mocks.seedNativeChatLaunchDraftForAgentTab
@@ -36,7 +36,6 @@ const mocks = vi.hoisted(() => ({
trackTerminalPaneSplit: vi.fn(),
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
getRuntimeEnvironmentIdForWorktree: vi.fn(),
hasMaterializedWebRuntimeBrowserPage: vi.fn()
}))
@@ -74,10 +73,6 @@ vi.mock('@/lib/feature-education-telemetry', () => ({
trackTerminalPaneSplit: mocks.trackTerminalPaneSplit
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree
}))
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({
deliverLaunchPromptToAgentTab: mocks.deliverLaunchPromptToAgentTab,
seedNativeChatLaunchDraftForAgentTab: mocks.seedNativeChatLaunchDraftForAgentTab
@@ -28,7 +28,6 @@ const mocks = vi.hoisted(() => ({
trackTerminalPaneSplit: vi.fn(),
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
getRuntimeEnvironmentIdForWorktree: vi.fn(),
hasMaterializedWebRuntimeBrowserPage: vi.fn()
}))
@@ -65,10 +64,6 @@ vi.mock('@/lib/feature-education-telemetry', () => ({
trackTerminalPaneSplit: mocks.trackTerminalPaneSplit
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree
}))
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({
deliverLaunchPromptToAgentTab: mocks.deliverLaunchPromptToAgentTab,
seedNativeChatLaunchDraftForAgentTab: mocks.seedNativeChatLaunchDraftForAgentTab
@@ -32,7 +32,6 @@ const mocks = vi.hoisted(() => ({
trackTerminalPaneSplit: vi.fn(),
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
getRuntimeEnvironmentIdForWorktree: vi.fn(),
hasMaterializedWebRuntimeBrowserPage: vi.fn()
}))
@@ -73,10 +72,6 @@ vi.mock('@/lib/feature-education-telemetry', () => ({
trackTerminalPaneSplit: mocks.trackTerminalPaneSplit
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree
}))
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({
deliverLaunchPromptToAgentTab: mocks.deliverLaunchPromptToAgentTab,
seedNativeChatLaunchDraftForAgentTab: mocks.seedNativeChatLaunchDraftForAgentTab
@@ -29,7 +29,6 @@ const mocks = vi.hoisted(() => ({
trackTerminalPaneSplit: vi.fn(),
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
getRuntimeEnvironmentIdForWorktree: vi.fn(),
hasMaterializedWebRuntimeBrowserPage: vi.fn()
}))
@@ -64,10 +63,6 @@ vi.mock('@/lib/feature-education-telemetry', () => ({
trackTerminalPaneSplit: mocks.trackTerminalPaneSplit
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree
}))
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({
deliverLaunchPromptToAgentTab: mocks.deliverLaunchPromptToAgentTab,
seedNativeChatLaunchDraftForAgentTab: mocks.seedNativeChatLaunchDraftForAgentTab
@@ -31,7 +31,7 @@ const mocks = vi.hoisted(() => ({
trackTerminalPaneSplit: vi.fn(),
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
getRuntimeEnvironmentIdForWorktree: vi.fn(),
runtimeEnvironmentIdWithLegacyFallbackUnlessTwin: vi.fn(),
hasMaterializedWebRuntimeBrowserPage: vi.fn(),
recoverWebSessionTerminalOrphansBeforeApply: vi.fn(
async (_state: unknown, snapshot: unknown) => snapshot
@@ -76,8 +76,8 @@ vi.mock('@/lib/feature-education-telemetry', () => ({
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree,
runtimeEnvironmentIdWithLegacyFallbackUnlessTwin: mocks.getRuntimeEnvironmentIdForWorktree
runtimeEnvironmentIdWithLegacyFallbackUnlessTwin:
mocks.runtimeEnvironmentIdWithLegacyFallbackUnlessTwin
}))
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({