@atelierai/uco 1.0.1 → 1.0.3

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 (57) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +3 -3
  3. package/dist/codegen/emit.js +1 -1
  4. package/dist/codegen/fetch.js +1 -1
  5. package/dist/codegen/fetch.js.map +1 -1
  6. package/dist/commands/devops/login.js +1 -1
  7. package/dist/commands/devops/login.js.map +1 -1
  8. package/dist/commands/devops/setup-mcp.js +1 -1
  9. package/dist/commands/devops/setup-mcp.js.map +1 -1
  10. package/dist/commands/gen.js +2 -2
  11. package/dist/commands/gen.js.map +1 -1
  12. package/dist/commands/init.js +31 -10
  13. package/dist/commands/init.js.map +1 -1
  14. package/dist/commands/ping.js +2 -2
  15. package/dist/commands/ping.js.map +1 -1
  16. package/dist/config/resolve.js +1 -1
  17. package/dist/config/resolve.js.map +1 -1
  18. package/dist/devops/utils/agents.js +1 -1
  19. package/dist/devops/utils/agents.js.map +1 -1
  20. package/dist/generated/tools.js +1 -1
  21. package/dist/generated/tools.js.map +1 -1
  22. package/docs/INSTALL.zh-CN.md +1 -1
  23. package/package.json +13 -4
  24. package/skills/uco-setup/SKILL.md +2 -2
  25. package/skills/uco-setup/references/recovery.md +26 -2
  26. package/skills/unity-editor/SKILL.md +1 -1
  27. package/vendor/plugin/com.atelierai.unity.copilot/CHANGELOG.md +48 -15
  28. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetResolverMenu.cs +3 -1
  29. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Branding/ProductInfo.cs +4 -5
  30. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/CopilotServerManager.cs +32 -42
  31. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Skills/Skill_InitialSetup.cs +1 -1
  32. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/MenuItems.cs +7 -66
  33. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Overlay/OpenWindowButton.cs +2 -2
  34. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/UnityCopilotProjectSettingsProvider.cs +5 -5
  35. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.Connection.cs +177 -409
  36. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs +3 -10
  37. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.cs +2 -71
  38. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/UpdatePopupWindow.cs +2 -2
  39. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Config.cs +6 -1
  40. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Static.cs +9 -12
  41. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.cs +1 -1
  42. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolsManifestGenerator.cs +1 -1
  43. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityCopilotUpdateProjectSettings.cs +2 -2
  44. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UpdateChecker.cs +3 -3
  45. package/vendor/plugin/com.atelierai.unity.copilot/README.md +2 -2
  46. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Config.cs +11 -62
  47. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.cs +1 -1
  48. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Utils/EnvironmentUtils.cs +7 -25
  49. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Environment/EnvironmentUtilsTests.cs +260 -320
  50. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Tool/ToolSetEnabledStateTests.cs +1 -1
  51. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/UI/MainWindowEditor.StatusLogicTests.cs +0 -125
  52. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/UI/SceneViewToolbarOverlayTests.cs +1 -1
  53. package/vendor/plugin/com.atelierai.unity.copilot/package.json +3 -3
  54. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Services/DeviceAuthFlow.cs +0 -133
  55. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Services/DeviceAuthFlow.cs.meta +0 -11
  56. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/CopilotServerManagerAutoStartTests.cs +0 -43
  57. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/CopilotServerManagerAutoStartTests.cs.meta +0 -11
