MCPWindow.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. using System.Linq;
  2. using UnityEditor;
  3. using UnityEngine;
  4. using LLM.Editor.Core;
  5. using System.Threading;
  6. using LLM.Editor.Client;
  7. using LLM.Editor.Helper;
  8. using LLM.Editor.Commands;
  9. using LLM.Editor.Analysis;
  10. using System.Threading.Tasks;
  11. using System.Collections.Generic;
  12. namespace LLM.Editor.GUI
  13. {
  14. public class MCPWindow : EditorWindow
  15. {
  16. private string _promptText = "The grenade launcher should hit the target.";
  17. private Vector2 _scrollPos;
  18. private bool _isRequestInProgress;
  19. private readonly List<Object> _stagedContextObjects = new();
  20. private List<string> _requestedRoles = new();
  21. private bool _isAnalysisContextRequested;
  22. private ILlmApiClient _apiClient;
  23. private CancellationTokenSource _cancellationTokenSource;
  24. private List<string> _currentPlan = new();
  25. // --- Fields for Dummy API Client UI ---
  26. private List<string> _dummyTriggerPhrases = new();
  27. private int _selectedDummyTriggerIndex;
  28. private bool _dummyResponsesLoaded;
  29. [MenuItem("LLM/MCP Assistant")]
  30. public static void ShowWindow()
  31. {
  32. GetWindow<MCPWindow>("MCP Assistant");
  33. }
  34. private void OnEnable()
  35. {
  36. CommandExecutor.OnQueueUpdated += Repaint;
  37. CommandExecutor.OnPlanUpdated += HandlePlanUpdated;
  38. RequestAnalysisContextCommand.OnAnalysisContextRequested += HandleAnalysisContextRequested;
  39. CommandExecutor.OnContextReadyForNextTurn += HandleContextReadyForNextTurn;
  40. _apiClient = ApiClientFactory.GetClient();
  41. LoadDummyTestCases();
  42. }
  43. private void OnDisable()
  44. {
  45. CommandExecutor.OnQueueUpdated -= Repaint;
  46. CommandExecutor.OnPlanUpdated -= HandlePlanUpdated;
  47. RequestAnalysisContextCommand.OnAnalysisContextRequested -= HandleAnalysisContextRequested;
  48. CommandExecutor.OnContextReadyForNextTurn -= HandleContextReadyForNextTurn;
  49. _cancellationTokenSource?.Cancel();
  50. _cancellationTokenSource?.Dispose();
  51. }
  52. private void HandlePlanUpdated(List<string> plan)
  53. {
  54. _currentPlan = plan;
  55. Repaint();
  56. }
  57. private void HandleAnalysisContextRequested(RequestAnalysisContextParams contextParams)
  58. {
  59. _isAnalysisContextRequested = true;
  60. _requestedRoles = new List<string>(contextParams.subjectRoles);
  61. while (_stagedContextObjects.Count < _requestedRoles.Count)
  62. {
  63. _stagedContextObjects.Add(null);
  64. }
  65. Repaint();
  66. }
  67. private void HandleContextReadyForNextTurn(string detailedContext)
  68. {
  69. Debug.Log("[MCPWindow] Received detailed context. Sending follow-up to LLM.");
  70. _ = SendFollowUpAsync(detailedContext);
  71. }
  72. private void OnGUI()
  73. {
  74. EditorGUILayout.LabelField("LLM Co-Pilot", EditorStyles.boldLabel);
  75. DrawSessionManagement();
  76. EditorGUILayout.Space();
  77. EditorGUI.BeginDisabledGroup(!SessionManager.HasActiveSession());
  78. DrawContextStagingArea();
  79. EditorGUILayout.Space();
  80. if (!(_apiClient is DummyApiClient))
  81. {
  82. DrawSelectionHint();
  83. }
  84. EditorGUILayout.Space();
  85. EditorGUILayout.LabelField("Enter your request:");
  86. // --- Conditional UI for Prompt Input ---
  87. if (_apiClient is DummyApiClient)
  88. {
  89. if (!_dummyResponsesLoaded)
  90. {
  91. EditorGUILayout.HelpBox("Loading dummy responses...", MessageType.Info);
  92. }
  93. else if (_dummyTriggerPhrases.Any())
  94. {
  95. _selectedDummyTriggerIndex = EditorGUILayout.Popup("Select Test Case", _selectedDummyTriggerIndex, _dummyTriggerPhrases.ToArray());
  96. // Update prompt text based on selection to be used by the send function
  97. if(_selectedDummyTriggerIndex < _dummyTriggerPhrases.Count)
  98. {
  99. _promptText = _dummyTriggerPhrases[_selectedDummyTriggerIndex];
  100. }
  101. }
  102. else
  103. {
  104. EditorGUILayout.HelpBox("No dummy test cases found in DummyResponses.json.", MessageType.Warning);
  105. }
  106. }
  107. else
  108. {
  109. // Default text area for other clients like Gemini
  110. _promptText = EditorGUILayout.TextArea(_promptText, GUILayout.Height(60));
  111. }
  112. // --- End of Conditional UI ---
  113. DrawActionButtons();
  114. EditorGUILayout.Space();
  115. DrawPlanArea();
  116. EditorGUILayout.Space();
  117. DrawCommandQueue();
  118. EditorGUI.EndDisabledGroup();
  119. }
  120. private void LoadDummyTestCases()
  121. {
  122. var testCases = DummyApiClient.GetTestCases();
  123. if (testCases != null && testCases.Any())
  124. {
  125. _dummyTriggerPhrases = testCases.Select(c => c.TriggerPhrase).ToList();
  126. }
  127. else
  128. {
  129. _dummyTriggerPhrases = new List<string> { "No C# test cases found" };
  130. }
  131. _dummyResponsesLoaded = true;
  132. Repaint();
  133. }
  134. private void DrawSessionManagement()
  135. {
  136. EditorGUILayout.BeginHorizontal();
  137. if (SessionManager.HasActiveSession())
  138. {
  139. EditorGUILayout.LabelField($"Session: {SessionManager.GetCurrentSessionId()}");
  140. if (GUILayout.Button("End Session"))
  141. {
  142. SessionManager.EndSession();
  143. ClearStagingArea();
  144. _currentPlan.Clear();
  145. }
  146. }
  147. else
  148. {
  149. if (GUILayout.Button("Start New Session"))
  150. {
  151. SessionManager.StartNewSession();
  152. }
  153. }
  154. EditorGUILayout.EndHorizontal();
  155. }
  156. private void DrawContextStagingArea()
  157. {
  158. EditorGUILayout.LabelField("Manual Context", EditorStyles.boldLabel);
  159. EditorGUILayout.HelpBox("You can manually provide context by dragging and dropping any asset (prefab, script, etc.) or GameObject into the slots below.", MessageType.Info);
  160. if (GUILayout.Button("Add Manual Context Slot", GUILayout.Width(200)))
  161. {
  162. _stagedContextObjects.Add(null);
  163. if (_isAnalysisContextRequested) _requestedRoles.Add("Custom Role");
  164. }
  165. var indexToRemove = -1;
  166. for (var i = 0; i < _stagedContextObjects.Count; i++)
  167. {
  168. EditorGUILayout.BeginHorizontal();
  169. var label = (_isAnalysisContextRequested && i < _requestedRoles.Count) ? _requestedRoles[i] : $"Slot {i + 1}";
  170. _stagedContextObjects[i] = EditorGUILayout.ObjectField(label, _stagedContextObjects[i], typeof(Object), true);
  171. if (GUILayout.Button("X", GUILayout.Width(20)))
  172. {
  173. indexToRemove = i;
  174. }
  175. EditorGUILayout.EndHorizontal();
  176. }
  177. if (indexToRemove != -1)
  178. {
  179. _stagedContextObjects.RemoveAt(indexToRemove);
  180. if (_isAnalysisContextRequested && indexToRemove < _requestedRoles.Count)
  181. {
  182. _requestedRoles.RemoveAt(indexToRemove);
  183. }
  184. Repaint();
  185. }
  186. }
  187. private void DrawSelectionHint()
  188. {
  189. EditorGUILayout.LabelField("Automatic Context (From Selection)", EditorStyles.boldLabel);
  190. var selectedObjects = Selection.objects;
  191. if (selectedObjects.Length > 0)
  192. {
  193. EditorGUILayout.HelpBox("The following selected objects will automatically be included as context.", MessageType.None);
  194. EditorGUI.BeginDisabledGroup(true);
  195. foreach (var obj in selectedObjects)
  196. {
  197. EditorGUILayout.ObjectField(obj.name, obj, typeof(Object), true);
  198. }
  199. EditorGUI.EndDisabledGroup();
  200. }
  201. else
  202. {
  203. EditorGUILayout.HelpBox("No objects are currently selected in the editor.", MessageType.None);
  204. }
  205. }
  206. private void DrawActionButtons()
  207. {
  208. EditorGUILayout.BeginHorizontal();
  209. EditorGUI.BeginDisabledGroup(_isRequestInProgress);
  210. if (_isAnalysisContextRequested)
  211. {
  212. if (GUILayout.Button("Provide Staged Context to LLM"))
  213. {
  214. _stagedContextObjects.AddRange(Selection.objects);
  215. var contextSummary = ContextBuilder.BuildTier1Summary(_stagedContextObjects);
  216. var followUpMessage = "Here is the context you requested:\n" + contextSummary;
  217. _ = SendFollowUpAsync(followUpMessage);
  218. }
  219. }
  220. else
  221. {
  222. if (GUILayout.Button("Send Prompt"))
  223. {
  224. _ = SendInitialPromptAsync();
  225. }
  226. }
  227. EditorGUI.EndDisabledGroup();
  228. EditorGUI.BeginDisabledGroup(!_isRequestInProgress);
  229. if (GUILayout.Button("Stop", GUILayout.Width(80)))
  230. {
  231. _cancellationTokenSource?.Cancel();
  232. }
  233. EditorGUI.EndDisabledGroup();
  234. EditorGUILayout.EndHorizontal();
  235. }
  236. private void DrawPlanArea()
  237. {
  238. if (_currentPlan == null || !_currentPlan.Any()) return;
  239. EditorGUILayout.LabelField("Current Plan", EditorStyles.boldLabel);
  240. EditorGUILayout.BeginVertical(EditorStyles.helpBox);
  241. for(var i = 0; i < _currentPlan.Count; i++)
  242. {
  243. var step = _currentPlan[i];
  244. var label = $"{i + 1}. {step}";
  245. EditorGUILayout.LabelField(label, EditorStyles.wordWrappedLabel);
  246. }
  247. EditorGUILayout.EndVertical();
  248. }
  249. private void DrawCommandQueue()
  250. {
  251. EditorGUILayout.LabelField("Pending Commands", EditorStyles.boldLabel);
  252. _scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, EditorStyles.helpBox);
  253. if (CommandExecutor.HasPendingCommands())
  254. {
  255. var nextCommand = CommandExecutor.GetNextCommand();
  256. EditorGUILayout.LabelField("Next Up: " + nextCommand.commandName);
  257. EditorGUI.BeginDisabledGroup(_isRequestInProgress);
  258. if (GUILayout.Button("Execute Next Command"))
  259. {
  260. _ = CommandExecutor.ExecuteNextCommand();
  261. }
  262. EditorGUI.EndDisabledGroup();
  263. }
  264. else
  265. {
  266. EditorGUILayout.LabelField("No pending commands.");
  267. }
  268. EditorGUILayout.EndScrollView();
  269. }
  270. private async Task SendInitialPromptAsync()
  271. {
  272. _isRequestInProgress = true;
  273. _cancellationTokenSource = new CancellationTokenSource();
  274. _currentPlan.Clear();
  275. Repaint();
  276. try
  277. {
  278. var combinedContext = new List<Object>();
  279. combinedContext.AddRange(_stagedContextObjects);
  280. if (!(_apiClient is DummyApiClient))
  281. {
  282. combinedContext.AddRange(Selection.objects);
  283. }
  284. await _apiClient.SendPrompt(_promptText, combinedContext, _cancellationTokenSource.Token);
  285. }
  286. catch (TaskCanceledException)
  287. {
  288. Debug.Log("[MCPWindow] Task was cancelled.");
  289. }
  290. finally
  291. {
  292. _isRequestInProgress = false;
  293. _cancellationTokenSource.Dispose();
  294. _cancellationTokenSource = null;
  295. Repaint();
  296. }
  297. }
  298. private async Task SendFollowUpAsync(string followUpMessage)
  299. {
  300. _isRequestInProgress = true;
  301. _cancellationTokenSource = new CancellationTokenSource();
  302. Repaint();
  303. try
  304. {
  305. await _apiClient.SendFollowUp(followUpMessage, _cancellationTokenSource.Token);
  306. }
  307. catch (TaskCanceledException)
  308. {
  309. Debug.Log("[MCPWindow] Task was cancelled.");
  310. }
  311. finally
  312. {
  313. _isRequestInProgress = false;
  314. _cancellationTokenSource.Dispose();
  315. _cancellationTokenSource = null;
  316. if (_isAnalysisContextRequested)
  317. {
  318. ClearStagingArea();
  319. }
  320. Repaint();
  321. }
  322. }
  323. private void ClearStagingArea()
  324. {
  325. _isAnalysisContextRequested = false;
  326. _stagedContextObjects.Clear();
  327. _requestedRoles.Clear();
  328. }
  329. }
  330. }