diff --git a/src/renderer/src/runtime/sync-runtime-graph-workspace-publication.test.ts b/src/renderer/src/runtime/sync-runtime-graph-workspace-publication.test.ts index 823e68d0508..9a5116400c0 100644 --- a/src/renderer/src/runtime/sync-runtime-graph-workspace-publication.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph-workspace-publication.test.ts @@ -52,6 +52,33 @@ describe('buildMobileSessionTabSnapshots', () => { expect(restored.snapshotVersion).toBeGreaterThan(initial.snapshotVersion) }) + it('publishes a new instance identity when unchanged content is recreated', () => { + const worktree = { + id: 'wt-1', + instanceId: 'old-instance', + repoId: 'repo-1' + } + const base = makeState({ + worktreesByRepo: { 'repo-1': [worktree] } as unknown as AppState['worktreesByRepo'], + tabsByWorktree: { + 'wt-1': [{ id: 'term-1', title: 'Terminal 1' }] + } as unknown as AppState['tabsByWorktree'] + }) + const initial = buildMobileSessionTabSnapshots(base)[0]! + const recreated = { + ...base, + worktreesByRepo: { + 'repo-1': [{ ...worktree, instanceId: 'new-instance' }] + } as unknown as AppState['worktreesByRepo'] + } + + const next = buildMobileSessionTabSnapshots(recreated)[0]! + + expect(initial.worktreeInstanceId).toBe('old-instance') + expect(next.worktreeInstanceId).toBe('new-instance') + expect(next.snapshotVersion).toBeGreaterThan(initial.snapshotVersion) + }) + it('publishes browser and editor color + pin state from unified tabs', () => { const fileId = '/repo/README.md' const state = makeState({ diff --git a/src/renderer/src/runtime/sync-runtime-graph/mobile-session-capture.ts b/src/renderer/src/runtime/sync-runtime-graph/mobile-session-capture.ts index 921249fb82d..16c556c86d6 100644 --- a/src/renderer/src/runtime/sync-runtime-graph/mobile-session-capture.ts +++ b/src/renderer/src/runtime/sync-runtime-graph/mobile-session-capture.ts @@ -157,6 +157,7 @@ export function canReuseMobileSessionSnapshot( ): boolean { return ( previous.worktreeId === next.worktreeId && + previous.worktreeInstanceId === next.worktreeInstanceId && previous.terminalTabs === next.terminalTabs && previous.browserWorkspaces === next.browserWorkspaces && previous.unifiedTabs === next.unifiedTabs && diff --git a/src/renderer/src/runtime/sync-runtime-graph/mobile-session-inputs.ts b/src/renderer/src/runtime/sync-runtime-graph/mobile-session-inputs.ts index 16c2f4d0da3..4792c59b824 100644 --- a/src/renderer/src/runtime/sync-runtime-graph/mobile-session-inputs.ts +++ b/src/renderer/src/runtime/sync-runtime-graph/mobile-session-inputs.ts @@ -1,6 +1,7 @@ import type { AppState } from '@/store/types' import { parsePaneKey, makePaneKey } from '../../../../shared/stable-pane-id' import { nativeChatLaunchAgentForLeaf } from '../../components/native-chat/native-chat-leaf-routing' +import { getIndexedWorktreesById } from '@/store/worktree-repo-index' import { EMPTY_NARROWED_BY_KEY, EMPTY_WORKTREE_BROWSER_WORKSPACES, @@ -146,6 +147,9 @@ export function buildMobileSessionWorktreeInputs( const activeTabId = state.activeTabId return { worktreeId, + worktreeInstanceId: getIndexedWorktreesById(state.worktreesByRepo, worktreeId).find( + (worktree) => worktree.instanceId !== undefined + )?.instanceId, terminalTabs, browserWorkspaces, unifiedTabs: state.unifiedTabsByWorktree[worktreeId] ?? EMPTY_WORKTREE_UNIFIED_TABS, 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 91e89a9cb73..401323a43ca 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 @@ -208,16 +208,29 @@ export function buildMobileSessionTabSnapshots( } const candidateVersion = ++graphState.mobileSessionSnapshotVersion if (cached && jsonContentEquals(cached.content, content)) { + const snapshot = + cached.snapshot.worktreeInstanceId === inputs.worktreeInstanceId + ? cached.snapshot + : { + worktree: worktreeId, + ...(inputs.worktreeInstanceId + ? { worktreeInstanceId: inputs.worktreeInstanceId } + : {}), + publicationEpoch: mobilePublicationEpoch, + snapshotVersion: candidateVersion, + ...content + } graphState.mobileSessionSnapshotCacheByWorktree.set(worktreeId, { inputs, content, - snapshot: cached.snapshot + snapshot }) - snapshots.push(cached.snapshot) + snapshots.push(snapshot) continue } const snapshot: RuntimeMobileSessionTabsSnapshot = { worktree: worktreeId, + ...(inputs.worktreeInstanceId ? { worktreeInstanceId: inputs.worktreeInstanceId } : {}), publicationEpoch: mobilePublicationEpoch, snapshotVersion: candidateVersion, ...content diff --git a/src/renderer/src/runtime/sync-runtime-graph/types.ts b/src/renderer/src/runtime/sync-runtime-graph/types.ts index 85b1d961d5f..b51b8cd8afa 100644 --- a/src/renderer/src/runtime/sync-runtime-graph/types.ts +++ b/src/renderer/src/runtime/sync-runtime-graph/types.ts @@ -107,6 +107,7 @@ export type MountedTerminalSurfaceCapture = { */ export type MobileSessionWorktreeInputs = { worktreeId: string + worktreeInstanceId: string | undefined terminalTabs: AppState['tabsByWorktree'][string] browserWorkspaces: AppState['browserTabsByWorktree'][string] unifiedTabs: AppState['unifiedTabsByWorktree'][string] diff --git a/src/shared/runtime-session-contracts.ts b/src/shared/runtime-session-contracts.ts index cd0dd7a5cc7..17fe75d5108 100644 --- a/src/shared/runtime-session-contracts.ts +++ b/src/shared/runtime-session-contracts.ts @@ -215,6 +215,8 @@ export const UNPUBLISHED_WORKTREE_PUBLICATION_EPOCH = 'none' export type RuntimeMobileSessionTabsSnapshot = { worktree: string + /** Immutable catalog identity used to fence snapshots across path reuse. */ + worktreeInstanceId?: string publicationEpoch: string snapshotVersion: number activeGroupId: string | null