@@ -1,409 +1,177 @@
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 com.IvanMurzak.ReflectorNet.Utils;
14
- using com.AtelierAI.Unity.Copilot.Editor.Services;
15
- using com.AtelierAI.Unity.Copilot.Editor.UI.Controls;
16
- using com.AtelierAI.Uco.Framework;
17
- using R3;
18
- using UnityEngine;
19
- using UnityEngine.UIElements;
20
- using static com.AtelierAI.Uco.Framework.Common.Consts.MCP.Server;
21
-
22
- namespace com.AtelierAI.Unity.Copilot.Editor.UI
23
- {
24
- public partial class MainWindowEditor
25
- {
26
- private TextField? _inputFieldHost;
27
- private VisualElement? _connectionStatusCircle;
28
- private Label? _connectionStatusText;
29
- private readonly SerialDisposable _authRejectedSubscription = new();
30
-
31
- private void SetupConnectionSection(VisualElement root)
32
- {
33
- _inputFieldHost = root.Q<TextField>("InputServerURL");
34
- var btnConnect = root.Q<Button>("btnConnectOrDisconnect");
35
- _connectionStatusCircle = root.Q<VisualElement>("connectionStatusCircle");
36
- _connectionStatusText = root.Q<Label>("connectionStatusText");
37
-
38
- _btnConnect = btnConnect;
39
- _timelinePointUnity = root.Q<VisualElement>("TimelinePointUnity");
40
- if (_timelinePointUnity != null)
41
- _timelinePointUnity.tooltip = Tooltip_UnityTimelineLabel;
42
- if (_connectionStatusCircle != null)
43
- _connectionStatusCircle.tooltip = Tooltip_UnityTimelineLabel;
44
- if (_connectionStatusText != null)
45
- _connectionStatusText.tooltip = Tooltip_UnityTimelineLabel;
46
-
47
- _aiAgentLabelsContainer = root.Q<VisualElement>("aiAgentLabelsContainer");
48
- _aiAgentStatusCircle = root.Q<VisualElement>("aiAgentStatusCircle");
49
-
50
- var timelinePointAiAgent = root.Q<VisualElement>("TimelinePointAiAgent");
51
- if (timelinePointAiAgent != null)
52
- timelinePointAiAgent.tooltip = Tooltip_AiAgentTimelineLabel;
53
-
54
- _inputFieldHost.value = UnityCopilotPluginEditor.LocalHost;
55
- _inputFieldHost.RegisterCallback<FocusOutEvent>(evt =>
56
- {
57
- var newValue = _inputFieldHost.value;
58
- if (UnityCopilotPluginEditor.LocalHost == newValue)
59
- return;
60
-
61
- UnityCopilotPluginEditor.LocalHost = newValue;
62
- SaveChanges($"[{nameof(MainWindowEditor)}] Host Changed: {newValue}");
63
- Invalidate();
64
-
65
- UnityCopilotPluginEditor.Instance.DisposeMcpPluginInstance();
66
- UnityBuildAndConnect();
67
- });
68
-
69
- SubscribeToConnectionState((state, keepConnected) =>
70
- {
71
- UpdateConnectionUI(state, keepConnected);
72
- });
73
-
74
- UnityCopilotPluginEditor.PluginProperty
75
- .WhereNotNull()
76
- .Subscribe(plugin =>
77
- {
78
- _authRejectedSubscription.Disposable = plugin.OnAuthorizationRejected
79
- .ObserveOnCurrentSynchronizationContext()
80
- .Subscribe(_ => OnAuthorizationRejected());
81
- })
82
- .AddTo(_disposables);
83
-
84
- btnConnect.RegisterCallback<ClickEvent>(evt => HandleConnectButton(btnConnect.text));
85
- }
86
-
87
- private void OnAuthorizationRejected()
88
- {
89
- if (UnityCopilotPluginEditor.ConnectionMode != ConnectionMode.Cloud)
90
- return;
91
-
92
- Debug.LogWarning("[Unity Co-Pilot] The server rejected the authorization token. " +
93
- "The token has been cleared. Please click 'Authorize' to obtain a new token.");
94
-
95
- UnityCopilotPluginEditor.CloudToken = null;
96
- UnityCopilotPluginEditor.Instance.Save();
97
-
98
- UpdateCloudAuthState();
99
- RefreshConnectionUI();
100
- }
101
-
102
- private void UpdateConnectionUI(ConnectionState state, bool keepConnected)
103
- {
104
- if (_inputFieldHost == null || _connectionStatusText == null
105
- || _btnConnect == null || _connectionStatusCircle == null)
106
- return;
107
-
108
- UpdateHostFieldState(_inputFieldHost, keepConnected, state);
109
- _connectionStatusText.text = "Unity: " + GetConnectionStatusText(state, keepConnected);
110
- _btnConnect.text = GetButtonText(state, keepConnected);
111
- var isConnect = _btnConnect.text == ServerButtonText_Connect;
112
- _btnConnect.EnableInClassList("btn-primary", isConnect);
113
- _btnConnect.EnableInClassList("btn-secondary", !isConnect);
114
- SetStatusIndicator(_connectionStatusCircle, GetConnectionStatusClass(state, keepConnected));
115
-
116
- if (!(state == ConnectionState.Connected && keepConnected))
117
- SetAiAgentStatus(false);
118
-
119
- UpdateCloudAuthState();
120
- }
121
-
122
- /// <summary>
123
- /// Reads the current connection state and refreshes the Unity connection row UI.
124
- /// Call this whenever the UI might be stale (e.g. after mode switch).
125
- /// </summary>
126
- private void RefreshConnectionUI()
127
- {
128
- var state = UnityCopilotPluginEditor.ConnectionState.CurrentValue;
129
- var keepConnected = UnityCopilotPluginEditor.KeepConnected;
130
- UpdateConnectionUI(state, keepConnected);
131
- }
132
-
133
- /// <summary>
134
- /// Schedules a delayed <see cref="RefreshConnectionUI"/> to catch state changes
135
- /// that arrive after a mode switch or reconnect (e.g. async SignalR handshake).
136
- /// </summary>
137
- private void ScheduleConnectionUIRefresh()
138
- {
139
- rootVisualElement?.schedule.Execute(() => RefreshConnectionUI()).ExecuteLater(500);
140
- rootVisualElement?.schedule.Execute(() => RefreshConnectionUI()).ExecuteLater(2000);
141
- }
142
-
143
- internal static bool IsHostFieldReadOnly(bool keepConnected, ConnectionState state) =>
144
- keepConnected || state != ConnectionState.Disconnected;
145
-
146
- private static void UpdateHostFieldState(TextField field, bool keepConnected, ConnectionState state)
147
- {
148
- var isReadOnly = IsHostFieldReadOnly(keepConnected, state);
149
- field.isReadOnly = isReadOnly;
150
- var defaultUrl = $"http://localhost:{UnityCopilotPlugin.GeneratePortFromDirectory()}";
151
- field.tooltip = keepConnected
152
- ? "Editable only when Unity disconnected from the server."
153
- : $"Usually the server is hosted locally at {defaultUrl}. Feel free to connect to a remote server if needed. The connection is established using WebSocket.";
154
-
155
- field.EnableInClassList("disabled-text-field", isReadOnly);
156
- field.EnableInClassList("enabled-text-field", !isReadOnly);
157
- }
158
-
159
- private void HandleConnectButton(string buttonText)
160
- {
161
- if (buttonText.Equals(ServerButtonText_Connect, StringComparison.OrdinalIgnoreCase))
162
- {
163
- ConnectToServer();
164
- }
165
- else
166
- {
167
- UnityCopilotPluginEditor.KeepConnected = false;
168
- UnityCopilotPluginEditor.Instance.Save();
169
- if (UnityCopilotPluginEditor.Instance.HasMcpPluginInstance)
170
- _ = UnityCopilotPluginEditor.Instance.Disconnect();
171
- }
172
- ScheduleConnectionUIRefresh();
173
- }
174
-
175
- /// <summary>
176
- /// Initiates connection to the server. Called by both the Connect button and the
177
- /// connection alert panel's Connect button.
178
- /// </summary>
179
- private static void ConnectToServer()
180
- {
181
- UnityCopilotPluginEditor.KeepConnected = true;
182
- UnityCopilotPluginEditor.Instance.Save();
183
- UnityBuildAndConnect();
184
- }
185
-
186
- private void SetupConnectionModeToggle(VisualElement root)
187
- {
188
- var container = root.Q<VisualElement>("segmentConnectionMode");
189
- if (container == null) return;
190
-
191
- var control = new SegmentedControl("Custom", "Cloud");
192
- control.SetTooltips(
193
- "Connect to your own server. The plugin starts a local server automatically and manages its lifecycle. Use this when you want full control over the server configuration, port, and authorization settings.",
194
- "Connect to a remote server hosted in the cloud (e.g. ai-game.dev). No local server is started — the plugin connects directly to a built-in cloud endpoint (Cloud URL is predefined and not configurable). Requires authorization via device code flow.");
195
- container.Add(control);
196
-
197
- var inputServerUrl = root.Q<TextField>("InputServerURL");
198
- var mcpServerPoint = root.Q<VisualElement>("TimelinePointMcpServer");
199
- var cloudAuthSection = root.Q<VisualElement>("cloudAuthSection");
200
-
201
- void UpdateModeVisibility(ConnectionMode mode)
202
- {
203
- var isCustom = mode == ConnectionMode.Custom;
204
- if (inputServerUrl != null) inputServerUrl.style.display = isCustom ? DisplayStyle.Flex : DisplayStyle.None;
205
- if (mcpServerPoint != null) mcpServerPoint.style.display = isCustom ? DisplayStyle.Flex : DisplayStyle.None;
206
- if (cloudAuthSection != null) cloudAuthSection.style.display = isCustom ? DisplayStyle.None : DisplayStyle.Flex;
207
- }
208
-
209
- var currentMode = UnityCopilotPluginEditor.ConnectionMode;
210
- control.SetValueWithoutNotify(currentMode == ConnectionMode.Custom ? 0 : 1);
211
- UpdateModeVisibility(currentMode);
212
-
213
- control.RegisterCallback<ChangeEvent<int>>(evt =>
214
- {
215
- if (evt.newValue == 0)
216
- {
217
- UnityCopilotPluginEditor.ConnectionMode = ConnectionMode.Custom;
218
- UnityCopilotPluginEditor.Instance.Save();
219
- UpdateModeVisibility(ConnectionMode.Custom);
220
- UpdateCloudAuthState();
221
-
222
- // Start local server if configured and reconnect to it
223
- CopilotServerManager.StartServerIfNeeded();
224
- ReconnectAfterModeSwitch();
225
- ScheduleConnectionUIRefresh();
226
- }
227
- else
228
- {
229
- UnityCopilotPluginEditor.ConnectionMode = ConnectionMode.Cloud;
230
-
231
- // Cloud requires authorization
232
- UnityCopilotPluginEditor.AuthOption = AuthOption.required;
233
-
234
- UnityCopilotPluginEditor.Instance.Save();
235
- UpdateModeVisibility(ConnectionMode.Cloud);
236
- UpdateCloudAuthState();
237
-
238
- // Stop local server — not needed in Cloud mode
239
- if (CopilotServerManager.IsRunning || CopilotServerManager.IsStarting)
240
- CopilotServerManager.StopServer();
241
-
242
- // Reconnect to cloud server (only if authorized)
243
- if (!string.IsNullOrEmpty(UnityCopilotPluginEditor.CloudToken))
244
- ReconnectAfterModeSwitch();
245
- ScheduleConnectionUIRefresh();
246
- }
247
- });
248
- }
249
-
250
- internal static bool IsAuthFlowRunning(DeviceAuthFlowState state) =>
251
- state == DeviceAuthFlowState.Initiating
252
- || state == DeviceAuthFlowState.WaitingForUser
253
- || state == DeviceAuthFlowState.Polling;
254
-
255
- internal static string GetAuthFlowStatusMessage(DeviceAuthFlowState state, string? userCode, string? errorMessage) => state switch
256
- {
257
- DeviceAuthFlowState.Initiating => "Initiating...",
258
- DeviceAuthFlowState.WaitingForUser => $"Code: {userCode} — Authorize in browser",
259
- DeviceAuthFlowState.Polling => $"Code: {userCode} — Waiting for authorization...",
260
- DeviceAuthFlowState.Authorized => "Authorized!",
261
- DeviceAuthFlowState.Failed => $"Failed: {errorMessage}",
262
- DeviceAuthFlowState.Expired => "Expired — try again",
263
- DeviceAuthFlowState.Cancelled => "Cancelled",
264
- _ => ""
265
- };
266
-
267
- private void SetupCloudAuthSection(VisualElement root)
268
- {
269
- var inputCloudToken = root.Q<TextField>("inputCloudToken");
270
- var btnRevoke = root.Q<Button>("btnCloudRevoke");
271
- var btnAuthorize = root.Q<Button>("btnCloudAuthorize");
272
- var statusLabel = root.Q<Label>("labelCloudAuthStatus");
273
- if (inputCloudToken == null || btnAuthorize == null) return;
274
-
275
- _btnAuthorize = btnAuthorize;
276
-
277
- inputCloudToken.isPasswordField = true;
278
- inputCloudToken.RegisterCallback<KeyDownEvent>(evt =>
279
- {
280
- if (evt.keyCode == KeyCode.C && (evt.ctrlKey || evt.commandKey))
281
- {
282
- GUIUtility.systemCopyBuffer = inputCloudToken.value;
283
- evt.StopPropagation();
284
- }
285
- });
286
-
287
- const string tokenPlaceholder = "Token — press Authorize";
288
- void SetTokenValue(string? token)
289
- {
290
- var isEmpty = string.IsNullOrEmpty(token);
291
- inputCloudToken.value = isEmpty ? tokenPlaceholder : token!;
292
- inputCloudToken.EnableInClassList("token-placeholder", isEmpty);
293
- }
294
-
295
- SetTokenValue(UnityCopilotPluginEditor.CloudToken);
296
- UpdateCloudAuthState();
297
-
298
- void UpdateRevokeButtonVisibility()
299
- {
300
- if (btnRevoke != null)
301
- btnRevoke.style.display = string.IsNullOrEmpty(UnityCopilotPluginEditor.CloudToken)
302
- ? DisplayStyle.None
303
- : DisplayStyle.Flex;
304
- }
305
- UpdateRevokeButtonVisibility();
306
-
307
- btnRevoke?.RegisterCallback<ClickEvent>(evt =>
308
- {
309
- UnityCopilotPluginEditor.CloudToken = null;
310
- UnityCopilotPluginEditor.Instance.Save();
311
- SetTokenValue(null);
312
- UpdateRevokeButtonVisibility();
313
-
314
- if (statusLabel != null)
315
- {
316
- statusLabel.text = "Token revoked.";
317
- statusLabel.style.display = DisplayStyle.Flex;
318
- }
319
-
320
- UpdateCloudAuthState();
321
-
322
- // Disconnect if currently in Cloud mode
323
- if (UnityCopilotPluginEditor.ConnectionMode == ConnectionMode.Cloud
324
- && UnityCopilotPluginEditor.Instance.HasMcpPluginInstance)
325
- _ = UnityCopilotPluginEditor.Instance.Disconnect();
326
- });
327
-
328
- _startAuthorizeAction = async () =>
329
- {
330
- // If currently running, cancel
331
- if (_deviceAuthFlow != null && IsAuthFlowRunning(_deviceAuthFlow.State))
332
- {
333
- _deviceAuthFlow.Cancel();
334
- return;
335
- }
336
-
337
- _deviceAuthFlow?.Cancel();
338
- _deviceAuthFlow = new DeviceAuthFlow();
339
- var capturedFlow = _deviceAuthFlow; // Capture to avoid stale field reference in async callbacks
340
-
341
- capturedFlow.OnStateChanged += state =>
342
- {
343
- // Use RunAsync (EditorApplication.update-based) instead of delayCall so that
344
- // the UI updates even when the Unity Editor window is not focused — delayCall
345
- // is throttled/paused when Unity loses application focus.
346
- MainThread.Instance.RunAsync(() =>
347
- {
348
- // Ignore stale events from a previous auth flow
349
- if (_deviceAuthFlow != capturedFlow) return;
350
-
351
- if (statusLabel != null)
352
- {
353
- statusLabel.text = GetAuthFlowStatusMessage(state, capturedFlow.UserCode, capturedFlow.ErrorMessage);
354
- statusLabel.style.display = string.IsNullOrEmpty(statusLabel.text)
355
- ? DisplayStyle.None
356
- : DisplayStyle.Flex;
357
- }
358
- if (state == DeviceAuthFlowState.Authorized && inputCloudToken != null)
359
- {
360
- SetTokenValue(UnityCopilotPluginEditor.CloudToken);
361
- UpdateRevokeButtonVisibility();
362
- UpdateCloudAuthState();
363
- }
364
- if (state == DeviceAuthFlowState.Authorized)
365
- {
366
- // Reconnect to cloud server with the new token (only if still in Cloud mode)
367
- if (UnityCopilotPluginEditor.ConnectionMode == ConnectionMode.Cloud)
368
- ReconnectAfterModeSwitch();
369
- }
370
- if (btnAuthorize != null)
371
- {
372
- btnAuthorize.text = IsAuthFlowRunning(state) ? "Cancel" : "Authorize";
373
- }
374
- Repaint();
375
- });
376
- };
377
-
378
- await capturedFlow.StartAsync(UnityCopilotPlugin.UnityConnectionConfig.CloudServerBaseUrl, "Unity Editor");
379
- };
380
-
381
- btnAuthorize.RegisterCallback<ClickEvent>(_ => _startAuthorizeAction?.Invoke());
382
- }
383
-
384
- private void SetupConnectionAlerts(VisualElement root)
385
- {
386
- var container = root.Q<VisualElement>("connectionAlertContainer");
387
- if (container == null) return;
388
-
389
- // Auth alert — shown when Cloud mode is active but no token
390
- _connectionAuthAlert = new AlertPanel(
391
- "Authorization Required",
392
- "Cloud mode requires authentication to connect. Press the button below to authorize your device."
393
- );
394
- _connectionAuthAlert.SetButton("Authorize", () => _startAuthorizeAction?.Invoke());
395
- container.Add(_connectionAuthAlert.Root);
396
-
397
- // Connect alert — shown when authorized but Unity is not connected
398
- _connectionConnectAlert = new AlertPanel(
399
- "Connection Required",
400
- "Cloud authorization is complete but Unity is not connected to the server."
401
- );
402
- _connectionConnectAlert.SetButton("Connect", ConnectToServer);
403
- container.Add(_connectionConnectAlert.Root);
404
-
405
- // Initial visibility
406
- UpdateCloudAuthState();
407
- }
408
- }
409
- }
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 com.IvanMurzak.ReflectorNet.Utils;
14
+ using com.AtelierAI.Unity.Copilot.Editor.UI.Controls;
15
+ using com.AtelierAI.Uco.Framework;
16
+ using R3;
17
+ using UnityEngine;
18
+ using UnityEngine.UIElements;
19
+ using static com.AtelierAI.Uco.Framework.Common.Consts.MCP.Server;
20
+
21
+ namespace com.AtelierAI.Unity.Copilot.Editor.UI
22
+ {
23
+ public partial class MainWindowEditor
24
+ {
25
+ private TextField? _inputFieldHost;
26
+ private VisualElement? _connectionStatusCircle;
27
+ private Label? _connectionStatusText;
28
+ private readonly SerialDisposable _authRejectedSubscription = new();
29
+
30
+ private void SetupConnectionSection(VisualElement root)
31
+ {
32
+ _inputFieldHost = root.Q<TextField>("InputServerURL");
33
+ var btnConnect = root.Q<Button>("btnConnectOrDisconnect");
34
+ _connectionStatusCircle = root.Q<VisualElement>("connectionStatusCircle");
35
+ _connectionStatusText = root.Q<Label>("connectionStatusText");
36
+
37
+ _btnConnect = btnConnect;
38
+ _timelinePointUnity = root.Q<VisualElement>("TimelinePointUnity");
39
+ if (_timelinePointUnity != null)
40
+ _timelinePointUnity.tooltip = Tooltip_UnityTimelineLabel;
41
+ if (_connectionStatusCircle != null)
42
+ _connectionStatusCircle.tooltip = Tooltip_UnityTimelineLabel;
43
+ if (_connectionStatusText != null)
44
+ _connectionStatusText.tooltip = Tooltip_UnityTimelineLabel;
45
+
46
+ _aiAgentLabelsContainer = root.Q<VisualElement>("aiAgentLabelsContainer");
47
+ _aiAgentStatusCircle = root.Q<VisualElement>("aiAgentStatusCircle");
48
+
49
+ var timelinePointAiAgent = root.Q<VisualElement>("TimelinePointAiAgent");
50
+ if (timelinePointAiAgent != null)
51
+ timelinePointAiAgent.tooltip = Tooltip_AiAgentTimelineLabel;
52
+
53
+ _inputFieldHost.value = UnityCopilotPluginEditor.LocalHost;
54
+ _inputFieldHost.RegisterCallback<FocusOutEvent>(evt =>
55
+ {
56
+ var newValue = _inputFieldHost.value;
57
+ if (UnityCopilotPluginEditor.LocalHost == newValue)
58
+ return;
59
+
60
+ UnityCopilotPluginEditor.LocalHost = newValue;
61
+ SaveChanges($"[{nameof(MainWindowEditor)}] Host Changed: {newValue}");
62
+ Invalidate();
63
+
64
+ UnityCopilotPluginEditor.Instance.DisposeMcpPluginInstance();
65
+ UnityBuildAndConnect();
66
+ });
67
+
68
+ SubscribeToConnectionState((state, keepConnected) =>
69
+ {
70
+ UpdateConnectionUI(state, keepConnected);
71
+ });
72
+
73
+ UnityCopilotPluginEditor.PluginProperty
74
+ .WhereNotNull()
75
+ .Subscribe(plugin =>
76
+ {
77
+ _authRejectedSubscription.Disposable = plugin.OnAuthorizationRejected
78
+ .ObserveOnCurrentSynchronizationContext()
79
+ .Subscribe(_ => OnAuthorizationRejected());
80
+ })
81
+ .AddTo(_disposables);
82
+
83
+ btnConnect.RegisterCallback<ClickEvent>(evt => HandleConnectButton(btnConnect.text));
84
+ }
85
+
86
+ private void OnAuthorizationRejected()
87
+ {
88
+ Debug.LogWarning("[Unity Copilot] The server rejected the authorization token. " +
89
+ "The token has been cleared. Update the token (or regenerate it) and reconnect.");
90
+
91
+ UnityCopilotPluginEditor.LocalToken = null;
92
+ UnityCopilotPluginEditor.Instance.Save();
93
+ RefreshConnectionUI();
94
+ }
95
+
96
+ private void UpdateConnectionUI(ConnectionState state, bool keepConnected)
97
+ {
98
+ if (_inputFieldHost == null || _connectionStatusText == null
99
+ || _btnConnect == null || _connectionStatusCircle == null)
100
+ return;
101
+
102
+ UpdateHostFieldState(_inputFieldHost, keepConnected, state);
103
+ _connectionStatusText.text = "Unity: " + GetConnectionStatusText(state, keepConnected);
104
+ _btnConnect.text = GetButtonText(state, keepConnected);
105
+ var isConnect = _btnConnect.text == ServerButtonText_Connect;
106
+ _btnConnect.EnableInClassList("btn-primary", isConnect);
107
+ _btnConnect.EnableInClassList("btn-secondary", !isConnect);
108
+ SetStatusIndicator(_connectionStatusCircle, GetConnectionStatusClass(state, keepConnected));
109
+
110
+ if (!(state == ConnectionState.Connected && keepConnected))
111
+ SetAiAgentStatus(false);
112
+ }
113
+
114
+ /// <summary>
115
+ /// Reads the current connection state and refreshes the Unity connection row UI.
116
+ /// Call this whenever the UI might be stale.
117
+ /// </summary>
118
+ private void RefreshConnectionUI()
119
+ {
120
+ var state = UnityCopilotPluginEditor.ConnectionState.CurrentValue;
121
+ var keepConnected = UnityCopilotPluginEditor.KeepConnected;
122
+ UpdateConnectionUI(state, keepConnected);
123
+ }
124
+
125
+ /// <summary>
126
+ /// Schedules a delayed <see cref="RefreshConnectionUI"/> to catch state changes
127
+ /// that arrive after a reconnect (e.g. async handshake).
128
+ /// </summary>
129
+ private void ScheduleConnectionUIRefresh()
130
+ {
131
+ rootVisualElement?.schedule.Execute(() => RefreshConnectionUI()).ExecuteLater(500);
132
+ rootVisualElement?.schedule.Execute(() => RefreshConnectionUI()).ExecuteLater(2000);
133
+ }
134
+
135
+ internal static bool IsHostFieldReadOnly(bool keepConnected, ConnectionState state) =>
136
+ keepConnected || state != ConnectionState.Disconnected;
137
+
138
+ private static void UpdateHostFieldState(TextField field, bool keepConnected, ConnectionState state)
139
+ {
140
+ var isReadOnly = IsHostFieldReadOnly(keepConnected, state);
141
+ field.isReadOnly = isReadOnly;
142
+ var defaultUrl = $"http://localhost:{UnityCopilotPlugin.GeneratePortFromDirectory()}";
143
+ field.tooltip = keepConnected
144
+ ? "Editable only when Unity disconnected from the server."
145
+ : $"Usually the server is hosted locally at {defaultUrl}. Feel free to connect to a remote server if needed. The connection is established using WebSocket.";
146
+
147
+ field.EnableInClassList("disabled-text-field", isReadOnly);
148
+ field.EnableInClassList("enabled-text-field", !isReadOnly);
149
+ }
150
+
151
+ private void HandleConnectButton(string buttonText)
152
+ {
153
+ if (buttonText.Equals(ServerButtonText_Connect, StringComparison.OrdinalIgnoreCase))
154
+ {
155
+ ConnectToServer();
156
+ }
157
+ else
158
+ {
159
+ UnityCopilotPluginEditor.KeepConnected = false;
160
+ UnityCopilotPluginEditor.Instance.Save();
161
+ if (UnityCopilotPluginEditor.Instance.HasMcpPluginInstance)
162
+ _ = UnityCopilotPluginEditor.Instance.Disconnect();
163
+ }
164
+ ScheduleConnectionUIRefresh();
165
+ }
166
+
167
+ /// <summary>
168
+ /// Initiates connection to the server. Called by the Connect button.
169
+ /// </summary>
170
+ private static void ConnectToServer()
171
+ {
172
+ UnityCopilotPluginEditor.KeepConnected = true;
173
+ UnityCopilotPluginEditor.Instance.Save();
174
+ UnityBuildAndConnect();
175
+ }
176
+ }
177
+ }
@@ -142,9 +142,7 @@ namespace com.AtelierAI.Unity.Copilot.Editor.UI
142
142
  "The AI agent connection indicator shows whether an external AI client (Claude, Cursor, " +
