#include "app/editor/system/proposal_drawer.h" #include #include #include #include "absl/strings/str_format.h" #include "absl/time/time.h" #include "imgui/imgui.h" #include "app/gui/core/icons.h" #include "cli/service/rom/rom_sandbox_manager.h" // Policy evaluation support (optional, only in main yaze build) #ifdef YAZE_ENABLE_POLICY_FRAMEWORK #include "cli/service/planning/policy_evaluator.h" #endif namespace yaze { namespace editor { ProposalDrawer::ProposalDrawer() { RefreshProposals(); } void ProposalDrawer::Draw() { if (!visible_) return; // Set drawer position on the right side ImGuiIO& io = ImGui::GetIO(); ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x - drawer_width_, 0), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(drawer_width_, io.DisplaySize.y), ImGuiCond_Always); ImGuiWindowFlags flags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse; if (ImGui::Begin("Agent Proposals", &visible_, flags)) { if (needs_refresh_) { RefreshProposals(); needs_refresh_ = false; if (!selected_proposal_id_.empty() && !selected_proposal_) { SelectProposal(selected_proposal_id_); } } // Header with refresh button if (ImGui::Button(ICON_MD_REFRESH " Refresh")) { RefreshProposals(); } ImGui::SameLine(); DrawStatusFilter(); ImGui::Separator(); // Split view: proposal list on top, details on bottom float list_height = ImGui::GetContentRegionAvail().y * 0.4f; ImGui::BeginChild("ProposalList", ImVec2(0, list_height), true); DrawProposalList(); ImGui::EndChild(); if (selected_proposal_) { ImGui::Separator(); ImGui::BeginChild("ProposalDetail", ImVec2(0, 0), true); DrawProposalDetail(); ImGui::EndChild(); } } ImGui::End(); // Confirmation dialog if (show_confirm_dialog_) { ImGui::OpenPopup("Confirm Action"); show_confirm_dialog_ = false; } if (ImGui::BeginPopupModal("Confirm Action", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("Are you sure you want to %s this proposal?", confirm_action_.c_str()); ImGui::Separator(); if (ImGui::Button("Yes", ImVec2(120, 0))) { if (confirm_action_ == "accept") { AcceptProposal(confirm_proposal_id_); } else if (confirm_action_ == "reject") { RejectProposal(confirm_proposal_id_); } else if (confirm_action_ == "delete") { DeleteProposal(confirm_proposal_id_); } ImGui::CloseCurrentPopup(); RefreshProposals(); } ImGui::SameLine(); if (ImGui::Button("No", ImVec2(120, 0))) { ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } #ifdef YAZE_ENABLE_POLICY_FRAMEWORK // Policy override dialog (NEW) if (show_override_dialog_) { ImGui::OpenPopup("Override Policy"); show_override_dialog_ = false; } if (ImGui::BeginPopupModal("Override Policy", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), ICON_MD_WARNING " Policy Override Required"); ImGui::Separator(); ImGui::TextWrapped("This proposal has policy warnings."); ImGui::TextWrapped("Do you want to override and accept anyway?"); ImGui::Spacing(); ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "Note: This action will be logged."); ImGui::Separator(); if (ImGui::Button("Override and Accept", ImVec2(150, 0))) { confirm_action_ = "accept"; show_confirm_dialog_ = true; ImGui::CloseCurrentPopup(); } ImGui::SameLine(); if (ImGui::Button("Cancel", ImVec2(150, 0))) { ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } #endif // YAZE_ENABLE_POLICY_FRAMEWORK } void ProposalDrawer::DrawProposalList() { if (proposals_.empty()) { ImGui::TextWrapped("No proposals found."); ImGui::TextWrapped("Run CLI command: z3ed agent run --prompt \"...\""); return; } ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY; if (ImGui::BeginTable("ProposalsTable", 3, flags)) { ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 60.0f); ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 80.0f); ImGui::TableSetupColumn("Prompt", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableHeadersRow(); for (const auto& proposal : proposals_) { ImGui::TableNextRow(); // ID column ImGui::TableSetColumnIndex(0); bool is_selected = (proposal.id == selected_proposal_id_); if (ImGui::Selectable(proposal.id.c_str(), is_selected, ImGuiSelectableFlags_SpanAllColumns)) { SelectProposal(proposal.id); } // Status column ImGui::TableSetColumnIndex(1); switch (proposal.status) { case cli::ProposalRegistry::ProposalStatus::kPending: ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Pending"); break; case cli::ProposalRegistry::ProposalStatus::kAccepted: ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Accepted"); break; case cli::ProposalRegistry::ProposalStatus::kRejected: ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Rejected"); break; } // Prompt column (truncated) ImGui::TableSetColumnIndex(2); std::string truncated = proposal.prompt; if (truncated.length() > 30) { truncated = truncated.substr(0, 27) + "..."; } ImGui::TextWrapped("%s", truncated.c_str()); } ImGui::EndTable(); } } void ProposalDrawer::DrawProposalDetail() { if (!selected_proposal_) return; const auto& p = *selected_proposal_; // Metadata section if (ImGui::CollapsingHeader("Metadata", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Text("ID: %s", p.id.c_str()); ImGui::Text("Sandbox: %s", p.sandbox_id.c_str()); ImGui::Text("Created: %s", absl::FormatTime(p.created_at).c_str()); if (p.reviewed_at.has_value()) { ImGui::Text("Reviewed: %s", absl::FormatTime(*p.reviewed_at).c_str()); } ImGui::Text("Commands: %d", p.commands_executed); ImGui::Text("Bytes Changed: %d", p.bytes_changed); ImGui::Separator(); ImGui::TextWrapped("Prompt: %s", p.prompt.c_str()); ImGui::TextWrapped("Description: %s", p.description.c_str()); } // Diff section if (ImGui::CollapsingHeader("Diff", ImGuiTreeNodeFlags_DefaultOpen)) { if (diff_content_.empty() && std::filesystem::exists(p.diff_path)) { std::ifstream diff_file(p.diff_path); if (diff_file.is_open()) { std::stringstream buffer; buffer << diff_file.rdbuf(); diff_content_ = buffer.str(); } } if (!diff_content_.empty()) { ImGui::BeginChild("DiffContent", ImVec2(0, 150), true, ImGuiWindowFlags_HorizontalScrollbar); ImGui::TextUnformatted(diff_content_.c_str()); ImGui::EndChild(); } else { ImGui::TextWrapped("No diff available"); } } // Log section if (ImGui::CollapsingHeader("Execution Log")) { if (log_content_.empty() && std::filesystem::exists(p.log_path)) { std::ifstream log_file(p.log_path); if (log_file.is_open()) { std::stringstream buffer; std::string line; int line_count = 0; while (std::getline(log_file, line) && line_count < log_display_lines_) { buffer << line << "\n"; line_count++; } if (line_count >= log_display_lines_) { buffer << "... (truncated, see " << p.log_path.string() << ")\n"; } log_content_ = buffer.str(); } } if (!log_content_.empty()) { ImGui::BeginChild("LogContent", ImVec2(0, 150), true, ImGuiWindowFlags_HorizontalScrollbar); ImGui::TextUnformatted(log_content_.c_str()); ImGui::EndChild(); } else { ImGui::TextWrapped("No log available"); } } // Policy Status section (NEW) #ifdef YAZE_ENABLE_POLICY_FRAMEWORK DrawPolicyStatus(); #endif // Action buttons ImGui::Separator(); DrawActionButtons(); } void ProposalDrawer::DrawStatusFilter() { const char* filter_labels[] = {"All", "Pending", "Accepted", "Rejected"}; int current_filter = static_cast(status_filter_); ImGui::SetNextItemWidth(120.0f); if (ImGui::Combo("Filter", ¤t_filter, filter_labels, 4)) { status_filter_ = static_cast(current_filter); RefreshProposals(); } } void ProposalDrawer::DrawPolicyStatus() { #ifdef YAZE_ENABLE_POLICY_FRAMEWORK if (!selected_proposal_) return; const auto& p = *selected_proposal_; // Only evaluate policies for pending proposals if (p.status != cli::ProposalRegistry::ProposalStatus::kPending) { return; } if (ImGui::CollapsingHeader("Policy Status", ImGuiTreeNodeFlags_DefaultOpen)) { auto& policy_eval = cli::PolicyEvaluator::GetInstance(); if (!policy_eval.IsEnabled()) { ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), ICON_MD_INFO " No policies configured"); ImGui::TextWrapped("Create .yaze/policies/agent.yaml to enable policy evaluation"); return; } // Evaluate proposal against policies auto policy_result = policy_eval.EvaluateProposal(p.id); if (!policy_result.ok()) { ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), ICON_MD_ERROR " Policy evaluation failed"); ImGui::TextWrapped("%s", policy_result.status().message().data()); return; } const auto& result = policy_result.value(); // Overall status if (result.is_clean()) { ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), ICON_MD_CHECK_CIRCLE " All policies passed"); } else if (result.passed) { ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), ICON_MD_WARNING " Passed with warnings"); } else { ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), ICON_MD_CANCEL " Critical violations found"); } ImGui::Separator(); // Show critical violations if (!result.critical_violations.empty()) { ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), ICON_MD_BLOCK " Critical Violations:"); for (const auto& violation : result.critical_violations) { ImGui::Bullet(); ImGui::TextWrapped("%s: %s", violation.policy_name.c_str(), violation.message.c_str()); if (!violation.details.empty()) { ImGui::Indent(); ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", violation.details.c_str()); ImGui::Unindent(); } } ImGui::Separator(); } // Show warnings if (!result.warnings.empty()) { ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), ICON_MD_WARNING " Warnings:"); for (const auto& violation : result.warnings) { ImGui::Bullet(); ImGui::TextWrapped("%s: %s", violation.policy_name.c_str(), violation.message.c_str()); if (!violation.details.empty()) { ImGui::Indent(); ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", violation.details.c_str()); ImGui::Unindent(); } } ImGui::Separator(); } // Show info messages if (!result.info.empty()) { ImGui::TextColored(ImVec4(0.5f, 0.5f, 1.0f, 1.0f), ICON_MD_INFO " Information:"); for (const auto& violation : result.info) { ImGui::Bullet(); ImGui::TextWrapped("%s: %s", violation.policy_name.c_str(), violation.message.c_str()); } } } #endif // YAZE_ENABLE_POLICY_FRAMEWORK } void ProposalDrawer::DrawActionButtons() { if (!selected_proposal_) return; const auto& p = *selected_proposal_; bool is_pending = p.status == cli::ProposalRegistry::ProposalStatus::kPending; // Evaluate policies to determine if Accept button should be enabled bool can_accept = true; bool needs_override = false; #ifdef YAZE_ENABLE_POLICY_FRAMEWORK if (is_pending) { auto& policy_eval = cli::PolicyEvaluator::GetInstance(); if (policy_eval.IsEnabled()) { auto policy_result = policy_eval.EvaluateProposal(p.id); if (policy_result.ok()) { const auto& result = policy_result.value(); can_accept = !result.has_critical_violations(); needs_override = result.can_accept_with_override(); } } } #endif // YAZE_ENABLE_POLICY_FRAMEWORK // Accept button (only for pending proposals, gated by policy) if (is_pending) { if (!can_accept) { ImGui::BeginDisabled(); } if (ImGui::Button(ICON_MD_CHECK " Accept", ImVec2(-1, 0))) { if (needs_override) { // Show override confirmation dialog show_override_dialog_ = true; confirm_proposal_id_ = p.id; } else { // Proceed directly to accept confirmation confirm_action_ = "accept"; confirm_proposal_id_ = p.id; show_confirm_dialog_ = true; } } if (!can_accept) { ImGui::EndDisabled(); ImGui::SameLine(); ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "(Blocked by policy)"); } // Reject button (only for pending proposals) if (ImGui::Button(ICON_MD_CLOSE " Reject", ImVec2(-1, 0))) { confirm_action_ = "reject"; confirm_proposal_id_ = p.id; show_confirm_dialog_ = true; } } // Delete button (for all proposals) if (ImGui::Button(ICON_MD_DELETE " Delete", ImVec2(-1, 0))) { confirm_action_ = "delete"; confirm_proposal_id_ = p.id; show_confirm_dialog_ = true; } } void ProposalDrawer::FocusProposal(const std::string& proposal_id) { visible_ = true; selected_proposal_id_ = proposal_id; selected_proposal_ = nullptr; needs_refresh_ = true; } void ProposalDrawer::RefreshProposals() { auto& registry = cli::ProposalRegistry::Instance(); std::optional filter; switch (status_filter_) { case StatusFilter::kPending: filter = cli::ProposalRegistry::ProposalStatus::kPending; break; case StatusFilter::kAccepted: filter = cli::ProposalRegistry::ProposalStatus::kAccepted; break; case StatusFilter::kRejected: filter = cli::ProposalRegistry::ProposalStatus::kRejected; break; case StatusFilter::kAll: filter = std::nullopt; break; } proposals_ = registry.ListProposals(filter); // Clear selection if proposal no longer exists if (!selected_proposal_id_.empty()) { bool found = false; for (const auto& p : proposals_) { if (p.id == selected_proposal_id_) { found = true; break; } } if (!found) { selected_proposal_id_.clear(); selected_proposal_ = nullptr; diff_content_.clear(); log_content_.clear(); } } } void ProposalDrawer::SelectProposal(const std::string& proposal_id) { selected_proposal_id_ = proposal_id; selected_proposal_ = nullptr; diff_content_.clear(); log_content_.clear(); // Find the proposal in our list for (auto& p : proposals_) { if (p.id == proposal_id) { selected_proposal_ = &p; break; } } } absl::Status ProposalDrawer::AcceptProposal(const std::string& proposal_id) { auto& registry = cli::ProposalRegistry::Instance(); // Get proposal metadata to find sandbox auto proposal_or = registry.GetProposal(proposal_id); if (!proposal_or.ok()) { return proposal_or.status(); } const auto& proposal = *proposal_or; // Check if ROM is available if (!rom_) { return absl::FailedPreconditionError( "No ROM loaded. Cannot merge proposal changes."); } // Find sandbox ROM path using the sandbox_id from the proposal auto& sandbox_mgr = cli::RomSandboxManager::Instance(); auto sandboxes = sandbox_mgr.ListSandboxes(); std::filesystem::path sandbox_rom_path; for (const auto& sandbox : sandboxes) { if (sandbox.id == proposal.sandbox_id) { sandbox_rom_path = sandbox.rom_path; break; } } if (sandbox_rom_path.empty()) { return absl::NotFoundError( absl::StrFormat("Sandbox ROM not found for proposal %s (sandbox: %s)", proposal_id, proposal.sandbox_id)); } // Verify sandbox ROM exists std::error_code ec; if (!std::filesystem::exists(sandbox_rom_path, ec)) { return absl::NotFoundError( absl::StrFormat("Sandbox ROM file does not exist: %s", sandbox_rom_path.string())); } // Load sandbox ROM data Rom sandbox_rom; auto load_status = sandbox_rom.LoadFromFile(sandbox_rom_path.string()); if (!load_status.ok()) { return absl::InternalError( absl::StrFormat("Failed to load sandbox ROM: %s", load_status.message())); } // Merge sandbox ROM data into main ROM // Copy the entire ROM data vector from sandbox to main ROM const auto& sandbox_data = sandbox_rom.vector(); auto merge_status = rom_->WriteVector(0, sandbox_data); if (!merge_status.ok()) { return absl::InternalError( absl::StrFormat("Failed to merge sandbox ROM data: %s", merge_status.message())); } // Update proposal status auto status = registry.UpdateStatus( proposal_id, cli::ProposalRegistry::ProposalStatus::kAccepted); if (status.ok()) { // Mark ROM as dirty so save prompts appear // Note: Rom tracks dirty state internally via Write operations // The WriteVector call above already marked it as dirty } needs_refresh_ = true; return status; } absl::Status ProposalDrawer::RejectProposal(const std::string& proposal_id) { auto& registry = cli::ProposalRegistry::Instance(); auto status = registry.UpdateStatus( proposal_id, cli::ProposalRegistry::ProposalStatus::kRejected); needs_refresh_ = true; return status; } absl::Status ProposalDrawer::DeleteProposal(const std::string& proposal_id) { auto& registry = cli::ProposalRegistry::Instance(); auto status = registry.RemoveProposal(proposal_id); if (proposal_id == selected_proposal_id_) { selected_proposal_id_.clear(); selected_proposal_ = nullptr; diff_content_.clear(); log_content_.clear(); } needs_refresh_ = true; return status; } } // namespace editor } // namespace yaze