@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,88 +1,88 @@
1
- <div align="center">
2
- <h1>Unity Copilot</h1>
3
- <p>Drive the Unity Editor from any AI agent over plain REST + WebSocket — no MCP protocol on the wire.</p>
4
- <p><code>com.atelierai.unity.copilot</code></p>
5
- </div>
6
-
7
- Unity Copilot is a Unity Editor/Runtime plugin that exposes the Editor as an
8
- HTTP service: 160+ tools covering scenes, GameObjects, components, assets,
9
- prefabs, scripts, packages, screenshots, console, tests, and builds — plus
10
- prompts and resources. A self-hosted Node bridge connects the Editor to the
11
- outside world; the **uco** CLI drives the whole surface from any agent or
12
- terminal.
13
-
14
- ## Highlights
15
-
16
- - **160+ Editor tools** — authoring, code (including Roslyn compile-and-execute
17
- with confirmation gating for risky operations), diagnostics, build & tests,
18
- visuals, physics
19
- - **Three-surface AI skills** — `uco setup-skills` generates focused skill
20
- bundles (setup / lifecycle / live-editor) for Claude Code, Codex, Cursor,
21
- and other agents, refreshed from the live tool catalog
22
- - **Deterministic, safe connectivity** — one port per project (hashed from the
23
- project path), bearer-token auth, localhost by default, no vendor lock-in:
24
- anything that can speak HTTP or run a CLI can drive the Editor
25
- - **Durable calls** — async invocations return a call id queryable later
26
- (`uco call get <id>`); structured error envelopes survive the whole stack
27
- - **Dual-editor support** — gates run against Unity 2022.3 LTS and 6000.5
28
-
29
- ## Requirements
30
-
31
- - Unity **2022.3 or newer** (tested on 2022.3 LTS and 6000.5)
32
- - **Node.js 20+** for the bridge server (the plugin auto-discovers the `uco`
33
- npm package — global or project-local — or set `nodeServerPath` in the
34
- config to point at a specific install)
35
-
36
- ## Install
37
-
38
- ### Via the uco CLI (recommended)
39
-
40
- ```bash
41
- npm install -g @atelierai/uco
42
- uco install <path-to-your-unity-project>
43
- ```
44
-
45
- This embeds the plugin package, stages the framework DLLs, writes the initial
46
- config, and can generate agent skills and wrappers (`uco setup-skills`).
47
-
48
- ### Via OpenUPM
49
-
50
- Add the scoped registry to `Packages/manifest.json` and reference the package:
51
-
52
- ```json
53
- {
54
- "dependencies": {
55
- "com.atelierai.unity.copilot": "1.0.0"
56
- },
57
- "scopedRegistries": [
58
- {
59
- "name": "package.openupm.com",
60
- "url": "https://package.openupm.com",
61
- "scopes": [ "com.atelierai.unity.copilot" ]
62
- }
63
- ]
64
- }
65
- ```
66
-
67
- ## Quick start
68
-
69
- ```bash
70
- uco ping # health-check the bridge
71
- uco call unity-tool-list # list every tool the Editor exposes
72
- uco exec --method-body --return-type int --code 'return GameObject.FindObjectsOfType<Camera>().Length;'
73
- ```
74
-
75
- Configuration lives in `UserSettings/uco-config.json` (migrated automatically
76
- from the legacy filename by older→newer versions of the plugin).
77
-
78
- ## Wire protocol
79
-
80
- Plain HTTP + WebSocket with JSON envelopes (`/api/tools/...`). There is no MCP
81
- protocol on the wire — the bridge was deliberately stripped down to REST so
82
- any agent, script, or CI job can use it without an MCP SDK.
83
-
84
- ## Attribution & license
85
-
86
- Derived from [IvanMurzak/Unity-MCP](https://github.com/IvanMurzak/Unity-MCP)
87
- (Apache-2.0). Licensed under the Apache License 2.0 — see [LICENSE](LICENSE)
88
- for the dual copyright notice.
1
+ <div align="center">
2
+ <h1>Unity Copilot</h1>
3
+ <p>Drive the Unity Editor from any AI agent over plain REST + WebSocket — a single simple wire protocol on the wire.</p>
4
+ <p><code>com.atelierai.unity.copilot</code></p>
5
+ </div>
6
+
7
+ Unity Copilot is a Unity Editor/Runtime plugin that exposes the Editor as an
8
+ HTTP service: 160+ tools covering scenes, GameObjects, components, assets,
9
+ prefabs, scripts, packages, screenshots, console, tests, and builds — plus
10
+ prompts and resources. A self-hosted Node bridge connects the Editor to the
11
+ outside world; the **uco** CLI drives the whole surface from any agent or
12
+ terminal.
13
+
14
+ ## Highlights
15
+
16
+ - **160+ Editor tools** — authoring, code (including Roslyn compile-and-execute
17
+ with confirmation gating for risky operations), diagnostics, build & tests,
18
+ visuals, physics
19
+ - **Three-surface AI skills** — `uco setup-skills` generates focused skill
20
+ bundles (setup / lifecycle / live-editor) for Claude Code, Codex, Cursor,
21
+ and other agents, refreshed from the live tool catalog
22
+ - **Deterministic, safe connectivity** — one port per project (hashed from the
23
+ project path), bearer-token auth, localhost by default, no vendor lock-in:
24
+ anything that can speak HTTP or run a CLI can drive the Editor
25
+ - **Durable calls** — async invocations return a call id queryable later
26
+ (`uco call get <id>`); structured error envelopes survive the whole stack
27
+ - **Dual-editor support** — gates run against Unity 2022.3 LTS and 6000.5
28
+
29
+ ## Requirements
30
+
31
+ - Unity **2022.3 or newer** (tested on 2022.3 LTS and 6000.5)
32
+ - **Node.js 20+** for the bridge server (the plugin auto-discovers the `uco`
33
+ npm package — global or project-local — or set `nodeServerPath` in the
34
+ config to point at a specific install)
35
+
36
+ ## Install
37
+
38
+ ### Via the uco CLI (recommended)
39
+
40
+ ```bash
41
+ npm install -g @atelierai/uco
42
+ uco install <path-to-your-unity-project>
43
+ ```
44
+
45
+ This embeds the plugin package, stages the framework DLLs, writes the initial
46
+ config, and can generate agent skills and wrappers (`uco setup-skills`).
47
+
48
+ ### Via OpenUPM
49
+
50
+ Add the scoped registry to `Packages/manifest.json` and reference the package:
51
+
52
+ ```json
53
+ {
54
+ "dependencies": {
55
+ "com.atelierai.unity.copilot": "1.0.0"
56
+ },
57
+ "scopedRegistries": [
58
+ {
59
+ "name": "package.openupm.com",
60
+ "url": "https://package.openupm.com",
61
+ "scopes": [ "com.atelierai.unity.copilot" ]
62
+ }
63
+ ]
64
+ }
65
+ ```
66
+
67
+ ## Quick start
68
+
69
+ ```bash
70
+ uco ping # health-check the bridge
71
+ uco call unity-tool-list # list every tool the Editor exposes
72
+ uco exec --method-body --return-type int --code 'return GameObject.FindObjectsOfType<Camera>().Length;'
73
+ ```
74
+
75
+ Configuration lives in `UserSettings/uco-config.json` (migrated automatically
76
+ from the legacy filename by older→newer versions of the plugin).
77
+
78
+ ## Wire protocol
79
+
80
+ Plain HTTP + WebSocket with JSON envelopes (`/api/tools/...`). The wire is plain JSON
81
+ protocol on the wire — the bridge was deliberately stripped down to REST so
82
+ any agent, script, or CI job can drive it with nothing but an HTTP client.
83
+
84
+ ## Attribution & license
85
+
86
+ Derived from [IvanMurzak/Unity-MCP](https://github.com/IvanMurzak/Unity-MCP)
87
+ (Apache-2.0). Licensed under the Apache License 2.0 — see [LICENSE](LICENSE)
88
+ for the dual copyright notice.
@@ -1,207 +1,208 @@
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.Collections.Generic;
14
- using System.IO;
15
- using System.Linq;
16
- using System.Text.Json;
17
- using System.Threading.Tasks;
18
- using Microsoft.Extensions.Logging;
19
- using UnityEngine;
20
-
21
- namespace com.AtelierAI.Unity.Copilot
22
- {
23
- using ILogger = Microsoft.Extensions.Logging.ILogger;
24
- public class BufferedFileLogStorage : FileLogStorage
25
- {
26
- protected readonly int _flushEntriesThreshold;
27
- protected readonly LogEntry[] _logEntriesBuffer;
28
- protected int _logEntriesBufferLength;
29
-
30
- public BufferedFileLogStorage(
31
- ILogger? logger = null,
32
- int flushEntriesThreshold = 100,
33
- string? cacheFilePath = null,
34
- string? cacheFileName = null,
35
- int fileBufferSize = 4096,
36
- int maxFileSizeMB = DefaultMaxFileSizeMB,
37
- JsonSerializerOptions? jsonOptions = null)
38
- : base(logger, cacheFilePath, cacheFileName, fileBufferSize, maxFileSizeMB, jsonOptions)
39
- {
40
- if (flushEntriesThreshold <= 0)
41
- throw new ArgumentOutOfRangeException(nameof(flushEntriesThreshold), "Flush entries threshold must be greater than zero.");
42
-
43
- _flushEntriesThreshold = flushEntriesThreshold;
44
- _logEntriesBuffer = new LogEntry[flushEntriesThreshold];
45
- _logEntriesBufferLength = 0;
46
- }
47
-
48
- public override void Flush()
49
- {
50
- if (_isDisposed.Value)
51
- {
52
- _logger.LogWarning("{method} called but already disposed, ignored.",
53
- nameof(Flush));
54
- return;
55
- }
56
- lock (_fileMutex)
57
- {
58
- // Flush buffered entries to file
59
- if (_logEntriesBufferLength > 0)
60
- {
61
- var entriesToFlush = new LogEntry[_logEntriesBufferLength];
62
- Array.Copy(_logEntriesBuffer, entriesToFlush, _logEntriesBufferLength);
63
- base.AppendInternal(entriesToFlush);
64
- _logEntriesBufferLength = 0;
65
- }
66
- fileWriteStream?.Flush();
67
- }
68
- }
69
- public override Task FlushAsync()
70
- {
71
- if (_isDisposed.Value)
72
- {
73
- _logger.LogWarning("{method} called but already disposed, ignored.",
74
- nameof(FlushAsync));
75
- return Task.CompletedTask;
76
- }
77
- lock (_fileMutex)
78
- {
79
- // Flush buffered entries to file
80
- if (_logEntriesBufferLength > 0)
81
- {
82
- var entriesToFlush = new LogEntry[_logEntriesBufferLength];
83
- Array.Copy(_logEntriesBuffer, entriesToFlush, _logEntriesBufferLength);
84
- base.AppendInternal(entriesToFlush);
85
- _logEntriesBufferLength = 0;
86
- }
87
- fileWriteStream?.Flush();
88
- }
89
- return Task.CompletedTask;
90
- }
91
-
92
- protected override void AppendInternal(params LogEntry[] entries)
93
- {
94
- if (_isDisposed.Value)
95
- {
96
- _logger.LogWarning("{method} called but already disposed, ignored.",
97
- nameof(AppendInternal));
98
- return;
99
- }
100
- if (_logEntriesBufferLength >= _flushEntriesThreshold)
101
- {
102
- base.AppendInternal(_logEntriesBuffer);
103
- _logEntriesBufferLength = 0;
104
- }
105
- foreach (var entry in entries)
106
- {
107
- _logEntriesBuffer[_logEntriesBufferLength] = entry;
108
- _logEntriesBufferLength++;
109
-
110
- if (_logEntriesBufferLength >= _flushEntriesThreshold)
111
- {
112
- base.AppendInternal(_logEntriesBuffer);
113
- _logEntriesBufferLength = 0;
114
- }
115
- }
116
- }
117
-
118
- /// <summary>
119
- /// Closes and disposes the current file stream if open. Clears the log cache file.
120
- /// </summary>
121
- public override LogClearResult Clear()
122
- {
123
- if (_isDisposed.Value)
124
- {
125
- _logger.LogWarning("{method} called but already disposed, ignored.",
126
- nameof(Clear));
127
- return new LogClearResult
128
- {
129
- Ok = false,
130
- Strategy = "storage-disposed",
131
- Path = filePath,
132
- Error = "The log storage is disposed."
133
- };
134
- }
135
- lock (_fileMutex)
136
- {
137
- _logEntriesBufferLength = 0;
138
- return ClearFileLocked();
139
- }
140
- }
141
-
142
- public override LogEntry[] Query(
143
- int maxEntries = 100,
144
- LogType? logTypeFilter = null,
145
- bool includeStackTrace = false,
146
- int lastMinutes = 0)
147
- {
148
- if (_isDisposed.Value)
149
- {
150
- _logger.LogWarning("{method} called but already disposed, ignored.",
151
- nameof(Query));
152
- return Array.Empty<LogEntry>();
153
- }
154
- lock (_fileMutex)
155
- {
156
- return QueryInternal(maxEntries, logTypeFilter, includeStackTrace, lastMinutes);
157
- }
158
- }
159
-
160
- protected override LogEntry[] QueryInternal(
161
- int maxEntries = 100,
162
- LogType? logTypeFilter = null,
163
- bool includeStackTrace = false,
164
- int lastMinutes = 0)
165
- {
166
- var result = new List<LogEntry>();
167
- var cutoffTime = lastMinutes > 0
168
- ? System.DateTime.Now.AddMinutes(-lastMinutes)
169
- : System.DateTime.MinValue;
170
-
171
- // 1. Get from buffer (Newest are at the end of buffer)
172
- for (int i = _logEntriesBufferLength - 1; i >= 0; i--)
173
- {
174
- var entry = _logEntriesBuffer[i];
175
- if (logTypeFilter.HasValue && entry.LogType != logTypeFilter.Value)
176
- continue;
177
-
178
- if (lastMinutes > 0)
179
- {
180
- if (entry.Timestamp < cutoffTime)
181
- {
182
- return result.AsEnumerable().Reverse().ToArray();
183
- }
184
- }
185
-
186
- result.Add(entry);
187
- if (result.Count >= maxEntries)
188
- return result.AsEnumerable().Reverse().ToArray();
189
- }
190
-
191
- // 2. Exit if we already have enough entries
192
- var neededLogsCount = maxEntries - result.Count;
193
- if (neededLogsCount <= 0)
194
- return result.AsEnumerable().Reverse().ToArray();
195
-
196
- result.Reverse();
197
-
198
- // 3. Get from file
199
- var fileEntries = base.QueryInternal(neededLogsCount, logTypeFilter, includeStackTrace, lastMinutes);
200
- result.AddRange(fileEntries);
201
-
202
- return result.ToArray();
203
- }
204
-
205
- ~BufferedFileLogStorage() => Dispose();
206
- }
207
- }
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.Collections.Generic;
14
+ using System.IO;
15
+ using System.Linq;
16
+ using System.Text.Json;
17
+ using System.Threading.Tasks;
18
+ using Microsoft.Extensions.Logging;
19
+ using UnityEngine;
20
+
21
+ namespace com.AtelierAI.Unity.Copilot
22
+ {
23
+ using ILogger = Microsoft.Extensions.Logging.ILogger;
24
+ public class BufferedFileLogStorage : FileLogStorage
25
+ {
26
+ protected readonly int _flushEntriesThreshold;
27
+ protected readonly LogEntry[] _logEntriesBuffer;
28
+ protected int _logEntriesBufferLength;
29
+
30
+ public BufferedFileLogStorage(
31
+ ILogger? logger = null,
32
+ int flushEntriesThreshold = 100,
33
+ string? cacheFilePath = null,
34
+ string? cacheFileName = null,
35
+ int fileBufferSize = 4096,
36
+ int maxFileSizeMB = DefaultMaxFileSizeMB,
37
+ JsonSerializerOptions? jsonOptions = null)
38
+ : base(logger, cacheFilePath, cacheFileName, fileBufferSize, maxFileSizeMB, jsonOptions)
39
+ {
40
+ if (flushEntriesThreshold <= 0)
41
+ throw new ArgumentOutOfRangeException(nameof(flushEntriesThreshold), "Flush entries threshold must be greater than zero.");
42
+
43
+ _flushEntriesThreshold = flushEntriesThreshold;
44
+ _logEntriesBuffer = new LogEntry[flushEntriesThreshold];
45
+ _logEntriesBufferLength = 0;
46
+ }
47
+
48
+ public override void Flush()
49
+ {
50
+ if (_isDisposed.Value)
51
+ {
52
+ _logger.LogWarning("{method} called but already disposed, ignored.",
53
+ nameof(Flush));
54
+ return;
55
+ }
56
+ lock (_fileMutex)
57
+ {
58
+ // Flush buffered entries to file
59
+ if (_logEntriesBufferLength > 0)
60
+ {
61
+ var entriesToFlush = new LogEntry[_logEntriesBufferLength];
62
+ Array.Copy(_logEntriesBuffer, entriesToFlush, _logEntriesBufferLength);
63
+ base.AppendInternal(entriesToFlush);
64
+ _logEntriesBufferLength = 0;
65
+ }
66
+ fileWriteStream?.Flush();
67
+ }
68
+ }
69
+ public override Task FlushAsync()
70
+ {
71
+ if (_isDisposed.Value)
72
+ {
73
+ _logger.LogWarning("{method} called but already disposed, ignored.",
74
+ nameof(FlushAsync));
75
+ return Task.CompletedTask;
76
+ }
77
+ lock (_fileMutex)
78
+ {
79
+ // Flush buffered entries to file
80
+ if (_logEntriesBufferLength > 0)
81
+ {
82
+ var entriesToFlush = new LogEntry[_logEntriesBufferLength];
83
+ Array.Copy(_logEntriesBuffer, entriesToFlush, _logEntriesBufferLength);
84
+ base.AppendInternal(entriesToFlush);
85
+ _logEntriesBufferLength = 0;
86
+ }
87
+ fileWriteStream?.Flush();
88
+ }
89
+ return Task.CompletedTask;
90
+ }
91
+
92
+ protected override void AppendInternal(params LogEntry[] entries)
93
+ {
94
+ if (_isDisposed.Value)
95
+ {
96
+ _logger.LogWarning("{method} called but already disposed, ignored.",
97
+ nameof(AppendInternal));
98
+ return;
99
+ }
100
+ if (_logEntriesBufferLength >= _flushEntriesThreshold)
101
+ {
102
+ base.AppendInternal(_logEntriesBuffer);
103
+ _logEntriesBufferLength = 0;
104
+ }
105
+ foreach (var entry in entries)
106
+ {
107
+ _logEntriesBuffer[_logEntriesBufferLength] = entry;
108
+ _logEntriesBufferLength++;
109
+
110
+ if (_logEntriesBufferLength >= _flushEntriesThreshold)
111
+ {
112
+ base.AppendInternal(_logEntriesBuffer);
113
+ _logEntriesBufferLength = 0;
114
+ }
115
+ }
116
+ }
117
+
118
+ /// <summary>
119
+ /// Closes and disposes the current file stream if open. Clears the log cache file.
120
+ /// </summary>
121
+ public override LogClearResult Clear()
122
+ {
123
+ if (_isDisposed.Value)
124
+ {
125
+ _logger.LogWarning("{method} called but already disposed, ignored.",
126
+ nameof(Clear));
127
+ return new LogClearResult
128
+ {
129
+ Ok = false,
130
+ Strategy = "storage-disposed",
131
+ Path = filePath,
132
+ Error = "The log storage is disposed."
133
+ };
134
+ }
135
+ lock (_fileMutex)
136
+ {
137
+ _logEntriesBufferLength = 0;
138
+ return ClearFileLocked();
139
+ }
140
+ }
141
+
142
+ public override LogEntry[] Query(
143
+ int maxEntries = 100,
144
+ LogType? logTypeFilter = null,
145
+ bool includeStackTrace = false,
146
+ int lastMinutes = 0)
147
+ {
148
+ if (_isDisposed.Value)
149
+ {
150
+ _logger.LogWarning("{method} called but already disposed, ignored.",
151
+ nameof(Query));
152
+ return Array.Empty<LogEntry>();
153
+ }
154
+ lock (_fileMutex)
155
+ {
156
+ return QueryInternal(maxEntries, logTypeFilter, includeStackTrace, lastMinutes);
157
+ }
158
+ }
159
+
160
+ protected override LogEntry[] QueryInternal(
161
+ int maxEntries = 100,
162
+ LogType? logTypeFilter = null,
163
+ bool includeStackTrace = false,
164
+ int lastMinutes = 0)
165
+ {
166
+ var result = new List<LogEntry>();
167
+ var cutoffTime = lastMinutes > 0
168
+ ? System.DateTime.Now.AddMinutes(-lastMinutes)
169
+ : System.DateTime.MinValue;
170
+
171
+ // 1. Get from buffer (Newest are at the end of buffer)
172
+ for (int i = _logEntriesBufferLength - 1; i >= 0; i--)
173
+ {
174
+ var entry = _logEntriesBuffer[i];
175
+ if (logTypeFilter.HasValue && entry.LogType != logTypeFilter.Value)
176
+ continue;
177
+
178
+ if (lastMinutes > 0)
179
+ {
180
+ if (entry.Timestamp < cutoffTime)
181
+ {
182
+ return result.AsEnumerable().Reverse().ToArray();
183
+ }
184
+ }
185
+
186
+ result.Add(entry);
187
+ if (result.Count >= maxEntries)
188
+ return result.AsEnumerable().Reverse().ToArray();
189
+ }
190
+
191
+ // 2. Exit if we already have enough entries
192
+ var neededLogsCount = maxEntries - result.Count;
193
+ if (neededLogsCount <= 0)
194
+ return result.AsEnumerable().Reverse().ToArray();
195
+
196
+ result.Reverse();
197
+
198
+ // 3. Get from file
199
+ var fileEntries = base.QueryInternal(neededLogsCount, logTypeFilter, includeStackTrace, lastMinutes);
200
+ result.AddRange(fileEntries);
201
+
202
+ return result.ToArray();
203
+ }
204
+
205
+ // No finalizer: Dispose is plain managed cleanup; running arbitrary code on
206
+ // the finalizer thread during domain unload is never safe.
207
+ }
208
+ }
@@ -542,6 +542,8 @@ namespace com.AtelierAI.Unity.Copilot
542
542
  GC.SuppressFinalize(this);
543
543
  }
544
544
 
545
- ~FileLogStorage() => Dispose();
545
+ // No finalizer: Dispose performs file I/O / Unity static-event unsubscription,
546
+ // which must never run on the finalizer thread during domain unload
547
+ // (same crash class as uco-domain-reload-crash-20260919).
546
548
  }
547
549
  }