mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 02:17:31 +00:00
* feat(browser): list restored tabs in tab list and load them on demand After a restart Orca restores browser tabs lazily: a tab gets a page only once the user opens it. tab list only reported tabs with a live page, so an agent in a worktree with 26 restored tabs saw one and concluded there were none. tab list now reads the tab bar from the session snapshot the renderer already publishes on every tab change, and reports every tab in tab-bar order. Tabs without a page carry loaded: false. tab switch and tab close --index resolve indexes against the same list, and switching to an unloaded tab loads it in the background first. Waking a page now also waits for its first load to finish, so the next command does not read a blank page. Headless serve keeps every page live, so it lists as before. * fix(browser): list the tab bar even when Orca has no window open With the window closed the app keeps running and its restored tabs still exist, so tab list should report them. Headless snapshots are built from live pages only, and the merge dedupes those, so dropping the window check lists nothing extra there. * fix(browser): list an id held by a live page and a client page once * fix(browser): wait for the first load of a tab headless serve wakes Headless serve starts a restored page and returned at once, so the next command could read a blank page mid-load. It now waits for the first load, as the window path already did. * refactor(browser): keep the tab listing beside the browser tab catalog Main moved the runtime browser commands into runtime/browser/ since this was written. Also drops a window lookup the catalog no longer uses, and corrects the headless note: those snapshots list saved pages that have not started yet. * fix(browser): keep paired-server mirrors out of tab list and close indexed rows like --page A window also publishes the browser tabs it mirrors from paired runtimes, which this host can never load, so the tab bar is read only for workspaces this host owns. tab close --index now closes the listed row exactly as --page does, keeping its worktree, so an unstarted headless tab is retired instead of reported closed and left saved. With nothing active, the first live page is marked current again, matching where a pageless command goes. * fix(browser): wait for first load only on tab switch, and show tab indexes from the listing Every --page command wakes an unloaded page, including the tab show a remote browser pane awaits before streaming, so waiting up to 10 s for the first load there held the stream back. Only tab switch waits now. tab show reports a tab's index from the same listing tab list and close use. * fix(browser): list restored tabs in an SSH host's bridged workspace, and reject fractional close indexes An SSH host's workspace can have no repo row on the desktop, yet its pages render in the desktop window, so the bridge's scoped workspace counts as this host's own. A non-integer --index no longer falls through to closing the active tab. * test(browser): give the restored-tabs e2e's timeout and lease wait their reasons * test(browser): read restored-tabs RPC results through the shared e2e reader Reuses the browser e2e rpc helper instead of a local Reflect.get copy, and types the listing test's snapshot rows. * fix(browser): index tab show in the caller's scope, as tab list does * fix(browser): report an out-of-range close index as browser_tab_not_found Matches tab switch, so agents can recover by re-listing. The restored-tabs e2e now asserts the raw loaded value instead of coercing a missing one. --------- Co-authored-by: fanzzzd <fanzzzd@outlook.com>
255 lines
8.2 KiB
TypeScript
255 lines
8.2 KiB
TypeScript
import type {
|
|
BrowserCertificateFailure,
|
|
BrowserCookieImportResult,
|
|
BrowserLoadError,
|
|
BrowserSessionProfile,
|
|
BrowserSessionProfileSource
|
|
} from './browser-workspace-types'
|
|
import type { BROWSER_UNAVAILABLE_ERROR_CODE } from './runtime-session-contracts'
|
|
|
|
export type BrowserSnapshotRef = { ref: string; role: string; name: string }
|
|
|
|
// agent-browser's `snapshot --delta` answer; `full` carries the tree, the others change the last one.
|
|
export type BrowserSnapshotDelta =
|
|
| { kind: 'full'; revision: number; tree: string }
|
|
| { kind: 'unchanged'; revision: number; baseRevision: number }
|
|
| {
|
|
kind: 'delta'
|
|
revision: number
|
|
baseRevision: number
|
|
treeChange?: { startLine: number; deleteCount: number; lines: string[] }
|
|
}
|
|
|
|
export type BrowserSnapshotResult = {
|
|
browserPageId: string
|
|
snapshot: string | BrowserSnapshotDelta
|
|
refs: BrowserSnapshotRef[]
|
|
url: string
|
|
title: string
|
|
}
|
|
|
|
/** What an agent's page action caused; absent for people's input and from runtimes that predate it. */
|
|
export type BrowserActionEffects = {
|
|
/** Set only when the action changed them. */
|
|
url?: string
|
|
title?: string
|
|
/** A navigation was still loading at the settle cap, so no page changes are listed. */
|
|
loading?: boolean
|
|
loadError?: string
|
|
/** No browserPageId: the tab was requested but had not opened by the settle cap. */
|
|
newTabs?: { browserPageId?: string; url: string }[]
|
|
popups?: { action: 'blocked' | 'opened-external' | 'window'; origin: string }[]
|
|
downloads?: { filename: string; origin: string }[]
|
|
consoleErrors?: string[]
|
|
/** Snapshot lines changed since the last reading; `full` is the whole new tree. Absent when unread. */
|
|
page?: 'unchanged' | { lines: string[]; removedLines: number; full: boolean; truncated: boolean }
|
|
/** False when the page was still changing at the settle cap. */
|
|
settled: boolean
|
|
}
|
|
|
|
export type BrowserClickResult = { clicked: string }
|
|
export type BrowserGotoResult = { url: string; title: string }
|
|
export type BrowserFillResult = { filled: string }
|
|
export type BrowserTypeResult = { typed: boolean }
|
|
export type BrowserSelectResult = { selected: string }
|
|
export type BrowserScrollResult = { scrolled: 'up' | 'down' }
|
|
export type BrowserBackResult = { url: string; title: string }
|
|
export type BrowserReloadResult = { url: string; title: string }
|
|
export type BrowserScreenshotAnnotation = {
|
|
ref: string
|
|
number: number
|
|
role: string
|
|
name?: string
|
|
box: { x: number; y: number; width: number; height: number }
|
|
}
|
|
|
|
export type BrowserScreenshotResult = {
|
|
data: string
|
|
format: 'png' | 'jpeg'
|
|
// Set only for --if-changed; false means nothing was captured and data is empty.
|
|
changed?: boolean
|
|
annotations?: BrowserScreenshotAnnotation[]
|
|
}
|
|
|
|
export type BrowserScreencastReadyResult = {
|
|
type: 'ready'
|
|
subscriptionId: string
|
|
browserPageId: string
|
|
format: 'jpeg' | 'png'
|
|
tab: BrowserTabInfo
|
|
}
|
|
|
|
export type BrowserScreencastEndResult = { type: 'end'; subscriptionId: string }
|
|
export type BrowserScreencastDialogResult = { type: 'dialog'; dialogType: string; message: string }
|
|
export type BrowserScreencastDialogClosedResult = { type: 'dialogClosed' }
|
|
export type BrowserScreencastErrorResult = { type: 'error'; message: string }
|
|
|
|
export type BrowserScreencastResult =
|
|
| BrowserScreencastReadyResult
|
|
| BrowserScreencastEndResult
|
|
| BrowserScreencastDialogResult
|
|
| BrowserScreencastDialogClosedResult
|
|
| BrowserScreencastErrorResult
|
|
|
|
export type BrowserEvalResult = { result: string; origin: string }
|
|
|
|
export type BrowserTabInfo = {
|
|
browserPageId: string
|
|
index: number
|
|
url: string
|
|
title: string
|
|
active: boolean
|
|
loadError?: BrowserLoadError | null
|
|
certificateFailure?: BrowserCertificateFailure | null
|
|
worktreeId?: string | null
|
|
profileId?: string | null
|
|
profileLabel?: string | null
|
|
/** Set on a popup window (OAuth, payment): the tab whose page opened it. */
|
|
openerBrowserPageId?: string
|
|
/** False for a restored tab whose page has not loaded; a command aimed at it loads it. */
|
|
loaded?: boolean
|
|
}
|
|
|
|
export type BrowserTabListResult = { tabs: BrowserTabInfo[] }
|
|
export type BrowserTabSwitchResult = { switched: number; browserPageId: string }
|
|
|
|
export type BrowserTabSetProfileResult = {
|
|
browserPageId: string
|
|
profileId: string | null
|
|
profileLabel: string | null
|
|
}
|
|
|
|
export type BrowserTabShowResult = { tab: BrowserTabInfo }
|
|
export type BrowserTabCurrentResult = { tab: BrowserTabInfo }
|
|
|
|
export type BrowserTabProfileShowResult = {
|
|
browserPageId: string
|
|
worktreeId: string | null
|
|
profileId: string | null
|
|
profileLabel: string | null
|
|
}
|
|
|
|
export type BrowserTabProfileCloneResult = {
|
|
browserPageId: string
|
|
sourceBrowserPageId: string
|
|
profileId: string | null
|
|
profileLabel: string | null
|
|
}
|
|
|
|
export type BrowserProfileListResult = {
|
|
profiles: BrowserSessionProfile[]
|
|
executorRuntimeId?: string
|
|
}
|
|
export type BrowserProfileCreateResult = { profile: BrowserSessionProfile | null }
|
|
export type BrowserProfileDeleteResult = { deleted: boolean; profileId: string }
|
|
export type BrowserDetectedProfileInfo = { name: string; directory: string }
|
|
|
|
export type BrowserDetectedInfo = {
|
|
family: BrowserSessionProfileSource['browserFamily']
|
|
label: string
|
|
profiles: BrowserDetectedProfileInfo[]
|
|
selectedProfile: string
|
|
}
|
|
|
|
export type BrowserDetectProfilesResult = { browsers: BrowserDetectedInfo[] }
|
|
export type BrowserProfileImportFromBrowserResult = BrowserCookieImportResult & {
|
|
executorRuntimeId?: string
|
|
}
|
|
export type BrowserProfileClearDefaultCookiesResult = { cleared: boolean }
|
|
export type BrowserHoverResult = { hovered: string }
|
|
export type BrowserDragResult = { dragged: { from: string; to: string } }
|
|
// Older hosts and server-hosted pages report only the count.
|
|
export type BrowserUploadResult = { uploaded: number; frameUrl?: string }
|
|
export type BrowserWaitResult = { waited: boolean }
|
|
export type BrowserCheckResult = { checked: boolean }
|
|
export type BrowserFocusResult = { focused: string }
|
|
export type BrowserClearResult = { cleared: string }
|
|
export type BrowserSelectAllResult = { selected: string }
|
|
export type BrowserKeypressResult = { pressed: string }
|
|
export type BrowserPdfResult = { data: string }
|
|
|
|
export type BrowserCookie = {
|
|
name: string
|
|
value: string
|
|
domain: string
|
|
path: string
|
|
expires: number
|
|
httpOnly: boolean
|
|
secure: boolean
|
|
sameSite: string
|
|
}
|
|
|
|
export type BrowserCookieGetResult = { cookies: BrowserCookie[] }
|
|
export type BrowserCookieSetResult = { success: boolean }
|
|
export type BrowserCookieDeleteResult = { deleted: boolean }
|
|
|
|
export type BrowserViewportResult = {
|
|
width: number
|
|
height: number
|
|
deviceScaleFactor: number
|
|
mobile: boolean
|
|
}
|
|
|
|
export type BrowserGeolocationResult = {
|
|
latitude: number
|
|
longitude: number
|
|
accuracy: number
|
|
}
|
|
|
|
export type BrowserInterceptedRequest = {
|
|
id: string
|
|
url: string
|
|
method: string
|
|
headers: Record<string, string>
|
|
resourceType: string
|
|
}
|
|
|
|
export type BrowserInterceptEnableResult = { enabled: boolean; patterns: string[] }
|
|
export type BrowserInterceptDisableResult = { disabled: boolean }
|
|
|
|
export type BrowserConsoleEntry = {
|
|
level: string
|
|
text: string
|
|
timestamp?: number
|
|
url?: string
|
|
line?: number
|
|
}
|
|
|
|
export type BrowserConsoleResult = { entries: BrowserConsoleEntry[]; truncated: boolean }
|
|
|
|
export type BrowserNetworkEntry = {
|
|
url: string
|
|
method: string
|
|
// Absent while a request is pending or after it failed.
|
|
status?: number
|
|
mimeType?: string
|
|
size?: number
|
|
timestamp?: number
|
|
}
|
|
|
|
export type BrowserNetworkLogResult = { entries: BrowserNetworkEntry[]; truncated: boolean }
|
|
export type BrowserCaptureStartResult = { capturing: boolean }
|
|
export type BrowserCaptureStopResult = { stopped: boolean }
|
|
export type BrowserTabCreateResult = { browserPageId: string }
|
|
|
|
export type BrowserErrorCode =
|
|
| typeof BROWSER_UNAVAILABLE_ERROR_CODE
|
|
| 'browser_command_unavailable'
|
|
| 'browser_profile_unavailable'
|
|
| 'browser_screencast_unavailable'
|
|
| 'browser_certificate_trust_unavailable'
|
|
| 'browser_no_tab'
|
|
| 'browser_tab_not_found'
|
|
| 'browser_tab_closed'
|
|
| 'browser_tab_changed'
|
|
| 'browser_owner_unavailable'
|
|
| 'browser_stale_ref'
|
|
| 'browser_ref_not_found'
|
|
| 'browser_navigation_failed'
|
|
| 'browser_element_not_interactable'
|
|
| 'browser_eval_error'
|
|
| 'browser_cdp_error'
|
|
| 'browser_debugger_detached'
|
|
| 'browser_timeout'
|
|
| 'browser_error'
|