mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 16:02:24 +00:00
* fix(terminal): mount one surface per workspace id in the workbench (STA-4846) * test(terminal): pin the workbench projection against under-selecting Losing a surface unmounts live terminals, which is worse than the duplicate mount STA-4846 fixes, so cover every catalog shape that reaches the workbench: local-only rows that name no host, an unqualified row colliding with a host-qualified one, two SSH hosts on one id, folder rows across three hosts, folder ids alongside git worktree ids, and a whole-catalog assertion that the emitted id set equals the distinct input id set. Also pin the `useAllWorktrees` -> `useWorktreeMap` swap: both read the same WeakMap-cached snapshot, so the zustand compare is unchanged. Harden the folder tie-break to require the row to name its own host. `getCatalogOwnerHostId` defaults an unstamped row to `local`, which would let a row that never named a host win the `local` tie and mount another host's path; it now keeps first-wins instead of guessing. * fix(terminal): surface the unresolvable folder-surface collision When two hosts publish the same folder-workspace id and the active workspace's host cannot be resolved, the projection drops one row's folderPath first-wins. That path is the PTY cwd for any tab without a startupCwd, so the drop was silent. Warn on it, and pin the two tie-break branches the unit tests missed: a colliding row that is not the active workspace, and the same collision with the rows in swapped order (a host reconnect re-appends its rows, flipping which row is first mid-session). * test(e2e): ride out Playwright's spurious main-process evaluate rejection `e2e / changed e2e specs` failed on `pr11346-selected-runtime-add.spec.ts` with "Execution context was destroyed, most likely because of a navigation" from the paired client's first `app.evaluate` — the isolated-HOME assert that runs one millisecond after `electron.launch()` resolves, which is before the app is `ready`. Nothing navigates there: Playwright raises that message for any main-process CDP failure that is neither a JS error nor a closed session, and `ElectronApplication.evaluate` is unreliable on Electron 27+ (microsoft/playwright#33737). Reproduced locally, and a plain re-run of the same commit went green. Extract the retry `installTerminalPtyWriteSpy` already carried for this exact message into `retryTransientMainEvaluate`, and use it for the launch-time home read in all three launchers. The read is idempotent and a real boundary escape still throws on the first successful read. Also forward the paired client's process logs before the assert instead of after: this failure reached CI with none of the client's own output, because forwarding had not started yet. * test(e2e): wait on the owning group before asserting a Cmd-J browser tab is active `changed e2e specs` then failed at the remote browser-page step: the store poll had already seen `activeBrowserTabId` land on the mirrored workspace, but `[data-tab-id=...][data-active="true"]` never appeared. `data-active` on a `BrowserTab` is the strip's active tab, which comes from the owning group's `activeTabId` — not from `activeBrowserTabId` — so the DOM assert was racing an activation the poll never waited for. The simulator rows in the same spec already poll the group; the two browser-page rows did not. Poll the same triple for them, so a genuinely stuck group fails with the ids it ended on instead of a bare "element(s) not found".
82 lines
3.1 KiB
TypeScript
82 lines
3.1 KiB
TypeScript
import type { RuntimeClient } from '../../../src/cli/runtime/client'
|
|
import type { RuntimeMobileSessionTabsResult } from '../../../src/shared/runtime-types'
|
|
|
|
/**
|
|
* The host's own view of a worktree's session tabs. Asking the host directly is what makes a close
|
|
* test a real oracle: a client-side re-derivation of who owns the tab stops guarding the moment the
|
|
* ownership policy it copied changes.
|
|
*/
|
|
export async function readHostTabs(
|
|
hostClient: RuntimeClient,
|
|
repoPath: string
|
|
): Promise<RuntimeMobileSessionTabsResult> {
|
|
const response = await hostClient.call<RuntimeMobileSessionTabsResult>('session.tabs.list', {
|
|
worktree: `path:${repoPath}`
|
|
})
|
|
return response.result
|
|
}
|
|
|
|
type HostBrowserPageRow = { browserPageId: string; url: string }
|
|
|
|
/**
|
|
* The host's own browser page registry for a workspace, addressed by worktree selector.
|
|
*
|
|
* Why not readHostTabs: a headless paired host does not project browser pages into
|
|
* session.tabs.list — that snapshot carries only terminals, and it additionally hides client-placed
|
|
* pages from any peer that does not advertise `BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY`, which the
|
|
* CLI socket deliberately does not. `browser.tabList` has neither limitation, so it is the only
|
|
* oracle that answers "does the host still hold this page" for both placements.
|
|
*/
|
|
async function readHostBrowserPages(
|
|
hostClient: RuntimeClient,
|
|
worktreeSelector: string,
|
|
timeoutMs?: number
|
|
): Promise<HostBrowserPageRow[]> {
|
|
const response = await hostClient.call<{ tabs: HostBrowserPageRow[] }>(
|
|
'browser.tabList',
|
|
{ worktree: worktreeSelector },
|
|
{ timeoutMs }
|
|
)
|
|
return response.result.tabs
|
|
}
|
|
|
|
/** The page ids the host still holds for a repo-backed worktree. */
|
|
export async function readHostBrowserPageIds(
|
|
hostClient: RuntimeClient,
|
|
repoPath: string
|
|
): Promise<string[]> {
|
|
const tabs = await readHostBrowserPages(hostClient, `path:${repoPath}`)
|
|
return tabs.map((tab) => tab.browserPageId).sort()
|
|
}
|
|
|
|
/**
|
|
* Where the host believes one of its browser pages is.
|
|
*
|
|
* For a client-hosted page this is the record the client keeps current by publishing metadata, and
|
|
* it is the URL page recovery navigates a restored page back to — so it is the difference between
|
|
* restoring a tab and restoring it where the user actually was.
|
|
*/
|
|
export async function readHostBrowserPageUrl(
|
|
hostClient: RuntimeClient,
|
|
repoPath: string,
|
|
browserPageId: string
|
|
): Promise<string | null> {
|
|
const tabs = await readHostBrowserPages(hostClient, `path:${repoPath}`)
|
|
return tabs.find((tab) => tab.browserPageId === browserPageId)?.url ?? null
|
|
}
|
|
|
|
/**
|
|
* Every browser page URL the host holds, for callers that address the workspace by selector
|
|
* (`id:`/`path:`) rather than repo path — a folder workspace has no repo path.
|
|
*
|
|
* Why the explicit ceiling: a paired host's client is constructed with a 5s default, which the
|
|
* first tabList can outrun while the host brings its browser session up.
|
|
*/
|
|
export async function readHostBrowserPageUrls(
|
|
hostClient: RuntimeClient,
|
|
worktreeSelector: string
|
|
): Promise<string[]> {
|
|
const tabs = await readHostBrowserPages(hostClient, worktreeSelector, 15_000)
|
|
return tabs.map((tab) => tab.url)
|
|
}
|