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 "imgui/imgui.h"
16#include "rom/rom.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,
42 int max_lines = 100) {
43 std::ifstream file(path);
44 if (!file.is_open()) {
45 return "";
46 }
47
48 std::ostringstream content;
49 std::string line;
50 int line_count = 0;
51 while (std::getline(file, line) && line_count < max_lines) {
52 content << line << "\n";
53 ++line_count;
54 }
55
56 if (line_count >= max_lines) {
57 content << "\n... (truncated)\n";
58 }
59
60 return content.str();
61}
62
63} // namespace
64
68
70 context_ = context;
71}
72
74 toast_manager_ = toast_manager;
75}
76
78 rom_ = rom;
79}
80
85
86void AgentProposalsPanel::Draw(float available_height) {
87 if (needs_refresh_) {
89 }
90
91 const auto& theme = AgentUI::GetTheme();
92
93 // Status filter
95
96 ImGui::Separator();
97
98 // Calculate heights
99 float detail_height = selected_proposal_ && !compact_mode_ ? 200.0f : 0.0f;
100 float list_height =
101 available_height > 0
102 ? available_height - ImGui::GetCursorPosY() - detail_height
103 : ImGui::GetContentRegionAvail().y - detail_height;
104
105 // Proposal list
106 ImGui::PushStyleColor(ImGuiCol_ChildBg, theme.panel_bg_darker);
107 ImGui::BeginChild("ProposalList", ImVec2(0, list_height), false);
109 ImGui::EndChild();
110 ImGui::PopStyleColor();
111
112 // Detail view (only in non-compact mode)
114 ImGui::Separator();
116 }
117
118 // Confirmation dialog
120 ImGui::OpenPopup("Confirm Action");
121 }
122
123 if (ImGui::BeginPopupModal("Confirm Action", nullptr,
124 ImGuiWindowFlags_AlwaysAutoResize)) {
125 ImGui::Text("Are you sure you want to %s proposal %s?",
126 confirm_action_.c_str(), confirm_proposal_id_.c_str());
127 ImGui::Separator();
128
129 if (ImGui::Button("Yes", ImVec2(80, 0))) {
130 if (confirm_action_ == "accept") {
132 } else if (confirm_action_ == "reject") {
134 } else if (confirm_action_ == "delete") {
136 }
137 show_confirm_dialog_ = false;
138 ImGui::CloseCurrentPopup();
139 }
140 ImGui::SameLine();
141 if (ImGui::Button("No", ImVec2(80, 0))) {
142 show_confirm_dialog_ = false;
143 ImGui::CloseCurrentPopup();
144 }
145 ImGui::EndPopup();
146 }
147}
148
150 if (!context_)
151 return;
152
153 auto& proposal_state = context_->proposal_state();
154 const char* filter_labels[] = {"All", "Pending", "Accepted", "Rejected"};
155 int current_filter = static_cast<int>(proposal_state.filter_mode);
156
157 if (compact_mode_) {
158 // Compact: just show pending count badge
159 ImGui::Text("%s Proposals", ICON_MD_RULE);
160 ImGui::SameLine();
161 int pending = GetPendingCount();
162 if (pending > 0) {
163 AgentUI::StatusBadge(absl::StrFormat("%d", pending).c_str(),
165 }
166 } else {
167 // Full: show filter buttons
168 for (int i = 0; i < 4; ++i) {
169 if (i > 0)
170 ImGui::SameLine();
171 bool selected = (current_filter == i);
172 if (selected) {
173 ImGui::PushStyleColor(ImGuiCol_Button,
174 ImGui::GetStyle().Colors[ImGuiCol_ButtonActive]);
175 }
176 if (ImGui::SmallButton(filter_labels[i])) {
177 proposal_state.filter_mode = static_cast<ProposalState::FilterMode>(i);
178 needs_refresh_ = true;
179 }
180 if (selected) {
181 ImGui::PopStyleColor();
182 }
183 }
184 }
185}
186
188 if (proposals_.empty()) {
189 ImGui::Spacing();
190 ImGui::TextDisabled("No proposals found");
191 return;
192 }
193
194 // Filter proposals based on current filter
196 if (context_) {
197 filter_mode = context_->proposal_state().filter_mode;
198 }
199
200 for (const auto& proposal : proposals_) {
201 // Apply filter
202 if (filter_mode != ProposalState::FilterMode::kAll) {
203 bool matches = false;
204 switch (filter_mode) {
206 matches = (proposal.status ==
208 break;
210 matches = (proposal.status ==
212 break;
214 matches = (proposal.status ==
216 break;
217 default:
218 matches = true;
219 }
220 if (!matches)
221 continue;
222 }
223
224 DrawProposalRow(proposal);
225 }
226}
227
230 const auto& theme = AgentUI::GetTheme();
231 bool is_selected = (proposal.id == selected_proposal_id_);
232
233 ImGui::PushID(proposal.id.c_str());
234
235 // Selectable row
236 if (ImGui::Selectable("##row", is_selected,
237 ImGuiSelectableFlags_SpanAllColumns |
238 ImGuiSelectableFlags_AllowOverlap,
239 ImVec2(0, compact_mode_ ? 24.0f : 40.0f))) {
240 SelectProposal(proposal.id);
241 }
242
243 ImGui::SameLine();
244
245 // Status icon
246 ImVec4 status_color = GetStatusColor(proposal.status);
247 ImGui::TextColored(status_color, "%s", GetStatusIcon(proposal.status));
248 ImGui::SameLine();
249
250 // Proposal ID and description
251 if (compact_mode_) {
252 ImGui::Text("%s", proposal.id.c_str());
253 ImGui::SameLine();
254 ImGui::TextDisabled("(%d changes)", proposal.bytes_changed);
255 } else {
256 ImGui::BeginGroup();
257 ImGui::Text("%s", proposal.id.c_str());
258 ImGui::TextDisabled("%s", proposal.description.empty()
259 ? "(no description)"
260 : proposal.description.c_str());
261 ImGui::EndGroup();
262
263 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 150.0f);
264
265 // Time and stats
266 ImGui::BeginGroup();
267 ImGui::TextDisabled("%s", FormatRelativeTime(proposal.created_at).c_str());
268 ImGui::TextDisabled("%d bytes, %d cmds", proposal.bytes_changed,
269 proposal.commands_executed);
270 ImGui::EndGroup();
271 }
272
273 // Quick actions (only for pending)
275 ImGui::SameLine(ImGui::GetContentRegionAvail().x -
276 (compact_mode_ ? 60.0f : 100.0f));
277 DrawQuickActions(proposal);
278 }
279
280 ImGui::PopID();
281}
282
285 const auto& theme = AgentUI::GetTheme();
286
287 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
288
289 if (compact_mode_) {
290 // Just accept/reject icons
291 ImGui::PushStyleColor(ImGuiCol_Text, theme.status_success);
292 if (ImGui::SmallButton(ICON_MD_CHECK)) {
293 confirm_action_ = "accept";
294 confirm_proposal_id_ = proposal.id;
296 }
297 ImGui::PopStyleColor();
298
299 ImGui::SameLine();
300
301 ImGui::PushStyleColor(ImGuiCol_Text, theme.status_error);
302 if (ImGui::SmallButton(ICON_MD_CLOSE)) {
303 confirm_action_ = "reject";
304 confirm_proposal_id_ = proposal.id;
306 }
307 ImGui::PopStyleColor();
308 } else {
309 // Full buttons
310 if (AgentUI::StyledButton(ICON_MD_CHECK " Accept", theme.status_success,
311 ImVec2(0, 0))) {
312 confirm_action_ = "accept";
313 confirm_proposal_id_ = proposal.id;
315 }
316 ImGui::SameLine();
317 if (AgentUI::StyledButton(ICON_MD_CLOSE " Reject", theme.status_error,
318 ImVec2(0, 0))) {
319 confirm_action_ = "reject";
320 confirm_proposal_id_ = proposal.id;
322 }
323 }
324
325 ImGui::PopStyleColor();
326}
327
330 return;
331
332 const auto& theme = AgentUI::GetTheme();
333
334 ImGui::BeginChild("ProposalDetail", ImVec2(0, 0), true);
335
336 // Header
337 ImGui::TextColored(theme.proposal_accent, "%s %s", ICON_MD_PREVIEW,
338 selected_proposal_->id.c_str());
339 ImGui::SameLine();
340 ImVec4 status_color = GetStatusColor(selected_proposal_->status);
341 ImGui::TextColored(status_color, "(%s)",
343
344 // Description
345 if (!selected_proposal_->description.empty()) {
346 ImGui::TextWrapped("%s", selected_proposal_->description.c_str());
347 }
348
349 ImGui::Separator();
350
351 // Tabs for diff/log
352 if (ImGui::BeginTabBar("DetailTabs")) {
353 if (ImGui::BeginTabItem("Diff")) {
354 DrawDiffView();
355 ImGui::EndTabItem();
356 }
357 if (ImGui::BeginTabItem("Log")) {
358 DrawLogView();
359 ImGui::EndTabItem();
360 }
361 ImGui::EndTabBar();
362 }
363
364 ImGui::EndChild();
365}
366
368 const auto& theme = AgentUI::GetTheme();
369
370 if (diff_content_.empty() && selected_proposal_) {
371 diff_content_ = ReadFileContents(selected_proposal_->diff_path, 200);
372 }
373
374 if (diff_content_.empty()) {
375 ImGui::TextDisabled("No diff available");
376 return;
377 }
378
379 ImGui::PushStyleColor(ImGuiCol_ChildBg, theme.code_bg_color);
380 ImGui::BeginChild("DiffContent", ImVec2(0, 0), false,
381 ImGuiWindowFlags_HorizontalScrollbar);
382
383 // Simple diff rendering with color highlighting
384 std::istringstream stream(diff_content_);
385 std::string line;
386 while (std::getline(stream, line)) {
387 if (line.empty()) {
388 ImGui::NewLine();
389 continue;
390 }
391 if (line[0] == '+') {
392 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.8f, 0.4f, 1.0f));
393 } else if (line[0] == '-') {
394 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.8f, 0.4f, 0.4f, 1.0f));
395 } else if (line[0] == '@') {
396 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.6f, 0.8f, 1.0f));
397 } else {
398 ImGui::PushStyleColor(ImGuiCol_Text, theme.text_secondary_color);
399 }
400 ImGui::TextUnformatted(line.c_str());
401 ImGui::PopStyleColor();
402 }
403
404 ImGui::EndChild();
405 ImGui::PopStyleColor();
406}
407
409 const auto& theme = AgentUI::GetTheme();
410
411 if (log_content_.empty() && selected_proposal_) {
412 log_content_ = ReadFileContents(selected_proposal_->log_path, 100);
413 }
414
415 if (log_content_.empty()) {
416 ImGui::TextDisabled("No log available");
417 return;
418 }
419
420 ImGui::PushStyleColor(ImGuiCol_ChildBg, theme.code_bg_color);
421 ImGui::BeginChild("LogContent", ImVec2(0, 0), false,
422 ImGuiWindowFlags_HorizontalScrollbar);
423 ImGui::TextUnformatted(log_content_.c_str());
424 ImGui::EndChild();
425 ImGui::PopStyleColor();
426}
427
428void AgentProposalsPanel::FocusProposal(const std::string& proposal_id) {
429 SelectProposal(proposal_id);
430 if (context_) {
432 }
433}
434
436 needs_refresh_ = false;
438
439 // Update context state
440 if (context_) {
441 auto& proposal_state = context_->proposal_state();
442 proposal_state.total_proposals = static_cast<int>(proposals_.size());
443 proposal_state.pending_proposals = 0;
444 proposal_state.accepted_proposals = 0;
445 proposal_state.rejected_proposals = 0;
446
447 for (const auto& p : proposals_) {
448 switch (p.status) {
450 ++proposal_state.pending_proposals;
451 break;
453 ++proposal_state.accepted_proposals;
454 break;
456 ++proposal_state.rejected_proposals;
457 break;
458 }
459 }
460 }
461
462 // Clear selected if it no longer exists
463 if (!selected_proposal_id_.empty()) {
464 bool found = false;
465 for (const auto& p : proposals_) {
466 if (p.id == selected_proposal_id_) {
467 found = true;
469 break;
470 }
471 }
472 if (!found) {
473 selected_proposal_id_.clear();
474 selected_proposal_ = nullptr;
475 diff_content_.clear();
476 log_content_.clear();
477 }
478 }
479}
480
482 int count = 0;
483 for (const auto& p : proposals_) {
485 ++count;
486 }
487 }
488 return count;
489}
490
491void AgentProposalsPanel::SelectProposal(const std::string& proposal_id) {
492 if (proposal_id == selected_proposal_id_) {
493 return;
494 }
495
496 selected_proposal_id_ = proposal_id;
497 selected_proposal_ = nullptr;
498 diff_content_.clear();
499 log_content_.clear();
500
501 for (const auto& p : proposals_) {
502 if (p.id == proposal_id) {
504 break;
505 }
506 }
507}
508
522
525 const auto& theme = AgentUI::GetTheme();
526 switch (status) {
528 return theme.status_warning;
530 return theme.status_success;
532 return theme.status_error;
533 default:
534 return theme.text_secondary_color;
535 }
536}
537
539 const std::string& proposal_id) {
542
543 if (status.ok()) {
544 if (toast_manager_) {
545 toast_manager_->Show(absl::StrFormat("%s Proposal %s accepted",
546 ICON_MD_CHECK_CIRCLE, proposal_id),
547 ToastType::kSuccess, 3.0f);
548 }
549 needs_refresh_ = true;
550
551 // Notify via callback
554 }
555 } else if (toast_manager_) {
557 absl::StrFormat("Failed to accept proposal: %s", status.message()),
558 ToastType::kError, 5.0f);
559 }
560
561 return status;
562}
563
565 const std::string& proposal_id) {
568
569 if (status.ok()) {
570 if (toast_manager_) {
571 toast_manager_->Show(absl::StrFormat("%s Proposal %s rejected",
572 ICON_MD_CANCEL, proposal_id),
573 ToastType::kInfo, 3.0f);
574 }
575 needs_refresh_ = true;
576
577 // Notify via callback
580 }
581 } else if (toast_manager_) {
583 absl::StrFormat("Failed to reject proposal: %s", status.message()),
584 ToastType::kError, 5.0f);
585 }
586
587 return status;
588}
589
591 const std::string& proposal_id) {
592 auto status = cli::ProposalRegistry::Instance().RemoveProposal(proposal_id);
593
594 if (status.ok()) {
595 if (toast_manager_) {
596 toast_manager_->Show(absl::StrFormat("%s Proposal %s deleted",
597 ICON_MD_DELETE, proposal_id),
598 ToastType::kInfo, 3.0f);
599 }
600 needs_refresh_ = true;
601
602 if (selected_proposal_id_ == proposal_id) {
603 selected_proposal_id_.clear();
604 selected_proposal_ = nullptr;
605 diff_content_.clear();
606 log_content_.clear();
607 }
608 } else if (toast_manager_) {
610 absl::StrFormat("Failed to delete proposal: %s", status.message()),
611 ToastType::kError, 5.0f);
612 }
613
614 return status;
615}
616
617} // namespace editor
618} // 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)