@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.
- package/CHANGELOG.md +32 -8
- package/package.json +2 -2
- package/vendor/plugin/com.atelierai.unity.copilot/CHANGELOG.md +4 -4
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/ToolGroups.SetEnabled.cs +92 -92
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/CopilotServerManager.cs +0 -147
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.Editor.cs +16 -0
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotServer.cs +3 -3
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolGroupRegistry.cs +461 -460
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotPromptsWindow.uss +11 -11
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotResourcesWindow.uss +16 -16
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotToolsWindow.uss +36 -36
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/MainWindow.uss +183 -183
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/{_mcp-list-window.uss → _copilot-list-window.uss} +18 -18
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/_list-view.uss +77 -77
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotPromptsWindow.uxml +18 -18
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotResourcesWindow.uxml +18 -18
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotToolsWindow.uxml +18 -18
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/MainWindow.uxml +115 -115
- package/vendor/plugin/com.atelierai.unity.copilot/Plugins/ReflectorNet.dll +0 -0
- package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.Common.dll +0 -0
- package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.dll +0 -0
- package/vendor/plugin/com.atelierai.unity.copilot/README.md +88 -88
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/BufferedFileLogStorage.cs +208 -207
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/FileLogStorage.cs +3 -1
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/UnityLogCollector.cs +282 -280
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.cs +1 -1
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetExtractorDevelopmentDependencyTests.cs +148 -148
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetExtractorFlatLayoutTests.cs +166 -166
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetInstallManifestTests.cs +436 -436
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetLegacyMigrationTests.cs +1 -1
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetPackageInstallerStaleVersionCleanupTests.cs +200 -200
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Environment/EnvironmentUtilsTests.cs +1 -1
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/GameObject/TestJsonSchema.cs +199 -199
- package/vendor/plugin/com.atelierai.unity.copilot/package.json +1 -1
- /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
|
-
|
|
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.
|
|
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;
|