From 6c40f9190d220d89a671dacecb9257808cabffd4 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Mon, 19 Oct 2026 01:48:21 -0400 Subject: [PATCH] Editor files that share a path across two hosts: the tab lookup, the load report and the phone close match the right file (dark, layout-owner build only) (#30569) * fix(layout): two owners' files at one path keep their own host, load report entry and phone retire (layout-owner build only) The bare-path tab's file lookup took the first record of its path, the load report keyed file records by path alone, and a headless host's phone retire found and removed records by path. * fix(layout): a path-id preview tab's flag stays on its own owner's record, and a stale phone id retires nothing (layout-owner build only) --- .../mobile-session-editor-projection.test.ts | 78 +++++++++++++++++++ .../mobile-session-editor-projection.ts | 55 ++++++++++--- ...workspace-layout-editor-file-owner.test.ts | 64 +++++++++++++++ .../workspace-layout-editor-tab-file.ts | 32 +++++--- .../workspace-layout-load-records.ts | 5 +- .../workspace-layout-load-report.ts | 3 +- .../workspace-layout-owned-editor-id.test.ts | 11 +-- .../workspace-layout-published.ts | 7 +- .../workspace-layout-save-tabs.ts | 8 +- .../workspace-layout/workspace-layout-save.ts | 6 +- .../workspace-layout-tab-host.ts | 16 ++-- 11 files changed, 235 insertions(+), 50 deletions(-) diff --git a/src/main/runtime/mobile-session-editor-projection.test.ts b/src/main/runtime/mobile-session-editor-projection.test.ts index b0a2803d984..c3417c56f08 100644 --- a/src/main/runtime/mobile-session-editor-projection.test.ts +++ b/src/main/runtime/mobile-session-editor-projection.test.ts @@ -95,3 +95,81 @@ describe('headless editor tabs under an owned id', () => { ]) }) }) + +const ENV = 'env-1' + +/** A runtime-environment and a local file at one path: the runtime one listed first and owned. */ +function twoOwnerSession(): WorkspaceSessionState { + const base = ownedSession() + const file = { filePath: FILE, relativePath: 'notes.ts', worktreeId: WT, language: 'typescript' } + const tab = base.unifiedTabs![WT]![0]! + return { + ...base, + openFilesByWorktree: { + [WT]: [{ ...file, runtimeEnvironmentId: ENV, dirtyDraftContent: 'runtime draft' }, file] + }, + unifiedTabs: { + [WT]: [ + { ...tab, id: 'tab-local', entityId: FILE, color: null, isPinned: false }, + { ...tab, id: 'tab-runtime', entityId: ownedEditorFileId(FILE, WT, ENV) } + ] + }, + tabGroups: { + [WT]: [ + { + id: 'group-1', + worktreeId: WT, + activeTabId: 'tab-local', + tabOrder: ['tab-local', 'tab-runtime'] + } + ] + }, + activeTabIdByWorktree: { [WT]: 'tab-local' }, + activeTabTypeByWorktree: { [WT]: 'editor' }, + activeFileIdByWorktree: { [WT]: FILE } + } +} + +describe('two owners of one path on a headless host, layout-owner build', () => { + it("lists each record under its own owner's tab", () => { + expect(buildHeadlessMobileSessionEditorTabs(WT, twoOwnerSession())).toEqual([ + expect.objectContaining({ id: 'tab-runtime', isDirty: true, isActive: false }), + expect.objectContaining({ id: 'tab-local', isDirty: false, isActive: true }) + ]) + }) + + it('retiring the runtime tab keeps the local record, its tab and the active file', () => { + const retired = retireHeadlessEditorTab( + twoOwnerSession(), + WT, + { id: 'tab-runtime', filePath: FILE }, + true + ) + + expect(retired?.openFilesByWorktree?.[WT]).toEqual([ + twoOwnerSession().openFilesByWorktree?.[WT]?.[1] + ]) + expect(retired?.unifiedTabs?.[WT]?.map((tab) => tab.id)).toEqual(['tab-local']) + expect(retired?.activeFileIdByWorktree?.[WT]).toBe(FILE) + expect(retired?.activeTabIdByWorktree?.[WT]).toBe('tab-local') + }) + + it("retiring the local tab ignores the runtime record's draft and keeps that record", () => { + const retired = retireHeadlessEditorTab(twoOwnerSession(), WT, { + id: 'tab-local', + filePath: FILE + }) + + expect(retired?.openFilesByWorktree?.[WT]?.map((file) => file.runtimeEnvironmentId)).toEqual([ + ENV + ]) + expect(retired?.unifiedTabs?.[WT]?.map((tab) => tab.id)).toEqual(['tab-runtime']) + expect(retired?.activeFileIdByWorktree?.[WT]).toBeNull() + }) + + it('retires nothing for a tab id neither owner is listed under', () => { + expect( + retireHeadlessEditorTab(twoOwnerSession(), WT, { id: 'tab-gone', filePath: FILE }, true) + ).toBeNull() + }) +}) diff --git a/src/main/runtime/mobile-session-editor-projection.ts b/src/main/runtime/mobile-session-editor-projection.ts index f44dae89398..48375b6ba38 100644 --- a/src/main/runtime/mobile-session-editor-projection.ts +++ b/src/main/runtime/mobile-session-editor-projection.ts @@ -9,7 +9,11 @@ import type { RuntimeMobileSessionTabsSnapshot } from '../../shared/runtime-types' import type { Tab } from '../../shared/tab-types' -import { editorTabShowsFile } from '../../shared/workspace-layout/workspace-layout-editor-tab-file' +import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch' +import { + editorTabNamesFile, + sameEditorFile +} from '../../shared/workspace-layout/workspace-layout-editor-tab-file' import type { PersistedOpenFile, WorkspaceSessionState @@ -17,16 +21,30 @@ import type { export type HeadlessEditorTab = RuntimeMobileSessionMarkdownTab | RuntimeMobileSessionFileTab +function editorWorkspace(session: WorkspaceSessionState, worktreeId: string) { + return { + worktreeId, + tabs: session.unifiedTabs?.[worktreeId] ?? [], + editorFiles: session.openFilesByWorktree?.[worktreeId] + } +} + function editorUnifiedTab( session: WorkspaceSessionState, worktreeId: string, file: PersistedOpenFile ): Tab | undefined { - return (session.unifiedTabs?.[worktreeId] ?? []).find( - (tab) => tab.contentType === 'editor' && editorTabShowsFile(tab.entityId, file, worktreeId) + const workspace = editorWorkspace(session, worktreeId) + return workspace.tabs.find( + (tab) => tab.contentType === 'editor' && editorTabNamesFile(workspace, tab.entityId, file) ) } +/** A file's phone tab id: its unified tab's, else its path. */ +function listedTabId(unifiedTab: Tab | undefined, file: PersistedOpenFile): string { + return unifiedTab?.id ?? file.filePath +} + function editorTab( session: WorkspaceSessionState, worktreeId: string, @@ -35,8 +53,7 @@ function editorTab( const unifiedTab = editorUnifiedTab(session, worktreeId, file) const activeFileId = session.activeFileIdByWorktree?.[worktreeId] const common = { - // A file's id is its path, which is also the unified tab's entity. - id: unifiedTab?.id ?? file.filePath, + id: listedTabId(unifiedTab, file), title: file.relativePath.split(/[\\/]/).pop() || file.relativePath || 'File', filePath: file.filePath, relativePath: file.relativePath, @@ -44,7 +61,7 @@ function editorTab( isActive: session.activeTabTypeByWorktree?.[worktreeId] === 'editor' && !!activeFileId && - editorTabShowsFile(activeFileId, file, worktreeId), + editorTabNamesFile(editorWorkspace(session, worktreeId), activeFileId, file), color: unifiedTab?.color ?? null, isPinned: unifiedTab?.isPinned === true } @@ -74,6 +91,23 @@ export function buildHeadlessMobileSessionEditorTabs( ) } +/** The record the phone's tab names: two owners can hold one path, so by the id it was listed under. */ +function listedFile( + files: readonly PersistedOpenFile[], + session: WorkspaceSessionState, + worktreeId: string, + tab: Pick +): PersistedOpenFile | undefined { + const samePath = files.filter((candidate) => candidate.filePath === tab.filePath) + // Why no fallback for two: a stale id would retire the other owner's record. + return samePath.length < 2 + ? samePath[0] + : samePath.find( + (candidate) => + listedTabId(editorUnifiedTab(session, worktreeId, candidate), candidate) === tab.id + ) +} + /** The session without one persisted editor tab, or null when it holds no such tab. */ export function retireHeadlessEditorTab( session: WorkspaceSessionState, @@ -82,7 +116,9 @@ export function retireHeadlessEditorTab( force = false ): WorkspaceSessionState | null { const files = session.openFilesByWorktree?.[worktreeId] ?? [] - const file = files.find((candidate) => candidate.filePath === tab.filePath) + const file = ORCA_LAYOUT_OWNER_IS_MAIN + ? listedFile(files, session, worktreeId, tab) + : files.find((candidate) => candidate.filePath === tab.filePath) if (!file) { return null } @@ -100,12 +136,13 @@ export function retireHeadlessEditorTab( ...(group.recentTabIds ? { recentTabIds: withoutTab(group.recentTabIds) } : {}) })) const activeFileId = session.activeFileIdByWorktree?.[worktreeId] - const wasActiveFile = !!activeFileId && editorTabShowsFile(activeFileId, file, worktreeId) + const wasActiveFile = + !!activeFileId && editorTabNamesFile(editorWorkspace(session, worktreeId), activeFileId, file) return { ...session, openFilesByWorktree: { ...session.openFilesByWorktree, - [worktreeId]: files.filter((file) => file.filePath !== tab.filePath) + [worktreeId]: files.filter((candidate) => !sameEditorFile(candidate, file)) }, unifiedTabs: { ...session.unifiedTabs, diff --git a/src/shared/workspace-layout/workspace-layout-editor-file-owner.test.ts b/src/shared/workspace-layout/workspace-layout-editor-file-owner.test.ts index 85072bdc42d..3acc0df0071 100644 --- a/src/shared/workspace-layout/workspace-layout-editor-file-owner.test.ts +++ b/src/shared/workspace-layout/workspace-layout-editor-file-owner.test.ts @@ -9,8 +9,11 @@ import type { PersistedOpenFile, WorkspaceSessionState } from '../workspace-sess import { applyLayoutCommand } from './workspace-layout-commands' import type { CommandOf } from './workspace-layout-command-types' import { build, testContext } from './workspace-layout-command.test-fixture' +import { editorTabFile } from './workspace-layout-editor-tab-file' import { loadWorkspaceLayout } from './workspace-layout-load' +import { diffStoredSession } from './workspace-layout-load-report' import type { LayoutEditorFile, WorkspaceLayoutModel } from './workspace-layout-model' +import { publishWorkspaceLayout } from './workspace-layout-published' import { patchStoredSession } from './workspace-layout-record-patch' import { saveWorkspaceLayout } from './workspace-layout-save' import { addWorkspace, emptySession, GIT_KEY } from './workspace-layout-session.test-fixture' @@ -189,6 +192,67 @@ describe('editor file records keyed by path and owner, layout-owner build', () = expect(patched.openFilesByWorktree?.[FLOATING]).toEqual([local, runtime]) }) + it('a path-id tab names the local record when the runtime record is listed first', () => { + const { model } = build(testContext(), [ + open(GIT_KEY, runtimeId(GIT_KEY), RUNTIME_FILE), + open(GIT_KEY, PATH, LOCAL_FILE) + ]) + const workspace = model.workspaces[GIT_KEY]! + const hostOf = (entityId: string) => + publishWorkspaceLayout(workspace, LOCAL_EXECUTION_HOST_ID).tabs.find( + (tab) => tab.entityId === entityId + )?.executionHostId + + expect(owners(model, GIT_KEY)).toEqual([ENV, 'local']) + expect(editorTabFile(workspace, PATH)?.runtimeEnvironmentId).toBeUndefined() + expect(hostOf(PATH)).toBe(LOCAL_EXECUTION_HOST_ID) + expect(hostOf(runtimeId(GIT_KEY))).toBe(`runtime:${ENV}`) + }) + + it("the load report sees a change to one owner's record of a shared path", () => { + const local = { ...LOCAL_FILE, worktreeId: FLOATING } + const runtime = { ...RUNTIME_FILE, worktreeId: FLOATING } + const session = (files: PersistedOpenFile[]): WorkspaceSessionState => ({ + ...emptySession(), + openFilesByWorktree: { [FLOATING]: files } + }) + + const changes = diffStoredSession(session([local, runtime]), session([runtime])) + + expect(changes.filter((change) => change.table === 'file')).toEqual([ + expect.objectContaining({ field: '*', before: local, after: undefined }) + ]) + }) + + it("a reload keeps a path-id preview tab's flag on its own owner's record only", () => { + const stored: WorkspaceSessionState = addWorkspace(emptySession(), GIT_KEY, [ + { + id: 'group-1', + tabs: [ + { id: PATH, kind: 'editor', entry: { isPreview: true } }, + { id: runtimeId(GIT_KEY), kind: 'editor' } + ] + } + ]) + stored.openFilesByWorktree = { + [GIT_KEY]: [ + { ...RUNTIME_FILE, worktreeId: GIT_KEY }, + { ...LOCAL_FILE, worktreeId: GIT_KEY, isPreview: true } + ] + } + let id = 0 + const loaded = loadWorkspaceLayout(LOCAL_EXECUTION_HOST_ID, stored, { + mintId: () => `minted-${++id}`, + mintLeafId: () => `00000000-0000-4000-8000-${String(++id).padStart(12, '0')}` + }) + const tabs = loaded.layout.workspaces[GIT_KEY]!.tabs + + expect(tabs.map((tab) => tab.isPreview)).toEqual([true, undefined]) + expect(saveWorkspaceLayout(loaded).openFilesByWorktree?.[GIT_KEY]).toEqual( + stored.openFilesByWorktree[GIT_KEY] + ) + }) + it('match by path alone when the switch is off, as a headless host does today', () => { mocks.layoutOwnerIsMain = false const { model } = bothOpen(FLOATING, localId(FLOATING)) diff --git a/src/shared/workspace-layout/workspace-layout-editor-tab-file.ts b/src/shared/workspace-layout/workspace-layout-editor-tab-file.ts index 6c35805dca7..13b59e8bde6 100644 --- a/src/shared/workspace-layout/workspace-layout-editor-tab-file.ts +++ b/src/shared/workspace-layout/workspace-layout-editor-tab-file.ts @@ -42,21 +42,35 @@ export function holdsEditorFileId( ) } -export function editorTabFile( - workspace: { worktreeId: string; editorFiles?: readonly F[] }, - entityId: string -): F | undefined { - return workspace.editorFiles?.find((file) => - editorTabShowsFile(entityId, file, workspace.worktreeId) - ) -} - type EditorWorkspace = { worktreeId: string tabs: readonly { entityId: string }[] editorFiles?: readonly F[] } +/** The record an editor tab shows; in a layout-owner build a bare path skips owned-id tabs' records. */ +export function editorTabFile( + workspace: EditorWorkspace, + entityId: string +): F | undefined { + return ORCA_LAYOUT_OWNER_IS_MAIN + ? editorTabFiles(workspace, entityId)[0] + : workspace.editorFiles?.find((file) => + editorTabShowsFile(entityId, file, workspace.worktreeId) + ) +} + +/** Whether an editor file id names this record, as `editorTabFile` resolves it. */ +export function editorTabNamesFile( + workspace: EditorWorkspace, + entityId: string, + file: F +): boolean { + return ORCA_LAYOUT_OWNER_IS_MAIN + ? editorTabFile(workspace, entityId) === file + : editorTabShowsFile(entityId, file, workspace.worktreeId) +} + /** * The records an editor tab shows. The window gives a path's bare id to one owner only, so a bare * id that matches several records names those no owned-id tab claims. diff --git a/src/shared/workspace-layout/workspace-layout-load-records.ts b/src/shared/workspace-layout/workspace-layout-load-records.ts index f8316b75c63..c0e2a50d95a 100644 --- a/src/shared/workspace-layout/workspace-layout-load-records.ts +++ b/src/shared/workspace-layout/workspace-layout-load-records.ts @@ -6,7 +6,7 @@ import { EDITOR_DRAFT_FIELDS } from './workspace-layout-beside' import { childRecord, omitStoredFields, pickStoredFields } from './stored-record-fields' -import { editorFileKey, editorTabShowsFile } from './workspace-layout-editor-tab-file' +import { editorFileKey, editorTabNamesFile } from './workspace-layout-editor-tab-file' import type { WorkspaceLoadArgs } from './workspace-layout-load-types' import type { LayoutBrowserTab, LayoutEditorFile, LayoutTab } from './workspace-layout-model' @@ -51,13 +51,14 @@ export function loadEditorFiles( if (!files) { return undefined } + const workspace = { worktreeId, tabs, editorFiles: files } return files.map((file) => { const draft = pickStoredFields(file, EDITOR_DRAFT_FIELDS) if (Object.keys(draft).length > 0) { childRecord(view.editorDrafts, key)[editorFileKey(file)] = draft } const fileTabs = tabs.filter( - (tab) => tab.kind === 'editor' && editorTabShowsFile(tab.entityId, file, worktreeId) + (tab) => tab.kind === 'editor' && editorTabNamesFile(workspace, tab.entityId, file) ) // The file's copy is true when any of its tabs is preview (split groups share one file). const demoted = file.isPreview === true ? [] : fileTabs.filter((tab) => tab.isPreview) diff --git a/src/shared/workspace-layout/workspace-layout-load-report.ts b/src/shared/workspace-layout/workspace-layout-load-report.ts index b5144fd5a34..babd2d92425 100644 --- a/src/shared/workspace-layout/workspace-layout-load-report.ts +++ b/src/shared/workspace-layout/workspace-layout-load-report.ts @@ -3,6 +3,7 @@ import type { WorkspaceSessionState } from '../workspace-session-state-types' import type { DiskTable } from './workspace-layout-disk-fields' +import { editorFileKey } from './workspace-layout-editor-tab-file' import { collectLayoutLeafIdsInOrder } from './terminal-pane-tree' import { paneKeyOf } from './workspace-layout-model' @@ -78,7 +79,7 @@ export function diskRecords(session: WorkspaceSessionState): DiskRecords { groups.forEach((group) => records.group.set(`${key}|${group.id}`, group)) } for (const [key, files] of Object.entries(session.openFilesByWorktree ?? {})) { - files.forEach((file) => records.file.set(`${key}|${file.filePath}`, file)) + files.forEach((file) => records.file.set(`${key}|${editorFileKey(file)}`, file)) } for (const [key, tabs] of Object.entries(session.browserTabsByWorktree ?? {})) { tabs.forEach((tab) => records.browser.set(`${key}|${tab.id}`, tab)) diff --git a/src/shared/workspace-layout/workspace-layout-owned-editor-id.test.ts b/src/shared/workspace-layout/workspace-layout-owned-editor-id.test.ts index 91ec3182331..7c23e36941a 100644 --- a/src/shared/workspace-layout/workspace-layout-owned-editor-id.test.ts +++ b/src/shared/workspace-layout/workspace-layout-owned-editor-id.test.ts @@ -105,14 +105,9 @@ describe('editor tabs under an owned id, layout-owner build', () => { const { layout } = load(session) const workspace = layout.workspaces[GIT_KEY]! - expect( - tabExecutionHostId( - workspace.tabs[0]!, - workspace.editorFiles, - LOCAL_EXECUTION_HOST_ID, - workspace.worktreeId - ) - ).toBe('runtime:env-1') + expect(tabExecutionHostId(workspace.tabs[0]!, workspace, LOCAL_EXECUTION_HOST_ID)).toBe( + 'runtime:env-1' + ) const closed = applyLayoutCommand( layout, { type: 'closeTabs', workspace: GIT_KEY, tabIds: [fileId] }, diff --git a/src/shared/workspace-layout/workspace-layout-published.ts b/src/shared/workspace-layout/workspace-layout-published.ts index 995d4db75c5..d4ffdd1d90e 100644 --- a/src/shared/workspace-layout/workspace-layout-published.ts +++ b/src/shared/workspace-layout/workspace-layout-published.ts @@ -96,12 +96,7 @@ function publishTab( id: tab.id, kind: tab.kind, entityId: tab.entityId, - executionHostId: tabExecutionHostId( - tab, - workspace.editorFiles, - partition, - workspace.worktreeId - ), + executionHostId: tabExecutionHostId(tab, workspace, partition), createdAt: tab.createdAt, customTitle: tab.customTitle, color: tab.color, diff --git a/src/shared/workspace-layout/workspace-layout-save-tabs.ts b/src/shared/workspace-layout/workspace-layout-save-tabs.ts index de188b58074..ee7d8af351e 100644 --- a/src/shared/workspace-layout/workspace-layout-save-tabs.ts +++ b/src/shared/workspace-layout/workspace-layout-save-tabs.ts @@ -8,10 +8,10 @@ import { pickStoredFields } from './stored-record-fields' import { collectLayoutLeafIdsInOrder } from './terminal-pane-tree' import type { DesktopLayoutView, LayoutContentFacts } from './workspace-layout-beside' import type { - LayoutEditorFile, LayoutLeaf, LayoutTab, - LayoutTerminalTab + LayoutTerminalTab, + WorkspaceLayout } from './workspace-layout-model' import { tabExecutionHostId } from './workspace-layout-tab-host' @@ -27,7 +27,7 @@ export type WorkspaceSaveScope = { workspaceKey: string worktreeId: string hostId: ExecutionHostId - editorFiles: readonly LayoutEditorFile[] | undefined + workspace: Pick leaves: Readonly> facts: LayoutContentFacts view: DesktopLayoutView @@ -94,7 +94,7 @@ export function saveTabBarEntry( entityId: tab.entityId, groupId: placement.groupId, worktreeId: scope.worktreeId, - executionHostId: tabExecutionHostId(tab, scope.editorFiles, scope.hostId, scope.worktreeId), + executionHostId: tabExecutionHostId(tab, scope.workspace, scope.hostId), contentType: tab.kind, label, ...(tab.generatedTitle !== undefined ? { generatedLabel: tab.generatedTitle } : {}), diff --git a/src/shared/workspace-layout/workspace-layout-save.ts b/src/shared/workspace-layout/workspace-layout-save.ts index 86161cc02e7..48df4f06af0 100644 --- a/src/shared/workspace-layout/workspace-layout-save.ts +++ b/src/shared/workspace-layout/workspace-layout-save.ts @@ -15,7 +15,7 @@ import { FACT_SESSION_FIELDS, VIEW_SESSION_FIELDS } from './workspace-layout-beside' -import { editorFileKey, editorTabShowsFile } from './workspace-layout-editor-tab-file' +import { editorFileKey, editorTabNamesFile } from './workspace-layout-editor-tab-file' import type { LoadedWorkspaceLayout } from './workspace-layout-load-types' import { collectLayoutLeafIdsInOrder } from './terminal-pane-tree' import { paneKeyOf, tabsInOrder, type WorkspaceLayout } from './workspace-layout-model' @@ -43,7 +43,7 @@ function saveWorkspace( workspaceKey: key, worktreeId, hostId: loaded.layout.hostId, - editorFiles: workspace.editorFiles, + workspace, leaves: workspace.leaves ?? {}, facts, view @@ -104,7 +104,7 @@ function saveWorkspace( session.openFilesByWorktree[key] = workspace.editorFiles.map((file): PersistedOpenFile => ({ ...file, worktreeId, - ...(previewTabs.some((tab) => editorTabShowsFile(tab.entityId, file, worktreeId)) + ...(previewTabs.some((tab) => editorTabNamesFile(workspace, tab.entityId, file)) ? { isPreview: true } : {}), ...view.editorDrafts[key]?.[editorFileKey(file)] diff --git a/src/shared/workspace-layout/workspace-layout-tab-host.ts b/src/shared/workspace-layout/workspace-layout-tab-host.ts index d523afd00e5..d0af19000a2 100644 --- a/src/shared/workspace-layout/workspace-layout-tab-host.ts +++ b/src/shared/workspace-layout/workspace-layout-tab-host.ts @@ -8,7 +8,7 @@ import { } from '../execution-host' import type { TabContentType } from '../tab-types' import type { PersistedOpenFile } from '../workspace-session-state-types' -import { editorTabShowsFile } from './workspace-layout-editor-tab-file' +import { editorTabFile } from './workspace-layout-editor-tab-file' type FileOwner = Pick< PersistedOpenFile, @@ -18,14 +18,14 @@ type FileOwner = Pick< /** An editor tab follows its file's owner, as the window stamps it; every other tab its partition. */ export function tabExecutionHostId( tab: { kind: TabContentType; entityId: string }, - files: readonly FileOwner[] | undefined, - hostId: ExecutionHostId, - worktreeId: string + workspace: { + worktreeId: string + tabs: readonly { entityId: string }[] + editorFiles?: readonly FileOwner[] + }, + hostId: ExecutionHostId ): ExecutionHostId { - const file = - tab.kind === 'editor' - ? files?.find((entry) => editorTabShowsFile(tab.entityId, entry, worktreeId)) - : undefined + const file = tab.kind === 'editor' ? editorTabFile(workspace, tab.entityId) : undefined if (file?.externalSshTargetId) { return toSshExecutionHostId(file.externalSshTargetId) }