diff --git a/src/main/runtime/session-tabs/mobile-session-hydration.ts b/src/main/runtime/session-tabs/mobile-session-hydration.ts index eb44f6df004..087f9e10d94 100644 --- a/src/main/runtime/session-tabs/mobile-session-hydration.ts +++ b/src/main/runtime/session-tabs/mobile-session-hydration.ts @@ -259,11 +259,20 @@ export abstract class OrcaRuntimeWithHydrateHeadlessMobileSessionTabsFromWorkspa ? mergedActiveTab.parentTabId : mergedActiveTab.id : null - const rebuiltTabGroups: RuntimeMobileSessionTabGroup[] = navigationGroups + // Saved groups without a saved active group record no selection; keep the restored tab selected. + const restoredGroups = + hasCurrentNavigation || navigationActiveGroupId || !activeTopLevelId + ? navigationGroups + : navigationGroups?.map((group) => + group.activeTabId === null && group.tabOrder.includes(activeTopLevelId) + ? { ...group, activeTabId: activeTopLevelId } + : group + ) + const rebuiltTabGroups: RuntimeMobileSessionTabGroup[] = restoredGroups ? appendBrowserTabOrder( mergeMobileSessionTabGroups( entryWorktreeId, - navigationGroups, + restoredGroups, mergedTabs, mergedActiveTab?.type === 'terminal' ? mergedActiveTab : null, session.tabGroups?.[entryWorktreeId] diff --git a/tests/e2e/layout-compat-baselines.json b/tests/e2e/layout-compat-baselines.json index c26924d3fd3..881a017c1bc 100644 --- a/tests/e2e/layout-compat-baselines.json +++ b/tests/e2e/layout-compat-baselines.json @@ -1332,6 +1332,7 @@ "activeTabType", "publicationEpoch", "snapshotVersion", + "tabGroupLayout", "tabGroups", "tabs", "type", diff --git a/tests/e2e/paired-client-project-removal-stops-host-terminals.spec.ts b/tests/e2e/paired-client-project-removal-stops-host-terminals.spec.ts index d89b937ad21..c0161a81b0c 100644 --- a/tests/e2e/paired-client-project-removal-stops-host-terminals.spec.ts +++ b/tests/e2e/paired-client-project-removal-stops-host-terminals.spec.ts @@ -11,6 +11,8 @@ import { type PairedElectronClient } from './helpers/paired-electron-client' import { createRestartSession } from './helpers/orca-restart' +import { readFreshTerminalInventory } from './helpers/terminal-inventory-observation' +import type { RuntimeTerminalListResult } from '../../src/shared/runtime-types' function seededRepoPathOrSkip(): string { const repoPath = existsSync(TEST_REPO_PATH_FILE) @@ -105,20 +107,27 @@ test('removing a project from a paired client stops its shells on the host', asy state?.setActiveView('terminal') state?.setActiveWorktree(id) }, worktreeId) - const listHostTerminals = (): Promise => - callRuntime<{ terminals: { handle: string }[] }>( - paired.page, - paired.environmentId, - 'terminal.list', - { worktree: `id:${worktreeId}` } - ).then((listed) => listed.terminals.map((terminal) => terminal.handle)) + let handle: string | undefined await expect - .poll(() => listHostTerminals().then((handles) => handles.length), { - timeout: 45_000, - message: 'Opening the worktree never created a host terminal' - }) - .toBeGreaterThan(0) - const [handle] = await listHostTerminals() + .poll( + async () => { + const listed = await readFreshTerminalInventory(() => + callRuntime( + paired.page, + paired.environmentId, + 'terminal.list', + { + worktree: `id:${worktreeId}` + } + ) + ) + // A row listed before its shell spawns has no PTY yet, and its handle is replaced once one does. + handle = listed?.terminals.find((terminal) => terminal.ptyId)?.handle + return handle + }, + { timeout: 45_000, message: 'Opening the worktree never created a host terminal' } + ) + .toBeDefined() // Retried because the first send can land before the shell is writable or has its prompt. await expect diff --git a/tests/e2e/paired-mobile-desktop-tab-composition.spec.ts b/tests/e2e/paired-mobile-desktop-tab-composition.spec.ts index b7735fe03f3..7388ff81ebe 100644 --- a/tests/e2e/paired-mobile-desktop-tab-composition.spec.ts +++ b/tests/e2e/paired-mobile-desktop-tab-composition.spec.ts @@ -75,11 +75,20 @@ function desktopTab(page: Page, hostTabId: string) { /** Drops one desktop tab onto the leading edge of another, as a user drags in the tab strip. */ async function dragTab(page: Page, hostTabId: string, ontoHostTabId: string): Promise { + // The active tab docks at a scrolled strip's edge until a drag starts, so drop where it really sits. + const onto = desktopTab(page, ontoHostTabId) + await onto.hover() + await page.mouse.wheel(0, -10_000) + await expect + .poll(() => + onto.evaluate((tab) => tab.closest('[data-tab-strip-slot]')?.parentElement?.scrollLeft) + ) + .toBe(0) const from = (await desktopTab(page, hostTabId).boundingBox())! - const onto = (await desktopTab(page, ontoHostTabId).boundingBox())! + const target = (await onto.boundingBox())! await page.mouse.move(from.x + from.width / 2, from.y + from.height / 2) await page.mouse.down() - await page.mouse.move(onto.x + onto.width * 0.25, onto.y + onto.height / 2, { steps: 8 }) + await page.mouse.move(target.x + target.width * 0.25, target.y + target.height / 2, { steps: 8 }) await page.mouse.up() } @@ -149,14 +158,15 @@ test('phone shows the desktop tab strip of a server workspace, also with the win ) await expect.poll(() => desktop.page.locator(SORTABLE_TAB).count(), { timeout: 30_000 }).toBe(3) - // A server-placed browser page sits in the same strip. - await call( - socket, - 'browser.tabCreate', - { worktree, url: 'about:blank', activate: false }, - z.looseObject({}), - serverHostId - ) + // A server-placed browser page sits in the same strip. Created by the server itself: one the phone + // creates through the desktop is hosted by the desktop (relayed-browser-tab-create.ts). + const { browserPageId } = ( + await host.client.call<{ browserPageId: string }>('browser.tabCreate', { + worktree, + url: 'about:blank', + activate: false + }) + ).result const phoneStrip = async (): Promise => ((await phoneTabs()).tabGroups ?? []).map((group) => group.tabOrder) await expect @@ -206,9 +216,9 @@ test('phone shows the desktop tab strip of a server workspace, also with the win (await phoneTabs()).tabs.filter((tab) => tab.type === 'browser').map((tab) => tab.id) ).toHaveLength(1) expect(await publishedStrip()).toEqual(arranged) - // Only the server holds this workspace's tabs; the desktop's own answer has none of them. + // Only the server holds this workspace's tabs; the desktop serves its phone just the server page it mirrors (#36447). const untargeted = await call(socket, 'session.tabs.list', { worktree }, SessionTabsSchema) - expect(untargeted.tabs).toEqual([]) + expect(untargeted.tabs.map((tab) => tab.id)).toEqual([browserPageId]) // Selection is each device's own, as for a local workspace. await call(