mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
A pane close by its terminal handle closes only that pane, even before the window lists it (dark, layout-owner build only) (#30920)
* fix(runtime): main's relisting for a close carries main's tab layout, so a pane close stays a pane close (dark, layout-owner build only) Co-Authored-By: Claude <noreply@anthropic.com> * fix(layout-owner): the relist touches only a row it added, with its own copy of the layout * fix(layout-owner): store the relist's layout only when it changed anything --------- Co-authored-by: E2E Test <e2e@test.local> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
E2E Test
parent
f1135dd4ca
commit
af6fe23623
@@ -0,0 +1,6 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
// The same no-widening table, in a layout-owner build: a close must not widen there either.
|
||||
vi.mock('../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
|
||||
|
||||
await import('./orca-runtime-terminal-close-no-widening.test')
|
||||
@@ -30,6 +30,7 @@ type Condition =
|
||||
| 'no-live-pty'
|
||||
| 'kill-fails'
|
||||
| 'nothing-published'
|
||||
| 'pane-unlisted'
|
||||
|
||||
/** The desktop renderer's split tab as it publishes it, with the given PTY bindings. */
|
||||
function syncRendererSplit(
|
||||
@@ -136,6 +137,42 @@ function arrange(entry: Entry, condition: Condition): CloseContinuityHarness {
|
||||
]
|
||||
})
|
||||
}
|
||||
if (condition === 'pane-unlisted') {
|
||||
// The renderer's list lags main: a sibling row with no layout yet, and none for the closed pane.
|
||||
harness.runtime.syncWindowGraph(1, {
|
||||
tabs: [
|
||||
{
|
||||
tabId: TAB_ID,
|
||||
worktreeId: WORKTREE_ID,
|
||||
title: 'Fixture shell',
|
||||
activeLeafId: SIBLING_LEAF_ID,
|
||||
layout: { type: 'leaf' as const, leafId: SIBLING_LEAF_ID }
|
||||
}
|
||||
],
|
||||
leaves: [],
|
||||
mobileSessionTabs: [
|
||||
{
|
||||
worktree: WORKTREE_ID,
|
||||
publicationEpoch: 'renderer:pane-unlisted',
|
||||
snapshotVersion: 4,
|
||||
activeGroupId: null,
|
||||
activeTabId: `${TAB_ID}::${SIBLING_LEAF_ID}`,
|
||||
activeTabType: 'terminal' as const,
|
||||
tabs: [
|
||||
{
|
||||
type: 'terminal' as const,
|
||||
id: `${TAB_ID}::${SIBLING_LEAF_ID}`,
|
||||
parentTabId: TAB_ID,
|
||||
leafId: SIBLING_LEAF_ID,
|
||||
ptyId: SIBLING_PTY_ID,
|
||||
title: 'Fixture shell',
|
||||
isActive: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
if (condition === 'kill-fails') {
|
||||
harness.kill.mockReturnValue(false)
|
||||
}
|
||||
@@ -221,6 +258,7 @@ const rows: [Entry, Condition][] = [
|
||||
['cli-pty', 'unbound-sibling'],
|
||||
// An owner copy with no panes once read as "one pane", so the close took the whole tab.
|
||||
['cli-pty', 'nothing-published'],
|
||||
['cli-pty', 'pane-unlisted'],
|
||||
['phone-desktop', 'normal'],
|
||||
['phone-desktop', 'pinned'],
|
||||
['phone-desktop', 'unbound-sibling'],
|
||||
|
||||
@@ -6,6 +6,9 @@ import type { WorkspaceSessionState } from '../../../../shared/workspace-session
|
||||
import { makePaneKey, parsePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import { terminalLayoutContainsLeaf } from '../../../../shared/workspace-session-pane-ownership'
|
||||
import type { ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import type { RuntimeMobileSessionTabsSnapshot } from '../../../../shared/runtime-types'
|
||||
import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types'
|
||||
import { cloneTerminalLayoutSnapshot } from '../../mobile-session-layout-projection'
|
||||
|
||||
type RelistedPane = {
|
||||
tabId: string
|
||||
@@ -28,7 +31,11 @@ export function relistPaneMainBinds(
|
||||
host: {
|
||||
hostId: (tabId: string) => ExecutionHostId | undefined
|
||||
session: (hostId: ExecutionHostId) => WorkspaceSessionState | null | undefined
|
||||
relist: (pty: RuntimePtyWorktreeRecord, pane: RelistedPane) => void
|
||||
relist: (
|
||||
pty: RuntimePtyWorktreeRecord,
|
||||
pane: RelistedPane,
|
||||
layout: TerminalLayoutSnapshot
|
||||
) => void
|
||||
}
|
||||
): void {
|
||||
if (
|
||||
@@ -55,12 +62,44 @@ export function relistPaneMainBinds(
|
||||
) {
|
||||
return
|
||||
}
|
||||
host.relist(pty, {
|
||||
tabId: pane.tabId,
|
||||
leafId: pane.leafId,
|
||||
title: null,
|
||||
activate: false,
|
||||
selectIfNoActiveTab: false,
|
||||
notify: false
|
||||
})
|
||||
host.relist(
|
||||
pty,
|
||||
{
|
||||
tabId: pane.tabId,
|
||||
leafId: pane.leafId,
|
||||
title: null,
|
||||
activate: false,
|
||||
selectIfNoActiveTab: false,
|
||||
notify: false
|
||||
},
|
||||
layout
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Every row of the relisted pane's tab takes main's layout. A row built for the pane alone holds
|
||||
* only that pane, and a close reading it would take the pane for the tab's last. Under the switch
|
||||
* main writes a split before the window does, so its layout is the tab's newest.
|
||||
*/
|
||||
export function withMainTabLayout(
|
||||
snapshot: RuntimeMobileSessionTabsSnapshot,
|
||||
relisted: { tabId: string; ptyId: string },
|
||||
layout: TerminalLayoutSnapshot
|
||||
): RuntimeMobileSessionTabsSnapshot {
|
||||
const inTab = (row: RuntimeMobileSessionTabsSnapshot['tabs'][number]): boolean =>
|
||||
row.type === 'terminal' && row.parentTabId === relisted.tabId
|
||||
// A relist the membership gate refused leaves the window's rows as they were.
|
||||
if (
|
||||
!snapshot.tabs.some(
|
||||
(row) => inTab(row) && row.type === 'terminal' && row.ptyId === relisted.ptyId
|
||||
)
|
||||
) {
|
||||
return snapshot
|
||||
}
|
||||
return {
|
||||
...snapshot,
|
||||
tabs: snapshot.tabs.map((row) =>
|
||||
inTab(row) ? { ...row, parentLayout: cloneTerminalLayoutSnapshot(layout) } : row
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable unicorn/no-useless-spread */
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
|
||||
import { relistPaneMainBinds } from './layout-owner-close-relisting'
|
||||
import { relistPaneMainBinds, withMainTabLayout } from './layout-owner-close-relisting'
|
||||
import { OrcaRuntimeWithCreateRuntimeOwnedMobileSessionTerminal } from '../launch/mobile-terminal-owned-create'
|
||||
import type { OrcaRuntimeWithResolveTerminalSplitSourceAuthority } from '../launch/terminal-split-source-authority'
|
||||
import type {
|
||||
@@ -145,8 +145,15 @@ export abstract class OrcaRuntimeWithWaitForMobileTerminalSurface extends OrcaRu
|
||||
relistPaneMainBinds(authority, live.pty, {
|
||||
hostId: (tabId) => this.getWorkspaceSessionHostIdsForTab(authority.worktreeId, tabId)[0],
|
||||
session: (hostId) => this.store?.getWorkspaceSession?.(hostId),
|
||||
relist: (pty, pane) =>
|
||||
relist: (pty, pane, layout) => {
|
||||
this.publishPtyBackedMobileSessionTerminal(authority.worktreeId, pty, pane)
|
||||
const relisted = this.mobileSessionTabsByWorktree.get(authority.worktreeId)
|
||||
const withLayout =
|
||||
relisted && withMainTabLayout(relisted, { tabId: pane.tabId, ptyId: pty.ptyId }, layout)
|
||||
if (withLayout && withLayout !== relisted) {
|
||||
this.storeMobileSessionSnapshot(authority.worktreeId, withLayout)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
const surface = this.findMobileTerminalSurfaceForPty(authority.worktreeId, authority.ptyId)
|
||||
|
||||
Reference in New Issue
Block a user