From b9b81fced3f19de7b8832b311a822a26798818c6 Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 6 Oct 2026 13:12:59 -0700 Subject: [PATCH 1/7] Prevent local session publication from claiming paired host tabs --- .../src/lib/terminal-tab-execution-hosts.ts | 14 + ...ime-graph-foreign-host-publication.test.ts | 276 ++++++++++++++++++ .../sync-runtime-graph/graph-publication.ts | 23 +- .../mobile-session-snapshots.ts | 36 ++- .../mobile-session-surfaces.ts | 20 +- 5 files changed, 353 insertions(+), 16 deletions(-) create mode 100644 src/renderer/src/lib/terminal-tab-execution-hosts.ts create mode 100644 src/renderer/src/runtime/sync-runtime-graph-foreign-host-publication.test.ts diff --git a/src/renderer/src/lib/terminal-tab-execution-hosts.ts b/src/renderer/src/lib/terminal-tab-execution-hosts.ts new file mode 100644 index 000000000000..e3dd33c8a116 --- /dev/null +++ b/src/renderer/src/lib/terminal-tab-execution-hosts.ts @@ -0,0 +1,14 @@ +import type { ExecutionHostId } from '../../../shared/execution-host' +import type { Tab } from '../../../shared/tab-types' + +export function indexTerminalTabExecutionHosts( + tabs: readonly Tab[] +): ReadonlyMap { + return new Map( + tabs.flatMap((tab) => + tab.contentType === 'terminal' && tab.executionHostId + ? [[tab.entityId, tab.executionHostId] as const, [tab.id, tab.executionHostId] as const] + : [] + ) + ) +} diff --git a/src/renderer/src/runtime/sync-runtime-graph-foreign-host-publication.test.ts b/src/renderer/src/runtime/sync-runtime-graph-foreign-host-publication.test.ts new file mode 100644 index 000000000000..fab4cb4fe058 --- /dev/null +++ b/src/renderer/src/runtime/sync-runtime-graph-foreign-host-publication.test.ts @@ -0,0 +1,276 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { ExecutionHostId } from '../../../shared/execution-host' +import type { RuntimeRendererSyncWindowGraph } from '../../../shared/runtime-types' +import { toRemoteRuntimePtyId } from '../../../shared/remote-runtime-pty-id' +import type { OpenFile } from '../store/slices/editor' +import { + buildMobileSessionTabSnapshots, + registerRuntimeTerminalTab, + setRuntimeGraphStoreStateGetter, + setRuntimeGraphSyncEnabled +} from './sync-runtime-graph' +import { syncRuntimeGraph } from './sync-runtime-graph/graph-publication' +import { makeState } from './sync-runtime-graph-test-harness' +import { applyWebSessionTabsSnapshot } from './web-session-tabs-sync' +import { makeSnapshot, makeState as makeMirrorState } from './web-session-tabs-sync-test-harness' + +const workspaceId = 'host-publication-workspace' +const leafId = '11111111-1111-4111-8111-111111111111' + +afterEach(() => { + setRuntimeGraphSyncEnabled(false) + setRuntimeGraphStoreStateGetter(null) + vi.unstubAllGlobals() +}) + +function editor(runtimeEnvironmentId?: string): OpenFile { + return { + id: 'host-publication-editor', + filePath: '/repo/README.md', + relativePath: 'README.md', + worktreeId: workspaceId, + language: 'markdown', + isDirty: false, + mode: 'edit', + runtimeEnvironmentId + } +} + +function terminalState(executionHostId: ExecutionHostId, ptyId: string | null = null) { + return makeState({ + tabsByWorktree: { + [workspaceId]: [ + { + id: 'host-publication-terminal', + ptyId, + worktreeId: workspaceId, + title: 'Terminal', + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 1 + } + ] + }, + unifiedTabsByWorktree: { + [workspaceId]: [ + { + id: 'host-publication-terminal', + entityId: 'host-publication-terminal', + worktreeId: workspaceId, + groupId: 'host-publication-group', + executionHostId, + contentType: 'terminal', + label: 'Terminal', + customLabel: null, + color: null, + sortOrder: 0, + createdAt: 1 + } + ] + }, + terminalLayoutsByTabId: { + 'host-publication-terminal': { + root: { type: 'leaf', leafId }, + activeLeafId: leafId, + expandedLeafId: null, + ...(ptyId ? { ptyIdsByLeafId: { [leafId]: ptyId } } : {}) + } + } + }) +} + +describe('local host session publication', () => { + it('does not advertise a foreign runtime editor as a local file', () => { + const state = makeState({ openFiles: [editor('wsl-owner')] }) + expect(buildMobileSessionTabSnapshots(state).flatMap((snapshot) => snapshot.tabs)).toEqual([]) + }) + + it('cannot turn a correctly owned editor into another host’s file on a second client', () => { + const file = editor('wsl-owner') + const publication = buildMobileSessionTabSnapshots(makeState({ openFiles: [file] }))[0]! + const editorTabs = publication.tabs.filter( + (tab) => tab.type === 'file' || tab.type === 'markdown' + ) + const initial = makeMirrorState({ activeWorktreeId: workspaceId }) + const ownedSnapshot = makeSnapshot( + [ + { + type: 'file', + id: 'owned-wsl-editor', + title: 'README.md', + filePath: file.filePath, + relativePath: file.relativePath, + language: file.language, + mode: 'edit', + isDirty: false, + isActive: true + } + ], + { worktree: workspaceId } + ) + const owned = { + ...initial, + ...applyWebSessionTabsSnapshot(initial, ownedSnapshot, 'wsl-owner') + } + const updated = { + ...owned, + ...applyWebSessionTabsSnapshot( + owned, + makeSnapshot(editorTabs, { worktree: workspaceId }), + 'mac-owner' + ) + } + expect(updated.openFiles.map((entry) => entry.runtimeEnvironmentId)).toEqual(['wsl-owner']) + expect( + updated.unifiedTabsByWorktree[ownedSnapshot.worktree]?.map((tab) => tab.executionHostId) + ).toEqual(['runtime:wsl-owner']) + }) + + it('does not advertise a cold foreign runtime terminal as local pending work', () => { + const state = terminalState('runtime:wsl-owner') + expect(buildMobileSessionTabSnapshots(state).flatMap((snapshot) => snapshot.tabs)).toEqual([]) + }) + + it('withholds a legacy editor whose canonical tab proves a foreign owner', () => { + const file = editor() + const state = terminalState('local') + const terminalTab = state.unifiedTabsByWorktree[workspaceId]![0]! + state.tabsByWorktree = {} + state.openFiles = [file] + state.unifiedTabsByWorktree = { + [workspaceId]: [ + { + ...terminalTab, + id: file.id, + entityId: file.id, + executionHostId: 'runtime:wsl-owner', + contentType: 'editor' + } + ] + } + expect(buildMobileSessionTabSnapshots(state).flatMap((snapshot) => snapshot.tabs)).toEqual([]) + expect(state.openFiles).toEqual([file]) + }) + + it('does not advertise a legacy foreign PTY without a mirrored-tab prefix', () => { + const state = terminalState('local', toRemoteRuntimePtyId('terminal-handle', 'wsl-owner')) + expect(buildMobileSessionTabSnapshots(state).flatMap((snapshot) => snapshot.tabs)).toEqual([]) + }) + + it('still advertises local editors and cold terminals', () => { + expect( + buildMobileSessionTabSnapshots(makeState({ openFiles: [editor()] })).flatMap( + (snapshot) => snapshot.tabs + ) + ).toHaveLength(1) + expect( + buildMobileSessionTabSnapshots(terminalState('local')).flatMap((snapshot) => snapshot.tabs) + ).toHaveLength(1) + }) + + it('still advertises direct SSH terminals owned through this host', () => { + expect( + buildMobileSessionTabSnapshots(terminalState('ssh:ssh-target', 'ssh-pty-1')).flatMap( + (snapshot) => snapshot.tabs + ) + ).toHaveLength(1) + }) + + it('still advertises direct SSH editors', () => { + const file = { ...editor(), externalSshTargetId: 'ssh-target' } + expect( + buildMobileSessionTabSnapshots(makeState({ openFiles: [file] })).flatMap( + (snapshot) => snapshot.tabs + ) + ).toHaveLength(1) + }) + + it('filters foreign editors in live folder workspaces without changing their drafts', () => { + const folderKey = 'folder:host-publication-folder' + const file = { ...editor('wsl-owner'), worktreeId: folderKey, isDirty: true } + const drafts = { [file.id]: 'unsaved work on the WSL host' } + const state = makeState({ + openFiles: [file], + editorDrafts: drafts, + folderWorkspaces: [ + { + id: 'host-publication-folder', + projectGroupId: 'group', + name: 'Folder', + folderPath: '/repo', + linkedTask: null, + comment: '', + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: 1, + createdAt: 1, + updatedAt: 1 + } + ] + }) + const snapshots = buildMobileSessionTabSnapshots(state) + expect(snapshots).toHaveLength(1) + expect(snapshots[0]?.worktree).toBe(folderKey) + expect(snapshots[0]?.tabs).toEqual([]) + expect(state.openFiles).toEqual([file]) + expect(state.editorDrafts).toBe(drafts) + expect(state.editorDrafts[file.id]).toBe('unsaved work on the WSL host') + const local = { ...state, openFiles: [{ ...file, runtimeEnvironmentId: null }] } + expect(buildMobileSessionTabSnapshots(local)[0]?.tabs).toHaveLength(1) + }) + + it('rebuilds the inventory when only the captured terminal owner changes', () => { + const local = terminalState('local') + const initial = buildMobileSessionTabSnapshots(local)[0]! + const foreign = { + ...local, + unifiedTabsByWorktree: terminalState('runtime:wsl-owner').unifiedTabsByWorktree + } + const changed = buildMobileSessionTabSnapshots(foreign)[0]! + expect(initial.tabs).toHaveLength(1) + expect(changed.tabs).toEqual([]) + expect(changed.snapshotVersion).toBeGreaterThan(initial.snapshotVersion) + }) + + it.each(['local', 'runtime:wsl-owner', 'ssh:ssh-target'] as const)( + 'publishes mounted graph authority only for local-managed %s terminals', + async (owner) => { + const graphs: RuntimeRendererSyncWindowGraph[] = [] + vi.stubGlobal('HTMLElement', class HTMLElement {}) + vi.stubGlobal('window', { + api: { + runtime: { + syncWindowGraph: async (graph: RuntimeRendererSyncWindowGraph) => { + graphs.push(graph) + return {} + } + } + } + }) + const state = terminalState(owner) + setRuntimeGraphStoreStateGetter(() => state) + setRuntimeGraphSyncEnabled(true) + const unregister = registerRuntimeTerminalTab({ + tabId: 'host-publication-terminal', + worktreeId: workspaceId, + getManager: () => null, + getContainer: () => null, + getPtyIdForPane: () => null, + getTabWideAgentHintLeafId: () => null + }) + try { + await syncRuntimeGraph() + expect(graphs).toHaveLength(1) + expect(graphs[0]?.tabs).toHaveLength(owner === 'runtime:wsl-owner' ? 0 : 1) + expect(graphs[0]?.mobileSessionTabs?.flatMap((snapshot) => snapshot.tabs)).toHaveLength( + owner === 'runtime:wsl-owner' ? 0 : 1 + ) + } finally { + unregister() + } + } + ) +}) diff --git a/src/renderer/src/runtime/sync-runtime-graph/graph-publication.ts b/src/renderer/src/runtime/sync-runtime-graph/graph-publication.ts index f74c97f6e410..c137ee9a6c9d 100644 --- a/src/renderer/src/runtime/sync-runtime-graph/graph-publication.ts +++ b/src/renderer/src/runtime/sync-runtime-graph/graph-publication.ts @@ -6,6 +6,8 @@ import { } from '@/components/terminal-pane/terminal-parked-watcher-registry' import { serializePaneTree } from '@/components/terminal-pane/layout-serialization' import { getSystemPrefersDark } from '@/lib/terminal-theme' +import { indexTerminalTabExecutionHosts } from '@/lib/terminal-tab-execution-hosts' +import type { ExecutionHostId } from '../../../../shared/execution-host' import type { RuntimeMobileSessionTabsSnapshot, RuntimeRendererSyncWindowGraph @@ -15,7 +17,7 @@ import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { applyNativeChatLaunchDraftResolved } from '../native-chat-launch-draft-runtime-resolution' import { resolveTerminalLayoutRoot } from '../remote-terminal-layout-resolution' import { buildMobileSessionTabSnapshots } from './mobile-session-snapshots' -import { isWebOnlyMirroredTerminalTab } from './mobile-session-surfaces' +import { isRemoteRuntimeTerminalTab } from './mobile-session-surfaces' import { resolveRuntimeTerminalTitle } from './sync-projections' import { collectAmbiguousTerminalTabIds, @@ -34,7 +36,12 @@ export async function syncRuntimeGraph(): Promise { const systemPrefersDark = getSystemPrefersDark() const ambiguousTerminalTabIds = collectAmbiguousTerminalTabIds(state.tabsByWorktree) const terminalTabsByWorktree = new Map>() + const terminalHostsByWorktree = new Map>() for (const [worktreeId, tabs] of Object.entries(state.tabsByWorktree)) { + terminalHostsByWorktree.set( + worktreeId, + indexTerminalTabExecutionHosts(state.unifiedTabsByWorktree[worktreeId] ?? []) + ) const tabsById = new Map() for (const tab of tabs) { // Duplicate ids in one worktree are malformed persisted state; don't @@ -70,7 +77,13 @@ export async function syncRuntimeGraph(): Promise { if (!tab) { continue } - if (isWebOnlyMirroredTerminalTab(tab, state.terminalLayoutsByTabId[registeredTab.tabId])) { + if ( + isRemoteRuntimeTerminalTab( + tab, + state.terminalLayoutsByTabId[registeredTab.tabId], + terminalHostsByWorktree.get(registeredTab.worktreeId)?.get(tab.id) + ) + ) { continue } const manager = registeredTab.getManager() @@ -129,7 +142,11 @@ export async function syncRuntimeGraph(): Promise { const layout = state.terminalLayoutsByTabId[tab.id] if ( findRegisteredTerminalTab(tab.id, worktreeId) !== null || - isWebOnlyMirroredTerminalTab(tab, layout) + isRemoteRuntimeTerminalTab( + tab, + layout, + terminalHostsByWorktree.get(worktreeId)?.get(tab.id) + ) ) { continue } diff --git a/src/renderer/src/runtime/sync-runtime-graph/mobile-session-snapshots.ts b/src/renderer/src/runtime/sync-runtime-graph/mobile-session-snapshots.ts index 7dc106a6cd45..7a92ee71fba6 100644 --- a/src/renderer/src/runtime/sync-runtime-graph/mobile-session-snapshots.ts +++ b/src/renderer/src/runtime/sync-runtime-graph/mobile-session-snapshots.ts @@ -1,4 +1,6 @@ import { getSystemPrefersDark } from '@/lib/terminal-theme' +import { indexTerminalTabExecutionHosts } from '@/lib/terminal-tab-execution-hosts' +import { parseExecutionHostId } from '../../../../shared/execution-host' import type { AppState } from '@/store/types' import type { RuntimeMobileSessionSnapshotTab, @@ -21,7 +23,7 @@ import { getMobileTerminalTheme } from './mobile-terminal-theme' import { isMobilePublishableBrowserWorkspace, isMobilePublishableOpenFile, - isWebOnlyMirroredTerminalTab + isRemoteRuntimeTerminalTab } from './mobile-session-surfaces' import { appendFallbackEditorTabsToGroups, @@ -91,15 +93,31 @@ export function buildMobileSessionTabSnapshots( inputs.browserWorkspaces.map((workspace) => [workspace.id, workspace]) ) const unifiedTabById = new Map(inputs.unifiedTabs.map((tab) => [tab.id, tab])) + const terminalHostById = indexTerminalTabExecutionHosts(inputs.unifiedTabs) + const foreignEditorFileIds = new Set( + inputs.unifiedTabs + .filter( + (tab) => + (tab.contentType === 'editor' || tab.contentType === 'diff') && + parseExecutionHostId(tab.executionHostId)?.kind === 'runtime' + ) + .map((tab) => tab.entityId) + ) + const isPublishableFile = (file: AppState['openFiles'][number]): boolean => + !foreignEditorFileIds.has(file.id) && isMobilePublishableOpenFile(file) const openFilesForWorktree = inputs.openFilesById const editorIds = inputs.openFileIds.filter((fileId) => { const file = openFilesForWorktree?.get(fileId) - return file ? isMobilePublishableOpenFile(file) : false + return file ? isPublishableFile(file) : false }) const terminalIds = [...terminalTabById.values()] .filter( (terminal) => - !isWebOnlyMirroredTerminalTab(terminal, inputs.terminalLayoutByTabId.get(terminal.id)) + !isRemoteRuntimeTerminalTab( + terminal, + inputs.terminalLayoutByTabId.get(terminal.id), + terminalHostById.get(terminal.id) + ) ) .map((terminal) => terminal.id) const groupProjection = buildMobileSessionGroupProjection(inputs, { @@ -118,14 +136,18 @@ export function buildMobileSessionTabSnapshots( const terminal = terminalTabById.get(item.id) if ( !terminal || - isWebOnlyMirroredTerminalTab(terminal, inputs.terminalLayoutByTabId.get(terminal.id)) + isRemoteRuntimeTerminalTab( + terminal, + inputs.terminalLayoutByTabId.get(terminal.id), + terminalHostById.get(terminal.id) + ) ) { continue } tabs.push(...buildMobileTerminalSurfaceTabs(inputs, terminal, item.tabId)) } else if (item.type === 'editor') { const file = openFilesForWorktree?.get(item.id) - if (!file || !isMobilePublishableOpenFile(file)) { + if (!file || !isPublishableFile(file)) { continue } const unifiedTab = item.tabId ? unifiedTabById.get(item.tabId) : undefined @@ -161,7 +183,7 @@ export function buildMobileSessionTabSnapshots( continue } const file = openFilesForWorktree.get(unifiedTab.entityId) - if (!file || !isMobilePublishableOpenFile(file)) { + if (!file || !isPublishableFile(file)) { continue } const fallbackTab = @@ -172,7 +194,7 @@ export function buildMobileSessionTabSnapshots( emittedEditorTabIds.add(unifiedTab.id) } for (const file of openFilesForWorktree.values()) { - if (!isMobilePublishableOpenFile(file) || emittedEditorFileIds.has(file.id)) { + if (!isPublishableFile(file) || emittedEditorFileIds.has(file.id)) { continue } if (unifiedEditorFileIds.has(file.id)) { diff --git a/src/renderer/src/runtime/sync-runtime-graph/mobile-session-surfaces.ts b/src/renderer/src/runtime/sync-runtime-graph/mobile-session-surfaces.ts index 216d2dd04488..2c98d4bf5640 100644 --- a/src/renderer/src/runtime/sync-runtime-graph/mobile-session-surfaces.ts +++ b/src/renderer/src/runtime/sync-runtime-graph/mobile-session-surfaces.ts @@ -1,5 +1,7 @@ import { collectLeafIdsInOrder } from '@/components/terminal-pane/layout-serialization' import type { AppState } from '@/store/types' +import { getOpenFileExecutionHostId } from '@/lib/unified-tab-host-ownership' +import { parseExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host' import { isClaudeManagementTitle } from '../../../../shared/agent-detection' import { isTerminalLeafId, makePaneKey } from '../../../../shared/stable-pane-id' import { isWebTerminalSurfaceTabId } from '../../../../shared/terminal-surface-id' @@ -20,19 +22,22 @@ export function isRemoteRuntimePtyId(ptyId: string | null | undefined): boolean return typeof ptyId === 'string' && parseRemoteRuntimePtyId(ptyId) !== null } -export function isWebOnlyMirroredTerminalTab( +export function isRemoteRuntimeTerminalTab( tab: Pick[number], 'id' | 'ptyId'>, - layout: AppState['terminalLayoutsByTabId'][string] | undefined + layout: AppState['terminalLayoutsByTabId'][string] | undefined, + executionHostId?: ExecutionHostId ): boolean { - if (!isWebTerminalSurfaceTabId(tab.id)) { - return false + if (parseExecutionHostId(executionHostId)?.kind === 'runtime') { + return true } const layoutPtyIds = Object.values(layout?.ptyIdsByLeafId ?? {}) const ptyIds = [tab.ptyId, ...layoutPtyIds].filter( (ptyId): ptyId is string => typeof ptyId === 'string' && ptyId.length > 0 ) - // Only-remote/no-PTY tabs are web mirrors; legacy local-PTY tabs still publish. - return ptyIds.every(isRemoteRuntimePtyId) + // Remote handles belong to the paired host even when a legacy tab has no mirror prefix. + return ( + ptyIds.some(isRemoteRuntimePtyId) || (isWebTerminalSurfaceTabId(tab.id) && ptyIds.length === 0) + ) } export function getRuntimeLeafIdsForTerminal( @@ -82,6 +87,9 @@ export function isMobileFileDiffSource( } export function isMobilePublishableOpenFile(file: AppState['openFiles'][number]): boolean { + if (parseExecutionHostId(getOpenFileExecutionHostId(file))?.kind === 'runtime') { + return false + } // Combined diff tabs use display labels as paths and need the desktop renderer. return !( file.diffSource === 'combined-all' || From a515672c11cdd3144ae173d57caa7ae68bdbbeac Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 6 Oct 2026 15:45:49 -0700 Subject: [PATCH 2/7] Keep paired-host SSH editors out of local publication --- ...ime-graph-foreign-host-publication.test.ts | 40 +++++++++++++++++++ .../mobile-session-surfaces.ts | 7 +++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/runtime/sync-runtime-graph-foreign-host-publication.test.ts b/src/renderer/src/runtime/sync-runtime-graph-foreign-host-publication.test.ts index fab4cb4fe058..7d8ab82c0c59 100644 --- a/src/renderer/src/runtime/sync-runtime-graph-foreign-host-publication.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph-foreign-host-publication.test.ts @@ -11,6 +11,7 @@ import { } from './sync-runtime-graph' import { syncRuntimeGraph } from './sync-runtime-graph/graph-publication' import { makeState } from './sync-runtime-graph-test-harness' +import { captureWorktreeOperationGenerationSnapshot } from '../lib/worktree-operation-generation' import { applyWebSessionTabsSnapshot } from './web-session-tabs-sync' import { makeSnapshot, makeState as makeMirrorState } from './web-session-tabs-sync-test-harness' @@ -81,6 +82,45 @@ function terminalState(executionHostId: ExecutionHostId, ptyId: string | null = } describe('local host session publication', () => { + it.each(['hub-a', 'hub-b'])( + 'does not advertise an SSH editor reached through %s', + (environmentId) => { + const file = { + ...editor(), + operationProvenance: { + ownershipProjection: 'explicit' as const, + generation: captureWorktreeOperationGenerationSnapshot({ + executionHostId: 'ssh:target', + runtimeEnvironmentId: environmentId + }) + } + } + expect( + buildMobileSessionTabSnapshots(makeState({ openFiles: [file] })).flatMap( + (snapshot) => snapshot.tabs + ) + ).toEqual([]) + } + ) + + it('publishes a captured direct SSH editor despite stale runtime metadata', () => { + const file = { + ...editor('stale-hub'), + operationProvenance: { + ownershipProjection: 'explicit' as const, + generation: captureWorktreeOperationGenerationSnapshot({ + executionHostId: 'ssh:target', + runtimeEnvironmentId: null + }) + } + } + expect( + buildMobileSessionTabSnapshots(makeState({ openFiles: [file] })).flatMap( + (snapshot) => snapshot.tabs + ) + ).toHaveLength(1) + }) + it('does not advertise a foreign runtime editor as a local file', () => { const state = makeState({ openFiles: [editor('wsl-owner')] }) expect(buildMobileSessionTabSnapshots(state).flatMap((snapshot) => snapshot.tabs)).toEqual([]) diff --git a/src/renderer/src/runtime/sync-runtime-graph/mobile-session-surfaces.ts b/src/renderer/src/runtime/sync-runtime-graph/mobile-session-surfaces.ts index 2c98d4bf5640..de3e0c050c16 100644 --- a/src/renderer/src/runtime/sync-runtime-graph/mobile-session-surfaces.ts +++ b/src/renderer/src/runtime/sync-runtime-graph/mobile-session-surfaces.ts @@ -87,7 +87,12 @@ export function isMobileFileDiffSource( } export function isMobilePublishableOpenFile(file: AppState['openFiles'][number]): boolean { - if (parseExecutionHostId(getOpenFileExecutionHostId(file))?.kind === 'runtime') { + const captured = file.operationProvenance?.generation.route + const environmentId = captured ? captured.runtimeEnvironmentId : file.runtimeEnvironmentId + if ( + environmentId?.trim() || + parseExecutionHostId(getOpenFileExecutionHostId(file))?.kind === 'runtime' + ) { return false } // Combined diff tabs use display labels as paths and need the desktop renderer. From 65157fcbbc25bff0c0ec55b9f94b553e3130c08f Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 6 Oct 2026 15:50:49 -0700 Subject: [PATCH 3/7] test: route new orchestration SQLite fixtures through Node --- config/scripts/vitest-sqlite-database-runtime-files.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/scripts/vitest-sqlite-database-runtime-files.mjs b/config/scripts/vitest-sqlite-database-runtime-files.mjs index 9098a6f566db..cec9f43adefd 100644 --- a/config/scripts/vitest-sqlite-database-runtime-files.mjs +++ b/config/scripts/vitest-sqlite-database-runtime-files.mjs @@ -1,5 +1,7 @@ // Database opening and lifecycle fixtures exercise Node SQLite behavior. export const SQLITE_DATABASE_RUNTIME_INCLUDE = [ + 'src/main/runtime/orchestration/orchestration-party-location.test.ts', + 'src/main/runtime/runtime-orchestration-sender-names.test.ts', 'src/main/ai-vault-search/session-search-file-write.test.ts', 'src/main/ai-vault-search/session-search-live-transcript.test.ts', 'src/main/ai-vault/session-scanner-devin-contention.test.ts', From e9666b55bb8f91878ab456ed820b4a6fba4a031e Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 6 Oct 2026 17:44:44 -0700 Subject: [PATCH 4/7] Use upstream SQLite runtime registrations without duplicates --- config/scripts/vitest-sqlite-database-runtime-files.mjs | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/scripts/vitest-sqlite-database-runtime-files.mjs b/config/scripts/vitest-sqlite-database-runtime-files.mjs index cec9f43adefd..9098a6f566db 100644 --- a/config/scripts/vitest-sqlite-database-runtime-files.mjs +++ b/config/scripts/vitest-sqlite-database-runtime-files.mjs @@ -1,7 +1,5 @@ // Database opening and lifecycle fixtures exercise Node SQLite behavior. export const SQLITE_DATABASE_RUNTIME_INCLUDE = [ - 'src/main/runtime/orchestration/orchestration-party-location.test.ts', - 'src/main/runtime/runtime-orchestration-sender-names.test.ts', 'src/main/ai-vault-search/session-search-file-write.test.ts', 'src/main/ai-vault-search/session-search-live-transcript.test.ts', 'src/main/ai-vault/session-scanner-devin-contention.test.ts', From 8e4ce19034b9f4cb8817f441e41b5ded3dcde7f1 Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 6 Oct 2026 18:09:32 -0700 Subject: [PATCH 5/7] Preserve ambiguous terminal execution hosts in the ownership index --- .../src/lib/terminal-tab-execution-hosts.ts | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/renderer/src/lib/terminal-tab-execution-hosts.ts b/src/renderer/src/lib/terminal-tab-execution-hosts.ts index e3dd33c8a116..61c76e5e0e86 100644 --- a/src/renderer/src/lib/terminal-tab-execution-hosts.ts +++ b/src/renderer/src/lib/terminal-tab-execution-hosts.ts @@ -3,12 +3,20 @@ import type { Tab } from '../../../shared/tab-types' export function indexTerminalTabExecutionHosts( tabs: readonly Tab[] -): ReadonlyMap { - return new Map( - tabs.flatMap((tab) => - tab.contentType === 'terminal' && tab.executionHostId - ? [[tab.entityId, tab.executionHostId] as const, [tab.id, tab.executionHostId] as const] - : [] - ) - ) +): ReadonlyMap { + const hosts = new Map() + for (const tab of tabs) { + if (tab.contentType !== 'terminal' || !tab.executionHostId) { + continue + } + for (const id of [tab.entityId, tab.id]) { + const existing = hosts.get(id) + // Conflicting persisted owners remain ambiguous regardless of row order. + hosts.set( + id, + existing === undefined || existing === tab.executionHostId ? tab.executionHostId : null + ) + } + } + return hosts } From 35dc91f76493adf0f842be6efa98a02e7994d284 Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 6 Oct 2026 18:09:53 -0700 Subject: [PATCH 6/7] Withhold terminals whose persisted execution hosts conflict --- ...untime-graph-foreign-host-publication.test.ts | 16 ++++++++++++++++ .../sync-runtime-graph/graph-publication.ts | 2 +- .../mobile-session-surfaces.ts | 4 ++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/runtime/sync-runtime-graph-foreign-host-publication.test.ts b/src/renderer/src/runtime/sync-runtime-graph-foreign-host-publication.test.ts index 7d8ab82c0c59..eca80387e0b6 100644 --- a/src/renderer/src/runtime/sync-runtime-graph-foreign-host-publication.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph-foreign-host-publication.test.ts @@ -172,6 +172,22 @@ describe('local host session publication', () => { expect(buildMobileSessionTabSnapshots(state).flatMap((snapshot) => snapshot.tabs)).toEqual([]) }) + it.each([true, false])( + 'withholds contradictory terminal owners in either row order (%s)', + (reverse) => { + const state = terminalState('runtime:wsl-owner') + const original = state.unifiedTabsByWorktree[workspaceId]![0]! + const rows = [ + original, + { ...original, id: 'legacy-local-alias', executionHostId: 'local' as const } + ] + state.unifiedTabsByWorktree = { [workspaceId]: reverse ? rows.toReversed() : rows } + + expect(buildMobileSessionTabSnapshots(state).flatMap((snapshot) => snapshot.tabs)).toEqual([]) + expect(state.tabsByWorktree[workspaceId]).toHaveLength(1) + } + ) + it('withholds a legacy editor whose canonical tab proves a foreign owner', () => { const file = editor() const state = terminalState('local') diff --git a/src/renderer/src/runtime/sync-runtime-graph/graph-publication.ts b/src/renderer/src/runtime/sync-runtime-graph/graph-publication.ts index c137ee9a6c9d..cd621ad5db2f 100644 --- a/src/renderer/src/runtime/sync-runtime-graph/graph-publication.ts +++ b/src/renderer/src/runtime/sync-runtime-graph/graph-publication.ts @@ -36,7 +36,7 @@ export async function syncRuntimeGraph(): Promise { const systemPrefersDark = getSystemPrefersDark() const ambiguousTerminalTabIds = collectAmbiguousTerminalTabIds(state.tabsByWorktree) const terminalTabsByWorktree = new Map>() - const terminalHostsByWorktree = new Map>() + const terminalHostsByWorktree = new Map>() for (const [worktreeId, tabs] of Object.entries(state.tabsByWorktree)) { terminalHostsByWorktree.set( worktreeId, diff --git a/src/renderer/src/runtime/sync-runtime-graph/mobile-session-surfaces.ts b/src/renderer/src/runtime/sync-runtime-graph/mobile-session-surfaces.ts index de3e0c050c16..a2801ff44b7a 100644 --- a/src/renderer/src/runtime/sync-runtime-graph/mobile-session-surfaces.ts +++ b/src/renderer/src/runtime/sync-runtime-graph/mobile-session-surfaces.ts @@ -25,9 +25,9 @@ export function isRemoteRuntimePtyId(ptyId: string | null | undefined): boolean export function isRemoteRuntimeTerminalTab( tab: Pick[number], 'id' | 'ptyId'>, layout: AppState['terminalLayoutsByTabId'][string] | undefined, - executionHostId?: ExecutionHostId + executionHostId?: ExecutionHostId | null ): boolean { - if (parseExecutionHostId(executionHostId)?.kind === 'runtime') { + if (executionHostId === null || parseExecutionHostId(executionHostId)?.kind === 'runtime') { return true } const layoutPtyIds = Object.values(layout?.ptyIdsByLeafId ?? {}) From 4bd05b5353a3fddecc1bdccf1cbd94a1720b5e99 Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 6 Oct 2026 18:20:38 -0700 Subject: [PATCH 7/7] Run the upstream agent launch database fixture in Node --- config/scripts/vitest-sqlite-database-runtime-files.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/config/scripts/vitest-sqlite-database-runtime-files.mjs b/config/scripts/vitest-sqlite-database-runtime-files.mjs index 9098a6f566db..e837c26efb27 100644 --- a/config/scripts/vitest-sqlite-database-runtime-files.mjs +++ b/config/scripts/vitest-sqlite-database-runtime-files.mjs @@ -34,6 +34,7 @@ export const SQLITE_DATABASE_RUNTIME_INCLUDE = [ 'src/main/runtime/claude-structured-session-integration.test.ts', 'src/main/runtime/claude-structured-startup-fault-is-not-an-exit.test.ts', 'src/main/runtime/exit-provenance-audit.test.ts', + 'src/main/runtime/rpc/methods/agent-launch-instant-tab.test.ts', 'src/main/runtime/orchestration-cli-subprocess.test.ts', 'src/main/runtime/orchestration-dispatch-mailbox-delivery.test.ts', 'src/main/runtime/orchestration-mailbox-crash-recovery.test.ts',