Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
204e95a
fix: resolve UV path override not being detected in System Requirements
whatevertogo Jan 11, 2026
25e5d05
fix: improve uv/uvx detection robustness on macOS and Linux
whatevertogo Jan 11, 2026
cc22320
Merge branch 'CoplayDev:main' into main
whatevertogo Jan 11, 2026
8d5fa2f
refactor: unify process execution with ExecPath.TryRun and add Window…
whatevertogo Jan 11, 2026
84cee8c
fix: improve version parsing to handle both spaces and parentheses
whatevertogo Jan 11, 2026
510e631
refactor: improve platform detectors with absolute path resolution
whatevertogo Jan 11, 2026
bf41479
fix: improve error handling in PathResolverService by logging exceptions
whatevertogo Jan 11, 2026
f39857c
Remove .meta files added after fork and update .gitignore
whatevertogo Jan 11, 2026
c4be11c
Merge branch 'CoplayDev:main' into main
whatevertogo Jan 12, 2026
1832715
Update .gitignore
whatevertogo Jan 12, 2026
554ddd0
save .meta
whatevertogo Jan 12, 2026
fb5909d
refactor: unify uv/uvx naming and path detection across platforms
whatevertogo Jan 12, 2026
254125a
fix: improve validation light(uvxPathStatus) logic for UVX path overr…
whatevertogo Jan 12, 2026
f9ae5d5
refactor: streamline UV version validation and unify path detection m…
whatevertogo Jan 12, 2026
84cb9c6
fix: add type handling for Claude Code client in config JSON builder
whatevertogo Jan 12, 2026
ee33077
fix: correct command from 'uvx' to 'uv' for Python version listing in…
whatevertogo Jan 12, 2026
2c3ebcd
Merge branch 'CoplayDev:main' into main
whatevertogo Jan 14, 2026
66fe194
Merge branch 'CoplayDev:main' into main
whatevertogo Jan 15, 2026
79bce47
Merge branch 'CoplayDev:main' into main
whatevertogo Jan 16, 2026
b0eb274
feat: Add ActionTrace system for editor event tracking and replay
whatevertogo Jan 16, 2026
ec59a59
refactor: split EventStore into partial classes for better maintainab…
whatevertogo Jan 16, 2026
4e54591
fix: update stylesheet references and comment unsupported styles in A…
whatevertogo Jan 16, 2026
e599807
refactor: update comments and documentation in action trace files for…
whatevertogo Jan 16, 2026
f475bee
feat: enhance ActionTrace with layered settings, preset system, and q…
whatevertogo Jan 17, 2026
af77674
feat: optimize performance in ActionTrace by replacing LINQ with manu…
whatevertogo Jan 17, 2026
f547208
feat: enhance ActionTrace settings with Range attributes and dynamic …
whatevertogo Jan 17, 2026
44b7045
feat: add GlobalIdHelper for cross-session stable object identificati…
whatevertogo Jan 17, 2026
a86506c
fix: replace delayCall with EditorApplication.update to avoid memory …
whatevertogo Jan 18, 2026
f340636
Merge branch 'main' into feature/ActionTrace
whatevertogo Jan 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
533 changes: 533 additions & 0 deletions MCPForUnity/Editor/ActionTrace/Capture/ActionTraceEventEmitter.cs

Large diffs are not rendered by default.

228 changes: 228 additions & 0 deletions MCPForUnity/Editor/ActionTrace/Capture/AssetChangePostprocessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
using System;
using System.Collections.Generic;
using MCPForUnity.Editor.ActionTrace.Core;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEngine;

