@atelierai/uco 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +32 -8
  2. package/package.json +2 -2
  3. package/vendor/plugin/com.atelierai.unity.copilot/CHANGELOG.md +4 -4
  4. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/ToolGroups.SetEnabled.cs +92 -92
  5. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/CopilotServerManager.cs +0 -147
  6. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.Editor.cs +16 -0
  7. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotServer.cs +3 -3
  8. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolGroupRegistry.cs +461 -460
  9. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotPromptsWindow.uss +11 -11
  10. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotResourcesWindow.uss +16 -16
  11. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotToolsWindow.uss +36 -36
  12. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/MainWindow.uss +183 -183
  13. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/{_mcp-list-window.uss → _copilot-list-window.uss} +18 -18
  14. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/_list-view.uss +77 -77
  15. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotPromptsWindow.uxml +18 -18
  16. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotResourcesWindow.uxml +18 -18
  17. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotToolsWindow.uxml +18 -18
  18. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/MainWindow.uxml +115 -115
  19. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/ReflectorNet.dll +0 -0
  20. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.Common.dll +0 -0
  21. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.dll +0 -0
  22. package/vendor/plugin/com.atelierai.unity.copilot/README.md +88 -88
  23. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/BufferedFileLogStorage.cs +208 -207
  24. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/FileLogStorage.cs +3 -1
  25. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/UnityLogCollector.cs +282 -280
  26. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.cs +1 -1
  27. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetExtractorDevelopmentDependencyTests.cs +148 -148
  28. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetExtractorFlatLayoutTests.cs +166 -166
  29. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetInstallManifestTests.cs +436 -436
  30. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetLegacyMigrationTests.cs +1 -1
  31. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetPackageInstallerStaleVersionCleanupTests.cs +200 -200
  32. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Environment/EnvironmentUtilsTests.cs +1 -1
  33. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/GameObject/TestJsonSchema.cs +199 -199
  34. package/vendor/plugin/com.atelierai.unity.copilot/package.json +1 -1
  35. /package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/{_mcp-list-window.uss.meta → _copilot-list-window.uss.meta} +0 -0
