yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
agent_editor.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstdlib>
5#include <cstring>
6#include <filesystem>
7#include <fstream>
8#include <memory>
9
11// Centralized UI theme
12#include "app/gui/style/theme.h"
13
16
17#include "absl/strings/match.h"
18#include "absl/strings/str_format.h"
19#include "absl/time/clock.h"
20#include "absl/time/time.h"
25#include "app/gui/core/icons.h"
27#include "rom/rom.h"
30#include "imgui/misc/cpp/imgui_stdlib.h"
31#include "util/file_util.h"
32#include "util/platform_paths.h"
35
36#ifdef YAZE_WITH_GRPC
39#endif
40
41#if defined(YAZE_WITH_JSON)
42#include "nlohmann/json.hpp"
43#endif
44
45namespace yaze {
46namespace editor {
47
50 agent_chat_ = std::make_unique<AgentChat>();
51 local_coordinator_ = std::make_unique<AgentCollaborationCoordinator>();
52 prompt_editor_ = std::make_unique<TextEditor>();
53 common_tiles_editor_ = std::make_unique<TextEditor>();
54
55 // Initialize default configuration (legacy)
59
60 // Initialize default bot profile
61 current_profile_.name = "Default Z3ED Bot";
62 current_profile_.description = "Default bot for Zelda 3 ROM editing";
67 current_profile_.tags = {"default", "z3ed"};
68
69 // Setup text editors
70 prompt_editor_->SetLanguageDefinition(
72 prompt_editor_->SetReadOnly(false);
73 prompt_editor_->SetShowWhitespaces(false);
74
75 common_tiles_editor_->SetLanguageDefinition(
77 common_tiles_editor_->SetReadOnly(false);
78 common_tiles_editor_->SetShowWhitespaces(false);
79
80 // Ensure profiles directory exists
82
84 {"Persona", "Define persona and goals", false},
85 {"Tool Stack", "Select the agent's tools", false},
86 {"Automation", "Configure automation hooks", false},
87 {"Validation", "Describe E2E validation", false},
88 {"E2E Checklist", "Track readiness for end-to-end runs", false}};
90 "Describe the persona, tone, and constraints for this agent.";
91}
92
94
96 // Base initialization
98
99 // Register cards with the card registry
101
102 // Register EditorPanel instances with PanelManager
104 auto* panel_manager = dependencies_.panel_manager;
105
106 // Register all agent EditorPanels with callbacks
107 panel_manager->RegisterEditorPanel(std::make_unique<AgentConfigurationPanel>(
108 [this]() { DrawConfigurationPanel(); }));
109 panel_manager->RegisterEditorPanel(std::make_unique<AgentStatusPanel>(
110 [this]() { DrawStatusPanel(); }));
111 panel_manager->RegisterEditorPanel(std::make_unique<AgentPromptEditorPanel>(
112 [this]() { DrawPromptEditorPanel(); }));
113 panel_manager->RegisterEditorPanel(std::make_unique<AgentBotProfilesPanel>(
114 [this]() { DrawBotProfilesPanel(); }));
115 panel_manager->RegisterEditorPanel(std::make_unique<AgentChatHistoryPanel>(
116 [this]() { DrawChatHistoryViewer(); }));
117 panel_manager->RegisterEditorPanel(std::make_unique<AgentMetricsDashboardPanel>(
118 [this]() { DrawAdvancedMetricsPanel(); }));
119 panel_manager->RegisterEditorPanel(std::make_unique<AgentBuilderPanel>(
120 [this]() { DrawAgentBuilderPanel(); }));
121 panel_manager->RegisterEditorPanel(
122 std::make_unique<AgentChatPanel>(agent_chat_.get()));
123
124 // Knowledge Base panel (callback set by AgentUiController)
125 panel_manager->RegisterEditorPanel(std::make_unique<AgentKnowledgeBasePanel>(
126 [this]() {
129 } else {
130 ImGui::TextDisabled("Knowledge service not available");
131 ImGui::TextWrapped(
132 "Build with Z3ED_AI=ON to enable the knowledge service.");
133 }
134 }));
135 }
136}
137
139 // Panel descriptors are now auto-created by RegisterEditorPanel() calls
140 // in Initialize(). No need for duplicate RegisterPanel() calls here.
141}
142
143absl::Status AgentEditor::Load() {
144 // Load agent configuration from project/settings
145 // Try to load all bot profiles
146 loaded_profiles_.clear();
147 auto profiles_dir = GetProfilesDirectory();
148 if (std::filesystem::exists(profiles_dir)) {
149 for (const auto& entry :
150 std::filesystem::directory_iterator(profiles_dir)) {
151 if (entry.path().extension() == ".json") {
152 std::ifstream file(entry.path());
153 if (file.is_open()) {
154 std::string json_content((std::istreambuf_iterator<char>(file)),
155 std::istreambuf_iterator<char>());
156 auto profile_or = JsonToProfile(json_content);
157 if (profile_or.ok()) {
158 loaded_profiles_.push_back(profile_or.value());
159 }
160 }
161 }
162 }
163 }
164 return absl::OkStatus();
165}
166
167absl::Status AgentEditor::Save() {
168 // Save current profile
169 current_profile_.modified_at = absl::Now();
171}
172
173absl::Status AgentEditor::Update() {
174 if (!active_)
175 return absl::OkStatus();
176
177 // Draw configuration dashboard
179
180 // Chat widget is drawn separately (not here)
181
182 return absl::OkStatus();
183}
184
186 ProposalDrawer* proposal_drawer,
187 Rom* rom) {
188 toast_manager_ = toast_manager;
189 proposal_drawer_ = proposal_drawer;
190 rom_ = rom;
191
192 // Auto-load API keys from environment
193 if (const char* gemini_key = std::getenv("GEMINI_API_KEY")) {
194 current_profile_.gemini_api_key = gemini_key;
195 current_config_.gemini_api_key = gemini_key;
196 // Auto-select gemini provider if key is available and no provider set
197 if (current_profile_.provider == "mock") {
198 current_profile_.provider = "gemini";
199 current_profile_.model = "gemini-2.5-flash";
200 current_config_.provider = "gemini";
201 current_config_.model = "gemini-2.5-flash";
202 }
203 }
204
205 if (const char* openai_key = std::getenv("OPENAI_API_KEY")) {
206 current_profile_.openai_api_key = openai_key;
207 current_config_.openai_api_key = openai_key;
208 // Auto-select openai if no gemini key and provider is mock
209 if (current_profile_.provider == "mock" &&
211 current_profile_.provider = "openai";
212 current_profile_.model = "gpt-4o-mini";
213 current_config_.provider = "openai";
214 current_config_.model = "gpt-4o-mini";
215 }
216 }
217
218 if (agent_chat_) {
219 agent_chat_->Initialize(toast_manager, proposal_drawer);
220 if (rom) {
221 agent_chat_->SetRomContext(rom);
222 }
223 }
224
227
228#ifdef YAZE_WITH_GRPC
229 if (agent_chat_) {
230 harness_telemetry_bridge_.SetAgentChat(agent_chat_.get());
231 test::TestManager::Get().SetHarnessListener(&harness_telemetry_bridge_);
232 }
233#endif
234
235 // Push initial configuration to the agent service
237}
238
240 rom_ = rom;
241 if (agent_chat_) {
242 agent_chat_->SetRomContext(rom);
243 }
244}
245
247 if (!active_) {
248 return;
249 }
250
251 // Animate retro effects
252 ImGuiIO& imgui_io = ImGui::GetIO();
253 pulse_animation_ += imgui_io.DeltaTime * 2.0f;
254 scanline_offset_ += imgui_io.DeltaTime * 0.4f;
255 if (scanline_offset_ > 1.0f) {
256 scanline_offset_ -= 1.0f;
257 }
258 glitch_timer_ += imgui_io.DeltaTime * 5.0f;
259 blink_counter_ = static_cast<int>(pulse_animation_ * 2.0f) % 2;
260}
261
263 const auto& theme = AgentUI::GetTheme();
264
265 auto HelpMarker = [](const char* desc) {
266 ImGui::SameLine();
267 ImGui::TextDisabled("(?)");
268 if (ImGui::IsItemHovered()) {
269 ImGui::BeginTooltip();
270 ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
271 ImGui::TextUnformatted(desc);
272 ImGui::PopTextWrapPos();
273 ImGui::EndTooltip();
274 }
275 };
276
278 theme.accent_color);
279
280 float avail_width = ImGui::GetContentRegionAvail().x;
281 ImVec2 button_size(avail_width / 2 - 8, 46);
282
283 auto ProviderButton = [&](const char* label, const char* provider_id,
284 const ImVec4& color) {
285 bool selected = current_profile_.provider == provider_id;
286 ImVec4 base_color = selected ? color : theme.panel_bg_darker;
287 if (AgentUI::StyledButton(label, base_color, button_size)) {
288 current_profile_.provider = provider_id;
289 }
290 };
291
292 ProviderButton(ICON_MD_SETTINGS " Mock", "mock", theme.provider_mock);
293 ImGui::SameLine();
294 ProviderButton(ICON_MD_CLOUD " Ollama", "ollama", theme.provider_ollama);
295 ProviderButton(ICON_MD_SMART_TOY " Gemini API", "gemini",
296 theme.provider_gemini);
297 ImGui::SameLine();
298 ProviderButton(ICON_MD_TERMINAL " Local CLI", "gemini-cli",
299 theme.provider_gemini);
300 ImGui::Spacing();
301 ProviderButton(ICON_MD_AUTO_AWESOME " OpenAI", "openai",
302 theme.provider_openai);
303
304 ImGui::Spacing();
305 ImGui::Separator();
306 ImGui::Spacing();
307
308 // Provider-specific settings
309 if (current_profile_.provider == "ollama") {
311 theme.accent_color);
312 ImGui::Text("Model:");
313 ImGui::SetNextItemWidth(-1);
314 static char model_buf[128] = "qwen2.5-coder:7b";
315 if (!current_profile_.model.empty()) {
316 strncpy(model_buf, current_profile_.model.c_str(),
317 sizeof(model_buf) - 1);
318 }
319 if (ImGui::InputTextWithHint("##ollama_model",
320 "e.g., qwen2.5-coder:7b, llama3.2",
321 model_buf, sizeof(model_buf))) {
322 current_profile_.model = model_buf;
323 }
324
325 ImGui::Text("Host URL:");
326 ImGui::SetNextItemWidth(-1);
327 static char host_buf[256] = "http://localhost:11434";
328 strncpy(host_buf, current_profile_.ollama_host.c_str(),
329 sizeof(host_buf) - 1);
330 if (ImGui::InputText("##ollama_host", host_buf, sizeof(host_buf))) {
331 current_profile_.ollama_host = host_buf;
332 }
333 } else if (current_profile_.provider == "gemini" ||
334 current_profile_.provider == "gemini-cli") {
336 theme.accent_color);
337
338 if (ImGui::Button(ICON_MD_REFRESH " Load from Env (GEMINI_API_KEY)")) {
339 const char* gemini_key = std::getenv("GEMINI_API_KEY");
340 if (gemini_key) {
341 current_profile_.gemini_api_key = gemini_key;
343 if (toast_manager_) {
344 toast_manager_->Show("Gemini API key loaded", ToastType::kSuccess);
345 }
346 } else if (toast_manager_) {
347 toast_manager_->Show("GEMINI_API_KEY not found", ToastType::kWarning);
348 }
349 }
350 HelpMarker("Loads GEMINI_API_KEY from your environment");
351
352 ImGui::Spacing();
353 ImGui::Text("Model:");
354 ImGui::SetNextItemWidth(-1);
355 static char model_buf[128] = "gemini-2.5-flash";
356 if (!current_profile_.model.empty()) {
357 strncpy(model_buf, current_profile_.model.c_str(),
358 sizeof(model_buf) - 1);
359 }
360 if (ImGui::InputTextWithHint("##gemini_model", "e.g., gemini-2.5-flash",
361 model_buf, sizeof(model_buf))) {
362 current_profile_.model = model_buf;
363 }
364
365 ImGui::Text("API Key:");
366 ImGui::SetNextItemWidth(-1);
367 static char key_buf[256] = "";
368 if (!current_profile_.gemini_api_key.empty() && key_buf[0] == '\0') {
369 strncpy(key_buf, current_profile_.gemini_api_key.c_str(),
370 sizeof(key_buf) - 1);
371 }
372 if (ImGui::InputText("##gemini_key", key_buf, sizeof(key_buf),
373 ImGuiInputTextFlags_Password)) {
375 }
376 if (!current_profile_.gemini_api_key.empty()) {
377 ImGui::TextColored(theme.status_success,
378 ICON_MD_CHECK_CIRCLE " API key configured");
379 }
380 } else if (current_profile_.provider == "openai") {
382 theme.accent_color);
383
384 if (ImGui::Button(ICON_MD_REFRESH " Load from Env (OPENAI_API_KEY)")) {
385 const char* openai_key = std::getenv("OPENAI_API_KEY");
386 if (openai_key) {
387 current_profile_.openai_api_key = openai_key;
389 if (toast_manager_) {
390 toast_manager_->Show("OpenAI API key loaded", ToastType::kSuccess);
391 }
392 } else if (toast_manager_) {
393 toast_manager_->Show("OPENAI_API_KEY not found", ToastType::kWarning);
394 }
395 }
396 HelpMarker("Loads OPENAI_API_KEY from your environment");
397
398 ImGui::Spacing();
399 ImGui::Text("Model:");
400 ImGui::SetNextItemWidth(-1);
401 static char openai_model_buf[128] = "gpt-4o-mini";
402 if (!current_profile_.model.empty()) {
403 strncpy(openai_model_buf, current_profile_.model.c_str(),
404 sizeof(openai_model_buf) - 1);
405 }
406 if (ImGui::InputTextWithHint("##openai_model", "e.g., gpt-4o-mini",
407 openai_model_buf, sizeof(openai_model_buf))) {
408 current_profile_.model = openai_model_buf;
409 }
410
411 ImGui::Text("API Key:");
412 ImGui::SetNextItemWidth(-1);
413 static char openai_key_buf[256] = "";
414 if (!current_profile_.openai_api_key.empty() &&
415 openai_key_buf[0] == '\0') {
416 strncpy(openai_key_buf, current_profile_.openai_api_key.c_str(),
417 sizeof(openai_key_buf) - 1);
418 }
419 if (ImGui::InputText("##openai_key", openai_key_buf,
420 sizeof(openai_key_buf),
421 ImGuiInputTextFlags_Password)) {
422 current_profile_.openai_api_key = openai_key_buf;
423 }
424 if (!current_profile_.openai_api_key.empty()) {
425 ImGui::TextColored(theme.status_success,
426 ICON_MD_CHECK_CIRCLE " API key configured");
427 }
428 }
429
430 ImGui::Spacing();
432 theme.text_info);
433
434 ImGui::Checkbox("Verbose logging", &current_profile_.verbose);
435 HelpMarker("Logs provider requests/responses to console");
436 ImGui::Checkbox("Show reasoning traces", &current_profile_.show_reasoning);
437 ImGui::Checkbox("Stream responses", &current_profile_.stream_responses);
438
439 ImGui::SliderFloat("Temperature", &current_profile_.temperature, 0.0f, 1.0f);
440 ImGui::SliderFloat("Top P", &current_profile_.top_p, 0.0f, 1.0f);
441 ImGui::SliderInt("Max output tokens", &current_profile_.max_output_tokens,
442 256, 4096);
443 ImGui::SliderInt(ICON_MD_LOOP " Max Tool Iterations",
446 "Maximum number of tool calls the agent can make while solving a single "
447 "request.");
448 ImGui::SliderInt(ICON_MD_REFRESH " Max Retry Attempts",
450 HelpMarker("Number of times to retry API calls on failure.");
451
452 ImGui::Spacing();
454 theme.text_secondary_gray);
455
456 static char name_buf[128];
457 strncpy(name_buf, current_profile_.name.c_str(), sizeof(name_buf) - 1);
458 if (ImGui::InputText("Name", name_buf, sizeof(name_buf))) {
459 current_profile_.name = name_buf;
460 }
461
462 static char desc_buf[256];
463 strncpy(desc_buf, current_profile_.description.c_str(),
464 sizeof(desc_buf) - 1);
465 if (ImGui::InputTextMultiline("Description", desc_buf, sizeof(desc_buf),
466 ImVec2(-1, 64))) {
467 current_profile_.description = desc_buf;
468 }
469
470 ImGui::Text("Tags (comma-separated)");
471 static char tags_buf[256];
472 if (tags_buf[0] == '\0' && !current_profile_.tags.empty()) {
473 std::string tags_str;
474 for (size_t i = 0; i < current_profile_.tags.size(); ++i) {
475 if (i > 0)
476 tags_str += ", ";
477 tags_str += current_profile_.tags[i];
478 }
479 strncpy(tags_buf, tags_str.c_str(), sizeof(tags_buf) - 1);
480 }
481 if (ImGui::InputText("##profile_tags", tags_buf, sizeof(tags_buf))) {
482 current_profile_.tags.clear();
483 std::string tags_str(tags_buf);
484 size_t pos = 0;
485 while ((pos = tags_str.find(',')) != std::string::npos) {
486 std::string tag = tags_str.substr(0, pos);
487 tag.erase(0, tag.find_first_not_of(" \t"));
488 tag.erase(tag.find_last_not_of(" \t") + 1);
489 if (!tag.empty()) {
490 current_profile_.tags.push_back(tag);
491 }
492 tags_str.erase(0, pos + 1);
493 }
494 tags_str.erase(0, tags_str.find_first_not_of(" \t"));
495 tags_str.erase(tags_str.find_last_not_of(" \t") + 1);
496 if (!tags_str.empty()) {
497 current_profile_.tags.push_back(tags_str);
498 }
499 }
500
501 ImGui::Spacing();
502 ImGui::PushStyleColor(ImGuiCol_Button, theme.status_success);
503 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, theme.status_success);
504 if (ImGui::Button(ICON_MD_CHECK " Apply & Save Configuration",
505 ImVec2(-1, 40))) {
519
521 Save();
522
523 if (toast_manager_) {
524 toast_manager_->Show("Configuration applied and saved",
526 }
527 }
528 ImGui::PopStyleColor(2);
529
531}
532
534 const auto& theme = AgentUI::GetTheme();
535
537 if (ImGui::BeginChild("ChatStatusCard", ImVec2(0, 140), true)) {
539 theme.accent_color);
540
541 bool chat_active = agent_chat_ && *agent_chat_->active();
542 AgentUI::RenderStatusIndicator(chat_active ? "Active" : "Inactive",
543 chat_active);
544 ImGui::SameLine();
545 if (!chat_active && ImGui::SmallButton("Open")) {
547 }
548
549 ImGui::Spacing();
550 ImGui::Text("Provider:");
551 ImGui::SameLine();
553 if (!current_profile_.model.empty()) {
554 ImGui::TextDisabled("Model: %s", current_profile_.model.c_str());
555 }
556 }
557 ImGui::EndChild();
558
559 ImGui::Spacing();
560
561 if (ImGui::BeginChild("RomStatusCard", ImVec2(0, 110), true)) {
563 theme.accent_color);
564 if (rom_ && rom_->is_loaded()) {
565 ImGui::TextColored(theme.status_success, ICON_MD_CHECK_CIRCLE " Loaded");
566 ImGui::TextDisabled("Tools: Ready");
567 } else {
568 ImGui::TextColored(theme.status_warning, ICON_MD_WARNING " Not Loaded");
569 ImGui::TextDisabled("Load a ROM to enable tool calls.");
570 }
571 }
572 ImGui::EndChild();
573
574 ImGui::Spacing();
575
576 if (ImGui::BeginChild("QuickTipsCard", ImVec2(0, 130), true)) {
578 theme.accent_color);
579 ImGui::BulletText("Ctrl+H: Toggle chat popup");
580 ImGui::BulletText("Ctrl+P: View proposals");
581 ImGui::BulletText("Edit prompts in Prompt Editor");
582 ImGui::BulletText("Create and save custom bots");
583 }
584 ImGui::EndChild();
586}
587
589 if (ImGui::CollapsingHeader(ICON_MD_ANALYTICS " Quick Metrics")) {
590 ImGui::TextDisabled("View detailed metrics in the Metrics tab");
591 }
592}
593
595 const auto& theme = AgentUI::GetTheme();
597 theme.accent_color);
598
599 ImGui::Text("File:");
600 ImGui::SetNextItemWidth(-45);
601 if (ImGui::BeginCombo("##prompt_file", active_prompt_file_.c_str())) {
602 const char* options[] = {"system_prompt.txt", "system_prompt_v2.txt",
603 "system_prompt_v3.txt"};
604 for (const char* option : options) {
605 bool selected = active_prompt_file_ == option;
606 if (ImGui::Selectable(option, selected)) {
607 active_prompt_file_ = option;
609 }
610 }
611 ImGui::EndCombo();
612 }
613
614 ImGui::SameLine();
615 if (ImGui::SmallButton(ICON_MD_REFRESH)) {
617 }
618 if (ImGui::IsItemHovered()) {
619 ImGui::SetTooltip("Reload from disk");
620 }
621
623 std::string asset_path = "agent/" + active_prompt_file_;
624 auto content_result = AssetLoader::LoadTextFile(asset_path);
625 if (content_result.ok()) {
626 prompt_editor_->SetText(*content_result);
627 current_profile_.system_prompt = *content_result;
629 if (toast_manager_) {
631 absl::StrFormat(ICON_MD_CHECK_CIRCLE " Loaded %s",
633 ToastType::kSuccess, 2.0f);
634 }
635 } else {
636 std::string placeholder = absl::StrFormat(
637 "# System prompt file not found: %s\n"
638 "# Error: %s\n\n"
639 "# Ensure the file exists in assets/agent/%s\n",
640 active_prompt_file_, content_result.status().message(),
642 prompt_editor_->SetText(placeholder);
644 }
645 }
646
647 ImGui::Spacing();
648 if (prompt_editor_) {
649 ImVec2 editor_size(ImGui::GetContentRegionAvail().x,
650 ImGui::GetContentRegionAvail().y - 60);
651 prompt_editor_->Render("##prompt_editor", editor_size, true);
652
653 ImGui::Spacing();
654 if (ImGui::Button(ICON_MD_SAVE " Save Prompt to Profile", ImVec2(-1, 0))) {
656 if (toast_manager_) {
657 toast_manager_->Show("System prompt saved to profile",
659 }
660 }
661 }
662
663 ImGui::Spacing();
664 ImGui::TextWrapped(
665 "Edit the system prompt that guides the agent's behavior. Changes are "
666 "stored on the active bot profile.");
667}
668
670 const auto& theme = AgentUI::GetTheme();
671 AgentUI::RenderSectionHeader(ICON_MD_FOLDER, "Bot Profile Manager",
672 theme.accent_color);
673 ImGui::Spacing();
674
675 ImGui::BeginChild("CurrentProfile", ImVec2(0, 150), true);
677 theme.accent_color);
678 ImGui::Text("Name: %s", current_profile_.name.c_str());
679 ImGui::Text("Provider: %s", current_profile_.provider.c_str());
680 if (!current_profile_.model.empty()) {
681 ImGui::Text("Model: %s", current_profile_.model.c_str());
682 }
683 ImGui::TextWrapped(
684 "Description: %s",
686 ? "No description"
687 : current_profile_.description.c_str());
688 ImGui::EndChild();
689
690 ImGui::Spacing();
691
692 if (ImGui::Button(ICON_MD_ADD " Create New Profile", ImVec2(-1, 0))) {
693 BotProfile new_profile = current_profile_;
694 new_profile.name = "New Profile";
695 new_profile.created_at = absl::Now();
696 new_profile.modified_at = absl::Now();
697 current_profile_ = new_profile;
698 if (toast_manager_) {
699 toast_manager_->Show("New profile created. Configure and save it.",
701 }
702 }
703
704 ImGui::Spacing();
706 theme.accent_color);
707
708 ImGui::BeginChild("ProfilesList", ImVec2(0, 0), true);
709 if (loaded_profiles_.empty()) {
710 ImGui::TextDisabled(
711 "No saved profiles. Create and save a profile to see it here.");
712 } else {
713 for (size_t i = 0; i < loaded_profiles_.size(); ++i) {
714 const auto& profile = loaded_profiles_[i];
715 ImGui::PushID(static_cast<int>(i));
716
717 bool is_current = (profile.name == current_profile_.name);
718 ImVec2 button_size(ImGui::GetContentRegionAvail().x - 80, 0);
719 ImVec4 button_color =
720 is_current ? theme.accent_color : theme.panel_bg_darker;
721 if (AgentUI::StyledButton(profile.name.c_str(), button_color,
722 button_size)) {
723 if (auto status = LoadBotProfile(profile.name); status.ok()) {
724 if (toast_manager_) {
726 absl::StrFormat("Loaded profile: %s", profile.name),
728 }
729 } else if (toast_manager_) {
730 toast_manager_->Show(std::string(status.message()),
732 }
733 }
734
735 ImGui::SameLine();
736 ImGui::PushStyleColor(ImGuiCol_Button, theme.status_warning);
737 if (ImGui::SmallButton(ICON_MD_DELETE)) {
738 if (auto status = DeleteBotProfile(profile.name); status.ok()) {
739 if (toast_manager_) {
741 absl::StrFormat("Deleted profile: %s", profile.name),
743 }
744 } else if (toast_manager_) {
745 toast_manager_->Show(std::string(status.message()),
747 }
748 }
749 ImGui::PopStyleColor();
750
751 ImGui::TextDisabled(" %s | %s", profile.provider.c_str(),
752 profile.description.empty()
753 ? "No description"
754 : profile.description.c_str());
755 ImGui::Spacing();
756 ImGui::PopID();
757 }
758 }
759 ImGui::EndChild();
760}
761
763 const auto& theme = AgentUI::GetTheme();
764 AgentUI::RenderSectionHeader(ICON_MD_HISTORY, "Chat History Viewer",
765 theme.accent_color);
766
767 if (ImGui::Button(ICON_MD_REFRESH " Refresh History")) {
769 }
770 ImGui::SameLine();
771 if (ImGui::Button(ICON_MD_DELETE_FOREVER " Clear History")) {
772 if (agent_chat_) {
773 agent_chat_->ClearHistory();
774 cached_history_.clear();
775 }
776 }
777
779 cached_history_ = agent_chat_->GetAgentService()->GetHistory();
781 }
782
783 ImGui::Spacing();
784 ImGui::Separator();
785
786 ImGui::BeginChild("HistoryList", ImVec2(0, 0), true);
787 if (cached_history_.empty()) {
788 ImGui::TextDisabled(
789 "No chat history. Start a conversation in the chat window.");
790 } else {
791 for (const auto& msg : cached_history_) {
792 bool from_user = (msg.sender == cli::agent::ChatMessage::Sender::kUser);
793 ImVec4 color =
794 from_user ? theme.user_message_color : theme.agent_message_color;
795
796 ImGui::PushStyleColor(ImGuiCol_Text, color);
797 ImGui::Text("%s:", from_user ? "User" : "Agent");
798 ImGui::PopStyleColor();
799
800 ImGui::SameLine();
801 ImGui::TextDisabled("%s",
802 absl::FormatTime("%H:%M:%S", msg.timestamp,
803 absl::LocalTimeZone())
804 .c_str());
805
806 ImGui::TextWrapped("%s", msg.message.c_str());
807 ImGui::Spacing();
808 ImGui::Separator();
809 }
810 }
811 ImGui::EndChild();
812}
813
815 const auto& theme = AgentUI::GetTheme();
817 theme.accent_color);
818 ImGui::Spacing();
819
820 if (agent_chat_) {
821 auto metrics = agent_chat_->GetAgentService()->GetMetrics();
822 if (ImGui::BeginTable("MetricsTable", 2,
823 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
824 ImGui::TableSetupColumn("Metric", ImGuiTableColumnFlags_WidthFixed,
825 200.0f);
826 ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch);
827 ImGui::TableHeadersRow();
828
829 auto Row = [](const char* label, const std::string& value) {
830 ImGui::TableNextRow();
831 ImGui::TableSetColumnIndex(0);
832 ImGui::Text("%s", label);
833 ImGui::TableSetColumnIndex(1);
834 ImGui::TextDisabled("%s", value.c_str());
835 };
836
837 Row("Total Messages",
838 absl::StrFormat("%d user / %d agent", metrics.total_user_messages,
839 metrics.total_agent_messages));
840 Row("Tool Calls", absl::StrFormat("%d", metrics.total_tool_calls));
841 Row("Commands", absl::StrFormat("%d", metrics.total_commands));
842 Row("Proposals", absl::StrFormat("%d", metrics.total_proposals));
843 Row("Average Latency (s)",
844 absl::StrFormat("%.2f", metrics.average_latency_seconds));
845 Row("Elapsed (s)",
846 absl::StrFormat("%.2f", metrics.total_elapsed_seconds));
847
848 ImGui::EndTable();
849 }
850 } else {
851 ImGui::TextDisabled("Initialize the chat system to see metrics.");
852 }
853}
854
856 const auto& theme = AgentUI::GetTheme();
857 AgentUI::RenderSectionHeader(ICON_MD_GRID_ON, "Common Tiles Reference",
858 theme.accent_color);
859 ImGui::Spacing();
860
861 ImGui::TextWrapped(
862 "Customize the tile reference file that AI uses for tile placement. "
863 "Organize tiles by category and provide hex IDs with descriptions.");
864
865 ImGui::Spacing();
866
867 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Load", ImVec2(100, 0))) {
868 auto content = AssetLoader::LoadTextFile("agent/common_tiles.txt");
869 if (content.ok()) {
870 common_tiles_editor_->SetText(*content);
872 if (toast_manager_) {
873 toast_manager_->Show(ICON_MD_CHECK_CIRCLE " Common tiles loaded",
874 ToastType::kSuccess, 2.0f);
875 }
876 }
877 }
878
879 ImGui::SameLine();
880 if (ImGui::Button(ICON_MD_SAVE " Save", ImVec2(100, 0))) {
881 if (toast_manager_) {
883 ICON_MD_INFO " Save to project directory (coming soon)",
884 ToastType::kInfo, 2.0f);
885 }
886 }
887
888 ImGui::SameLine();
889 if (ImGui::SmallButton(ICON_MD_REFRESH)) {
891 }
892 if (ImGui::IsItemHovered()) {
893 ImGui::SetTooltip("Reload from disk");
894 }
895
897 auto content = AssetLoader::LoadTextFile("agent/common_tiles.txt");
898 if (content.ok()) {
899 common_tiles_editor_->SetText(*content);
900 } else {
901 std::string default_tiles =
902 "# Common Tile16 Reference\n"
903 "# Format: 0xHEX = Description\n\n"
904 "[grass_tiles]\n"
905 "0x020 = Grass (standard)\n\n"
906 "[nature_tiles]\n"
907 "0x02E = Tree (oak)\n"
908 "0x003 = Bush\n\n"
909 "[water_tiles]\n"
910 "0x14C = Water (top edge)\n"
911 "0x14D = Water (middle)\n";
912 common_tiles_editor_->SetText(default_tiles);
913 }
915 }
916
917 ImGui::Separator();
918 ImGui::Spacing();
919
921 ImVec2 editor_size(ImGui::GetContentRegionAvail().x,
922 ImGui::GetContentRegionAvail().y);
923 common_tiles_editor_->Render("##tiles_editor", editor_size, true);
924 }
925}
926
928 const auto& theme = AgentUI::GetTheme();
929 AgentUI::RenderSectionHeader(ICON_MD_ADD, "Create New System Prompt",
930 theme.accent_color);
931 ImGui::Spacing();
932
933 ImGui::TextWrapped(
934 "Create a custom system prompt from scratch or start from a template.");
935 ImGui::Separator();
936
937 ImGui::Text("Prompt Name:");
938 ImGui::SetNextItemWidth(-1);
939 ImGui::InputTextWithHint("##new_prompt_name", "e.g., custom_prompt.txt",
941
942 ImGui::Spacing();
943 ImGui::Text("Start from template:");
944
945 auto LoadTemplate = [&](const char* path, const char* label) {
946 if (ImGui::Button(label, ImVec2(-1, 0))) {
947 auto content = AssetLoader::LoadTextFile(path);
948 if (content.ok() && prompt_editor_) {
949 prompt_editor_->SetText(*content);
950 if (toast_manager_) {
951 toast_manager_->Show("Template loaded", ToastType::kSuccess, 1.5f);
952 }
953 }
954 }
955 };
956
957 LoadTemplate("agent/system_prompt.txt", ICON_MD_FILE_COPY " v1 (Basic)");
958 LoadTemplate("agent/system_prompt_v2.txt", ICON_MD_FILE_COPY " v2 (Enhanced)");
959 LoadTemplate("agent/system_prompt_v3.txt", ICON_MD_FILE_COPY " v3 (Proactive)");
960
961 if (ImGui::Button(ICON_MD_NOTE_ADD " Blank Template", ImVec2(-1, 0))) {
962 if (prompt_editor_) {
963 std::string blank_template =
964 "# Custom System Prompt\n\n"
965 "You are an AI assistant for ROM hacking.\n\n"
966 "## Your Role\n"
967 "- Help users understand ROM data\n"
968 "- Provide accurate information\n"
969 "- Use tools when needed\n\n"
970 "## Guidelines\n"
971 "1. Always provide text_response after tool calls\n"
972 "2. Be helpful and accurate\n"
973 "3. Explain your reasoning\n";
974 prompt_editor_->SetText(blank_template);
975 if (toast_manager_) {
976 toast_manager_->Show("Blank template created", ToastType::kSuccess,
977 1.5f);
978 }
979 }
980 }
981
982 ImGui::Spacing();
983 ImGui::Separator();
984
985 ImGui::PushStyleColor(ImGuiCol_Button, theme.status_success);
986 if (ImGui::Button(ICON_MD_SAVE " Save New Prompt", ImVec2(-1, 40))) {
987 if (std::strlen(new_prompt_name_) > 0 && prompt_editor_) {
988 std::string filename = new_prompt_name_;
989 if (!absl::EndsWith(filename, ".txt")) {
990 filename += ".txt";
991 }
992 if (toast_manager_) {
994 absl::StrFormat(ICON_MD_SAVE " Prompt saved as %s", filename),
995 ToastType::kSuccess, 3.0f);
996 }
997 std::memset(new_prompt_name_, 0, sizeof(new_prompt_name_));
998 } else if (toast_manager_) {
999 toast_manager_->Show(ICON_MD_WARNING " Enter a name for the prompt",
1000 ToastType::kWarning, 2.0f);
1001 }
1002 }
1003 ImGui::PopStyleColor();
1004
1005 ImGui::Spacing();
1006 ImGui::TextWrapped(
1007 "Note: New prompts are saved to your project. Use the Prompt Editor to "
1008 "edit existing prompts.");
1009}
1010
1012 const auto& theme = AgentUI::GetTheme();
1014 theme.accent_color);
1015
1016 if (!agent_chat_) {
1017 ImGui::TextDisabled("Chat system not initialized.");
1018 return;
1019 }
1020
1021 ImGui::BeginChild("AgentBuilderPanel", ImVec2(0, 0), false);
1022 ImGui::Columns(2, nullptr, false);
1023 ImGui::TextColored(theme.accent_color, "Stages");
1024 ImGui::Separator();
1025
1026 for (size_t i = 0; i < builder_state_.stages.size(); ++i) {
1027 auto& stage = builder_state_.stages[i];
1028 ImGui::PushID(static_cast<int>(i));
1029 bool selected = builder_state_.active_stage == static_cast<int>(i);
1030 if (ImGui::Selectable(stage.name.c_str(), selected)) {
1031 builder_state_.active_stage = static_cast<int>(i);
1032 }
1033 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 24.0f);
1034 ImGui::Checkbox("##stage_done", &stage.completed);
1035 ImGui::PopID();
1036 }
1037
1038 ImGui::NextColumn();
1039 ImGui::TextColored(theme.text_info, "Stage Details");
1040 ImGui::Separator();
1041
1042 int stage_index =
1043 std::clamp(builder_state_.active_stage, 0,
1044 static_cast<int>(builder_state_.stages.size()) - 1);
1045 int completed_stages = 0;
1046 for (const auto& stage : builder_state_.stages) {
1047 if (stage.completed) {
1048 ++completed_stages;
1049 }
1050 }
1051
1052 switch (stage_index) {
1053 case 0: {
1054 static std::string new_goal;
1055 ImGui::Text("Persona + Goals");
1056 ImGui::InputTextMultiline("##persona_notes",
1057 &builder_state_.persona_notes, ImVec2(-1, 120));
1058 ImGui::Spacing();
1059 ImGui::TextDisabled("Add Goal");
1060 ImGui::InputTextWithHint("##goal_input", "e.g. Document dungeon plan",
1061 &new_goal);
1062 ImGui::SameLine();
1063 if (ImGui::Button(ICON_MD_ADD) && !new_goal.empty()) {
1064 builder_state_.goals.push_back(new_goal);
1065 new_goal.clear();
1066 }
1067 for (size_t i = 0; i < builder_state_.goals.size(); ++i) {
1068 ImGui::BulletText("%s", builder_state_.goals[i].c_str());
1069 ImGui::SameLine();
1070 ImGui::PushID(static_cast<int>(i));
1071 if (ImGui::SmallButton(ICON_MD_CLOSE)) {
1072 builder_state_.goals.erase(builder_state_.goals.begin() + i);
1073 ImGui::PopID();
1074 break;
1075 }
1076 ImGui::PopID();
1077 }
1078 break;
1079 }
1080 case 1: {
1081 ImGui::Text("Tool Stack");
1082 auto tool_checkbox = [&](const char* label, bool* value) {
1083 ImGui::Checkbox(label, value);
1084 };
1085 tool_checkbox("Resources", &builder_state_.tools.resources);
1086 tool_checkbox("Dungeon", &builder_state_.tools.dungeon);
1087 tool_checkbox("Overworld", &builder_state_.tools.overworld);
1088 tool_checkbox("Dialogue", &builder_state_.tools.dialogue);
1089 tool_checkbox("GUI Automation", &builder_state_.tools.gui);
1090 tool_checkbox("Music", &builder_state_.tools.music);
1091 tool_checkbox("Sprite", &builder_state_.tools.sprite);
1092 tool_checkbox("Emulator", &builder_state_.tools.emulator);
1093 break;
1094 }
1095 case 2: {
1096 ImGui::Text("Automation");
1097 ImGui::Checkbox("Auto-run harness plan", &builder_state_.auto_run_tests);
1098 ImGui::Checkbox("Auto-sync ROM context", &builder_state_.auto_sync_rom);
1099 ImGui::Checkbox("Auto-focus proposal drawer",
1101 ImGui::TextWrapped(
1102 "Enable these options to push harness dashboards/test plans when "
1103 "executing plans.");
1104 break;
1105 }
1106 case 3: {
1107 ImGui::Text("Validation Criteria");
1108 ImGui::InputTextMultiline("##validation_notes",
1109 &builder_state_.stages[stage_index].summary,
1110 ImVec2(-1, 120));
1111 break;
1112 }
1113 case 4: {
1114 ImGui::Text("E2E Checklist");
1115 float progress =
1116 builder_state_.stages.empty()
1117 ? 0.0f
1118 : static_cast<float>(completed_stages) /
1119 static_cast<float>(builder_state_.stages.size());
1120 ImGui::ProgressBar(progress, ImVec2(-1, 0),
1121 absl::StrFormat("%d/%zu complete", completed_stages,
1122 builder_state_.stages.size())
1123 .c_str());
1124 ImGui::Checkbox("Ready for automation handoff",
1126 ImGui::TextDisabled("Auto-sync ROM: %s",
1127 builder_state_.auto_sync_rom ? "ON" : "OFF");
1128 ImGui::TextDisabled("Auto-focus proposals: %s",
1129 builder_state_.auto_focus_proposals ? "ON" : "OFF");
1130 break;
1131 }
1132 }
1133
1134 ImGui::Columns(1);
1135 ImGui::Separator();
1136
1137 float completion_ratio =
1138 builder_state_.stages.empty()
1139 ? 0.0f
1140 : static_cast<float>(completed_stages) /
1141 static_cast<float>(builder_state_.stages.size());
1142 ImGui::TextDisabled("Overall Progress");
1143 ImGui::ProgressBar(completion_ratio, ImVec2(-1, 0));
1144 ImGui::TextDisabled("E2E Ready: %s",
1145 builder_state_.ready_for_e2e ? "Yes" : "No");
1146
1147 if (ImGui::Button(ICON_MD_LINK " Apply to Chat")) {
1148 auto* service = agent_chat_->GetAgentService();
1149 if (service) {
1155 prefs.gui = builder_state_.tools.gui;
1158#ifdef YAZE_WITH_GRPC
1160#endif
1161 service->SetToolPreferences(prefs);
1162
1163 auto agent_cfg = service->GetConfig();
1164 agent_cfg.max_tool_iterations = current_profile_.max_tool_iterations;
1165 agent_cfg.max_retry_attempts = current_profile_.max_retry_attempts;
1166 agent_cfg.verbose = current_profile_.verbose;
1167 agent_cfg.show_reasoning = current_profile_.show_reasoning;
1168 service->SetConfig(agent_cfg);
1169 }
1170
1171 agent_chat_->SetLastPlanSummary(builder_state_.persona_notes);
1172
1173 if (toast_manager_) {
1174 toast_manager_->Show("Builder tool plan synced to chat",
1175 ToastType::kSuccess, 2.0f);
1176 }
1177 }
1178 ImGui::SameLine();
1179
1180 ImGui::InputTextWithHint("##blueprint_path", "Path to blueprint...",
1182 std::filesystem::path blueprint_path =
1184 ? (std::filesystem::temp_directory_path() / "agent_builder.json")
1185 : std::filesystem::path(builder_state_.blueprint_path);
1186
1187 if (ImGui::Button(ICON_MD_SAVE " Save Blueprint")) {
1188 auto status = SaveBuilderBlueprint(blueprint_path);
1189 if (toast_manager_) {
1190 if (status.ok()) {
1191 toast_manager_->Show("Builder blueprint saved", ToastType::kSuccess,
1192 2.0f);
1193 } else {
1194 toast_manager_->Show(std::string(status.message()),
1195 ToastType::kError, 3.5f);
1196 }
1197 }
1198 }
1199 ImGui::SameLine();
1200 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Load Blueprint")) {
1201 auto status = LoadBuilderBlueprint(blueprint_path);
1202 if (toast_manager_) {
1203 if (status.ok()) {
1204 toast_manager_->Show("Builder blueprint loaded", ToastType::kSuccess,
1205 2.0f);
1206 } else {
1207 toast_manager_->Show(std::string(status.message()),
1208 ToastType::kError, 3.5f);
1209 }
1210 }
1211 }
1212
1213 ImGui::EndChild();
1214}
1215
1216absl::Status AgentEditor::SaveBuilderBlueprint(const std::filesystem::path& path) {
1217#if defined(YAZE_WITH_JSON)
1218 nlohmann::json json;
1219 json["persona_notes"] = builder_state_.persona_notes;
1220 json["goals"] = builder_state_.goals;
1221 json["auto_run_tests"] = builder_state_.auto_run_tests;
1222 json["auto_sync_rom"] = builder_state_.auto_sync_rom;
1223 json["auto_focus_proposals"] = builder_state_.auto_focus_proposals;
1224 json["ready_for_e2e"] = builder_state_.ready_for_e2e;
1225 json["tools"] = {
1226 {"resources", builder_state_.tools.resources},
1227 {"dungeon", builder_state_.tools.dungeon},
1228 {"overworld", builder_state_.tools.overworld},
1229 {"dialogue", builder_state_.tools.dialogue},
1230 {"gui", builder_state_.tools.gui},
1231 {"music", builder_state_.tools.music},
1232 {"sprite", builder_state_.tools.sprite},
1233 {"emulator", builder_state_.tools.emulator},
1234 };
1235 json["stages"] = nlohmann::json::array();
1236 for (const auto& stage : builder_state_.stages) {
1237 json["stages"].push_back({{"name", stage.name},
1238 {"summary", stage.summary},
1239 {"completed", stage.completed}});
1240 }
1241
1242 std::error_code ec;
1243 std::filesystem::create_directories(path.parent_path(), ec);
1244 std::ofstream file(path);
1245 if (!file.is_open()) {
1246 return absl::InternalError(
1247 absl::StrFormat("Failed to open blueprint: %s", path.string()));
1248 }
1249 file << json.dump(2);
1250 builder_state_.blueprint_path = path.string();
1251 return absl::OkStatus();
1252#else
1253 (void)path;
1254 return absl::UnimplementedError("Blueprint export requires JSON support");
1255#endif
1256}
1257
1258absl::Status AgentEditor::LoadBuilderBlueprint(const std::filesystem::path& path) {
1259#if defined(YAZE_WITH_JSON)
1260 std::ifstream file(path);
1261 if (!file.is_open()) {
1262 return absl::NotFoundError(
1263 absl::StrFormat("Blueprint not found: %s", path.string()));
1264 }
1265
1266 nlohmann::json json;
1267 file >> json;
1268
1269 builder_state_.persona_notes = json.value("persona_notes", "");
1270 builder_state_.goals.clear();
1271 if (json.contains("goals") && json["goals"].is_array()) {
1272 for (const auto& goal : json["goals"]) {
1273 if (goal.is_string()) {
1274 builder_state_.goals.push_back(goal.get<std::string>());
1275 }
1276 }
1277 }
1278 if (json.contains("tools") && json["tools"].is_object()) {
1279 auto tools = json["tools"];
1280 builder_state_.tools.resources = tools.value("resources", true);
1281 builder_state_.tools.dungeon = tools.value("dungeon", true);
1282 builder_state_.tools.overworld = tools.value("overworld", true);
1283 builder_state_.tools.dialogue = tools.value("dialogue", true);
1284 builder_state_.tools.gui = tools.value("gui", false);
1285 builder_state_.tools.music = tools.value("music", false);
1286 builder_state_.tools.sprite = tools.value("sprite", false);
1287 builder_state_.tools.emulator = tools.value("emulator", false);
1288 }
1289 builder_state_.auto_run_tests = json.value("auto_run_tests", false);
1290 builder_state_.auto_sync_rom = json.value("auto_sync_rom", true);
1292 json.value("auto_focus_proposals", true);
1293 builder_state_.ready_for_e2e = json.value("ready_for_e2e", false);
1294 if (json.contains("stages") && json["stages"].is_array()) {
1295 builder_state_.stages.clear();
1296 for (const auto& stage : json["stages"]) {
1297 AgentBuilderState::Stage builder_stage;
1298 builder_stage.name = stage.value("name", std::string{});
1299 builder_stage.summary = stage.value("summary", std::string{});
1300 builder_stage.completed = stage.value("completed", false);
1301 builder_state_.stages.push_back(builder_stage);
1302 }
1303 }
1304 builder_state_.blueprint_path = path.string();
1305 return absl::OkStatus();
1306#else
1307 (void)path;
1308 return absl::UnimplementedError("Blueprint import requires JSON support");
1309#endif
1310}
1311
1312absl::Status AgentEditor::SaveBotProfile(const BotProfile& profile) {
1313#if defined(YAZE_WITH_JSON)
1314 auto dir_status = EnsureProfilesDirectory();
1315 if (!dir_status.ok()) return dir_status;
1316
1317 std::filesystem::path profile_path =
1318 GetProfilesDirectory() / (profile.name + ".json");
1319 std::ofstream file(profile_path);
1320 if (!file.is_open()) {
1321 return absl::InternalError("Failed to open profile file for writing");
1322 }
1323
1324 file << ProfileToJson(profile);
1325 file.close();
1326 return Load();
1327#else
1328 return absl::UnimplementedError(
1329 "JSON support required for profile management");
1330#endif
1331}
1332
1333absl::Status AgentEditor::LoadBotProfile(const std::string& name) {
1334#if defined(YAZE_WITH_JSON)
1335 std::filesystem::path profile_path =
1336 GetProfilesDirectory() / (name + ".json");
1337 if (!std::filesystem::exists(profile_path)) {
1338 return absl::NotFoundError(absl::StrFormat("Profile '%s' not found", name));
1339 }
1340
1341 std::ifstream file(profile_path);
1342 if (!file.is_open()) {
1343 return absl::InternalError("Failed to open profile file");
1344 }
1345
1346 std::string json_content((std::istreambuf_iterator<char>(file)),
1347 std::istreambuf_iterator<char>());
1348
1349 auto profile_or = JsonToProfile(json_content);
1350 if (!profile_or.ok()) {
1351 return profile_or.status();
1352 }
1353 current_profile_ = *profile_or;
1354
1368
1370 return absl::OkStatus();
1371#else
1372 return absl::UnimplementedError(
1373 "JSON support required for profile management");
1374#endif
1375}
1376
1377absl::Status AgentEditor::DeleteBotProfile(const std::string& name) {
1378 std::filesystem::path profile_path =
1379 GetProfilesDirectory() / (name + ".json");
1380 if (!std::filesystem::exists(profile_path)) {
1381 return absl::NotFoundError(absl::StrFormat("Profile '%s' not found", name));
1382 }
1383
1384 std::filesystem::remove(profile_path);
1385 return Load();
1386}
1387
1388std::vector<AgentEditor::BotProfile> AgentEditor::GetAllProfiles() const {
1389 return loaded_profiles_;
1390}
1391
1410
1411absl::Status AgentEditor::ExportProfile(const BotProfile& profile, const std::filesystem::path& path) {
1412#if defined(YAZE_WITH_JSON)
1413 auto status = SaveBotProfile(profile);
1414 if (!status.ok()) return status;
1415
1416 std::ofstream file(path);
1417 if (!file.is_open()) {
1418 return absl::InternalError("Failed to open file for export");
1419 }
1420 file << ProfileToJson(profile);
1421 return absl::OkStatus();
1422#else
1423 (void)profile;
1424 (void)path;
1425 return absl::UnimplementedError("JSON support required");
1426#endif
1427}
1428
1429absl::Status AgentEditor::ImportProfile(const std::filesystem::path& path) {
1430#if defined(YAZE_WITH_JSON)
1431 if (!std::filesystem::exists(path)) {
1432 return absl::NotFoundError("Import file not found");
1433 }
1434
1435 std::ifstream file(path);
1436 if (!file.is_open()) {
1437 return absl::InternalError("Failed to open import file");
1438 }
1439
1440 std::string json_content((std::istreambuf_iterator<char>(file)),
1441 std::istreambuf_iterator<char>());
1442
1443 auto profile_or = JsonToProfile(json_content);
1444 if (!profile_or.ok()) {
1445 return profile_or.status();
1446 }
1447
1448 return SaveBotProfile(*profile_or);
1449#else
1450 (void)path;
1451 return absl::UnimplementedError("JSON support required");
1452#endif
1453}
1454
1455std::filesystem::path AgentEditor::GetProfilesDirectory() const {
1457 if (!config_dir.ok()) {
1458 return std::filesystem::current_path() / ".yaze" / "agent" / "profiles";
1459 }
1460 return *config_dir / "agent" / "profiles";
1461}
1462
1464 auto dir = GetProfilesDirectory();
1465 std::error_code ec;
1466 std::filesystem::create_directories(dir, ec);
1467 if (ec) {
1468 return absl::InternalError(
1469 absl::StrFormat("Failed to create profiles directory: %s",
1470 ec.message()));
1471 }
1472 return absl::OkStatus();
1473}
1474
1475std::string AgentEditor::ProfileToJson(const BotProfile& profile) const {
1476#if defined(YAZE_WITH_JSON)
1477 nlohmann::json json;
1478 json["name"] = profile.name;
1479 json["description"] = profile.description;
1480 json["provider"] = profile.provider;
1481 json["model"] = profile.model;
1482 json["ollama_host"] = profile.ollama_host;
1483 json["gemini_api_key"] = profile.gemini_api_key;
1484 json["openai_api_key"] = profile.openai_api_key;
1485 json["system_prompt"] = profile.system_prompt;
1486 json["verbose"] = profile.verbose;
1487 json["show_reasoning"] = profile.show_reasoning;
1488 json["max_tool_iterations"] = profile.max_tool_iterations;
1489 json["max_retry_attempts"] = profile.max_retry_attempts;
1490 json["temperature"] = profile.temperature;
1491 json["top_p"] = profile.top_p;
1492 json["max_output_tokens"] = profile.max_output_tokens;
1493 json["stream_responses"] = profile.stream_responses;
1494 json["tags"] = profile.tags;
1495 json["created_at"] = absl::FormatTime(absl::RFC3339_full, profile.created_at,
1496 absl::UTCTimeZone());
1497 json["modified_at"] = absl::FormatTime(
1498 absl::RFC3339_full, profile.modified_at, absl::UTCTimeZone());
1499
1500 return json.dump(2);
1501#else
1502 return "{}";
1503#endif
1504}
1505
1506absl::StatusOr<AgentEditor::BotProfile> AgentEditor::JsonToProfile(const std::string& json_str) const {
1507#if defined(YAZE_WITH_JSON)
1508 try {
1509 nlohmann::json json = nlohmann::json::parse(json_str);
1510
1511 BotProfile profile;
1512 profile.name = json.value("name", "Unnamed Profile");
1513 profile.description = json.value("description", "");
1514 profile.provider = json.value("provider", "mock");
1515 profile.model = json.value("model", "");
1516 profile.ollama_host = json.value("ollama_host", "http://localhost:11434");
1517 profile.gemini_api_key = json.value("gemini_api_key", "");
1518 profile.openai_api_key = json.value("openai_api_key", "");
1519 profile.system_prompt = json.value("system_prompt", "");
1520 profile.verbose = json.value("verbose", false);
1521 profile.show_reasoning = json.value("show_reasoning", true);
1522 profile.max_tool_iterations = json.value("max_tool_iterations", 4);
1523 profile.max_retry_attempts = json.value("max_retry_attempts", 3);
1524 profile.temperature = json.value("temperature", 0.25f);
1525 profile.top_p = json.value("top_p", 0.95f);
1526 profile.max_output_tokens = json.value("max_output_tokens", 2048);
1527 profile.stream_responses = json.value("stream_responses", false);
1528
1529 if (json.contains("tags") && json["tags"].is_array()) {
1530 for (const auto& tag : json["tags"]) {
1531 profile.tags.push_back(tag.get<std::string>());
1532 }
1533 }
1534
1535 if (json.contains("created_at")) {
1536 absl::Time created;
1537 if (absl::ParseTime(absl::RFC3339_full,
1538 json["created_at"].get<std::string>(), &created,
1539 nullptr)) {
1540 profile.created_at = created;
1541 }
1542 }
1543
1544 if (json.contains("modified_at")) {
1545 absl::Time modified;
1546 if (absl::ParseTime(absl::RFC3339_full,
1547 json["modified_at"].get<std::string>(), &modified,
1548 nullptr)) {
1549 profile.modified_at = modified;
1550 }
1551 }
1552
1553 return profile;
1554 } catch (const std::exception& e) {
1555 return absl::InternalError(
1556 absl::StrFormat("Failed to parse profile JSON: %s", e.what()));
1557 }
1558#else
1559 return absl::UnimplementedError("JSON support required");
1560#endif
1561}
1562
1566
1568 current_config_ = config;
1569
1570 if (agent_chat_) {
1571 auto* service = agent_chat_->GetAgentService();
1572 if (service) {
1573 cli::AIServiceConfig provider_config;
1574 provider_config.provider =
1575 config.provider.empty() ? "auto" : config.provider;
1576 provider_config.model = config.model;
1577 provider_config.ollama_host = config.ollama_host;
1578 provider_config.gemini_api_key = config.gemini_api_key;
1579 provider_config.openai_api_key = config.openai_api_key;
1580 provider_config.verbose = config.verbose;
1581
1582 auto status = service->ConfigureProvider(provider_config);
1583 if (!status.ok() && toast_manager_) {
1584 toast_manager_->Show(std::string(status.message()), ToastType::kError);
1585 }
1586
1587 auto agent_cfg = service->GetConfig();
1588 agent_cfg.max_tool_iterations = config.max_tool_iterations;
1589 agent_cfg.max_retry_attempts = config.max_retry_attempts;
1590 agent_cfg.verbose = config.verbose;
1591 agent_cfg.show_reasoning = config.show_reasoning;
1592 service->SetConfig(agent_cfg);
1593 }
1594 }
1595}
1596
1598 return agent_chat_ && *agent_chat_->active();
1599}
1600
1602 if (agent_chat_) {
1603 agent_chat_->set_active(active);
1604 }
1605}
1606
1610
1612 if (agent_chat_) {
1613 agent_chat_->set_active(true);
1614 }
1615}
1616
1617absl::StatusOr<AgentEditor::SessionInfo> AgentEditor::HostSession(const std::string& session_name, CollaborationMode mode) {
1618 current_mode_ = mode;
1619
1620 if (mode == CollaborationMode::kLocal) {
1621 auto session_or = local_coordinator_->HostSession(session_name);
1622 if (!session_or.ok()) return session_or.status();
1623
1624 SessionInfo info;
1625 info.session_id = session_or->session_id;
1626 info.session_name = session_or->session_name;
1627 info.participants = session_or->participants;
1628
1629 in_session_ = true;
1633
1634 if (toast_manager_) {
1636 absl::StrFormat("Hosting local session: %s", session_name),
1637 ToastType::kSuccess, 3.0f);
1638 }
1639 return info;
1640 }
1641
1642#ifdef YAZE_WITH_GRPC
1643 if (mode == CollaborationMode::kNetwork) {
1644 if (!network_coordinator_) {
1645 return absl::FailedPreconditionError(
1646 "Network coordinator not initialized. Connect to a server first.");
1647 }
1648
1649 const char* username = std::getenv("USER");
1650 if (!username) {
1651 username = std::getenv("USERNAME");
1652 }
1653 if (!username) {
1654 username = "unknown";
1655 }
1656
1657 auto session_or =
1658 network_coordinator_->HostSession(session_name, username);
1659 if (!session_or.ok()) return session_or.status();
1660
1661 SessionInfo info;
1662 info.session_id = session_or->session_id;
1663 info.session_name = session_or->session_name;
1664 info.participants = session_or->participants;
1665
1666 in_session_ = true;
1670
1671 if (toast_manager_) {
1673 absl::StrFormat("Hosting network session: %s", session_name),
1674 ToastType::kSuccess, 3.0f);
1675 }
1676
1677 return info;
1678 }
1679#endif
1680
1681 return absl::InvalidArgumentError("Unsupported collaboration mode");
1682}
1683
1684absl::StatusOr<AgentEditor::SessionInfo> AgentEditor::JoinSession(const std::string& session_code, CollaborationMode mode) {
1685 current_mode_ = mode;
1686
1687 if (mode == CollaborationMode::kLocal) {
1688 auto session_or = local_coordinator_->JoinSession(session_code);
1689 if (!session_or.ok()) return session_or.status();
1690
1691 SessionInfo info;
1692 info.session_id = session_or->session_id;
1693 info.session_name = session_or->session_name;
1694 info.participants = session_or->participants;
1695
1696 in_session_ = true;
1700
1701 if (toast_manager_) {
1703 absl::StrFormat("Joined local session: %s", session_code),
1704 ToastType::kSuccess, 3.0f);
1705 }
1706
1707 return info;
1708 }
1709
1710#ifdef YAZE_WITH_GRPC
1711 if (mode == CollaborationMode::kNetwork) {
1712 if (!network_coordinator_) {
1713 return absl::FailedPreconditionError(
1714 "Network coordinator not initialized. Connect to a server first.");
1715 }
1716
1717 const char* username = std::getenv("USER");
1718 if (!username) {
1719 username = std::getenv("USERNAME");
1720 }
1721 if (!username) {
1722 username = "unknown";
1723 }
1724
1725 auto session_or = network_coordinator_->JoinSession(session_code, username);
1726 if (!session_or.ok()) return session_or.status();
1727
1728 SessionInfo info;
1729 info.session_id = session_or->session_id;
1730 info.session_name = session_or->session_name;
1731 info.participants = session_or->participants;
1732
1733 in_session_ = true;
1737
1738 if (toast_manager_) {
1740 absl::StrFormat("Joined network session: %s", session_code),
1741 ToastType::kSuccess, 3.0f);
1742 }
1743
1744 return info;
1745 }
1746#endif
1747
1748 return absl::InvalidArgumentError("Unsupported collaboration mode");
1749}
1750
1752 if (!in_session_) {
1753 return absl::FailedPreconditionError("Not in a session");
1754 }
1755
1757 auto status = local_coordinator_->LeaveSession();
1758 if (!status.ok()) return status;
1759 }
1760#ifdef YAZE_WITH_GRPC
1762 if (network_coordinator_) {
1763 auto status = network_coordinator_->LeaveSession();
1764 if (!status.ok()) return status;
1765 }
1766 }
1767#endif
1768
1769 in_session_ = false;
1770 current_session_id_.clear();
1771 current_session_name_.clear();
1772 current_participants_.clear();
1773
1774 if (toast_manager_) {
1775 toast_manager_->Show("Left collaboration session", ToastType::kInfo, 3.0f);
1776 }
1777
1778 return absl::OkStatus();
1779}
1780
1781absl::StatusOr<AgentEditor::SessionInfo> AgentEditor::RefreshSession() {
1782 if (!in_session_) {
1783 return absl::FailedPreconditionError("Not in a session");
1784 }
1785
1787 auto session_or = local_coordinator_->RefreshSession();
1788 if (!session_or.ok()) return session_or.status();
1789
1790 SessionInfo info;
1791 info.session_id = session_or->session_id;
1792 info.session_name = session_or->session_name;
1793 info.participants = session_or->participants;
1795 return info;
1796 }
1797
1798 SessionInfo info;
1802 return info;
1803}
1804
1805absl::Status AgentEditor::CaptureSnapshot(std::filesystem::path* output_path, const CaptureConfig& config) {
1806#ifdef YAZE_WITH_GRPC
1807 using yaze::test::CaptureActiveWindow;
1808 using yaze::test::CaptureHarnessScreenshot;
1809 using yaze::test::CaptureWindowByName;
1810
1811 absl::StatusOr<yaze::test::ScreenshotArtifact> result;
1812 switch (config.mode) {
1814 result = CaptureHarnessScreenshot("");
1815 break;
1817 result = CaptureActiveWindow("");
1818 if (!result.ok()) {
1819 result = CaptureHarnessScreenshot("");
1820 }
1821 break;
1823 if (!config.specific_window_name.empty()) {
1824 result = CaptureWindowByName(config.specific_window_name, "");
1825 } else {
1826 result = CaptureActiveWindow("");
1827 }
1828 if (!result.ok()) {
1829 result = CaptureHarnessScreenshot("");
1830 }
1831 break;
1832 }
1833 }
1834
1835 if (!result.ok()) {
1836 return result.status();
1837 }
1838 *output_path = result->file_path;
1839 return absl::OkStatus();
1840#else
1841 (void)output_path;
1842 (void)config;
1843 return absl::UnimplementedError("Screenshot capture requires YAZE_WITH_GRPC");
1844#endif
1845}
1846
1847absl::Status AgentEditor::SendToGemini(const std::filesystem::path& image_path, const std::string& prompt) {
1848#ifdef YAZE_WITH_GRPC
1849 const char* api_key =
1851 ? std::getenv("GEMINI_API_KEY")
1853 if (!api_key || std::strlen(api_key) == 0) {
1854 return absl::FailedPreconditionError(
1855 "Gemini API key not configured (set GEMINI_API_KEY)");
1856 }
1857
1858 cli::GeminiConfig config;
1859 config.api_key = api_key;
1860 config.model = current_profile_.model.empty() ? "gemini-2.5-flash"
1863
1864 cli::GeminiAIService gemini_service(config);
1865 auto response =
1866 gemini_service.GenerateMultimodalResponse(image_path.string(), prompt);
1867 if (!response.ok()) {
1868 return response.status();
1869 }
1870
1871 if (agent_chat_) {
1872 auto* service = agent_chat_->GetAgentService();
1873 if (service) {
1874 auto history = service->GetHistory();
1875 cli::agent::ChatMessage agent_msg;
1877 agent_msg.message = response->text_response;
1878 agent_msg.timestamp = absl::Now();
1879 history.push_back(agent_msg);
1880 service->ReplaceHistory(history);
1881 }
1882 }
1883
1884 if (toast_manager_) {
1885 toast_manager_->Show("Gemini vision response added to chat",
1886 ToastType::kSuccess, 2.5f);
1887 }
1888 return absl::OkStatus();
1889#else
1890 (void)image_path;
1891 (void)prompt;
1892 return absl::UnimplementedError("Gemini integration requires YAZE_WITH_GRPC");
1893#endif
1894}
1895
1896#ifdef YAZE_WITH_GRPC
1897absl::Status AgentEditor::ConnectToServer(const std::string& server_url) {
1898 try {
1899 network_coordinator_ =
1900 std::make_unique<NetworkCollaborationCoordinator>(server_url);
1901
1902 if (toast_manager_) {
1904 absl::StrFormat("Connected to server: %s", server_url),
1905 ToastType::kSuccess, 3.0f);
1906 }
1907
1908 return absl::OkStatus();
1909 } catch (const std::exception& e) {
1910 return absl::InternalError(
1911 absl::StrFormat("Failed to connect to server: %s", e.what()));
1912 }
1913}
1914
1915void AgentEditor::DisconnectFromServer() {
1917 LeaveSession();
1918 }
1919 network_coordinator_.reset();
1920
1921 if (toast_manager_) {
1922 toast_manager_->Show("Disconnected from server", ToastType::kInfo, 2.5f);
1923 }
1924}
1925
1926bool AgentEditor::IsConnectedToServer() const {
1927 return network_coordinator_ && network_coordinator_->IsConnected();
1928}
1929#endif
1930
1932
1934
1935std::optional<AgentEditor::SessionInfo> AgentEditor::GetCurrentSession() const {
1936 if (!in_session_) return std::nullopt;
1938}
1939
1942
1945
1946} // namespace editor
1947} // namespace yaze
static absl::StatusOr< std::string > LoadTextFile(const std::string &relative_path)
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:24
bool is_loaded() const
Definition rom.h:128
absl::StatusOr< AgentResponse > GenerateMultimodalResponse(const std::string &, const std::string &)
void SetCurrentProfile(const BotProfile &profile)
void ApplyConfig(const AgentConfig &config)
void InitializeWithDependencies(ToastManager *toast_manager, ProposalDrawer *proposal_drawer, Rom *rom)
absl::Status SaveBuilderBlueprint(const std::filesystem::path &path)
absl::StatusOr< SessionInfo > JoinSession(const std::string &session_code, CollaborationMode mode=CollaborationMode::kLocal)
void SetChatActive(bool active)
KnowledgePanelCallback knowledge_panel_callback_
absl::StatusOr< BotProfile > JsonToProfile(const std::string &json) const
absl::StatusOr< SessionInfo > RefreshSession()
absl::Status ImportProfile(const std::filesystem::path &path)
void Initialize() override
ProposalDrawer * proposal_drawer_
std::vector< cli::agent::ChatMessage > cached_history_
AgentBuilderState builder_state_
absl::Status Save() override
std::string current_session_name_
CollaborationMode GetCurrentMode() const
absl::Status SendToGemini(const std::filesystem::path &image_path, const std::string &prompt)
absl::Status EnsureProfilesDirectory()
absl::Status LoadBuilderBlueprint(const std::filesystem::path &path)
absl::Status ExportProfile(const BotProfile &profile, const std::filesystem::path &path)
CollaborationMode current_mode_
absl::Status SaveBotProfile(const BotProfile &profile)
absl::Status DeleteBotProfile(const std::string &name)
std::filesystem::path GetProfilesDirectory() const
ToastManager * toast_manager_
AgentConfig GetCurrentConfig() const
std::unique_ptr< TextEditor > common_tiles_editor_
absl::StatusOr< SessionInfo > HostSession(const std::string &session_name, CollaborationMode mode=CollaborationMode::kLocal)
std::string ProfileToJson(const BotProfile &profile) const
std::unique_ptr< TextEditor > prompt_editor_
std::unique_ptr< AgentCollaborationCoordinator > local_coordinator_
absl::Status CaptureSnapshot(std::filesystem::path *output_path, const CaptureConfig &config)
absl::Status Load() override
absl::Status LoadBotProfile(const std::string &name)
std::vector< BotProfile > GetAllProfiles() const
std::vector< std::string > current_participants_
std::unique_ptr< AgentChat > agent_chat_
absl::Status Update() override
std::vector< BotProfile > loaded_profiles_
std::optional< SessionInfo > GetCurrentSession() const
Rom * rom() const
Definition editor.h:227
EditorDependencies dependencies_
Definition editor.h:237
EditorType type_
Definition editor.h:236
void RegisterEditorPanel(std::unique_ptr< EditorPanel > panel)
Register an EditorPanel instance for central drawing.
ImGui drawer for displaying and managing agent proposals.
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f)
static TestManager & Get()
static absl::StatusOr< std::filesystem::path > GetConfigDirectory()
Get the user-specific configuration directory for YAZE.
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_SETTINGS
Definition icons.h:1699
#define ICON_MD_LINK
Definition icons.h:1090
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_CHAT
Definition icons.h:394
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_NOTE_ADD
Definition icons.h:1330
#define ICON_MD_STAR
Definition icons.h:1848
#define ICON_MD_CHECK
Definition icons.h:397
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_AUTO_AWESOME
Definition icons.h:214
#define ICON_MD_LOOP
Definition icons.h:1156
#define ICON_MD_TIPS_AND_UPDATES
Definition icons.h:1988
#define ICON_MD_EDIT
Definition icons.h:645
#define ICON_MD_AUTO_FIX_HIGH
Definition icons.h:218
#define ICON_MD_GRID_ON
Definition icons.h:896
#define ICON_MD_LIST
Definition icons.h:1094
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_FILE_COPY
Definition icons.h:743
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_TERMINAL
Definition icons.h:1951
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_FOLDER
Definition icons.h:809
#define ICON_MD_DELETE_FOREVER
Definition icons.h:531
#define ICON_MD_CLOUD
Definition icons.h:423
#define ICON_MD_CLOSE
Definition icons.h:418
#define ICON_MD_ANALYTICS
Definition icons.h:154
#define ICON_MD_GAMEPAD
Definition icons.h:866
#define ICON_MD_SMART_TOY
Definition icons.h:1781
#define ICON_MD_HISTORY
Definition icons.h:946
void RenderStatusIndicator(const char *label, bool active)
bool StyledButton(const char *label, const ImVec4 &color, const ImVec2 &size)
const AgentUITheme & GetTheme()
void RenderSectionHeader(const char *icon, const char *label, const ImVec4 &color)
void RenderProviderBadge(const char *provider)
void HelpMarker(const char *desc)
static const LanguageDefinition & CPlusPlus()
struct yaze::editor::AgentEditor::AgentBuilderState::ToolPlan tools
std::vector< std::string > tags
std::vector< std::string > participants