mirror of
https://github.com/stablyai/orca
synced 2026-04-21 14:17:16 +00:00
fix: prevent split-group container teardown when switching worktrees (#726)
This commit is contained in:
parent
19d22b0667
commit
48c24935c3
3 changed files with 162 additions and 14 deletions
|
|
@ -30,6 +30,10 @@ import BrowserPane, { destroyPersistentWebview } from './browser-pane/BrowserPan
|
|||
import { reconcileTabOrder } from './tab-bar/reconcile-order'
|
||||
import TabGroupSplitLayout from './tab-group/TabGroupSplitLayout'
|
||||
import { shouldAutoCreateInitialTerminal } from './terminal/initial-terminal'
|
||||
import {
|
||||
getEffectiveLayoutForWorktree as getEffectiveLayout,
|
||||
anyMountedWorktreeHasLayout as computeAnyMountedWorktreeHasLayout
|
||||
} from './terminal/split-group-mount'
|
||||
import CodexRestartChip from './CodexRestartChip'
|
||||
|
||||
const EditorPanel = lazy(() => import('./editor/EditorPanel'))
|
||||
|
|
@ -106,18 +110,8 @@ function Terminal(): React.JSX.Element | null {
|
|||
? (browserTabsByWorktree[activeWorktreeId] ?? [])
|
||||
: []
|
||||
const getEffectiveLayoutForWorktree = useCallback(
|
||||
(worktreeId: string) => {
|
||||
const layout = layoutByWorktree[worktreeId]
|
||||
if (layout) {
|
||||
return layout
|
||||
}
|
||||
const groups = groupsByWorktree[worktreeId] ?? []
|
||||
const fallbackGroupId = activeGroupIdByWorktree[worktreeId] ?? groups[0]?.id ?? null
|
||||
if (!fallbackGroupId) {
|
||||
return undefined
|
||||
}
|
||||
return { type: 'leaf', groupId: fallbackGroupId } as const
|
||||
},
|
||||
(worktreeId: string) =>
|
||||
getEffectiveLayout(worktreeId, layoutByWorktree, groupsByWorktree, activeGroupIdByWorktree),
|
||||
[activeGroupIdByWorktree, groupsByWorktree, layoutByWorktree]
|
||||
)
|
||||
const effectiveActiveLayout = activeWorktreeId
|
||||
|
|
@ -215,6 +209,13 @@ function Terminal(): React.JSX.Element | null {
|
|||
mountedWorktreeIdsRef.current.delete(id)
|
||||
}
|
||||
}
|
||||
const anyMountedWorktreeHasLayout = computeAnyMountedWorktreeHasLayout(
|
||||
allWorktrees.map((wt) => wt.id),
|
||||
mountedWorktreeIdsRef.current,
|
||||
layoutByWorktree,
|
||||
groupsByWorktree,
|
||||
activeGroupIdByWorktree
|
||||
)
|
||||
// Auto-create first tab when worktree activates
|
||||
useEffect(() => {
|
||||
if (!workspaceSessionReady) {
|
||||
|
|
@ -826,8 +827,10 @@ function Terminal(): React.JSX.Element | null {
|
|||
— tab groups + terminal extend to the top of the window instead.
|
||||
The old summary label (workspace / active surface) is removed. */}
|
||||
|
||||
{effectiveActiveLayout ? (
|
||||
<div className="relative flex flex-1 min-w-0 min-h-0 overflow-hidden">
|
||||
{anyMountedWorktreeHasLayout ? (
|
||||
<div
|
||||
className={`relative flex flex-1 min-w-0 min-h-0 overflow-hidden${effectiveActiveLayout ? '' : ' hidden'}`}
|
||||
>
|
||||
{/* Why: each mounted worktree surface is absolutely positioned so we
|
||||
can preserve hidden trees without reflowing the active one. Keep
|
||||
a relative anchor here so those panes size to the workspace body
|
||||
|
|
|
|||
100
src/renderer/src/components/terminal/split-group-mount.test.ts
Normal file
100
src/renderer/src/components/terminal/split-group-mount.test.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { getEffectiveLayoutForWorktree, anyMountedWorktreeHasLayout } from './split-group-mount'
|
||||
import type { TabGroup, TabGroupLayoutNode } from '../../../../shared/types'
|
||||
|
||||
function makeGroup(id: string, worktreeId: string): TabGroup {
|
||||
return { id, worktreeId, activeTabId: null, tabOrder: [] }
|
||||
}
|
||||
|
||||
describe('getEffectiveLayoutForWorktree', () => {
|
||||
it('returns the explicit layout when one exists', () => {
|
||||
const layout: TabGroupLayoutNode = { type: 'leaf', groupId: 'g1' }
|
||||
const result = getEffectiveLayoutForWorktree('wt-1', { 'wt-1': layout }, {}, {})
|
||||
expect(result).toBe(layout)
|
||||
})
|
||||
|
||||
it('falls back to a synthetic leaf from the active group', () => {
|
||||
const result = getEffectiveLayoutForWorktree(
|
||||
'wt-1',
|
||||
{},
|
||||
{ 'wt-1': [makeGroup('g1', 'wt-1'), makeGroup('g2', 'wt-1')] },
|
||||
{ 'wt-1': 'g2' }
|
||||
)
|
||||
expect(result).toEqual({ type: 'leaf', groupId: 'g2' })
|
||||
})
|
||||
|
||||
it('falls back to the first group when no active group is set', () => {
|
||||
const result = getEffectiveLayoutForWorktree(
|
||||
'wt-1',
|
||||
{},
|
||||
{ 'wt-1': [makeGroup('g1', 'wt-1')] },
|
||||
{}
|
||||
)
|
||||
expect(result).toEqual({ type: 'leaf', groupId: 'g1' })
|
||||
})
|
||||
|
||||
it('returns undefined when the worktree has no layout and no groups', () => {
|
||||
const result = getEffectiveLayoutForWorktree('wt-1', {}, {}, {})
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('anyMountedWorktreeHasLayout', () => {
|
||||
const layout: TabGroupLayoutNode = { type: 'leaf', groupId: 'g1' }
|
||||
|
||||
it('returns true when the active worktree has a layout', () => {
|
||||
const result = anyMountedWorktreeHasLayout(
|
||||
['wt-1'],
|
||||
new Set(['wt-1']),
|
||||
{ 'wt-1': layout },
|
||||
{},
|
||||
{}
|
||||
)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('returns true when only a non-active mounted worktree has a layout (the bug fix)', () => {
|
||||
// wt-1 has a layout, wt-2 (the newly active one) does not
|
||||
const result = anyMountedWorktreeHasLayout(
|
||||
['wt-1', 'wt-2'],
|
||||
new Set(['wt-1', 'wt-2']),
|
||||
{ 'wt-1': layout },
|
||||
{},
|
||||
{}
|
||||
)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when no mounted worktree has a layout', () => {
|
||||
const result = anyMountedWorktreeHasLayout(
|
||||
['wt-1', 'wt-2'],
|
||||
new Set(['wt-1', 'wt-2']),
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('ignores worktrees that exist but are not mounted', () => {
|
||||
const result = anyMountedWorktreeHasLayout(
|
||||
['wt-1', 'wt-2'],
|
||||
new Set(['wt-2']), // only wt-2 is mounted
|
||||
{ 'wt-1': layout }, // only wt-1 has a layout
|
||||
{},
|
||||
{}
|
||||
)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('considers fallback groups when no explicit layout exists', () => {
|
||||
const result = anyMountedWorktreeHasLayout(
|
||||
['wt-1'],
|
||||
new Set(['wt-1']),
|
||||
{},
|
||||
{ 'wt-1': [makeGroup('g1', 'wt-1')] },
|
||||
{}
|
||||
)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
})
|
||||
45
src/renderer/src/components/terminal/split-group-mount.ts
Normal file
45
src/renderer/src/components/terminal/split-group-mount.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import type { TabGroup, TabGroupLayoutNode } from '../../../../shared/types'
|
||||
|
||||
/**
|
||||
* Derive the effective layout for a worktree: either its explicit layout
|
||||
* or a synthetic leaf wrapping its first/active group.
|
||||
*/
|
||||
export function getEffectiveLayoutForWorktree(
|
||||
worktreeId: string,
|
||||
layoutByWorktree: Record<string, TabGroupLayoutNode | undefined>,
|
||||
groupsByWorktree: Record<string, TabGroup[]>,
|
||||
activeGroupIdByWorktree: Record<string, string | undefined>
|
||||
): TabGroupLayoutNode | undefined {
|
||||
const layout = layoutByWorktree[worktreeId]
|
||||
if (layout) {
|
||||
return layout
|
||||
}
|
||||
const groups = groupsByWorktree[worktreeId] ?? []
|
||||
const fallbackGroupId = activeGroupIdByWorktree[worktreeId] ?? groups[0]?.id ?? null
|
||||
if (!fallbackGroupId) {
|
||||
return undefined
|
||||
}
|
||||
return { type: 'leaf', groupId: fallbackGroupId } as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if any mounted worktree has a split-group layout.
|
||||
*
|
||||
* Why: the split-group container hosts ALL mounted worktrees' pane trees.
|
||||
* Gating it on only the *active* worktree's layout causes the entire tree
|
||||
* to unmount when switching to a newly-activated worktree that has no
|
||||
* groups yet — destroying PaneManagers, xterm buffers, and PTY connections.
|
||||
*/
|
||||
export function anyMountedWorktreeHasLayout(
|
||||
allWorktreeIds: string[],
|
||||
mountedWorktreeIds: ReadonlySet<string>,
|
||||
layoutByWorktree: Record<string, TabGroupLayoutNode | undefined>,
|
||||
groupsByWorktree: Record<string, TabGroup[]>,
|
||||
activeGroupIdByWorktree: Record<string, string | undefined>
|
||||
): boolean {
|
||||
return allWorktreeIds.some(
|
||||
(id) =>
|
||||
mountedWorktreeIds.has(id) &&
|
||||
getEffectiveLayoutForWorktree(id, layoutByWorktree, groupsByWorktree, activeGroupIdByWorktree)
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue