diff --git a/src/renderer/src/runtime/sync-runtime-graph.test.ts b/src/renderer/src/runtime/sync-runtime-graph.test.ts index ef657bf3a15..6068766a724 100644 --- a/src/renderer/src/runtime/sync-runtime-graph.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph.test.ts @@ -21,6 +21,8 @@ function makeState(overrides: Partial = {}): AppState { tabBarOrderByWorktree: {}, activeFileId: null, activeFileIdByWorktree: {}, + activeTabType: 'terminal', + activeTabTypeByWorktree: {}, activeBrowserTabIdByWorktree: {}, browserTabsByWorktree: {}, browserPagesByWorkspace: {}, @@ -53,6 +55,8 @@ function makeSharedOverrides(): Partial { unifiedTabsByWorktree: {}, tabBarOrderByWorktree: {}, activeFileIdByWorktree: {}, + activeTabType: 'terminal', + activeTabTypeByWorktree: {}, activeBrowserTabIdByWorktree: {}, browserTabsByWorktree: {}, browserPagesByWorkspace: {}, @@ -606,6 +610,448 @@ describe('buildMobileSessionTabSnapshots', () => { expect(tab).not.toHaveProperty('diffSource') }) + it('publishes a missing non-markdown editor with its unified tab id and split group', () => { + const fileId = '/repo/src/app.ts' + const state = makeState({ + activeGroupIdByWorktree: { 'wt-1': 'group-left' }, + groupsByWorktree: { + 'wt-1': [ + { + id: 'group-left', + activeTabId: 'browser-tab-1', + tabOrder: ['browser-tab-1'], + recentTabIds: ['browser-tab-1'] + }, + { + id: 'group-right', + activeTabId: 'editor-tab-1', + tabOrder: [], + recentTabIds: [] + } + ] + } as unknown as AppState['groupsByWorktree'], + layoutByWorktree: { + 'wt-1': { + type: 'split', + direction: 'horizontal', + first: { type: 'leaf', groupId: 'group-left' }, + second: { type: 'leaf', groupId: 'group-right' } + } + } as unknown as AppState['layoutByWorktree'], + unifiedTabsByWorktree: { + 'wt-1': [ + { + id: 'browser-tab-1', + groupId: 'group-left', + contentType: 'browser', + entityId: 'browser-1', + title: 'Docs' + }, + { + id: 'editor-tab-1', + groupId: 'group-right', + contentType: 'editor', + entityId: fileId, + title: 'app.ts' + } + ] + } as unknown as AppState['unifiedTabsByWorktree'], + browserTabsByWorktree: { + 'wt-1': [ + { + id: 'browser-1', + worktreeId: 'wt-1', + activePageId: 'page-1', + pageIds: ['page-1'], + url: 'https://example.test', + title: 'Docs', + loading: false, + faviconUrl: null, + canGoBack: false, + canGoForward: false, + loadError: null, + createdAt: 1 + } + ] + } as unknown as AppState['browserTabsByWorktree'], + browserPagesByWorkspace: { + 'browser-1': [ + { + id: 'page-1', + workspaceId: 'browser-1', + worktreeId: 'wt-1', + url: 'https://example.test', + title: 'Docs', + loading: false, + faviconUrl: null, + canGoBack: false, + canGoForward: false, + loadError: null, + createdAt: 1 + } + ] + } as unknown as AppState['browserPagesByWorkspace'], + openFiles: [ + { + id: fileId, + filePath: fileId, + relativePath: 'src/app.ts', + worktreeId: 'wt-1', + language: 'typescript', + mode: 'edit', + isDirty: false + } + ] + }) + + const snapshot = buildMobileSessionTabSnapshots(state)[0] + + expect(snapshot?.tabs.map((tab) => tab.id)).toEqual(['browser-tab-1', 'editor-tab-1']) + expect(snapshot?.tabs.at(-1)).toMatchObject({ + type: 'file', + id: 'editor-tab-1', + relativePath: 'src/app.ts', + isActive: false + }) + expect(snapshot?.activeTabId).toBe('browser-tab-1') + expect(snapshot?.tabGroups).toEqual([ + { + id: 'group-left', + activeTabId: 'browser-tab-1', + tabOrder: ['browser-tab-1'], + recentTabIds: ['browser-tab-1'] + }, + { + id: 'group-right', + activeTabId: 'editor-tab-1', + tabOrder: ['editor-tab-1'], + recentTabIds: [] + } + ]) + expect(snapshot?.tabGroupLayout).toEqual({ + type: 'split', + direction: 'horizontal', + first: { type: 'leaf', groupId: 'group-left' }, + second: { type: 'leaf', groupId: 'group-right' } + }) + }) + + it('does not conflate same-path edit and diff editor tabs in the fallback', () => { + const fileId = '/repo/src/app.ts' + const diffId = 'wt-1::diff::unstaged::src/app.ts' + const state = makeState({ + activeGroupIdByWorktree: { 'wt-1': 'group-1' }, + groupsByWorktree: { + 'wt-1': [ + { + id: 'group-1', + activeTabId: 'editor-tab-1', + tabOrder: ['editor-tab-1'], + recentTabIds: ['editor-tab-1'] + } + ] + } as unknown as AppState['groupsByWorktree'], + unifiedTabsByWorktree: { + 'wt-1': [ + { + id: 'editor-tab-1', + groupId: 'group-1', + contentType: 'editor', + entityId: fileId, + title: 'app.ts' + } + ] + } as unknown as AppState['unifiedTabsByWorktree'], + openFiles: [ + { + id: fileId, + filePath: fileId, + relativePath: 'src/app.ts', + worktreeId: 'wt-1', + language: 'typescript', + mode: 'edit', + isDirty: false + }, + { + id: diffId, + filePath: fileId, + relativePath: 'src/app.ts', + worktreeId: 'wt-1', + language: 'typescript', + mode: 'diff', + diffSource: 'unstaged', + isDirty: false + } + ] + }) + + const snapshot = buildMobileSessionTabSnapshots(state)[0] + + expect(snapshot?.tabs).toMatchObject([ + { type: 'file', id: 'editor-tab-1', mode: 'edit', relativePath: 'src/app.ts' }, + { type: 'file', id: diffId, mode: 'diff', diffSource: 'unstaged', relativePath: 'src/app.ts' } + ]) + expect(snapshot?.tabGroups).toEqual([ + { + id: 'group-1', + activeTabId: 'editor-tab-1', + tabOrder: ['editor-tab-1', diffId], + recentTabIds: ['editor-tab-1'] + } + ]) + }) + + it('recovers a duplicate split editor tab for an already-emitted file id', () => { + const fileId = '/repo/src/app.ts' + const state = makeState({ + activeGroupIdByWorktree: { 'wt-1': 'group-left' }, + groupsByWorktree: { + 'wt-1': [ + { + id: 'group-left', + activeTabId: 'editor-left', + tabOrder: ['editor-left'], + recentTabIds: ['editor-left'] + }, + { + id: 'group-right', + activeTabId: 'editor-right', + tabOrder: [], + recentTabIds: [] + } + ] + } as unknown as AppState['groupsByWorktree'], + layoutByWorktree: { + 'wt-1': { + type: 'split', + direction: 'horizontal', + first: { type: 'leaf', groupId: 'group-left' }, + second: { type: 'leaf', groupId: 'group-right' } + } + } as unknown as AppState['layoutByWorktree'], + unifiedTabsByWorktree: { + 'wt-1': [ + { + id: 'editor-left', + groupId: 'group-left', + contentType: 'editor', + entityId: fileId, + title: 'app.ts' + }, + { + id: 'editor-right', + groupId: 'group-right', + contentType: 'editor', + entityId: fileId, + title: 'app.ts' + } + ] + } as unknown as AppState['unifiedTabsByWorktree'], + openFiles: [ + { + id: fileId, + filePath: fileId, + relativePath: 'src/app.ts', + worktreeId: 'wt-1', + language: 'typescript', + mode: 'edit', + isDirty: false + } + ] + }) + + const snapshot = buildMobileSessionTabSnapshots(state)[0] + + expect(snapshot?.tabs).toMatchObject([ + { type: 'file', id: 'editor-left', relativePath: 'src/app.ts' }, + { type: 'file', id: 'editor-right', relativePath: 'src/app.ts' } + ]) + expect(snapshot?.tabGroups).toEqual([ + { + id: 'group-left', + activeTabId: 'editor-left', + tabOrder: ['editor-left'], + recentTabIds: ['editor-left'] + }, + { + id: 'group-right', + activeTabId: 'editor-right', + tabOrder: ['editor-right'], + recentTabIds: [] + } + ]) + expect(snapshot?.tabGroupLayout).toEqual({ + type: 'split', + direction: 'horizontal', + first: { type: 'leaf', groupId: 'group-left' }, + second: { type: 'leaf', groupId: 'group-right' } + }) + }) + + it('uses unified editor ids in legacy no-group order without duplicating file ids', () => { + const fileId = '/repo/src/app.ts' + const state = makeState({ + tabBarOrderByWorktree: { 'wt-1': [fileId] }, + unifiedTabsByWorktree: { + 'wt-1': [ + { + id: 'editor-tab-1', + groupId: 'group-1', + contentType: 'editor', + entityId: fileId, + title: 'app.ts' + } + ] + } as unknown as AppState['unifiedTabsByWorktree'], + openFiles: [ + { + id: fileId, + filePath: fileId, + relativePath: 'src/app.ts', + worktreeId: 'wt-1', + language: 'typescript', + mode: 'edit', + isDirty: false + } + ] + }) + + const snapshot = buildMobileSessionTabSnapshots(state)[0] + + expect(snapshot?.tabs).toMatchObject([ + { type: 'file', id: 'editor-tab-1', relativePath: 'src/app.ts' } + ]) + expect(snapshot?.tabs).toHaveLength(1) + }) + + it('recovers a missing diff unified tab in its split group', () => { + const diffId = 'wt-1::diff::unstaged::src/app.ts' + const state = makeState({ + activeGroupIdByWorktree: { 'wt-1': 'group-left' }, + groupsByWorktree: { + 'wt-1': [ + { + id: 'group-left', + activeTabId: 'terminal-left', + tabOrder: [], + recentTabIds: [] + }, + { + id: 'group-right', + activeTabId: 'diff-tab-right', + tabOrder: [], + recentTabIds: [] + } + ] + } as unknown as AppState['groupsByWorktree'], + layoutByWorktree: { + 'wt-1': { + type: 'split', + direction: 'horizontal', + first: { type: 'leaf', groupId: 'group-left' }, + second: { type: 'leaf', groupId: 'group-right' } + } + } as unknown as AppState['layoutByWorktree'], + unifiedTabsByWorktree: { + 'wt-1': [ + { + id: 'diff-tab-right', + groupId: 'group-right', + contentType: 'diff', + entityId: diffId, + title: 'app.ts' + } + ] + } as unknown as AppState['unifiedTabsByWorktree'], + openFiles: [ + { + id: diffId, + filePath: '/repo/src/app.ts', + relativePath: 'src/app.ts', + worktreeId: 'wt-1', + language: 'typescript', + mode: 'diff', + diffSource: 'unstaged', + isDirty: false + } + ] + }) + + const snapshot = buildMobileSessionTabSnapshots(state)[0] + + expect(snapshot?.tabs).toMatchObject([ + { + type: 'file', + id: 'diff-tab-right', + mode: 'diff', + diffSource: 'unstaged', + relativePath: 'src/app.ts' + } + ]) + expect(snapshot?.tabGroups).toEqual([ + { + id: 'group-right', + activeTabId: 'diff-tab-right', + tabOrder: ['diff-tab-right'], + recentTabIds: [] + } + ]) + expect(snapshot?.tabGroupLayout).toEqual({ type: 'leaf', groupId: 'group-right' }) + }) + + it('gates fallback editor active state on the worktree active tab type', () => { + const fileId = '/repo/src/app.ts' + const state = { + activeFileId: '/repo/other-worktree.ts', + activeFileIdByWorktree: { 'wt-1': fileId }, + groupsByWorktree: { + 'wt-1': [ + { + id: 'group-1', + activeTabId: fileId, + tabOrder: [], + recentTabIds: [] + } + ] + } as unknown as AppState['groupsByWorktree'], + openFiles: [ + { + id: fileId, + filePath: fileId, + relativePath: 'src/app.ts', + worktreeId: 'wt-1', + language: 'typescript', + mode: 'edit', + isDirty: false + } + ] + } satisfies Partial + + const terminalSnapshot = buildMobileSessionTabSnapshots( + makeState({ + ...state, + activeTabTypeByWorktree: { 'wt-1': 'terminal' } + }) + )[0] + const editorSnapshot = buildMobileSessionTabSnapshots( + makeState({ + ...state, + activeTabTypeByWorktree: { 'wt-1': 'editor' } + }) + )[0] + + expect(terminalSnapshot?.tabs).toMatchObject([ + { type: 'file', id: fileId, relativePath: 'src/app.ts', isActive: false } + ]) + expect(terminalSnapshot?.activeTabId).toBeNull() + expect(terminalSnapshot?.activeTabType).toBeNull() + expect(editorSnapshot?.tabs).toMatchObject([ + { type: 'file', id: fileId, relativePath: 'src/app.ts', isActive: true } + ]) + expect(editorSnapshot?.activeTabId).toBe(fileId) + expect(editorSnapshot?.activeTabType).toBe('file') + }) + it('keeps duplicate file ids scoped to their worktree', () => { const sharedRemotePath = '/home/dev/project/README.md' const previewId = `markdown-preview::${sharedRemotePath}` diff --git a/src/renderer/src/runtime/sync-runtime-graph.ts b/src/renderer/src/runtime/sync-runtime-graph.ts index 9588aad9193..cba5040b706 100644 --- a/src/renderer/src/runtime/sync-runtime-graph.ts +++ b/src/renderer/src/runtime/sync-runtime-graph.ts @@ -26,6 +26,7 @@ import { isTerminalLeafId, makePaneKey } from '../../../shared/stable-pane-id' import { isWebTerminalSurfaceTabId } from '../../../shared/terminal-surface-id' import { isClaudeManagementTitle } from '../../../shared/agent-detection' import type { + Tab, TabGroup, TabGroupLayoutNode, TerminalLayoutSnapshot, @@ -53,6 +54,10 @@ type OpenFileIndexes = { byWorktreeAndId: OpenFileByWorktreeAndId idsByWorktree: Map } +type FallbackEditorTabTarget = { + tabId: string + groupId: string | null +} type TabsProjectionCacheEntry = { tabs: NonNullable worktreeIdJson: string @@ -184,6 +189,8 @@ export type RuntimeMobileSessionSyncKey = { tabBarOrderByWorktree: AppState['tabBarOrderByWorktree'] activeFileId: AppState['activeFileId'] activeFileIdByWorktree: AppState['activeFileIdByWorktree'] + activeTabType: AppState['activeTabType'] + activeTabTypeByWorktree: AppState['activeTabTypeByWorktree'] activeTabId: AppState['activeTabId'] activeBrowserTabIdByWorktree: AppState['activeBrowserTabIdByWorktree'] agentStatusEpoch: number @@ -222,6 +229,8 @@ export function canSkipRuntimeMobileSessionSyncKeyBuild( state.tabBarOrderByWorktree === previousState.tabBarOrderByWorktree && state.activeFileId === previousState.activeFileId && state.activeFileIdByWorktree === previousState.activeFileIdByWorktree && + state.activeTabType === previousState.activeTabType && + state.activeTabTypeByWorktree === previousState.activeTabTypeByWorktree && state.browserTabsByWorktree === previousState.browserTabsByWorktree && state.browserPagesByWorkspace === previousState.browserPagesByWorkspace && state.activeBrowserTabIdByWorktree === previousState.activeBrowserTabIdByWorktree && @@ -274,6 +283,8 @@ export function getRuntimeMobileSessionSyncKey( tabBarOrderByWorktree: state.tabBarOrderByWorktree, activeFileId: state.activeFileId, activeFileIdByWorktree: state.activeFileIdByWorktree, + activeTabType: state.activeTabType, + activeTabTypeByWorktree: state.activeTabTypeByWorktree, activeTabId: state.activeTabId, activeBrowserTabIdByWorktree: state.activeBrowserTabIdByWorktree ?? EMPTY_ACTIVE_BROWSER_TAB_ID_BY_WORKTREE, @@ -478,6 +489,8 @@ export function runtimeMobileSessionSyncKeysEqual( a.tabBarOrderByWorktree === b.tabBarOrderByWorktree && a.activeFileId === b.activeFileId && a.activeFileIdByWorktree === b.activeFileIdByWorktree && + a.activeTabType === b.activeTabType && + a.activeTabTypeByWorktree === b.activeTabTypeByWorktree && a.activeTabId === b.activeTabId && a.activeBrowserTabIdByWorktree === b.activeBrowserTabIdByWorktree && a.agentStatusEpoch === b.agentStatusEpoch && @@ -670,6 +683,8 @@ export function buildMobileSessionTabSnapshots( browserIds: [...browserWorkspaceByIdForWorktree.keys()] }) const tabs: RuntimeMobileSessionSnapshotTab[] = [] + const emittedEditorFileIds = new Set() + const emittedEditorTabIds = new Set() for (const item of groupProjection.order) { if (item.type === 'terminal') { @@ -706,6 +721,8 @@ export function buildMobileSessionTabSnapshots( } else { tabs.push(buildMobileFileTab(state, file, item.tabId)) } + emittedEditorFileIds.add(file.id) + emittedEditorTabIds.add(item.tabId ?? item.id) } else if (item.type === 'browser') { const workspace = browserWorkspaceByIdForWorktree.get(item.id) if (!workspace) { @@ -715,7 +732,75 @@ 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)) + for (const unifiedTab of unifiedEditorTabs) { + if (emittedEditorTabIds.has(unifiedTab.id)) { + continue + } + const file = openFilesForWorktree.get(unifiedTab.entityId) + if (!file) { + continue + } + const markdown = buildMobileMarkdownTab( + state, + openFileIndexes.byWorktreeAndId, + editorDraftVersionByFileId, + file, + unifiedTab.id + ) + const fallbackTab = markdown ?? buildMobileFileTab(state, file, unifiedTab.id) + tabs.push(fallbackTab) + fallbackEditorTabs.push({ + tabId: fallbackTab.id, + groupId: unifiedTab.groupId + }) + emittedEditorTabIds.add(unifiedTab.id) + } + for (const file of openFilesForWorktree.values()) { + if (emittedEditorFileIds.has(file.id)) { + continue + } + if (unifiedEditorFileIds.has(file.id)) { + emittedEditorFileIds.add(file.id) + continue + } + const markdown = buildMobileMarkdownTab( + state, + openFileIndexes.byWorktreeAndId, + editorDraftVersionByFileId, + file + ) + const fallbackTab = markdown ?? buildMobileFileTab(state, file) + tabs.push(fallbackTab) + fallbackEditorTabs.push({ + tabId: fallbackTab.id, + groupId: null + }) + emittedEditorFileIds.add(file.id) + } + } + const active = tabs.find((tab) => tab.isActive) ?? null + const tabGroups = appendFallbackEditorTabsToGroups( + groupProjection.tabGroups, + state.groupsByWorktree[worktreeId] ?? [], + activeGroupId, + fallbackEditorTabs, + active?.id ?? null + ) + const tabGroupLayout = + tabGroups && tabGroups.length > 0 + ? pruneTabGroupLayout( + (state.layoutByWorktree ?? EMPTY_LAYOUT_BY_WORKTREE)[worktreeId], + new Set(tabGroups.map((group) => group.id)) + ) + : groupProjection.tabGroupLayout snapshots.push({ worktree: worktreeId, publicationEpoch: mobileSessionPublicationEpoch, @@ -723,10 +808,8 @@ export function buildMobileSessionTabSnapshots( activeGroupId, activeTabId: active?.id ?? null, activeTabType: active?.type ?? null, - ...(groupProjection.tabGroups && groupProjection.tabGroups.length > 0 - ? { tabGroups: groupProjection.tabGroups } - : {}), - ...(groupProjection.tabGroupLayout ? { tabGroupLayout: groupProjection.tabGroupLayout } : {}), + ...(tabGroups && tabGroups.length > 0 ? { tabGroups } : {}), + ...(tabGroupLayout ? { tabGroupLayout } : {}), tabs }) } @@ -734,6 +817,125 @@ export function buildMobileSessionTabSnapshots( return snapshots } +function isEditorSurfaceTab(tab: Pick): boolean { + // Why: mobile file snapshots can faithfully mirror ordinary edit/diff files; + // conflict review and check-details tabs require metadata this contract lacks. + return tab.contentType === 'editor' || tab.contentType === 'diff' +} + +function getEditorUnifiedTabsForWorktree( + state: Pick, + worktreeId: string +): Tab[] { + return (state.unifiedTabsByWorktree[worktreeId] ?? []).filter(isEditorSurfaceTab) +} + +function applyUnifiedEditorTabIdsToLegacyOrder( + order: readonly VisibleTabRef[], + state: Pick, + worktreeId: string +): VisibleTabRef[] { + const unifiedEditorTabs = getEditorUnifiedTabsForWorktree(state, worktreeId) + if (unifiedEditorTabs.length === 0) { + return [...order] + } + const firstUnifiedTabByFileId = new Map() + for (const tab of unifiedEditorTabs) { + if (!firstUnifiedTabByFileId.has(tab.entityId)) { + firstUnifiedTabByFileId.set(tab.entityId, tab.id) + } + } + return order.map((item) => { + if (item.type !== 'editor' || item.tabId) { + return item + } + const tabId = firstUnifiedTabByFileId.get(item.id) + return tabId ? { ...item, tabId } : item + }) +} + +function appendFallbackEditorTabsToGroups( + tabGroups: RuntimeMobileSessionTabGroup[] | undefined, + sourceGroups: readonly TabGroup[], + activeGroupId: string | null, + fallbackTabs: readonly FallbackEditorTabTarget[], + activeTabId: string | null +): RuntimeMobileSessionTabGroup[] | undefined { + if (fallbackTabs.length === 0) { + return tabGroups + } + const result = [...(tabGroups ?? [])] + const sourceGroupsById = new Map(sourceGroups.map((group) => [group.id, group])) + const groupIndexById = new Map(result.map((group, index) => [group.id, index])) + const firstTargetGroupId = + result[0]?.id ?? + (activeGroupId && sourceGroupsById.has(activeGroupId) ? activeGroupId : null) ?? + sourceGroups[0]?.id ?? + null + const fallbackTabIdSet = new Set(fallbackTabs.map((tab) => tab.tabId)) + + for (const fallback of fallbackTabs) { + const targetGroupId = + fallback.groupId ?? + (activeGroupId && (groupIndexById.has(activeGroupId) || sourceGroupsById.has(activeGroupId)) + ? activeGroupId + : firstTargetGroupId) + if (!targetGroupId) { + continue + } + let targetIndex = groupIndexById.get(targetGroupId) + if (targetIndex === undefined) { + const sourceGroup = sourceGroupsById.get(targetGroupId) + const group: RuntimeMobileSessionTabGroup = { + id: targetGroupId, + activeTabId: sourceGroup?.activeTabId ?? null, + tabOrder: [], + recentTabIds: sourceGroup?.recentTabIds ?? [] + } + targetIndex = result.length + groupIndexById.set(targetGroupId, targetIndex) + result.push(group) + } + const group = result[targetIndex]! + if (!group.tabOrder.includes(fallback.tabId)) { + result[targetIndex] = { + ...group, + tabOrder: [...group.tabOrder, fallback.tabId] + } + } + } + + if (result.length === 0) { + return tabGroups + } + + const activeFallbackTabId = activeTabId && fallbackTabIdSet.has(activeTabId) ? activeTabId : null + + return result.map((group) => { + const tabOrder = [...group.tabOrder] + const tabOrderSet = new Set(tabOrder) + const activeFallbackTabIdForGroup = + activeFallbackTabId && tabOrderSet.has(activeFallbackTabId) ? activeFallbackTabId : null + const activeTabIdForGroup = + activeFallbackTabIdForGroup ?? + (group.activeTabId && tabOrderSet.has(group.activeTabId) ? group.activeTabId : null) + const recentTabIds = (group.recentTabIds ?? []).filter((tabId) => tabOrderSet.has(tabId)) + if ( + activeFallbackTabId && + tabOrderSet.has(activeFallbackTabId) && + !recentTabIds.includes(activeFallbackTabId) + ) { + recentTabIds.push(activeFallbackTabId) + } + return { + ...group, + activeTabId: activeTabIdForGroup, + tabOrder, + recentTabIds + } + }) +} + function isRemoteRuntimePtyId(ptyId: string | null | undefined): boolean { return typeof ptyId === 'string' && parseRemoteRuntimePtyId(ptyId) !== null } @@ -859,9 +1061,13 @@ function buildMobileSessionGroupProjection( const groups = state.groupsByWorktree[worktreeId] ?? [] if (groups.length === 0) { return { - order: getActiveTabNavOrder(state, worktreeId, { - editorIds: ids.editorIds - }) + order: applyUnifiedEditorTabIdsToLegacyOrder( + getActiveTabNavOrder(state, worktreeId, { + editorIds: ids.editorIds + }), + state, + worktreeId + ) } } @@ -1137,7 +1343,7 @@ function buildMobileMarkdownTab( isDirty: file.isDirty || sourceFile.isDirty, isActive: unifiedTabId ? isUnifiedTabActiveInActiveGroup(state, file.worktreeId, unifiedTabId) - : state.activeFileId === file.id, + : isFileActiveEditorSurface(state, file), sourceFileId: sourceFile.id, sourceFilePath: sourceFile.filePath, sourceRelativePath: sourceFile.relativePath, @@ -1165,10 +1371,24 @@ function buildMobileFileTab( isDirty: file.isDirty, isActive: unifiedTabId ? isUnifiedTabActiveInActiveGroup(state, file.worktreeId, unifiedTabId) - : state.activeFileId === file.id + : isFileActiveEditorSurface(state, file) } } +function isFileActiveEditorSurface( + state: Pick< + AppState, + 'activeFileId' | 'activeFileIdByWorktree' | 'activeTabType' | 'activeTabTypeByWorktree' + >, + file: Pick +): boolean { + const activeType = state.activeTabTypeByWorktree?.[file.worktreeId] ?? state.activeTabType + return ( + activeType === 'editor' && + (state.activeFileIdByWorktree?.[file.worktreeId] ?? state.activeFileId) === file.id + ) +} + function isMobileFileDiffSource( diffSource: AppState['openFiles'][number]['diffSource'] ): diffSource is 'staged' | 'unstaged' {