diff --git a/docs/z3ed/README.md b/docs/z3ed/README.md index 19e4aa45..62f31a3d 100644 --- a/docs/z3ed/README.md +++ b/docs/z3ed/README.md @@ -178,7 +178,9 @@ Z3ED supports multiple AI providers. Configuration is resolved with command-line 1. **Live LLM Testing (1-2h)**: Verify function calling with real models (Ollama/Gemini). 2. **GUI Chat Enhancements (4-6h)**: Persist chat state, surface proposal shortcuts, and add toast notifications when new proposals arrive from chats. 3. **Expand Tool Coverage (8-10h)**: Add new read-only tools for inspecting dialogue, sprites, and regions. -4. **Windows Cross-Platform Testing (8-10h)**: Validate `z3ed` and the test harness on Windows. +4. **Collaborative Sessions**: Expand the infrastructure of `z3ed` and `yaze` to support collaborative sessions where users can edit the same game and query the AI model together. +5. **Multi-modal Gemini for image feedback**: Take screenshots of the map for Gemini to have more context to tool and function calls. +6. **Windows Cross-Platform Testing (8-10h)**: Validate `z3ed` and the test harness on Windows. ## 9. Troubleshooting diff --git a/src/app/editor/editor_manager.cc b/src/app/editor/editor_manager.cc index 8441ea33..6364fa5f 100644 --- a/src/app/editor/editor_manager.cc +++ b/src/app/editor/editor_manager.cc @@ -223,6 +223,11 @@ void EditorManager::Initialize(const std::string& filename) { // Set the popup manager in the context context_.popup_manager = popup_manager_.get(); +#ifdef YAZE_WITH_GRPC + agent_chat_widget_.SetToastManager(&toast_manager_); + agent_chat_widget_.SetProposalDrawer(&proposal_drawer_); +#endif + // Load critical user settings first LoadUserSettings(); diff --git a/src/app/editor/system/agent_chat_widget.cc b/src/app/editor/system/agent_chat_widget.cc index 9139375b..ab43e2e4 100644 --- a/src/app/editor/system/agent_chat_widget.cc +++ b/src/app/editor/system/agent_chat_widget.cc @@ -1,18 +1,142 @@ #include "app/editor/system/agent_chat_widget.h" +#include +#include #include +#include +#include #include #include -#include "imgui.h" +#include "absl/strings/str_format.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "app/core/platform/file_dialog.h" +#include "app/editor/system/proposal_drawer.h" +#include "app/editor/system/toast_manager.h" +#include "app/gui/icons.h" +#include "imgui/imgui.h" +#include "imgui/misc/cpp/imgui_stdlib.h" +#include "nlohmann/json.hpp" namespace { +using yaze::cli::agent::ChatMessage; + const ImVec4 kUserColor = ImVec4(0.88f, 0.76f, 0.36f, 1.0f); const ImVec4 kAgentColor = ImVec4(0.56f, 0.82f, 0.62f, 1.0f); const ImVec4 kJsonTextColor = ImVec4(0.78f, 0.83f, 0.90f, 1.0f); +const ImVec4 kProposalPanelColor = ImVec4(0.20f, 0.35f, 0.20f, 0.35f); -void RenderTable(const yaze::cli::agent::ChatMessage::TableData& table_data) { +std::filesystem::path ExpandUserPath(std::string path) { + if (!path.empty() && path.front() == '~') { + const char* home = std::getenv("HOME"); + if (home != nullptr) { + path.replace(0, 1, home); + } + } + return std::filesystem::path(path); +} + +std::filesystem::path ResolveHistoryPath() { + std::filesystem::path base = ExpandUserPath(yaze::core::GetConfigDirectory()); + if (base.empty()) { + base = ExpandUserPath(".yaze"); + } + auto directory = base / "agent"; + return directory / "chat_history.json"; +} + +absl::Time ParseTimestamp(const nlohmann::json& value) { + if (!value.is_string()) { + return absl::Now(); + } + absl::Time parsed; + if (absl::ParseTime(absl::RFC3339_full, value.get(), + absl::UTCTimeZone(), &parsed)) { + return parsed; + } + return absl::Now(); +} + +nlohmann::json SerializeTableData(const ChatMessage::TableData& table) { + nlohmann::json json; + json["headers"] = table.headers; + json["rows"] = table.rows; + return json; +} + +std::optional ParseTableData(const nlohmann::json& json) { + if (!json.is_object()) { + return std::nullopt; + } + + ChatMessage::TableData table; + if (json.contains("headers") && json["headers"].is_array()) { + for (const auto& header : json["headers"]) { + if (header.is_string()) { + table.headers.push_back(header.get()); + } + } + } + + if (json.contains("rows") && json["rows"].is_array()) { + for (const auto& row : json["rows"]) { + if (!row.is_array()) { + continue; + } + std::vector row_values; + for (const auto& value : row) { + if (value.is_string()) { + row_values.push_back(value.get()); + } else { + row_values.push_back(value.dump()); + } + } + table.rows.push_back(std::move(row_values)); + } + } + + if (table.headers.empty() && table.rows.empty()) { + return std::nullopt; + } + + return table; +} + +nlohmann::json SerializeProposal(const ChatMessage::ProposalSummary& proposal) { + nlohmann::json json; + json["id"] = proposal.id; + json["change_count"] = proposal.change_count; + json["executed_commands"] = proposal.executed_commands; + json["sandbox_rom_path"] = proposal.sandbox_rom_path.string(); + json["proposal_json_path"] = proposal.proposal_json_path.string(); + return json; +} + +std::optional ParseProposal( + const nlohmann::json& json) { + if (!json.is_object()) { + return std::nullopt; + } + + ChatMessage::ProposalSummary summary; + summary.id = json.value("id", ""); + summary.change_count = json.value("change_count", 0); + summary.executed_commands = json.value("executed_commands", 0); + if (json.contains("sandbox_rom_path") && json["sandbox_rom_path"].is_string()) { + summary.sandbox_rom_path = json["sandbox_rom_path"].get(); + } + if (json.contains("proposal_json_path") && json["proposal_json_path"].is_string()) { + summary.proposal_json_path = json["proposal_json_path"].get(); + } + if (summary.id.empty()) { + return std::nullopt; + } + return summary; +} + +void RenderTable(const ChatMessage::TableData& table_data) { const int column_count = static_cast(table_data.headers.size()); if (column_count <= 0) { ImGui::TextDisabled("(empty)"); @@ -50,60 +174,365 @@ namespace editor { AgentChatWidget::AgentChatWidget() { title_ = "Agent Chat"; memset(input_buffer_, 0, sizeof(input_buffer_)); + history_path_ = ResolveHistoryPath(); } void AgentChatWidget::SetRomContext(Rom* rom) { agent_service_.SetRomContext(rom); } -void AgentChatWidget::Draw() { - if (!active_) { +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; + } + } + + std::ifstream file(history_path_); + if (!file.good()) { return; } - ImGui::Begin(title_.c_str(), &active_); + try { + nlohmann::json json; + file >> json; + if (!json.contains("messages") || !json["messages"].is_array()) { + return; + } + + std::vector history; + for (const auto& item : json["messages"]) { + if (!item.is_object()) { + continue; + } + + ChatMessage message; + std::string sender = item.value("sender", "agent"); + message.sender = + sender == "user" ? ChatMessage::Sender::kUser + : ChatMessage::Sender::kAgent; + message.message = item.value("message", ""); + message.timestamp = ParseTimestamp(item["timestamp"]); + + if (item.contains("json_pretty") && item["json_pretty"].is_string()) { + message.json_pretty = item["json_pretty"].get(); + } + + if (item.contains("table_data")) { + message.table_data = ParseTableData(item["table_data"]); + } + + if (item.contains("metrics") && item["metrics"].is_object()) { + ChatMessage::SessionMetrics metrics; + const auto& metrics_json = item["metrics"]; + metrics.turn_index = metrics_json.value("turn_index", 0); + metrics.total_user_messages = + metrics_json.value("total_user_messages", 0); + metrics.total_agent_messages = + metrics_json.value("total_agent_messages", 0); + metrics.total_tool_calls = + metrics_json.value("total_tool_calls", 0); + metrics.total_commands = metrics_json.value("total_commands", 0); + metrics.total_proposals = metrics_json.value("total_proposals", 0); + metrics.total_elapsed_seconds = + metrics_json.value("total_elapsed_seconds", 0.0); + metrics.average_latency_seconds = + metrics_json.value("average_latency_seconds", 0.0); + message.metrics = metrics; + } + + if (item.contains("proposal")) { + message.proposal = ParseProposal(item["proposal"]); + } + + history.push_back(std::move(message)); + } + + if (!history.empty()) { + agent_service_.ReplaceHistory(std::move(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); + } + } + } catch (const std::exception& e) { + if (toast_manager_) { + toast_manager_->Show( + absl::StrFormat("Failed to load chat history: %s", e.what()), + ToastType::kError, 6.0f); + } + } +} + +void AgentChatWidget::PersistHistory() { + if (!history_loaded_ || !history_dirty_) { + return; + } - // Display message history const auto& history = agent_service_.GetHistory(); - if (ImGui::BeginChild("History", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), + + nlohmann::json json; + json["version"] = 2; + json["messages"] = nlohmann::json::array(); + + for (const auto& message : history) { + nlohmann::json entry; + entry["sender"] = + message.sender == ChatMessage::Sender::kUser ? "user" : "agent"; + entry["message"] = message.message; + entry["timestamp"] = absl::FormatTime(absl::RFC3339_full, + message.timestamp, + absl::UTCTimeZone()); + + if (message.json_pretty.has_value()) { + entry["json_pretty"] = *message.json_pretty; + } + if (message.table_data.has_value()) { + entry["table_data"] = SerializeTableData(*message.table_data); + } + if (message.metrics.has_value()) { + const auto& metrics = *message.metrics; + nlohmann::json metrics_json; + metrics_json["turn_index"] = metrics.turn_index; + metrics_json["total_user_messages"] = metrics.total_user_messages; + metrics_json["total_agent_messages"] = metrics.total_agent_messages; + metrics_json["total_tool_calls"] = metrics.total_tool_calls; + metrics_json["total_commands"] = metrics.total_commands; + metrics_json["total_proposals"] = metrics.total_proposals; + metrics_json["total_elapsed_seconds"] = metrics.total_elapsed_seconds; + metrics_json["average_latency_seconds"] = + metrics.average_latency_seconds; + entry["metrics"] = metrics_json; + } + if (message.proposal.has_value()) { + entry["proposal"] = SerializeProposal(*message.proposal); + } + + json["messages"].push_back(std::move(entry)); + } + + 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 create chat history directory", + ToastType::kError, 5.0f); + } + return; + } + } + + std::ofstream file(history_path_); + if (!file.is_open()) { + if (toast_manager_) { + toast_manager_->Show("Cannot write chat history", + ToastType::kError, 5.0f); + } + return; + } + + file << json.dump(2); + 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); +} + +void AgentChatWidget::RenderMessage(const ChatMessage& msg, int index) { + ImGui::PushID(index); + + const bool from_user = (msg.sender == ChatMessage::Sender::kUser); + const ImVec4 header_color = from_user ? kUserColor : kAgentColor; + 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()); + + 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()); + 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& proposal = *msg.proposal; + ImGui::PushStyleColor(ImGuiCol_ChildBg, kProposalPanelColor); + 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(ImVec4(0.8f, 1.0f, 0.8f, 1.0f), + "%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& history = agent_service_.GetHistory(); + float reserved_height = ImGui::GetFrameHeightWithSpacing() * 4.0f; + + if (ImGui::BeginChild("History", + ImVec2(0, -reserved_height), false, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_HorizontalScrollbar)) { - for (size_t index = 0; index < history.size(); ++index) { - const auto& msg = history[index]; - ImGui::PushID(static_cast(index)); - - const bool from_user = - msg.sender == cli::agent::ChatMessage::Sender::kUser; - const ImVec4 header_color = from_user ? kUserColor : kAgentColor; - const char* header_label = from_user ? "You" : "Agent"; - - ImGui::TextColored(header_color, "%s", header_label); - - ImGui::Indent(); - - if (msg.json_pretty.has_value()) { - if (ImGui::SmallButton("Copy JSON")) { - ImGui::SetClipboardText(msg.json_pretty->c_str()); - } - ImGui::SameLine(); - ImGui::TextDisabled("Structured response"); + if (history.empty()) { + ImGui::TextDisabled("No messages yet. Start the conversation below."); + } else { + for (size_t index = 0; index < history.size(); ++index) { + RenderMessage(history[index], static_cast(index)); } - - 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()); - ImGui::PopStyleColor(); - } else { - ImGui::TextWrapped("%s", msg.message.c_str()); - } - - ImGui::Unindent(); - ImGui::Spacing(); - ImGui::Separator(); - ImGui::PopID(); } if (history.size() > last_history_size_) { @@ -112,17 +541,57 @@ void AgentChatWidget::Draw() { } ImGui::EndChild(); last_history_size_ = history.size(); +} - // Display input text box - if (ImGui::InputText("Input", input_buffer_, sizeof(input_buffer_), - ImGuiInputTextFlags_EnterReturnsTrue)) { - if (strlen(input_buffer_) > 0) { - (void)agent_service_.SendMessage(input_buffer_); - memset(input_buffer_, 0, sizeof(input_buffer_)); +void AgentChatWidget::RenderInputBox() { + ImGui::Separator(); + ImGui::Text("Message:"); + + bool submitted = ImGui::InputTextMultiline( + "##agent_input", input_buffer_, sizeof(input_buffer_), + ImVec2(-1, 80.0f), + ImGuiInputTextFlags_AllowTabInput | + ImGuiInputTextFlags_EnterReturnsTrue); + + 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'; } - ImGui::SetKeyboardFocusHere(-1); // Refocus input + ImGui::SetKeyboardFocusHere(-1); + send = false; } + ImGui::Spacing(); + if (ImGui::Button(absl::StrFormat("%s Send", ICON_MD_SEND).c_str(), + ImVec2(120, 0)) || send) { + if (std::strlen(input_buffer_) > 0) { + history_dirty_ = true; + EnsureHistoryLoaded(); + auto response = agent_service_.SendMessage(input_buffer_); + memset(input_buffer_, 0, sizeof(input_buffer_)); + HandleAgentResponse(response); + PersistHistory(); + ImGui::SetKeyboardFocusHere(-1); + } + } + + ImGui::SameLine(); + ImGui::TextDisabled("Enter to send • Shift+Enter for newline"); +} + +void AgentChatWidget::Draw() { + if (!active_) { + return; + } + + EnsureHistoryLoaded(); + + ImGui::Begin(title_.c_str(), &active_); + RenderHistory(); + RenderInputBox(); ImGui::End(); } diff --git a/src/app/editor/system/agent_chat_widget.h b/src/app/editor/system/agent_chat_widget.h index d083cc86..bc7b2f4c 100644 --- a/src/app/editor/system/agent_chat_widget.h +++ b/src/app/editor/system/agent_chat_widget.h @@ -1,8 +1,12 @@ #ifndef YAZE_SRC_APP_EDITOR_SYSTEM_AGENT_CHAT_WIDGET_H_ #define YAZE_SRC_APP_EDITOR_SYSTEM_AGENT_CHAT_WIDGET_H_ +#include #include +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/time/time.h" #include "cli/service/agent/conversational_agent_service.h" namespace yaze { @@ -11,6 +15,9 @@ class Rom; namespace editor { +class ProposalDrawer; +class ToastManager; + class AgentChatWidget { public: AgentChatWidget(); @@ -19,16 +26,46 @@ class AgentChatWidget { void SetRomContext(Rom* rom); + void SetToastManager(ToastManager* toast_manager) { + toast_manager_ = toast_manager; + } + + void SetProposalDrawer(ProposalDrawer* drawer) { + proposal_drawer_ = drawer; + } + bool* active() { return &active_; } bool is_active() const { return active_; } void set_active(bool active) { active_ = active; } private: + void EnsureHistoryLoaded(); + void PersistHistory(); + void RenderHistory(); + void RenderMessage(const cli::agent::ChatMessage& msg, int index); + void RenderProposalQuickActions(const cli::agent::ChatMessage& msg, + int index); + void RenderInputBox(); + void HandleAgentResponse( + const absl::StatusOr& response); + int CountKnownProposals() const; + void FocusProposalDrawer(const std::string& proposal_id); + void NotifyProposalCreated(const cli::agent::ChatMessage& msg, + int new_total_proposals); + cli::agent::ConversationalAgentService agent_service_; char input_buffer_[1024]; bool active_ = false; std::string title_; size_t last_history_size_ = 0; + bool history_loaded_ = false; + bool history_dirty_ = false; + std::filesystem::path history_path_; + int last_proposal_count_ = 0; + ToastManager* toast_manager_ = nullptr; + ProposalDrawer* proposal_drawer_ = nullptr; + std::string pending_focus_proposal_id_; + absl::Time last_persist_time_ = absl::InfinitePast(); }; } // namespace editor diff --git a/src/app/editor/system/proposal_drawer.cc b/src/app/editor/system/proposal_drawer.cc index 5adf4d86..cab9f372 100644 --- a/src/app/editor/system/proposal_drawer.cc +++ b/src/app/editor/system/proposal_drawer.cc @@ -40,6 +40,9 @@ void ProposalDrawer::Draw() { if (needs_refresh_) { RefreshProposals(); needs_refresh_ = false; + if (!selected_proposal_id_.empty() && !selected_proposal_) { + SelectProposal(selected_proposal_id_); + } } // Header with refresh button @@ -441,6 +444,13 @@ void ProposalDrawer::DrawActionButtons() { } } +void ProposalDrawer::FocusProposal(const std::string& proposal_id) { + visible_ = true; + selected_proposal_id_ = proposal_id; + selected_proposal_ = nullptr; + needs_refresh_ = true; +} + void ProposalDrawer::RefreshProposals() { auto& registry = cli::ProposalRegistry::Instance(); diff --git a/src/app/editor/system/proposal_drawer.h b/src/app/editor/system/proposal_drawer.h index 16ea05bf..bb9898c4 100644 --- a/src/app/editor/system/proposal_drawer.h +++ b/src/app/editor/system/proposal_drawer.h @@ -44,6 +44,7 @@ class ProposalDrawer { void Hide() { visible_ = false; } void Toggle() { visible_ = !visible_; } bool IsVisible() const { return visible_; } + void FocusProposal(const std::string& proposal_id); private: void DrawProposalList(); diff --git a/src/cli/service/agent/conversational_agent_service.cc b/src/cli/service/agent/conversational_agent_service.cc index 3630e876..161bcfe7 100644 --- a/src/cli/service/agent/conversational_agent_service.cc +++ b/src/cli/service/agent/conversational_agent_service.cc @@ -445,9 +445,17 @@ absl::StatusOr ConversationalAgentService::SendMessage( "⚠️ Failed to prepare a proposal automatically: ", proposal_status.message())); } - - ChatMessage chat_response = - CreateMessage(ChatMessage::Sender::kAgent, response_text); + ChatMessage chat_response = + CreateMessage(ChatMessage::Sender::kAgent, response_text); + if (proposal_result.has_value()) { + ChatMessage::ProposalSummary summary; + summary.id = proposal_result->metadata.id; + summary.change_count = proposal_result->change_count; + summary.executed_commands = proposal_result->executed_commands; + summary.sandbox_rom_path = proposal_result->metadata.sandbox_rom_path; + summary.proposal_json_path = proposal_result->proposal_json_path; + chat_response.proposal = summary; + } ++metrics_.agent_messages; ++metrics_.turns_completed; metrics_.total_latency += absl::Now() - turn_start; @@ -465,6 +473,48 @@ const std::vector& ConversationalAgentService::GetHistory() const { return history_; } +void ConversationalAgentService::ReplaceHistory( + std::vector history) { + history_ = std::move(history); + TrimHistoryIfNeeded(); + RebuildMetricsFromHistory(); +} + +void ConversationalAgentService::RebuildMetricsFromHistory() { + metrics_ = InternalMetrics{}; + + ChatMessage::SessionMetrics snapshot{}; + bool has_snapshot = false; + + for (const auto& message : history_) { + if (message.sender == ChatMessage::Sender::kUser) { + ++metrics_.user_messages; + } else if (message.sender == ChatMessage::Sender::kAgent) { + ++metrics_.agent_messages; + ++metrics_.turns_completed; + } + + if (message.proposal.has_value()) { + ++metrics_.proposals_created; + } + + if (message.metrics.has_value()) { + snapshot = *message.metrics; + has_snapshot = true; + } + } + + if (has_snapshot) { + metrics_.user_messages = snapshot.total_user_messages; + metrics_.agent_messages = snapshot.total_agent_messages; + metrics_.tool_calls = snapshot.total_tool_calls; + metrics_.commands_generated = snapshot.total_commands; + metrics_.proposals_created = snapshot.total_proposals; + metrics_.turns_completed = snapshot.turn_index; + metrics_.total_latency = absl::Seconds(snapshot.total_elapsed_seconds); + } +} + } // namespace agent } // namespace cli } // namespace yaze diff --git a/src/cli/service/agent/conversational_agent_service.h b/src/cli/service/agent/conversational_agent_service.h index dd84ed85..cb4ec2de 100644 --- a/src/cli/service/agent/conversational_agent_service.h +++ b/src/cli/service/agent/conversational_agent_service.h @@ -1,6 +1,7 @@ #ifndef YAZE_SRC_CLI_SERVICE_AGENT_CONVERSATIONAL_AGENT_SERVICE_H_ #define YAZE_SRC_CLI_SERVICE_AGENT_CONVERSATIONAL_AGENT_SERVICE_H_ +#include #include #include #include @@ -9,6 +10,7 @@ #include "absl/time/time.h" #include "cli/service/ai/ai_service.h" #include "cli/service/agent/tool_dispatcher.h" +#include "cli/service/agent/proposal_executor.h" namespace yaze { @@ -23,6 +25,13 @@ struct ChatMessage { std::vector headers; std::vector> rows; }; + struct ProposalSummary { + std::string id; + int change_count = 0; + int executed_commands = 0; + std::filesystem::path sandbox_rom_path; + std::filesystem::path proposal_json_path; + }; Sender sender; std::string message; absl::Time timestamp; @@ -39,6 +48,7 @@ struct ChatMessage { double average_latency_seconds = 0.0; }; std::optional metrics; + std::optional proposal; }; enum class AgentOutputFormat { @@ -81,6 +91,8 @@ class ConversationalAgentService { ChatMessage::SessionMetrics GetMetrics() const; + void ReplaceHistory(std::vector history); + private: struct InternalMetrics { int user_messages = 0; @@ -94,6 +106,7 @@ class ConversationalAgentService { void TrimHistoryIfNeeded(); ChatMessage::SessionMetrics BuildMetricsSnapshot() const; + void RebuildMetricsFromHistory(); std::vector history_; std::unique_ptr ai_service_;