Skip to main content

oxios_kernel/kernel_handle/
browser_api.rs

1//! Browser API — browser backend facade.
2//!
3//! When the `browser` feature is enabled, lazily initializes an
4//! `Arc<oxibrowser_core::Browser>` on first use from an async context.
5//! This avoids the `block_on` panic that occurs when constructing
6//! inside an existing tokio runtime.
7
8use std::sync::Arc;
9
10/// Browser management system calls.
11///
12/// Wraps the embedded OxiBrowser engine. `BrowserTool` borrows the `Browser`
13/// via `browser()` for agent tool calls.
14///
15/// Initialization is **lazy**: the browser engine is only created on the
16/// first call to [`browser()`](Self::browser) from an async context.
17/// [`from_config()`](Self::from_config) and [`Default::default()`] are
18/// cheap and never panic.
19#[cfg(feature = "browser")]
20pub struct BrowserApi {
21    inner: tokio::sync::OnceCell<Arc<oxibrowser_core::Browser>>,
22    config: Option<oxibrowser_core::BrowserConfig>,
23}
24
25#[cfg(feature = "browser")]
26impl Clone for BrowserApi {
27    fn clone(&self) -> Self {
28        Self {
29            inner: self.inner.clone(),
30            config: self.config.clone(),
31        }
32    }
33}
34
35#[cfg(feature = "browser")]
36impl BrowserApi {
37    /// Create a new BrowserApi that will lazily initialize a Browser from config.
38    ///
39    /// This is cheap — no I/O or runtime calls happen here.
40    pub fn from_config(config: &oxibrowser_core::BrowserConfig) -> Self {
41        Self {
42            inner: tokio::sync::OnceCell::new(),
43            config: Some(config.clone()),
44        }
45    }
46
47    /// Create a new BrowserApi from an already-initialized Browser.
48    pub fn new(browser: Arc<oxibrowser_core::Browser>) -> Self {
49        let cell = tokio::sync::OnceCell::new();
50        // SAFETY: cell was just created, it's empty.
51        match cell.set(browser) {
52            Ok(()) => {}
53            Err(_) => unreachable!("OnceCell was just created"),
54        }
55        Self {
56            inner: cell,
57            config: None,
58        }
59    }
60
61    /// Get the browser engine, initializing it lazily if needed.
62    ///
63    /// Must be called from an async context (tokio runtime active).
64    /// Returns `Err` if no config was provided and the browser was not
65    /// pre-initialized.
66    pub async fn browser(&self) -> anyhow::Result<&Arc<oxibrowser_core::Browser>> {
67        self.inner
68            .get_or_try_init(|| async {
69                let config = self
70                    .config
71                    .as_ref()
72                    .ok_or_else(|| anyhow::anyhow!("BrowserApi has no config and was not pre-initialized"))?;
73                let browser = oxibrowser_core::Browser::new(config.clone())
74                    .await
75                    .map_err(|e| anyhow::anyhow!("Failed to initialize browser engine: {}", e))?;
76                Ok(Arc::new(browser))
77            })
78            .await
79    }
80
81    /// Shut down the browser engine (if it was initialized).
82    pub async fn shutdown(&self) -> anyhow::Result<()> {
83        if let Some(browser) = self.inner.get() {
84            browser.close().await?;
85        }
86        Ok(())
87    }
88}
89
90/// Default — creates an uninitialized BrowserApi.
91///
92/// This is used by `from_subsystems` and when `browser.enabled = false`.
93/// It will return an error on [`browser()`](Self::browser) if no config
94/// is ever set, but will **not panic**.
95#[cfg(feature = "browser")]
96impl Default for BrowserApi {
97    fn default() -> Self {
98        Self {
99            inner: tokio::sync::OnceCell::new(),
100            config: None,
101        }
102    }
103}
104
105/// Zero-sized browser placeholder when the `browser` feature is disabled.
106#[cfg(not(feature = "browser"))]
107pub struct BrowserApi;
108
109#[cfg(not(feature = "browser"))]
110impl Default for BrowserApi {
111    fn default() -> Self {
112        BrowserApi
113    }
114}