diff --git a/src/app/core/project.h b/src/app/core/project.h index ea06f653..99c114b1 100644 --- a/src/app/core/project.h +++ b/src/app/core/project.h @@ -114,7 +114,7 @@ struct YazeProject { // AI Agent Settings struct AgentSettings { std::string ai_provider = "auto"; // auto, gemini, ollama, mock - std::string ai_model; // e.g., "gemini-2.0-flash-exp", "llama3:latest" + std::string ai_model; // e.g., "gemini-2.5-flash", "llama3:latest" std::string ollama_host = "http://localhost:11434"; std::string gemini_api_key; // Optional, can use env var std::string custom_system_prompt; // Path to custom prompt (relative to project) diff --git a/src/app/editor/agent/agent_chat_widget.cc b/src/app/editor/agent/agent_chat_widget.cc index 5a6bf155..aada65f0 100644 --- a/src/app/editor/agent/agent_chat_widget.cc +++ b/src/app/editor/agent/agent_chat_widget.cc @@ -103,6 +103,12 @@ AgentChatWidget::AgentChatWidget() { memset(input_buffer_, 0, sizeof(input_buffer_)); history_path_ = ResolveHistoryPath(); history_supported_ = AgentChatHistoryCodec::Available(); + + // Enable panels by default for better visibility + show_agent_config_ = true; + show_z3ed_commands_ = true; + show_rom_sync_ = false; // Keep this one off by default + show_snapshot_preview_ = false; } void AgentChatWidget::SetRomContext(Rom* rom) { @@ -335,26 +341,34 @@ void AgentChatWidget::RenderMessage(const ChatMessage& msg, int index) { 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, ImVec4(0.3f, 0.3f, 0.4f, 0.6f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.4f, 0.4f, 0.5f, 0.8f)); + 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.json_pretty.has_value()) { - if (ImGui::SmallButton("Copy JSON")) { - ImGui::SetClipboardText(msg.json_pretty->c_str()); - if (toast_manager_) { - toast_manager_->Show("Copied JSON to clipboard", ToastType::kInfo, - 2.5f); - } - } - ImGui::SameLine(); - ImGui::TextDisabled("Structured response"); - } - if (msg.table_data.has_value()) { RenderTable(*msg.table_data); } else if (msg.json_pretty.has_value()) { - ImGui::PushStyleColor(ImGuiCol_Text, kJsonTextColor); - ImGui::TextUnformatted(msg.json_pretty->c_str()); + // Don't show JSON as a message - it's internal structure + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 0.8f)); + ImGui::TextDisabled(ICON_MD_DATA_OBJECT " (Structured response)"); ImGui::PopStyleColor(); } else { ImGui::TextWrapped("%s", msg.message.c_str()); @@ -418,17 +432,27 @@ void AgentChatWidget::RenderProposalQuickActions(const ChatMessage& msg, void AgentChatWidget::RenderHistory() { const auto& history = agent_service_.GetHistory(); float reserved_height = ImGui::GetFrameHeightWithSpacing() * 4.0f; - reserved_height += 220.0f; + reserved_height += 100.0f; // Reduced to 100 for much taller chat area - if (ImGui::BeginChild("History", ImVec2(0, -reserved_height), false, - ImGuiWindowFlags_AlwaysVerticalScrollbar | - ImGuiWindowFlags_HorizontalScrollbar)) { + // Styled chat history container + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.08f, 0.08f, 0.10f, 0.95f)); + if (ImGui::BeginChild("History", ImVec2(0, -reserved_height), true, + ImGuiWindowFlags_AlwaysVerticalScrollbar)) { if (history.empty()) { - ImGui::TextDisabled("No messages yet. Start the conversation below."); + // 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_) { @@ -436,31 +460,33 @@ void AgentChatWidget::RenderHistory() { } } ImGui::EndChild(); + ImGui::PopStyleColor(); last_history_size_ = history.size(); } void AgentChatWidget::RenderInputBox() { ImGui::Separator(); - ImGui::Text("Message:"); + ImGui::TextColored(ImVec4(1.0f, 0.843f, 0.0f, 1.0f), ICON_MD_EDIT " Message:"); bool submitted = ImGui::InputTextMultiline( - "##agent_input", input_buffer_, sizeof(input_buffer_), ImVec2(-1, 80.0f), - ImGuiInputTextFlags_AllowTabInput | ImGuiInputTextFlags_EnterReturnsTrue); + "##agent_input", input_buffer_, sizeof(input_buffer_), ImVec2(-1, 60.0f), + ImGuiInputTextFlags_None); - bool send = submitted; - if (submitted && ImGui::GetIO().KeyShift) { - size_t len = std::strlen(input_buffer_); - if (len + 1 < sizeof(input_buffer_)) { - input_buffer_[len] = '\n'; - input_buffer_[len + 1] = '\0'; + // 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::SetKeyboardFocusHere(-1); - send = false; } ImGui::Spacing(); + + // Send button row + 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(absl::StrFormat("%s Send", ICON_MD_SEND).c_str(), - ImVec2(120, 0)) || + ImVec2(140, 0)) || send) { if (std::strlen(input_buffer_) > 0) { history_dirty_ = true; @@ -472,9 +498,114 @@ void AgentChatWidget::RenderInputBox() { ImGui::SetKeyboardFocusHere(-1); } } - + ImGui::PopStyleColor(2); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Send message (Ctrl+Enter)"); + } + ImGui::SameLine(); - ImGui::TextDisabled("Enter to send • Shift+Enter for newline"); + 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() { @@ -487,7 +618,7 @@ void AgentChatWidget::Draw() { // Poll for new messages in collaborative sessions PollSharedHistory(); - ImGui::SetNextWindowSize(ImVec2(1200, 800), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(1400, 1000), ImGuiCond_FirstUseEver); ImGui::Begin(title_.c_str(), &active_, ImGuiWindowFlags_MenuBar); // Simplified menu bar @@ -524,10 +655,10 @@ void AgentChatWidget::Draw() { 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 + // Connection status bar at top (compact) ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImVec2 bar_start = ImGui::GetCursorScreenPos(); - ImVec2 bar_size(ImGui::GetContentRegionAvail().x, 75); + ImVec2 bar_size(ImGui::GetContentRegionAvail().x, 55); // Reduced from 75 to 55 // Gradient background ImU32 color_top = ImGui::GetColorU32(ImVec4(0.18f, 0.22f, 0.28f, 1.0f)); @@ -546,37 +677,142 @@ void AgentChatWidget::Draw() { ImGui::BeginChild("AgentChat_ConnectionBar", bar_size, false, ImGuiWindowFlags_NoScrollbar); ImGui::PushID("ConnectionBar"); { - ImGui::Spacing(); + // 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); - // Provider selection - ImGui::TextColored(accent_color, ICON_MD_SMART_TOY " Provider:"); + // Compact single row layout + ImGui::TextColored(accent_color, ICON_MD_SMART_TOY); ImGui::SameLine(); - ImGui::SetNextItemWidth(110); + 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::TextColored(accent_color, ICON_MD_PSYCHOLOGY " Model:"); + 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::SetNextItemWidth(180); - ImGui::InputText("##main_model", agent_config_.model_buffer, sizeof(agent_config_.model_buffer)); + 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 " Reasoning", &agent_config_.show_reasoning); + ImGui::Checkbox(ICON_MD_VISIBILITY, &agent_config_.show_reasoning); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Show reasoning"); + } - // Connection status indicator - ImGui::SameLine(ImGui::GetContentRegionAvail().x - 100); + // 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::TextColored(collaboration_status_color_, ICON_MD_CHECK_CIRCLE " Session Active"); - } else { - ImGui::TextDisabled(ICON_MD_CANCEL " No Session"); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - 25); + ImGui::TextColored(collaboration_status_color_, ICON_MD_CHECK_CIRCLE); } } ImGui::PopID(); @@ -586,9 +822,13 @@ void AgentChatWidget::Draw() { // Main layout: Chat area (left, 70%) + Control panels (right, 30%) if (ImGui::BeginTable("AgentChat_MainLayout", 2, - ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV)) { + 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 (emphasized) @@ -603,19 +843,7 @@ void AgentChatWidget::Draw() { ImGui::PushID("ControlsColumn"); ImGui::BeginChild("AgentChat_ControlPanels", ImVec2(0, 0), false); - // Quick Actions - if (ImGui::CollapsingHeader(ICON_MD_BOLT " Quick Actions", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::PushID("QuickActions"); - if (ImGui::Button(ICON_MD_REFRESH " Reset Chat", ImVec2(-1, 0))) { - agent_service_.ResetConversation(); - if (toast_manager_) { - toast_manager_->Show("Conversation reset", ToastType::kInfo); - } - } - ImGui::PopID(); - } - - // Collapsible panels + // Collapsible panels (organized by priority) if (show_agent_config_) { RenderAgentConfigPanel(); } @@ -624,9 +852,11 @@ void AgentChatWidget::Draw() { RenderZ3EDCommandPanel(); } - RenderCollaborationPanel(); + // Multimodal before collaboration (more frequently used) RenderMultimodalPanel(); + RenderCollaborationPanel(); + if (show_rom_sync_) { RenderRomSyncPanel(); } @@ -649,29 +879,29 @@ void AgentChatWidget::RenderCollaborationPanel() { 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); - // Top section: Mode selector in a styled box - ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.15f, 0.18f, 0.22f, 0.8f)); - ImGui::BeginChild("Collab_ModeSelector", ImVec2(0, 45), true, ImGuiWindowFlags_NoScrollbar); - { - ImGui::TextColored(ImVec4(1.0f, 0.843f, 0.0f, 1.0f), ICON_MD_SETTINGS_ETHERNET " Collaboration 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)); + if (!ImGui::CollapsingHeader(ICON_MD_PEOPLE " Collaboration & Network")) { + ImGui::PopID(); + return; } - ImGui::EndChild(); - ImGui::PopStyleColor(); + + // Mode selector (compact inline) + 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)); ImGui::Spacing(); - // Main content in table layout - if (ImGui::BeginTable("Collab_MainTable", 2, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_Resizable)) { - ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 320); - ImGui::TableSetupColumn("Controls", ImGuiTableColumnFlags_WidthStretch); + // Main content in table layout (fixed size to prevent auto-resize) + if (ImGui::BeginTable("Collab_MainTable", 2, ImGuiTableFlags_BordersInnerV)) { + ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 180); + ImGui::TableSetupColumn("Controls", ImGuiTableColumnFlags_WidthFixed, + ImGui::GetContentRegionAvail().x - 180); ImGui::TableNextRow(); // LEFT COLUMN: Session Details @@ -680,7 +910,7 @@ void AgentChatWidget::RenderCollaborationPanel() { ImGui::PushID("StatusColumn"); ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.15f, 0.2f, 0.18f, 0.4f)); - ImGui::BeginChild("Collab_SessionDetails", ImVec2(0, 180), true); + ImGui::BeginChild("Collab_SessionDetails", ImVec2(0, 100), true); ImGui::TextColored(ImVec4(1.0f, 0.843f, 0.0f, 1.0f), ICON_MD_INFO " Session Status:"); ImGui::Spacing(); if (connected) { @@ -707,7 +937,7 @@ void AgentChatWidget::RenderCollaborationPanel() { ImGui::TextWrapped("%s", collaboration_state_.session_id.c_str()); 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##copy_session_id")) { + if (ImGui::Button(ICON_MD_CONTENT_COPY " 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); @@ -1046,7 +1276,7 @@ void AgentChatWidget::RenderMultimodalPanel() { ImGui::TextColored(gemini_color, ICON_MD_EDIT " Vision Prompt:"); ImGui::InputTextMultiline("##mm_gemini_prompt_textarea", multimodal_prompt_buffer_, IM_ARRAYSIZE(multimodal_prompt_buffer_), - ImVec2(-1, 80)); + ImVec2(-1, 60)); if (!can_send) ImGui::BeginDisabled(); @@ -1341,7 +1571,7 @@ void AgentChatWidget::RenderZ3EDCommandPanel() { } ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.14f, 0.12f, 0.18f, 1.0f)); - ImGui::BeginChild("Z3ED_CommandsChild", ImVec2(0, 300), true); + ImGui::BeginChild("Z3ED_CommandsChild", ImVec2(0, 200), true); ImGui::TextColored(command_color, ICON_MD_CODE " Command Palette"); ImGui::Separator(); @@ -1438,22 +1668,27 @@ void AgentChatWidget::RenderZ3EDCommandPanel() { (void)status; // Acknowledge result } } + ImGui::PopStyleColor(2); // FIX: Pop the styles before EndTable ImGui::EndTable(); } if (!z3ed_command_state_.command_output.empty()) { ImGui::Spacing(); - ImGui::Text(ICON_MD_TERMINAL " Output"); + ImGui::TextColored(command_color, ICON_MD_OUTPUT " Output:"); ImGui::Separator(); - ImGui::BeginChild("CommandOutput", ImVec2(0, 100), true, + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.1f, 0.1f, 0.12f, 0.8f)); + ImGui::BeginChild("Z3ED_CommandOutput", ImVec2(0, 100), true, ImGuiWindowFlags_HorizontalScrollbar); ImGui::TextWrapped("%s", z3ed_command_state_.command_output.c_str()); ImGui::EndChild(); + ImGui::PopStyleColor(); } ImGui::EndChild(); ImGui::PopStyleColor(); + + ImGui::PopID(); // FIX: Pop the Z3EDCmdPanel ID } void AgentChatWidget::RenderRomSyncPanel() { @@ -1463,7 +1698,7 @@ void AgentChatWidget::RenderRomSyncPanel() { } ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.18f, 0.14f, 0.12f, 1.0f)); - ImGui::BeginChild("RomSync", ImVec2(0, 250), true); + ImGui::BeginChild("RomSync", ImVec2(0, 200), true); ImGui::Text(ICON_MD_STORAGE " ROM State"); ImGui::Separator(); @@ -1562,7 +1797,7 @@ void AgentChatWidget::RenderSnapshotPreviewPanel() { } ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.12f, 0.12f, 0.16f, 1.0f)); - ImGui::BeginChild("SnapshotPreview", ImVec2(0, 300), true); + ImGui::BeginChild("SnapshotPreview", ImVec2(0, 200), true); if (multimodal_state_.last_capture_path.has_value()) { ImGui::Text(ICON_MD_IMAGE " Latest Capture"); diff --git a/src/app/editor/agent/agent_chat_widget.h b/src/app/editor/agent/agent_chat_widget.h index a471d25b..e880df93 100644 --- a/src/app/editor/agent/agent_chat_widget.h +++ b/src/app/editor/agent/agent_chat_widget.h @@ -229,6 +229,25 @@ public: void HandleSnapshotReceived(const std::string& snapshot_data, const std::string& snapshot_type); void HandleProposalReceived(const std::string& proposal_data); + // Chat session management + struct ChatSession { + std::string id; + std::string name; + cli::agent::ConversationalAgentService agent_service; + size_t last_history_size = 0; + bool history_loaded = false; + bool history_dirty = false; + std::filesystem::path history_path; + absl::Time last_persist_time = absl::InfinitePast(); + + ChatSession(const std::string& session_id, const std::string& session_name) + : id(session_id), name(session_name) {} + }; + + std::vector chat_sessions_; + int active_session_index_ = 0; + + // Legacy single session support (will migrate to sessions) cli::agent::ConversationalAgentService agent_service_; char input_buffer_[1024]; bool active_ = false; diff --git a/src/app/editor/agent/agent_editor.cc b/src/app/editor/agent/agent_editor.cc index 2e8c63a8..f3e6e341 100644 --- a/src/app/editor/agent/agent_editor.cc +++ b/src/app/editor/agent/agent_editor.cc @@ -218,8 +218,10 @@ void AgentEditor::DrawConfigurationPanel() { toast_manager_->Show("GEMINI_API_KEY not found in environment", ToastType::kWarning); } } else { + // Immediately apply to chat widget + ApplyConfig(current_config_); if (toast_manager_) { - toast_manager_->Show("Gemini API key loaded from environment", ToastType::kSuccess); + toast_manager_->Show("Gemini API key loaded and applied", ToastType::kSuccess); } } } diff --git a/src/app/editor/editor_manager.cc b/src/app/editor/editor_manager.cc index 3e4ecb3d..1f3eca76 100644 --- a/src/app/editor/editor_manager.cc +++ b/src/app/editor/editor_manager.cc @@ -301,7 +301,7 @@ void EditorManager::Initialize(const std::string& filename) { // Create Gemini service cli::GeminiConfig config; config.api_key = api_key; - config.model = "gemini-2.0-flash-exp"; // Use vision-capable model + config.model = "gemini-2.5-flash"; // Use vision-capable model config.verbose = false; cli::GeminiAIService gemini_service(config); @@ -350,10 +350,18 @@ void EditorManager::Initialize(const std::string& filename) { // Initialize editor selection dialog callback editor_selection_dialog_.SetSelectionCallback([this](EditorType type) { - if (!current_editor_set_) return; - editor_selection_dialog_.MarkRecentlyUsed(type); + // Handle agent editor separately (doesn't require ROM) + if (type == EditorType::kAgent) { +#ifdef YAZE_WITH_GRPC + agent_editor_.set_active(true); +#endif + return; + } + + if (!current_editor_set_) return; + switch (type) { case EditorType::kOverworld: current_editor_set_->overworld_editor_.set_active(true); diff --git a/src/cli/service/ai/gemini_ai_service.cc b/src/cli/service/ai/gemini_ai_service.cc index edb36ec3..8fbd3de3 100644 --- a/src/cli/service/ai/gemini_ai_service.cc +++ b/src/cli/service/ai/gemini_ai_service.cc @@ -499,16 +499,21 @@ absl::StatusOr GeminiAIService::ParseGeminiResponse( // Try to parse as JSON object auto parsed_text = nlohmann::json::parse(text_content, nullptr, false); if (!parsed_text.is_discarded()) { + // Extract text_response if (parsed_text.contains("text_response") && parsed_text["text_response"].is_string()) { agent_response.text_response = parsed_text["text_response"].get(); } + + // Extract reasoning if (parsed_text.contains("reasoning") && parsed_text["reasoning"].is_string()) { agent_response.reasoning = parsed_text["reasoning"].get(); } + + // Extract commands if (parsed_text.contains("commands") && parsed_text["commands"].is_array()) { for (const auto& cmd : parsed_text["commands"]) { @@ -521,6 +526,30 @@ absl::StatusOr GeminiAIService::ParseGeminiResponse( } } } + + // Extract tool_calls from the parsed JSON + if (parsed_text.contains("tool_calls") && + parsed_text["tool_calls"].is_array()) { + for (const auto& call : parsed_text["tool_calls"]) { + if (call.contains("tool_name") && call["tool_name"].is_string()) { + ToolCall tool_call; + tool_call.tool_name = call["tool_name"].get(); + + if (call.contains("args") && call["args"].is_object()) { + for (auto& [key, value] : call["args"].items()) { + if (value.is_string()) { + tool_call.args[key] = value.get(); + } else if (value.is_number()) { + tool_call.args[key] = std::to_string(value.get()); + } else if (value.is_boolean()) { + tool_call.args[key] = value.get() ? "true" : "false"; + } + } + } + agent_response.tool_calls.push_back(tool_call); + } + } + } } else { // If parsing the full object fails, fallback to extracting commands from text std::vector lines = absl::StrSplit(text_content, '\n'); diff --git a/src/cli/service/ai/prompt_builder.cc b/src/cli/service/ai/prompt_builder.cc index 7f252bc2..97b0a0e7 100644 --- a/src/cli/service/ai/prompt_builder.cc +++ b/src/cli/service/ai/prompt_builder.cc @@ -76,10 +76,21 @@ std::vector BuildCatalogueSearchPaths(const std::string& explicit_path } } + // Try to get executable directory for better path resolution + fs::path exe_dir; + try { + exe_dir = fs::current_path(); + } catch (...) { + exe_dir = "."; + } + const std::vector defaults = { "assets/agent/prompt_catalogue.yaml", "../assets/agent/prompt_catalogue.yaml", "../../assets/agent/prompt_catalogue.yaml", + "../../../assets/agent/prompt_catalogue.yaml", // From build/bin/ + "../../../../assets/agent/prompt_catalogue.yaml", // From build/bin/yaze.app/Contents/MacOS/ + "../Resources/assets/agent/prompt_catalogue.yaml", // macOS app bundle "assets/z3ed/prompt_catalogue.yaml", "../assets/z3ed/prompt_catalogue.yaml", }; diff --git a/test/integration/test_ai_tile_placement.cc b/test/integration/test_ai_tile_placement.cc index 987dc060..693c84c7 100644 --- a/test/integration/test_ai_tile_placement.cc +++ b/test/integration/test_ai_tile_placement.cc @@ -102,7 +102,7 @@ TEST_F(AITilePlacementTest, DISABLED_VisionAnalysisBasic) { cli::GeminiConfig config; config.api_key = api_key; - config.model = "gemini-2.0-flash-exp"; + config.model = "gemini-2.5-flash"; cli::GeminiAIService gemini_service(config); cli::ai::VisionActionRefiner refiner(&gemini_service); diff --git a/test/multimodal/test_gemini_vision.cc b/test/multimodal/test_gemini_vision.cc index a40081fc..d6549ad2 100644 --- a/test/multimodal/test_gemini_vision.cc +++ b/test/multimodal/test_gemini_vision.cc @@ -72,7 +72,7 @@ class GeminiVisionTest : public ::testing::Test { TEST_F(GeminiVisionTest, BasicImageAnalysis) { cli::GeminiConfig config; config.api_key = api_key_; - config.model = "gemini-2.0-flash-exp"; // Vision-capable model + config.model = "gemini-2.5-flash"; // Vision-capable model config.verbose = false; cli::GeminiAIService service(config); @@ -96,7 +96,7 @@ TEST_F(GeminiVisionTest, BasicImageAnalysis) { TEST_F(GeminiVisionTest, ImageWithSpecificPrompt) { cli::GeminiConfig config; config.api_key = api_key_; - config.model = "gemini-2.0-flash-exp"; + config.model = "gemini-2.5-flash"; config.verbose = false; cli::GeminiAIService service(config); @@ -124,7 +124,7 @@ TEST_F(GeminiVisionTest, ImageWithSpecificPrompt) { TEST_F(GeminiVisionTest, InvalidImagePath) { cli::GeminiConfig config; config.api_key = api_key_; - config.model = "gemini-2.0-flash-exp"; + config.model = "gemini-2.5-flash"; cli::GeminiAIService service(config); @@ -147,7 +147,7 @@ TEST_F(GeminiVisionTest, ScreenshotCaptureIntegration) { cli::GeminiConfig config; config.api_key = api_key_; - config.model = "gemini-2.0-flash-exp"; + config.model = "gemini-2.5-flash"; config.verbose = false; cli::GeminiAIService service(config); @@ -178,7 +178,7 @@ TEST_F(GeminiVisionTest, ScreenshotCaptureIntegration) { TEST_F(GeminiVisionTest, MultipleRequestsSequential) { cli::GeminiConfig config; config.api_key = api_key_; - config.model = "gemini-2.0-flash-exp"; + config.model = "gemini-2.5-flash"; config.verbose = false; cli::GeminiAIService service(config); @@ -203,7 +203,7 @@ TEST_F(GeminiVisionTest, MultipleRequestsSequential) { TEST_F(GeminiVisionTest, RateLimitHandling) { cli::GeminiConfig config; config.api_key = api_key_; - config.model = "gemini-2.0-flash-exp"; + config.model = "gemini-2.5-flash"; config.verbose = false; cli::GeminiAIService service(config);