Fix mobile file tab close sync

Close mobile session file tabs through the underlying editor file so desktop stops republishing closed docs/files.
This commit is contained in:
Jinwoo Hong
2026-06-21 19:41:48 -07:00
committed by GitHub
parent d9b2d89406
commit a8feafe92a
3 changed files with 164 additions and 2 deletions
+5 -1
View File
@@ -78,6 +78,7 @@ import {
releaseBrowserAutomationVisibility
} from '@/components/browser-pane/browser-automation-visibility'
import { attachMobileMarkdownBridge } from '@/runtime/mobile-markdown-bridge'
import { closeMobileSessionTabInStore } from '@/runtime/mobile-session-tab-close'
import { createWorktreeChangeRefreshQueue } from './worktree-change-refresh-queue'
import { subscribeRuntimeClientEvents } from '@/runtime/runtime-client-events'
import { createRuntimeClientEventsSync } from './runtime-client-events-sync'
@@ -1606,7 +1607,10 @@ export function useIpcEvents(): void {
guardPinnedTabClose({
isPinned: isPinnedSessionTab(store, worktreeId, tabId),
tabLabel: resolvePinnedTabLabel(store, worktreeId, tabId),
onClose: () => useAppStore.getState().closeUnifiedTab(tabId)
onClose: () => {
const currentStore = useAppStore.getState()
closeMobileSessionTabInStore(currentStore, worktreeId, tabId)
}
})
})
)
@@ -0,0 +1,29 @@
import type { AppState } from '../store/types'
const EDITOR_SESSION_CONTENT_TYPES = new Set(['editor', 'diff', 'conflict-review', 'check-details'])
export function closeMobileSessionTabInStore(
store: Pick<AppState, 'unifiedTabsByWorktree' | 'openFiles' | 'closeFile' | 'closeUnifiedTab'>,
worktreeId: string,
tabId: string
): boolean {
const unifiedTab = (store.unifiedTabsByWorktree[worktreeId] ?? []).find(
(tab) => tab.id === tabId || tab.entityId === tabId
)
if (unifiedTab && EDITOR_SESSION_CONTENT_TYPES.has(unifiedTab.contentType)) {
store.closeFile(unifiedTab.entityId)
return true
}
const fallbackFile = store.openFiles.find(
(file) => file.worktreeId === worktreeId && file.id === tabId
)
if (fallbackFile) {
// Why: mobile may receive fallback file-id tabs from openFiles after the
// unified tab wrapper has already closed; close the source file too.
store.closeFile(fallbackFile.id)
return true
}
return store.closeUnifiedTab(tabId) !== null
}
+130 -1
View File
@@ -3,6 +3,8 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
import type { Tab, TabGroup } from '../../../../shared/types'
import type * as AgentStatusModule from '@/lib/agent-status'
import { FLOATING_TERMINAL_WORKTREE_ID, getDefaultUIState } from '../../../../shared/constants'
import { buildMobileSessionTabSnapshots } from '../../runtime/sync-runtime-graph'
import { closeMobileSessionTabInStore } from '../../runtime/mobile-session-tab-close'
// Mock sonner (imported by repos.ts)
vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } }))
@@ -102,7 +104,7 @@ const mockApi = {
// @ts-expect-error -- mock
globalThis.window = { api: mockApi }
import { createTestStore } from './store-test-helpers'
import { createTestStore, makeOpenFile, makeTabGroup, makeUnifiedTab } from './store-test-helpers'
const WT = 'repo1::/tmp/feature'
@@ -295,6 +297,133 @@ describe('TabsSlice', () => {
expect(result).toBeNull()
})
it('removes a mobile-closed markdown tab from open files so it is not republished', () => {
const groupId = 'editor-group'
const file = makeOpenFile({
id: '/tmp/feature/README.md',
filePath: '/tmp/feature/README.md',
relativePath: 'README.md',
language: 'markdown',
worktreeId: WT
})
const tab = makeUnifiedTab({
id: 'readme-unified',
entityId: file.id,
contentType: 'editor',
label: 'README.md',
worktreeId: WT,
groupId
})
store.setState({
openFiles: [file],
unifiedTabsByWorktree: { [WT]: [tab] },
groupsByWorktree: {
[WT]: [
makeTabGroup({
id: groupId,
worktreeId: WT,
activeTabId: tab.id,
tabOrder: [tab.id],
recentTabIds: [tab.id]
})
]
},
activeGroupIdByWorktree: { [WT]: groupId },
activeFileId: file.id,
activeFileIdByWorktree: { [WT]: file.id },
activeWorktreeId: WT,
activeTabType: 'editor',
activeTabTypeByWorktree: { [WT]: 'editor' }
})
expect(buildMobileSessionTabSnapshots(store.getState())[0]?.tabs).toMatchObject([
{ id: tab.id, type: 'markdown', filePath: file.filePath }
])
expect(closeMobileSessionTabInStore(store.getState(), WT, tab.id)).toBe(true)
expect(store.getState().openFiles).toEqual([])
expect(buildMobileSessionTabSnapshots(store.getState())[0]?.tabs ?? []).toEqual([])
})
it('removes a mobile-closed regular file tab from open files so fallback closes do not resurrect', () => {
const groupId = 'editor-group'
const file = makeOpenFile({
id: '/tmp/feature/src/app.ts',
filePath: '/tmp/feature/src/app.ts',
relativePath: 'src/app.ts',
language: 'typescript',
worktreeId: WT
})
const tab = makeUnifiedTab({
id: 'app-unified',
entityId: file.id,
contentType: 'editor',
label: 'app.ts',
worktreeId: WT,
groupId
})
store.setState({
openFiles: [file],
unifiedTabsByWorktree: { [WT]: [tab] },
groupsByWorktree: {
[WT]: [
makeTabGroup({
id: groupId,
worktreeId: WT,
activeTabId: tab.id,
tabOrder: [tab.id],
recentTabIds: [tab.id]
})
]
},
activeGroupIdByWorktree: { [WT]: groupId },
activeFileId: file.id,
activeFileIdByWorktree: { [WT]: file.id },
activeWorktreeId: WT,
activeTabType: 'editor',
activeTabTypeByWorktree: { [WT]: 'editor' }
})
expect(buildMobileSessionTabSnapshots(store.getState())[0]?.tabs).toMatchObject([
{ id: tab.id, type: 'file', filePath: file.filePath }
])
expect(closeMobileSessionTabInStore(store.getState(), WT, tab.id)).toBe(true)
expect(store.getState().openFiles).toEqual([])
expect(buildMobileSessionTabSnapshots(store.getState())[0]?.tabs ?? []).toEqual([])
})
it('closes a mobile fallback file-id tab after the unified wrapper is already gone', () => {
const file = makeOpenFile({
id: '/tmp/feature/src/app.ts',
filePath: '/tmp/feature/src/app.ts',
relativePath: 'src/app.ts',
language: 'typescript',
worktreeId: WT
})
store.setState({
openFiles: [file],
unifiedTabsByWorktree: { [WT]: [] },
groupsByWorktree: { [WT]: [] },
activeFileId: file.id,
activeFileIdByWorktree: { [WT]: file.id },
activeWorktreeId: WT,
activeTabType: 'editor',
activeTabTypeByWorktree: { [WT]: 'editor' }
})
expect(buildMobileSessionTabSnapshots(store.getState())[0]?.tabs).toMatchObject([
{ id: file.id, type: 'file', filePath: file.filePath }
])
expect(closeMobileSessionTabInStore(store.getState(), WT, file.id)).toBe(true)
expect(store.getState().openFiles).toEqual([])
expect(buildMobileSessionTabSnapshots(store.getState())[0]?.tabs ?? []).toEqual([])
})
it('activates the previously-active tab (MRU) instead of the visual neighbor', () => {
const t1 = store.getState().createUnifiedTab(WT, 'terminal')
const t2 = store.getState().createUnifiedTab(WT, 'terminal')