feat: add console observation command

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
BB-fat
2026-07-07 11:08:12 +08:00
co-authored by Cursor
parent ce29ddaa53
commit 7f5093d97a
23 changed files with 1538 additions and 44 deletions
@@ -82,6 +82,10 @@ fn main() {
dump!(GetHtmlResult, "tool_get_html_result");
dump!(ScreenshotParams, "tool_screenshot_params");
dump!(ScreenshotResult, "tool_screenshot_result");
dump!(ConsoleParams, "tool_console_params");
dump!(ConsoleResult, "tool_console_result");
dump!(ConsoleEntry, "tool_console_entry");
dump!(ConsoleStackFrame, "tool_console_stack_frame");
dump!(EvaluateParams, "tool_evaluate_params");
dump!(EvaluateResult, "tool_evaluate_result");
+11
View File
@@ -62,6 +62,8 @@ pub enum Method {
ToolGetHtml,
#[serde(rename = "tool.screenshot")]
ToolScreenshot,
#[serde(rename = "tool.console")]
ToolConsole,
#[serde(rename = "tool.evaluate")]
ToolEvaluate,
#[serde(rename = "tool.wait_for_navigation")]
@@ -124,6 +126,7 @@ impl Method {
| Method::ToolSnapshot
| Method::ToolGetHtml
| Method::ToolScreenshot
| Method::ToolConsole
| Method::ToolWaitForNavigation
| Method::ToolWaitMs
| Method::ToolRequestHelp => false,
@@ -159,6 +162,13 @@ mod tests {
assert_eq!(serde_json::to_value(method).unwrap(), json!("cancel"));
}
#[test]
fn console_method_round_trips() {
let method: Method = serde_json::from_value(json!("tool.console")).unwrap();
assert_eq!(method, Method::ToolConsole);
assert_eq!(serde_json::to_value(method).unwrap(), json!("tool.console"));
}
#[test]
fn cancel_params_and_result_round_trip() {
let params: CancelParams = serde_json::from_value(json!({ "rpc_id": "wait-1" })).unwrap();
@@ -176,6 +186,7 @@ mod tests {
assert!(!Method::ToolSnapshot.is_mutating());
assert!(!Method::ToolGetHtml.is_mutating());
assert!(!Method::ToolScreenshot.is_mutating());
assert!(!Method::ToolConsole.is_mutating());
assert!(!Method::ToolWaitForNavigation.is_mutating());
assert!(!Method::ToolWaitMs.is_mutating());
}
+147
View File
@@ -0,0 +1,147 @@
//! `tool.console` — read buffered console/log/exception messages.
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ConsoleParams {
pub session_id: String,
/// Optional target tab. Defaults to the Agent Window's currently active tab.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tab_id: Option<i64>,
/// Return entries with sequence strictly greater than this cursor.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub since: Option<u64>,
/// Maximum number of entries to return. Extension applies safe defaults and caps.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 1))]
pub limit: Option<u32>,
/// Maximum characters returned per entry text. Extension applies safe defaults and caps.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 1))]
pub max_text_chars: Option<u32>,
/// Include structured stack frames. Defaults to false.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub include_stack: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum ConsoleEntryKind {
Console,
Exception,
Log,
}
impl ConsoleEntryKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Console => "console",
Self::Exception => "exception",
Self::Log => "log",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ConsoleStackFrame {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub function_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub line: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub column: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ConsoleEntry {
pub sequence: u64,
pub kind: ConsoleEntryKind,
pub level: String,
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub line: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub column: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp: Option<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub stack_trace: Vec<ConsoleStackFrame>,
#[serde(default)]
pub truncated: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ConsoleResult {
pub tab_id: i64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub entries: Vec<ConsoleEntry>,
pub next_since: u64,
#[serde(default)]
pub truncated: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn console_params_omit_optional_fields() {
let params = ConsoleParams {
session_id: "aa11".into(),
tab_id: None,
since: None,
limit: None,
max_text_chars: None,
include_stack: None,
};
let value = serde_json::to_value(&params).unwrap();
assert_eq!(value["session_id"], "aa11");
assert!(value.get("tab_id").is_none());
assert!(value.get("since").is_none());
assert!(value.get("limit").is_none());
assert!(value.get("max_text_chars").is_none());
assert!(value.get("include_stack").is_none());
let round: ConsoleParams = serde_json::from_value(value).unwrap();
assert_eq!(round, params);
}
#[test]
fn console_result_round_trips_with_stack_and_truncation() {
let result = ConsoleResult {
tab_id: 7,
entries: vec![ConsoleEntry {
sequence: 12,
kind: ConsoleEntryKind::Exception,
level: "error".into(),
text: "Uncaught TypeError: x is undefined".into(),
url: Some("https://example.test/app.js".into()),
line: Some(42),
column: Some(7),
timestamp: Some(1234.5),
stack_trace: vec![ConsoleStackFrame {
function_name: Some("render".into()),
url: Some("https://example.test/app.js".into()),
line: Some(42),
column: Some(7),
}],
truncated: true,
}],
next_since: 12,
truncated: true,
};
let value = serde_json::to_value(&result).unwrap();
assert_eq!(value["entries"][0]["kind"], json!("exception"));
assert_eq!(
value["entries"][0]["stack_trace"][0]["function_name"],
json!("render")
);
let round: ConsoleResult = serde_json::from_value(value).unwrap();
assert_eq!(round, result);
}
}
+2
View File
@@ -1,5 +1,6 @@
//! Typed params/results for the 21 `tool.*` RPC methods (§7).
pub mod console;
pub mod dialog;
pub mod human_loop;
pub mod interaction;
@@ -10,6 +11,7 @@ pub mod session;
pub mod tabs;
pub mod waits;
pub use console::*;
pub use dialog::*;
pub use human_loop::*;
pub use interaction::*;