@@ -1,280 +1,282 @@
1
- /*
2
- ┌──────────────────────────────────────────────────────────────────┐
3
- │ Author: Ivan Murzak (https://github.com/IvanMurzak) │
4
- │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │
5
- │ Copyright (c) 2025 Ivan Murzak │
6
- │ Licensed under the Apache License, Version 2.0. │
7
- │ See the LICENSE file in the project root for more information. │
8
- └──────────────────────────────────────────────────────────────────┘
9
- */
10
-
11
- #nullable enable
12
- using System;
13
- using System.Text.RegularExpressions;
14
- using System.Threading;
15
- using System.Threading.Tasks;
16
- using com.AtelierAI.Unity.Copilot.Runtime.Utils;
17
- using com.AtelierAI.Unity.Copilot.Utils;
18
- using com.AtelierAI.Uco.Framework.Common;
19
- using com.IvanMurzak.ReflectorNet;
20
- using com.IvanMurzak.ReflectorNet.Utils;
21
- using UnityEngine;
22
-
23
- namespace com.AtelierAI.Unity.Copilot
24
- {
25
- /// <summary>
26
- /// Result of a detailed console query: the filtered entries plus the
27
- /// collector's loss accounting so "no errors" is distinguishable from
28
- /// "errors were dropped" (COCli-07).
29
- /// </summary>
30
- public class ConsoleLogsQueryResult
31
- {
32
- public LogEntry[] Entries { get; set; } = Array.Empty<LogEntry>();
33
- public int DroppedEntries { get; set; }
34
- public int TruncatedEntries { get; set; }
35
- }
36
-
37
- /// <summary>
38
- /// Collects Unity log messages and manages saving/loading them to/from a cache file.
39
- /// </summary>
40
- public class UnityLogCollector : IDisposable
41
- {
42
- private static readonly Regex RichTextRegex = new Regex(@"</?(b|i|size|color|material|quad|a)\b[^>]*>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
43
-
44
- const int MaxMessageCharacters = 16 * 1024;
45
-
46
- readonly ILogStorage _logStorage;
47
- readonly ThreadSafeBool _isDisposed = new(false);
48
- // This collector's channel sink and the sink it displaced, so Dispose
49
- // can restore the previous channel owner instead of clearing it.
50
- readonly Action<PluginDiagnosticEntry> _channelSink;
51
- Action<PluginDiagnosticEntry>? _previousChannelSink;
52
- int _droppedEntries;
53
- int _truncatedEntries;
54
-
55
- public UnityLogCollector(ILogStorage logStorage)
56
- {
57
- if (!MainThread.Instance.IsMainThread)
58
- throw new Exception($"{GetType().GetTypeShortName()} constructor must be initialized on the main thread.");
59
-
60
- _logStorage = logStorage ?? throw new ArgumentNullException(nameof(logStorage));
61
-
62
- _channelSink = AppendPluginDiagnostic;
63
- _previousChannelSink = PluginDiagnostics.CurrentSink;
64
- Application.logMessageReceivedThreaded += OnLogMessageReceived;
65
- PluginDiagnostics.Install(_channelSink);
66
- }
67
-
68
- public LogClearResult Clear()
69
- {
70
- if (_isDisposed.Value)
71
- return new LogClearResult
72
- {
73
- Ok = false,
74
- Strategy = "collector-disposed",
75
- Error = "The Unity log collector is disposed."
76
- };
77
-
78
- return _logStorage.Clear();
79
- }
80
-
81
- /// <summary>
82
- /// Synchronously saves all current log entries to the cache file.
83
- /// </summary>
84
- public void Save()
85
- {
86
- if (_isDisposed.Value)
87
- return;
88
-
89
- _logStorage.Flush();
90
- }
91
-
92
- /// <summary>
93
- /// Asynchronously saves all current log entries to the cache file.
94
- /// </summary>
95
- /// <returns>A task that completes when the save operation is finished.</returns>
96
- public Task SaveAsync()
97
- {
98
- if (_isDisposed.Value)
99
- return Task.CompletedTask;
100
-
101
- return _logStorage.FlushAsync();
102
- }
103
-
104
- public Task<LogEntry[]> QueryAsync(
105
- int maxEntries = 100,
106
- LogType? logTypeFilter = null,
107
- bool includeStackTrace = false,
108
- int lastMinutes = 0)
109
- {
110
- return _logStorage.QueryAsync(maxEntries, logTypeFilter, includeStackTrace, lastMinutes);
111
- }
112
-
113
- public LogEntry[] Query(
114
- int maxEntries = 100,
115
- LogType? logTypeFilter = null,
116
- bool includeStackTrace = false,
117
- int lastMinutes = 0)
118
- {
119
- return _logStorage.Query(maxEntries, logTypeFilter, includeStackTrace, lastMinutes);
120
- }
121
-
122
- /// <summary>
123
- /// Detailed query with operation/correlation/source/time-boundary
124
- /// filters and loss accounting (COCli-07). Existing filter names and
125
- /// semantics are unchanged; the new filters compose with them.
126
- /// </summary>
127
- public ConsoleLogsQueryResult QueryDetailed(
128
- int maxEntries = 100,
129
- LogType? logTypeFilter = null,
130
- bool includeStackTrace = false,
131
- int lastMinutes = 0,
132
- string? correlationId = null,
133
- string? operationId = null,
134
- string? source = null,
135
- long? sinceUnixMs = null)
136
- {
137
- if (maxEntries < 1)
138
- throw new ArgumentOutOfRangeException(nameof(maxEntries));
139
- if (!string.IsNullOrEmpty(source)
140
- && source != LogEntry.SourceProduct
141
- && source != LogEntry.SourceBridge
142
- && source != LogEntry.SourceTool
143
- && source != LogEntry.SourceUnity)
144
- {
145
- throw new ArgumentException(
146
- $"source must be one of: {LogEntry.SourceProduct}, {LogEntry.SourceBridge}, {LogEntry.SourceTool}, {LogEntry.SourceUnity}.");
147
- }
148
-
149
- // Oversubscribe the storage query so the id/source filters below
150
- // can still fill maxEntries from a wider recent window.
151
- var fetchLimit = string.IsNullOrEmpty(correlationId) && string.IsNullOrEmpty(operationId)
152
- ? maxEntries
153
- : Math.Min(2_000, maxEntries * 10);
154
- var candidates = _logStorage.Query(
155
- fetchLimit, logTypeFilter, includeStackTrace, lastMinutes);
156
-
157
- var results = new System.Collections.Generic.List<LogEntry>(candidates.Length);
158
- var boundary = sinceUnixMs.HasValue
159
- ? DateTimeOffset.FromUnixTimeMilliseconds(sinceUnixMs.Value).LocalDateTime
160
- : (DateTime?)null;
161
- // Storage returns newest-first; keep the newest maxEntries after
162
- // filtering, then present them oldest-first for readability.
163
- for (var i = 0; i < candidates.Length && results.Count < maxEntries; i++)
164
- {
165
- var entry = candidates[i];
166
- if (!string.IsNullOrEmpty(correlationId)
167
- && !string.Equals(entry.CorrelationId, correlationId, StringComparison.Ordinal))
168
- continue;
169
- if (!string.IsNullOrEmpty(operationId)
170
- && !string.Equals(entry.OperationId, operationId, StringComparison.Ordinal))
171
- continue;
172
- if (!string.IsNullOrEmpty(source)
173
- && !string.Equals(entry.Source, source, StringComparison.Ordinal))
174
- continue;
175
- if (boundary.HasValue && entry.Timestamp < boundary.Value)
176
- continue;
177
- results.Add(entry);
178
- }
179
- results.Reverse();
180
-
181
- return new ConsoleLogsQueryResult
182
- {
183
- Entries = results.ToArray(),
184
- DroppedEntries = Volatile.Read(ref _droppedEntries) + PluginDiagnostics.DroppedEntries,
185
- TruncatedEntries = Volatile.Read(ref _truncatedEntries),
186
- };
187
- }
188
-
189
- /// <summary>Sink for the plugin/bridge/tool diagnostics channel.</summary>
190
- void AppendPluginDiagnostic(PluginDiagnosticEntry entry)
191
- {
192
- if (_isDisposed.Value || entry == null) return;
193
- try
194
- {
195
- _logStorage.Append(new LogEntry(
196
- logType: entry.LogType,
197
- message: BoundMessage(entry.Message, out var truncated),
198
- source: entry.Source,
199
- correlationId: entry.CorrelationId,
200
- operationId: entry.OperationId,
201
- timestamp: DateTime.Now,
202
- stackTrace: entry.StackTrace));
203
- if (truncated)
204
- Interlocked.Increment(ref _truncatedEntries);
205
- }
206
- catch
207
- {
208
- Interlocked.Increment(ref _droppedEntries);
209
- }
210
- }
211
-
212
- void OnLogMessageReceived(string message, string stackTrace, LogType type)
213
- {
214
- try
215
- {
216
- // Strip rich text tags
217
- var cleanMessage = BoundMessage(RichTextRegex.Replace(message, string.Empty), out var truncated);
218
-
219
- // COCli-07 attribution: entries produced on the main thread
220
- // inside an owned execution window carry that call's ids and
221
- // classify as product; everything Unity forwards otherwise
222
- // stays source=unity with empty correlation fields.
223
- string? correlationId = null;
224
- string? operationId = null;
225
- var source = LogEntry.SourceUnity;
226
- if (MainThread.Instance.IsMainThread && LogCallScope.Current is { } frame)
227
- {
228
- correlationId = frame.CorrelationId;
229
- operationId = frame.OperationId;
230
- source = LogEntry.SourceProduct;
231
- }
232
-
233
- _logStorage.Append(new LogEntry(
234
- logType: type,
235
- message: cleanMessage,
236
- source: source,
237
- correlationId: correlationId,
238
- operationId: operationId,
239
- timestamp: DateTime.Now,
240
- stackTrace: string.IsNullOrEmpty(stackTrace) ? null : stackTrace));
241
- if (truncated)
242
- Interlocked.Increment(ref _truncatedEntries);
243
- }
244
- catch
245
- {
246
- Interlocked.Increment(ref _droppedEntries);
247
- }
248
- }
249
-
250
- string BoundMessage(string message, out bool truncated)
251
- {
252
- if (message.Length <= MaxMessageCharacters)
253
- {
254
- truncated = false;
255
- return message;
256
- }
257
- truncated = true;
258
- return message.Substring(0, MaxMessageCharacters);
259
- }
260
-
261
- public void Dispose()
262
- {
263
- if (!_isDisposed.TrySetTrue())
264
- return; // already disposed
265
-
266
- Application.logMessageReceivedThreaded -= OnLogMessageReceived;
267
- // Hand the channel back to whatever sink was installed before this
268
- // collector (e.g. the plugin's own collector) instead of clearing
269
- // it: a temporary collector must not disable console separation for
270
- // the rest of the session. A newer install is left untouched.
271
- PluginDiagnostics.RestoreIfCurrent(_channelSink, _previousChannelSink);
272
- _previousChannelSink = null;
273
- _logStorage.Dispose();
274
-
275
- GC.SuppressFinalize(this);
276
- }
277
-
278
- ~UnityLogCollector() => Dispose();
279
- }
280
- }
1
+ /*
2
+ ┌──────────────────────────────────────────────────────────────────┐
3
+ │ Author: Ivan Murzak (https://github.com/IvanMurzak) │
4
+ │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │
5
+ │ Copyright (c) 2025 Ivan Murzak │
6
+ │ Licensed under the Apache License, Version 2.0. │
7
+ │ See the LICENSE file in the project root for more information. │
8
+ └──────────────────────────────────────────────────────────────────┘
9
+ */
10
+
11
+ #nullable enable
12
+ using System;
13
+ using System.Text.RegularExpressions;
14
+ using System.Threading;
15
+ using System.Threading.Tasks;
16
+ using com.AtelierAI.Unity.Copilot.Runtime.Utils;
17
+ using com.AtelierAI.Unity.Copilot.Utils;
18
+ using com.AtelierAI.Uco.Framework.Common;
19
+ using com.IvanMurzak.ReflectorNet;
20
+ using com.IvanMurzak.ReflectorNet.Utils;
21
+ using UnityEngine;
22
+
23
+ namespace com.AtelierAI.Unity.Copilot
24
+ {
25
+ /// <summary>
26
+ /// Result of a detailed console query: the filtered entries plus the
27
+ /// collector's loss accounting so "no errors" is distinguishable from
28
+ /// "errors were dropped" (COCli-07).
29
+ /// </summary>
30
+ public class ConsoleLogsQueryResult
31
+ {
32
+ public LogEntry[] Entries { get; set; } = Array.Empty<LogEntry>();
33
+ public int DroppedEntries { get; set; }
34
+ public int TruncatedEntries { get; set; }
35
+ }
36
+
37
+ /// <summary>
38
+ /// Collects Unity log messages and manages saving/loading them to/from a cache file.
39
+ /// </summary>
40
+ public class UnityLogCollector : IDisposable
41
+ {
42
+ private static readonly Regex RichTextRegex = new Regex(@"</?(b|i|size|color|material|quad|a)\b[^>]*>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
43
+
44
+ const int MaxMessageCharacters = 16 * 1024;
45
+
46
+ readonly ILogStorage _logStorage;
47
+ readonly ThreadSafeBool _isDisposed = new(false);
48
+ // This collector's channel sink and the sink it displaced, so Dispose
49
+ // can restore the previous channel owner instead of clearing it.
50
+ readonly Action<PluginDiagnosticEntry> _channelSink;
51
+ Action<PluginDiagnosticEntry>? _previousChannelSink;
52
+ int _droppedEntries;
53
+ int _truncatedEntries;
54
+
55
+ public UnityLogCollector(ILogStorage logStorage)
56
+ {
57
+ if (!MainThread.Instance.IsMainThread)
58
+ throw new Exception($"{GetType().GetTypeShortName()} constructor must be initialized on the main thread.");
59
+
60
+ _logStorage = logStorage ?? throw new ArgumentNullException(nameof(logStorage));
61
+
62
+ _channelSink = AppendPluginDiagnostic;
63
+ _previousChannelSink = PluginDiagnostics.CurrentSink;
64
+ Application.logMessageReceivedThreaded += OnLogMessageReceived;
65
+ PluginDiagnostics.Install(_channelSink);
66
+ }
67
+
68
+ public LogClearResult Clear()
69
+ {
70
+ if (_isDisposed.Value)
71
+ return new LogClearResult
72
+ {
73
+ Ok = false,
74
+ Strategy = "collector-disposed",
75
+ Error = "The Unity log collector is disposed."
76
+ };
77
+
78
+ return _logStorage.Clear();
79
+ }
80
+
81
+ /// <summary>
82
+ /// Synchronously saves all current log entries to the cache file.
83
+ /// </summary>
84
+ public void Save()
85
+ {
86
+ if (_isDisposed.Value)
87
+ return;
88
+
89
+ _logStorage.Flush();
90
+ }
91
+
92
+ /// <summary>
93
+ /// Asynchronously saves all current log entries to the cache file.
94
+ /// </summary>
95
+ /// <returns>A task that completes when the save operation is finished.</returns>
96
+ public Task SaveAsync()
97
+ {
98
+ if (_isDisposed.Value)
99
+ return Task.CompletedTask;
100
+
101
+ return _logStorage.FlushAsync();
102
+ }
103
+
104
+ public Task<LogEntry[]> QueryAsync(
105
+ int maxEntries = 100,
106
+ LogType? logTypeFilter = null,
107
+ bool includeStackTrace = false,
108
+ int lastMinutes = 0)
109
+ {
110
+ return _logStorage.QueryAsync(maxEntries, logTypeFilter, includeStackTrace, lastMinutes);
111
+ }
112
+
113
+ public LogEntry[] Query(
114
+ int maxEntries = 100,
115
+ LogType? logTypeFilter = null,
116
+ bool includeStackTrace = false,
117
+ int lastMinutes = 0)
118
+ {
119
+ return _logStorage.Query(maxEntries, logTypeFilter, includeStackTrace, lastMinutes);
120
+ }
121
+
122
+ /// <summary>
123
+ /// Detailed query with operation/correlation/source/time-boundary
124
+ /// filters and loss accounting (COCli-07). Existing filter names and
125
+ /// semantics are unchanged; the new filters compose with them.
126
+ /// </summary>
127
+ public ConsoleLogsQueryResult QueryDetailed(
128
+ int maxEntries = 100,
129
+ LogType? logTypeFilter = null,
130
+ bool includeStackTrace = false,
131
+ int lastMinutes = 0,
132
+ string? correlationId = null,
133
+ string? operationId = null,
134
+ string? source = null,
135
+ long? sinceUnixMs = null)
136
+ {
137
+ if (maxEntries < 1)
138
+ throw new ArgumentOutOfRangeException(nameof(maxEntries));
139
+ if (!string.IsNullOrEmpty(source)
140
+ && source != LogEntry.SourceProduct
141
+ && source != LogEntry.SourceBridge
142
+ && source != LogEntry.SourceTool
143
+ && source != LogEntry.SourceUnity)
144
+ {
145
+ throw new ArgumentException(
146
+ $"source must be one of: {LogEntry.SourceProduct}, {LogEntry.SourceBridge}, {LogEntry.SourceTool}, {LogEntry.SourceUnity}.");
147
+ }
148
+
149
+ // Oversubscribe the storage query so the id/source filters below
150
+ // can still fill maxEntries from a wider recent window.
151
+ var fetchLimit = string.IsNullOrEmpty(correlationId) && string.IsNullOrEmpty(operationId)
152
+ ? maxEntries
153
+ : Math.Min(2_000, maxEntries * 10);
154
+ var candidates = _logStorage.Query(
155
+ fetchLimit, logTypeFilter, includeStackTrace, lastMinutes);
156
+
157
+ var results = new System.Collections.Generic.List<LogEntry>(candidates.Length);
158
+ var boundary = sinceUnixMs.HasValue
159
+ ? DateTimeOffset.FromUnixTimeMilliseconds(sinceUnixMs.Value).LocalDateTime
160
+ : (DateTime?)null;
161
+ // Storage returns newest-first; keep the newest maxEntries after
162
+ // filtering, then present them oldest-first for readability.
163
+ for (var i = 0; i < candidates.Length && results.Count < maxEntries; i++)
164
+ {
165
+ var entry = candidates[i];
166
+ if (!string.IsNullOrEmpty(correlationId)
167
+ && !string.Equals(entry.CorrelationId, correlationId, StringComparison.Ordinal))
168
+ continue;
169
+ if (!string.IsNullOrEmpty(operationId)
170
+ && !string.Equals(entry.OperationId, operationId, StringComparison.Ordinal))
171
+ continue;
172
+ if (!string.IsNullOrEmpty(source)
173
+ && !string.Equals(entry.Source, source, StringComparison.Ordinal))
174
+ continue;
175
+ if (boundary.HasValue && entry.Timestamp < boundary.Value)
176
+ continue;
177
+ results.Add(entry);
178
+ }
179
+ results.Reverse();
180
+
181
+ return new ConsoleLogsQueryResult
182
+ {
183
+ Entries = results.ToArray(),
184
+ DroppedEntries = Volatile.Read(ref _droppedEntries) + PluginDiagnostics.DroppedEntries,
185
+ TruncatedEntries = Volatile.Read(ref _truncatedEntries),
186
+ };
187
+ }
188
+
189
+ /// <summary>Sink for the plugin/bridge/tool diagnostics channel.</summary>
190
+ void AppendPluginDiagnostic(PluginDiagnosticEntry entry)
191
+ {
192
+ if (_isDisposed.Value || entry == null) return;
193
+ try
194
+ {
195
+ _logStorage.Append(new LogEntry(
196
+ logType: entry.LogType,
197
+ message: BoundMessage(entry.Message, out var truncated),
198
+ source: entry.Source,
199
+ correlationId: entry.CorrelationId,
200
+ operationId: entry.OperationId,
201
+ timestamp: DateTime.Now,
202
+ stackTrace: entry.StackTrace));
203
+ if (truncated)
204
+ Interlocked.Increment(ref _truncatedEntries);
205
+ }
206
+ catch
207
+ {
208
+ Interlocked.Increment(ref _droppedEntries);
209
+ }
210
+ }
211
+
212
+ void OnLogMessageReceived(string message, string stackTrace, LogType type)
213
+ {
214
+ try
215
+ {
216
+ // Strip rich text tags
217
+ var cleanMessage = BoundMessage(RichTextRegex.Replace(message, string.Empty), out var truncated);
218
+
219
+ // COCli-07 attribution: entries produced on the main thread
220
+ // inside an owned execution window carry that call's ids and
221
+ // classify as product; everything Unity forwards otherwise
222
+ // stays source=unity with empty correlation fields.
223
+ string? correlationId = null;
224
+ string? operationId = null;
225
+ var source = LogEntry.SourceUnity;
226
+ if (MainThread.Instance.IsMainThread && LogCallScope.Current is { } frame)
227
+ {
228
+ correlationId = frame.CorrelationId;
229
+ operationId = frame.OperationId;
230
+ source = LogEntry.SourceProduct;
231
+ }
232
+
233
+ _logStorage.Append(new LogEntry(
234
+ logType: type,
235
+ message: cleanMessage,
236
+ source: source,
237
+ correlationId: correlationId,
238
+ operationId: operationId,
239
+ timestamp: DateTime.Now,
240
+ stackTrace: string.IsNullOrEmpty(stackTrace) ? null : stackTrace));
241
+ if (truncated)
242
+ Interlocked.Increment(ref _truncatedEntries);
243
+ }
244
+ catch
245
+ {
246
+ Interlocked.Increment(ref _droppedEntries);
247
+ }
248
+ }
249
+
250
+ string BoundMessage(string message, out bool truncated)
251
+ {
252
+ if (message.Length <= MaxMessageCharacters)
253
+ {
254
+ truncated = false;
255
+ return message;
256
+ }
257
+ truncated = true;
258
+ return message.Substring(0, MaxMessageCharacters);
259
+ }
260
+
261
+ public void Dispose()
262
+ {
263
+ if (!_isDisposed.TrySetTrue())
264
+ return; // already disposed
265
+
266
+ Application.logMessageReceivedThreaded -= OnLogMessageReceived;
267
+ // Hand the channel back to whatever sink was installed before this
268
+ // collector (e.g. the plugin's own collector) instead of clearing
269
+ // it: a temporary collector must not disable console separation for
270
+ // the rest of the session. A newer install is left untouched.
271
+ PluginDiagnostics.RestoreIfCurrent(_channelSink, _previousChannelSink);
272
+ _previousChannelSink = null;
273
+ _logStorage.Dispose();
274
+
275
+ GC.SuppressFinalize(this);
276
+ }
277
+
278
+ // No finalizer: Dispose performs file I/O / Unity static-event unsubscription,
279
+ // which must never run on the finalizer thread during domain unload
280
+ // (same crash class as uco-domain-reload-crash-20260919).
281
+ }
282
+ }
@@ -26,7 +26,7 @@ namespace com.AtelierAI.Unity.Copilot
26
26
 
27
27
  public partial class UnityCopilotPlugin : IDisposable
28
28
  {
29
- public const string Version = "1.0.6";
29
+ public const string Version = "1.0.8";
30
30
 
31
31
  private static int _singletonCount = 0;
32
32
  public static bool HasAnyInstance => _singletonCount > 0;