mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(mobile): phones on a host with no window list each terminal once (#29085)
* fix(mobile): phones on a host with no window list each terminal once With no window attached, the phone tab list was built from the stored terminal rows as they are, so a leftover row from an older build that repeats another tab's terminal showed as a second tab on the same terminal. Build it from the shared Loader's result instead, as the window and main's other readers do. * perf(mobile): load each stored partition once per loop over worktrees Review: list-all and graph sync hydrate once per worktree, and the Loader reads the whole partition, so each call repeated it. The loops now share one cache for their duration. * docs(mobile): say why the per-loop Loader cache ignores the host id * perf(mobile): run the Loader only for a partition that repeats a row or terminal A partition with no repeated row id and no terminal bound in two mounted panes is built from its raw rows exactly as before, so titles, pane ids and cost there are unchanged. * fix(mobile): compare terminals as the Loader does before deciding to load Review: a relay terminal id reused by a later incarnation is a different terminal to the Loader, so it must not trigger it either. * fix(mobile): a legacy row's terminals repeat by id alone, as the Loader matches them Review: comparing every binding by incarnation missed a legacy row repeating an SSH relay terminal with no incarnation recorded; the Loader's legacy-row check matches raw ids. * fix(mobile): drop only the legacy rows the Loader absorbs, per worktree Replaces the partition-wide Loader swap and its per-loop cache: a phone on a host with no window now differs from main only where it then matches the desktop window, which tests compare across main, the branch and the window. * test(mobile): compare tab groups too in the phone, main and window views
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import type { RuntimeMobileSessionTerminalTab } from '../../shared/runtime-types'
|
||||
import type { TerminalTab } from '../../shared/terminal-tab-types'
|
||||
import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types'
|
||||
import {
|
||||
readCanonicalPtyOwners,
|
||||
resolveCanonicalPtyClaim
|
||||
} from '../../shared/workspace-layout/legacy-terminal-row-claims'
|
||||
import {
|
||||
cloneTerminalLayoutSnapshot,
|
||||
collectPersistedTerminalLeafIds,
|
||||
@@ -8,12 +12,42 @@ import {
|
||||
isPersistedTerminalLeafActive
|
||||
} from './mobile-session-layout-projection'
|
||||
|
||||
/**
|
||||
* Drops a legacy row with no tab-bar entry whose every terminal a tab-bar tab of this workspace
|
||||
* holds (the shared Loader's rule; the window hides it too), and moves a selection on it to that tab.
|
||||
*/
|
||||
function withoutAbsorbedLegacyRows(
|
||||
worktreeId: string,
|
||||
persistedTabs: readonly TerminalTab[],
|
||||
session: WorkspaceSessionState
|
||||
): { rows: TerminalTab[]; selection: WorkspaceSessionState } {
|
||||
const canonical = readCanonicalPtyOwners(session, worktreeId, persistedTabs)
|
||||
const canonicalTabIdByAbsorbedTabId = new Map<string, string>()
|
||||
const rows = persistedTabs.filter((row) => {
|
||||
const claim = resolveCanonicalPtyClaim(session, row, canonical)
|
||||
if (claim.kind === 'subsumed') {
|
||||
canonicalTabIdByAbsorbedTabId.set(row.id, claim.canonicalTabId)
|
||||
}
|
||||
return claim.kind !== 'subsumed'
|
||||
})
|
||||
const activeTabId = session.activeTabIdByWorktree?.[worktreeId] ?? session.activeTabId
|
||||
const followed = activeTabId ? canonicalTabIdByAbsorbedTabId.get(activeTabId) : undefined
|
||||
const selection = followed
|
||||
? {
|
||||
...session,
|
||||
activeTabIdByWorktree: { ...session.activeTabIdByWorktree, [worktreeId]: followed }
|
||||
}
|
||||
: session
|
||||
return { rows, selection }
|
||||
}
|
||||
|
||||
export function buildHeadlessMobileSessionTerminalTabs(
|
||||
worktreeId: string,
|
||||
persistedTabs: readonly TerminalTab[],
|
||||
session: WorkspaceSessionState
|
||||
): RuntimeMobileSessionTerminalTab[] {
|
||||
return [...persistedTabs]
|
||||
const { rows, selection } = withoutAbsorbedLegacyRows(worktreeId, persistedTabs, session)
|
||||
return rows
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder || a.createdAt - b.createdAt)
|
||||
.flatMap((tab, index) => {
|
||||
const layout = session.terminalLayoutsByTabId?.[tab.id]
|
||||
@@ -43,7 +77,7 @@ export function buildHeadlessMobileSessionTerminalTabs(
|
||||
...(tab.color != null ? { color: tab.color } : {}),
|
||||
...(tab.isPinned ? { isPinned: true } : {}),
|
||||
...(tab.viewMode ? { viewMode: tab.viewMode } : {}),
|
||||
isActive: isPersistedTerminalLeafActive(session, worktreeId, tab.id, leafId, layout)
|
||||
isActive: isPersistedTerminalLeafActive(selection, worktreeId, tab.id, leafId, layout)
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// A phone on a host with no window may show something other than main only where it then shows
|
||||
// what the desktop window does; the window's side is pinned in headless-phone-window-parity.test.ts.
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getDefaultSettings } from '../../../shared/constants'
|
||||
import {
|
||||
HEADLESS_PHONE_LAYOUT_CASES,
|
||||
HEADLESS_PHONE_WORKTREES,
|
||||
type PhoneLayoutView
|
||||
} from '../../../shared/workspace-layout/headless-phone-layout.test-fixture'
|
||||
import { leaf } from '../../../shared/workspace-layout/workspace-layout-session.test-fixture'
|
||||
import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types'
|
||||
import { deriveHeadlessLegacyTerminalLeafId } from '../mobile-session-layout-projection'
|
||||
import { OrcaRuntimeService } from '../orca-runtime'
|
||||
|
||||
const repos = [
|
||||
{ id: 'repo-1', path: '/repos', displayName: 'repo-1', badgeColor: 'blue', addedAt: 1 }
|
||||
]
|
||||
|
||||
function hydratedRuntime(session: WorkspaceSessionState) {
|
||||
const runtime = new OrcaRuntimeService({
|
||||
getRepo: (id: string) => repos.find((repo) => repo.id === id),
|
||||
getRepos: () => repos,
|
||||
getAllWorktreeMeta: () => ({}),
|
||||
getWorktreeMeta: () => undefined,
|
||||
addRepo: () => {},
|
||||
updateRepo: () => {
|
||||
throw new Error('unused')
|
||||
},
|
||||
setWorktreeMeta: () => {
|
||||
throw new Error('unused')
|
||||
},
|
||||
removeWorktreeMeta: () => {},
|
||||
getGitHubCache: () => ({ pr: {}, issue: {} }),
|
||||
getRetiredWorktreeNameRegistry: () => ({ exhaustedTiers: 0, names: [] }),
|
||||
addRetiredWorktreeName: () => {},
|
||||
mergeRetiredWorktreeNames: () => false,
|
||||
getSettings: () => getDefaultSettings('/tmp/home'),
|
||||
getWorkspaceSession: () => session,
|
||||
setWorkspaceSession: () => {}
|
||||
})
|
||||
runtime.hydrateHeadlessMobileSessionTabsFromWorkspaceSession(undefined, { force: true })
|
||||
return runtime
|
||||
}
|
||||
|
||||
function phoneView(session: WorkspaceSessionState): PhoneLayoutView {
|
||||
const view: PhoneLayoutView = {}
|
||||
for (const [worktreeId, snapshot] of hydratedRuntime(session).mobileSessionTabsByWorktree) {
|
||||
view[worktreeId] = {
|
||||
panes: snapshot.tabs.flatMap((tab) =>
|
||||
tab.type === 'terminal' ? [`${tab.parentTabId} ${tab.ptyId ?? '-'} ${tab.title}`] : []
|
||||
),
|
||||
active: snapshot.activeTabId?.split('::')[0] ?? null,
|
||||
groups: (snapshot.tabGroups ?? []).map((group) => ({
|
||||
tabs: group.tabOrder,
|
||||
active: group.activeTabId
|
||||
}))
|
||||
}
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
describe('headless phone terminal tabs', () => {
|
||||
it.each(HEADLESS_PHONE_LAYOUT_CASES)('$name', (scenario) => {
|
||||
for (const worktreeId of scenario.phoneFollowsDesktop) {
|
||||
expect(scenario.desktop[worktreeId]).not.toEqual(scenario.main[worktreeId])
|
||||
}
|
||||
const expected = Object.fromEntries(
|
||||
Object.entries(scenario.main).map(([worktreeId, view]) => [
|
||||
worktreeId,
|
||||
scenario.phoneFollowsDesktop.includes(worktreeId) ? scenario.desktop[worktreeId] : view
|
||||
])
|
||||
)
|
||||
expect(phoneView(scenario.build())).toEqual(expected)
|
||||
})
|
||||
|
||||
it('keeps pane ids in worktrees with no absorbed row', () => {
|
||||
const scenario = HEADLESS_PHONE_LAYOUT_CASES.find((entry) =>
|
||||
entry.name.startsWith('duplicate in one worktree')
|
||||
)!
|
||||
const { B, C, D } = HEADLESS_PHONE_WORKTREES
|
||||
const leafIds = (worktreeId: string) =>
|
||||
hydratedRuntime(scenario.build())
|
||||
.mobileSessionTabsByWorktree.get(worktreeId)
|
||||
?.tabs.flatMap((tab) => (tab.type === 'terminal' ? [tab.leafId] : []))
|
||||
expect(leafIds(B)).toEqual([leaf(4), leaf(5)])
|
||||
expect(leafIds(C)).toEqual([leaf(6)])
|
||||
expect(leafIds(D)).toEqual([deriveHeadlessLegacyTerminalLeafId('tab-old')])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
// What the desktop window shows at startup for the partitions a phone on a host with no window is
|
||||
// compared against in headless-mobile-session-absorbed-rows.test.ts.
|
||||
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import type * as AgentStatusModule from '@/lib/agent-status'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../../shared/execution-host'
|
||||
import {
|
||||
HEADLESS_PHONE_LAYOUT_CASES,
|
||||
type PhoneLayoutView
|
||||
} from '../../../shared/workspace-layout/headless-phone-layout.test-fixture'
|
||||
import { createStoreSessionMockApi } from '../store/slices/store-session-test-harness'
|
||||
import { createTestStore } from '../store/slices/store-test-helpers'
|
||||
import {
|
||||
bootWindowFromPartitions,
|
||||
projectStartupWindowView
|
||||
} from './startup-window-view.test-fixture'
|
||||
|
||||
vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } }))
|
||||
vi.mock('@/lib/agent-status', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof AgentStatusModule>()
|
||||
return { ...actual, detectAgentStatusFromTitle: vi.fn().mockReturnValue(null) }
|
||||
})
|
||||
|
||||
createStoreSessionMockApi()
|
||||
|
||||
/** Terminals of a projected pane tree, in order; see `projectStartupWindowView`. */
|
||||
function paneTerminals(node: unknown): (string | null)[] {
|
||||
if (typeof node !== 'object' || node === null) {
|
||||
return []
|
||||
}
|
||||
if ('pane' in node) {
|
||||
return [typeof node.pane === 'string' ? node.pane : null]
|
||||
}
|
||||
return [
|
||||
...('first' in node ? paneTerminals(node.first) : []),
|
||||
...('second' in node ? paneTerminals(node.second) : [])
|
||||
]
|
||||
}
|
||||
|
||||
function windowView(view: Record<string, unknown>, worktreeIds: string[]): PhoneLayoutView {
|
||||
const result: PhoneLayoutView = {}
|
||||
for (const worktreeId of worktreeIds) {
|
||||
const tabIds = Object.keys(view)
|
||||
.filter((key) => key.startsWith(`${worktreeId} group `) && key.endsWith(' tabs'))
|
||||
.flatMap((key) => (Array.isArray(view[key]) ? view[key].map(String) : []))
|
||||
const panes = tabIds.flatMap((tabId) => {
|
||||
const terminal = `${worktreeId} terminal ${tabId}`
|
||||
if (!(`${terminal} panes` in view)) {
|
||||
return []
|
||||
}
|
||||
const title = String(view[`${terminal} phone and CLI title`])
|
||||
const terminals = paneTerminals(view[`${terminal} panes`])
|
||||
return terminals.length === 0
|
||||
? [`${tabId} (no layout yet) ${title}`]
|
||||
: terminals.map((ptyId) => `${tabId} ${ptyId ?? '-'} ${title}`)
|
||||
})
|
||||
const active = view[`${worktreeId} active terminal`]
|
||||
const groups = Object.keys(view)
|
||||
.filter((key) => key.startsWith(`${worktreeId} group `) && key.endsWith(' tabs'))
|
||||
.map((key) => {
|
||||
const groupActive = view[key.replace(/ tabs$/, ' active tab')]
|
||||
return {
|
||||
tabs: Array.isArray(view[key]) ? view[key].map(String) : [],
|
||||
active: typeof groupActive === 'string' ? groupActive : null
|
||||
}
|
||||
})
|
||||
result[worktreeId] = { panes, active: typeof active === 'string' ? active : null, groups }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
it.each(HEADLESS_PHONE_LAYOUT_CASES)('window at startup: $name', async (scenario) => {
|
||||
const state = await bootWindowFromPartitions(createTestStore(), [
|
||||
{ hostId: LOCAL_EXECUTION_HOST_ID, session: scenario.build() }
|
||||
])
|
||||
expect(windowView(projectStartupWindowView(state), Object.keys(scenario.desktop))).toEqual(
|
||||
scenario.desktop
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,281 @@
|
||||
// Stored partitions for comparing what a phone is sent from a host with no window against what
|
||||
// the desktop window shows, one worktree per case so a fix in one cannot leak into another.
|
||||
|
||||
import type { WorkspaceSessionState } from '../workspace-session-state-types'
|
||||
import { LEGACY_WORKSPACE_LAYOUT_PROFILES } from './workspace-layout-legacy-profile.test-fixture'
|
||||
import { addWorkspace, emptySession, GIT_KEY, leaf } from './workspace-layout-session.test-fixture'
|
||||
|
||||
const A = 'repo-1::/repos/a'
|
||||
const B = 'repo-1::/repos/b'
|
||||
const C = 'repo-1::/repos/c'
|
||||
const D = 'repo-1::/repos/d'
|
||||
|
||||
/** Per worktree: each terminal pane in order as `tab terminal title`, the selected tab, and each
|
||||
* tab group's tabs and selected tab. */
|
||||
export type PhoneLayoutView = Record<
|
||||
string,
|
||||
{ panes: string[]; active: string | null; groups: { tabs: string[]; active: string | null }[] }
|
||||
>
|
||||
|
||||
export type HeadlessPhoneLayoutCase = {
|
||||
name: string
|
||||
build: () => WorkspaceSessionState
|
||||
/** What main sends a phone from a host with no window. */
|
||||
main: PhoneLayoutView
|
||||
/** What the desktop window shows at startup. */
|
||||
desktop: PhoneLayoutView
|
||||
/** Worktrees where the phone now shows the desktop's view; it shows main's everywhere else. */
|
||||
phoneFollowsDesktop: string[]
|
||||
}
|
||||
|
||||
function legacyFixture(name: string): WorkspaceSessionState {
|
||||
return LEGACY_WORKSPACE_LAYOUT_PROFILES.find((profile) => profile.name === name)!.build()
|
||||
}
|
||||
|
||||
/** A row saved before pane layouts existed, in its own worktree. */
|
||||
function withLayoutlessRow(session: WorkspaceSessionState): WorkspaceSessionState {
|
||||
session.tabsByWorktree[D] = [
|
||||
{
|
||||
id: 'tab-old',
|
||||
ptyId: 'pty-old',
|
||||
worktreeId: D,
|
||||
title: 'Terminal 1',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1
|
||||
}
|
||||
]
|
||||
return session
|
||||
}
|
||||
|
||||
function contaminationSession(): WorkspaceSessionState {
|
||||
let session = emptySession()
|
||||
session = addWorkspace(session, A, [
|
||||
{
|
||||
id: 'ga',
|
||||
tabs: [
|
||||
{ id: 'tab-a', leaves: [[leaf(1), 'pty-a']] },
|
||||
{ id: 'tab-b', leaves: [[leaf(2), 'pty-b']] }
|
||||
]
|
||||
}
|
||||
])
|
||||
session.tabsByWorktree[A]!.push({
|
||||
...session.tabsByWorktree[A]![0]!,
|
||||
id: 'tab-legacy',
|
||||
title: 'Terminal 3',
|
||||
sortOrder: 2,
|
||||
createdAt: 1_700_000_009_000
|
||||
})
|
||||
session.terminalLayoutsByTabId['tab-legacy'] = {
|
||||
root: { type: 'leaf', leafId: leaf(3) },
|
||||
activeLeafId: leaf(3),
|
||||
expandedLeafId: null,
|
||||
ptyIdsByLeafId: { [leaf(3)]: 'pty-a' }
|
||||
}
|
||||
session = addWorkspace(session, B, [
|
||||
{
|
||||
id: 'gb',
|
||||
tabs: [
|
||||
{ id: 'tab-c', leaves: [[leaf(4), 'pty-c']] },
|
||||
{ id: 'tab-d', leaves: [[leaf(5), 'pty-d']] }
|
||||
]
|
||||
}
|
||||
])
|
||||
session.terminalSurfaceTombstonesByPaneKey = {
|
||||
[`tab-d:${leaf(5)}`]: {
|
||||
worktreeId: B,
|
||||
parentTabId: 'tab-d',
|
||||
leafId: leaf(5),
|
||||
ptyId: 'pty-d',
|
||||
incarnationId: 'incarnation-d',
|
||||
retiredAt: 1
|
||||
}
|
||||
}
|
||||
session = addWorkspace(session, C, [
|
||||
{ id: 'gc', tabs: [{ id: 'tab-e', leaves: [[leaf(6), 'pty-e']] }] }
|
||||
])
|
||||
session.unifiedTabs![C]![0]!.label = 'vim'
|
||||
return withLayoutlessRow(session)
|
||||
}
|
||||
|
||||
function staleRowTerminalSession(): WorkspaceSessionState {
|
||||
let session = addWorkspace(emptySession(), A, [
|
||||
{
|
||||
id: 'ga',
|
||||
tabs: [
|
||||
{ id: 'tab-a', leaves: [[leaf(1), 'pty-a']] },
|
||||
{ id: 'tab-b', leaves: [[leaf(2), 'pty-b']] }
|
||||
]
|
||||
}
|
||||
])
|
||||
session.tabsByWorktree[A]![0]!.ptyId = 'pty-b'
|
||||
session = addWorkspace(session, C, [
|
||||
{ id: 'gc', tabs: [{ id: 'tab-e', leaves: [[leaf(6), 'pty-e']] }] }
|
||||
])
|
||||
session.unifiedTabs![C]![0]!.label = 'vim'
|
||||
return withLayoutlessRow(session)
|
||||
}
|
||||
|
||||
function terminalInTwoWorktreesSession(): WorkspaceSessionState {
|
||||
let session = addWorkspace(emptySession(), A, [
|
||||
{ id: 'ga', tabs: [{ id: 'tab-a', leaves: [[leaf(1), 'pty-shared']] }] }
|
||||
])
|
||||
session = addWorkspace(session, B, [
|
||||
{ id: 'gb', tabs: [{ id: 'tab-b', leaves: [[leaf(2), 'pty-shared']] }] }
|
||||
])
|
||||
return session
|
||||
}
|
||||
|
||||
function sshRelayDuplicateSession(): WorkspaceSessionState {
|
||||
const session = legacyFixture('legacy row repeating a tab-bar tab’s terminal')
|
||||
return JSON.parse(JSON.stringify(session).replaceAll('"pty-a"', '"ssh:target-1@@pty-1"'))
|
||||
}
|
||||
|
||||
const oneGroup = (tabs: string[], active: string) => [{ tabs, active }]
|
||||
const duplicate = {
|
||||
main: {
|
||||
panes: ['tab-a pty-a Terminal 1', 'tab-b pty-b Terminal 2', 'tab-legacy pty-a Terminal 3'],
|
||||
active: 'tab-a',
|
||||
groups: oneGroup(['tab-a', 'tab-b', 'tab-legacy'], 'tab-a')
|
||||
},
|
||||
desktop: {
|
||||
panes: ['tab-a pty-a Terminal 1', 'tab-b pty-b Terminal 2'],
|
||||
active: 'tab-a',
|
||||
groups: oneGroup(['tab-a', 'tab-b'], 'tab-a')
|
||||
}
|
||||
}
|
||||
const tabBarPair = (first: string, second: string) => {
|
||||
const [firstTab, secondTab] = [first.split(' ')[0]!, second.split(' ')[0]!]
|
||||
return {
|
||||
panes: [`${first} Terminal 1`, `${second} Terminal 2`],
|
||||
active: firstTab,
|
||||
groups: oneGroup([firstTab, secondTab], firstTab)
|
||||
}
|
||||
}
|
||||
const single = (pane: string) => {
|
||||
const tab = pane.split(' ')[0]!
|
||||
return { panes: [`${pane} Terminal 1`], active: tab, groups: oneGroup([tab], tab) }
|
||||
}
|
||||
const vimTab = single('tab-e pty-e')
|
||||
const layoutless = {
|
||||
main: single('tab-old pty-old'),
|
||||
// The window binds a layoutless row's terminal when its pane mounts, not at startup.
|
||||
desktop: {
|
||||
panes: ['tab-old (no layout yet) Terminal 1'],
|
||||
active: null,
|
||||
groups: oneGroup(['tab-old'], 'tab-old')
|
||||
}
|
||||
}
|
||||
const splitRowGroups = oneGroup(['tab-a', 'tab-b', 'tab-legacy'], 'tab-a')
|
||||
|
||||
export const HEADLESS_PHONE_LAYOUT_CASES: readonly HeadlessPhoneLayoutCase[] = [
|
||||
{
|
||||
name: 'legacy row repeating a tab-bar tab’s terminal',
|
||||
build: () => legacyFixture('legacy row repeating a tab-bar tab’s terminal'),
|
||||
main: { [GIT_KEY]: duplicate.main },
|
||||
desktop: { [GIT_KEY]: duplicate.desktop },
|
||||
phoneFollowsDesktop: [GIT_KEY]
|
||||
},
|
||||
{
|
||||
name: 'legacy row repeating a tab-bar tab’s terminal, selected',
|
||||
build: () => legacyFixture('legacy row repeating a tab-bar tab’s terminal, selected'),
|
||||
main: {
|
||||
[GIT_KEY]: {
|
||||
...duplicate.main,
|
||||
active: 'tab-legacy',
|
||||
groups: oneGroup(['tab-a', 'tab-b', 'tab-legacy'], 'tab-legacy')
|
||||
}
|
||||
},
|
||||
desktop: { [GIT_KEY]: duplicate.desktop },
|
||||
phoneFollowsDesktop: [GIT_KEY]
|
||||
},
|
||||
{
|
||||
name: 'legacy row repeating an SSH relay terminal with no incarnation recorded',
|
||||
build: sshRelayDuplicateSession,
|
||||
main: {
|
||||
[GIT_KEY]: {
|
||||
...duplicate.main,
|
||||
panes: duplicate.main.panes.map((pane) => pane.replace('pty-a', 'ssh:target-1@@pty-1'))
|
||||
}
|
||||
},
|
||||
desktop: {
|
||||
[GIT_KEY]: {
|
||||
...duplicate.desktop,
|
||||
panes: duplicate.desktop.panes.map((pane) => pane.replace('pty-a', 'ssh:target-1@@pty-1'))
|
||||
}
|
||||
},
|
||||
phoneFollowsDesktop: [GIT_KEY]
|
||||
},
|
||||
{
|
||||
// The window unbinds the repeated pane but keeps the row; the phone keeps main's view.
|
||||
name: 'legacy split row sharing one pane’s terminal with a tab-bar tab',
|
||||
build: () => legacyFixture('legacy split row sharing one pane’s terminal with a tab-bar tab'),
|
||||
main: {
|
||||
[GIT_KEY]: {
|
||||
panes: [
|
||||
'tab-a pty-a Terminal 1',
|
||||
'tab-b pty-b Terminal 2',
|
||||
'tab-legacy pty-a Terminal 3',
|
||||
'tab-legacy pty-c Terminal 3'
|
||||
],
|
||||
active: 'tab-a',
|
||||
groups: splitRowGroups
|
||||
}
|
||||
},
|
||||
desktop: {
|
||||
[GIT_KEY]: {
|
||||
panes: [
|
||||
'tab-a pty-a Terminal 1',
|
||||
'tab-b pty-b Terminal 2',
|
||||
'tab-legacy - Terminal 3',
|
||||
'tab-legacy pty-c Terminal 3'
|
||||
],
|
||||
active: 'tab-a',
|
||||
groups: splitRowGroups
|
||||
}
|
||||
},
|
||||
phoneFollowsDesktop: []
|
||||
},
|
||||
{
|
||||
name: 'duplicate in one worktree, with a tombstone, a label and a layoutless row in others',
|
||||
build: contaminationSession,
|
||||
main: {
|
||||
[A]: duplicate.main,
|
||||
[B]: tabBarPair('tab-c pty-c', 'tab-d pty-d'),
|
||||
[C]: vimTab,
|
||||
[D]: layoutless.main
|
||||
},
|
||||
desktop: {
|
||||
[A]: duplicate.desktop,
|
||||
[B]: tabBarPair('tab-c pty-c', 'tab-d pty-d'),
|
||||
[C]: vimTab,
|
||||
[D]: layoutless.desktop
|
||||
},
|
||||
phoneFollowsDesktop: [A]
|
||||
},
|
||||
{
|
||||
name: 'tab-bar row whose stale row terminal is another tab’s pane',
|
||||
build: staleRowTerminalSession,
|
||||
main: {
|
||||
[A]: tabBarPair('tab-a pty-a', 'tab-b pty-b'),
|
||||
[C]: vimTab,
|
||||
[D]: layoutless.main
|
||||
},
|
||||
desktop: {
|
||||
[A]: tabBarPair('tab-a pty-a', 'tab-b pty-b'),
|
||||
[C]: vimTab,
|
||||
[D]: layoutless.desktop
|
||||
},
|
||||
phoneFollowsDesktop: []
|
||||
},
|
||||
{
|
||||
name: 'one terminal in tab-bar tabs of two worktrees',
|
||||
build: terminalInTwoWorktreesSession,
|
||||
main: { [A]: single('tab-a pty-shared'), [B]: single('tab-b pty-shared') },
|
||||
desktop: { [A]: single('tab-a pty-shared'), [B]: single('tab-b pty-shared') },
|
||||
phoneFollowsDesktop: []
|
||||
}
|
||||
]
|
||||
|
||||
export const HEADLESS_PHONE_WORKTREES = { A, B, C, D }
|
||||
Reference in New Issue
Block a user