@@ -24,16 +24,12 @@ import {
2424 clickElement ,
2525 collectSnapshot ,
2626 getViewportInfo ,
27- hasTakeoverBanner ,
2827 hoverElement ,
29- isTakeoverDone ,
3028 pageContainsText ,
3129 pressKeyOnPage ,
3230 readPageText ,
33- removeTakeoverBanner ,
3431 scrollPage ,
3532 selectOptionInElement ,
36- showTakeoverBanner ,
3733 typeIntoElement ,
3834} from '@/main/browser-agent/page-functions'
3935import * as session from '@/main/browser-agent/session'
@@ -73,13 +69,22 @@ function recordNotice(notice: string): void {
7369 if ( pendingNotices . length < 10 ) pendingNotices . push ( notice )
7470}
7571
72+ /**
73+ * Takeover state lives here (session-level, not in the page) so the panel's
74+ * takeover strip survives navigations and tab switches. The reason rides
75+ * every page-state push; the panel's Done chip sends `takeover-done`.
76+ */
77+ let takeoverReason : string | null = null
78+ let takeoverDone = false
79+
7680function pageStateFor ( contents : WebContents ) : BrowserPageState {
7781 return {
7882 url : contents . getURL ( ) ,
7983 title : contents . getTitle ( ) ,
8084 loading : contents . isLoading ( ) ,
8185 canGoBack : contents . navigationHistory . canGoBack ( ) ,
8286 canGoForward : contents . navigationHistory . canGoForward ( ) ,
87+ ...( takeoverReason !== null ? { takeoverReason } : { } ) ,
8388 }
8489}
8590
@@ -344,34 +349,38 @@ export function parseKeyCombo(combo: string): ParsedCombo {
344349
345350/**
346351 * Hands control to the user IN THE PANEL: the page is already natively
347- * interactive there, so a banner on the page explains what to do and carries
348- * the "Done" button; the tool resolves when the user clicks it.
352+ * interactive there, so the panel chrome shows a takeover strip (driven by
353+ * `takeoverReason` on page-state pushes) with the Done chip; the tool
354+ * resolves when that chip sends the `takeover-done` panel action. Nothing is
355+ * injected into the page, so the strip never covers page content and
356+ * survives navigations.
349357 */
350358async function runTakeover ( reason : string ) : Promise < unknown > {
351359 const tab = session . ensureTab ( )
352360 const contents = tab . view . webContents
353- await execInPage ( contents , showTakeoverBanner , [ reason ] ) . catch ( ( ) => { } )
361+ takeoverReason = reason
362+ takeoverDone = false
363+ pushPageState ( contents )
354364
355365 const startedAt = Date . now ( )
356- while ( Date . now ( ) - startedAt < TAKEOVER_MAX_MS ) {
357- await sleep ( TAKEOVER_POLL_MS )
358- if ( ! session . hasSession ( ) || contents . isDestroyed ( ) ) {
359- throw new ToolError (
360- 'The browser session was closed during takeover. Ask the user what happened, then reopen with browser_navigate.'
361- )
362- }
363- const done = await execInPage ( contents , isTakeoverDone , [ ] ) . catch ( ( ) => false )
364- if ( done ) {
365- await execInPage ( contents , removeTakeoverBanner , [ ] ) . catch ( ( ) => { } )
366- return { completed : true , elapsedMs : Date . now ( ) - startedAt }
367- }
368- // The banner disappears on navigation; keep it visible until the user is done.
369- const bannerVisible = await execInPage ( contents , hasTakeoverBanner , [ ] ) . catch ( ( ) => true )
370- if ( ! bannerVisible ) {
371- await execInPage ( contents , showTakeoverBanner , [ reason ] ) . catch ( ( ) => { } )
366+ try {
367+ while ( Date . now ( ) - startedAt < TAKEOVER_MAX_MS ) {
368+ await sleep ( TAKEOVER_POLL_MS )
369+ if ( ! session . hasSession ( ) || contents . isDestroyed ( ) ) {
370+ throw new ToolError (
371+ 'The browser session was closed during takeover. Ask the user what happened, then reopen with browser_navigate.'
372+ )
373+ }
374+ if ( takeoverDone ) {
375+ return { completed : true , elapsedMs : Date . now ( ) - startedAt }
376+ }
372377 }
378+ throw new ToolError ( 'Takeover timed out after 12 hours without the user finishing.' )
379+ } finally {
380+ takeoverReason = null
381+ takeoverDone = false
382+ if ( ! contents . isDestroyed ( ) ) pushPageState ( contents )
373383 }
374- throw new ToolError ( 'Takeover timed out after 12 hours without the user finishing.' )
375384}
376385
377386// ---------------------------------------------------------------------------
@@ -620,6 +629,12 @@ export async function executeTool(
620629
621630/** Browser-chrome commands from the panel header; fire-and-forget. */
622631export async function handlePanelAction ( action : BrowserPanelAction ) : Promise < void > {
632+ // The Done chip on the panel's takeover strip: hands control back to the
633+ // agent. Meaningful only while a takeover is actually waiting.
634+ if ( action . action === 'takeover-done' ) {
635+ if ( takeoverReason !== null ) takeoverDone = true
636+ return
637+ }
623638 // Navigate bootstraps the session: the user can open the panel manually
624639 // (before the agent ever touched the browser) and drive it from the URL
625640 // bar. The other chrome actions need an existing page.
0 commit comments