Skip to content

Commit 6d929c0

Browse files
committed
fix banner
1 parent fb4c2de commit 6d929c0

6 files changed

Lines changed: 67 additions & 73 deletions

File tree

apps/desktop/electron-builder.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ electronFuses:
2020
enableNodeCliInspectArguments: false
2121
enableEmbeddedAsarIntegrityValidation: true
2222
onlyLoadAppFromAsar: true
23-
strictlyRequireAllFuses: true
23+
resetAdHocDarwinSignature: true
2424

2525
mac:
2626
category: public.app-category.developer-tools

apps/desktop/src/main/browser-agent/driver.ts

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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'
3935
import * 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+
7680
function 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
*/
350358
async 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. */
622631
export 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.

apps/desktop/src/main/browser-agent/page-functions.ts

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ export interface PageActionError {
2020
error: string
2121
}
2222

23-
export const TAKEOVER_BANNER_ID = '__sim-takeover-banner'
24-
export const TAKEOVER_DONE_ATTR = 'data-sim-takeover-done'
25-
2623
/**
2724
* Builds the page snapshot: a structural outline (headings, landmarks) with
2825
* interactive elements carrying numeric ids, walking open shadow roots and
@@ -79,7 +76,6 @@ export function collectSnapshot(): unknown {
7976
let truncated = false
8077

8178
const isVisible = (el: Element): boolean => {
82-
if (el.closest('#__sim-takeover-banner')) return false
8379
const rect = el.getBoundingClientRect()
8480
if (rect.width <= 0 || rect.height <= 0) return false
8581
const doc = el.ownerDocument
@@ -441,42 +437,3 @@ export function getViewportInfo(): unknown {
441437
height: window.innerHeight,
442438
}
443439
}
444-
445-
export function showTakeoverBanner(reason: string): unknown {
446-
document.documentElement.removeAttribute('data-sim-takeover-done')
447-
if (document.getElementById('__sim-takeover-banner')) return { shown: true }
448-
const bar = document.createElement('div')
449-
bar.id = '__sim-takeover-banner'
450-
bar.style.cssText =
451-
'position:fixed;top:0;left:0;right:0;z-index:2147483647;background:#18181b;color:#fff;' +
452-
'font:14px system-ui,sans-serif;padding:10px 16px;display:flex;align-items:center;' +
453-
'gap:12px;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,0.35)'
454-
const label = document.createElement('span')
455-
label.textContent = `Sim handed you control: ${reason}`
456-
const button = document.createElement('button')
457-
button.textContent = 'Done — give control back to Sim'
458-
button.style.cssText =
459-
'background:#6f5ff2;color:#fff;border:0;border-radius:6px;padding:6px 12px;' +
460-
'font:600 13px system-ui,sans-serif;cursor:pointer'
461-
button.addEventListener('click', () => {
462-
document.documentElement.setAttribute('data-sim-takeover-done', '1')
463-
bar.remove()
464-
})
465-
bar.append(label, button)
466-
document.documentElement.append(bar)
467-
return { shown: true }
468-
}
469-
470-
export function isTakeoverDone(): boolean {
471-
return document.documentElement.getAttribute('data-sim-takeover-done') === '1'
472-
}
473-
474-
export function hasTakeoverBanner(): boolean {
475-
return Boolean(document.getElementById('__sim-takeover-banner'))
476-
}
477-
478-
export function removeTakeoverBanner(): boolean {
479-
document.getElementById('__sim-takeover-banner')?.remove()
480-
document.documentElement.removeAttribute('data-sim-takeover-done')
481-
return true
482-
}

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { useCallback, useEffect, useRef, useState } from 'react'
4-
import { Tooltip } from '@sim/emcn'
4+
import { Chip, Tooltip } from '@sim/emcn'
55
import { ArrowLeft, ArrowRight, Cursor, RefreshCw } from '@sim/emcn/icons'
66
import { reportBrowserPanelBounds, sendBrowserPanelAction } from '@/lib/browser-agent/transport'
77
import { useBrowserSessionStore } from '@/stores/browser-session/store'
@@ -154,6 +154,20 @@ export function BrowserSession() {
154154
}}
155155
/>
156156
</div>
157+
{/* Takeover strip: sits in the panel chrome ABOVE the page rect, so it
158+
never covers page content. Done hands control back to Sim. */}
159+
{typeof pageState?.takeoverReason === 'string' && (
160+
<div className='flex items-center gap-2 border-[var(--border)] border-b px-3 py-1.5'>
161+
<p className='min-w-0 flex-1 truncate text-[var(--text-muted)] text-caption'>
162+
{pageState.takeoverReason.trim()
163+
? `Sim is waiting for you — ${pageState.takeoverReason.trim()}`
164+
: 'Sim is waiting for you'}
165+
</p>
166+
<Chip variant='primary' onClick={() => sendBrowserPanelAction('takeover-done')}>
167+
Done
168+
</Chip>
169+
</div>
170+
)}
157171
{/* Host area: the real page is overlaid exactly on this rect. */}
158172
<div ref={hostRef} className='relative flex-1 overflow-hidden bg-[var(--surface-secondary)]'>
159173
{(!pageState || !sessionAlive) && (

packages/browser-protocol/src/index.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,12 @@ export interface BrowserPanelBounds {
6969

7070
/**
7171
* Browser-chrome commands from the panel header (URL bar, back/forward,
72-
* reload). Page interactions need no protocol — the user acts on the real
73-
* embedded page directly.
72+
* reload) plus `takeover-done`, sent by the panel's takeover strip when the
73+
* user finishes a hand-control-back request. Page interactions need no
74+
* protocol — the user acts on the real embedded page directly.
7475
*/
7576
export interface BrowserPanelAction {
76-
action: 'navigate' | 'reload' | 'back' | 'forward'
77+
action: 'navigate' | 'reload' | 'back' | 'forward' | 'takeover-done'
7778
/** Absolute URL for `navigate` (typed into the panel's URL bar). */
7879
url?: string
7980
}
@@ -85,6 +86,13 @@ export interface BrowserPageState {
8586
loading: boolean
8687
canGoBack: boolean
8788
canGoForward: boolean
89+
/**
90+
* Set while `browser_request_takeover` is waiting on the user: the model's
91+
* reason for handing over control (e.g. "Log in to your account"). The
92+
* panel renders its takeover strip from this; it is session-level state,
93+
* so it survives page navigations and tab switches.
94+
*/
95+
takeoverReason?: string
8896
}
8997

9098
/**

packages/desktop-bridge/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export interface SimDesktopBrowserAgentApi {
2323
* rejects for tool-level failures (those ride `ok: false`).
2424
*/
2525
executeTool(tool: BrowserToolName, params: Record<string, unknown>): Promise<BrowserToolResponse>
26-
/** Browser-chrome commands from the panel header (URL bar, back, reload). */
26+
/** Browser-chrome commands from the panel (URL bar, back, reload, takeover Done). */
2727
panelAction(action: BrowserPanelAction): void
2828
/**
2929
* Report where the browser panel sits in the window (CSS pixels relative

0 commit comments

Comments
 (0)