fix: activate browser view before CLI commands so webviews mount after restart

After app restart, activeTabType defaults to 'terminal' which keeps the
browser container at display:none. Electron won't start webview guest
processes inside hidden containers, so dom-ready never fires and
registerGuest never runs — making persisted browser tabs inoperable.

ensureBrowserWorktreeActive now sends browser:activateView IPC to flip
activeTabType to 'browser', ensuring the webview mounts and registers
before the CLI command executes.
This commit is contained in:
Jinwoo-H 2026-04-19 23:18:31 -04:00
parent dd31dd30bd
commit 377a0525b2
4 changed files with 25 additions and 2 deletions

View file

@ -1189,8 +1189,10 @@ export class OrcaRuntimeService {
}
// Why: browser tabs only mount (and become operable) when their worktree is
// the active worktree in the renderer. If the CLI targets a different worktree,
// we must switch the UI first so the webview mounts and registerGuest fires.
// the active worktree in the renderer AND activeTabType is 'browser'. If either
// condition is false, the webview stays in display:none and Electron won't start
// its guest process — dom-ready never fires, registerGuest never runs, and CLI
// browser commands fail with "CDP connection refused".
private async ensureBrowserWorktreeActive(worktreeId: string): Promise<void> {
const win = this.getAuthoritativeWindow()
const repoId = worktreeId.split('::')[0]
@ -1198,6 +1200,9 @@ export class OrcaRuntimeService {
return
}
win.webContents.send('ui:activateWorktree', { repoId, worktreeId })
// Why: switching worktree alone sets activeView='terminal'. Browser webviews
// won't mount until activeTabType is 'browser'. Send a second IPC to flip it.
win.webContents.send('browser:activateView', { worktreeId })
// Why: give the renderer time to mount the webview after switching worktrees.
// The webview needs to attach and fire dom-ready before registerGuest runs.
await new Promise((resolve) => setTimeout(resolve, 500))

View file

@ -111,6 +111,7 @@ export type BrowserApi = {
onNavigationUpdate: (
callback: (event: { browserPageId: string; url: string; title: string }) => void
) => () => void
onActivateView: (callback: (data: { worktreeId: string }) => void) => () => void
onOpenLinkInOrcaTab: (
callback: (event: { browserPageId: string; url: string }) => void
) => () => void

View file

@ -669,6 +669,13 @@ const api = {
return () => ipcRenderer.removeListener('browser:navigation-update', listener)
},
onActivateView: (callback: (data: { worktreeId: string }) => void): (() => void) => {
const listener = (_event: Electron.IpcRendererEvent, data: { worktreeId: string }) =>
callback(data)
ipcRenderer.on('browser:activateView', listener)
return () => ipcRenderer.removeListener('browser:activateView', listener)
},
onOpenLinkInOrcaTab: (
callback: (event: { browserPageId: string; url: string }) => void
): (() => void) => {

View file

@ -168,6 +168,16 @@ export function useIpcEvents(): void {
})
)
// Why: browser webviews only start their guest process when the container
// has display != none. After app restart, activeTabType defaults to 'terminal'
// so persisted browser tabs never mount. The main process sends this IPC
// before browser commands so the webview can start and registerGuest fires.
unsubs.push(
window.api.browser.onActivateView(() => {
useAppStore.getState().setActiveTabType('browser')
})
)
unsubs.push(
window.api.browser.onOpenLinkInOrcaTab(({ browserPageId, url }) => {
const store = useAppStore.getState()