From 14f0096077e5db03424271b08c7dfcb8e9580b43 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:56:21 -0700 Subject: [PATCH] Omit unsupported combined diff tabs from mobile session snapshots (#6520) Combined diff tabs (such as branch, commit, or uncommitted changes) use display labels as relative paths and require the desktop-only combined renderer. Excluding these tabs from the mobile snapshots prevents mobile clients from incorrectly attempting to read them via files.read. --- .../src/runtime/sync-runtime-graph.test.ts | 79 +++++++++++++++++++ .../src/runtime/sync-runtime-graph.ts | 33 ++++++-- 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/runtime/sync-runtime-graph.test.ts b/src/renderer/src/runtime/sync-runtime-graph.test.ts index 08509c65b5d..2de176df7b9 100644 --- a/src/renderer/src/runtime/sync-runtime-graph.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph.test.ts @@ -725,6 +725,85 @@ describe('buildMobileSessionTabSnapshots', () => { expect(tab).not.toHaveProperty('diffSource') }) + it.each([ + ['combined-branch', 'wt-1::all-diffs::branch::main', 'Branch Changes (main)'], + ['combined-commit', 'wt-1::all-diffs::commit::abc123', 'Commit abc123'], + ['combined-all', 'wt-1::all-diffs::uncommitted', 'All Changes'], + ['combined-uncommitted', 'wt-1::all-diffs::uncommitted::unstaged', 'Changes'] + ] as const)('omits unsupported %s diff tabs from mobile file snapshots', (source, id, label) => { + const state = makeState({ + browserTabsByWorktree: {}, + tabBarOrderByWorktree: { 'wt-1': [id] }, + activeFileId: id, + activeFileIdByWorktree: { 'wt-1': id }, + activeTabType: 'editor', + activeTabTypeByWorktree: { 'wt-1': 'editor' }, + openFiles: [ + { + id, + filePath: '/repo', + relativePath: label, + worktreeId: 'wt-1', + language: 'plaintext', + mode: 'diff', + diffSource: source, + isDirty: false + } + ] + }) + + const snapshot = buildMobileSessionTabSnapshots(state)[0] + + expect(snapshot?.tabs).toEqual([]) + expect(snapshot?.activeTabId).toBeNull() + expect(snapshot?.activeTabType).toBeNull() + }) + + it('does not recover unsupported combined diff tabs through split-group fallback', () => { + const combinedId = 'wt-1::all-diffs::branch::main' + const state = makeState({ + activeGroupIdByWorktree: { 'wt-1': 'group-right' }, + groupsByWorktree: { + 'wt-1': [ + { + id: 'group-right', + activeTabId: 'combined-tab-right', + tabOrder: [], + recentTabIds: [] + } + ] + } as unknown as AppState['groupsByWorktree'], + unifiedTabsByWorktree: { + 'wt-1': [ + { + id: 'combined-tab-right', + groupId: 'group-right', + contentType: 'diff', + entityId: combinedId, + title: 'Branch Changes (main)' + } + ] + } as unknown as AppState['unifiedTabsByWorktree'], + openFiles: [ + { + id: combinedId, + filePath: '/repo', + relativePath: 'Branch Changes (main)', + worktreeId: 'wt-1', + language: 'plaintext', + mode: 'diff', + diffSource: 'combined-branch', + isDirty: false + } + ] + }) + + const snapshot = buildMobileSessionTabSnapshots(state)[0] + + expect(snapshot?.tabs).toEqual([]) + expect(snapshot?.tabGroups).toBeUndefined() + }) + it('publishes a missing non-markdown editor with its unified tab id and split group', () => { const fileId = '/repo/src/app.ts' const state = makeState({ diff --git a/src/renderer/src/runtime/sync-runtime-graph.ts b/src/renderer/src/runtime/sync-runtime-graph.ts index f8ebf0cbb2c..27f3113316b 100644 --- a/src/renderer/src/runtime/sync-runtime-graph.ts +++ b/src/renderer/src/runtime/sync-runtime-graph.ts @@ -700,7 +700,11 @@ export function buildMobileSessionTabSnapshots( const unifiedTabByIdForWorktree = new Map( (state.unifiedTabsByWorktree[worktreeId] ?? []).map((tab) => [tab.id, tab]) ) - const editorIds = openFileIndexes.idsByWorktree.get(worktreeId) ?? [] + const openFilesForWorktree = openFileIndexes.byWorktreeAndId.get(worktreeId) + const editorIds = (openFileIndexes.idsByWorktree.get(worktreeId) ?? []).filter((fileId) => { + const file = openFilesForWorktree?.get(fileId) + return file ? isMobilePublishableOpenFile(file) : false + }) const publishableTerminalIds = [...terminalTabByIdForWorktree.values()] .filter((terminal) => !isWebOnlyMirroredTerminalTab(state, terminal)) .map((terminal) => terminal.id) @@ -732,8 +736,8 @@ export function buildMobileSessionTabSnapshots( ) ) } else if (item.type === 'editor') { - const file = openFileIndexes.byWorktreeAndId.get(worktreeId)?.get(item.id) - if (!file) { + const file = openFilesForWorktree?.get(item.id) + if (!file || !isMobilePublishableOpenFile(file)) { continue } const markdown = buildMobileMarkdownTab( @@ -774,7 +778,6 @@ export function buildMobileSessionTabSnapshots( // Why: split-group projection can miss plain editor files during hydration. // Publish the missing file so paired mobile/web clients still mirror it. const fallbackEditorTabs: FallbackEditorTabTarget[] = [] - const openFilesForWorktree = openFileIndexes.byWorktreeAndId.get(worktreeId) if (openFilesForWorktree) { const unifiedEditorTabs = getEditorUnifiedTabsForWorktree(state, worktreeId) const unifiedEditorFileIds = new Set(unifiedEditorTabs.map((tab) => tab.entityId)) @@ -783,7 +786,7 @@ export function buildMobileSessionTabSnapshots( continue } const file = openFilesForWorktree.get(unifiedTab.entityId) - if (!file) { + if (!file || !isMobilePublishableOpenFile(file)) { continue } const markdown = buildMobileMarkdownTab( @@ -802,6 +805,9 @@ export function buildMobileSessionTabSnapshots( emittedEditorTabIds.add(unifiedTab.id) } for (const file of openFilesForWorktree.values()) { + if (!isMobilePublishableOpenFile(file)) { + continue + } if (emittedEditorFileIds.has(file.id)) { continue } @@ -1435,6 +1441,23 @@ function isMobileFileDiffSource( return diffSource === 'staged' || diffSource === 'unstaged' } +function isMobileUnsupportedCombinedDiffSource( + diffSource: AppState['openFiles'][number]['diffSource'] +): boolean { + return ( + diffSource === 'combined-all' || + diffSource === 'combined-uncommitted' || + diffSource === 'combined-branch' || + diffSource === 'combined-commit' + ) +} + +function isMobilePublishableOpenFile(file: AppState['openFiles'][number]): boolean { + // Why: combined diff tabs use display labels as relative paths and require + // the desktop combined renderer; mobile would otherwise try files.read. + return !isMobileUnsupportedCombinedDiffSource(file.diffSource) +} + function buildMobileBrowserTab( state: AppState, workspace: NonNullable[number],