diff --git a/src/renderer/src/hooks/ipc-events/terminal-worktree-visibility.ts b/src/renderer/src/hooks/ipc-events/terminal-worktree-visibility.ts index 9f252f7e935..416ca5e02ea 100644 --- a/src/renderer/src/hooks/ipc-events/terminal-worktree-visibility.ts +++ b/src/renderer/src/hooks/ipc-events/terminal-worktree-visibility.ts @@ -1,6 +1,8 @@ -import { hasRenderableTerminalWorktreeSurface as hasTerminalWorktreeRow } from '@/lib/terminal-worktree-route' +import { + hasRenderableTerminalWorktreeSurface as hasTerminalWorktreeRow, + resolveTerminalWorktreeCatalogHostId +} from '@/lib/terminal-worktree-route' import { importNewExternalWorktreeInboxPaths } from '@/components/sidebar/new-external-worktrees-inbox-actions' -import { resolveWorktreeOperationRoute } from '@/lib/worktree-operation-route' import { findRepoForHost, getRepoHostIdentity } from '@/store/slices/repo-host-identity' import { getRepoIdFromWorktreeId } from '@/store/slices/worktree-helpers' import { @@ -22,10 +24,7 @@ export async function ensureTerminalWorktreeVisible(worktreeId: string): Promise if (hasTerminalWorktreeRow(state, worktreeId)) { return } - const route = resolveWorktreeOperationRoute(state, worktreeId) - const hostId = route?.runtimeEnvironmentId - ? (`runtime:${encodeURIComponent(route.runtimeEnvironmentId)}` as const) - : route?.executionHostId + const hostId = resolveTerminalWorktreeCatalogHostId(state, worktreeId) if (!hostId) { throw hiddenTerminalWorktreeError() } diff --git a/src/renderer/src/lib/terminal-worktree-route.ts b/src/renderer/src/lib/terminal-worktree-route.ts index 2c8cab3b269..591485d2e53 100644 --- a/src/renderer/src/lib/terminal-worktree-route.ts +++ b/src/renderer/src/lib/terminal-worktree-route.ts @@ -1,6 +1,10 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' import { isEphemeralSetupTerminalWorktreeId } from '../../../shared/ephemeral-setup-terminal-worktree-id' -import { parseExecutionHostId } from '../../../shared/execution-host' +import { + parseExecutionHostId, + toRuntimeExecutionHostId, + type ExecutionHostId +} from '../../../shared/execution-host' import { parseWorkspaceKey } from '../../../shared/workspace-scope' import type { AppState } from '@/store/types' import { getIndexedWorktreesById } from '@/store/worktree-repo-index' @@ -24,6 +28,21 @@ export type TerminalWorktreeRoute = { runtimeEnvironmentId: string | null } +/** The catalog host whose rows can render this worktree, or null when ownership is unresolved. */ +export function resolveTerminalWorktreeCatalogHostId( + state: AppState, + worktreeId: string +): ExecutionHostId | null { + const route = resolveWorktreeOperationRoute(state, worktreeId) + if (!route) { + return null + } + // Why: a paired-HUB row can publish only its environment owner, leaving the physical host null. + return route.runtimeEnvironmentId + ? toRuntimeExecutionHostId(route.runtimeEnvironmentId) + : route.executionHostId +} + export function hasRenderableTerminalWorktreeSurface( state: AppState, worktreeId: string | null | undefined @@ -41,15 +60,11 @@ export function hasRenderableTerminalWorktreeSurface( ) } // Only the workbench's row index can host new tabs; detected rows and inline setup ids cannot. - const route = resolveWorktreeOperationRoute(state, worktreeId) - const hostId = route?.executionHostId - if (!hostId) { + const catalogHostId = resolveTerminalWorktreeCatalogHostId(state, worktreeId) + if (!catalogHostId) { return false } const repoId = getRepoIdFromWorktreeId(worktreeId) - const catalogHostId = route.runtimeEnvironmentId - ? (`runtime:${encodeURIComponent(route.runtimeEnvironmentId)}` as const) - : hostId const matchOptions = worktreeHostMatchOptions(state, repoId, catalogHostId) return state.worktreesByRepo ? getIndexedWorktreesById(state.worktreesByRepo, worktreeId).some((row) => diff --git a/src/renderer/src/lib/terminal-worktree-surface.test.ts b/src/renderer/src/lib/terminal-worktree-surface.test.ts index 805f0f058aa..19329d4c730 100644 --- a/src/renderer/src/lib/terminal-worktree-surface.test.ts +++ b/src/renderer/src/lib/terminal-worktree-surface.test.ts @@ -28,6 +28,16 @@ describe('hasRenderableTerminalWorktreeSurface', () => { } }) + it('accepts a paired-runtime row that carries only its environment owner', () => { + const store = state({ + repos: [{ id: 'repo-1', executionHostId: 'runtime:hub' }], + worktreesByRepo: { + 'repo-1': [{ id: 'repo-1::/workspace', repoId: 'repo-1', runtimeOwnerEnvironmentId: 'hub' }] + } + } as unknown as Partial) + expect(hasRenderableTerminalWorktreeSurface(store, 'repo-1::/workspace')).toBe(true) + }) + it('rejects a hidden detected worktree even when its repo is registered', () => { const store = state({ detectedWorktreesByRepo: {