mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
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:
@@ -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
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user