feat: Add Agent Chat History Popup for Enhanced User Interaction

- Introduced the AgentChatHistoryPopup class to provide a sidebar for displaying recent chat messages, improving accessibility and user experience.
- Implemented message filtering options (All, User Only, Agent Only) and auto-scroll functionality for better message management.
- Enhanced the AgentChatWidget to synchronize chat history with the new popup, ensuring users have up-to-date information at their fingertips.
- Updated CMake configuration to include the new build_cleaner target for improved project maintenance.
This commit is contained in:
scawful
2025-10-05 05:31:07 -04:00
parent e490dea2e5
commit 2aeb384034
6 changed files with 372 additions and 0 deletions

View File

@@ -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"
)

View File

@@ -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);

View File

@@ -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();

View File

@@ -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
)

View File

@@ -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<int>(message_filter_);
ImGui::SetNextItemWidth(-1);
if (ImGui::Combo("##filter", &current_filter, filter_labels, 3)) {
message_filter_ = static_cast<MessageFilter>(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<cli::agent::ChatMessage>& 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

View File

@@ -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 <string>
#include <vector>
#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<cli::agent::ChatMessage>& history);
// Notify of new message (triggers auto-scroll)
void NotifyNewMessage();
// Set callback for opening full chat window
using OpenChatCallback = std::function<void()>;
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<cli::agent::ChatMessage> 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