diff --git a/CMakeLists.txt b/CMakeLists.txt index 409a58bb..7f4a38a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -394,3 +394,9 @@ endif() # Packaging configuration include(cmake/packaging.cmake) +add_custom_target(build_cleaner + COMMAND ${CMAKE_COMMAND} -E echo "Running scripts/build_cleaner.py --dry-run" + COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_SOURCE_DIR} ${Python3_EXECUTABLE} scripts/build_cleaner.py --dry-run + COMMENT "Validate CMake source lists and includes" +) + diff --git a/src/app/editor/agent/agent_chat_widget.cc b/src/app/editor/agent/agent_chat_widget.cc index ee9a4758..68c498b5 100644 --- a/src/app/editor/agent/agent_chat_widget.cc +++ b/src/app/editor/agent/agent_chat_widget.cc @@ -18,6 +18,7 @@ #include "util/file_util.h" #include "app/editor/agent/agent_chat_history_codec.h" #include "app/editor/system/proposal_drawer.h" +#include "app/editor/system/agent_chat_history_popup.h" #include "app/editor/system/toast_manager.h" #include "app/gui/icons.h" #include "app/core/project.h" @@ -122,6 +123,20 @@ void AgentChatWidget::SetProposalDrawer(ProposalDrawer* drawer) { } } +void AgentChatWidget::SetChatHistoryPopup(AgentChatHistoryPopup* popup) { + chat_history_popup_ = popup; + + // Set up callback to open this chat window + if (chat_history_popup_) { + chat_history_popup_->SetOpenChatCallback([this]() { + this->set_active(true); + }); + + // Initial sync + SyncHistoryToPopup(); + } +} + void AgentChatWidget::EnsureHistoryLoaded() { if (history_loaded_) { return; @@ -221,6 +236,9 @@ void AgentChatWidget::PersistHistory() { 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; @@ -321,6 +339,9 @@ void AgentChatWidget::HandleAgentResponse( 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) { @@ -1833,6 +1854,14 @@ void AgentChatWidget::HandleProposalReceived( } } +void AgentChatWidget::SyncHistoryToPopup() { + if (!chat_history_popup_) return; + + const auto& history = agent_service_.GetHistory(); + chat_history_popup_->UpdateHistory(history); + chat_history_popup_->NotifyNewMessage(); +} + void AgentChatWidget::RenderSystemPromptEditor() { ImGui::BeginChild("SystemPromptEditor", ImVec2(0, 0), false); diff --git a/src/app/editor/agent/agent_chat_widget.h b/src/app/editor/agent/agent_chat_widget.h index e880df93..845e6321 100644 --- a/src/app/editor/agent/agent_chat_widget.h +++ b/src/app/editor/agent/agent_chat_widget.h @@ -25,6 +25,7 @@ namespace editor { class ProposalDrawer; class ToastManager; +class AgentChatHistoryPopup; /** * @class AgentChatWidget @@ -86,6 +87,8 @@ class AgentChatWidget { void SetToastManager(ToastManager* toast_manager); void SetProposalDrawer(ProposalDrawer* drawer); + + void SetChatHistoryPopup(AgentChatHistoryPopup* popup); void SetCollaborationCallbacks(const CollaborationCallbacks& callbacks) { collaboration_callbacks_ = callbacks; @@ -228,6 +231,9 @@ public: void HandleRomSyncReceived(const std::string& diff_data, const std::string& rom_hash); void HandleSnapshotReceived(const std::string& snapshot_data, const std::string& snapshot_type); void HandleProposalReceived(const std::string& proposal_data); + + // History synchronization + void SyncHistoryToPopup(); // Chat session management struct ChatSession { @@ -261,6 +267,7 @@ public: int last_proposal_count_ = 0; ToastManager* toast_manager_ = nullptr; ProposalDrawer* proposal_drawer_ = nullptr; + AgentChatHistoryPopup* chat_history_popup_ = nullptr; std::string pending_focus_proposal_id_; absl::Time last_persist_time_ = absl::InfinitePast(); diff --git a/src/app/editor/editor_library.cmake b/src/app/editor/editor_library.cmake index bed17837..e007aefd 100644 --- a/src/app/editor/editor_library.cmake +++ b/src/app/editor/editor_library.cmake @@ -41,6 +41,7 @@ set( app/editor/system/shortcut_manager.cc app/editor/system/popup_manager.cc app/editor/system/proposal_drawer.cc + app/editor/system/agent_chat_history_popup.cc app/editor/agent/agent_chat_history_codec.cc ) diff --git a/src/app/editor/system/agent_chat_history_popup.cc b/src/app/editor/system/agent_chat_history_popup.cc new file mode 100644 index 00000000..d0fedaa2 --- /dev/null +++ b/src/app/editor/system/agent_chat_history_popup.cc @@ -0,0 +1,231 @@ +#include "app/editor/system/agent_chat_history_popup.h" + +#include "absl/strings/str_format.h" +#include "absl/time/time.h" +#include "imgui/imgui.h" +#include "app/gui/icons.h" +#include "app/editor/system/toast_manager.h" + +namespace yaze { +namespace editor { + +namespace { + +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 kTimestampColor = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); + +} // namespace + +AgentChatHistoryPopup::AgentChatHistoryPopup() {} + +void AgentChatHistoryPopup::Draw() { + if (!visible_) return; + + // Set drawer position on the LEFT side + ImGuiIO& io = ImGui::GetIO(); + ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(drawer_width_, io.DisplaySize.y), + ImGuiCond_Always); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoCollapse; + + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.10f, 0.95f)); + + if (ImGui::Begin(ICON_MD_CHAT " Chat History", &visible_, flags)) { + // Header with controls + if (ImGui::Button(ICON_MD_OPEN_IN_NEW " Open Full Chat")) { + if (open_chat_callback_) { + open_chat_callback_(); + } + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_MD_REFRESH " Refresh")) { + // Trigger external refresh through callback + if (toast_manager_) { + toast_manager_->Show("Refreshing chat history...", ToastType::kInfo, 1.5f); + } + } + + ImGui::Separator(); + + // Filter dropdown + const char* filter_labels[] = {"All Messages", "User Only", "Agent Only"}; + int current_filter = static_cast(message_filter_); + ImGui::SetNextItemWidth(-1); + if (ImGui::Combo("##filter", ¤t_filter, filter_labels, 3)) { + message_filter_ = static_cast(current_filter); + } + + ImGui::Spacing(); + + // Auto-scroll checkbox + ImGui::Checkbox("Auto-scroll", &auto_scroll_); + + ImGui::Separator(); + + // Message count indicator + int visible_count = 0; + for (const auto& msg : messages_) { + if (msg.is_internal) continue; + if (message_filter_ == MessageFilter::kUserOnly && + msg.sender != cli::agent::ChatMessage::Sender::kUser) continue; + if (message_filter_ == MessageFilter::kAgentOnly && + msg.sender != cli::agent::ChatMessage::Sender::kAgent) continue; + visible_count++; + } + + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), + "%d message%s", visible_count, visible_count == 1 ? "" : "s"); + + ImGui::Separator(); + + // Message list + ImGui::BeginChild("MessageList", ImVec2(0, -45), true); + DrawMessageList(); + + if (needs_scroll_) { + ImGui::SetScrollHereY(1.0f); + needs_scroll_ = false; + } + + ImGui::EndChild(); + + // Action buttons at bottom + ImGui::Separator(); + DrawActionButtons(); + } + ImGui::End(); + + ImGui::PopStyleColor(); +} + +void AgentChatHistoryPopup::DrawMessageList() { + if (messages_.empty()) { + ImGui::TextDisabled("No messages yet. Start a conversation in the chat window."); + return; + } + + // Calculate starting index for display limit + int start_index = messages_.size() > display_limit_ ? + messages_.size() - display_limit_ : 0; + + for (int i = start_index; i < messages_.size(); ++i) { + const auto& msg = messages_[i]; + + // Skip internal messages + if (msg.is_internal) continue; + + // Apply filter + if (message_filter_ == MessageFilter::kUserOnly && + msg.sender != cli::agent::ChatMessage::Sender::kUser) continue; + if (message_filter_ == MessageFilter::kAgentOnly && + msg.sender != cli::agent::ChatMessage::Sender::kAgent) continue; + + DrawMessage(msg, i); + } +} + +void AgentChatHistoryPopup::DrawMessage(const cli::agent::ChatMessage& msg, int index) { + ImGui::PushID(index); + + bool from_user = (msg.sender == cli::agent::ChatMessage::Sender::kUser); + ImVec4 header_color = from_user ? kUserColor : kAgentColor; + const char* sender_label = from_user ? ICON_MD_PERSON " You" : ICON_MD_SMART_TOY " Agent"; + + // Message header with sender and timestamp + ImGui::PushStyleColor(ImGuiCol_Text, header_color); + ImGui::Text("%s", sender_label); + ImGui::PopStyleColor(); + + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Text, kTimestampColor); + ImGui::Text("%s", absl::FormatTime("%H:%M:%S", msg.timestamp, absl::LocalTimeZone()).c_str()); + ImGui::PopStyleColor(); + + // Message content + ImGui::Indent(10.0f); + + if (msg.table_data.has_value()) { + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.9f, 1.0f), ICON_MD_TABLE_CHART " [Table Data]"); + } else if (msg.json_pretty.has_value()) { + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.9f, 1.0f), ICON_MD_DATA_OBJECT " [Structured Response]"); + } else { + // Truncate long messages + std::string content = msg.message; + if (content.length() > 200) { + content = content.substr(0, 197) + "..."; + } + ImGui::TextWrapped("%s", content.c_str()); + } + + // Show proposal indicator if present + if (msg.proposal.has_value()) { + ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.4f, 1.0f), + ICON_MD_PREVIEW " Proposal: %s", msg.proposal->id.c_str()); + } + + ImGui::Unindent(10.0f); + ImGui::Spacing(); + ImGui::Separator(); + + ImGui::PopID(); +} + +void AgentChatHistoryPopup::DrawActionButtons() { + if (ImGui::Button(ICON_MD_DELETE " Clear History", ImVec2(-1, 0))) { + ClearHistory(); + } + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Clear all messages from the popup view\n(Full history preserved in chat window)"); + } +} + +void AgentChatHistoryPopup::UpdateHistory(const std::vector& history) { + bool had_messages = !messages_.empty(); + int old_size = messages_.size(); + + messages_ = history; + + // Auto-scroll if new messages arrived + if (auto_scroll_ && messages_.size() > old_size) { + needs_scroll_ = true; + } +} + +void AgentChatHistoryPopup::NotifyNewMessage() { + if (auto_scroll_) { + needs_scroll_ = true; + } + + // Flash the window to draw attention + if (toast_manager_ && !visible_) { + toast_manager_->Show(ICON_MD_CHAT " New message received", ToastType::kInfo, 2.0f); + } +} + +void AgentChatHistoryPopup::ClearHistory() { + messages_.clear(); + + if (toast_manager_) { + toast_manager_->Show("Chat history popup cleared", ToastType::kInfo, 2.0f); + } +} + +void AgentChatHistoryPopup::ExportHistory() { + // TODO: Implement export functionality + if (toast_manager_) { + toast_manager_->Show("Export feature coming soon", ToastType::kInfo, 2.0f); + } +} + +void AgentChatHistoryPopup::ScrollToBottom() { + needs_scroll_ = true; +} + +} // namespace editor +} // namespace yaze diff --git a/src/app/editor/system/agent_chat_history_popup.h b/src/app/editor/system/agent_chat_history_popup.h new file mode 100644 index 00000000..c095af14 --- /dev/null +++ b/src/app/editor/system/agent_chat_history_popup.h @@ -0,0 +1,98 @@ +#ifndef YAZE_APP_EDITOR_SYSTEM_AGENT_CHAT_HISTORY_POPUP_H +#define YAZE_APP_EDITOR_SYSTEM_AGENT_CHAT_HISTORY_POPUP_H + +#include +#include + +#include "cli/service/agent/conversational_agent_service.h" + +namespace yaze { +namespace editor { + +class ToastManager; + +/** + * @class AgentChatHistoryPopup + * @brief ImGui popup drawer for displaying chat history on the left side + * + * Provides a quick-access sidebar for viewing recent chat messages, + * complementing the ProposalDrawer on the right. Features: + * - Recent message list with timestamps + * - User/Agent message differentiation + * - Scroll to view older messages + * - Quick actions (clear, export, open full chat) + * - Syncs with AgentChatWidget and AgentEditor + * + * Positioned on the LEFT side of the screen as a slide-out panel. + */ +class AgentChatHistoryPopup { + public: + AgentChatHistoryPopup(); + ~AgentChatHistoryPopup() = default; + + // Set dependencies + void SetToastManager(ToastManager* toast_manager) { + toast_manager_ = toast_manager; + } + + // Render the popup UI + void Draw(); + + // Show/hide the popup + void Show() { visible_ = true; } + void Hide() { visible_ = false; } + void Toggle() { visible_ = !visible_; } + bool IsVisible() const { return visible_; } + + // Update history from service + void UpdateHistory(const std::vector& history); + + // Notify of new message (triggers auto-scroll) + void NotifyNewMessage(); + + // Set callback for opening full chat window + using OpenChatCallback = std::function; + void SetOpenChatCallback(OpenChatCallback callback) { + open_chat_callback_ = std::move(callback); + } + + private: + void DrawMessageList(); + void DrawMessage(const cli::agent::ChatMessage& msg, int index); + void DrawActionButtons(); + + void ClearHistory(); + void ExportHistory(); + void ScrollToBottom(); + + bool visible_ = false; + bool needs_scroll_ = false; + bool auto_scroll_ = true; + + // History state + std::vector messages_; + int display_limit_ = 50; // Show last 50 messages + + // UI state + float drawer_width_ = 400.0f; + float min_drawer_width_ = 300.0f; + float max_drawer_width_ = 600.0f; + bool is_resizing_ = false; + + // Filter state + enum class MessageFilter { + kAll, + kUserOnly, + kAgentOnly + }; + MessageFilter message_filter_ = MessageFilter::kAll; + + // Dependencies + ToastManager* toast_manager_ = nullptr; + OpenChatCallback open_chat_callback_; +}; + +} // namespace editor +} // namespace yaze + +#endif // YAZE_APP_EDITOR_SYSTEM_AGENT_CHAT_HISTORY_POPUP_H