143
143
  "Copilot, etc.) is currently connected to the server.\n\n" +
144
144
  "It is completely normal for the AI agent to appear disconnected:\n" +
145
- " • With 'Cloud' connection mode the AI agent connects on-demand and may disconnect " +
146
- "between interactions.\n" +
147
- " • With a local server the AI agent connects only when it needs to invoke a tool " +
145
+ " • The AI agent connects only when it needs to invoke a tool " +
148
146
  "or read a resource, then may drop the session.\n\n" +
149
147
  "The AI agent will always reconnect automatically whenever it needs to perform an " +
150
148
  "action in Unity — no manual intervention is required.\n\n" +
@@ -170,8 +168,6 @@ namespace com.AtelierAI.Unity.Copilot.Editor.UI
170
168
  private VisualElement? _aiAgentLabelsContainer;
171
169
  private VisualElement? _aiAgentStatusCircle;
172
170
 
173
- private DeviceAuthFlow? _deviceAuthFlow;
174
-
175
171
  private long _mcpServerDataVersion;
176
172
  private long _aiAgentDataVersion;
177
173
 
@@ -181,9 +177,6 @@ namespace com.AtelierAI.Unity.Copilot.Editor.UI
181
177
 
182
178
  SetupSettingsSection(root);
183
179
  SetupConnectionSection(root);
