Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 170 additions & 27 deletions src/apps/desktop/src/api/browser_control_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ pub async fn browser_control_list_browsers() -> Result<BrowserControlBrowsersRes
#[serde(rename_all = "camelCase")]
pub struct BrowserControlStatusResponse {
pub cdp_available: bool,
pub default_cdp_supported: bool,
pub default_cdp_enabled: bool,
pub browser_kind: String,
pub browser_version: Option<String>,
pub port: u16,
Expand All @@ -103,11 +105,40 @@ pub async fn browser_control_get_status(
request: BrowserControlStatusRequest,
) -> Result<BrowserControlStatusResponse, String> {
let port = request.port;
let available = BrowserLauncher::is_cdp_available(port).await;
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);
let user_profile_connection =
CdpClient::browser_connection_for_kind(port, &configured_kind).await;
let legacy_version =
if user_profile_connection.is_none() && BrowserLauncher::is_cdp_available(port).await {
CdpClient::get_version(port).await.ok()
} else {
None
};
// Chrome and Edge share the logical 9222 slot in Settings. Do not report
// the selected browser as connected merely because the other one owns a
// legacy fixed-port endpoint left from an earlier selection.
let legacy_matches_selection = legacy_version.as_ref().is_some_and(|version| {
let detected = version
.browser
.as_deref()
.and_then(BrowserLauncher::browser_kind_from_cdp_version);
match &configured_kind {
BrowserKind::Chrome | BrowserKind::Edge => {
detected.map(|kind| kind == configured_kind).unwrap_or(true)
}
_ => true,
}
});
let available = user_profile_connection.is_some() || legacy_matches_selection;

let (version, page_count, actual_kind) = if available {
let ver_info = CdpClient::get_version(port).await.ok();
let ver_info = if let Some(connection) = &user_profile_connection {
connection.client.browser_version().await.ok()
} else {
legacy_version
};
let ver = ver_info.as_ref().and_then(|v| v.browser.clone());
// Identify the actual browser from CDP version response.
let kind = ver
Expand All @@ -116,22 +147,26 @@ pub async fn browser_control_get_status(
.unwrap_or_else(|| configured_kind.clone());
// Only count targets of type "page" (real browser tabs),
// not service workers, browser targets, etc.
let pages = CdpClient::list_pages(port)
.await
.ok()
.map(|p| {
p.iter()
.filter(|t| t.page_type.as_deref() == Some("page"))
.count()
})
.unwrap_or(0);
let pages = if let Some(connection) = &user_profile_connection {
connection.client.browser_pages().await.ok()
} else {
CdpClient::list_pages(port).await.ok()
}
.map(|p| {
p.iter()
.filter(|t| t.page_type.as_deref() == Some("page"))
.count()
})
.unwrap_or(0);
(ver, pages, kind)
} else {
(None, 0, configured_kind)
};

Ok(BrowserControlStatusResponse {
cdp_available: available,
default_cdp_supported,
default_cdp_enabled,
browser_kind: actual_kind.to_string(),
browser_version: version,
port,
Expand All @@ -153,6 +188,10 @@ pub struct BrowserControlLaunchResponse {
pub status: String,
pub message: Option<String>,
pub browser_kind: String,
/// Remote debugging settings URL, sent when the user has to open it
/// themselves because the platform cannot open a `chrome://` URL for them.
#[serde(skip_serializing_if = "Option::is_none")]
pub setup_url: Option<String>,
}

fn to_launch_response(kind: &BrowserKind, result: LaunchResult) -> BrowserControlLaunchResponse {
Expand All @@ -162,30 +201,95 @@ fn to_launch_response(kind: &BrowserKind, result: LaunchResult) -> BrowserContro
status: "already_connected".into(),
message: None,
browser_kind: kind.to_string(),
setup_url: None,
},
LaunchResult::Launched => BrowserControlLaunchResponse {
success: true,
status: "launched".into(),
message: None,
browser_kind: kind.to_string(),
setup_url: None,
},
LaunchResult::UserProfileReady { .. } => BrowserControlLaunchResponse {
success: false,
status: "user_profile_ready".into(),
message: None,
browser_kind: kind.to_string(),
setup_url: None,
},
LaunchResult::UserProfileSetupRequired {
instructions,
setup_url,
opened,
..
} => BrowserControlLaunchResponse {
success: false,
// The two cases need different guidance: one asks the user to
// finish on a page that is already in front of them, the other
// asks them to open that page first.
status: if opened {
"requires_user_profile_setup".into()
} else {
"requires_manual_user_profile_setup".into()
},
message: Some(instructions),
browser_kind: kind.to_string(),
setup_url: Some(setup_url),
},
LaunchResult::LaunchedButCdpNotReady { message, .. } => BrowserControlLaunchResponse {
success: false,
status: "cdp_not_ready".into(),
message: Some(message),
browser_kind: kind.to_string(),
setup_url: None,
},
LaunchResult::BrowserRunningWithoutCdp { instructions, .. } => {
BrowserControlLaunchResponse {
success: false,
status: "needs_restart".into(),
message: Some(instructions),
browser_kind: kind.to_string(),
setup_url: None,
}
}
}
}

async fn complete_launch(
kind: &BrowserKind,
logical_port: u16,
result: LaunchResult,
) -> Result<BrowserControlLaunchResponse, String> {
match result {
LaunchResult::UserProfileReady { endpoint } => {
let connection = CdpClient::connect_user_profile_browser(
logical_port,
endpoint.port,
kind,
&endpoint.web_socket_url,
)
.await;
if let Err(error) = connection {
return Ok(BrowserControlLaunchResponse {
success: false,
status: "user_profile_connection_failed".into(),
message: Some(error.to_string()),
browser_kind: kind.to_string(),
setup_url: None,
});
}
Ok(BrowserControlLaunchResponse {
success: true,
status: "connected_user_profile".into(),
message: None,
browser_kind: kind.to_string(),
setup_url: None,
})
}
other => Ok(to_launch_response(kind, other)),
}
}

/// Launch the user's default browser with CDP debug port.
#[tauri::command]
pub async fn browser_control_launch(
Expand All @@ -194,11 +298,64 @@ pub async fn browser_control_launch(
let port = request.port;
let kind = selected_browser_kind().await?;

if CdpClient::browser_connection_for_kind(port, &kind)
.await
.is_some()
{
return Ok(to_launch_response(&kind, LaunchResult::AlreadyConnected));
}

// The logical port is shared across browser choices. Drop only the lookup
// entry when the user switches browsers; any already-attached page session
// keeps its transport alive, but new actions cannot accidentally reuse it.
if CdpClient::browser_connection(port).await.is_some() {
CdpClient::remove_browser_connection(port).await;
}

let result = BrowserLauncher::launch_with_cdp(&kind, port)
.await
.map_err(|e| e.to_string())?;

Ok(to_launch_response(&kind, result))
complete_launch(&kind, port, result).await
}

/// Open the selected browser's persistent guarded-CDP setting and wait for the
/// user-owned consent toggle. Once enabled, immediately request and retain the
/// real-profile connection so the Settings action is one continuous flow.
#[tauri::command]
pub async fn browser_control_enable_default_cdp(
request: BrowserControlLaunchRequest,
) -> Result<BrowserControlLaunchResponse, String> {
let port = request.port;
let kind = selected_browser_kind().await?;

if !BrowserLauncher::supports_default_cdp(&kind) {
return Ok(BrowserControlLaunchResponse {
success: false,
status: "default_cdp_unsupported".into(),
message: Some(format!(
"{} does not expose a supported persistent guarded-CDP setting",
kind
)),
browser_kind: kind.to_string(),
setup_url: None,
});
}

if CdpClient::browser_connection_for_kind(port, &kind)
.await
.is_some()
{
return Ok(to_launch_response(&kind, LaunchResult::AlreadyConnected));
}
if CdpClient::browser_connection(port).await.is_some() {
CdpClient::remove_browser_connection(port).await;
}

let result = BrowserLauncher::enable_default_cdp(&kind, port)
.await
.map_err(|e| e.to_string())?;
complete_launch(&kind, port, result).await
}

/// Restart the user's default browser with CDP debug port enabled.
Expand All @@ -213,19 +370,5 @@ pub async fn browser_control_restart_with_cdp(
.await
.map_err(|e| e.to_string())?;

Ok(to_launch_response(&kind, result))
}

/// Create a macOS .app wrapper for the browser with CDP enabled.
#[tauri::command]
pub async fn browser_control_create_launcher() -> Result<String, String> {
#[cfg(target_os = "macos")]
{
let kind = selected_browser_kind().await?;
BrowserLauncher::create_cdp_launcher_app(&kind, DEFAULT_CDP_PORT).map_err(|e| e.to_string())
}
#[cfg(not(target_os = "macos"))]
{
Err("CDP launcher app creation is only supported on macOS".into())
}
complete_launch(&kind, port, result).await
}
2 changes: 1 addition & 1 deletion src/apps/desktop/src/api/remote_workspace_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ pub const REMOTE_WORKSPACE_COMMAND_POLICIES: &[(&str, RemoteWorkspacePolicy)] =
),
("archive_session", RemoteWorkspacePolicy::LegacyUnaudited),
(
"browser_control_create_launcher",
"browser_control_enable_default_cdp",
RemoteWorkspacePolicy::LocalOnly,
),
(
Expand Down
2 changes: 1 addition & 1 deletion src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1803,8 +1803,8 @@ pub async fn run() {
api::browser_control_api::browser_control_list_browsers,
api::browser_control_api::browser_control_get_status,
api::browser_control_api::browser_control_launch,
api::browser_control_api::browser_control_enable_default_cdp,
api::browser_control_api::browser_control_restart_with_cdp,
api::browser_control_api::browser_control_create_launcher,
// Insights API
api::insights_api::generate_insights,
api::insights_api::get_latest_insights,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ The user will primarily request you perform software engineering tasks. This inc
- When the user explicitly asks to complete work and review it carefully, finish the implementation first, then dispatch one independent read-only `CodeReview` Task. Do not run concurrent review tasks or fan out `CodeReview` into architecture, performance, security, product, or other invented dimensions: broader coverage belongs to the unified `/review` path, which selects bounded review lenses and owns cost confirmation. Do not launch review by default for every task.
- Treat reviewer output as adversarial evidence. The reviewer never fixes its own findings. Apply accepted fixes in the implementation agent. If substantive fixes make the original verdict stale and the risk warrants another pass, request at most one fresh independent re-review.
- When WebFetch reports a redirect, follow the redirect URL if it is relevant and safe for the user's request.
- For browser and web-page work, route in this order: (0) only opening or showing a URL for the user, with no page reading or interaction: use `ControlHub` with `domain: "browser"`, `action: "open_builtin"`; (1) reading page content that does not require the user's login state: use WebFetch; (2) pages that require the user's login state or JavaScript interaction: use `ControlHub` with `domain: "browser"` (connect, snapshot, then act through `@eN` refs) — `connect` drives BitFun's managed browser profile, which is separate from the user's everyday browser; it persists cookies and logins across runs, so if the page shows a login wall, ask the user to sign in once in that window instead of retrying navigation or entering credentials yourself; (3) non-Chromium browsers (Firefox/Safari) or native desktop apps, including Electron apps: use `ComputerUse` desktop actions only when `ComputerUse` appears in your current tool list; if it does not, tell the user the task needs the Computer Use mode (enabled via the Computer use setting) instead of guessing another path or calling an unavailable tool. `ControlHub` covers ordinary web pages. For a browser-only workflow that `ControlHub` explicitly cannot support, such as a compatible cloud-browser workflow, load `agent-browser` via `Skill(skill="agent-browser")` only when that skill is available; do not use it as a substitute for `ComputerUse` on native desktop apps.
- For browser and web-page work, route in this order: (0) only opening or showing a URL for the user, with no page reading or interaction: use `ControlHub` with `domain: "browser"`, `action: "open_builtin"`; (1) reading page content that does not require the user's login state: use WebFetch; (2) pages that require the user's login state or JavaScript interaction: use `ControlHub` with `domain: "browser"` (connect, snapshot, then act through `@eN` refs) — Chrome 144+ and Edge request access to the currently running real profile, preserving tabs and login state after the user clicks **Enable default CDP** in BitFun Settings > Browser control, enables Remote debugging in the browser-owned page, and approves BitFun; other supported Chromium browsers reuse a real-profile endpoint when available and otherwise use BitFun's persistent managed profile; (3) non-Chromium browsers (Firefox/Safari) or native desktop apps, including Electron apps: use `ComputerUse` desktop actions only when `ComputerUse` appears in your current tool list; if it does not, tell the user the task needs the Computer Use mode (enabled via the Computer use setting) instead of guessing another path or calling an unavailable tool. `ControlHub` covers ordinary web pages. For a browser-only workflow that `ControlHub` explicitly cannot support, such as a compatible cloud-browser workflow, load `agent-browser` via `Skill(skill="agent-browser")` only when that skill is available; do not use it as a substitute for `ComputerUse` on native desktop apps.
- When multiple tool calls are independent, run them in parallel. Keep dependent operations sequential, and never use placeholders or guess missing parameters.
- Use specialized tools for file reads, edits, searches, and deletions because they preserve workspace context and permissions. Use ExecCommand for commands that genuinely need a shell. Do not use shell commands only to communicate with the user.
- For security-sensitive tasks, support defensive analysis and remediation only. Refuse malicious code, exploit workflows, credential harvesting, or instructions that would facilitate abuse.
Expand Down
4 changes: 2 additions & 2 deletions src/crates/assembly/agent-content/prompts/agents/claw_mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ When a first-class tool exists for an action, use the tool directly instead of a

Use `ControlHub` for browser automation, terminal signalling, and routing/capability introspection only when it appears in your current tool list:

- `domain: "browser"` for websites and web apps in BitFun's managed browser profile through CDP.
- `domain: "browser"` for websites and web apps through CDP. Chrome 144+ and Edge connect to the user's current profile after explicit approval; other Chromium browsers reuse a real-profile endpoint when available or use BitFun's persistent managed profile.
- `domain: "terminal"` for signalling existing terminal sessions, such as interrupting or killing them.
- `domain: "meta"` for capability and route checks.

For browser and web-page work, route in this order:

1. Only opening, showing, previewing, or displaying a URL for the user (no page reading, no interaction): use `ControlHub` with `domain: "browser"`, `action: "open_builtin"`, `params: { url }`. The page renders in BitFun's built-in right-side browser panel. Do not delegate this to a `ComputerUse` sub-agent and do not call `connect`/`navigate` for it.
2. Reading page content that does not require the user's login state: use `WebFetch`.
3. Pages that require the user's login state or JavaScript interaction: use `ControlHub` with `domain: "browser"` (connect, snapshot, then act through `@eN` refs). `connect` drives BitFun's managed browser profile, which is separate from the user's everyday browser; it persists cookies and logins across runs, so if the page shows a login wall, ask the user to sign in once in that window instead of retrying navigation or entering credentials yourself.
3. Pages that require the user's login state or JavaScript interaction: use `ControlHub` with `domain: "browser"` (connect, snapshot, then act through `@eN` refs). On Chrome 144+ and Edge, `connect` requests access to the currently running real profile; for one-time setup, ask the user to click **Enable default CDP** in BitFun Settings > Browser control, enable Remote debugging in the browser-owned page, and approve BitFun. Other supported Chromium browsers reuse a real-profile endpoint when available and otherwise use BitFun's persistent managed profile.
4. Non-Chromium browsers (Firefox/Safari) or native desktop apps: delegate to the `ComputerUse` sub-agent as described below.

Do not use `ControlHub` for local computer, operating-system, or desktop UI work. Desktop and system actions have moved to the dedicated `ComputerUse` tool/agent. This includes screenshots, OCR, mouse, keyboard, app state, app launching, opening local files and non-http(s) URLs through the OS, clipboard access, OS facts, and local scripts.
Expand Down
Loading
Loading