mirror of
https://github.com/stablyai/orca.git
synced 2026-10-26 00:03:55 +00:00
fix: reuse existing right split for emulator
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import type { TabGroupLayoutNode } from '../../../shared/types'
|
||||
|
||||
type SearchResult = {
|
||||
containsSource: boolean
|
||||
reusableGroupId: string | null
|
||||
}
|
||||
|
||||
function firstLeafGroupId(node: TabGroupLayoutNode): string {
|
||||
if (node.type === 'leaf') {
|
||||
return node.groupId
|
||||
}
|
||||
return firstLeafGroupId(node.first)
|
||||
}
|
||||
|
||||
function findReusableRightSplitTarget(
|
||||
node: TabGroupLayoutNode,
|
||||
sourceGroupId: string
|
||||
): SearchResult {
|
||||
if (node.type === 'leaf') {
|
||||
return {
|
||||
containsSource: node.groupId === sourceGroupId,
|
||||
reusableGroupId: null
|
||||
}
|
||||
}
|
||||
|
||||
const first = findReusableRightSplitTarget(node.first, sourceGroupId)
|
||||
if (first.containsSource) {
|
||||
return {
|
||||
containsSource: true,
|
||||
// Why: prefer the closest existing right pane; it preserves nested split intent.
|
||||
reusableGroupId:
|
||||
first.reusableGroupId ??
|
||||
(node.direction === 'horizontal' ? firstLeafGroupId(node.second) : null)
|
||||
}
|
||||
}
|
||||
|
||||
const second = findReusableRightSplitTarget(node.second, sourceGroupId)
|
||||
return {
|
||||
containsSource: second.containsSource,
|
||||
reusableGroupId: second.reusableGroupId
|
||||
}
|
||||
}
|
||||
|
||||
export function findReusableRightSplitGroupId(
|
||||
layout: TabGroupLayoutNode | undefined,
|
||||
sourceGroupId: string
|
||||
): string | null {
|
||||
if (!layout) {
|
||||
return null
|
||||
}
|
||||
return findReusableRightSplitTarget(layout, sourceGroupId).reusableGroupId
|
||||
}
|
||||
@@ -10,6 +10,7 @@ const mockStoreState = vi.hoisted(() => ({
|
||||
dropUnifiedTab: vi.fn(),
|
||||
focusGroup: vi.fn(),
|
||||
groupsByWorktree: {} as Record<string, { id: string }[]>,
|
||||
layoutByWorktree: {} as Record<string, unknown>,
|
||||
settings: { mobileEmulatorEnabled: true },
|
||||
setActiveTab: vi.fn(),
|
||||
setActiveTabType: vi.fn(),
|
||||
@@ -34,6 +35,7 @@ describe('ensureSimulatorTab', () => {
|
||||
mockStoreState.activeGroupIdByWorktree = { 'wt-1': 'group-1' }
|
||||
mockStoreState.activeWorktreeId = 'wt-1'
|
||||
mockStoreState.groupsByWorktree = { 'wt-1': [{ id: 'group-1' }] }
|
||||
mockStoreState.layoutByWorktree = { 'wt-1': { type: 'leaf', groupId: 'group-1' } }
|
||||
mockStoreState.settings = { mobileEmulatorEnabled: true }
|
||||
mockStoreState.unifiedTabsByWorktree = {
|
||||
'wt-1': [{ id: 'sim-1', groupId: 'group-1', contentType: 'simulator' }]
|
||||
@@ -116,6 +118,37 @@ describe('ensureSimulatorTab', () => {
|
||||
expect(mockStoreState.setActiveTabType).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reuses an existing right split when requested', async () => {
|
||||
mockStoreState.groupsByWorktree = { 'wt-1': [{ id: 'group-1' }, { id: 'group-2' }] }
|
||||
mockStoreState.layoutByWorktree = {
|
||||
'wt-1': {
|
||||
type: 'split',
|
||||
direction: 'horizontal',
|
||||
first: { type: 'leaf', groupId: 'group-1' },
|
||||
second: { type: 'leaf', groupId: 'group-2' }
|
||||
}
|
||||
}
|
||||
mockStoreState.unifiedTabsByWorktree = { 'wt-1': [] }
|
||||
mockStoreState.createUnifiedTab.mockReturnValue({
|
||||
id: 'sim-2',
|
||||
groupId: 'group-2',
|
||||
contentType: 'simulator'
|
||||
})
|
||||
const { ensureSimulatorTab } = await import('./ensure-simulator-tab')
|
||||
|
||||
expect(ensureSimulatorTab('wt-1', { placement: 'rightSplit' })).toBe('sim-2')
|
||||
|
||||
expect(mockStoreState.createUnifiedTabInSplit).not.toHaveBeenCalled()
|
||||
expect(mockStoreState.createUnifiedTab).toHaveBeenCalledWith('wt-1', 'simulator', {
|
||||
label: 'Mobile Emulator',
|
||||
targetGroupId: 'group-2',
|
||||
activate: true
|
||||
})
|
||||
expect(mockStoreState.activateTab).toHaveBeenCalledWith('sim-2')
|
||||
expect(mockStoreState.focusGroup).toHaveBeenCalledWith('wt-1', 'group-2')
|
||||
expect(mockStoreState.setActiveTabType).toHaveBeenCalledWith('simulator')
|
||||
})
|
||||
|
||||
it('falls back to the source group when atomic right split creation fails', async () => {
|
||||
mockStoreState.unifiedTabsByWorktree = { 'wt-1': [] }
|
||||
mockStoreState.createUnifiedTabInSplit.mockReturnValue(null)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useAppStore } from '@/store'
|
||||
import { findReusableRightSplitGroupId } from './emulator-right-split-target'
|
||||
import { cancelPendingSimulatorPaneShutdown } from './simulator-pane-shutdown-scheduler'
|
||||
import { shouldShutdownSimulatorForPaneUnmountFromTabs } from './simulator-tab-shutdown'
|
||||
|
||||
@@ -58,6 +59,22 @@ export function ensureSimulatorTab(
|
||||
}
|
||||
|
||||
if (options?.placement === 'rightSplit' && shouldSurface) {
|
||||
const reusableRightGroupId = findReusableRightSplitGroupId(
|
||||
store.layoutByWorktree[worktreeId],
|
||||
sourceGroupId
|
||||
)
|
||||
if (reusableRightGroupId) {
|
||||
const tab = store.createUnifiedTab(worktreeId, 'simulator', {
|
||||
label: 'Mobile Emulator',
|
||||
targetGroupId: reusableRightGroupId,
|
||||
activate: true
|
||||
})
|
||||
store.activateTab(tab.id)
|
||||
store.setActiveTabType('simulator')
|
||||
store.focusGroup(worktreeId, tab.groupId)
|
||||
return tab.id
|
||||
}
|
||||
|
||||
// Why: publish the simulator directly in its split group. A two-step
|
||||
// create-then-move can persist the midpoint during dev reload/HMR.
|
||||
const splitTab = store.createUnifiedTabInSplit(
|
||||
|
||||
Reference in New Issue
Block a user