mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 09:27:29 +00:00
fix(runtime): keep the restored tab selected after a serve restart; update stale paired e2e specs (#39980)
* fix(runtime): keep a restored tab selected after orca serve restarts Since #38844 a cold rebuild restores the saved tab groups, and a saved group's null active tab was kept as an explicit "nothing selected". A headless server never saves its own selection, so after a restart its workspaces listed every terminal with none selected. Saved groups now only declare a null selection when the session also saved an active group; otherwise the restored tab stays selected, as before. * test(e2e): bring layout-compat and paired-client specs up to current behavior - old-phone baseline: phone snapshots carry tabGroupLayout since #38844. - tab composition: drop onto where the active tab sits once a drag undocks it; create the server-placed browser page on the server, since a phone's create through the desktop is desktop-hosted (#37705); the desktop serves its phone the server page it mirrors (#36447). - project removal: retry the host's "can't answer yet" terminal list and wait for a terminal whose shell has spawned. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -1332,6 +1332,7 @@
|
||||
"activeTabType",
|
||||
"publicationEpoch",
|
||||
"snapshotVersion",
|
||||
"tabGroupLayout",
|
||||
"tabGroups",
|
||||
"tabs",
|
||||
"type",
|
||||
|
||||
@@ -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<string[]> =>
|
||||
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<RuntimeTerminalListResult>(
|
||||
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
|
||||
|
||||
@@ -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<void> {
|
||||
// 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<Strip> =>
|
||||
((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(
|
||||
|
||||
Reference in New Issue
Block a user