use agentchrome::connection::ManagedSession;
use agentchrome::coords::BoundingBox;
use agentchrome::error::{AppError, ExitCode};
pub(crate) async fn frame_viewport_offset(
managed: &ManagedSession,
frame_ctx: &agentchrome::frame::FrameContext,
) -> Result<(f64, f64), AppError> {
let Some(frame_id) = agentchrome::frame::frame_id(frame_ctx) else {
return Ok((0.0, 0.0)); };
let owner = managed
.send_command(
"DOM.getFrameOwner",
Some(serde_json::json!({ "frameId": frame_id })),
)
.await?;
let backend_node_id = owner["backendNodeId"].as_i64().unwrap_or(0);
if backend_node_id == 0 {
return Ok((0.0, 0.0));
}
let box_model = managed
.send_command(
"DOM.getBoxModel",
Some(serde_json::json!({ "backendNodeId": backend_node_id })),
)
.await?;
let content = box_model["model"]["content"].as_array();
let (frame_x, frame_y) = if let Some(c) = content {
let x = c.first().and_then(serde_json::Value::as_f64).unwrap_or(0.0);
let y = c.get(1).and_then(serde_json::Value::as_f64).unwrap_or(0.0);
(x, y)
} else {
(0.0, 0.0)
};
Ok((frame_x, frame_y))
}
pub(crate) async fn resolve_element_box(
managed: &ManagedSession,
frame_ctx: Option<&agentchrome::frame::FrameContext>,
selector: &str,
) -> Result<BoundingBox, AppError> {
if let Some(ctx) = frame_ctx
&& is_element_css_selector(selector)
&& let Some(exec_ctx_id) = agentchrome::frame::execution_context_id(ctx)
{
let css = &selector[4..];
let object_id = query_selector_in_execution_context(managed, exec_ctx_id, css)
.await?
.ok_or_else(|| AppError::css_selector_not_found(css))?;
let page_global =
fetch_bounding_box_inner(managed, serde_json::json!({ "objectId": object_id })).await?;
let (off_x, off_y) = frame_viewport_offset(managed, ctx).await?;
return Ok(BoundingBox {
x: page_global.x - off_x,
y: page_global.y - off_y,
width: page_global.width,
height: page_global.height,
});
}
let backend_node_id = resolve_backend_node_id(managed, frame_ctx, selector).await?;
fetch_bounding_box(managed, backend_node_id).await
}
async fn query_selector_in_execution_context(
managed: &ManagedSession,
execution_context_id: i64,
css: &str,
) -> Result<Option<String>, AppError> {
let json_quoted = serde_json::to_string(css).map_err(|e| AppError {
message: format!("Failed to encode selector: {e}"),
code: ExitCode::GeneralError,
custom_json: None,
})?;
let expression = format!("document.querySelector({json_quoted})");
let result = managed
.send_command(
"Runtime.evaluate",
Some(serde_json::json!({
"expression": expression,
"contextId": execution_context_id,
"returnByValue": false,
})),
)
.await?;
if result["result"]["subtype"].as_str() == Some("null") {
return Ok(None);
}
Ok(result["result"]["objectId"]
.as_str()
.map(std::string::ToString::to_string))
}
fn is_element_uid(target: &str) -> bool {
if !target.starts_with('s') {
return false;
}
let rest = &target[1..];
!rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit())
}
fn is_element_css_selector(target: &str) -> bool {
target.starts_with("css:")
}
async fn resolve_backend_node_id(
managed: &ManagedSession,
frame_ctx: Option<&agentchrome::frame::FrameContext>,
target: &str,
) -> Result<i64, AppError> {
if is_element_uid(target) {
let state = crate::snapshot::read_snapshot_state()
.map_err(|e| AppError {
message: format!("Failed to read snapshot state: {e}"),
code: ExitCode::GeneralError,
custom_json: None,
})?
.ok_or_else(AppError::no_snapshot_state)?;
let backend_node_id = state
.uid_map
.get(target)
.copied()
.ok_or_else(|| AppError::element_target_not_found(target))?;
Ok(backend_node_id)
} else if is_element_css_selector(target) {
let selector = &target[4..];
let effective = if let Some(ctx) = frame_ctx {
agentchrome::frame::frame_session(ctx, managed)
} else {
managed
};
let doc_response = effective.send_command("DOM.getDocument", None).await?;
let root_node_id = doc_response["root"]["nodeId"]
.as_i64()
.ok_or_else(|| AppError::css_selector_not_found(selector))?;
let query_params = serde_json::json!({
"nodeId": root_node_id,
"selector": selector,
});
let query_response = effective
.send_command("DOM.querySelector", Some(query_params))
.await?;
let node_id = query_response["nodeId"].as_i64().unwrap_or(0);
if node_id == 0 {
return Err(AppError::css_selector_not_found(selector));
}
let describe_params = serde_json::json!({ "nodeId": node_id });
let describe_response = effective
.send_command("DOM.describeNode", Some(describe_params))
.await?;
let backend_node_id = describe_response["node"]["backendNodeId"]
.as_i64()
.ok_or_else(|| AppError::css_selector_not_found(selector))?;
Ok(backend_node_id)
} else {
Err(AppError::element_target_not_found(target))
}
}
async fn fetch_bounding_box(
managed: &ManagedSession,
backend_node_id: i64,
) -> Result<BoundingBox, AppError> {
let params = serde_json::json!({ "backendNodeId": backend_node_id });
fetch_bounding_box_inner(managed, params).await
}
async fn fetch_bounding_box_inner(
managed: &ManagedSession,
params: serde_json::Value,
) -> Result<BoundingBox, AppError> {
let box_result = managed
.send_command("DOM.getBoxModel", Some(params))
.await?;
let border = box_result["model"]["border"].as_array();
let (x, y, width, height) = match border {
Some(c) if c.len() >= 8 => {
let x1 = c[0].as_f64().unwrap_or(0.0);
let y1 = c[1].as_f64().unwrap_or(0.0);
let x3 = c[4].as_f64().unwrap_or(0.0);
let y3 = c[5].as_f64().unwrap_or(0.0);
(x1, y1, x3 - x1, y3 - y1)
}
_ => (0.0, 0.0, 0.0, 0.0),
};
Ok(BoundingBox {
x,
y,
width,
height,
})
}