yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
agent_proposals_panel.cc
Go to the documentation of this file.
1#define IMGUI_DEFINE_MATH_OPERATORS
2
4
5#include <fstream>
6#include <sstream>
7#include <string>
8
9#include "absl/strings/str_format.h"
10#include "absl/time/clock.h"
11#include "absl/time/time.h"
14#include "app/gui/core/icons.h"
15#include "rom/rom.h"
16#include "imgui/imgui.h"
17
18namespace yaze {
19namespace editor {
20
21namespace {
22
23std::string FormatRelativeTime(absl::Time timestamp) {
24 if (timestamp == absl::InfinitePast()) {
25 return "—";
26 }
27 absl::Duration delta = absl::Now() - timestamp;
28 if (delta < absl::Seconds(60)) {
29 return "just now";
30 }
31 if (delta < absl::Minutes(60)) {
32 return absl::StrFormat("%dm ago",
33 static_cast<int>(delta / absl::Minutes(1)));
34 }
35 if (delta < absl::Hours(24)) {
36 return absl::StrFormat("%dh ago", static_cast<int>(delta / absl::Hours(1)));
37 }
38 return absl::FormatTime("%b %d", timestamp, absl::LocalTimeZone());
39}
40
41std::string ReadFileContents(const std::filesystem::path& path, int max_lines = 100) {
42 std::ifstream file(path);
43 if (!file.is_open()) {
44 return "";
45 }
46
47 std::ostringstream content;
48 std::string line;
49 int line_count = 0;
50 while (std::getline(file, line) && line_count < max_lines) {
51 content << line << "\n";
52 ++line_count;
53 }
54
55 if (line_count >= max_lines) {
56 content << "\n... (truncated)\n";
57 }
58
59 return content.str();
60}
61
62} // namespace
63
67
69 context_ = context;
70}
71
73 toast_manager_ = toast_manager;
74}
75
77
82
83void AgentProposalsPanel::Draw(float available_height) {
84 if (needs_refresh_) {
86 }
87
88 const auto& theme = AgentUI::GetTheme();
89
90 // Status filter
92
93 ImGui::Separator();
94
95 // Calculate heights
96 float detail_height = selected_proposal_ && !compact_mode_ ? 200.0f : 0.0f;
97 float list_height = available_height > 0
98 ? available_height - ImGui::GetCursorPosY() - detail_height
99 : ImGui::GetContentRegionAvail().y - detail_height;
100
101 // Proposal list
102 ImGui::PushStyleColor(ImGuiCol_ChildBg, theme.panel_bg_darker);
103 ImGui::BeginChild("ProposalList", ImVec2(0, list_height), false);
105 ImGui::EndChild();
106 ImGui::PopStyleColor();
107
108 // Detail view (only in non-compact mode)
110 ImGui::Separator();
112 }
113
114 // Confirmation dialog
116 ImGui::OpenPopup("Confirm Action");
117 }
118
119 if (ImGui::BeginPopupModal("Confirm Action", nullptr,
120 ImGuiWindowFlags_AlwaysAutoResize)) {
121 ImGui::Text("Are you sure you want to %s proposal %s?",
122 confirm_action_.c_str(), confirm_proposal_id_.c_str());
123 ImGui::Separator();
124
125 if (ImGui::Button("Yes", ImVec2(80, 0))) {
126 if (confirm_action_ == "accept") {
128 } else if (confirm_action_ == "reject") {
130 } else if (confirm_action_ == "delete") {
132 }
133 show_confirm_dialog_ = false;
134 ImGui::CloseCurrentPopup();
135 }
136 ImGui::SameLine();
137 if (ImGui::Button("No", ImVec2(80, 0))) {
138 show_confirm_dialog_ = false;
139 ImGui::CloseCurrentPopup();
140 }
141 ImGui::EndPopup();
142 }
143}
144
146 if (!context_) return;
147
148 auto& proposal_state = context_->proposal_state();
149 const char* filter_labels[] = {"All", "Pending", "Accepted", "Rejected"};
150 int current_filter = static_cast<int>(proposal_state.filter_mode);
151
152 if (compact_mode_) {
153 // Compact: just show pending count badge
154 ImGui::Text("%s Proposals", ICON_MD_RULE);
155 ImGui::SameLine();
156 int pending = GetPendingCount();
157 if (pending > 0) {
158 AgentUI::StatusBadge(absl::StrFormat("%d", pending).c_str(),
160 }
161 } else {
162 // Full: show filter buttons
163 for (int i = 0; i < 4; ++i) {
164 if (i > 0) ImGui::SameLine();
165 bool selected = (current_filter == i);
166 if (selected) {
167 ImGui::PushStyleColor(ImGuiCol_Button,
168 ImGui::GetStyle().Colors[ImGuiCol_ButtonActive]);
169 }
170 if (ImGui::SmallButton(filter_labels[i])) {
171 proposal_state.filter_mode =
172 static_cast<ProposalState::FilterMode>(i);
173 needs_refresh_ = true;
174 }
175 if (selected) {
176 ImGui::PopStyleColor();
177 }
178 }
179 }
180}
181
183 if (proposals_.empty()) {
184 ImGui::Spacing();
185 ImGui::TextDisabled("No proposals found");
186 return;
187 }
188
189 // Filter proposals based on current filter
191 if (context_) {
192 filter_mode = context_->proposal_state().filter_mode;
193 }
194
195 for (const auto& proposal : proposals_) {
196 // Apply filter
197 if (filter_mode != ProposalState::FilterMode::kAll) {
198 bool matches = false;
199 switch (filter_mode) {
201 matches = (proposal.status ==
203 break;
205 matches = (proposal.status ==
207 break;
209 matches = (proposal.status ==
211 break;
212 default:
213 matches = true;
214 }
215 if (!matches) continue;
216 }
217
218 DrawProposalRow(proposal);
219 }
220}
221
224 const auto& theme = AgentUI::GetTheme();
225 bool is_selected = (proposal.id == selected_proposal_id_);
226
227 ImGui::PushID(proposal.id.c_str());
228
229 // Selectable row
230 if (ImGui::Selectable("##row", is_selected,
231 ImGuiSelectableFlags_SpanAllColumns |
232 ImGuiSelectableFlags_AllowOverlap,
233 ImVec2(0, compact_mode_ ? 24.0f : 40.0f))) {
234 SelectProposal(proposal.id);
235 }
236
237 ImGui::SameLine();
238
239 // Status icon
240 ImVec4 status_color = GetStatusColor(proposal.status);
241 ImGui::TextColored(status_color, "%s", GetStatusIcon(proposal.status));
242 ImGui::SameLine();
243
244 // Proposal ID and description
245 if (compact_mode_) {
246 ImGui::Text("%s", proposal.id.c_str());
247 ImGui::SameLine();
248 ImGui::TextDisabled("(%d changes)", proposal.bytes_changed);
249 } else {
250 ImGui::BeginGroup();
251 ImGui::Text("%s", proposal.id.c_str());
252 ImGui::TextDisabled("%s", proposal.description.empty()
253 ? "(no description)"
254 : proposal.description.c_str());
255 ImGui::EndGroup();
256
257 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 150.0f);
258
259 // Time and stats
260 ImGui::BeginGroup();
261 ImGui::TextDisabled("%s", FormatRelativeTime(proposal.created_at).c_str());
262 ImGui::TextDisabled("%d bytes, %d cmds", proposal.bytes_changed,
263 proposal.commands_executed);
264 ImGui::EndGroup();
265 }
266
267 // Quick actions (only for pending)
269 ImGui::SameLine(ImGui::GetContentRegionAvail().x - (compact_mode_ ? 60.0f : 100.0f));
270 DrawQuickActions(proposal);
271 }
272
273 ImGui::PopID();
274}
275
278 const auto& theme = AgentUI::GetTheme();
279
280 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
281
282 if (compact_mode_) {
283 // Just accept/reject icons
284 ImGui::PushStyleColor(ImGuiCol_Text, theme.status_success);
285 if (ImGui::SmallButton(ICON_MD_CHECK)) {
286 confirm_action_ = "accept";
287 confirm_proposal_id_ = proposal.id;
289 }
290 ImGui::PopStyleColor();
291
292 ImGui::SameLine();
293
294 ImGui::PushStyleColor(ImGuiCol_Text, theme.status_error);
295 if (ImGui::SmallButton(ICON_MD_CLOSE)) {
296 confirm_action_ = "reject";
297 confirm_proposal_id_ = proposal.id;
299 }
300 ImGui::PopStyleColor();
301 } else {
302 // Full buttons
303 if (AgentUI::StyledButton(ICON_MD_CHECK " Accept", theme.status_success,
304 ImVec2(0, 0))) {
305 confirm_action_ = "accept";
306 confirm_proposal_id_ = proposal.id;
308 }
309 ImGui::SameLine();
310 if (AgentUI::StyledButton(ICON_MD_CLOSE " Reject", theme.status_error,
311 ImVec2(0, 0))) {
312 confirm_action_ = "reject";
313 confirm_proposal_id_ = proposal.id;
315 }
316 }
317
318 ImGui::PopStyleColor();
319}
320
322 if (!selected_proposal_) return;
323
324 const auto& theme = AgentUI::GetTheme();
325
326 ImGui::BeginChild("ProposalDetail", ImVec2(0, 0), true);
327
328 // Header
329 ImGui::TextColored(theme.proposal_accent, "%s %s", ICON_MD_PREVIEW,
330 selected_proposal_->id.c_str());
331 ImGui::SameLine();
332 ImVec4 status_color = GetStatusColor(selected_proposal_->status);
333 ImGui::TextColored(status_color, "(%s)",
335
336 // Description
337 if (!selected_proposal_->description.empty()) {
338 ImGui::TextWrapped("%s", selected_proposal_->description.c_str());
339 }
340
341 ImGui::Separator();
342
343 // Tabs for diff/log
344 if (ImGui::BeginTabBar("DetailTabs")) {
345 if (ImGui::BeginTabItem("Diff")) {
346 DrawDiffView();
347 ImGui::EndTabItem();
348 }
349 if (ImGui::BeginTabItem("Log")) {
350 DrawLogView();
351 ImGui::EndTabItem();
352 }
353 ImGui::EndTabBar();
354 }
355
356 ImGui::EndChild();
357}
358
360 const auto& theme = AgentUI::GetTheme();
361
362 if (diff_content_.empty() && selected_proposal_) {
363 diff_content_ = ReadFileContents(selected_proposal_->diff_path, 200);
364 }
365
366 if (diff_content_.empty()) {
367 ImGui::TextDisabled("No diff available");
368 return;
369 }
370
371 ImGui::PushStyleColor(ImGuiCol_ChildBg, theme.code_bg_color);
372 ImGui::BeginChild("DiffContent", ImVec2(0, 0), false,
373 ImGuiWindowFlags_HorizontalScrollbar);
374
375 // Simple diff rendering with color highlighting
376 std::istringstream stream(diff_content_);
377 std::string line;
378 while (std::getline(stream, line)) {
379 if (line.empty()) {
380 ImGui::NewLine();
381 continue;
382 }
383 if (line[0] == '+') {
384 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.8f, 0.4f, 1.0f));
385 } else if (line[0] == '-') {
386 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.8f, 0.4f, 0.4f, 1.0f));
387 } else if (line[0] == '@') {
388 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.6f, 0.8f, 1.0f));
389 } else {
390 ImGui::PushStyleColor(ImGuiCol_Text, theme.text_secondary_color);
391 }
392 ImGui::TextUnformatted(line.c_str());
393 ImGui::PopStyleColor();
394 }
395
396 ImGui::EndChild();
397 ImGui::PopStyleColor();
398}
399
401 const auto& theme = AgentUI::GetTheme();
402
403 if (log_content_.empty() && selected_proposal_) {
404 log_content_ = ReadFileContents(selected_proposal_->log_path, 100);
405 }
406
407 if (log_content_.empty()) {
408 ImGui::TextDisabled("No log available");
409 return;
410 }
411
412 ImGui::PushStyleColor(ImGuiCol_ChildBg, theme.code_bg_color);
413 ImGui::BeginChild("LogContent", ImVec2(0, 0), false,
414 ImGuiWindowFlags_HorizontalScrollbar);
415 ImGui::TextUnformatted(log_content_.c_str());
416 ImGui::EndChild();
417 ImGui::PopStyleColor();
418}
419
420void AgentProposalsPanel::FocusProposal(const std::string& proposal_id) {
421 SelectProposal(proposal_id);
422 if (context_) {
424 }
425}
426
428 needs_refresh_ = false;
430
431 // Update context state
432 if (context_) {
433 auto& proposal_state = context_->proposal_state();
434 proposal_state.total_proposals = static_cast<int>(proposals_.size());
435 proposal_state.pending_proposals = 0;
436 proposal_state.accepted_proposals = 0;
437 proposal_state.rejected_proposals = 0;
438
439 for (const auto& p : proposals_) {
440 switch (p.status) {
442 ++proposal_state.pending_proposals;
443 break;
445 ++proposal_state.accepted_proposals;
446 break;
448 ++proposal_state.rejected_proposals;
449 break;
450 }
451 }
452 }
453
454 // Clear selected if it no longer exists
455 if (!selected_proposal_id_.empty()) {
456 bool found = false;
457 for (const auto& p : proposals_) {
458 if (p.id == selected_proposal_id_) {
459 found = true;
461 break;
462 }
463 }
464 if (!found) {
465 selected_proposal_id_.clear();
466 selected_proposal_ = nullptr;
467 diff_content_.clear();
468 log_content_.clear();
469 }
470 }
471}
472
474 int count = 0;
475 for (const auto& p : proposals_) {
477 ++count;
478 }
479 }
480 return count;
481}
482
483void AgentProposalsPanel::SelectProposal(const std::string& proposal_id) {
484 if (proposal_id == selected_proposal_id_) {
485 return;
486 }
487
488 selected_proposal_id_ = proposal_id;
489 selected_proposal_ = nullptr;
490 diff_content_.clear();
491 log_content_.clear();
492
493 for (const auto& p : proposals_) {
494 if (p.id == proposal_id) {
496 break;
497 }
498 }
499}
500
514
517 const auto& theme = AgentUI::GetTheme();
518 switch (status) {
520 return theme.status_warning;
522 return theme.status_success;
524 return theme.status_error;
525 default:
526 return theme.text_secondary_color;
527 }
528}
529
531 const std::string& proposal_id) {
534
535 if (status.ok()) {
536 if (toast_manager_) {
538 absl::StrFormat("%s Proposal %s accepted", ICON_MD_CHECK_CIRCLE,
539 proposal_id),
540 ToastType::kSuccess, 3.0f);
541 }
542 needs_refresh_ = true;
543
544 // Notify via callback
547 }
548 } else if (toast_manager_) {
550 absl::StrFormat("Failed to accept proposal: %s", status.message()),
551 ToastType::kError, 5.0f);
552 }
553
554 return status;
555}
556
558 const std::string& proposal_id) {
561
562 if (status.ok()) {
563 if (toast_manager_) {
565 absl::StrFormat("%s Proposal %s rejected", ICON_MD_CANCEL,
566 proposal_id),
567 ToastType::kInfo, 3.0f);
568 }
569 needs_refresh_ = true;
570
571 // Notify via callback
574 }
575 } else if (toast_manager_) {
577 absl::StrFormat("Failed to reject proposal: %s", status.message()),
578 ToastType::kError, 5.0f);
579 }
580
581 return status;
582}
583
585 const std::string& proposal_id) {
586 auto status =
588
589 if (status.ok()) {
590 if (toast_manager_) {
592 absl::StrFormat("%s Proposal %s deleted", ICON_MD_DELETE,
593 proposal_id),
594 ToastType::kInfo, 3.0f);
595 }
596 needs_refresh_ = true;
597
598 if (selected_proposal_id_ == proposal_id) {
599 selected_proposal_id_.clear();
600 selected_proposal_ = nullptr;
601 diff_content_.clear();
602 log_content_.clear();
603 }
604 } else if (toast_manager_) {
606 absl::StrFormat("Failed to delete proposal: %s", status.message()),
607 ToastType::kError, 5.0f);
608 }
609
610 return status;
611}
612
613} // namespace editor
614} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:24
static ProposalRegistry & Instance()
absl::Status UpdateStatus(const std::string &proposal_id, ProposalStatus status)
absl::Status RemoveProposal(const std::string &proposal_id)
std::vector< ProposalMetadata > ListProposals(std::optional< ProposalStatus > filter_status=std::nullopt) const
void SetToastManager(ToastManager *toast_manager)
Set toast manager for notifications.
int GetPendingCount() const
Get count of pending proposals.
void SelectProposal(const std::string &proposal_id)
const char * GetStatusIcon(cli::ProposalRegistry::ProposalStatus status) const
void DrawProposalRow(const cli::ProposalRegistry::ProposalMetadata &proposal)
void DrawProposalList()
Draw just the proposal list (for custom layouts)
void SetRom(Rom *rom)
Set ROM reference for proposal merging.
void Draw(float available_height=0.0f)
Draw the complete proposals panel.
absl::Status DeleteProposal(const std::string &proposal_id)
ImVec4 GetStatusColor(cli::ProposalRegistry::ProposalStatus status) const
void SetContext(AgentUIContext *context)
Set the shared UI context.
std::vector< cli::ProposalRegistry::ProposalMetadata > proposals_
void FocusProposal(const std::string &proposal_id)
Focus a specific proposal by ID.
void DrawProposalDetail()
Draw the detail view for selected proposal.
const cli::ProposalRegistry::ProposalMetadata * selected_proposal_
void DrawQuickActions(const cli::ProposalRegistry::ProposalMetadata &proposal)
absl::Status AcceptProposal(const std::string &proposal_id)
absl::Status RejectProposal(const std::string &proposal_id)
void SetProposalCallbacks(const ProposalCallbacks &callbacks)
Set proposal callbacks.
void RefreshProposals()
Refresh the proposal list from registry.
Unified context for agent UI components.
ProposalState & proposal_state()
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f)
#define ICON_MD_CANCEL
Definition icons.h:364
#define ICON_MD_CHECK
Definition icons.h:397
#define ICON_MD_PENDING
Definition icons.h:1398
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_PREVIEW
Definition icons.h:1512
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_RULE
Definition icons.h:1633
#define ICON_MD_CLOSE
Definition icons.h:418
#define ICON_MD_HELP
Definition icons.h:933
bool StyledButton(const char *label, const ImVec4 &color, const ImVec2 &size)
const AgentUITheme & GetTheme()
void StatusBadge(const char *text, ButtonColor color)
std::string ReadFileContents(const std::filesystem::path &path, int max_lines=100)
Callbacks for proposal operations.
std::function< absl::Status(const std::string &) reject_proposal)
std::function< absl::Status(const std::string &) accept_proposal)