Files
yaze/src/app/editor/agent/agent_chat_widget.cc
scawful 8d2f8e478e feat: Add Automation Panel to Agent Chat Widget
- Introduced a new Automation Panel in the Agent Chat Widget, providing users with real-time automation status, quick action buttons, and a list of recent automation actions.
- Implemented connection status indicators and refresh functionality for automation state, enhancing user interaction and feedback.
- Updated the header file to declare the new RenderAutomationPanel method, ensuring proper integration within the widget's UI structure.
2025-10-06 01:12:43 -04:00

2984 lines
104 KiB
C++

#define IMGUI_DEFINE_MATH_OPERATORS
#include "app/editor/agent/agent_chat_widget.h"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#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/icons.h"
#include "app/rom.h"
#include "imgui/imgui.h"
#include "imgui/misc/cpp/imgui_stdlib.h"
#include "util/file_util.h"
#if defined(YAZE_WITH_GRPC)
#include "app/test/test_manager.h"
#endif
namespace {
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 = "") {
std::filesystem::path base = ExpandUserPath(yaze::util::GetConfigDirectory());
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<int>(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<int>(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<AgentChatHistoryCodec::Snapshot> 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<ChatMessage>& 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<int>(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", &current_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<int>(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<int>(chat_sessions_.size() + 1));
std::string session_name = absl::StrFormat(
"Chat %d", static_cast<int>(chat_sessions_.size() + 1));
chat_sessions_.emplace_back(session_id, session_name);
active_session_index_ = static_cast<int>(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<int>(i));
bool is_active = (active_session_index_ == static_cast<int>(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<int>(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<int*>(&collaboration_state_.mode),
static_cast<int>(CollaborationMode::kLocal));
ImGui::SameLine();
ImGui::RadioButton(ICON_MD_WIFI " Network##collab_mode_network",
reinterpret_cast<int*>(&collaboration_state_.mode),
static_cast<int>(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<int>(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<bool>(collaboration_callbacks_.host_session);
const bool can_join =
static_cast<bool>(collaboration_callbacks_.join_session);
const bool can_leave =
static_cast<bool>(collaboration_callbacks_.leave_session);
const bool can_refresh =
static_cast<bool>(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<bool>(multimodal_callbacks_.capture_snapshot);
bool can_send = static_cast<bool>(multimodal_callbacks_.send_to_gemini);
// Ultra-compact mode selector
ImGui::RadioButton("Full##mm_full",
reinterpret_cast<int*>(&multimodal_state_.capture_mode),
static_cast<int>(CaptureMode::kFullWindow));
ImGui::SameLine();
ImGui::RadioButton("Active##mm_active",
reinterpret_cast<int*>(&multimodal_state_.capture_mode),
static_cast<int>(CaptureMode::kActiveEditor));
ImGui::SameLine();
ImGui::RadioButton("Window##mm_window",
reinterpret_cast<int*>(&multimodal_state_.capture_mode),
static_cast<int>(CaptureMode::kSpecificWindow));
ImGui::SameLine();
ImGui::RadioButton("Region##mm_region",
reinterpret_cast<int*>(&multimodal_state_.capture_mode),
static_cast<int>(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");
AgentUI::PushPanelStyle();
if (ImGui::BeginChild("Automation_Panel", ImVec2(0, 180), true)) {
AgentUI::RenderSectionHeader(ICON_MD_SMART_TOY, "GUI Automation",
theme.provider_ollama);
// Connection status
bool connected = automation_state_.harness_connected;
ImVec4 status_color = connected ? theme.status_success : theme.status_warning;
const char* status_text = connected ? "Connected" : "Disconnected";
ImGui::TextColored(status_color, "%s %s",
connected ? ICON_MD_CHECK_CIRCLE : ICON_MD_WARNING,
status_text);
ImGui::SameLine();
if (ImGui::SmallButton(ICON_MD_REFRESH " Refresh")) {
if (automation_callbacks_.show_active_tests) {
automation_callbacks_.show_active_tests();
}
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Refresh automation status");
}
// Quick action buttons
ImGui::SameLine();
if (ImGui::SmallButton(ICON_MD_DASHBOARD " Dashboard")) {
if (automation_callbacks_.open_harness_dashboard) {
automation_callbacks_.open_harness_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");
}
// Recent automation actions
ImGui::Spacing();
ImGui::Separator();
ImGui::Text(ICON_MD_LIST " Recent Actions:");
if (automation_state_.recent_tests.empty()) {
ImGui::TextDisabled(" No recent actions");
} else {
ImGui::BeginChild("ActionQueue", ImVec2(0, 80), false);
for (const auto& test : automation_state_.recent_tests) {
ImGui::PushID(test.test_id.c_str());
// Status icon
ImVec4 action_color;
const char* status_icon;
if (test.status == "success" || test.status == "completed") {
action_color = theme.status_success;
status_icon = ICON_MD_CHECK_CIRCLE;
} else if (test.status == "running" || test.status == "in_progress") {
action_color = theme.provider_ollama;
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;
status_icon = ICON_MD_HELP;
}
ImGui::TextColored(action_color, "%s", status_icon);
ImGui::SameLine();
// Action name
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::TextDisabled("(%ds ago)", static_cast<int>(absl::ToInt64Seconds(elapsed)));
} else if (elapsed < absl::Minutes(60)) {
ImGui::TextDisabled("(%dm ago)", static_cast<int>(absl::ToInt64Minutes(elapsed)));
} else {
ImGui::TextDisabled("(%dh ago)", static_cast<int>(absl::ToInt64Hours(elapsed)));
}
}
// Message (if any)
if (!test.message.empty()) {
ImGui::Indent(20.0f);
ImGui::TextWrapped(ICON_MD_MESSAGE " %s", test.message.c_str());
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<bool>(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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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
// 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::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) {
// For now, store the path and mark as loaded
// Actual texture loading would need to use SDL_image or stb_image
// and then upload to GPU via ImGui backend
multimodal_state_.preview.loaded = true;
multimodal_state_.preview.show_preview = true;
// TODO: Implement actual texture loading using SDL_image or stb_image
// For now, just track that we have a valid image path
if (toast_manager_) {
toast_manager_->Show("Screenshot preview loaded", ToastType::kInfo, 2.0f);
}
}
void AgentChatWidget::UnloadScreenshotPreview() {
if (multimodal_state_.preview.texture_id != nullptr) {
// TODO: Free the texture from GPU
// This requires backend-specific cleanup
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, "%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, 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;
}
// TODO: Implement actual region capture
// This would involve:
// 1. Capturing the full screenshot
// 2. Cropping to the selected region
// 3. Saving the cropped image
if (toast_manager_) {
toast_manager_->Show(
absl::StrFormat("Region captured: %.0fx%.0f", width, height),
ToastType::kSuccess, 3.0f
);
}
// For now, just call the regular capture callback
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;
multimodal_state_.status_message = "Region captured";
multimodal_state_.last_updated = absl::Now();
LoadScreenshotPreview(captured_path);
MarkHistoryDirty();
}
}
}
} // namespace editor
} // namespace yaze