use crate::{cursor::proto::agent::v1 as pb, model::ToolCall, Error, Result}; pub(super) fn output( message: &pb::exec_client_message::Message, call: &ToolCall, ) -> Result<(String, bool)> { use pb::exec_client_message::Message; match message { Message::ShellResult(value) | Message::MiniSweAgentBashResult(value) => shell(value), Message::ReadResult(value) | Message::RedactedReadResult(value) => read(value), Message::WriteResult(value) => write(value), Message::DeleteResult(value) => delete(value), Message::GrepResult(value) => grep(value), Message::DiagnosticsResult(value) => diagnostics(value), Message::McpResult(value) => mcp(value), Message::ReadMcpResourceExecResult(value) => read_mcp(value), Message::SubagentResult(value) => task(value, call), _ => Err(Error::Protocol( "unsupported terminal ExecClientMessage".into(), )), } } fn shell(value: &pb::ShellResult) -> Result<(String, bool)> { use pb::shell_result::Result as R; let output = match value.result.as_ref().ok_or_else(|| missing("shell"))? { R::Success(success) if value.is_background == Some(true) => { let mut fields = vec![format!("shell_id={}", success.shell_id.unwrap_or_default())]; if let Some(pid) = success.pid.or(value.pid) { fields.push(format!("pid={pid}")); } if let Some(folder) = value.terminals_folder.as_deref().filter(|v| !v.is_empty()) { fields.push(format!("terminals_folder={folder}")); } let output = streams(&success.stdout, &success.stderr); let prefix = format!("shell running in background {}", fields.join(" ")); return Ok(( if output == "shell completed without output" { prefix } else { format!("{prefix}\n{output}") }, false, )); } R::Success(success) => return Ok((streams(&success.stdout, &success.stderr), false)), R::Failure(failure) => streams(&failure.stdout, &failure.stderr), R::Timeout(timeout) => format!( "shell timed out after {}ms in {}", timeout.timeout_ms, timeout.working_directory ), R::Rejected(rejected) => rejected.reason.clone(), R::SpawnError(error) => error.error.clone(), R::PermissionDenied(denied) => denied.error.clone(), }; Ok((output, true)) } fn streams(stdout: &str, stderr: &str) -> String { match (stdout.is_empty(), stderr.is_empty()) { (false, false) => format!("{stdout}\n\n\n{stderr}\n"), (false, true) => stdout.into(), (true, false) => stderr.into(), (true, true) => "shell completed without output".into(), } } fn read(value: &pb::ReadResult) -> Result<(String, bool)> { use pb::{read_result::Result as R, read_success::Output}; match value.result.as_ref().ok_or_else(|| missing("read"))? { R::Success(success) => Ok(( match success.output.as_ref() { Some(Output::Content(text)) => text.clone(), Some(Output::Data(bytes)) => format!("read binary bytes={}", bytes.len()), None => format!("read success path={}", success.path), }, false, )), R::Error(error) => Ok((error.error.clone(), true)), R::Rejected(rejected) => Ok((rejected.reason.clone(), true)), R::FileNotFound(value) => Ok((format!("file not found: {}", value.path), true)), R::PermissionDenied(value) => Ok((format!("permission denied: {}", value.path), true)), R::InvalidFile(value) => Ok((value.reason.clone(), true)), } } fn write(value: &pb::WriteResult) -> Result<(String, bool)> { use pb::write_result::Result as R; match value.result.as_ref().ok_or_else(|| missing("write"))? { R::Success(success) => Ok(( success.file_content_after_write.clone().unwrap_or_else(|| { format!( "write success path={} lines={}", success.path, success.lines_created ) }), false, )), R::PermissionDenied(value) => Ok((value.error.clone(), true)), R::NoSpace(value) => Ok((format!("no space left: {}", value.path), true)), R::Error(value) => Ok((value.error.clone(), true)), R::Rejected(value) => Ok((value.reason.clone(), true)), } } fn delete(value: &pb::DeleteResult) -> Result<(String, bool)> { use pb::delete_result::Result as R; match value.result.as_ref().ok_or_else(|| missing("delete"))? { R::Success(value) => Ok((format!("delete success path={}", value.path), false)), R::FileNotFound(value) => Ok((format!("file not found: {}", value.path), true)), R::NotFile(value) => Ok((format!("not file: {}", value.path), true)), R::PermissionDenied(value) => Ok((value.client_visible_error.clone(), true)), R::FileBusy(value) => Ok((format!("file busy: {}", value.path), true)), R::Rejected(value) => Ok((value.reason.clone(), true)), R::Error(value) => Ok((value.error.clone(), true)), } } fn grep(value: &pb::GrepResult) -> Result<(String, bool)> { use pb::grep_result::Result as R; match value.result.as_ref().ok_or_else(|| missing("grep"))? { R::Success(value) => Ok((grep_success(value), false)), R::Error(value) => Ok((value.error.clone(), true)), } } fn grep_success(value: &pb::GrepSuccess) -> String { let mut lines = Vec::new(); if let Some(result) = &value.active_editor_result { grep_union(result, &mut lines); } let mut workspaces = value.workspace_results.iter().collect::>(); workspaces.sort_unstable_by_key(|(name, _)| *name); for (_, result) in workspaces { grep_union(result, &mut lines); } if lines.is_empty() { format!( "No matches found for pattern `{}` in {}", value.pattern, value.path ) } else { lines.join("\n") } } fn grep_union(value: &pb::GrepUnionResult, lines: &mut Vec) { use pb::grep_union_result::Result as R; match value.result.as_ref() { Some(R::Files(value)) => { lines.extend(value.files.iter().cloned()); grep_truncation( value.client_truncated, value.ripgrep_truncated, value.total_files, "files", lines, ); } Some(R::Count(value)) => { lines.extend( value .counts .iter() .map(|count| format!("{}:{}", count.file, count.count)), ); grep_truncation( value.client_truncated, value.ripgrep_truncated, value.total_matches, "matches", lines, ); } Some(R::Content(value)) => { for file in &value.matches { lines.extend(file.matches.iter().map(|matched| { let separator = if matched.is_context_line { '-' } else { ':' }; let truncated = if matched.content_truncated { " [line truncated]" } else { "" }; format!( "{}{separator}{}{separator}{}{truncated}", file.file, matched.line_number, matched.content ) })); } grep_truncation( value.client_truncated, value.ripgrep_truncated, value.total_matched_lines, "matched lines", lines, ); } None => {} } } fn grep_truncation( client_truncated: bool, ripgrep_truncated: bool, total: i32, unit: &str, lines: &mut Vec, ) { if client_truncated || ripgrep_truncated { lines.push(format!("[Results truncated; {total} total {unit}]")); } } fn diagnostics(value: &pb::DiagnosticsResult) -> Result<(String, bool)> { use pb::diagnostics_result::Result as R; match value .result .as_ref() .ok_or_else(|| missing("diagnostics"))? { R::Success(value) => Ok((diagnostics_success(value), false)), R::Error(value) => Ok((value.error.clone(), true)), R::Rejected(value) => Ok((value.reason.clone(), true)), R::FileNotFound(value) => Ok((format!("file not found: {}", value.path), true)), R::PermissionDenied(value) => Ok((format!("permission denied: {}", value.path), true)), } } fn diagnostics_success(value: &pb::DiagnosticsSuccess) -> String { if value.diagnostics.is_empty() { return format!("No diagnostics found in {}", value.path); } let mut lines = value .diagnostics .iter() .map(|diagnostic| { let location = diagnostic_location(&value.path, diagnostic.range.as_ref()); let mut labels = vec![diagnostic_severity(diagnostic.severity)]; if !diagnostic.source.is_empty() { labels.push(diagnostic.source.as_str()); } if !diagnostic.code.is_empty() { labels.push(diagnostic.code.as_str()); } if diagnostic.is_stale { labels.push("stale"); } format!( "{}: [{}] {}", location, labels.join(" "), diagnostic.message ) }) .collect::>(); if value.total_diagnostics != value.diagnostics.len() as i32 { lines.push(format!( "[Reported {} diagnostics; received {} details]", value.total_diagnostics, value.diagnostics.len() )); } lines.join("\n") } fn diagnostic_location(path: &str, range: Option<&pb::Range>) -> String { let Some(range) = range else { return path.into(); }; let Some(start) = &range.start else { return path.into(); }; let mut location = format!( "{}:{}:{}", path, start.line.saturating_add(1), start.column.saturating_add(1) ); if let Some(end) = &range.end { location.push_str(&format!( "-{}:{}", end.line.saturating_add(1), end.column.saturating_add(1) )); } location } fn diagnostic_severity(value: i32) -> &'static str { match pb::DiagnosticSeverity::try_from(value) { Ok(pb::DiagnosticSeverity::Error) => "error", Ok(pb::DiagnosticSeverity::Warning) => "warning", Ok(pb::DiagnosticSeverity::Information) => "information", Ok(pb::DiagnosticSeverity::Hint) => "hint", Ok(pb::DiagnosticSeverity::Unspecified) | Err(_) => "diagnostic", } } fn mcp(value: &pb::McpResult) -> Result<(String, bool)> { use pb::mcp_result::Result as R; match value.result.as_ref().ok_or_else(|| missing("mcp"))? { R::Success(value) => Ok((mcp_content(value)?, value.is_error)), R::Error(value) => Ok((value.error.clone(), true)), R::Rejected(value) => Ok((value.reason.clone(), true)), R::PermissionDenied(value) => Ok((value.error.clone(), true)), R::ToolNotFound(value) => Ok((format!("MCP tool not found: {}", value.name), true)), R::ServerNotFound(value) => Ok((format!("MCP server not found: {}", value.name), true)), R::Approved(_) => Err(Error::Protocol("MCP approval is not terminal".into())), } } fn mcp_content(success: &pb::McpSuccess) -> Result { let mut content = Vec::new(); for item in &success.content { match item.content.as_ref() { Some(pb::mcp_tool_result_content_item::Content::Text(text)) => { if !text.text.is_empty() { content.push(text.text.clone()); } if let Some(location) = &text.output_location { content.push(format!( "MCP output file: {} ({} bytes, {} lines)", location.file_path, location.size_bytes, location.line_count )); } } Some(pb::mcp_tool_result_content_item::Content::Image(image)) => content.push(format!( "MCP image: {} ({} bytes)", image.mime_type, image.data.len() )), None => {} } } if let Some(structured) = &success.structured_content { let value = serde_json::Value::Object( structured .fields .iter() .map(|(key, value)| (key.clone(), super::super::prost_json(value))) .collect(), ); content.push(serde_json::to_string_pretty(&value)?); } Ok(if content.is_empty() { "MCP tool completed without content".into() } else { content.join("\n\n") }) } fn read_mcp(value: &pb::ReadMcpResourceExecResult) -> Result<(String, bool)> { use pb::read_mcp_resource_exec_result::Result as R; match value .result .as_ref() .ok_or_else(|| missing("read MCP resource"))? { R::Success(value) => Ok(( match value.content.as_ref() { Some(pb::read_mcp_resource_success::Content::Text(text)) => text.clone(), Some(pb::read_mcp_resource_success::Content::Blob(blob)) => { format!("read MCP resource blob={}", blob.len()) } None => format!("read MCP resource uri={}", value.uri), }, false, )), R::Error(value) => Ok((value.error.clone(), true)), R::Rejected(value) => Ok((value.reason.clone(), true)), R::NotFound(value) => Ok((format!("MCP resource not found: {}", value.uri), true)), } } fn task(value: &pb::SubagentResult, call: &ToolCall) -> Result<(String, bool)> { use pb::subagent_result::Result as R; match value.result.as_ref().ok_or_else(|| missing("subagent"))? { R::Success(value) if creates_subagent(call) => { let name = call .arguments .get("description") .and_then(serde_json::Value::as_str) .filter(|name| !name.is_empty()) .ok_or_else(|| Error::Protocol("Task call is missing description".into()))?; if value.agent_id.is_empty() { return Err(Error::Protocol("Task result is missing agent_id".into())); } let identity = format!("Subagent name: {name}\nSubagent ID: {}", value.agent_id); let content = value .final_message .as_deref() .filter(|message| !message.is_empty()) .map_or(identity.clone(), |message| { format!("{identity}\n\n{message}") }); Ok((content, false)) } R::Success(value) => Ok((value.final_message.clone().unwrap_or_default(), false)), R::Error(value) => Ok((value.error.clone(), true)), } } fn creates_subagent(call: &ToolCall) -> bool { matches!( call.arguments .get("resume") .and_then(serde_json::Value::as_str), None | Some("self") ) } #[cfg(test)] mod tests { use std::collections::HashMap; use super::*; #[test] fn grep_output_contains_file_and_match_details() { let value = pb::GrepResult { result: Some(pb::grep_result::Result::Success(pb::GrepSuccess { pattern: "Cursor".into(), path: "/workspace".into(), output_mode: "content".into(), workspace_results: HashMap::from([ ( "workspace-b".into(), pb::GrepUnionResult { result: Some(pb::grep_union_result::Result::Files( pb::GrepFilesResult { files: vec!["/workspace/Cargo.toml".into()], total_files: 1, ..Default::default() }, )), }, ), ( "workspace-a".into(), pb::GrepUnionResult { result: Some(pb::grep_union_result::Result::Content( pb::GrepContentResult { matches: vec![pb::GrepFileMatch { file: "/workspace/README.md".into(), matches: vec![pb::GrepContentMatch { line_number: 7, content: "Cursor BYOK".into(), ..Default::default() }], }], total_lines: 1, total_matched_lines: 1, ..Default::default() }, )), }, ), ]), active_editor_result: None, })), }; let (content, is_error) = grep(&value).unwrap(); assert!(!is_error); assert!(content.contains("/workspace/README.md:7:Cursor BYOK")); assert!(content.contains("/workspace/Cargo.toml")); assert!( content.find("/workspace/README.md").unwrap() < content.find("/workspace/Cargo.toml").unwrap(), "workspace map output must be deterministic" ); } #[test] fn diagnostics_output_contains_each_diagnostic_detail() { let value = pb::DiagnosticsResult { result: Some(pb::diagnostics_result::Result::Success( pb::DiagnosticsSuccess { path: "/workspace/src/main.rs".into(), diagnostics: vec![pb::Diagnostic { severity: pb::DiagnosticSeverity::Error as i32, range: Some(pb::Range { start: Some(pb::Position { line: 4, column: 8 }), end: Some(pb::Position { line: 4, column: 12, }), }), message: "cannot find value `name`".into(), source: "rustc".into(), code: "E0425".into(), is_stale: false, }], total_diagnostics: 1, }, )), }; let (content, is_error) = diagnostics(&value).unwrap(); assert!(!is_error); assert!(content.contains("/workspace/src/main.rs:5:9")); assert!(content.contains("-5:13")); assert!(content.contains("error")); assert!(content.contains("rustc")); assert!(content.contains("E0425")); assert!(content.contains("cannot find value `name`")); } } fn missing(name: &str) -> Error { Error::Protocol(format!("{name} returned no result")) }