cursor: parse thinking messages in the JSON logs (#1091)

* cursor: parse thinking messages in the JSON logs

* cursor: update log parsing

- update editTool and deleteTool
- add semSearch tool
This commit is contained in:
Solomon
2025-10-23 18:12:11 +01:00
committed by GitHub
parent 39d172129b
commit 30cfb07d3c

View File

@@ -1,5 +1,5 @@
use core::str;
use std::{path::Path, process::Stdio, sync::Arc, time::Duration};
use std::{collections::HashMap, path::Path, process::Stdio, sync::Arc, time::Duration};
use async_trait::async_trait;
use command_group::AsyncCommandGroup;
@@ -155,6 +155,8 @@ impl StandardCodingAgentExecutor for Cursor {
let mut current_assistant_message_buffer = String::new();
let mut current_assistant_message_index: Option<usize> = None;
let mut current_thinking_message_buffer = String::new();
let mut current_thinking_message_index: Option<usize> = None;
let worktree_str = current_dir.to_string_lossy().to_string();
@@ -195,11 +197,16 @@ impl StandardCodingAgentExecutor for Cursor {
}
let is_assistant_message = matches!(cursor_json, CursorJson::Assistant { .. });
let is_thinking_message = matches!(cursor_json, CursorJson::Thinking { .. });
if !is_assistant_message && current_assistant_message_index.is_some() {
// flush
current_assistant_message_index = None;
current_assistant_message_buffer.clear();
}
if !is_thinking_message && current_thinking_message_index.is_some() {
current_thinking_message_index = None;
current_thinking_message_buffer.clear();
}
match &cursor_json {
CursorJson::System { model, .. } => {
@@ -240,6 +247,27 @@ impl StandardCodingAgentExecutor for Cursor {
};
}
}
CursorJson::Thinking { text, .. } => {
if let Some(chunk) = text
&& !chunk.is_empty()
{
current_thinking_message_buffer.push_str(chunk);
let entry = NormalizedEntry {
timestamp: None,
entry_type: NormalizedEntryType::Thinking,
content: current_thinking_message_buffer.clone(),
metadata: None,
};
if let Some(id) = current_thinking_message_index {
msg_store.push_patch(ConversationPatch::replace(id, entry));
} else {
let id = entry_index_provider.next();
current_thinking_message_index = Some(id);
msg_store
.push_patch(ConversationPatch::add_normalized_entry(id, entry));
}
}
}
CursorJson::ToolCall {
subtype,
@@ -486,6 +514,15 @@ pub enum CursorJson {
#[serde(default)]
session_id: Option<String>,
},
#[serde(rename = "thinking")]
Thinking {
#[serde(default)]
subtype: Option<String>,
#[serde(default)]
text: Option<String>,
#[serde(default)]
session_id: Option<String>,
},
#[serde(rename = "tool_call")]
ToolCall {
#[serde(default)]
@@ -517,6 +554,7 @@ impl CursorJson {
CursorJson::System { session_id, .. } => session_id.clone(),
CursorJson::User { session_id, .. } => session_id.clone(),
CursorJson::Assistant { session_id, .. } => session_id.clone(),
CursorJson::Thinking { session_id, .. } => session_id.clone(),
CursorJson::ToolCall { session_id, .. } => session_id.clone(),
CursorJson::Result { .. } => None,
CursorJson::Unknown => None,
@@ -577,6 +615,12 @@ pub enum CursorToolCall {
#[serde(default)]
result: Option<serde_json::Value>,
},
#[serde(rename = "semSearchToolCall")]
SemSearch {
args: CursorSemSearchArgs,
#[serde(default)]
result: Option<serde_json::Value>,
},
#[serde(rename = "writeToolCall")]
Write {
args: CursorWriteArgs,
@@ -593,7 +637,7 @@ pub enum CursorToolCall {
Edit {
args: CursorEditArgs,
#[serde(default)]
result: Option<serde_json::Value>,
result: Option<CursorEditResult>,
},
#[serde(rename = "deleteToolCall")]
Delete {
@@ -628,6 +672,7 @@ impl CursorToolCall {
CursorToolCall::LS { .. } => "ls",
CursorToolCall::Glob { .. } => "glob",
CursorToolCall::Grep { .. } => "grep",
CursorToolCall::SemSearch { .. } => "semsearch",
CursorToolCall::Write { .. } => "write",
CursorToolCall::Read { .. } => "read",
CursorToolCall::Edit { .. } => "edit",
@@ -659,7 +704,7 @@ impl CursorToolCall {
format!("`{path}`"),
)
}
CursorToolCall::Edit { args, .. } => {
CursorToolCall::Edit { args, result, .. } => {
let path = make_path_relative(&args.path, worktree_path);
let mut changes = vec![];
@@ -694,6 +739,19 @@ impl CursorToolCall {
});
}
if changes.is_empty()
&& let Some(CursorEditResult::Success(CursorEditSuccessResult {
diff_string: Some(diff_string),
..
})) = &result
{
let hunks = extract_unified_diff_hunks(diff_string);
changes.push(FileChange::Edit {
unified_diff: concatenate_diff_hunks(&path, &hunks),
has_line_numbers: false,
});
}
(
ActionType::FileEdit {
path: path.clone(),
@@ -707,7 +765,7 @@ impl CursorToolCall {
(
ActionType::FileEdit {
path: path.clone(),
changes: vec![],
changes: vec![FileChange::Delete],
},
format!("`{path}`"),
)
@@ -731,6 +789,15 @@ impl CursorToolCall {
format!("`{pattern}`"),
)
}
CursorToolCall::SemSearch { args, .. } => {
let query = &args.query;
(
ActionType::Search {
query: query.clone(),
},
format!("`{query}`"),
)
}
CursorToolCall::Glob { args, .. } => {
let pattern = args.glob_pattern.clone().unwrap_or_else(|| "*".to_string());
if let Some(path) = args.path.as_ref().or(args.target_directory.as_ref()) {
@@ -940,7 +1007,7 @@ pub struct CursorLsArgs {
pub struct CursorGlobArgs {
#[serde(default, alias = "globPattern", alias = "glob_pattern")]
pub glob_pattern: Option<String>,
#[serde(default)]
#[serde(default, alias = "targetDirectory")]
pub path: Option<String>,
#[serde(default, alias = "target_directory")]
pub target_directory: Option<String>,
@@ -965,6 +1032,15 @@ pub struct CursorGrepArgs {
pub r#type: Option<String>,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub struct CursorSemSearchArgs {
pub query: String,
#[serde(default, alias = "targetDirectories")]
pub target_directories: Option<Vec<String>>,
#[serde(default)]
pub explanation: Option<String>,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub struct CursorWriteArgs {
pub path: String,
@@ -998,6 +1074,32 @@ pub struct CursorEditArgs {
pub multi_str_replace: Option<CursorMultiStrReplace>,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum CursorEditResult {
Success(CursorEditSuccessResult),
#[serde(untagged)]
Unknown {
#[serde(flatten)]
data: HashMap<String, serde_json::Value>,
},
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub struct CursorEditSuccessResult {
pub path: String,
#[serde(default, rename = "resultForModel")]
pub result_for_model: Option<String>,
#[serde(default, rename = "linesAdded")]
pub lines_added: Option<u64>,
#[serde(default, rename = "linesRemoved")]
pub lines_removed: Option<u64>,
#[serde(default, rename = "diffString")]
pub diff_string: Option<String>,
#[serde(default, rename = "afterFullFileContent")]
pub after_full_file_content: Option<String>,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub struct CursorApplyPatch {
#[serde(rename = "patchContent")]