From 917899bf8df866ed8fb7158e6d346ea726bcbf07 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:28:33 -0700 Subject: [PATCH] fix(runtime): stop paired desktops republishing mirrored editor tabs (#33259) * fix(runtime): stop paired desktops republishing mirrored editor tabs When two desktops pair both ways, B mirrored A's editor tabs and then published them in its own session inventory as if B had opened them. A then applied B's copy, which took over A's own tab. isMobilePublishableOpenFile now skips rows marked mirroredFromRuntimeSession, the marker is part of the open-files sync projection so a flip republishes, and the marker is saved with the session and restored so a relaunched client does not republish its restored mirrors before the host's first snapshot. Based on #25877 by BearMett. Co-authored-by: BearMett * fix(runtime): keep restored mirrors through unpublished frames and out of headless publication A restored mirror now carries its marker, so an unpublished host frame must not cull it (and its recovered draft). The windowless publication skips persisted mirrored rows for the same reason the window does. Co-authored-by: BearMett --------- Co-authored-by: BearMett --- .../mobile-session-editor-projection.test.ts | 17 +++ .../mobile-session-editor-projection.ts | 7 +- src/renderer/src/lib/workspace-session.ts | 6 +- ...-runtime-graph-projection-hot-path.test.ts | 2 + .../mirrored-editor-tab-persistence.test.ts | 140 ++++++++++++++++++ .../mirrored-editor-tab-republication.test.ts | 114 ++++++++++++++ .../mobile-session-surfaces.ts | 4 + .../sync-runtime-graph/sync-projections.ts | 1 + .../web-session-tabs-sync-editor-tabs.test.ts | 8 +- .../apply-preparation-browser.ts | 8 +- .../actions/hydrated-editor-session-state.ts | 1 + .../workspace-layout-disk-fields.ts | 1 + .../workspace-layout-fact-locations.test.ts | 1 + src/shared/workspace-session-editor-schema.ts | 1 + src/shared/workspace-session-state-types.ts | 1 + 15 files changed, 301 insertions(+), 11 deletions(-) create mode 100644 src/renderer/src/runtime/sync-runtime-graph/mirrored-editor-tab-persistence.test.ts create mode 100644 src/renderer/src/runtime/sync-runtime-graph/mirrored-editor-tab-republication.test.ts diff --git a/src/main/runtime/mobile-session-editor-projection.test.ts b/src/main/runtime/mobile-session-editor-projection.test.ts index c3417c56f08..19674d4af17 100644 --- a/src/main/runtime/mobile-session-editor-projection.test.ts +++ b/src/main/runtime/mobile-session-editor-projection.test.ts @@ -73,6 +73,23 @@ describe('headless editor tabs under an owned id', () => { expect(retired?.tabGroups?.[WT]?.[0]?.tabOrder).toEqual([]) }) + it('does not list a row mirrored from a paired host', () => { + const session = ownedSession() + session.openFilesByWorktree = { + [WT]: [ + { + filePath: FILE, + relativePath: 'notes.ts', + worktreeId: WT, + language: 'typescript', + runtimeEnvironmentId: 'peer', + mirroredFromRuntimeSession: true + } + ] + } + expect(buildHeadlessMobileSessionEditorTabs(WT, session)).toEqual([]) + }) + it('shows it as the active file', () => { expect(buildHeadlessMobileSessionEditorTabs(WT, activeOwnedSession())).toEqual([ expect.objectContaining({ id: 'tab-1', isActive: true }) diff --git a/src/main/runtime/mobile-session-editor-projection.ts b/src/main/runtime/mobile-session-editor-projection.ts index 51e9d73899b..4eae813867a 100644 --- a/src/main/runtime/mobile-session-editor-projection.ts +++ b/src/main/runtime/mobile-session-editor-projection.ts @@ -86,9 +86,10 @@ export function buildHeadlessMobileSessionEditorTabs( worktreeId: string, session: WorkspaceSessionState ): HeadlessEditorTab[] { - return (session.openFilesByWorktree?.[worktreeId] ?? []).map((file) => - editorTab(session, worktreeId, file) - ) + // A row mirrored from a paired host is that host's to publish, as in the window's publication. + return (session.openFilesByWorktree?.[worktreeId] ?? []) + .filter((file) => file.mirroredFromRuntimeSession !== true) + .map((file) => editorTab(session, worktreeId, file)) } /** The record the phone's tab names: two owners can hold one path, so by the id it was listed under. */ diff --git a/src/renderer/src/lib/workspace-session.ts b/src/renderer/src/lib/workspace-session.ts index f5bfd9e8639..280e9b15dd3 100644 --- a/src/renderer/src/lib/workspace-session.ts +++ b/src/renderer/src/lib/workspace-session.ts @@ -142,6 +142,7 @@ export function buildEditorSessionData( language: f.language, isPreview: f.isPreview || undefined, runtimeEnvironmentId: f.runtimeEnvironmentId, + ...(f.mirroredFromRuntimeSession === true ? { mirroredFromRuntimeSession: true } : {}), externalSshTargetId: f.externalSshTargetId, // Why: persist readOnly only when true; absence is the writable default on restore. ...(f.readOnly === true ? { readOnly: true } : {}), @@ -159,10 +160,7 @@ export function buildEditorSessionData( const activeFileEntries: [string, string][] = [] for (const [worktreeId, fileId] of Object.entries(activeFileIdByWorktree)) { - if (!fileId) { - continue - } - if (editFileIdsByWorktree[worktreeId]?.has(fileId)) { + if (fileId && editFileIdsByWorktree[worktreeId]?.has(fileId)) { activeFileEntries.push([worktreeId, fileId]) } } diff --git a/src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts b/src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts index 8797b917501..d76c7c011c1 100644 --- a/src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts @@ -59,6 +59,7 @@ function referenceOpenFilesProjection(openFiles: AppState['openFiles']): string mode: file.mode, diffSource: file.diffSource, isDirty: file.isDirty, + mirroredFromRuntimeSession: file.mirroredFromRuntimeSession, isUntitled: file.isUntitled, deleteUntouchedOnClose: file.deleteUntouchedOnClose, markdownPreviewSourceFileId: file.markdownPreviewSourceFileId @@ -423,6 +424,7 @@ describe('open-files and browser projections', () => { const openFileCases: AppState['openFiles'][] = [ [] as unknown as AppState['openFiles'], [makeOpenFile(0)] as unknown as AppState['openFiles'], + [makeOpenFile(0, { mirroredFromRuntimeSession: true })], [makeOpenFile(0, { isDirty: true })] as unknown as AppState['openFiles'], [ makeOpenFile(0, { isDirty: true, diffSource: 'working' }), diff --git a/src/renderer/src/runtime/sync-runtime-graph/mirrored-editor-tab-persistence.test.ts b/src/renderer/src/runtime/sync-runtime-graph/mirrored-editor-tab-persistence.test.ts new file mode 100644 index 00000000000..6cf1b2bae05 --- /dev/null +++ b/src/renderer/src/runtime/sync-runtime-graph/mirrored-editor-tab-persistence.test.ts @@ -0,0 +1,140 @@ +import { beforeEach, expect, it, vi } from 'vitest' +import { parseWorkspaceSessionSalvaging } from '../../../../shared/workspace-session-salvage' +import { UNPUBLISHED_WORKTREE_PUBLICATION_EPOCH } from '../../../../shared/runtime-types' +import { buildEditorSessionData } from '../../lib/workspace-session' +import { buildHydratedEditorSessionState } from '../../store/slices/editor/actions/hydrated-editor-session-state' +import { createTestStore, makeWorktree } from '../../store/slices/store-test-helpers' +import { createStoreSessionMockApi } from '../../store/slices/store-session-test-harness' +import { applyWebSessionTabsSnapshot } from '../web-session-tabs-sync/snapshot-api' +import { + ENV, + NOW, + WT, + makeSnapshot, + makeState, + resetWebSessionTabsSyncTestState +} from '../web-session-tabs-sync-test-harness' +import { buildMobileSessionTabSnapshots } from './mobile-session-snapshots' + +vi.mock('../../store', () => ({ useAppStore: { setState: vi.fn() } })) +createStoreSessionMockApi() +beforeEach(resetWebSessionTabsSyncTestState) + +it('does not republish a mirrored editor after persistence and hydration', () => { + const snapshot = makeSnapshot([ + { + type: 'file', + id: 'host-file-tab', + title: 'app.ts', + filePath: '/repo/app.ts', + relativePath: 'app.ts', + language: 'typescript', + mode: 'edit', + isDirty: false, + isActive: true + } + ]) + const state = makeState() + const applied = { ...state, ...applyWebSessionTabsSnapshot(state, snapshot, ENV, NOW) } + const persisted = buildEditorSessionData(applied.openFiles, {}, {}, {}, {}) + const restored = createTestStore() + restored.setState({ + repos: [{ id: 'repo', path: '/repo', displayName: 'Repo', badgeColor: '', addedAt: 0 }], + worktreesByRepo: { repo: [makeWorktree({ id: WT, repoId: 'repo', path: '/worktree' })] }, + activeWorktreeId: WT + }) + const loaded = parseWorkspaceSessionSalvaging({ + activeRepoId: 'repo', + activeWorktreeId: WT, + activeTabId: null, + tabsByWorktree: {}, + terminalLayoutsByTabId: {}, + ...persisted + }) + if (!loaded.ok) { + throw new Error(loaded.error) + } + expect(loaded.droppedCount).toBe(0) + expect(loaded.value.openFilesByWorktree?.[WT]?.[0]).toHaveProperty( + 'mirroredFromRuntimeSession', + true + ) + restored.getState().hydrateEditorSession(loaded.value) + + expect(restored.getState().openFiles).toHaveLength(1) + expect(restored.getState().openFiles[0]?.id).not.toBe('/repo/app.ts') + expect(buildMobileSessionTabSnapshots(restored.getState(), false).flatMap((s) => s.tabs)).toEqual( + [] + ) + expect(persisted.openFilesByWorktree?.[WT]?.[0]).toHaveProperty( + 'mirroredFromRuntimeSession', + true + ) + expect(restored.getState().openFiles[0]).toHaveProperty('mirroredFromRuntimeSession', true) + + restored.setState(applyWebSessionTabsSnapshot(restored.getState(), snapshot, ENV, NOW + 1)) + expect(buildMobileSessionTabSnapshots(restored.getState(), false).flatMap((s) => s.tabs)).toEqual( + [] + ) + + for (const flag of [false, undefined]) { + const local = buildEditorSessionData( + applied.openFiles.map((file) => ({ ...file, mirroredFromRuntimeSession: flag })), + {}, + {}, + {}, + {} + ) + expect(local.openFilesByWorktree?.[WT]?.[0]).not.toHaveProperty('mirroredFromRuntimeSession') + } +}) + +it('keeps a restored dirty mirror until the host publishes the worktree', () => { + const persisted = buildEditorSessionData( + [ + { + id: 'host-file', + filePath: '/repo/app.ts', + relativePath: 'app.ts', + worktreeId: WT, + language: 'typescript', + mode: 'edit', + isDirty: true, + runtimeEnvironmentId: ENV, + mirroredFromRuntimeSession: true + } + ], + { 'host-file': 'unsaved client edits' }, + {}, + {}, + {} + ) + const restored = buildHydratedEditorSessionState( + { + activeRepoId: 'repo', + activeWorktreeId: WT, + activeTabId: null, + tabsByWorktree: {}, + terminalLayoutsByTabId: {}, + ...persisted + }, + new Set([WT]), + WT + ).state + const state = makeState(restored) + const unpublished = makeSnapshot([], { + publicationEpoch: UNPUBLISHED_WORKTREE_PUBLICATION_EPOCH, + snapshotVersion: 0 + }) + const held = { ...state, ...applyWebSessionTabsSnapshot(state, unpublished, ENV, NOW) } + expect( + buildEditorSessionData(held.openFiles, held.editorDrafts ?? {}, {}, {}, {}) + .openFilesByWorktree?.[WT]?.[0]?.dirtyDraftContent + ).toBe('unsaved client edits') + + const closed = { + ...held, + ...applyWebSessionTabsSnapshot(held, makeSnapshot([]), ENV, NOW + 1) + } + expect(closed.openFiles).toEqual([]) +}) diff --git a/src/renderer/src/runtime/sync-runtime-graph/mirrored-editor-tab-republication.test.ts b/src/renderer/src/runtime/sync-runtime-graph/mirrored-editor-tab-republication.test.ts new file mode 100644 index 00000000000..c6a664b1611 --- /dev/null +++ b/src/renderer/src/runtime/sync-runtime-graph/mirrored-editor-tab-republication.test.ts @@ -0,0 +1,114 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { makeState as makePublicationState } from '../sync-runtime-graph-test-harness' +import { applyWebSessionTabsSnapshot } from '../web-session-tabs-sync/snapshot-api' +import { + ENV, + NOW, + WT, + makeSnapshot, + makeState, + resetWebSessionTabsSyncTestState +} from '../web-session-tabs-sync-test-harness' +import { buildMobileSessionTabSnapshots } from './mobile-session-snapshots' +import { getRuntimeMobileSessionSyncKey, runtimeMobileSessionSyncKeysEqual } from './sync-key' +import type { RuntimeMobileSessionFileTab } from '../../../../shared/runtime-types' + +vi.mock('../../store', () => ({ useAppStore: { setState: vi.fn() } })) + +describe('mirrored editor tab republication', () => { + beforeEach(resetWebSessionTabsSyncTestState) + + function mirrorHostTab(tab: RuntimeMobileSessionFileTab) { + const state = makeState() + const patch = applyWebSessionTabsSnapshot(state, makeSnapshot([tab]), ENV, NOW) + return makePublicationState({ ...state, ...patch }) + } + + const fileTab: RuntimeMobileSessionFileTab = { + type: 'file', + id: 'host-file-tab', + title: 'app.ts', + filePath: '/repo/app.ts', + relativePath: 'app.ts', + language: 'typescript', + mode: 'edit', + isDirty: false, + isActive: true + } + + it('does not republish a file mirrored from a host snapshot and caches the empty snapshot', () => { + const clientState = mirrorHostTab(fileTab) + expect(clientState.openFiles).toMatchObject([ + { worktreeId: WT, runtimeEnvironmentId: ENV, mirroredFromRuntimeSession: true } + ]) + expect(clientState.unifiedTabsByWorktree[WT]).toMatchObject([ + { id: 'host-file-tab', contentType: 'editor' } + ]) + const snapshots = buildMobileSessionTabSnapshots(clientState, false) + expect(snapshots).toMatchObject([{ worktree: WT, tabs: [] }]) + expect(buildMobileSessionTabSnapshots(clientState, false)[0]).toBe(snapshots[0]) + }) + + it('keeps a genuinely local file in the same worktree and retracts it after close', () => { + const mirroredState = mirrorHostTab(fileTab) + const clientState = { + ...mirroredState, + openFiles: [ + ...mirroredState.openFiles, + { + id: 'local-file', + filePath: '/repo/local.ts', + relativePath: 'local.ts', + worktreeId: WT, + language: 'typescript', + mode: 'edit' as const, + isDirty: false + } + ] + } + const snapshots = buildMobileSessionTabSnapshots(clientState, false) + expect(snapshots).toHaveLength(1) + expect(snapshots[0]?.tabs).toMatchObject([ + { type: 'file', id: 'local-file', filePath: '/repo/local.ts' } + ]) + expect(snapshots[0]?.tabGroups?.flatMap((group) => group.tabOrder)).not.toContain( + 'host-file-tab' + ) + expect(buildMobileSessionTabSnapshots(mirroredState, false)).toMatchObject([ + { worktree: WT, tabs: [] } + ]) + expect(buildMobileSessionTabSnapshots(clientState, false)[0]?.snapshotVersion).toBeGreaterThan( + snapshots[0]!.snapshotVersion + ) + }) + + it('filters mirrors without unified tabs or groups', () => { + const clientState = mirrorHostTab(fileTab) + expect( + buildMobileSessionTabSnapshots( + makePublicationState({ openFiles: clientState.openFiles }), + false + ) + ).toMatchObject([{ worktree: WT, tabs: [] }]) + }) + + it('resyncs when only the mirror ownership flag changes', () => { + const mirroredState = mirrorHostTab(fileTab) + const ownedState = { + ...mirroredState, + openFiles: mirroredState.openFiles.map((file) => ({ + ...file, + mirroredFromRuntimeSession: false + })) + } + const ownedKey = getRuntimeMobileSessionSyncKey(ownedState, undefined, undefined, false) + const mirroredKey = getRuntimeMobileSessionSyncKey(mirroredState, ownedState, ownedKey, false) + expect(runtimeMobileSessionSyncKeysEqual(ownedKey, mirroredKey)).toBe(false) + expect(buildMobileSessionTabSnapshots(ownedState, false)[0]?.tabs).toMatchObject([ + { type: 'file', id: 'host-file-tab' } + ]) + expect(buildMobileSessionTabSnapshots(mirroredState, false)).toMatchObject([ + { worktree: WT, tabs: [] } + ]) + }) +}) 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 2550795f83e..ac154ba0873 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 @@ -78,6 +78,10 @@ export function isMobileFileDiffSource( } export function isMobilePublishableOpenFile(file: AppState['openFiles'][number]): boolean { + // A tab mirrored from a paired host is that host's; republishing it echoes it back to the host. + if (file.mirroredFromRuntimeSession === true) { + return false + } // Combined diff tabs use display labels as paths and need the desktop renderer. if ( file.diffSource === 'combined-all' || diff --git a/src/renderer/src/runtime/sync-runtime-graph/sync-projections.ts b/src/renderer/src/runtime/sync-runtime-graph/sync-projections.ts index 44a1606b056..935417df872 100644 --- a/src/renderer/src/runtime/sync-runtime-graph/sync-projections.ts +++ b/src/renderer/src/runtime/sync-runtime-graph/sync-projections.ts @@ -109,6 +109,7 @@ export function buildRuntimeMobileOpenFilesProjection(openFiles: AppState['openF mode: file.mode, diffSource: file.diffSource, isDirty: file.isDirty, + mirroredFromRuntimeSession: file.mirroredFromRuntimeSession, isUntitled: file.isUntitled, deleteUntouchedOnClose: file.deleteUntouchedOnClose, markdownPreviewSourceFileId: file.markdownPreviewSourceFileId diff --git a/src/renderer/src/runtime/web-session-tabs-sync-editor-tabs.test.ts b/src/renderer/src/runtime/web-session-tabs-sync-editor-tabs.test.ts index ccca65f73c8..0ec291df910 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync-editor-tabs.test.ts +++ b/src/renderer/src/runtime/web-session-tabs-sync-editor-tabs.test.ts @@ -638,11 +638,15 @@ describe('applyWebSessionTabsSnapshot host-invariant editor dedup', () => { buildOwnedEditorFileId('/repo/README.md', WT, null), '/repo/README.md' ] + // An older peer republishes its mirror as its own row; this build no longer publishes mirrors. + const olderPeerFile: OpenFile = { ...ownedFile, mirroredFromRuntimeSession: undefined } const published = buildMobileSessionTabSnapshots( makePublisherState({ - openFiles: [ownedFile], + openFiles: [olderPeerFile], unifiedTabsByWorktree: { - [WT]: ids.flatMap((id) => stateWithVariant(id, ownedFile).unifiedTabsByWorktree[WT] ?? []) + [WT]: ids.flatMap( + (id) => stateWithVariant(id, olderPeerFile).unifiedTabsByWorktree[WT] ?? [] + ) } }) )[0] diff --git a/src/renderer/src/runtime/web-session-tabs-sync/apply-preparation-browser.ts b/src/renderer/src/runtime/web-session-tabs-sync/apply-preparation-browser.ts index 914edaa3259..ffdaee0d228 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync/apply-preparation-browser.ts +++ b/src/renderer/src/runtime/web-session-tabs-sync/apply-preparation-browser.ts @@ -10,7 +10,8 @@ import { buildMirroredEditorTabs } from './tab-builders' import { buildMirroredAgentTabs, isReadyBrowserTab, isReadyEditorTab } from './terminal-surfaces' import { hostSnapshotAffirmsAgentSessions, - hostSnapshotAffirmsClientHostedPages + hostSnapshotAffirmsClientHostedPages, + hostSnapshotAffirmsWorktreeContents } from '../host-session-snapshot-authority' import type { prepareWebSessionTabsSnapshotBase } from './apply-preparation-base' import type { OpenFile } from '../../store/slices/editor' @@ -143,8 +144,11 @@ export function prepareWebSessionTabsSnapshotBrowser( ...new Map(mirroredEditorTabs.map((entry) => [entry.file.id, entry.file])).values() ] const mirroredFileRecords = new Set(mirroredOpenFiles) + // A host that has not published this worktree yet proves nothing about its editor tabs. + const reconcilesEditorFiles = + reconcilesNonAgentTabs && hostSnapshotAffirmsWorktreeContents(snapshot) const removedEditorFileIds = new Set( - (reconcilesNonAgentTabs ? worktreeOpenFiles : []) + (reconcilesEditorFiles ? worktreeOpenFiles : []) .filter( (file) => file.runtimeEnvironmentId === environmentId && diff --git a/src/renderer/src/store/slices/editor/actions/hydrated-editor-session-state.ts b/src/renderer/src/store/slices/editor/actions/hydrated-editor-session-state.ts index 2bd5f3c07cd..67493006c9c 100644 --- a/src/renderer/src/store/slices/editor/actions/hydrated-editor-session-state.ts +++ b/src/renderer/src/store/slices/editor/actions/hydrated-editor-session-state.ts @@ -107,6 +107,7 @@ export function buildHydratedEditorSessionState( isDirty: !isReadOnly && pf.dirtyDraftContent !== undefined, isPreview: pf.isPreview, runtimeEnvironmentId: pf.runtimeEnvironmentId, + mirroredFromRuntimeSession: pf.mirroredFromRuntimeSession, externalSshTargetId: pf.externalSshTargetId, ...(isReadOnly ? { readOnly: true } : {}), ...(isReadOnly && pf.liveTail === true ? { liveTail: true } : {}), diff --git a/src/shared/workspace-layout/workspace-layout-disk-fields.ts b/src/shared/workspace-layout/workspace-layout-disk-fields.ts index 407aaae28b6..f15a20e498d 100644 --- a/src/shared/workspace-layout/workspace-layout-disk-fields.ts +++ b/src/shared/workspace-layout/workspace-layout-disk-fields.ts @@ -200,6 +200,7 @@ const FILE = { language: own('file', 'language'), isPreview: tab('isPreview'), runtimeEnvironmentId: own('file', 'runtimeEnvironmentId'), + mirroredFromRuntimeSession: own('file', 'mirroredFromRuntimeSession'), externalSshTargetId: own('file', 'externalSshTargetId'), dirtyDraftContent: own('editorDraft', 'dirtyDraftContent'), lastKnownDiskSignature: own('editorDraft', 'lastKnownDiskSignature'), diff --git a/src/shared/workspace-layout/workspace-layout-fact-locations.test.ts b/src/shared/workspace-layout/workspace-layout-fact-locations.test.ts index befad920128..0453e218367 100644 --- a/src/shared/workspace-layout/workspace-layout-fact-locations.test.ts +++ b/src/shared/workspace-layout/workspace-layout-fact-locations.test.ts @@ -126,6 +126,7 @@ function populate(loaded: LoadedWorkspaceLayout): LoadedWorkspaceLayout { fill(loaded, 'tab.terminal.forceHostRuntime', true) fill(loaded, 'file.externalSshTargetId', 'box') fill(loaded, 'file.runtimeEnvironmentId', 'env') + fill(loaded, 'file.mirroredFromRuntimeSession', true) fill(loaded, 'file.readOnly', true) fill(loaded, 'file.liveTail', true) // A focused and an expanded pane that is not the first, so a fallback is visible. diff --git a/src/shared/workspace-session-editor-schema.ts b/src/shared/workspace-session-editor-schema.ts index 730c3c8e621..84c23b739a0 100644 --- a/src/shared/workspace-session-editor-schema.ts +++ b/src/shared/workspace-session-editor-schema.ts @@ -10,6 +10,7 @@ export const persistedOpenFileSchema = z.object({ language: z.string(), isPreview: z.boolean().optional(), runtimeEnvironmentId: z.string().nullable().optional(), + mirroredFromRuntimeSession: z.literal(true).optional(), externalSshTargetId: z.string().trim().min(1).optional(), dirtyDraftContent: z.string().optional(), lastKnownDiskSignature: z.string().optional(), diff --git a/src/shared/workspace-session-state-types.ts b/src/shared/workspace-session-state-types.ts index 6b109e7651d..1340b80fa88 100644 --- a/src/shared/workspace-session-state-types.ts +++ b/src/shared/workspace-session-state-types.ts @@ -21,6 +21,7 @@ export type PersistedOpenFile = { language: string isPreview?: boolean runtimeEnvironmentId?: string | null + mirroredFromRuntimeSession?: true /** SSH target that owns an absolute path outside the worktree. */ externalSshTargetId?: string /** Unsaved editor buffer captured for hot exit; presence restores the tab dirty. */