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