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"
18#include "imgui/imgui.h"
19#include "rom/rom.h"
20
21namespace yaze {
22namespace editor {
23
24namespace {
25
26std::string FormatRelativeTime(absl::Time timestamp) {
27 if (timestamp == absl::InfinitePast()) {
28 return "—";
29 }
30 absl::Duration delta = absl::Now() - timestamp;
31 if (delta < absl::Seconds(60)) {
32 return "just now";
33 }
34 if (delta < absl::Minutes(60)) {
35 return absl::StrFormat("%dm ago",
36 static_cast<int>(delta / absl::Minutes(1)));
37 }
38 if (delta < absl::Hours(24)) {
39 return absl::StrFormat("%dh ago", static_cast<int>(delta / absl::Hours(1)));
40 }
41 return absl::FormatTime("%b %d", timestamp, absl::LocalTimeZone());
42}
43
44std::string ReadFileContents(const std::filesystem::path& path,
45 int max_lines = 100) {
46 std::ifstream file(path);
47 if (!file.is_open()) {
48 return "";
49 }
50
51 std::ostringstream content;
52 std::string line;
53 int line_count = 0;
54 while (std::getline(file, line) && line_count < max_lines) {
55 content << line << "\n";
56 ++line_count;
57 }
58
59 if (line_count >= max_lines) {
60 content << "\n... (truncated)\n";
61 }
62
63 return content.str();
64}
65
66} // namespace
67
71
73 context_ = context;
74}
75
77 toast_manager_ = toast_manager;
78}
79
81 rom_ = rom;
82}
83
88
89void AgentProposalsPanel::Draw(float available_height) {
90 if (needs_refresh_) {
92 }
93
94 const auto& theme = AgentUI::GetTheme();
95
96 // Status filter
98
99 ImGui::Separator();
100
101 // Calculate heights
102 float detail_height = selected_proposal_ && !compact_mode_ ? 200.0f : 0.0f;
103 float list_height =
104 available_height > 0
105 ? available_height - ImGui::GetCursorPosY() - detail_height
106 : ImGui::GetContentRegionAvail().y - detail_height;
107
108 // Proposal list
109 {
110 gui::StyledChild proposal_list("ProposalList", ImVec2(0, list_height),
111 {.bg = theme.panel_bg_darker});
113 }
114
115 // Detail view (only in non-compact mode)
117 ImGui::Separator();
119 }
120
121 // Confirmation dialog
123 ImGui::OpenPopup("Confirm Action");
124 }
125
126 if (ImGui::BeginPopupModal("Confirm Action", nullptr,
127 ImGuiWindowFlags_AlwaysAutoResize)) {
128 ImGui::Text("Are you sure you want to %s proposal %s?",
129 confirm_action_.c_str(), confirm_proposal_id_.c_str());
130 ImGui::Separator();
131
132 if (ImGui::Button("Yes", ImVec2(80, 0))) {
133 if (confirm_action_ == "accept") {
135 } else if (confirm_action_ == "reject") {
137 } else if (confirm_action_ == "delete") {
139 }
140 show_confirm_dialog_ = false;
141 ImGui::CloseCurrentPopup();
142 }
143 ImGui::SameLine();
144 if (ImGui::Button("No", ImVec2(80, 0))) {
145 show_confirm_dialog_ = false;
146 ImGui::CloseCurrentPopup();
147 }
148 ImGui::EndPopup();
149 }
150}
151
153 if (!context_)
154 return;
155
156 auto& proposal_state = context_->proposal_state();
157 const char* filter_labels[] = {"All", "Pending", "Accepted", "Rejected"};
158 int current_filter = static_cast<int>(proposal_state.filter_mode);
159
160 if (compact_mode_) {
161 // Compact: just show pending count badge
162 ImGui::Text("%s Proposals", ICON_MD_RULE);
163 ImGui::SameLine();
164 int pending = GetPendingCount();
165 if (pending > 0) {
166 AgentUI::StatusBadge(absl::StrFormat("%d", pending).c_str(),
168 }
169 } else {
170 // Full: show filter buttons
171 for (int i = 0; i < 4; ++i) {
172 if (i > 0)
173 ImGui::SameLine();
174 bool selected = (current_filter == i);
175 std::optional<gui::StyleColorGuard> filter_guard;
176 if (selected) {
177 filter_guard.emplace(ImGuiCol_Button,
178 ImGui::GetStyle().Colors[ImGuiCol_ButtonActive]);
179 }
180 if (ImGui::SmallButton(filter_labels[i])) {
181 proposal_state.filter_mode = static_cast<ProposalState::FilterMode>(i);
182 needs_refresh_ = true;
183 }
184 }
185 }
186}
187
189 if (proposals_.empty()) {
190 ImGui::Spacing();
191 ImGui::TextDisabled("No proposals found");
192 return;
193 }
194
195 // Filter proposals based on current filter
197 if (context_) {
198 filter_mode = context_->proposal_state().filter_mode;
199 }
200
201 for (const auto& proposal : proposals_) {
202 // Apply filter
203 if (filter_mode != ProposalState::FilterMode::kAll) {
204 bool matches = false;
205 switch (filter_mode) {
207 matches = (proposal.status ==
209 break;
211 matches = (proposal.status ==
213 break;
215 matches = (proposal.status ==
217 break;
218 default:
219 matches = true;
220 }
221 if (!matches)
222 continue;
223 }
224
225 DrawProposalRow(proposal);
226 }
227}
228
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 gui::StyleColorGuard transparent_guard(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
288
289 if (compact_mode_) {
290 // Just accept/reject icons
291 {
292 gui::StyleColorGuard accept_guard(ImGuiCol_Text, theme.status_success);
293 if (ImGui::SmallButton(ICON_MD_CHECK)) {
294 confirm_action_ = "accept";
295 confirm_proposal_id_ = proposal.id;
297 }
298 }
299
300 ImGui::SameLine();
301
302 {
303 gui::StyleColorGuard reject_guard(ImGuiCol_Text, theme.status_error);
304 if (ImGui::SmallButton(ICON_MD_CLOSE)) {
305 confirm_action_ = "reject";
306 confirm_proposal_id_ = proposal.id;
308 }
309 }
310 } else {
311 // Full buttons
312 if (AgentUI::StyledButton(ICON_MD_CHECK " Accept", theme.status_success,
313 ImVec2(0, 0))) {
314 confirm_action_ = "accept";
315 confirm_proposal_id_ = proposal.id;
317 }
318 ImGui::SameLine();
319 if (AgentUI::StyledButton(ICON_MD_CLOSE " Reject", theme.status_error,
320 ImVec2(0, 0))) {
321 confirm_action_ = "reject";
322 confirm_proposal_id_ = proposal.id;
324 }
325 }
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 (gui::BeginThemedTabBar("DetailTabs")) {
353 if (ImGui::BeginTabItem("Diff")) {
354 DrawDiffView();
355 ImGui::EndTabItem();
356 }
357 if (ImGui::BeginTabItem("Log")) {
358 DrawLogView();
359 ImGui::EndTabItem();
360 }
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 gui::StyledChild diff_child("DiffContent", ImVec2(0, 0),
380 {.bg = theme.code_bg_color}, 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 ImVec4 line_color;
392 if (line[0] == '+') {
393 line_color = ImVec4(0.4f, 0.8f, 0.4f, 1.0f);
394 } else if (line[0] == '-') {
395 line_color = ImVec4(0.8f, 0.4f, 0.4f, 1.0f);
396 } else if (line[0] == '@') {
397 line_color = ImVec4(0.4f, 0.6f, 0.8f, 1.0f);
398 } else {
399 line_color = theme.text_secondary_color;
400 }
401 gui::ColoredText(line.c_str(), line_color);
402 }
403}
404
406 const auto& theme = AgentUI::GetTheme();
407
408 if (log_content_.empty() && selected_proposal_) {
409 log_content_ = ReadFileContents(selected_proposal_->log_path, 100);
410 }
411
412 if (log_content_.empty()) {
413 ImGui::TextDisabled("No log available");
414 return;
415 }
416
417 gui::StyledChild log_child("LogContent", ImVec2(0, 0),
418 {.bg = theme.code_bg_color}, false,
419 ImGuiWindowFlags_HorizontalScrollbar);
420 if (log_child) {
421 ImGui::TextUnformatted(log_content_.c_str());
422 }
423}
424
425void AgentProposalsPanel::FocusProposal(const std::string& proposal_id) {
426 SelectProposal(proposal_id);
427 if (context_) {
429 }
430}
431
433 needs_refresh_ = false;
435
436 // Update context state
437 if (context_) {
438 auto& proposal_state = context_->proposal_state();
439 proposal_state.total_proposals = static_cast<int>(proposals_.size());
440 proposal_state.pending_proposals = 0;
441 proposal_state.accepted_proposals = 0;
442 proposal_state.rejected_proposals = 0;
443
444 for (const auto& p : proposals_) {
445 switch (p.status) {
447 ++proposal_state.pending_proposals;
448 break;
450 ++proposal_state.accepted_proposals;
451 break;
453 ++proposal_state.rejected_proposals;
454 break;
455 }
456 }
457 }
458
459 // Clear selected if it no longer exists
460 if (!selected_proposal_id_.empty()) {
461 bool found = false;
462 for (const auto& p : proposals_) {
463 if (p.id == selected_proposal_id_) {
464 found = true;
466 break;
467 }
468 }
469 if (!found) {
470 selected_proposal_id_.clear();
471 selected_proposal_ = nullptr;
472 diff_content_.clear();
473 log_content_.clear();
474 }
475 }
476}
477
479 int count = 0;
480 for (const auto& p : proposals_) {
482 ++count;
483 }
484 }
485 return count;
486}
487
488void AgentProposalsPanel::SelectProposal(const std::string& proposal_id) {
489 if (proposal_id == selected_proposal_id_) {
490 return;
491 }
492
493 selected_proposal_id_ = proposal_id;
494 selected_proposal_ = nullptr;
495 diff_content_.clear();
496 log_content_.clear();
497
498 for (const auto& p : proposals_) {
499 if (p.id == proposal_id) {
501 break;
502 }
503 }
504}
505
519
522 const auto& theme = AgentUI::GetTheme();
523 switch (status) {
525 return theme.status_warning;
527 return theme.status_success;
529 return theme.status_error;
530 default:
531 return theme.text_secondary_color;
532 }
533}
534
536 const std::string& proposal_id) {
539
540 if (status.ok()) {
541 if (toast_manager_) {
542 toast_manager_->Show(absl::StrFormat("%s Proposal %s accepted",
543 ICON_MD_CHECK_CIRCLE, proposal_id),
544 ToastType::kSuccess, 3.0f);
545 }
546 needs_refresh_ = true;
547
548 // Notify via callback
551 }
552 } else if (toast_manager_) {
554 absl::StrFormat("Failed to accept proposal: %s", status.message()),
555 ToastType::kError, 5.0f);
556 }
557
558 return status;
559}
560
562 const std::string& proposal_id) {
565
566 if (status.ok()) {
567 if (toast_manager_) {
568 toast_manager_->Show(absl::StrFormat("%s Proposal %s rejected",
569 ICON_MD_CANCEL, proposal_id),
570 ToastType::kInfo, 3.0f);
571 }
572 needs_refresh_ = true;
573
574 // Notify via callback
577 }
578 } else if (toast_manager_) {
580 absl::StrFormat("Failed to reject proposal: %s", status.message()),
581 ToastType::kError, 5.0f);
582 }
583
584 return status;
585}
586
588 const std::string& proposal_id) {
589 auto status = cli::ProposalRegistry::Instance().RemoveProposal(proposal_id);
590
591 if (status.ok()) {
592 if (toast_manager_) {
593 toast_manager_->Show(absl::StrFormat("%s Proposal %s deleted",
594 ICON_MD_DELETE, proposal_id),
595 ToastType::kInfo, 3.0f);
596 }
597 needs_refresh_ = true;
598
599 if (selected_proposal_id_ == proposal_id) {
600 selected_proposal_id_.clear();
601 selected_proposal_ = nullptr;
602 diff_content_.clear();
603 log_content_.clear();
604 }
605 } else if (toast_manager_) {
607 absl::StrFormat("Failed to delete proposal: %s", status.message()),
608 ToastType::kError, 5.0f);
609 }
610
611 return status;
612}
613
614} // namespace editor
615} // 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:28
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)
RAII guard for ImGui style colors.
Definition style_guard.h:27
RAII guard for ImGui child windows with optional styling.
#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)
void ColoredText(const char *text, const ImVec4 &color)
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
void EndThemedTabBar()
Callbacks for proposal operations.
std::function< absl::Status(const std::string &) reject_proposal)
std::function< absl::Status(const std::string &) accept_proposal)