184
- SetupConnectionModeToggle(root);
185
- SetupCloudAuthSection(root);
186
- SetupConnectionAlerts(root);
187
180
  SetupMcpServerSection(root);
188
181
  SetupAiAgentSection(root);
189
182
  SetupDebugButtons(root);
@@ -280,7 +273,7 @@ namespace com.AtelierAI.Unity.Copilot.Editor.UI
280
273
  UnityCopilotPluginEditor.LogLevel = Enum.TryParse<LogLevel>(evt.newValue, out var parsed)
281
274
  ? parsed
282
275
  : LogLevel.Warning;
283
- SaveChanges($"[Unity Co-Pilot] LogLevel Changed: {evt.newValue}");
276
+ SaveChanges($"[Unity Copilot] LogLevel Changed: {evt.newValue}");
284
277
  });
285
278
 
286
279
  var inputTimeoutMs = root.Q<IntegerField>("inputTimeoutMs");
@@ -297,7 +290,7 @@ namespace com.AtelierAI.Unity.Copilot.Editor.UI
297
290
 
298
291
  UnityCopilotPluginEditor.TimeoutMs = newValue;
299
292
 
300
- SaveChanges($"[Unity Co-Pilot] Timeout Changed: {newValue} ms");
293
+ SaveChanges($"[Unity Copilot] Timeout Changed: {newValue} ms");
301
294
  UnityBuildAndConnect();
302
295
  });
303
296