diff --git a/src/commands/checkpoint_agent/agent_presets.rs b/src/commands/checkpoint_agent/agent_presets.rs index b00b019b0..e8197d9fa 100644 --- a/src/commands/checkpoint_agent/agent_presets.rs +++ b/src/commands/checkpoint_agent/agent_presets.rs @@ -1409,8 +1409,21 @@ impl AgentCheckpointPreset for CursorPreset { ))); } + // Only checkpoint on file-mutating tools (Write, Delete, StrReplace) + let tool_name = hook_data + .get("tool_name") + .and_then(|v| v.as_str()) + .unwrap_or(""); + if !matches!(tool_name, "Write" | "Delete" | "StrReplace") { + return Err(GitAiError::PresetError(format!( + "Skipping Cursor hook for non-edit tool_name '{}'.", + tool_name + ))); + } + let file_path = hook_data - .get("file_path") + .get("tool_input") + .and_then(|ti| ti.get("file_path")) .and_then(|v| v.as_str()) .map(Self::normalize_cursor_path) .unwrap_or_default(); @@ -1439,6 +1452,12 @@ impl AgentCheckpointPreset for CursorPreset { }; if hook_event_name == "preToolUse" { + let will_edit = if !file_path.is_empty() { + Some(vec![file_path.clone()]) + } else { + None + }; + // early return, we're just adding a human checkpoint. return Ok(AgentRunResult { agent_id: AgentId { @@ -1451,7 +1470,7 @@ impl AgentCheckpointPreset for CursorPreset { transcript: None, repo_working_dir: Some(repo_working_dir), edited_filepaths: None, - will_edit_filepaths: None, + will_edit_filepaths: will_edit, dirty_files: None, }); } diff --git a/tests/integration/cursor.rs b/tests/integration/cursor.rs index 8e713ad22..797baef25 100644 --- a/tests/integration/cursor.rs +++ b/tests/integration/cursor.rs @@ -186,21 +186,25 @@ fn test_cursor_preset_multi_root_workspace_detection() { .map(|s| format!("\"{}\"", s)) .collect(); - let file_path_json = if file_path.is_empty() { + let tool_input_json = if file_path.is_empty() { String::new() } else { - format!(",\n \"file_path\": \"{}\"", file_path) + format!( + ",\n \"tool_input\": {{ \"file_path\": \"{}\" }}", + file_path + ) }; let hook_input = format!( r##"{{ "conversation_id": "test-conversation-id", "workspace_roots": [{}], - "hook_event_name": "preToolUse"{}, + "hook_event_name": "preToolUse", + "tool_name": "Write"{}, "model": "model-name-from-hook-test" }}"##, workspace_roots_json.join(", "), - file_path_json + tool_input_json ); let flags = AgentCheckpointFlags { @@ -298,7 +302,8 @@ fn test_cursor_preset_human_checkpoint_no_filepath() { "conversation_id": "test-conversation-id", "workspace_roots": ["/Users/test/workspace"], "hook_event_name": "preToolUse", - "file_path": "/Users/test/workspace/src/main.rs", + "tool_name": "Write", + "tool_input": { "file_path": "/Users/test/workspace/src/main.rs" }, "model": "model-name-from-hook-test" }"##; @@ -329,6 +334,7 @@ fn test_cursor_checkpoint_stdin_with_utf8_bom() { "conversation_id": "test-conversation-id", "workspace_roots": [repo.canonical_path().to_string_lossy().to_string()], "hook_event_name": "preToolUse", + "tool_name": "Write", "model": "model-name-from-hook-test" }) ); @@ -376,7 +382,8 @@ fn test_cursor_e2e_with_attribution() { "conversation_id": TEST_CONVERSATION_ID, "workspace_roots": [repo.canonical_path().to_string_lossy().to_string()], "hook_event_name": "postToolUse", - "file_path": file_path.to_string_lossy().to_string(), + "tool_name": "Write", + "tool_input": { "file_path": file_path.to_string_lossy().to_string() }, "model": "model-name-from-hook-test" }) .to_string(); @@ -482,7 +489,8 @@ fn test_cursor_e2e_with_resync() { "conversation_id": TEST_CONVERSATION_ID, "workspace_roots": [repo.canonical_path().to_string_lossy().to_string()], "hook_event_name": "postToolUse", - "file_path": file_path.to_string_lossy().to_string(), + "tool_name": "Write", + "tool_input": { "file_path": file_path.to_string_lossy().to_string() }, "model": "model-name-from-hook-test" }) .to_string();