diff --git a/config/deprecated-runtime-owner-importers-baseline.txt b/config/deprecated-runtime-owner-importers-baseline.txt index 4482935072e..45b8574fdbd 100644 --- a/config/deprecated-runtime-owner-importers-baseline.txt +++ b/config/deprecated-runtime-owner-importers-baseline.txt @@ -9,8 +9,6 @@ src/renderer/src/components/native-chat/native-chat-runtime-owner.ts src/renderer/src/components/right-sidebar/file-explorer-operation-owner.ts src/renderer/src/components/right-sidebar/use-file-explorer-tree-pane-state.ts src/renderer/src/components/tab-bar/use-tab-bar-runtime-model.ts -src/renderer/src/components/tab-bar/web-runtime-tab-move-mirror.ts -src/renderer/src/components/tab-group/useTabGroupActivationCommands.ts src/renderer/src/components/tab-group/useTabGroupCreationCommands.ts src/renderer/src/components/terminal-pane/terminal-pane-menu-paste.ts src/renderer/src/components/terminal-pane/terminal-pane-paste-execution.ts @@ -26,18 +24,11 @@ src/renderer/src/lib/file-preview.ts src/renderer/src/lib/launch-agent-background-session.ts src/renderer/src/lib/launch-agent-in-new-tab.ts src/renderer/src/lib/launch-ai-vault-session.ts -src/renderer/src/lib/structured-agent-session-tab-activation.ts -src/renderer/src/lib/tab-number-shortcuts.ts -src/renderer/src/lib/terminal-tab-owner-activation.ts src/renderer/src/lib/terminal-worktree-route.ts src/renderer/src/lib/web-runtime-worktree-terminal-after-wake.ts -src/renderer/src/lib/workspace-tab-palette-activation.ts -src/renderer/src/lib/worktree-activation.ts src/renderer/src/lib/worktree-creation-agent-seeds.ts -src/renderer/src/lib/worktree-sort-order-host-split.ts src/renderer/src/store/slices/browser/browser-page-conversion-actions.ts src/renderer/src/store/slices/browser/browser-tab-actions.ts -src/renderer/src/store/slices/tabs/tabs-host-mirroring.ts src/renderer/src/store/slices/terminal-tab-retirement.ts src/renderer/src/store/terminals/terminal-active-workspace-creation.ts src/renderer/src/store/terminals/terminal-pane-hibernation.ts diff --git a/config/focus-setting-read-baseline.txt b/config/focus-setting-read-baseline.txt index 924ca07fc28..c4bf9c10af3 100644 --- a/config/focus-setting-read-baseline.txt +++ b/config/focus-setting-read-baseline.txt @@ -360,6 +360,7 @@ 1 src/renderer/src/lib/worktree-row-open-in-owner.ts # reads focus through resolveOwnerWithLegacyFallback, newly counted (round 3 F2); F8 nets it down 9 src/renderer/src/lib/worktree-runtime-owner.ts # typed owner resolver, its null-collapsing wrapper, and the legacy-fallback adapter the folds move to (round 3 F0); deleting the deprecated pair (F8) nets it down 1 src/renderer/src/lib/worktree-sort-order-host-split.ts +1 src/renderer/src/lib/worktree-sort-order-persistence.ts # its split helper calls resolveOwnerWithLegacyFallback directly (round 3 F4); F8 nets it down 1 src/renderer/src/runtime/local-structured-session-tabs-sync/snapshot-apply.ts 17 src/renderer/src/runtime/runtime-jira-client.ts 2 src/renderer/src/runtime/runtime-jira-summary-client.ts diff --git a/src/renderer/src/components/tab-bar/web-runtime-tab-move-mirror.ts b/src/renderer/src/components/tab-bar/web-runtime-tab-move-mirror.ts index 9a0a9d714d0..f15aedab71e 100644 --- a/src/renderer/src/components/tab-bar/web-runtime-tab-move-mirror.ts +++ b/src/renderer/src/components/tab-bar/web-runtime-tab-move-mirror.ts @@ -1,5 +1,5 @@ import type { RuntimeMobileSessionTabMove } from '../../../../shared/runtime-types' -import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' +import { runtimeEnvironmentIdWithLegacyFallbackUnlessTwin } from '@/lib/worktree-runtime-owner' import { useAppStore } from '../../store' import { isWebRuntimeSessionActive } from '../../runtime/web-runtime-session-environment' import { moveWebRuntimeSessionTab } from '../../runtime/web-runtime-session-tab-move' @@ -9,7 +9,10 @@ export function mirrorWebRuntimeTabMove( worktreeId: string } ): void { - const environmentId = getRuntimeEnvironmentIdForWorktree(useAppStore.getState(), args.worktreeId) + const environmentId = runtimeEnvironmentIdWithLegacyFallbackUnlessTwin( + useAppStore.getState(), + args.worktreeId + ) if (!isWebRuntimeSessionActive(environmentId)) { return } diff --git a/src/renderer/src/components/tab-group/useTabGroupActivationCommands.ts b/src/renderer/src/components/tab-group/useTabGroupActivationCommands.ts index e1892c4bacc..7a7ec61aa57 100644 --- a/src/renderer/src/components/tab-group/useTabGroupActivationCommands.ts +++ b/src/renderer/src/components/tab-group/useTabGroupActivationCommands.ts @@ -5,7 +5,7 @@ import { focusTerminalTabSurface } from '../../lib/focus-terminal-tab-surface' import { TOGGLE_TERMINAL_PANE_EXPAND_EVENT } from '@/constants/terminal' import { activateWebRuntimeSessionTab } from '../../runtime/web-runtime-session-tab-lifecycle' import { isWebRuntimeSessionActive } from '../../runtime/web-runtime-session-environment' -import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' +import { runtimeEnvironmentIdWithLegacyFallbackUnlessTwin } from '@/lib/worktree-runtime-owner' import { browserWorkspaceHasRemoteOwner } from '@/runtime/remote-browser-tab-ownership' import { activateStructuredAgentSessionTab } from '@/lib/structured-agent-session-tab-activation' import type { TabGroupWorktreeSnapshot } from './useTabGroupItemProjections' @@ -38,7 +38,7 @@ export function useTabGroupActivationCommands({ } focusGroup(worktreeId, groupId) activateTab(item.id) - const runtimeEnvironmentId = getRuntimeEnvironmentIdForWorktree( + const runtimeEnvironmentId = runtimeEnvironmentIdWithLegacyFallbackUnlessTwin( useAppStore.getState(), worktreeId ) @@ -117,7 +117,7 @@ export function useTabGroupActivationCommands({ } focusGroup(worktreeId, groupId) activateTab(item.id) - const runtimeEnvironmentId = getRuntimeEnvironmentIdForWorktree( + const runtimeEnvironmentId = runtimeEnvironmentIdWithLegacyFallbackUnlessTwin( useAppStore.getState(), worktreeId ) diff --git a/src/renderer/src/lib/resolve-owner.ts b/src/renderer/src/lib/resolve-owner.ts index 402fe910c84..6e6894d9694 100644 --- a/src/renderer/src/lib/resolve-owner.ts +++ b/src/renderer/src/lib/resolve-owner.ts @@ -4,6 +4,7 @@ */ import { getRepoSshConnectionId, + parseExecutionHostId, toRuntimeExecutionHostId, toSshExecutionHostId, type ExecutionHostId @@ -250,3 +251,32 @@ export function runtimeEnvironmentIdUnlessTwin( } return null } + +/** + * A server route to `ssh:X` that only a repo row proves: no worktree row names that server. The + * row's `ssh:X` and the server's `connectionId: X` are different namespaces, so reads and spawn + * may follow it but a host write could land on another machine with the same path. + */ +export function isUnprovenServerSshRoute( + state: Pick, + workspaceId: string, + owner: HostAuthority +): boolean { + const { endpoint, at } = owner + if ( + endpoint.kind !== 'environment' || + at === 'local' || + parseWorkspaceKey(workspaceId)?.type === 'folder' + ) { + 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) + ) +} diff --git a/src/renderer/src/lib/structured-agent-session-tab-activation.test.ts b/src/renderer/src/lib/structured-agent-session-tab-activation.test.ts index 75a71085a82..6cc3a85abb6 100644 --- a/src/renderer/src/lib/structured-agent-session-tab-activation.test.ts +++ b/src/renderer/src/lib/structured-agent-session-tab-activation.test.ts @@ -18,7 +18,9 @@ vi.mock('@/components/native-chat/use-native-chat-composer-reveal-focus', () => requestNativeChatComposerFocus: mocks.requestNativeChatComposerFocus })) -vi.mock('./worktree-runtime-owner', () => ({ getRuntimeEnvironmentIdForWorktree: () => 'env-1' })) +vi.mock('./worktree-runtime-owner', () => ({ + runtimeEnvironmentIdWithLegacyFallbackUnlessTwin: () => 'env-1' +})) vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: mocks.callRuntimeRpc })) diff --git a/src/renderer/src/lib/structured-agent-session-tab-activation.ts b/src/renderer/src/lib/structured-agent-session-tab-activation.ts index 090f6426c31..c14d0525e05 100644 --- a/src/renderer/src/lib/structured-agent-session-tab-activation.ts +++ b/src/renderer/src/lib/structured-agent-session-tab-activation.ts @@ -1,4 +1,4 @@ -import { getRuntimeEnvironmentIdForWorktree } from './worktree-runtime-owner' +import { runtimeEnvironmentIdWithLegacyFallbackUnlessTwin } from './worktree-runtime-owner' import { requestNativeChatComposerFocus } from '@/components/native-chat/use-native-chat-composer-reveal-focus' import { useAppStore } from '@/store' import { callRuntimeRpc } from '@/runtime/runtime-rpc-client' @@ -49,7 +49,7 @@ export function activateStructuredAgentSessionTab(args: { state.activateTab(tab.id, { worktreeId: args.worktreeId }) state.setActiveTabType('agent-session', args.worktreeId) requestNativeChatComposerFocus(tab.id) - const environmentId = getRuntimeEnvironmentIdForWorktree(state, args.worktreeId) + const environmentId = runtimeEnvironmentIdWithLegacyFallbackUnlessTwin(state, args.worktreeId) void callRuntimeRpc( args.target ?? runtimeTargetForOwnerEnvironment(environmentId), 'session.tabs.activate', diff --git a/src/renderer/src/lib/tab-number-shortcuts.ts b/src/renderer/src/lib/tab-number-shortcuts.ts index 5477e076938..17562c906cf 100644 --- a/src/renderer/src/lib/tab-number-shortcuts.ts +++ b/src/renderer/src/lib/tab-number-shortcuts.ts @@ -1,7 +1,7 @@ import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface' import { useAppStore } from '@/store' import type { AppState } from '@/store/types' -import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' +import { runtimeEnvironmentIdWithLegacyFallbackUnlessTwin } from '@/lib/worktree-runtime-owner' import { dedupeTabOrder } from '../../../shared/workspace-layout/tab-order' import type { Tab } from '../../../shared/tab-types' import { activateWebRuntimeSessionTab } from '@/runtime/web-runtime-session-tab-lifecycle' @@ -60,7 +60,7 @@ export function activateTabNumberShortcut(index: number): boolean { } const worktreeId = target.worktreeId - const runtimeEnvironmentId = getRuntimeEnvironmentIdForWorktree(store, worktreeId) + const runtimeEnvironmentId = runtimeEnvironmentIdWithLegacyFallbackUnlessTwin(store, worktreeId) store.focusGroup(worktreeId, target.groupId) store.activateTab(target.id) diff --git a/src/renderer/src/lib/terminal-tab-owner-activation.test.ts b/src/renderer/src/lib/terminal-tab-owner-activation.test.ts index bef1c48b6a2..efe26bb3943 100644 --- a/src/renderer/src/lib/terminal-tab-owner-activation.test.ts +++ b/src/renderer/src/lib/terminal-tab-owner-activation.test.ts @@ -2,13 +2,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ activateWebRuntimeSessionTab: vi.fn(), - getRuntimeEnvironmentIdForWorktree: vi.fn(), + runtimeEnvironmentIdWithLegacyFallbackUnlessTwin: vi.fn(), resolveWebSessionVisibleTabId: vi.fn() })) vi.mock('@/store', () => ({ useAppStore: { getState: () => ({}) } })) vi.mock('@/lib/worktree-runtime-owner', () => ({ - getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree + runtimeEnvironmentIdWithLegacyFallbackUnlessTwin: + mocks.runtimeEnvironmentIdWithLegacyFallbackUnlessTwin })) vi.mock('@/runtime/web-runtime-session-tab-lifecycle', () => ({ activateWebRuntimeSessionTab: mocks.activateWebRuntimeSessionTab @@ -30,12 +31,12 @@ describe('activateTerminalTabOnOwner', () => { }) it('tells the paired server that owns the workspace, not the focused one', () => { - mocks.getRuntimeEnvironmentIdForWorktree.mockReturnValue('env-owner') + mocks.runtimeEnvironmentIdWithLegacyFallbackUnlessTwin.mockReturnValue('env-owner') mocks.resolveWebSessionVisibleTabId.mockReturnValue('unified-visible') activateTerminalTabOnOwner('wt-1', 'web-terminal-host-tab', 'leaf-b') - expect(mocks.getRuntimeEnvironmentIdForWorktree).toHaveBeenCalledWith({}, 'wt-1') + expect(mocks.runtimeEnvironmentIdWithLegacyFallbackUnlessTwin).toHaveBeenCalledWith({}, 'wt-1') expect(mocks.activateWebRuntimeSessionTab).toHaveBeenCalledWith({ worktreeId: 'wt-1', tabId: 'web-terminal-host-tab', @@ -47,7 +48,7 @@ describe('activateTerminalTabOnOwner', () => { }) it('makes no owner round trip for a workspace this client runs', () => { - mocks.getRuntimeEnvironmentIdForWorktree.mockReturnValue(null) + mocks.runtimeEnvironmentIdWithLegacyFallbackUnlessTwin.mockReturnValue(null) activateTerminalTabOnOwner('wt-local', 'tab-1', null) diff --git a/src/renderer/src/lib/terminal-tab-owner-activation.ts b/src/renderer/src/lib/terminal-tab-owner-activation.ts index 892e3850c59..5bf816fb685 100644 --- a/src/renderer/src/lib/terminal-tab-owner-activation.ts +++ b/src/renderer/src/lib/terminal-tab-owner-activation.ts @@ -1,5 +1,5 @@ import { useAppStore } from '@/store' -import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' +import { runtimeEnvironmentIdWithLegacyFallbackUnlessTwin } from '@/lib/worktree-runtime-owner' import { activateWebRuntimeSessionTab } from '@/runtime/web-runtime-session-tab-lifecycle' import { isWebRuntimeSessionActive } from '@/runtime/web-runtime-session-environment' import { resolveWebSessionVisibleTabId } from '@/runtime/web-session-focus-intent' @@ -17,7 +17,7 @@ export function activateTerminalTabOnOwner( leafId: string | null ): void { const state = useAppStore.getState() - const environmentId = getRuntimeEnvironmentIdForWorktree(state, worktreeId) + const environmentId = runtimeEnvironmentIdWithLegacyFallbackUnlessTwin(state, worktreeId) if (!environmentId || !isWebRuntimeSessionActive(environmentId)) { return } diff --git a/src/renderer/src/lib/workspace-tab-palette-activation.test.ts b/src/renderer/src/lib/workspace-tab-palette-activation.test.ts index b80e03c2b6b..1c98f3f261b 100644 --- a/src/renderer/src/lib/workspace-tab-palette-activation.test.ts +++ b/src/renderer/src/lib/workspace-tab-palette-activation.test.ts @@ -63,7 +63,7 @@ const mocks = vi.hoisted(() => { activateAndRevealWorkspace: vi.fn(), activateWebRuntimeSessionTab: vi.fn(), focusTerminalTabSurface: vi.fn(), - getRuntimeEnvironmentIdForWorktree: vi.fn(), + runtimeEnvironmentIdWithLegacyFallbackUnlessTwin: vi.fn(), isWebRuntimeSessionActive: vi.fn() } }) @@ -79,7 +79,8 @@ vi.mock('@/lib/focus-terminal-tab-surface', () => ({ })) vi.mock('@/lib/worktree-runtime-owner', () => ({ - getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree + runtimeEnvironmentIdWithLegacyFallbackUnlessTwin: + mocks.runtimeEnvironmentIdWithLegacyFallbackUnlessTwin })) vi.mock('@/runtime/web-runtime-session-tab-lifecycle', () => ({ @@ -172,7 +173,7 @@ describe('activateWorkspaceTabPaletteResult', () => { .find((worktree) => worktree.id === worktreeId) ) mocks.activateAndRevealWorkspace.mockReturnValue(true) - mocks.getRuntimeEnvironmentIdForWorktree.mockReturnValue('runtime-1') + mocks.runtimeEnvironmentIdWithLegacyFallbackUnlessTwin.mockReturnValue('runtime-1') mocks.isWebRuntimeSessionActive.mockReturnValue(false) }) diff --git a/src/renderer/src/lib/workspace-tab-palette-activation.ts b/src/renderer/src/lib/workspace-tab-palette-activation.ts index 45e56089e81..9a3dc6d92fd 100644 --- a/src/renderer/src/lib/workspace-tab-palette-activation.ts +++ b/src/renderer/src/lib/workspace-tab-palette-activation.ts @@ -1,5 +1,5 @@ import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface' -import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' +import { runtimeEnvironmentIdWithLegacyFallbackUnlessTwin } from '@/lib/worktree-runtime-owner' import { activateWebRuntimeSessionTab } from '@/runtime/web-runtime-session-tab-lifecycle' import { isWebRuntimeSessionActive } from '@/runtime/web-runtime-session-environment' import { useAppStore } from '@/store' @@ -119,7 +119,10 @@ export function activateWorkspaceTabPaletteResult( return { status: 'failed', reason: finalFailure } } - const runtimeEnvironmentId = getRuntimeEnvironmentIdForWorktree(state, result.worktreeId) + const runtimeEnvironmentId = runtimeEnvironmentIdWithLegacyFallbackUnlessTwin( + state, + result.worktreeId + ) state.focusGroup(result.worktreeId, result.groupId) state.activateTab(result.tabId, { worktreeId: result.worktreeId }) diff --git a/src/renderer/src/lib/worktree-activation.ts b/src/renderer/src/lib/worktree-activation.ts index 1ceb0375a67..54dd82185a6 100644 --- a/src/renderer/src/lib/worktree-activation.ts +++ b/src/renderer/src/lib/worktree-activation.ts @@ -7,7 +7,7 @@ import { registerWorktreeActivation } from '@/lib/worktree-activation-nav-regist import { workspaceHasSleepingAgentSessions } from '@/lib/worktree-agent-activation-gate' import { resumeSleepingAgentSessionsForWorktree } from '@/lib/resume-sleeping-agent-session' import { shouldAutoCreateInitialTerminal } from '@/components/terminal/initial-terminal' -import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' +import { runtimeEnvironmentIdWithLegacyFallbackUnlessTwin } from '@/lib/worktree-runtime-owner' import { folderWorkspaceKey, parseWorkspaceKey } from '../../../shared/workspace-scope' import { folderWorkspaceActivationBlocked, @@ -210,7 +210,10 @@ export function activateAndRevealWorktree( // 3. Core activation: setActiveWorktree also restores per-worktree state, clears unread, bumps dead PTY generations, refreshes GitHub state.setActiveWorktree(worktreeId, opts?.executionHostId) const postActivationState = useAppStore.getState() - const ownerRuntimeEnvironmentId = getRuntimeEnvironmentIdForWorktree(postActivationState, wt.id) + const ownerRuntimeEnvironmentId = runtimeEnvironmentIdWithLegacyFallbackUnlessTwin( + postActivationState, + wt.id + ) if (opts?.notifyHostRuntime !== false && isWebRuntimeSessionActive(ownerRuntimeEnvironmentId)) { // Why: paired web clients own only local selection, so the desktop host publishes session surfaces without treating it as a nav command. void activateWebRuntimeSessionWorktree({ diff --git a/src/renderer/src/lib/worktree-sort-order-host-split.test.ts b/src/renderer/src/lib/worktree-sort-order-host-split.test.ts index 9df953b13a7..691262042a2 100644 --- a/src/renderer/src/lib/worktree-sort-order-host-split.test.ts +++ b/src/renderer/src/lib/worktree-sort-order-host-split.test.ts @@ -34,4 +34,42 @@ describe('splitWorktreeSortOrderByHost', () => { ) expect(groups).toEqual([{ hostId: 'runtime:focused-env', orderedIds: ['legacy::wt'] }]) }) + + it('leaves out a twin and a row only a repo row places on a server', () => { + const groups = splitWorktreeSortOrderByHost( + { + settings: { activeRuntimeEnvironmentId: null }, + runtimeEnvironments: [{ id: 'hub-env' }], + runtimeEnvironmentCatalogHydrated: true, + repos: [ + { id: 'local-repo', connectionId: null, executionHostId: 'local' }, + { id: 'twin', connectionId: null, executionHostId: 'local' }, + { id: 'twin', connectionId: 'box', executionHostId: 'ssh:box' }, + { id: 'stale-hub', connectionId: 'target', executionHostId: 'runtime:hub-env' }, + { id: 'hub', connectionId: 'box', executionHostId: 'runtime:hub-env' } + ], + worktreesByRepo: { + 'local-repo': [{ id: 'local-repo::wt-a', repoId: 'local-repo' }], + twin: [ + { id: 'twin::wt', repoId: 'twin', hostId: 'local' }, + { id: 'twin::wt', repoId: 'twin', hostId: 'ssh:box' } + ], + 'stale-hub': [{ id: 'stale-hub::wt', repoId: 'stale-hub', hostId: 'ssh:target' }], + hub: [ + { + id: 'hub::wt', + repoId: 'hub', + hostId: 'ssh:box', + runtimeOwnerEnvironmentId: 'hub-env' + } + ] + } + }, + ['twin::wt', 'stale-hub::wt', 'hub::wt', 'local-repo::wt-a'] + ) + expect(groups).toEqual([ + { hostId: 'runtime:hub-env', orderedIds: ['hub::wt'] }, + { hostId: 'local', orderedIds: ['local-repo::wt-a'] } + ]) + }) }) diff --git a/src/renderer/src/lib/worktree-sort-order-host-split.ts b/src/renderer/src/lib/worktree-sort-order-host-split.ts index b313d56b618..f9766007111 100644 --- a/src/renderer/src/lib/worktree-sort-order-host-split.ts +++ b/src/renderer/src/lib/worktree-sort-order-host-split.ts @@ -1,5 +1,6 @@ import { LOCAL_EXECUTION_HOST_ID, toRuntimeExecutionHostId } from '../../../shared/execution-host' -import { getRuntimeEnvironmentIdForWorktree } from './worktree-runtime-owner' +import { isUnprovenServerSshRoute } from './resolve-owner' +import { resolveOwnerWithLegacyFallback } from './worktree-runtime-owner' import type { WorktreeRuntimeOwnerState } from './worktree-runtime-owner-state' export type WorktreeSortOrderHostGroup = { @@ -13,7 +14,8 @@ export type WorktreeSortOrderHostGroup = { * Why: persisted `sortOrder` lives in each host's `worktreeMeta` and is enriched * onto worktrees from their owner host. Stamping the full cross-host id list on * only the focused host loses other hosts' ordering and pollutes the focused - * host with foreign ids, so persist each host's ids on that host. + * host with foreign ids, so persist each host's ids on that host. Ids no host can be + * proven to own are left out. */ export function splitWorktreeSortOrderByHost( state: WorktreeRuntimeOwnerState, @@ -21,8 +23,16 @@ export function splitWorktreeSortOrderByHost( ): WorktreeSortOrderHostGroup[] { const groups = new Map() for (const id of orderedIds) { - const environmentId = getRuntimeEnvironmentIdForWorktree(state, id) - const hostId = environmentId ? toRuntimeExecutionHostId(environmentId) : LOCAL_EXECUTION_HOST_ID + const owner = resolveOwnerWithLegacyFallback(state, { workspaceId: id }) + // Why: a twin names no host to write to, and an unproven server route may be another machine. + if (owner.kind !== 'resolved' || isUnprovenServerSshRoute(state, id, owner.owner)) { + continue + } + const { endpoint } = owner.owner + const hostId = + endpoint.kind === 'environment' + ? toRuntimeExecutionHostId(endpoint.environmentId) + : LOCAL_EXECUTION_HOST_ID const existing = groups.get(hostId) if (existing) { existing.push(id) diff --git a/src/renderer/src/runtime/web-runtime-session.test.ts b/src/renderer/src/runtime/web-runtime-session.test.ts index 7fa11e4de27..f735f9bfd5a 100644 --- a/src/renderer/src/runtime/web-runtime-session.test.ts +++ b/src/renderer/src/runtime/web-runtime-session.test.ts @@ -76,7 +76,8 @@ vi.mock('@/lib/feature-education-telemetry', () => ({ })) vi.mock('@/lib/worktree-runtime-owner', () => ({ - getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree + getRuntimeEnvironmentIdForWorktree: mocks.getRuntimeEnvironmentIdForWorktree, + runtimeEnvironmentIdWithLegacyFallbackUnlessTwin: mocks.getRuntimeEnvironmentIdForWorktree })) vi.mock('@/lib/agent-launch-prompt-delivery', () => ({ diff --git a/src/renderer/src/store/slices/tabs/tabs-host-mirroring.ts b/src/renderer/src/store/slices/tabs/tabs-host-mirroring.ts index 77b16f1e630..b81e883e4b1 100644 --- a/src/renderer/src/store/slices/tabs/tabs-host-mirroring.ts +++ b/src/renderer/src/store/slices/tabs/tabs-host-mirroring.ts @@ -1,7 +1,7 @@ import type { AppState } from '../../types' import type { TerminalTab } from '../../../../../shared/terminal-tab-types' import { findTabAndWorktree } from '../tab-group-state' -import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' +import { runtimeEnvironmentIdWithLegacyFallbackUnlessTwin } from '@/lib/worktree-runtime-owner' import { locateTerminalTab } from '../../terminals/terminal-tab-location' /** @@ -33,7 +33,7 @@ export function mirrorTabPinnedToHost(state: AppState, tabId: string, isPinned: if ( !found || found.tab.contentType !== 'terminal' || - !getRuntimeEnvironmentIdForWorktree(state, found.worktreeId) + !runtimeEnvironmentIdWithLegacyFallbackUnlessTwin(state, found.worktreeId) ) { return } @@ -55,7 +55,7 @@ export function mirrorTabViewModeToHost( if ( !found || found.tab.contentType !== 'terminal' || - !getRuntimeEnvironmentIdForWorktree(state, found.worktreeId) + !runtimeEnvironmentIdWithLegacyFallbackUnlessTwin(state, found.worktreeId) ) { return }