namespace MCPForUnity.Editor.ActionTrace.Capture
{
/// <summary>
/// Asset postprocessor for tracking asset changes in ActionTrace.
/// Uses Unity's AssetPostprocessor callback pattern, not event subscription.
///
/// Events generated:
/// - AssetImported: When an asset is imported from outside
/// - AssetCreated: When a new asset is created in Unity
/// - AssetDeleted: When an asset is deleted
/// - AssetMoved: When an asset is moved/renamed
/// - AssetModified: When an existing asset is modified
///
/// All asset events use "Asset:{path}" format for TargetId to ensure
/// cross-session stability.
/// </summary>
internal sealed class AssetChangePostprocessor : AssetPostprocessor
{
private static void OnPostprocessAllAssets(
string[] importedAssets,
string[] deletedAssets,
string[] movedAssets,
string[] movedFromAssetPaths)
{
// ========== Imported Assets (includes newly created assets) ==========
foreach (var assetPath in importedAssets)
{
if (string.IsNullOrEmpty(assetPath)) continue;

// L1 Blacklist: Skip junk assets before creating events
if (!EventFilter.ShouldTrackAsset(assetPath))
continue;

string targetId = $"Asset:{assetPath}";
string assetType = GetAssetType(assetPath);

var payload = new Dictionary<string, object>
{
["path"] = assetPath,
["extension"] = System.IO.Path.GetExtension(assetPath),
["asset_type"] = assetType
};

// Distinguish between imported and newly created assets
if (IsNewlyCreatedAsset(assetPath))
{
RecordEvent(EventTypes.AssetCreated, targetId, payload);
}
else
{
RecordEvent(EventTypes.AssetImported, targetId, payload);
}
}

// ========== Deleted Assets ==========
foreach (var assetPath in deletedAssets)
{
if (string.IsNullOrEmpty(assetPath)) continue;

// L1 Blacklist: Skip junk assets
if (!EventFilter.ShouldTrackAsset(assetPath))
continue;

string targetId = $"Asset:{assetPath}";

var payload = new Dictionary<string, object>
{
["path"] = assetPath
};

RecordEvent(EventTypes.AssetDeleted, targetId, payload);
}

// ========== Moved Assets ==========
for (int i = 0; i < movedAssets.Length; i++)
{
if (string.IsNullOrEmpty(movedAssets[i])) continue;

var fromPath = i < movedFromAssetPaths.Length ? movedFromAssetPaths[i] : "";

// L1 Blacklist: Skip junk assets
if (!EventFilter.ShouldTrackAsset(movedAssets[i]))
continue;

string targetId = $"Asset:{movedAssets[i]}";

var payload = new Dictionary<string, object>
{
["to_path"] = movedAssets[i],
["from_path"] = fromPath
};

RecordEvent(EventTypes.AssetMoved, targetId, payload);
}

// ========== Modified Assets ==========
// Track asset modifications separately (e.g., texture imports, prefab changes)
foreach (var assetPath in importedAssets)
{
if (string.IsNullOrEmpty(assetPath)) continue;

// Only track modifications for existing assets
if (!IsNewlyCreatedAsset(assetPath) && EventFilter.ShouldTrackAsset(assetPath))
{
string targetId = $"Asset:{assetPath}";
string assetType = GetAssetType(assetPath);

// Only record modifications for certain asset types
if (ShouldTrackModification(assetPath))
{
var payload = new Dictionary<string, object>
{
["path"] = assetPath,
["extension"] = System.IO.Path.GetExtension(assetPath),
["asset_type"] = assetType
};

RecordEvent(EventTypes.AssetModified, targetId, payload);
}
}
}
Comment on lines +103 to +128
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Duplicate event emission for imported assets.

This section iterates importedAssets again and emits AssetModified events for non-newly-created assets. However, lines 56-58 already emit AssetImported for these same assets, resulting in duplicate events for the same import operation.

Since IsNewlyCreatedAsset always returns false, every imported asset that passes ShouldTrackModification will generate both an AssetImported event (line 58) and an AssetModified event (line 125).

Consider removing this section or making it mutually exclusive with the imported assets handling above.

🐛 Suggested fix
-            // ========== Modified Assets ==========
-            // Track asset modifications separately (e.g., texture imports, prefab changes)
-            foreach (var assetPath in importedAssets)
-            {
-                if (string.IsNullOrEmpty(assetPath)) continue;
-
-                // Only track modifications for existing assets
-                if (!IsNewlyCreatedAsset(assetPath) && EventFilter.ShouldTrackAsset(assetPath))
-                {
-                    string targetId = $"Asset:{assetPath}";
-                    string assetType = GetAssetType(assetPath);
-
-                    // Only record modifications for certain asset types
-                    if (ShouldTrackModification(assetPath))
-                    {
-                        var payload = new Dictionary<string, object>
-                        {
-                            ["path"] = assetPath,
-                            ["extension"] = System.IO.Path.GetExtension(assetPath),
-                            ["asset_type"] = assetType
-                        };
-
-                        RecordEvent(EventTypes.AssetModified, targetId, payload);
-                    }
-                }
-            }

If you need to distinguish modifications from imports, implement IsNewlyCreatedAsset properly first, then use it to emit either AssetCreated/AssetImported OR AssetModified, but not both.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ========== Modified Assets ==========
// Track asset modifications separately (e.g., texture imports, prefab changes)
foreach (var assetPath in importedAssets)
{
if (string.IsNullOrEmpty(assetPath)) continue;
// Only track modifications for existing assets
if (!IsNewlyCreatedAsset(assetPath) && EventFilter.ShouldTrackAsset(assetPath))
{
string targetId = $"Asset:{assetPath}";
string assetType = GetAssetType(assetPath);
// Only record modifications for certain asset types
if (ShouldTrackModification(assetPath))
{
var payload = new Dictionary<string, object>
{
["path"] = assetPath,
["extension"] = System.IO.Path.GetExtension(assetPath),
["asset_type"] = assetType
};
RecordEvent(EventTypes.AssetModified, targetId, payload);
}
}
}
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/ActionTrace/Capture/AssetChangePostprocessor.cs` around
lines 103 - 128, The code emits duplicate events for the same imported assets
because importedAssets are processed earlier to emit AssetImported (via
RecordEvent) and this later loop emits AssetModified for the same entries;
either remove this AssetModified loop or make it mutually exclusive by using
IsNewlyCreatedAsset correctly so an asset only triggers
AssetImported/AssetCreated OR AssetModified; update the logic around
importedAssets, IsNewlyCreatedAsset, ShouldTrackModification and RecordEvent (or
adjust ShouldTrackModification) to ensure only one event type is recorded per
asset.

}

/// <summary>
/// Determines if an asset was newly created vs imported.
/// Newly created assets have a .meta file with recent creation time.
/// </summary>
private static bool IsNewlyCreatedAsset(string assetPath)
{
try
{
string metaPath = assetPath + ".meta";
var meta = AssetDatabase.LoadMainAssetAtPath(metaPath);
// This is a simplified check - in production you'd check file creation time
return false; // Default to treating as imported for now
}
catch
{
return false;
}
}

/// <summary>
/// Determines if modifications to this asset type should be tracked.
/// Tracks modifications for commonly edited asset types.
/// </summary>
private static bool ShouldTrackModification(string assetPath)
{
string ext = System.IO.Path.GetExtension(assetPath).ToLower();
// Track modifications for these asset types
return ext == ".png" || ext == ".jpg" || ext == ".jpeg" ||
ext == ".psd" || ext == ".tif" ||
ext == ".fbx" || ext == ".obj" ||
ext == ".prefab" || ext == ".unity" ||
ext == ".anim" || ext == ".controller";
}

/// <summary>
/// Gets the asset type based on file extension.
/// </summary>
private static string GetAssetType(string assetPath)
{
string ext = System.IO.Path.GetExtension(assetPath).ToLower();
return ext switch
{
".cs" => "script",
".unity" => "scene",
".prefab" => "prefab",
".mat" => "material",
".png" or ".jpg" or ".jpeg" or ".gif" or ".tga" or ".psd" or ".tif" or ".bmp" => "texture",
".fbx" or ".obj" or ".blend" or ".3ds" => "model",
".anim" => "animation",
".controller" => "animator_controller",
".shader" => "shader",
".asset" => "scriptable_object",
".physicmaterial" => "physics_material",
".physicmaterial2d" => "physics_material_2d",
".guiskin" => "gui_skin",
".fontsettings" => "font",
".mixer" => "audio_mixer",
".rendertexture" => "render_texture",
".spriteatlas" => "sprite_atlas",
".tilepalette" => "tile_palette",
_ => "unknown"
};
}

/// <summary>
/// Records an event to the EventStore with proper context injection.
/// </summary>
private static void RecordEvent(string type, string targetId, Dictionary<string, object> payload)
{
try
{
// Inject VCS context into all recorded events
var vcsContext = VCS.VcsContextProvider.GetCurrentContext();
payload["vcs_context"] = vcsContext.ToDictionary();

// Inject Undo Group ID for undo_to_sequence functionality (P2.4)
int currentUndoGroup = Undo.GetCurrentGroup();
payload["undo_group"] = currentUndoGroup;

var evt = new EditorEvent(
sequence: 0,
timestampUnixMs: DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
type: type,
targetId: targetId,
payload: payload
);

// AssetPostprocessor callbacks run on main thread but outside update loop.
// Use delayCall to defer recording to main thread update, avoiding thread warnings.
UnityEditor.EditorApplication.delayCall += () => Core.EventStore.Record(evt);
}
catch (Exception ex)
{
McpLog.Warn($"[AssetChangePostprocessor] Failed to record event: {ex.Message}");
}
}
}
}
Loading