#define IMGUI_DEFINE_MATH_OPERATORS #include "app/editor/agent/agent_chat_widget.h" #include #include #include #include #include #include #include #include #include #include "absl/status/status.h" #include "absl/strings/match.h" #include "absl/strings/str_format.h" #include "absl/strings/str_join.h" #include "absl/time/clock.h" #include "absl/time/time.h" #include "app/core/project.h" #include "app/editor/agent/agent_chat_history_codec.h" #include "app/editor/agent/agent_ui_theme.h" #include "app/editor/agent/agent_chat_history_popup.h" #include "app/editor/system/proposal_drawer.h" #include "app/editor/system/toast_manager.h" #include "app/gui/core/icons.h" #include "app/rom.h" #include "imgui/imgui.h" #include "util/file_util.h" #include "util/platform_paths.h" #include #include #if defined(YAZE_WITH_GRPC) #include "app/test/test_manager.h" #endif namespace { namespace fs = std::filesystem; using yaze::cli::agent::ChatMessage; std::filesystem::path ExpandUserPath(std::string path) { if (!path.empty() && path.front() == '~') { const char* home = nullptr; #ifdef _WIN32 home = std::getenv("USERPROFILE"); #else home = std::getenv("HOME"); #endif if (home != nullptr) { path.replace(0, 1, home); } } return std::filesystem::path(path); } std::filesystem::path ResolveHistoryPath(const std::string& session_id = "") { auto config_dir = yaze::util::PlatformPaths::GetConfigDirectory(); if (!config_dir.ok()) { // Fallback to a local directory if config can't be determined. return fs::current_path() / ".yaze" / "agent" / "history" / (session_id.empty() ? "default.json" : session_id + ".json"); } fs::path base = *config_dir; if (base.empty()) { base = ExpandUserPath(".yaze"); } auto directory = base / "agent"; // If in a collaborative session, use shared history if (!session_id.empty()) { directory = directory / "sessions"; return directory / (session_id + "_history.json"); } return directory / "chat_history.json"; } void RenderTable(const ChatMessage::TableData& table_data) { const int column_count = static_cast(table_data.headers.size()); if (column_count <= 0) { ImGui::TextDisabled("(empty)"); return; } if (ImGui::BeginTable("structured_table", column_count, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { for (const auto& header : table_data.headers) { ImGui::TableSetupColumn(header.c_str()); } ImGui::TableHeadersRow(); for (const auto& row : table_data.rows) { ImGui::TableNextRow(); for (int col = 0; col < column_count; ++col) { ImGui::TableSetColumnIndex(col); if (col < static_cast(row.size())) { ImGui::TextWrapped("%s", row[col].c_str()); } else { ImGui::TextUnformatted("-"); } } } ImGui::EndTable(); } } } // namespace namespace yaze { namespace editor { AgentChatWidget::AgentChatWidget() { title_ = "Agent Chat"; memset(input_buffer_, 0, sizeof(input_buffer_)); history_path_ = ResolveHistoryPath(); history_supported_ = AgentChatHistoryCodec::Available(); automation_state_.recent_tests.reserve(8); // Initialize default session if (chat_sessions_.empty()) { chat_sessions_.emplace_back("default", "Main Session"); active_session_index_ = 0; } } void AgentChatWidget::SetRomContext(Rom* rom) { // Track if we've already initialized labels for this ROM instance static Rom* last_rom_initialized = nullptr; agent_service_.SetRomContext(rom); // Only initialize labels ONCE per ROM instance if (rom && rom->is_loaded() && rom->resource_label() && last_rom_initialized != rom) { core::YazeProject project; project.use_embedded_labels = true; auto labels_status = project.InitializeEmbeddedLabels(); if (labels_status.ok()) { rom->resource_label()->labels_ = project.resource_labels; rom->resource_label()->labels_loaded_ = true; last_rom_initialized = rom; // Mark as initialized int total_count = 0; for (const auto& [category, labels] : project.resource_labels) { total_count += labels.size(); } if (toast_manager_) { toast_manager_->Show( absl::StrFormat(ICON_MD_CHECK_CIRCLE " %d labels ready for AI", total_count), ToastType::kSuccess, 2.0f); } } } } void AgentChatWidget::SetToastManager(ToastManager* toast_manager) { toast_manager_ = toast_manager; } void AgentChatWidget::SetProposalDrawer(ProposalDrawer* drawer) { proposal_drawer_ = drawer; if (proposal_drawer_ && !pending_focus_proposal_id_.empty()) { proposal_drawer_->FocusProposal(pending_focus_proposal_id_); pending_focus_proposal_id_.clear(); } } void AgentChatWidget::SetChatHistoryPopup(AgentChatHistoryPopup* popup) { chat_history_popup_ = popup; if (!chat_history_popup_) return; // Set up callback to open this chat window chat_history_popup_->SetOpenChatCallback( [this]() { this->set_active(true); }); // Set up callback to send messages from popup chat_history_popup_->SetSendMessageCallback( [this](const std::string& message) { // Send message through the agent service auto response = agent_service_.SendMessage(message); HandleAgentResponse(response); PersistHistory(); }); // Set up callback to capture snapshots from popup chat_history_popup_->SetCaptureSnapshotCallback([this]() { if (multimodal_callbacks_.capture_snapshot) { std::filesystem::path output_path; auto status = multimodal_callbacks_.capture_snapshot(&output_path); if (status.ok()) { multimodal_state_.last_capture_path = output_path; multimodal_state_.last_updated = absl::Now(); if (toast_manager_) { toast_manager_->Show(ICON_MD_PHOTO " Screenshot captured", ToastType::kSuccess, 2.5f); } } else if (toast_manager_) { toast_manager_->Show( absl::StrFormat("Capture failed: %s", status.message()), ToastType::kError, 3.0f); } } }); // Initial sync SyncHistoryToPopup(); } void AgentChatWidget::EnsureHistoryLoaded() { if (history_loaded_) { return; } history_loaded_ = true; std::error_code ec; auto directory = history_path_.parent_path(); if (!directory.empty()) { std::filesystem::create_directories(directory, ec); if (ec) { if (toast_manager_) { toast_manager_->Show("Unable to prepare chat history directory", ToastType::kError, 5.0f); } return; } } if (!history_supported_) { if (!history_warning_displayed_ && toast_manager_) { toast_manager_->Show( "Chat history requires gRPC/JSON support and is disabled", ToastType::kWarning, 5.0f); history_warning_displayed_ = true; } return; } absl::StatusOr result = AgentChatHistoryCodec::Load(history_path_); if (!result.ok()) { if (result.status().code() == absl::StatusCode::kUnimplemented) { history_supported_ = false; if (!history_warning_displayed_ && toast_manager_) { toast_manager_->Show( "Chat history requires gRPC/JSON support and is disabled", ToastType::kWarning, 5.0f); history_warning_displayed_ = true; } return; } if (toast_manager_) { toast_manager_->Show(absl::StrFormat("Failed to load chat history: %s", result.status().ToString()), ToastType::kError, 6.0f); } return; } AgentChatHistoryCodec::Snapshot snapshot = std::move(result.value()); if (!snapshot.history.empty()) { agent_service_.ReplaceHistory(std::move(snapshot.history)); last_history_size_ = agent_service_.GetHistory().size(); last_proposal_count_ = CountKnownProposals(); history_dirty_ = false; last_persist_time_ = absl::Now(); if (toast_manager_) { toast_manager_->Show("Restored chat history", ToastType::kInfo, 3.5f); } } collaboration_state_.active = snapshot.collaboration.active; collaboration_state_.session_id = snapshot.collaboration.session_id; collaboration_state_.session_name = snapshot.collaboration.session_name; collaboration_state_.participants = snapshot.collaboration.participants; collaboration_state_.last_synced = snapshot.collaboration.last_synced; if (collaboration_state_.session_name.empty() && !collaboration_state_.session_id.empty()) { collaboration_state_.session_name = collaboration_state_.session_id; } multimodal_state_.last_capture_path = snapshot.multimodal.last_capture_path; multimodal_state_.status_message = snapshot.multimodal.status_message; multimodal_state_.last_updated = snapshot.multimodal.last_updated; } void AgentChatWidget::PersistHistory() { if (!history_loaded_ || !history_dirty_) { return; } if (!history_supported_) { history_dirty_ = false; if (!history_warning_displayed_ && toast_manager_) { toast_manager_->Show( "Chat history requires gRPC/JSON support and is disabled", ToastType::kWarning, 5.0f); history_warning_displayed_ = true; } return; } AgentChatHistoryCodec::Snapshot snapshot; snapshot.history = agent_service_.GetHistory(); snapshot.collaboration.active = collaboration_state_.active; snapshot.collaboration.session_id = collaboration_state_.session_id; snapshot.collaboration.session_name = collaboration_state_.session_name; // Sync to popup when persisting SyncHistoryToPopup(); snapshot.collaboration.participants = collaboration_state_.participants; snapshot.collaboration.last_synced = collaboration_state_.last_synced; snapshot.multimodal.last_capture_path = multimodal_state_.last_capture_path; snapshot.multimodal.status_message = multimodal_state_.status_message; snapshot.multimodal.last_updated = multimodal_state_.last_updated; absl::Status status = AgentChatHistoryCodec::Save(history_path_, snapshot); if (!status.ok()) { if (status.code() == absl::StatusCode::kUnimplemented) { history_supported_ = false; if (!history_warning_displayed_ && toast_manager_) { toast_manager_->Show( "Chat history requires gRPC/JSON support and is disabled", ToastType::kWarning, 5.0f); history_warning_displayed_ = true; } history_dirty_ = false; return; } if (toast_manager_) { toast_manager_->Show(absl::StrFormat("Failed to persist chat history: %s", status.ToString()), ToastType::kError, 6.0f); } return; } history_dirty_ = false; last_persist_time_ = absl::Now(); } int AgentChatWidget::CountKnownProposals() const { int total = 0; const auto& history = agent_service_.GetHistory(); for (const auto& message : history) { if (message.metrics.has_value()) { total = std::max(total, message.metrics->total_proposals); } else if (message.proposal.has_value()) { ++total; } } return total; } void AgentChatWidget::FocusProposalDrawer(const std::string& proposal_id) { if (proposal_id.empty()) { return; } if (proposal_drawer_) { proposal_drawer_->FocusProposal(proposal_id); } pending_focus_proposal_id_ = proposal_id; } void AgentChatWidget::NotifyProposalCreated(const ChatMessage& msg, int new_total_proposals) { int delta = std::max(1, new_total_proposals - last_proposal_count_); if (toast_manager_) { if (msg.proposal.has_value()) { const auto& proposal = *msg.proposal; toast_manager_->Show( absl::StrFormat("%s Proposal %s ready (%d change%s)", ICON_MD_PREVIEW, proposal.id, proposal.change_count, proposal.change_count == 1 ? "" : "s"), ToastType::kSuccess, 5.5f); } else { toast_manager_->Show( absl::StrFormat("%s %d new proposal%s queued", ICON_MD_PREVIEW, delta, delta == 1 ? "" : "s"), ToastType::kSuccess, 4.5f); } } if (msg.proposal.has_value()) { FocusProposalDrawer(msg.proposal->id); } } void AgentChatWidget::HandleAgentResponse( const absl::StatusOr& response) { if (!response.ok()) { if (toast_manager_) { toast_manager_->Show( absl::StrFormat("Agent error: %s", response.status().message()), ToastType::kError, 5.0f); } return; } const ChatMessage& message = response.value(); int total = CountKnownProposals(); if (message.metrics.has_value()) { total = std::max(total, message.metrics->total_proposals); } if (total > last_proposal_count_) { NotifyProposalCreated(message, total); } last_proposal_count_ = std::max(last_proposal_count_, total); // Sync history to popup after response SyncHistoryToPopup(); } void AgentChatWidget::RenderMessage(const ChatMessage& msg, int index) { // Skip internal messages (tool results meant only for the LLM) if (msg.is_internal) { return; } ImGui::PushID(index); const auto& theme = AgentUI::GetTheme(); const bool from_user = (msg.sender == ChatMessage::Sender::kUser); const ImVec4 header_color = from_user ? theme.user_message_color : theme.agent_message_color; const char* header_label = from_user ? "You" : "Agent"; ImGui::TextColored(header_color, "%s", header_label); ImGui::SameLine(); ImGui::TextDisabled( "%s", absl::FormatTime("%H:%M:%S", msg.timestamp, absl::LocalTimeZone()) .c_str()); // Add copy button for all messages ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, theme.button_copy); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, theme.button_copy_hover); if (ImGui::SmallButton(ICON_MD_CONTENT_COPY)) { std::string copy_text = msg.message; if (copy_text.empty() && msg.json_pretty.has_value()) { copy_text = *msg.json_pretty; } ImGui::SetClipboardText(copy_text.c_str()); if (toast_manager_) { toast_manager_->Show("Message copied", ToastType::kSuccess, 2.0f); } } ImGui::PopStyleColor(2); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Copy to clipboard"); } ImGui::Indent(); if (msg.table_data.has_value()) { RenderTable(*msg.table_data); } else if (msg.json_pretty.has_value()) { // Don't show JSON as a message - it's internal structure const auto& theme = AgentUI::GetTheme(); ImGui::PushStyleColor(ImGuiCol_Text, theme.json_text_color); ImGui::TextDisabled(ICON_MD_DATA_OBJECT " (Structured response)"); ImGui::PopStyleColor(); } else { ImGui::TextWrapped("%s", msg.message.c_str()); } if (msg.proposal.has_value()) { RenderProposalQuickActions(msg, index); } ImGui::Unindent(); ImGui::Spacing(); ImGui::Separator(); ImGui::PopID(); } void AgentChatWidget::RenderProposalQuickActions(const ChatMessage& msg, int index) { if (!msg.proposal.has_value()) { return; } const auto& theme = AgentUI::GetTheme(); const auto& proposal = *msg.proposal; ImGui::PushStyleColor(ImGuiCol_ChildBg, theme.proposal_panel_bg); ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); ImGui::BeginChild(absl::StrFormat("proposal_panel_%d", index).c_str(), ImVec2(0, ImGui::GetFrameHeight() * 3.2f), true, ImGuiWindowFlags_None); ImGui::TextColored(theme.proposal_accent, "%s Proposal %s", ICON_MD_PREVIEW, proposal.id.c_str()); ImGui::Text("Changes: %d", proposal.change_count); ImGui::Text("Commands: %d", proposal.executed_commands); if (!proposal.sandbox_rom_path.empty()) { ImGui::TextDisabled("Sandbox: %s", proposal.sandbox_rom_path.string().c_str()); } if (!proposal.proposal_json_path.empty()) { ImGui::TextDisabled("Manifest: %s", proposal.proposal_json_path.string().c_str()); } if (ImGui::SmallButton( absl::StrFormat("%s Review", ICON_MD_VISIBILITY).c_str())) { FocusProposalDrawer(proposal.id); } ImGui::SameLine(); if (ImGui::SmallButton( absl::StrFormat("%s Copy ID", ICON_MD_CONTENT_COPY).c_str())) { ImGui::SetClipboardText(proposal.id.c_str()); if (toast_manager_) { toast_manager_->Show("Proposal ID copied", ToastType::kInfo, 2.5f); } } ImGui::EndChild(); ImGui::PopStyleVar(); ImGui::PopStyleColor(); } void AgentChatWidget::RenderHistory() { const auto& theme = AgentUI::GetTheme(); const auto& history = agent_service_.GetHistory(); float reserved_height = ImGui::GetFrameHeightWithSpacing() * 4.0f; reserved_height += 100.0f; // Reduced to 100 for much taller chat area // Styled chat history container ImGui::PushStyleColor(ImGuiCol_ChildBg, theme.code_bg_color); if (ImGui::BeginChild("History", ImVec2(0, -reserved_height), true, ImGuiWindowFlags_AlwaysVerticalScrollbar)) { if (history.empty()) { // Centered empty state ImVec2 text_size = ImGui::CalcTextSize("No messages yet"); ImVec2 avail = ImGui::GetContentRegionAvail(); ImGui::SetCursorPosX((avail.x - text_size.x) / 2); ImGui::SetCursorPosY((avail.y - text_size.y) / 2); ImGui::TextDisabled(ICON_MD_CHAT " No messages yet"); ImGui::SetCursorPosX( (avail.x - ImGui::CalcTextSize("Start typing below to begin").x) / 2); ImGui::TextDisabled("Start typing below to begin"); } else { ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 12)); // More spacing between messages for (size_t index = 0; index < history.size(); ++index) { RenderMessage(history[index], static_cast(index)); } ImGui::PopStyleVar(); } if (history.size() > last_history_size_) { ImGui::SetScrollHereY(1.0f); } } ImGui::EndChild(); ImGui::PopStyleColor(); // Pop the color we pushed at line 531 last_history_size_ = history.size(); } void AgentChatWidget::RenderInputBox() { const auto& theme = AgentUI::GetTheme(); ImGui::Separator(); ImGui::TextColored(theme.command_text_color, ICON_MD_EDIT " Message:"); bool submitted = ImGui::InputTextMultiline( "##agent_input", input_buffer_, sizeof(input_buffer_), ImVec2(-1, 60.0f), ImGuiInputTextFlags_None); // Check for Ctrl+Enter to send (Enter alone adds newline) bool send = false; if (ImGui::IsItemFocused()) { if (ImGui::IsKeyPressed(ImGuiKey_Enter) && ImGui::GetIO().KeyCtrl) { send = true; } } ImGui::Spacing(); // Send button row ImGui::PushStyleColor(ImGuiCol_Button, theme.provider_gemini); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(theme.provider_gemini.x * 1.2f, theme.provider_gemini.y * 1.1f, theme.provider_gemini.z, theme.provider_gemini.w)); if (ImGui::Button(absl::StrFormat("%s Send", ICON_MD_SEND).c_str(), ImVec2(140, 0)) || send) { if (std::strlen(input_buffer_) > 0 && !waiting_for_response_) { history_dirty_ = true; EnsureHistoryLoaded(); pending_message_ = input_buffer_; waiting_for_response_ = true; memset(input_buffer_, 0, sizeof(input_buffer_)); // Send in next frame to avoid blocking // For now, send synchronously but show thinking indicator auto response = agent_service_.SendMessage(pending_message_); HandleAgentResponse(response); PersistHistory(); waiting_for_response_ = false; ImGui::SetKeyboardFocusHere(-1); } } ImGui::PopStyleColor(2); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Send message (Ctrl+Enter)"); } ImGui::SameLine(); ImGui::TextDisabled(ICON_MD_INFO " Ctrl+Enter: send • Enter: newline"); // Action buttons row below ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.5f, 0.0f, 0.7f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.843f, 0.0f, 0.9f)); if (ImGui::SmallButton(ICON_MD_DELETE_FOREVER " Clear")) { agent_service_.ResetConversation(); if (toast_manager_) { toast_manager_->Show("Conversation cleared", ToastType::kSuccess); } } ImGui::PopStyleColor(2); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Clear all messages from conversation"); } ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.4f, 0.35f, 0.6f, 0.7f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.502f, 0.0f, 0.502f, 0.9f)); if (ImGui::SmallButton(ICON_MD_PREVIEW " Proposals")) { if (proposal_drawer_) { // Focus proposal drawer } } ImGui::PopStyleColor(2); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("View code proposals"); } // Multimodal Vision controls integrated ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.5f, 0.7f, 0.7f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.196f, 0.6f, 0.8f, 0.9f)); if (ImGui::SmallButton(ICON_MD_PHOTO_CAMERA " Capture")) { // Quick capture with current mode if (multimodal_callbacks_.capture_snapshot) { std::filesystem::path captured_path; auto status = multimodal_callbacks_.capture_snapshot(&captured_path); if (status.ok()) { multimodal_state_.last_capture_path = captured_path; if (toast_manager_) { toast_manager_->Show("Screenshot captured", ToastType::kSuccess); } } else if (toast_manager_) { toast_manager_->Show( absl::StrFormat("Capture failed: %s", status.message()), ToastType::kError); } } } ImGui::PopStyleColor(2); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Capture screenshot for vision analysis"); } ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.3f, 0.3f, 0.7f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.863f, 0.078f, 0.235f, 0.9f)); if (ImGui::SmallButton(ICON_MD_STOP " Stop")) { // Stop generation (if implemented) if (toast_manager_) { toast_manager_->Show("Stop not yet implemented", ToastType::kWarning); } } ImGui::PopStyleColor(2); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Stop current generation"); } ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.5f, 0.3f, 0.7f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.133f, 0.545f, 0.133f, 0.9f)); if (ImGui::SmallButton(ICON_MD_SAVE " Export")) { // Export conversation if (toast_manager_) { toast_manager_->Show("Export not yet implemented", ToastType::kWarning); } } ImGui::PopStyleColor(2); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Export conversation history"); } // Vision prompt (inline when image captured) if (multimodal_state_.last_capture_path.has_value()) { ImGui::Spacing(); ImGui::TextColored(ImVec4(0.196f, 0.6f, 0.8f, 1.0f), ICON_MD_IMAGE " Vision prompt:"); ImGui::SetNextItemWidth(-200); ImGui::InputTextWithHint( "##quick_vision_prompt", "Ask about the screenshot...", multimodal_prompt_buffer_, sizeof(multimodal_prompt_buffer_)); ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.5f, 0.7f, 0.8f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.196f, 0.6f, 0.8f, 1.0f)); if (ImGui::Button(ICON_MD_SEND " Analyze##vision_send", ImVec2(180, 0))) { if (multimodal_callbacks_.send_to_gemini && !multimodal_state_.last_capture_path->empty()) { std::string prompt = multimodal_prompt_buffer_; auto status = multimodal_callbacks_.send_to_gemini( *multimodal_state_.last_capture_path, prompt); if (status.ok() && toast_manager_) { toast_manager_->Show("Vision analysis requested", ToastType::kSuccess); } } } ImGui::PopStyleColor(2); } } void AgentChatWidget::Draw() { if (!active_) { return; } EnsureHistoryLoaded(); // Poll for new messages in collaborative sessions PollSharedHistory(); ImGui::SetNextWindowSize(ImVec2(1400, 1000), ImGuiCond_FirstUseEver); ImGui::Begin(title_.c_str(), &active_, ImGuiWindowFlags_MenuBar); // Simplified menu bar if (ImGui::BeginMenuBar()) { if (ImGui::BeginMenu(ICON_MD_MENU " Actions")) { if (ImGui::MenuItem(ICON_MD_DELETE_FOREVER " Clear History")) { agent_service_.ResetConversation(); SyncHistoryToPopup(); if (toast_manager_) { toast_manager_->Show("Chat history cleared", ToastType::kInfo, 2.5f); } } ImGui::Separator(); if (ImGui::MenuItem(ICON_MD_REFRESH " Reset Conversation")) { agent_service_.ResetConversation(); SyncHistoryToPopup(); if (toast_manager_) { toast_manager_->Show("Conversation reset", ToastType::kInfo, 2.5f); } } ImGui::Separator(); if (ImGui::MenuItem(ICON_MD_SAVE " Export History")) { if (toast_manager_) { toast_manager_->Show("Export not yet implemented", ToastType::kWarning); } } ImGui::Separator(); if (ImGui::MenuItem(ICON_MD_ADD " New Session Tab")) { // Create new session if (!chat_sessions_.empty()) { ChatSession new_session( absl::StrFormat("session_%d", chat_sessions_.size()), absl::StrFormat("Session %d", chat_sessions_.size() + 1)); chat_sessions_.push_back(std::move(new_session)); active_session_index_ = chat_sessions_.size() - 1; if (toast_manager_) { toast_manager_->Show("New session created", ToastType::kSuccess); } } } ImGui::EndMenu(); } // Session tabs in menu bar (if multiple sessions) if (!chat_sessions_.empty() && chat_sessions_.size() > 1) { ImGui::Separator(); for (size_t i = 0; i < chat_sessions_.size(); ++i) { bool is_active = (i == active_session_index_); if (is_active) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.2f, 0.8f, 0.4f, 1.0f)); } if (ImGui::MenuItem(chat_sessions_[i].name.c_str(), nullptr, is_active)) { active_session_index_ = i; history_loaded_ = false; // Trigger reload SyncHistoryToPopup(); } if (is_active) { ImGui::PopStyleColor(); } } } ImGui::EndMenuBar(); } // Update reactive status color collaboration_status_color_ = collaboration_state_.active ? ImVec4(0.133f, 0.545f, 0.133f, 1.0f) : ImVec4(0.6f, 0.6f, 0.6f, 1.0f); // Connection status bar at top (taller for better visibility) ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImVec2 bar_start = ImGui::GetCursorScreenPos(); ImVec2 bar_size(ImGui::GetContentRegionAvail().x, 60); // Increased from 55 // Gradient background ImU32 color_top = ImGui::GetColorU32(ImVec4(0.18f, 0.22f, 0.28f, 1.0f)); ImU32 color_bottom = ImGui::GetColorU32(ImVec4(0.12f, 0.16f, 0.22f, 1.0f)); draw_list->AddRectFilledMultiColor( bar_start, ImVec2(bar_start.x + bar_size.x, bar_start.y + bar_size.y), color_top, color_top, color_bottom, color_bottom); // Colored accent bar based on provider ImVec4 accent_color = (agent_config_.ai_provider == "ollama") ? ImVec4(0.2f, 0.8f, 0.4f, 1.0f) : (agent_config_.ai_provider == "gemini") ? ImVec4(0.196f, 0.6f, 0.8f, 1.0f) : ImVec4(0.6f, 0.6f, 0.6f, 1.0f); draw_list->AddRectFilled(bar_start, ImVec2(bar_start.x + bar_size.x, bar_start.y + 3), ImGui::GetColorU32(accent_color)); ImGui::BeginChild("AgentChat_ConnectionBar", bar_size, false, ImGuiWindowFlags_NoScrollbar); ImGui::PushID("ConnectionBar"); { // Center content vertically in the 55px bar float content_height = ImGui::GetFrameHeight(); float vertical_padding = (55.0f - content_height) / 2.0f; ImGui::SetCursorPosY(ImGui::GetCursorPosY() + vertical_padding); // Compact single row layout (restored) ImGui::TextColored(accent_color, ICON_MD_SMART_TOY); ImGui::SameLine(); ImGui::SetNextItemWidth(95); const char* providers[] = {"Mock", "Ollama", "Gemini"}; int current_provider = (agent_config_.ai_provider == "mock") ? 0 : (agent_config_.ai_provider == "ollama") ? 1 : 2; if (ImGui::Combo("##main_provider", ¤t_provider, providers, 3)) { agent_config_.ai_provider = (current_provider == 0) ? "mock" : (current_provider == 1) ? "ollama" : "gemini"; // Auto-populate default models if (agent_config_.ai_provider == "ollama") { strncpy(agent_config_.model_buffer, "qwen2.5-coder:7b", sizeof(agent_config_.model_buffer) - 1); agent_config_.ai_model = agent_config_.model_buffer; } else if (agent_config_.ai_provider == "gemini") { strncpy(agent_config_.model_buffer, "gemini-2.5-flash", sizeof(agent_config_.model_buffer) - 1); agent_config_.ai_model = agent_config_.model_buffer; } } ImGui::SameLine(); if (agent_config_.ai_provider != "mock") { ImGui::SetNextItemWidth(150); ImGui::InputTextWithHint("##main_model", "Model name...", agent_config_.model_buffer, sizeof(agent_config_.model_buffer)); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("AI model name"); } } // Gemini API key input ImGui::SameLine(); if (agent_config_.ai_provider == "gemini") { ImGui::SetNextItemWidth(200); if (ImGui::InputTextWithHint("##main_api_key", "API Key (or load from env)...", agent_config_.gemini_key_buffer, sizeof(agent_config_.gemini_key_buffer), ImGuiInputTextFlags_Password)) { agent_config_.gemini_api_key = agent_config_.gemini_key_buffer; } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Gemini API Key (hidden)"); } ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.5f, 0.7f, 0.7f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.196f, 0.6f, 0.8f, 1.0f)); if (ImGui::SmallButton(ICON_MD_REFRESH)) { const char* gemini_key = nullptr; #ifdef _WIN32 char* env_key = nullptr; size_t len = 0; if (_dupenv_s(&env_key, &len, "GEMINI_API_KEY") == 0 && env_key != nullptr) { strncpy(agent_config_.gemini_key_buffer, env_key, sizeof(agent_config_.gemini_key_buffer) - 1); agent_config_.gemini_api_key = env_key; free(env_key); } #else gemini_key = std::getenv("GEMINI_API_KEY"); if (gemini_key) { strncpy(agent_config_.gemini_key_buffer, gemini_key, sizeof(agent_config_.gemini_key_buffer) - 1); agent_config_.gemini_api_key = gemini_key; } #endif if (!agent_config_.gemini_api_key.empty() && toast_manager_) { toast_manager_->Show("Key loaded", ToastType::kSuccess, 1.5f); } } ImGui::PopStyleColor(2); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Load from GEMINI_API_KEY"); } } ImGui::SameLine(); ImGui::Checkbox(ICON_MD_VISIBILITY, &agent_config_.show_reasoning); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Show reasoning"); } // Session management button ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.4f, 0.4f, 0.6f, 0.7f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.5f, 0.5f, 0.7f, 0.9f)); if (ImGui::SmallButton( absl::StrFormat("%s %d", ICON_MD_TAB, static_cast(chat_sessions_.size())) .c_str())) { ImGui::OpenPopup("SessionsPopup"); } ImGui::PopStyleColor(2); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Manage chat sessions"); } // Sessions popup if (ImGui::BeginPopup("SessionsPopup")) { ImGui::TextColored(ImVec4(1.0f, 0.843f, 0.0f, 1.0f), ICON_MD_TAB " Chat Sessions"); ImGui::Separator(); if (ImGui::Button(ICON_MD_ADD " New Session", ImVec2(200, 0))) { std::string session_id = absl::StrFormat( "session_%d", static_cast(chat_sessions_.size() + 1)); std::string session_name = absl::StrFormat( "Chat %d", static_cast(chat_sessions_.size() + 1)); chat_sessions_.emplace_back(session_id, session_name); active_session_index_ = static_cast(chat_sessions_.size() - 1); if (toast_manager_) { toast_manager_->Show("New session created", ToastType::kSuccess); } ImGui::CloseCurrentPopup(); } if (!chat_sessions_.empty()) { ImGui::Spacing(); ImGui::TextDisabled("Active Sessions:"); ImGui::Separator(); for (size_t i = 0; i < chat_sessions_.size(); ++i) { ImGui::PushID(static_cast(i)); bool is_active = (active_session_index_ == static_cast(i)); if (ImGui::Selectable(absl::StrFormat("%s %s%s", ICON_MD_CHAT, chat_sessions_[i].name, is_active ? " (active)" : "") .c_str(), is_active)) { active_session_index_ = static_cast(i); ImGui::CloseCurrentPopup(); } ImGui::PopID(); } } ImGui::EndPopup(); } // Session status (right side) if (collaboration_state_.active) { ImGui::SameLine(ImGui::GetContentRegionAvail().x - 25); ImGui::TextColored(collaboration_status_color_, ICON_MD_CHECK_CIRCLE); } } ImGui::PopID(); ImGui::EndChild(); ImGui::Spacing(); // Main layout: Chat area (left, 70%) + Control panels (right, 30%) if (ImGui::BeginTable("AgentChat_MainLayout", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_ContextMenuInBody)) { ImGui::TableSetupColumn("Chat", ImGuiTableColumnFlags_WidthStretch, 0.7f); ImGui::TableSetupColumn("Controls", ImGuiTableColumnFlags_WidthStretch, 0.3f); ImGui::TableHeadersRow(); ImGui::TableNextRow(); // LEFT: Chat area with ROM sync below ImGui::TableSetColumnIndex(0); ImGui::PushID("ChatColumn"); // Chat history and input (main area) RenderHistory(); RenderInputBox(); // ROM Sync inline below chat (when active) if (collaboration_state_.active || !rom_sync_state_.current_rom_hash.empty()) { ImGui::Spacing(); ImGui::Separator(); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 4)); ImGui::TextColored(ImVec4(1.0f, 0.647f, 0.0f, 1.0f), ICON_MD_SYNC " ROM Sync"); ImGui::SameLine(); if (!rom_sync_state_.current_rom_hash.empty()) { ImGui::TextColored( ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "%s", rom_sync_state_.current_rom_hash.substr(0, 12).c_str()); } ImGui::PopStyleVar(); } ImGui::PopID(); // RIGHT: Control panels (collapsible sections) ImGui::TableSetColumnIndex(1); ImGui::PushID("ControlsColumn"); ImGui::BeginChild("AgentChat_ControlPanels", ImVec2(0, 0), false); // All panels always visible (dense layout) ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6, 6)); // Tighter spacing ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 3)); // Compact padding if (ImGui::BeginTable("##commands_and_multimodal", 2)) { ImGui::TableSetupColumn("Commands", ImGuiTableColumnFlags_WidthFixed, 180); ImGui::TableSetupColumn("Multimodal", ImGuiTableColumnFlags_WidthFixed, ImGui::GetContentRegionAvail().x - 180); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); RenderZ3EDCommandPanel(); ImGui::TableSetColumnIndex(1); RenderMultimodalPanel(); ImGui::EndTable(); } RenderAutomationPanel(); RenderCollaborationPanel(); RenderRomSyncPanel(); RenderProposalManagerPanel(); RenderHarnessPanel(); RenderSystemPromptEditor(); ImGui::PopStyleVar(2); ImGui::EndChild(); ImGui::PopID(); ImGui::EndTable(); } ImGui::End(); } void AgentChatWidget::RenderCollaborationPanel() { ImGui::PushID("CollabPanel"); // Tighter style for more content ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 3)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(3, 2)); // Update reactive status color const bool connected = collaboration_state_.active; collaboration_status_color_ = connected ? ImVec4(0.133f, 0.545f, 0.133f, 1.0f) : ImVec4(0.6f, 0.6f, 0.6f, 1.0f); // Always visible (no collapsing header) ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.12f, 0.14f, 0.18f, 0.95f)); ImGui::BeginChild("CollabPanel", ImVec2(0, 140), true); // reduced height ImGui::TextColored(ImVec4(1.0f, 0.843f, 0.0f, 1.0f), ICON_MD_PEOPLE " Collaboration"); ImGui::SameLine(); ImGui::TextColored(ImVec4(1.0f, 0.843f, 0.0f, 1.0f), ICON_MD_SETTINGS_ETHERNET " Mode:"); ImGui::SameLine(); ImGui::RadioButton(ICON_MD_FOLDER " Local##collab_mode_local", reinterpret_cast(&collaboration_state_.mode), static_cast(CollaborationMode::kLocal)); ImGui::SameLine(); ImGui::RadioButton(ICON_MD_WIFI " Network##collab_mode_network", reinterpret_cast(&collaboration_state_.mode), static_cast(CollaborationMode::kNetwork)); // Main content in table layout (fixed size to prevent auto-resize) if (ImGui::BeginTable("Collab_MainTable", 2, ImGuiTableFlags_BordersInnerV)) { ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 150); ImGui::TableSetupColumn("Controls", ImGuiTableColumnFlags_WidthFixed, ImGui::GetContentRegionAvail().x - 150); ImGui::TableNextRow(); // LEFT COLUMN: Session Details ImGui::TableSetColumnIndex(0); ImGui::BeginGroup(); ImGui::PushID("StatusColumn"); ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.15f, 0.2f, 0.18f, 0.4f)); ImGui::BeginChild("Collab_SessionDetails", ImVec2(0, 60), true); // reduced height ImGui::TextColored(ImVec4(1.0f, 0.843f, 0.0f, 1.0f), ICON_MD_INFO " Session:"); if (connected) { ImGui::TextColored(collaboration_status_color_, ICON_MD_CHECK_CIRCLE " Connected"); } else { ImGui::TextDisabled(ICON_MD_CANCEL " Not connected"); } if (collaboration_state_.mode == CollaborationMode::kNetwork) { ImGui::TextColored(ImVec4(0.196f, 0.6f, 0.8f, 1.0f), ICON_MD_CLOUD " Server:"); ImGui::TextUnformatted(collaboration_state_.server_url.c_str()); } if (!collaboration_state_.session_name.empty()) { ImGui::TextColored(collaboration_status_color_, ICON_MD_LABEL " %s", collaboration_state_.session_name.c_str()); } if (!collaboration_state_.session_id.empty()) { ImGui::TextColored(collaboration_status_color_, ICON_MD_KEY " %s", collaboration_state_.session_id.c_str()); ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.4f, 0.4f, 0.6f, 0.6f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.416f, 0.353f, 0.804f, 1.0f)); if (ImGui::SmallButton(ICON_MD_CONTENT_COPY "##copy_session_id")) { ImGui::SetClipboardText(collaboration_state_.session_id.c_str()); if (toast_manager_) { toast_manager_->Show("Session code copied!", ToastType::kSuccess, 2.0f); } } ImGui::PopStyleColor(2); } if (collaboration_state_.last_synced != absl::InfinitePast()) { ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), ICON_MD_ACCESS_TIME " %s", absl::FormatTime("%H:%M:%S", collaboration_state_.last_synced, absl::LocalTimeZone()).c_str()); } ImGui::EndChild(); ImGui::PopStyleColor(); // Participants list below session details ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.12f, 0.16f, 0.14f, 0.4f)); ImGui::BeginChild("Collab_ParticipantsList", ImVec2(0, 0), true); if (collaboration_state_.participants.empty()) { ImGui::TextDisabled(ICON_MD_PEOPLE " No participants"); } else { ImGui::TextColored(collaboration_status_color_, ICON_MD_PEOPLE " %zu", collaboration_state_.participants.size()); for (size_t i = 0; i < collaboration_state_.participants.size(); ++i) { ImGui::PushID(static_cast(i)); ImGui::BulletText("%s", collaboration_state_.participants[i].c_str()); ImGui::PopID(); } } ImGui::EndChild(); ImGui::PopStyleColor(); ImGui::PopID(); // StatusColumn ImGui::EndGroup(); // RIGHT COLUMN: Controls ImGui::TableSetColumnIndex(1); ImGui::BeginGroup(); ImGui::PushID("ControlsColumn"); ImGui::BeginChild("Collab_Controls", ImVec2(0, 0), false); const bool can_host = static_cast(collaboration_callbacks_.host_session); const bool can_join = static_cast(collaboration_callbacks_.join_session); const bool can_leave = static_cast(collaboration_callbacks_.leave_session); const bool can_refresh = static_cast(collaboration_callbacks_.refresh_session); // Network mode: Show server URL input with styling if (collaboration_state_.mode == CollaborationMode::kNetwork) { ImGui::TextColored(ImVec4(0.196f, 0.6f, 0.8f, 1.0f), ICON_MD_CLOUD); ImGui::SameLine(); ImGui::SetNextItemWidth(100); ImGui::InputText("##collab_server_url", server_url_buffer_, IM_ARRAYSIZE(server_url_buffer_)); ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.5f, 0.7f, 0.8f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.196f, 0.6f, 0.8f, 1.0f)); if (ImGui::SmallButton(ICON_MD_LINK "##connect_server_btn")) { collaboration_state_.server_url = server_url_buffer_; if (toast_manager_) { toast_manager_->Show("Connecting to server...", ToastType::kInfo, 3.0f); } } ImGui::PopStyleColor(2); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Connect to collaboration server"); } } // Host session ImGui::TextColored(ImVec4(1.0f, 0.843f, 0.0f, 1.0f), ICON_MD_ADD_CIRCLE); ImGui::SameLine(); ImGui::SetNextItemWidth(100); ImGui::InputTextWithHint("##collab_session_name", "Session name...", session_name_buffer_, IM_ARRAYSIZE(session_name_buffer_)); ImGui::SameLine(); if (!can_host) ImGui::BeginDisabled(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.5f, 0.0f, 0.8f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.843f, 0.0f, 1.0f)); if (ImGui::SmallButton(ICON_MD_ROCKET_LAUNCH "##host_session_btn")) { std::string name = session_name_buffer_; if (name.empty()) { if (toast_manager_) { toast_manager_->Show("Enter a session name first", ToastType::kWarning, 3.0f); } } else { auto session_or = collaboration_callbacks_.host_session(name); if (session_or.ok()) { ApplyCollaborationSession(session_or.value(), /*update_action_timestamp=*/true); std::snprintf(join_code_buffer_, sizeof(join_code_buffer_), "%s", collaboration_state_.session_id.c_str()); session_name_buffer_[0] = '\0'; if (toast_manager_) { toast_manager_->Show(absl::StrFormat("Hosting session %s", collaboration_state_.session_id.c_str()), ToastType::kSuccess, 3.5f); } MarkHistoryDirty(); } else if (toast_manager_) { toast_manager_->Show(absl::StrFormat("Failed to host: %s", session_or.status().message()), ToastType::kError, 5.0f); } } } ImGui::PopStyleColor(2); if (!can_host) { if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { ImGui::SetTooltip("Provide host_session callback to enable hosting"); } ImGui::EndDisabled(); } else if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Host a new collaboration session"); } // Join session ImGui::TextColored(ImVec4(0.133f, 0.545f, 0.133f, 1.0f), ICON_MD_LOGIN); ImGui::SameLine(); ImGui::SetNextItemWidth(100); ImGui::InputTextWithHint("##collab_join_code", "Session code...", join_code_buffer_, IM_ARRAYSIZE(join_code_buffer_)); ImGui::SameLine(); if (!can_join) ImGui::BeginDisabled(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.1f, 0.4f, 0.1f, 0.8f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.133f, 0.545f, 0.133f, 1.0f)); if (ImGui::SmallButton(ICON_MD_MEETING_ROOM "##join_session_btn")) { std::string code = join_code_buffer_; if (code.empty()) { if (toast_manager_) { toast_manager_->Show("Enter a session code first", ToastType::kWarning, 3.0f); } } else { auto session_or = collaboration_callbacks_.join_session(code); if (session_or.ok()) { ApplyCollaborationSession(session_or.value(), /*update_action_timestamp=*/true); std::snprintf(join_code_buffer_, sizeof(join_code_buffer_), "%s", collaboration_state_.session_id.c_str()); if (toast_manager_) { toast_manager_->Show(absl::StrFormat("Joined session %s", collaboration_state_.session_id.c_str()), ToastType::kSuccess, 3.5f); } MarkHistoryDirty(); } else if (toast_manager_) { toast_manager_->Show(absl::StrFormat("Failed to join: %s", session_or.status().message()), ToastType::kError, 5.0f); } } } ImGui::PopStyleColor(2); if (!can_join) { if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { ImGui::SetTooltip("Provide join_session callback to enable joining"); } ImGui::EndDisabled(); } else if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Join an existing collaboration session"); } // Leave/Refresh if (collaboration_state_.active) { if (!can_leave) ImGui::BeginDisabled(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.2f, 0.2f, 0.8f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.863f, 0.078f, 0.235f, 1.0f)); if (ImGui::SmallButton(ICON_MD_LOGOUT "##leave_session_btn")) { absl::Status status = collaboration_callbacks_.leave_session ? collaboration_callbacks_.leave_session() : absl::OkStatus(); if (status.ok()) { collaboration_state_ = CollaborationState{}; join_code_buffer_[0] = '\0'; if (toast_manager_) { toast_manager_->Show("Left collaborative session", ToastType::kInfo, 3.0f); } MarkHistoryDirty(); } else if (toast_manager_) { toast_manager_->Show(absl::StrFormat("Failed to leave: %s", status.message()), ToastType::kError, 5.0f); } } ImGui::PopStyleColor(2); if (!can_leave) ImGui::EndDisabled(); ImGui::SameLine(); if (!can_refresh) ImGui::BeginDisabled(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.4f, 0.4f, 0.6f, 0.8f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.416f, 0.353f, 0.804f, 1.0f)); if (ImGui::SmallButton(ICON_MD_REFRESH "##refresh_collab_btn")) { RefreshCollaboration(); } ImGui::PopStyleColor(2); if (!can_refresh && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { ImGui::SetTooltip("Provide refresh_session callback to enable"); } if (!can_refresh) ImGui::EndDisabled(); } else { ImGui::TextDisabled(ICON_MD_INFO " Start or join a session to collaborate."); } ImGui::EndChild(); // Collab_Controls ImGui::PopID(); // ControlsColumn ImGui::EndGroup(); ImGui::EndTable(); } ImGui::EndChild(); ImGui::PopStyleColor(); // Pop the ChildBg color from line 1091 ImGui::PopStyleVar(2); // Pop the 2 StyleVars from lines 1082-1083 ImGui::PopID(); // CollabPanel } void AgentChatWidget::RenderMultimodalPanel() { const auto& theme = AgentUI::GetTheme(); ImGui::PushID("MultimodalPanel"); // Dense header (no collapsing for small panel) AgentUI::PushPanelStyle(); ImGui::BeginChild("Multimodal_Panel", ImVec2(0, 120), true); // Slightly taller AgentUI::RenderSectionHeader(ICON_MD_CAMERA, "Vision", theme.provider_gemini); bool can_capture = static_cast(multimodal_callbacks_.capture_snapshot); bool can_send = static_cast(multimodal_callbacks_.send_to_gemini); // Ultra-compact mode selector ImGui::RadioButton("Full##mm_full", reinterpret_cast(&multimodal_state_.capture_mode), static_cast(CaptureMode::kFullWindow)); ImGui::SameLine(); ImGui::RadioButton("Active##mm_active", reinterpret_cast(&multimodal_state_.capture_mode), static_cast(CaptureMode::kActiveEditor)); ImGui::SameLine(); ImGui::RadioButton("Window##mm_window", reinterpret_cast(&multimodal_state_.capture_mode), static_cast(CaptureMode::kSpecificWindow)); ImGui::SameLine(); ImGui::RadioButton("Region##mm_region", reinterpret_cast(&multimodal_state_.capture_mode), static_cast(CaptureMode::kRegionSelect)); if (!can_capture) ImGui::BeginDisabled(); if (ImGui::SmallButton(ICON_MD_PHOTO_CAMERA " Capture##mm_cap")) { if (multimodal_state_.capture_mode == CaptureMode::kRegionSelect) { // Begin region selection mode BeginRegionSelection(); } else if (multimodal_callbacks_.capture_snapshot) { std::filesystem::path captured_path; absl::Status status = multimodal_callbacks_.capture_snapshot(&captured_path); if (status.ok()) { multimodal_state_.last_capture_path = captured_path; multimodal_state_.status_message = absl::StrFormat("Captured %s", captured_path.string()); multimodal_state_.last_updated = absl::Now(); LoadScreenshotPreview(captured_path); if (toast_manager_) { toast_manager_->Show("Snapshot captured", ToastType::kSuccess, 3.0f); } MarkHistoryDirty(); } else { multimodal_state_.status_message = status.message(); multimodal_state_.last_updated = absl::Now(); if (toast_manager_) { toast_manager_->Show( absl::StrFormat("Snapshot failed: %s", status.message()), ToastType::kError, 5.0f); } } } } if (!can_capture) ImGui::EndDisabled(); ImGui::SameLine(); if (multimodal_state_.last_capture_path.has_value()) { ImGui::TextColored(theme.status_success, ICON_MD_CHECK_CIRCLE); } else { ImGui::TextDisabled(ICON_MD_CAMERA_ALT); } if (ImGui::IsItemHovered() && multimodal_state_.last_capture_path.has_value()) { ImGui::SetTooltip( "%s", multimodal_state_.last_capture_path->filename().string().c_str()); } if (!can_send) ImGui::BeginDisabled(); ImGui::SameLine(); if (ImGui::SmallButton(ICON_MD_SEND " Analyze##mm_send")) { if (!multimodal_state_.last_capture_path.has_value()) { if (toast_manager_) { toast_manager_->Show("Capture a snapshot first", ToastType::kWarning, 3.0f); } } else { std::string prompt = multimodal_prompt_buffer_; absl::Status status = multimodal_callbacks_.send_to_gemini( *multimodal_state_.last_capture_path, prompt); if (status.ok()) { multimodal_state_.status_message = "Submitted image to Gemini"; multimodal_state_.last_updated = absl::Now(); if (toast_manager_) { toast_manager_->Show("Gemini request sent", ToastType::kSuccess, 3.0f); } MarkHistoryDirty(); } else { multimodal_state_.status_message = status.message(); multimodal_state_.last_updated = absl::Now(); if (toast_manager_) { toast_manager_->Show( absl::StrFormat("Gemini request failed: %s", status.message()), ToastType::kError, 5.0f); } } } } if (!can_send) ImGui::EndDisabled(); // Screenshot preview section if (multimodal_state_.preview.loaded && multimodal_state_.preview.show_preview) { ImGui::Spacing(); ImGui::Separator(); ImGui::Text(ICON_MD_IMAGE " Preview:"); RenderScreenshotPreview(); } // Region selection active indicator if (multimodal_state_.region_selection.active) { ImGui::Spacing(); ImGui::Separator(); ImGui::TextColored(theme.provider_ollama, ICON_MD_CROP " Drag to select region"); if (ImGui::SmallButton("Cancel##region_cancel")) { multimodal_state_.region_selection.active = false; } } ImGui::EndChild(); AgentUI::PopPanelStyle(); ImGui::PopID(); // Handle region selection (overlay) if (multimodal_state_.region_selection.active) { HandleRegionSelection(); } } void AgentChatWidget::RenderAutomationPanel() { const auto& theme = AgentUI::GetTheme(); ImGui::PushID("AutomationPanel"); // Auto-poll for status updates PollAutomationStatus(); // Animate pulse and scanlines for retro effect automation_state_.pulse_animation += ImGui::GetIO().DeltaTime * 2.0f; automation_state_.scanline_offset += ImGui::GetIO().DeltaTime * 0.5f; if (automation_state_.scanline_offset > 1.0f) { automation_state_.scanline_offset -= 1.0f; } AgentUI::PushPanelStyle(); if (ImGui::BeginChild("Automation_Panel", ImVec2(0, 240), true)) { // === HEADER WITH RETRO GLITCH EFFECT === float pulse = 0.5f + 0.5f * std::sin(automation_state_.pulse_animation); ImVec4 header_glow = ImVec4( theme.provider_ollama.x + 0.3f * pulse, theme.provider_ollama.y + 0.2f * pulse, theme.provider_ollama.z + 0.4f * pulse, 1.0f ); ImGui::PushStyleColor(ImGuiCol_Text, header_glow); ImGui::TextWrapped("%s %s", ICON_MD_SMART_TOY, "GUI AUTOMATION"); ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::TextDisabled("[v0.4.x]"); // === CONNECTION STATUS WITH VISUAL EFFECTS === bool connected = automation_state_.harness_connected; ImVec4 status_color; const char* status_text; const char* status_icon; if (connected) { // Pulsing green for connected float green_pulse = 0.7f + 0.3f * std::sin(automation_state_.pulse_animation * 0.5f); status_color = ImVec4(0.1f, green_pulse, 0.3f, 1.0f); status_text = "ONLINE"; status_icon = ICON_MD_CHECK_CIRCLE; } else { // Pulsing red for disconnected float red_pulse = 0.6f + 0.4f * std::sin(automation_state_.pulse_animation * 1.5f); status_color = ImVec4(red_pulse, 0.2f, 0.2f, 1.0f); status_text = "OFFLINE"; status_icon = ICON_MD_ERROR; } ImGui::Separator(); ImGui::TextColored(status_color, "%s %s", status_icon, status_text); ImGui::SameLine(); ImGui::TextDisabled("| %s", automation_state_.grpc_server_address.c_str()); // === CONTROL BAR === ImGui::Spacing(); // Refresh button with pulse effect when auto-refresh is on bool auto_ref_pulse = automation_state_.auto_refresh_enabled && (static_cast(automation_state_.pulse_animation * 2.0f) % 2 == 0); if (auto_ref_pulse) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.7f, 0.8f)); } if (ImGui::SmallButton(ICON_MD_REFRESH " Refresh")) { PollAutomationStatus(); if (automation_callbacks_.show_active_tests) { automation_callbacks_.show_active_tests(); } } if (auto_ref_pulse) { ImGui::PopStyleColor(); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Refresh automation status\nAuto-refresh: %s (%.1fs)", automation_state_.auto_refresh_enabled ? "ON" : "OFF", automation_state_.refresh_interval_seconds); } // Auto-refresh toggle ImGui::SameLine(); ImGui::Checkbox("##auto_refresh", &automation_state_.auto_refresh_enabled); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Auto-refresh connection status"); } // Quick action buttons ImGui::SameLine(); if (ImGui::SmallButton(ICON_MD_DASHBOARD " Dashboard")) { if (automation_callbacks_.open_harness_dashboard) { automation_callbacks_.open_harness_dashboard(); } } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Open automation dashboard"); } ImGui::SameLine(); if (ImGui::SmallButton(ICON_MD_REPLAY " Replay")) { if (automation_callbacks_.replay_last_plan) { automation_callbacks_.replay_last_plan(); } } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Replay last automation plan"); } // === SETTINGS ROW === ImGui::Spacing(); ImGui::SetNextItemWidth(80.0f); ImGui::SliderFloat("##refresh_interval", &automation_state_.refresh_interval_seconds, 0.5f, 10.0f, "%.1fs"); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Auto-refresh interval"); } // === RECENT AUTOMATION ACTIONS WITH SCROLLING === ImGui::Spacing(); ImGui::Separator(); // Header with retro styling ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "%s RECENT ACTIONS", ICON_MD_LIST); ImGui::SameLine(); ImGui::TextDisabled("[%zu]", automation_state_.recent_tests.size()); if (automation_state_.recent_tests.empty()) { ImGui::Spacing(); ImGui::TextDisabled(" > No recent actions"); ImGui::TextDisabled(" > Waiting for automation tasks..."); // Add animated dots int dots = static_cast(automation_state_.pulse_animation) % 4; std::string dot_string(dots, '.'); ImGui::TextDisabled(" > %s", dot_string.c_str()); } else { // Scrollable action list with retro styling ImGui::BeginChild("ActionQueue", ImVec2(0, 100), true, ImGuiWindowFlags_AlwaysVerticalScrollbar); // Add scanline effect (visual only) ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImVec2 win_pos = ImGui::GetWindowPos(); ImVec2 win_size = ImGui::GetWindowSize(); // Draw scanlines for (float y = 0; y < win_size.y; y += 4.0f) { float offset_y = y + automation_state_.scanline_offset * 4.0f; if (offset_y < win_size.y) { draw_list->AddLine( ImVec2(win_pos.x, win_pos.y + offset_y), ImVec2(win_pos.x + win_size.x, win_pos.y + offset_y), IM_COL32(0, 0, 0, 20)); } } for (const auto& test : automation_state_.recent_tests) { ImGui::PushID(test.test_id.c_str()); // Status icon with animation for running tests ImVec4 action_color; const char* status_icon; bool is_running = false; if (test.status == "success" || test.status == "completed" || test.status == "passed") { action_color = theme.status_success; status_icon = ICON_MD_CHECK_CIRCLE; } else if (test.status == "running" || test.status == "in_progress") { is_running = true; float running_pulse = 0.5f + 0.5f * std::sin(automation_state_.pulse_animation * 3.0f); action_color = ImVec4( theme.provider_ollama.x * running_pulse, theme.provider_ollama.y * (0.8f + 0.2f * running_pulse), theme.provider_ollama.z * running_pulse, 1.0f ); status_icon = ICON_MD_PENDING; } else if (test.status == "failed" || test.status == "error") { action_color = theme.status_error; status_icon = ICON_MD_ERROR; } else { action_color = theme.text_secondary_color; status_icon = ICON_MD_HELP; } // Icon with pulse ImGui::TextColored(action_color, "%s", status_icon); ImGui::SameLine(); // Action name with monospace font ImGui::Text("> %s", test.name.c_str()); // Timestamp if (test.updated_at != absl::InfinitePast()) { ImGui::SameLine(); auto elapsed = absl::Now() - test.updated_at; if (elapsed < absl::Seconds(60)) { ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "[%ds]", static_cast(absl::ToInt64Seconds(elapsed))); } else if (elapsed < absl::Minutes(60)) { ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "[%dm]", static_cast(absl::ToInt64Minutes(elapsed))); } else { ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "[%dh]", static_cast(absl::ToInt64Hours(elapsed))); } } // Message (if any) with indentation if (!test.message.empty()) { ImGui::Indent(20.0f); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.7f, 0.7f, 0.7f, 1.0f)); ImGui::TextWrapped(" %s %s", ICON_MD_MESSAGE, test.message.c_str()); ImGui::PopStyleColor(); ImGui::Unindent(20.0f); } ImGui::PopID(); } ImGui::EndChild(); } } ImGui::EndChild(); AgentUI::PopPanelStyle(); ImGui::PopID(); } void AgentChatWidget::RefreshCollaboration() { if (!collaboration_callbacks_.refresh_session) { return; } auto session_or = collaboration_callbacks_.refresh_session(); if (!session_or.ok()) { if (session_or.status().code() == absl::StatusCode::kNotFound) { collaboration_state_ = CollaborationState{}; join_code_buffer_[0] = '\0'; MarkHistoryDirty(); } if (toast_manager_) { toast_manager_->Show(absl::StrFormat("Failed to refresh participants: %s", session_or.status().message()), ToastType::kError, 5.0f); } return; } ApplyCollaborationSession(session_or.value(), /*update_action_timestamp=*/false); MarkHistoryDirty(); } void AgentChatWidget::ApplyCollaborationSession( const CollaborationCallbacks::SessionContext& context, bool update_action_timestamp) { collaboration_state_.active = true; collaboration_state_.session_id = context.session_id; collaboration_state_.session_name = context.session_name.empty() ? context.session_id : context.session_name; collaboration_state_.participants = context.participants; collaboration_state_.last_synced = absl::Now(); if (update_action_timestamp) { last_collaboration_action_ = absl::Now(); } } void AgentChatWidget::MarkHistoryDirty() { history_dirty_ = true; const absl::Time now = absl::Now(); if (last_persist_time_ == absl::InfinitePast() || now - last_persist_time_ > absl::Seconds(2)) { PersistHistory(); } } void AgentChatWidget::SwitchToSharedHistory(const std::string& session_id) { // Save current local history before switching if (history_loaded_ && history_dirty_) { PersistHistory(); } // Switch to shared history path history_path_ = ResolveHistoryPath(session_id); history_loaded_ = false; // Load shared history EnsureHistoryLoaded(); // Initialize polling state last_known_history_size_ = agent_service_.GetHistory().size(); last_shared_history_poll_ = absl::Now(); if (toast_manager_) { toast_manager_->Show( absl::StrFormat("Switched to shared chat history for session %s", session_id), ToastType::kInfo, 3.0f); } } void AgentChatWidget::SwitchToLocalHistory() { // Save shared history before switching if (history_loaded_ && history_dirty_) { PersistHistory(); } // Switch back to local history history_path_ = ResolveHistoryPath(""); history_loaded_ = false; // Load local history EnsureHistoryLoaded(); if (toast_manager_) { toast_manager_->Show("Switched to local chat history", ToastType::kInfo, 3.0f); } } void AgentChatWidget::PollSharedHistory() { if (!collaboration_state_.active) { return; // Not in a collaborative session } const absl::Time now = absl::Now(); // Poll every 2 seconds if (now - last_shared_history_poll_ < absl::Seconds(2)) { return; } last_shared_history_poll_ = now; // Check if the shared history file has been updated auto result = AgentChatHistoryCodec::Load(history_path_); if (!result.ok()) { return; // File might not exist yet or be temporarily locked } const size_t new_size = result->history.size(); // If history has grown, reload it if (new_size > last_known_history_size_) { const size_t new_messages = new_size - last_known_history_size_; agent_service_.ReplaceHistory(std::move(result->history)); last_history_size_ = new_size; last_known_history_size_ = new_size; if (toast_manager_) { toast_manager_->Show( absl::StrFormat("📬 %zu new message%s from collaborators", new_messages, new_messages == 1 ? "" : "s"), ToastType::kInfo, 3.0f); } } } void AgentChatWidget::UpdateAgentConfig(const AgentConfigState& config) { agent_config_ = config; // Apply configuration to the agent service cli::agent::AgentConfig service_config; service_config.verbose = config.verbose; service_config.show_reasoning = config.show_reasoning; service_config.max_tool_iterations = config.max_tool_iterations; service_config.max_retry_attempts = config.max_retry_attempts; agent_service_.SetConfig(service_config); if (toast_manager_) { toast_manager_->Show("Agent configuration updated", ToastType::kSuccess, 2.5f); } } void AgentChatWidget::RenderAgentConfigPanel() { const auto& theme = AgentUI::GetTheme(); // Dense header (no collapsing) ImGui::PushStyleColor(ImGuiCol_ChildBg, theme.panel_bg_color); ImGui::BeginChild("AgentConfig", ImVec2(0, 140), true); // Reduced from 350 AgentUI::RenderSectionHeader(ICON_MD_SETTINGS, "Config", theme.command_text_color); // Compact provider selection int provider_idx = 0; if (agent_config_.ai_provider == "ollama") provider_idx = 1; else if (agent_config_.ai_provider == "gemini") provider_idx = 2; if (ImGui::RadioButton("Mock", &provider_idx, 0)) { agent_config_.ai_provider = "mock"; std::snprintf(agent_config_.provider_buffer, sizeof(agent_config_.provider_buffer), "mock"); } ImGui::SameLine(); if (ImGui::RadioButton("Ollama", &provider_idx, 1)) { agent_config_.ai_provider = "ollama"; std::snprintf(agent_config_.provider_buffer, sizeof(agent_config_.provider_buffer), "ollama"); } ImGui::SameLine(); if (ImGui::RadioButton("Gemini", &provider_idx, 2)) { agent_config_.ai_provider = "gemini"; std::snprintf(agent_config_.provider_buffer, sizeof(agent_config_.provider_buffer), "gemini"); } // Dense provider settings if (agent_config_.ai_provider == "ollama") { ImGui::InputText("##ollama_model", agent_config_.model_buffer, IM_ARRAYSIZE(agent_config_.model_buffer)); ImGui::InputText("##ollama_host", agent_config_.ollama_host_buffer, IM_ARRAYSIZE(agent_config_.ollama_host_buffer)); } else if (agent_config_.ai_provider == "gemini") { ImGui::InputText("##gemini_model", agent_config_.model_buffer, IM_ARRAYSIZE(agent_config_.model_buffer)); ImGui::InputText("##gemini_key", agent_config_.gemini_key_buffer, IM_ARRAYSIZE(agent_config_.gemini_key_buffer), ImGuiInputTextFlags_Password); } ImGui::Separator(); ImGui::Checkbox("Verbose", &agent_config_.verbose); ImGui::SameLine(); ImGui::Checkbox("Reasoning", &agent_config_.show_reasoning); ImGui::SetNextItemWidth(-1); ImGui::SliderInt("##max_iter", &agent_config_.max_tool_iterations, 1, 10, "Iter: %d"); if (ImGui::Button(ICON_MD_CHECK " Apply", ImVec2(-1, 0))) { agent_config_.ai_model = agent_config_.model_buffer; agent_config_.ollama_host = agent_config_.ollama_host_buffer; agent_config_.gemini_api_key = agent_config_.gemini_key_buffer; UpdateAgentConfig(agent_config_); } ImGui::EndChild(); ImGui::PopStyleColor(); // Pop the ChildBg color from line 1609 } void AgentChatWidget::RenderZ3EDCommandPanel() { ImGui::PushID("Z3EDCmdPanel"); ImVec4 command_color = ImVec4(1.0f, 0.647f, 0.0f, 1.0f); // Dense header (no collapsing) ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.14f, 0.12f, 0.18f, 0.95f)); ImGui::BeginChild("Z3ED_CommandsChild", ImVec2(0, 100), true); ImGui::TextColored(command_color, ICON_MD_TERMINAL " Commands"); ImGui::Separator(); ImGui::SetNextItemWidth(-60); ImGui::InputTextWithHint( "##z3ed_cmd", "Command...", z3ed_command_state_.command_input_buffer, IM_ARRAYSIZE(z3ed_command_state_.command_input_buffer)); ImGui::SameLine(); ImGui::BeginDisabled(z3ed_command_state_.command_running); if (ImGui::Button(ICON_MD_PLAY_ARROW "##z3ed_run", ImVec2(50, 0))) { if (z3ed_callbacks_.run_agent_task) { std::string command = z3ed_command_state_.command_input_buffer; z3ed_command_state_.command_running = true; auto status = z3ed_callbacks_.run_agent_task(command); z3ed_command_state_.command_running = false; if (status.ok() && toast_manager_) { toast_manager_->Show("Task started", ToastType::kSuccess, 2.0f); } } } ImGui::EndDisabled(); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Run command"); } // Compact action buttons (inline) if (ImGui::SmallButton(ICON_MD_PREVIEW)) { if (z3ed_callbacks_.list_proposals) { auto result = z3ed_callbacks_.list_proposals(); if (result.ok()) { const auto& proposals = *result; z3ed_command_state_.command_output = absl::StrJoin(proposals, "\n"); } } } if (ImGui::IsItemHovered()) ImGui::SetTooltip("List"); ImGui::SameLine(); if (ImGui::SmallButton(ICON_MD_DIFFERENCE)) { if (z3ed_callbacks_.diff_proposal) { auto result = z3ed_callbacks_.diff_proposal(""); if (result.ok()) z3ed_command_state_.command_output = *result; } } if (ImGui::IsItemHovered()) ImGui::SetTooltip("Diff"); ImGui::SameLine(); if (ImGui::SmallButton(ICON_MD_CHECK)) { if (z3ed_callbacks_.accept_proposal) { z3ed_callbacks_.accept_proposal(""); } } if (ImGui::IsItemHovered()) ImGui::SetTooltip("Accept"); ImGui::SameLine(); if (ImGui::SmallButton(ICON_MD_CLOSE)) { if (z3ed_callbacks_.reject_proposal) { z3ed_callbacks_.reject_proposal(""); } } if (ImGui::IsItemHovered()) ImGui::SetTooltip("Reject"); if (!z3ed_command_state_.command_output.empty()) { ImGui::Separator(); ImGui::TextDisabled( "%s", z3ed_command_state_.command_output.substr(0, 100).c_str()); } ImGui::EndChild(); ImGui::PopStyleColor(); // Pop the ChildBg color from line 1677 ImGui::PopID(); // Pop the Z3EDCmdPanel ID } void AgentChatWidget::RenderRomSyncPanel() { ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.18f, 0.14f, 0.12f, 1.0f)); ImGui::BeginChild("RomSync", ImVec2(0, 130), true); ImGui::Text(ICON_MD_STORAGE " ROM State"); ImGui::Separator(); // Display current ROM hash if (!rom_sync_state_.current_rom_hash.empty()) { ImGui::Text("Hash: %s", rom_sync_state_.current_rom_hash.substr(0, 16).c_str()); ImGui::SameLine(); if (ImGui::SmallButton(ICON_MD_CONTENT_COPY)) { ImGui::SetClipboardText(rom_sync_state_.current_rom_hash.c_str()); if (toast_manager_) { toast_manager_->Show("ROM hash copied", ToastType::kInfo, 2.0f); } } } else { ImGui::TextDisabled("No ROM loaded"); } if (rom_sync_state_.last_sync_time != absl::InfinitePast()) { ImGui::Text("Last Sync: %s", absl::FormatTime("%H:%M:%S", rom_sync_state_.last_sync_time, absl::LocalTimeZone()) .c_str()); } ImGui::Spacing(); ImGui::Checkbox("Auto-sync ROM changes", &rom_sync_state_.auto_sync_enabled); if (rom_sync_state_.auto_sync_enabled) { ImGui::SliderInt("Sync Interval (seconds)", &rom_sync_state_.sync_interval_seconds, 10, 120); } ImGui::Spacing(); ImGui::Separator(); bool can_sync = static_cast(rom_sync_callbacks_.generate_rom_diff) && collaboration_state_.active && collaboration_state_.mode == CollaborationMode::kNetwork; if (!can_sync) ImGui::BeginDisabled(); if (ImGui::Button(ICON_MD_CLOUD_UPLOAD " Send ROM Sync", ImVec2(-1, 0))) { if (rom_sync_callbacks_.generate_rom_diff) { auto diff_result = rom_sync_callbacks_.generate_rom_diff(); if (diff_result.ok()) { std::string hash = rom_sync_callbacks_.get_rom_hash ? rom_sync_callbacks_.get_rom_hash() : ""; rom_sync_state_.current_rom_hash = hash; rom_sync_state_.last_sync_time = absl::Now(); // TODO: Send via network coordinator if (toast_manager_) { toast_manager_->Show(ICON_MD_CLOUD_DONE " ROM synced to collaborators", ToastType::kSuccess, 3.0f); } } else if (toast_manager_) { toast_manager_->Show(absl::StrFormat(ICON_MD_ERROR " Sync failed: %s", diff_result.status().message()), ToastType::kError, 5.0f); } } } if (!can_sync) { ImGui::EndDisabled(); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Connect to a network session to sync ROM"); } } // Show pending syncs if (!rom_sync_state_.pending_syncs.empty()) { ImGui::Spacing(); ImGui::Text(ICON_MD_PENDING " Pending Syncs (%zu)", rom_sync_state_.pending_syncs.size()); ImGui::Separator(); ImGui::BeginChild("PendingSyncs", ImVec2(0, 80), true); for (const auto& sync : rom_sync_state_.pending_syncs) { ImGui::BulletText("%s", sync.substr(0, 40).c_str()); } ImGui::EndChild(); } ImGui::EndChild(); ImGui::PopStyleColor(); // Pop the ChildBg color from line 1758 } void AgentChatWidget::RenderSnapshotPreviewPanel() { if (!ImGui::CollapsingHeader(ICON_MD_PHOTO_CAMERA " Snapshot Preview", ImGuiTreeNodeFlags_DefaultOpen)) { return; } ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.12f, 0.12f, 0.16f, 1.0f)); ImGui::BeginChild("SnapshotPreview", ImVec2(0, 200), true); if (multimodal_state_.last_capture_path.has_value()) { ImGui::Text(ICON_MD_IMAGE " Latest Capture"); ImGui::Separator(); ImGui::TextWrapped( "%s", multimodal_state_.last_capture_path->filename().string().c_str()); // TODO: Load and display image thumbnail ImGui::TextDisabled("Preview: [Image preview not yet implemented]"); ImGui::Spacing(); bool can_share = collaboration_state_.active && collaboration_state_.mode == CollaborationMode::kNetwork; if (!can_share) ImGui::BeginDisabled(); if (ImGui::Button(ICON_MD_SHARE " Share with Collaborators", ImVec2(-1, 0))) { // TODO: Share snapshot via network coordinator if (toast_manager_) { toast_manager_->Show(ICON_MD_CHECK " Snapshot shared", ToastType::kSuccess, 3.0f); } } if (!can_share) { ImGui::EndDisabled(); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Connect to a network session to share snapshots"); } } } else { ImGui::TextDisabled(ICON_MD_NO_PHOTOGRAPHY " No snapshot captured yet"); ImGui::TextWrapped("Use the Multimodal panel to capture a snapshot"); } ImGui::EndChild(); ImGui::PopStyleColor(); // Pop the ChildBg color from line 1860 } void AgentChatWidget::RenderProposalManagerPanel() { ImGui::Text(ICON_MD_PREVIEW " Proposal Management"); ImGui::Separator(); if (z3ed_callbacks_.list_proposals) { auto proposals_result = z3ed_callbacks_.list_proposals(); if (proposals_result.ok()) { const auto& proposals = *proposals_result; ImGui::Text("Total Proposals: %zu", proposals.size()); ImGui::Spacing(); if (proposals.empty()) { ImGui::TextDisabled( "No proposals yet. Use the agent to create proposals."); } else { if (ImGui::BeginTable("ProposalsTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) { ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 100.0f); ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 150.0f); ImGui::TableHeadersRow(); for (const auto& proposal_id : proposals) { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::TextUnformatted(proposal_id.c_str()); ImGui::TableNextColumn(); ImGui::TextDisabled("Proposal details..."); ImGui::TableNextColumn(); ImGui::PushID(proposal_id.c_str()); if (ImGui::SmallButton(ICON_MD_VISIBILITY)) { FocusProposalDrawer(proposal_id); } ImGui::SameLine(); if (ImGui::SmallButton(ICON_MD_CHECK)) { if (z3ed_callbacks_.accept_proposal) { auto status = z3ed_callbacks_.accept_proposal(proposal_id); (void)status; // Acknowledge result } } ImGui::SameLine(); if (ImGui::SmallButton(ICON_MD_CLOSE)) { if (z3ed_callbacks_.reject_proposal) { auto status = z3ed_callbacks_.reject_proposal(proposal_id); (void)status; // Acknowledge result } } ImGui::PopID(); } ImGui::EndTable(); } } } else { std::string error_msg(proposals_result.status().message()); ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Failed to load proposals: %s", error_msg.c_str()); } } else { ImGui::TextDisabled("Proposal management not available"); ImGui::TextWrapped("Set up Z3ED command callbacks to enable this feature"); } } void AgentChatWidget::RenderHarnessPanel() { ImGui::PushID("HarnessPanel"); ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.12f, 0.16f, 0.22f, 0.95f)); ImGui::BeginChild("HarnessPanel", ImVec2(0, 220), true); ImGui::TextColored(ImVec4(0.392f, 0.863f, 1.0f, 1.0f), ICON_MD_PLAY_CIRCLE " Harness Automation"); ImGui::Separator(); ImGui::TextDisabled("Shared automation pipeline between CLI + Agent Chat"); ImGui::Spacing(); if (ImGui::BeginTable("HarnessActions", 2, ImGuiTableFlags_BordersInnerV)) { ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 170.0f); ImGui::TableSetupColumn("Telemetry", ImGuiTableColumnFlags_WidthStretch); ImGui::TableNextRow(); // Actions column ImGui::TableSetColumnIndex(0); ImGui::BeginGroup(); const bool has_callbacks = automation_callbacks_.open_harness_dashboard || automation_callbacks_.replay_last_plan || automation_callbacks_.show_active_tests; if (!has_callbacks) { ImGui::TextDisabled("Automation bridge not available"); ImGui::TextWrapped("Hook up AutomationCallbacks via EditorManager to enable controls."); } else { if (automation_callbacks_.open_harness_dashboard && ImGui::Button(ICON_MD_DASHBOARD " Dashboard", ImVec2(-FLT_MIN, 0))) { automation_callbacks_.open_harness_dashboard(); } if (automation_callbacks_.replay_last_plan && ImGui::Button(ICON_MD_REPLAY " Replay Last Plan", ImVec2(-FLT_MIN, 0))) { automation_callbacks_.replay_last_plan(); } if (automation_callbacks_.show_active_tests && ImGui::Button(ICON_MD_LIST " Active Tests", ImVec2(-FLT_MIN, 0))) { automation_callbacks_.show_active_tests(); } if (automation_callbacks_.focus_proposal) { ImGui::Spacing(); ImGui::TextDisabled("Proposal tools"); if (!pending_focus_proposal_id_.empty()) { ImGui::TextWrapped("Proposal %s active", pending_focus_proposal_id_.c_str()); if (ImGui::SmallButton(ICON_MD_VISIBILITY " View Proposal")) { automation_callbacks_.focus_proposal(pending_focus_proposal_id_); } } else { ImGui::TextDisabled("No proposal selected"); } } } ImGui::EndGroup(); // Telemetry column ImGui::TableSetColumnIndex(1); ImGui::BeginGroup(); ImGui::TextColored(ImVec4(0.6f, 0.78f, 1.0f, 1.0f), ICON_MD_QUERY_STATS " Live Telemetry"); ImGui::Spacing(); if (!automation_state_.recent_tests.empty()) { const float row_height = ImGui::GetTextLineHeightWithSpacing() * 2.0f + 6.0f; if (ImGui::BeginTable("HarnessTelemetryRows", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { ImGui::TableSetupColumn("Test", ImGuiTableColumnFlags_WidthStretch, 0.3f); ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 90.0f); ImGui::TableSetupColumn("Message", ImGuiTableColumnFlags_WidthStretch, 0.5f); ImGui::TableSetupColumn("Updated", ImGuiTableColumnFlags_WidthFixed, 120.0f); ImGui::TableHeadersRow(); for (const auto& entry : automation_state_.recent_tests) { ImGui::TableNextRow(ImGuiTableRowFlags_None, row_height); ImGui::TableSetColumnIndex(0); ImGui::TextWrapped("%s", entry.name.empty() ? entry.test_id.c_str() : entry.name.c_str()); ImGui::TableSetColumnIndex(1); const char* status = entry.status.c_str(); ImVec4 status_color = ImVec4(0.8f, 0.8f, 0.8f, 1.0f); if (absl::EqualsIgnoreCase(status, "passed")) { status_color = ImVec4(0.2f, 0.8f, 0.4f, 1.0f); } else if (absl::EqualsIgnoreCase(status, "failed") || absl::EqualsIgnoreCase(status, "timeout")) { status_color = ImVec4(0.95f, 0.4f, 0.4f, 1.0f); } else if (absl::EqualsIgnoreCase(status, "running")) { status_color = ImVec4(0.95f, 0.75f, 0.3f, 1.0f); } ImGui::TextColored(status_color, "%s", status); ImGui::TableSetColumnIndex(2); ImGui::TextWrapped("%s", entry.message.c_str()); ImGui::TableSetColumnIndex(3); if (entry.updated_at == absl::InfinitePast()) { ImGui::TextDisabled("-"); } else { const double seconds_ago = absl::ToDoubleSeconds(absl::Now() - entry.updated_at); ImGui::Text("%.0fs ago", seconds_ago); } } ImGui::EndTable(); } } else { ImGui::TextDisabled("No harness activity recorded yet"); } ImGui::EndGroup(); ImGui::EndTable(); } ImGui::EndChild(); ImGui::PopStyleColor(); // Pop the ChildBg color from line 1982 ImGui::PopID(); } void AgentChatWidget::RenderSystemPromptEditor() { ImGui::BeginChild("SystemPromptEditor", ImVec2(0, 0), false); // Toolbar if (ImGui::Button(ICON_MD_FOLDER_OPEN " Load V1")) { // Load embedded system_prompt.txt (v1) std::string prompt_v1 = util::LoadFile("assets/agent/system_prompt.txt"); if (!prompt_v1.empty()) { // Find or create system prompt tab bool found = false; for (auto& tab : open_files_) { if (tab.is_system_prompt) { tab.editor.SetText(prompt_v1); tab.filepath = ""; // Not saved to disk tab.filename = "system_prompt_v1.txt (built-in)"; found = true; break; } } if (!found) { FileEditorTab tab; tab.filename = "system_prompt_v1.txt (built-in)"; tab.filepath = ""; tab.is_system_prompt = true; tab.editor.SetLanguageDefinition( TextEditor::LanguageDefinition::CPlusPlus()); tab.editor.SetText(prompt_v1); open_files_.push_back(std::move(tab)); active_file_tab_ = static_cast(open_files_.size()) - 1; } if (toast_manager_) { toast_manager_->Show("System prompt V1 loaded", ToastType::kSuccess); } } else if (toast_manager_) { toast_manager_->Show("Could not load system prompt V1", ToastType::kError); } } ImGui::SameLine(); if (ImGui::Button(ICON_MD_FOLDER_OPEN " Load V2")) { // Load embedded system_prompt_v2.txt std::string prompt_v2 = util::LoadFile("assets/agent/system_prompt_v2.txt"); if (!prompt_v2.empty()) { // Find or create system prompt tab bool found = false; for (auto& tab : open_files_) { if (tab.is_system_prompt) { tab.editor.SetText(prompt_v2); tab.filepath = ""; // Not saved to disk tab.filename = "system_prompt_v2.txt (built-in)"; found = true; break; } } if (!found) { FileEditorTab tab; tab.filename = "system_prompt_v2.txt (built-in)"; tab.filepath = ""; tab.is_system_prompt = true; tab.editor.SetLanguageDefinition( TextEditor::LanguageDefinition::CPlusPlus()); tab.editor.SetText(prompt_v2); open_files_.push_back(std::move(tab)); active_file_tab_ = static_cast(open_files_.size()) - 1; } if (toast_manager_) { toast_manager_->Show("System prompt V2 loaded", ToastType::kSuccess); } } else if (toast_manager_) { toast_manager_->Show("Could not load system prompt V2", ToastType::kError); } } ImGui::SameLine(); if (ImGui::Button(ICON_MD_SAVE " Save to Project")) { // Save the current system prompt to project directory for (auto& tab : open_files_) { if (tab.is_system_prompt) { auto save_path = util::FileDialogWrapper::ShowSaveFileDialog( "custom_system_prompt", "txt"); if (!save_path.empty()) { std::ofstream file(save_path); if (file.is_open()) { file << tab.editor.GetText(); tab.filepath = save_path; tab.filename = util::GetFileName(save_path); tab.modified = false; if (toast_manager_) { toast_manager_->Show( absl::StrFormat("System prompt saved to %s", save_path), ToastType::kSuccess); } } else if (toast_manager_) { toast_manager_->Show("Failed to save system prompt", ToastType::kError); } } break; } } } ImGui::SameLine(); if (ImGui::Button(ICON_MD_NOTE_ADD " Create New")) { FileEditorTab tab; tab.filename = "custom_system_prompt.txt (unsaved)"; tab.filepath = ""; tab.is_system_prompt = true; tab.modified = true; tab.editor.SetLanguageDefinition( TextEditor::LanguageDefinition::CPlusPlus()); tab.editor.SetText( "# Custom System Prompt\n\nEnter your custom system prompt here...\n"); open_files_.push_back(std::move(tab)); active_file_tab_ = static_cast(open_files_.size()) - 1; } ImGui::SameLine(); if (ImGui::Button(ICON_MD_FOLDER_OPEN " Load Custom")) { auto filepath = util::FileDialogWrapper::ShowOpenFileDialog(); if (!filepath.empty()) { std::ifstream file(filepath); if (file.is_open()) { bool found = false; for (auto& tab : open_files_) { if (tab.is_system_prompt) { std::stringstream buffer; buffer << file.rdbuf(); tab.editor.SetText(buffer.str()); tab.filepath = filepath; tab.filename = util::GetFileName(filepath); tab.modified = false; found = true; break; } } if (!found) { FileEditorTab tab; tab.filename = util::GetFileName(filepath); tab.filepath = filepath; tab.is_system_prompt = true; tab.editor.SetLanguageDefinition( TextEditor::LanguageDefinition::CPlusPlus()); std::stringstream buffer; buffer << file.rdbuf(); tab.editor.SetText(buffer.str()); open_files_.push_back(std::move(tab)); active_file_tab_ = static_cast(open_files_.size()) - 1; } if (toast_manager_) { toast_manager_->Show("Custom system prompt loaded", ToastType::kSuccess); } } else if (toast_manager_) { toast_manager_->Show("Could not load file", ToastType::kError); } } } ImGui::Separator(); // Find and render system prompt editor bool found_prompt = false; for (size_t i = 0; i < open_files_.size(); ++i) { if (open_files_[i].is_system_prompt) { found_prompt = true; ImVec2 editor_size = ImVec2(0, ImGui::GetContentRegionAvail().y); open_files_[i].editor.Render("##SystemPromptEditor", editor_size); if (open_files_[i].editor.IsTextChanged()) { open_files_[i].modified = true; } break; } } if (!found_prompt) { ImGui::TextWrapped( "No system prompt loaded. Click 'Load Default' to edit the system " "prompt."); } ImGui::EndChild(); } void AgentChatWidget::RenderFileEditorTabs() { ImGui::BeginChild("FileEditorArea", ImVec2(0, 0), false); // Toolbar if (ImGui::Button(ICON_MD_NOTE_ADD " New File")) { ImGui::OpenPopup("NewFilePopup"); } ImGui::SameLine(); if (ImGui::Button(ICON_MD_FOLDER_OPEN " Open File")) { auto filepath = util::FileDialogWrapper::ShowOpenFileDialog(); if (!filepath.empty()) { OpenFileInEditor(filepath); } } // New file popup static char new_filename_buffer[256] = {}; if (ImGui::BeginPopup("NewFilePopup")) { ImGui::Text("Create New File"); ImGui::Separator(); ImGui::InputText("Filename", new_filename_buffer, sizeof(new_filename_buffer)); if (ImGui::Button("Create")) { if (strlen(new_filename_buffer) > 0) { CreateNewFileInEditor(new_filename_buffer); memset(new_filename_buffer, 0, sizeof(new_filename_buffer)); ImGui::CloseCurrentPopup(); } } ImGui::SameLine(); if (ImGui::Button("Cancel")) { ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } ImGui::Separator(); // File tabs if (!open_files_.empty()) { if (ImGui::BeginTabBar("FileTabs", ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_FittingPolicyScroll)) { for (size_t i = 0; i < open_files_.size(); ++i) { if (open_files_[i].is_system_prompt) continue; // Skip system prompt in file tabs bool open = true; std::string tab_label = open_files_[i].filename; if (open_files_[i].modified) { tab_label += " *"; } if (ImGui::BeginTabItem(tab_label.c_str(), &open)) { active_file_tab_ = static_cast(i); // File toolbar if (ImGui::Button(ICON_MD_SAVE " Save")) { if (!open_files_[i].filepath.empty()) { std::ofstream file(open_files_[i].filepath); if (file.is_open()) { file << open_files_[i].editor.GetText(); open_files_[i].modified = false; if (toast_manager_) { toast_manager_->Show("File saved", ToastType::kSuccess); } } else if (toast_manager_) { toast_manager_->Show("Failed to save file", ToastType::kError); } } else { auto save_path = util::FileDialogWrapper::ShowSaveFileDialog( open_files_[i].filename, ""); if (!save_path.empty()) { std::ofstream file(save_path); if (file.is_open()) { file << open_files_[i].editor.GetText(); open_files_[i].filepath = save_path; open_files_[i].modified = false; if (toast_manager_) { toast_manager_->Show("File saved", ToastType::kSuccess); } } } } } ImGui::SameLine(); ImGui::TextDisabled("%s", open_files_[i].filepath.empty() ? "(unsaved)" : open_files_[i].filepath.c_str()); ImGui::Separator(); // Editor ImVec2 editor_size = ImVec2(0, ImGui::GetContentRegionAvail().y); open_files_[i].editor.Render("##FileEditor", editor_size); if (open_files_[i].editor.IsTextChanged()) { open_files_[i].modified = true; } ImGui::EndTabItem(); } if (!open) { // Tab was closed open_files_.erase(open_files_.begin() + i); if (active_file_tab_ >= static_cast(i)) { active_file_tab_--; } break; } } ImGui::EndTabBar(); } } else { ImGui::TextWrapped( "No files open. Create a new file or open an existing one."); } ImGui::EndChild(); } void AgentChatWidget::OpenFileInEditor(const std::string& filepath) { // Check if file is already open for (size_t i = 0; i < open_files_.size(); ++i) { if (open_files_[i].filepath == filepath) { active_file_tab_ = static_cast(i); return; } } // Load the file std::ifstream file(filepath); if (!file.is_open()) { if (toast_manager_) { toast_manager_->Show("Could not open file", ToastType::kError); } return; } FileEditorTab tab; tab.filepath = filepath; // Extract filename from path size_t last_slash = filepath.find_last_of("/\\"); tab.filename = (last_slash != std::string::npos) ? filepath.substr(last_slash + 1) : filepath; // Set language based on extension std::string ext = util::GetFileExtension(filepath); if (ext == "cpp" || ext == "cc" || ext == "h" || ext == "hpp") { tab.editor.SetLanguageDefinition( TextEditor::LanguageDefinition::CPlusPlus()); } else if (ext == "c") { tab.editor.SetLanguageDefinition(TextEditor::LanguageDefinition::C()); } else if (ext == "lua") { tab.editor.SetLanguageDefinition(TextEditor::LanguageDefinition::Lua()); } std::stringstream buffer; buffer << file.rdbuf(); tab.editor.SetText(buffer.str()); open_files_.push_back(std::move(tab)); active_file_tab_ = static_cast(open_files_.size()) - 1; if (toast_manager_) { toast_manager_->Show("File loaded", ToastType::kSuccess); } } void AgentChatWidget::CreateNewFileInEditor(const std::string& filename) { FileEditorTab tab; tab.filename = filename; tab.modified = true; // Set language based on extension std::string ext = util::GetFileExtension(filename); if (ext == "cpp" || ext == "cc" || ext == "h" || ext == "hpp") { tab.editor.SetLanguageDefinition( TextEditor::LanguageDefinition::CPlusPlus()); } else if (ext == "c") { tab.editor.SetLanguageDefinition(TextEditor::LanguageDefinition::C()); } else if (ext == "lua") { tab.editor.SetLanguageDefinition(TextEditor::LanguageDefinition::Lua()); } open_files_.push_back(std::move(tab)); active_file_tab_ = static_cast(open_files_.size()) - 1; } void AgentChatWidget::LoadAgentSettingsFromProject( const core::YazeProject& project) { // Load AI provider settings from project agent_config_.ai_provider = project.agent_settings.ai_provider; agent_config_.ai_model = project.agent_settings.ai_model; agent_config_.ollama_host = project.agent_settings.ollama_host; agent_config_.gemini_api_key = project.agent_settings.gemini_api_key; agent_config_.show_reasoning = project.agent_settings.show_reasoning; agent_config_.verbose = project.agent_settings.verbose; agent_config_.max_tool_iterations = project.agent_settings.max_tool_iterations; agent_config_.max_retry_attempts = project.agent_settings.max_retry_attempts; // Copy to buffer for ImGui strncpy(agent_config_.provider_buffer, agent_config_.ai_provider.c_str(), sizeof(agent_config_.provider_buffer) - 1); strncpy(agent_config_.model_buffer, agent_config_.ai_model.c_str(), sizeof(agent_config_.model_buffer) - 1); strncpy(agent_config_.ollama_host_buffer, agent_config_.ollama_host.c_str(), sizeof(agent_config_.ollama_host_buffer) - 1); strncpy(agent_config_.gemini_key_buffer, agent_config_.gemini_api_key.c_str(), sizeof(agent_config_.gemini_key_buffer) - 1); // Load custom system prompt if specified if (project.agent_settings.use_custom_prompt && !project.agent_settings.custom_system_prompt.empty()) { std::string prompt_path = project.GetAbsolutePath(project.agent_settings.custom_system_prompt); std::ifstream file(prompt_path); if (file.is_open()) { // Load into system prompt tab bool found = false; for (auto& tab : open_files_) { if (tab.is_system_prompt) { std::stringstream buffer; buffer << file.rdbuf(); tab.editor.SetText(buffer.str()); tab.filepath = prompt_path; tab.filename = util::GetFileName(prompt_path); found = true; break; } } if (!found) { FileEditorTab tab; tab.filename = util::GetFileName(prompt_path); tab.filepath = prompt_path; tab.is_system_prompt = true; tab.editor.SetLanguageDefinition( TextEditor::LanguageDefinition::CPlusPlus()); std::stringstream buffer; buffer << file.rdbuf(); tab.editor.SetText(buffer.str()); open_files_.push_back(std::move(tab)); } } } } void AgentChatWidget::SaveAgentSettingsToProject(core::YazeProject& project) { // Save AI provider settings to project project.agent_settings.ai_provider = agent_config_.ai_provider; project.agent_settings.ai_model = agent_config_.ai_model; project.agent_settings.ollama_host = agent_config_.ollama_host; project.agent_settings.gemini_api_key = agent_config_.gemini_api_key; project.agent_settings.show_reasoning = agent_config_.show_reasoning; project.agent_settings.verbose = agent_config_.verbose; project.agent_settings.max_tool_iterations = agent_config_.max_tool_iterations; project.agent_settings.max_retry_attempts = agent_config_.max_retry_attempts; // Check if a custom system prompt is loaded for (const auto& tab : open_files_) { if (tab.is_system_prompt && !tab.filepath.empty()) { project.agent_settings.custom_system_prompt = project.GetRelativePath(tab.filepath); project.agent_settings.use_custom_prompt = true; break; } } } void AgentChatWidget::SetMultimodalCallbacks( const MultimodalCallbacks& callbacks) { multimodal_callbacks_ = callbacks; } void AgentChatWidget::SetAutomationCallbacks( const AutomationCallbacks& callbacks) { automation_callbacks_ = callbacks; } void AgentChatWidget::UpdateHarnessTelemetry( const AutomationTelemetry& telemetry) { auto predicate = [&](const AutomationTelemetry& entry) { return entry.test_id == telemetry.test_id; }; auto it = std::find_if(automation_state_.recent_tests.begin(), automation_state_.recent_tests.end(), predicate); if (it != automation_state_.recent_tests.end()) { *it = telemetry; } else { if (automation_state_.recent_tests.size() >= 16) { automation_state_.recent_tests.erase(automation_state_.recent_tests.begin()); } automation_state_.recent_tests.push_back(telemetry); } } void AgentChatWidget::SetLastPlanSummary(const std::string& /* summary */) { // Store the plan summary for display in the automation panel // TODO: Implement plan summary storage and display // This could be shown in the harness panel or logged if (toast_manager_) { toast_manager_->Show("Plan summary received", ToastType::kInfo, 2.0f); } } void AgentChatWidget::PollAutomationStatus() { // Check if we should poll based on interval and auto-refresh setting if (!automation_state_.auto_refresh_enabled) { return; } absl::Time now = absl::Now(); absl::Duration elapsed = now - automation_state_.last_poll; if (elapsed < absl::Seconds(automation_state_.refresh_interval_seconds)) { return; } // Update last poll time automation_state_.last_poll = now; // Check connection status bool was_connected = automation_state_.harness_connected; automation_state_.harness_connected = CheckHarnessConnection(); // Notify on status change if (was_connected != automation_state_.harness_connected && toast_manager_) { if (automation_state_.harness_connected) { toast_manager_->Show(ICON_MD_CHECK_CIRCLE " Automation harness connected", ToastType::kSuccess, 2.0f); } else { toast_manager_->Show(ICON_MD_WARNING " Automation harness disconnected", ToastType::kWarning, 2.0f); } } } bool AgentChatWidget::CheckHarnessConnection() { #if defined(YAZE_WITH_GRPC) try { // Attempt to get harness summaries from TestManager // If this succeeds, the harness infrastructure is working auto summaries = test::TestManager::Get().ListHarnessTestSummaries(); // If we get here, the test manager is operational // In a real implementation, you might want to ping the gRPC server automation_state_.connection_attempts = 0; return true; } catch (const std::exception& e) { automation_state_.connection_attempts++; return false; } #else return false; #endif } void AgentChatWidget::SyncHistoryToPopup() { if (!chat_history_popup_) { return; } // Get the current chat history from the agent service const auto& history = agent_service_.GetHistory(); // Update the popup with the latest history chat_history_popup_->UpdateHistory(history); } // Screenshot Preview Implementation void AgentChatWidget::LoadScreenshotPreview(const std::filesystem::path& image_path) { // Unload any existing preview first UnloadScreenshotPreview(); // Load the image using SDL SDL_Surface* surface = SDL_LoadBMP(image_path.string().c_str()); if (!surface) { if (toast_manager_) { toast_manager_->Show(absl::StrFormat("Failed to load image: %s", SDL_GetError()), ToastType::kError, 3.0f); } return; } // Get the renderer from ImGui backend ImGuiIO& io = ImGui::GetIO(); auto* backend_data = static_cast(io.BackendRendererUserData); SDL_Renderer* renderer = nullptr; if (backend_data) { // Assuming SDL renderer backend // The backend data structure has renderer as first member renderer = *reinterpret_cast(backend_data); } if (!renderer) { SDL_FreeSurface(surface); if (toast_manager_) { toast_manager_->Show("Failed to get SDL renderer", ToastType::kError, 3.0f); } return; } // Create texture from surface SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); if (!texture) { SDL_FreeSurface(surface); if (toast_manager_) { toast_manager_->Show(absl::StrFormat("Failed to create texture: %s", SDL_GetError()), ToastType::kError, 3.0f); } return; } // Store texture info multimodal_state_.preview.texture_id = reinterpret_cast(texture); multimodal_state_.preview.width = surface->w; multimodal_state_.preview.height = surface->h; multimodal_state_.preview.loaded = true; multimodal_state_.preview.show_preview = true; SDL_FreeSurface(surface); if (toast_manager_) { toast_manager_->Show(absl::StrFormat("Screenshot preview loaded (%dx%d)", surface->w, surface->h), ToastType::kSuccess, 2.0f); } } void AgentChatWidget::UnloadScreenshotPreview() { if (multimodal_state_.preview.texture_id != nullptr) { // Destroy the SDL texture SDL_Texture* texture = reinterpret_cast(multimodal_state_.preview.texture_id); SDL_DestroyTexture(texture); multimodal_state_.preview.texture_id = nullptr; } multimodal_state_.preview.loaded = false; multimodal_state_.preview.width = 0; multimodal_state_.preview.height = 0; } void AgentChatWidget::RenderScreenshotPreview() { if (!multimodal_state_.last_capture_path.has_value()) { ImGui::TextDisabled("No screenshot to preview"); return; } const auto& theme = AgentUI::GetTheme(); // Display filename std::string filename = multimodal_state_.last_capture_path->filename().string(); ImGui::TextColored(theme.text_secondary_color, "%s", filename.c_str()); // Preview controls if (ImGui::SmallButton(ICON_MD_CLOSE " Hide")) { multimodal_state_.preview.show_preview = false; } ImGui::SameLine(); if (multimodal_state_.preview.loaded && multimodal_state_.preview.texture_id) { // Display the actual texture ImVec2 preview_size( multimodal_state_.preview.width * multimodal_state_.preview.preview_scale, multimodal_state_.preview.height * multimodal_state_.preview.preview_scale ); ImGui::Image(multimodal_state_.preview.texture_id, preview_size); // Scale slider ImGui::SetNextItemWidth(150); ImGui::SliderFloat("##preview_scale", &multimodal_state_.preview.preview_scale, 0.1f, 2.0f, "Scale: %.1fx"); } else { // Placeholder when texture not loaded ImGui::BeginChild("PreviewPlaceholder", ImVec2(200, 150), true); ImGui::SetCursorPos(ImVec2(60, 60)); ImGui::TextColored(theme.text_secondary_color, ICON_MD_IMAGE); ImGui::SetCursorPosX(40); ImGui::TextWrapped("Preview placeholder"); ImGui::TextDisabled("(Texture loading not yet implemented)"); ImGui::EndChild(); } } // Region Selection Implementation void AgentChatWidget::BeginRegionSelection() { multimodal_state_.region_selection.active = true; multimodal_state_.region_selection.dragging = false; if (toast_manager_) { toast_manager_->Show(ICON_MD_CROP " Drag to select region", ToastType::kInfo, 3.0f); } } void AgentChatWidget::HandleRegionSelection() { if (!multimodal_state_.region_selection.active) { return; } // Get the full window viewport ImGuiViewport* viewport = ImGui::GetMainViewport(); ImVec2 viewport_pos = viewport->Pos; ImVec2 viewport_size = viewport->Size; // Draw semi-transparent overlay ImDrawList* draw_list = ImGui::GetForegroundDrawList(); ImVec2 overlay_min = viewport_pos; ImVec2 overlay_max = ImVec2(viewport_pos.x + viewport_size.x, viewport_pos.y + viewport_size.y); draw_list->AddRectFilled(overlay_min, overlay_max, IM_COL32(0, 0, 0, 100)); // Handle mouse input for region selection ImGuiIO& io = ImGui::GetIO(); ImVec2 mouse_pos = io.MousePos; // Start dragging if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !multimodal_state_.region_selection.dragging) { multimodal_state_.region_selection.dragging = true; multimodal_state_.region_selection.start_pos = mouse_pos; multimodal_state_.region_selection.end_pos = mouse_pos; } // Update drag if (multimodal_state_.region_selection.dragging && ImGui::IsMouseDown(ImGuiMouseButton_Left)) { multimodal_state_.region_selection.end_pos = mouse_pos; // Calculate selection rectangle ImVec2 start = multimodal_state_.region_selection.start_pos; ImVec2 end = multimodal_state_.region_selection.end_pos; multimodal_state_.region_selection.selection_min = ImVec2( std::min(start.x, end.x), std::min(start.y, end.y) ); multimodal_state_.region_selection.selection_max = ImVec2( std::max(start.x, end.x), std::max(start.y, end.y) ); // Draw selection rectangle draw_list->AddRect( multimodal_state_.region_selection.selection_min, multimodal_state_.region_selection.selection_max, IM_COL32(100, 180, 255, 255), 0.0f, 0, 2.0f ); // Draw dimensions label float width = multimodal_state_.region_selection.selection_max.x - multimodal_state_.region_selection.selection_min.x; float height = multimodal_state_.region_selection.selection_max.y - multimodal_state_.region_selection.selection_min.y; std::string dimensions = absl::StrFormat("%.0f x %.0f", width, height); ImVec2 label_pos = ImVec2( multimodal_state_.region_selection.selection_min.x + 5, multimodal_state_.region_selection.selection_min.y + 5 ); draw_list->AddText(label_pos, IM_COL32(255, 255, 255, 255), dimensions.c_str()); } // End dragging if (multimodal_state_.region_selection.dragging && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { multimodal_state_.region_selection.dragging = false; CaptureSelectedRegion(); multimodal_state_.region_selection.active = false; } // Cancel on Escape if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { multimodal_state_.region_selection.active = false; multimodal_state_.region_selection.dragging = false; if (toast_manager_) { toast_manager_->Show("Region selection cancelled", ToastType::kInfo); } } // Instructions overlay ImVec2 text_pos = ImVec2(viewport_pos.x + 20, viewport_pos.y + 20); draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 255), "Drag to select region (ESC to cancel)"); } void AgentChatWidget::CaptureSelectedRegion() { // Calculate region bounds ImVec2 min = multimodal_state_.region_selection.selection_min; ImVec2 max = multimodal_state_.region_selection.selection_max; float width = max.x - min.x; float height = max.y - min.y; // Validate selection if (width < 10 || height < 10) { if (toast_manager_) { toast_manager_->Show("Region too small", ToastType::kWarning); } return; } // Get the renderer from ImGui backend ImGuiIO& io = ImGui::GetIO(); auto* backend_data = static_cast(io.BackendRendererUserData); SDL_Renderer* renderer = nullptr; if (backend_data) { renderer = *reinterpret_cast(backend_data); } if (!renderer) { if (toast_manager_) { toast_manager_->Show("Failed to get SDL renderer", ToastType::kError, 3.0f); } return; } // Get renderer size int full_width = 0; int full_height = 0; if (SDL_GetRendererOutputSize(renderer, &full_width, &full_height) != 0) { if (toast_manager_) { toast_manager_->Show(absl::StrFormat("Failed to get renderer size: %s", SDL_GetError()), ToastType::kError, 3.0f); } return; } // Clamp region to renderer bounds int capture_x = std::max(0, static_cast(min.x)); int capture_y = std::max(0, static_cast(min.y)); int capture_width = std::min(static_cast(width), full_width - capture_x); int capture_height = std::min(static_cast(height), full_height - capture_y); if (capture_width <= 0 || capture_height <= 0) { if (toast_manager_) { toast_manager_->Show("Invalid capture region", ToastType::kError); } return; } // Create surface for the capture region SDL_Surface* surface = SDL_CreateRGBSurface(0, capture_width, capture_height, 32, 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000); if (!surface) { if (toast_manager_) { toast_manager_->Show(absl::StrFormat("Failed to create surface: %s", SDL_GetError()), ToastType::kError, 3.0f); } return; } // Read pixels from the selected region SDL_Rect region_rect = {capture_x, capture_y, capture_width, capture_height}; if (SDL_RenderReadPixels(renderer, ®ion_rect, SDL_PIXELFORMAT_ARGB8888, surface->pixels, surface->pitch) != 0) { SDL_FreeSurface(surface); if (toast_manager_) { toast_manager_->Show(absl::StrFormat("Failed to read pixels: %s", SDL_GetError()), ToastType::kError, 3.0f); } return; } // Generate output path std::filesystem::path screenshot_dir = std::filesystem::temp_directory_path() / "yaze" / "screenshots"; std::error_code ec; std::filesystem::create_directories(screenshot_dir, ec); const int64_t timestamp_ms = absl::ToUnixMillis(absl::Now()); std::filesystem::path output_path = screenshot_dir / std::filesystem::path(absl::StrFormat("region_%lld.bmp", static_cast(timestamp_ms))); // Save the cropped image if (SDL_SaveBMP(surface, output_path.string().c_str()) != 0) { SDL_FreeSurface(surface); if (toast_manager_) { toast_manager_->Show(absl::StrFormat("Failed to save screenshot: %s", SDL_GetError()), ToastType::kError, 3.0f); } return; } SDL_FreeSurface(surface); // Store the capture path and load preview multimodal_state_.last_capture_path = output_path; LoadScreenshotPreview(output_path); if (toast_manager_) { toast_manager_->Show( absl::StrFormat("Region captured: %dx%d", capture_width, capture_height), ToastType::kSuccess, 3.0f ); } // Call the Gemini callback if available if (multimodal_callbacks_.send_to_gemini) { std::filesystem::path captured_path; auto status = multimodal_callbacks_.capture_snapshot(&captured_path); if (status.ok()) { multimodal_state_.last_capture_path = captured_path; multimodal_state_.status_message = "Region captured"; multimodal_state_.last_updated = absl::Now(); LoadScreenshotPreview(captured_path); MarkHistoryDirty(); } } } } // namespace editor } // namespace yaze