From 22e567d3e485dbff926aa1b9ac6f901c364f95f8 Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Mon, 10 Aug 2026 06:15:27 -0700 Subject: [PATCH] fix(browser): reattach to the running browser on startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The browser keeps its remote debugging preference across its own restarts, and keeps an approved connection grant for as long as it stays running. BitFun's connection registry, however, is process-wide state, so restarting BitFun dropped the connection even though there was nothing on the browser side left to redo — Settings reported "not connected" until something asked for the browser again. Reattach at startup when, and only when, there is a live endpoint to attach to. This never starts a browser and never opens a settings page, and a denial or approval timeout is logged rather than surfaced, since the user did not ask for this particular connection. Settings also could not tell "ready, nothing attached yet" apart from "nothing to attach to" — both read as "not connected", which looks broken when the browser is in fact set up correctly. Report the ready state separately, and resolve it from a single endpoint probe that also answers whether the persistent setting is on. That probe is a file read plus a short local TCP connect, so unlike attaching it never prompts the browser. --- .../desktop/src/api/browser_control_api.rs | 61 ++++++++++++++++++- src/apps/desktop/src/lib.rs | 10 +++ .../tools/browser_control/browser_launcher.rs | 7 +++ .../config/components/SessionConfig.tsx | 11 +++- .../en-US/settings/session-config.json | 1 + .../zh-CN/settings/session-config.json | 1 + .../zh-TW/settings/session-config.json | 1 + 7 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/apps/desktop/src/api/browser_control_api.rs b/src/apps/desktop/src/api/browser_control_api.rs index 209948a7f..e36bd0421 100644 --- a/src/apps/desktop/src/api/browser_control_api.rs +++ b/src/apps/desktop/src/api/browser_control_api.rs @@ -18,6 +18,52 @@ fn default_cdp_port() -> u16 { DEFAULT_CDP_PORT } +/// Reattach to a browser that is already running with remote debugging on. +/// +/// The browser remembers the remote debugging preference across its own +/// restarts, and it keeps an approved connection grant for as long as it stays +/// running — but BitFun's connection registry lives in this process, so every +/// BitFun restart otherwise leaves Settings reporting "not connected" until +/// something asks for the browser. Reattaching here restores that connection +/// without the user having to click anything. +/// +/// This never starts a browser and never opens a settings page: when there is +/// no live endpoint to reattach to, it does nothing and leaves the on-demand +/// path to handle it. +pub fn init_on_startup() { + tokio::spawn(async { + let Ok(kind) = selected_browser_kind().await else { + return; + }; + let Some(endpoint) = BrowserLauncher::user_profile_debug_endpoint(&kind) else { + return; + }; + if CdpClient::browser_connection_for_kind(DEFAULT_CDP_PORT, &kind) + .await + .is_some() + { + return; + } + // A denial or an approval timeout is an ordinary outcome here, not an + // error worth surfacing: the user never asked for this connection. + match CdpClient::connect_user_profile_browser( + DEFAULT_CDP_PORT, + endpoint.port, + &kind, + &endpoint.web_socket_url, + ) + .await + { + Ok(_) => log::info!("Reattached to the running {} profile on startup", kind), + Err(error) => log::info!( + "Could not reattach to the running {} profile on startup: {}", + kind, + error + ), + } + }); +} + async fn selected_browser_kind() -> Result { let config = get_global_config_service() .await @@ -93,6 +139,10 @@ pub struct BrowserControlStatusResponse { pub cdp_available: bool, pub default_cdp_supported: bool, pub default_cdp_enabled: bool, + /// The selected browser is running with remote debugging on, so BitFun can + /// attach whenever it needs to. Distinguishes "ready, nothing attached yet" + /// from "nothing to attach to", which both used to read as "not connected". + pub browser_ready: bool, pub browser_kind: String, pub browser_version: Option, pub port: u16, @@ -107,7 +157,14 @@ pub async fn browser_control_get_status( let port = request.port; let configured_kind = selected_browser_kind().await?; let default_cdp_supported = BrowserLauncher::supports_default_cdp(&configured_kind); - let default_cdp_enabled = BrowserLauncher::is_default_cdp_enabled(&configured_kind); + // Probe the live endpoint once and answer both questions from it: whether + // the persistent setting is on, and whether there is something to attach to + // right now. The probe is a file read plus a short local TCP connect, so it + // never prompts the browser the way attaching does. + let user_profile_endpoint = BrowserLauncher::user_profile_debug_endpoint(&configured_kind); + let default_cdp_enabled = default_cdp_supported + && (user_profile_endpoint.is_some() + || BrowserLauncher::is_default_cdp_enabled(&configured_kind)); let user_profile_connection = CdpClient::browser_connection_for_kind(port, &configured_kind).await; let legacy_version = @@ -132,6 +189,7 @@ pub async fn browser_control_get_status( } }); let available = user_profile_connection.is_some() || legacy_matches_selection; + let browser_ready = available || user_profile_endpoint.is_some(); let (version, page_count, actual_kind) = if available { let ver_info = if let Some(connection) = &user_profile_connection { @@ -167,6 +225,7 @@ pub async fn browser_control_get_status( cdp_available: available, default_cdp_supported, default_cdp_enabled, + browser_ready, browser_kind: actual_kind.to_string(), browser_version: version, port, diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index 1118116a4..aa24b400d 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -1070,6 +1070,16 @@ pub async fn run() { step_started, ); + // Reattach to a browser that is already running with remote + // debugging on, so a BitFun restart does not drop the connection. + let step_started = Instant::now(); + api::browser_control_api::init_on_startup(); + startup_trace.record_elapsed_step( + "native_setup", + "browser_control_init_on_startup", + step_started, + ); + { let step_started = Instant::now(); let _terminal_state: tauri::State<'_, api::terminal_api::TerminalState> = diff --git a/src/crates/assembly/core/src/agentic/tools/browser_control/browser_launcher.rs b/src/crates/assembly/core/src/agentic/tools/browser_control/browser_launcher.rs index a0fc78c11..f25a72903 100644 --- a/src/crates/assembly/core/src/agentic/tools/browser_control/browser_launcher.rs +++ b/src/crates/assembly/core/src/agentic/tools/browser_control/browser_launcher.rs @@ -61,6 +61,13 @@ impl BrowserLauncher { provider::BrowserLauncher::is_default_cdp_enabled(kind) } + /// Browser-level endpoint published by a browser that is running right now + /// with remote debugging enabled. `None` means there is nothing to attach + /// to without going through the launch flow. + pub fn user_profile_debug_endpoint(kind: &BrowserKind) -> Option { + provider::BrowserLauncher::user_profile_debug_endpoint(kind) + } + pub async fn launch_with_cdp(kind: &BrowserKind, port: u16) -> BitFunResult { Ok(provider::BrowserLauncher::launch_with_cdp_options( kind, diff --git a/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx b/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx index ee951e4fd..75625fa5f 100644 --- a/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx @@ -143,6 +143,7 @@ const SessionSettingsPanels: React.FC = ({ variant } // ── Browser control state ─────────────────────────────────────────────── const [browserCdpAvailable, setBrowserCdpAvailable] = useState(false); + const [browserReady, setBrowserReady] = useState(false); const [browserDefaultCdpSupported, setBrowserDefaultCdpSupported] = useState(false); const [browserDefaultCdpEnabled, setBrowserDefaultCdpEnabled] = useState(false); const [browserKind, setBrowserKind] = useState(''); @@ -191,6 +192,7 @@ const SessionSettingsPanels: React.FC = ({ variant } cdpAvailable: boolean; defaultCdpSupported: boolean; defaultCdpEnabled: boolean; + browserReady: boolean; browserKind: string; browserVersion: string | null; port: number; @@ -201,6 +203,7 @@ const SessionSettingsPanels: React.FC = ({ variant } setBrowserCdpAvailable(s.cdpAvailable); setBrowserDefaultCdpSupported(s.defaultCdpSupported); setBrowserDefaultCdpEnabled(s.defaultCdpEnabled); + setBrowserReady(s.browserReady); setBrowserKind(s.browserKind); setBrowserVersion(s.browserVersion); setBrowserPageCount(s.pageCount); @@ -899,9 +902,15 @@ const SessionSettingsPanels: React.FC = ({ variant } const computerUseScreenLabel = computerUseStatusLoading ? t('loading.text') : computerUseScreen ? t('computerUse.granted') : t('computerUse.notGranted'); + // A ready browser is not a failure state: BitFun attaches to it the moment + // something needs it, so say that rather than the bare "not connected". const browserStatusLabel = browserCdpAvailable ? `${browserKind} · ${browserPageCount} ${t('browserControl.tabs')}` - : browserStatusLoading ? t('loading.text') : t('browserControl.notConnected'); + : browserStatusLoading + ? t('loading.text') + : browserReady + ? t('browserControl.readyNotConnected') + : t('browserControl.notConnected'); const browserSelectOptions: SelectOption[] = browserOptions.map((option) => ({ value: option.value, label: option.installed ? option.label : `${option.label} (${t('browserControl.notInstalled')})`, diff --git a/src/web-ui/src/locales/en-US/settings/session-config.json b/src/web-ui/src/locales/en-US/settings/session-config.json index 823e5bf43..6bb6f5b50 100644 --- a/src/web-ui/src/locales/en-US/settings/session-config.json +++ b/src/web-ui/src/locales/en-US/settings/session-config.json @@ -148,6 +148,7 @@ "status": "Connection", "statusDesc": "", "notConnected": "Not connected", + "readyNotConnected": "Ready, connects on use", "refreshStatus": "Refresh status", "connect": "Connect", "defaultCdp": "Default CDP", diff --git a/src/web-ui/src/locales/zh-CN/settings/session-config.json b/src/web-ui/src/locales/zh-CN/settings/session-config.json index 15331ce00..196216219 100644 --- a/src/web-ui/src/locales/zh-CN/settings/session-config.json +++ b/src/web-ui/src/locales/zh-CN/settings/session-config.json @@ -148,6 +148,7 @@ "status": "连接状态", "statusDesc": "", "notConnected": "未连接", + "readyNotConnected": "已就绪,使用时自动连接", "refreshStatus": "刷新状态", "connect": "连接浏览器", "defaultCdp": "默认 CDP", diff --git a/src/web-ui/src/locales/zh-TW/settings/session-config.json b/src/web-ui/src/locales/zh-TW/settings/session-config.json index 526b794b5..6b163bac2 100644 --- a/src/web-ui/src/locales/zh-TW/settings/session-config.json +++ b/src/web-ui/src/locales/zh-TW/settings/session-config.json @@ -145,6 +145,7 @@ "status": "連接狀態", "statusDesc": "", "notConnected": "未連接", + "readyNotConnected": "已就緒,使用時自動連接", "refreshStatus": "重新整理狀態", "connect": "連接瀏覽器", "defaultCdp": "預設 CDP",