yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
agent_chat.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <filesystem>
5#include <fstream>
6#include <iostream>
7#include <unordered_set>
8
9#include "absl/strings/ascii.h"
10#include "absl/strings/match.h"
11#include "absl/strings/str_format.h"
12#include "absl/strings/strip.h"
13#include "absl/time/clock.h"
14#include "absl/time/time.h"
19#include "app/gui/core/icons.h"
20#include "app/gui/core/style.h"
24#include "imgui/imgui.h"
25#include "imgui/misc/cpp/imgui_stdlib.h"
26#include "util/log.h"
27#include "util/platform_paths.h"
28
29#ifdef YAZE_WITH_JSON
30#include "nlohmann/json.hpp"
31#endif
32
33namespace yaze {
34namespace editor {
35
36namespace {
37
39 auto agent_dir = util::PlatformPaths::GetAppDataSubdirectory("agent");
40 if (agent_dir.ok()) {
41 return (*agent_dir / "agent_chat_history.json").string();
42 }
44 if (temp_dir.ok()) {
45 return (*temp_dir / "agent_chat_history.json").string();
46 }
47 return (std::filesystem::current_path() / "agent_chat_history.json").string();
48}
49
50std::optional<std::filesystem::path> ResolveAgentSessionsDir() {
51 auto agent_dir = util::PlatformPaths::GetAppDataSubdirectory("agent");
52 if (!agent_dir.ok()) {
53 return std::nullopt;
54 }
55 return *agent_dir / "sessions";
56}
57
58absl::Time FileTimeToAbsl(std::filesystem::file_time_type value) {
59 using FileClock = std::filesystem::file_time_type::clock;
60 auto now_file = FileClock::now();
61 auto now_sys = std::chrono::system_clock::now();
62 auto converted = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
63 value - now_file + now_sys);
64 return absl::FromChrono(converted);
65}
66
67std::string TrimTitle(const std::string& text) {
68 std::string trimmed = std::string(absl::StripAsciiWhitespace(text));
69 if (trimmed.empty()) {
70 return trimmed;
71 }
72 constexpr size_t kMaxLen = 64;
73 if (trimmed.size() > kMaxLen) {
74 trimmed = trimmed.substr(0, kMaxLen - 3);
75 trimmed.append("...");
76 }
77 return trimmed;
78}
79
80std::string BuildConversationTitle(const std::filesystem::path& path,
81 const std::vector<cli::agent::ChatMessage>& history) {
82 for (const auto& msg : history) {
84 !msg.message.empty()) {
85 std::string title = TrimTitle(msg.message);
86 if (!title.empty()) {
87 return title;
88 }
89 }
90 }
91 std::string fallback = path.stem().string();
92 if (!fallback.empty()) {
93 return fallback;
94 }
95 return "Untitled";
96}
97
98} // namespace
99
101 // Default initialization
102}
103
105 ProposalDrawer* proposal_drawer) {
106 toast_manager_ = toast_manager;
107 proposal_drawer_ = proposal_drawer;
108 if (active_history_path_.empty()) {
109 active_history_path_ = ResolveAgentChatHistoryPath();
110 }
111}
112
114 rom_ = rom;
115}
116
118 context_ = context;
119}
120
124
128
131 if (toast_manager_) {
132 toast_manager_->Show("Chat history cleared", ToastType::kInfo);
133 }
134}
135
136void AgentChat::SendMessage(const std::string& message) {
137 if (message.empty())
138 return;
139
141 thinking_animation_ = 0.0f;
143
144 // Send to service
145 auto status = agent_service_.SendMessage(message);
146 HandleAgentResponse(status);
147}
148
150 const absl::StatusOr<cli::agent::ChatMessage>& response) {
151 waiting_for_response_ = false;
152 if (!response.ok()) {
153 if (toast_manager_) {
155 "Agent Error: " + std::string(response.status().message()),
157 }
158 LOG_ERROR("AgentChat", "Agent Error: %s",
159 response.status().ToString().c_str());
160 } else {
162 }
163}
164
167 conversations_.clear();
168 return;
169 }
170 if (!force && last_conversation_refresh_ != absl::InfinitePast()) {
171 absl::Duration since = absl::Now() - last_conversation_refresh_;
172 if (since < absl::Seconds(5)) {
173 return;
174 }
175 }
176
177 if (active_history_path_.empty()) {
178 active_history_path_ = ResolveAgentChatHistoryPath();
179 }
180
181 std::vector<std::filesystem::path> candidates;
182 std::unordered_set<std::string> seen;
183
184 if (!active_history_path_.empty()) {
185 candidates.push_back(active_history_path_);
186 seen.insert(active_history_path_.string());
187 }
188
189 if (auto sessions_dir = ResolveAgentSessionsDir()) {
190 std::error_code ec;
191 if (std::filesystem::exists(*sessions_dir, ec)) {
192 for (const auto& entry :
193 std::filesystem::directory_iterator(*sessions_dir, ec)) {
194 if (ec) {
195 break;
196 }
197 if (!entry.is_regular_file(ec)) {
198 continue;
199 }
200 auto path = entry.path();
201 if (path.extension() != ".json") {
202 continue;
203 }
204 if (!absl::EndsWith(path.stem().string(), "_history")) {
205 continue;
206 }
207 const std::string key = path.string();
208 if (seen.insert(key).second) {
209 candidates.push_back(std::move(path));
210 }
211 }
212 }
213 }
214
215 conversations_.clear();
216 for (const auto& path : candidates) {
217 ConversationEntry entry;
218 entry.path = path;
219 entry.is_active = (!active_history_path_.empty() &&
220 path == active_history_path_);
221
222 auto snapshot_or = AgentChatHistoryCodec::Load(path);
223 if (snapshot_or.ok()) {
224 const auto& snapshot = snapshot_or.value();
225 entry.message_count = static_cast<int>(snapshot.history.size());
226 entry.title = BuildConversationTitle(path, snapshot.history);
227 if (!snapshot.history.empty()) {
228 entry.last_updated = snapshot.history.back().timestamp;
229 } else {
230 std::error_code ec;
231 if (std::filesystem::exists(path, ec)) {
232 entry.last_updated = FileTimeToAbsl(std::filesystem::last_write_time(path, ec));
233 }
234 }
235 if (entry.is_active && snapshot.history.empty()) {
236 entry.title = "Current Session";
237 }
238 } else {
239 entry.title = path.stem().string();
240 }
241
242 conversations_.push_back(std::move(entry));
243 }
244
245 std::sort(conversations_.begin(), conversations_.end(),
246 [](const ConversationEntry& a, const ConversationEntry& b) {
247 if (a.is_active != b.is_active) {
248 return a.is_active;
249 }
250 return a.last_updated > b.last_updated;
251 });
252
253 last_conversation_refresh_ = absl::Now();
254}
255
256void AgentChat::SelectConversation(const std::filesystem::path& path) {
257 if (path.empty()) {
258 return;
259 }
260 active_history_path_ = path;
261 auto status = LoadHistory(path.string());
262 if (!status.ok()) {
263 if (toast_manager_) {
264 toast_manager_->Show(std::string(status.message()), ToastType::kError,
265 3.0f);
266 }
267 } else {
268 ScrollToBottom();
269 }
270 RefreshConversationList(true);
271}
272
273void AgentChat::Draw(float available_height) {
274 if (!context_)
275 return;
276
277 RefreshConversationList(false);
278
279 // Chat container
280 gui::StyleVarGuard spacing_guard(ImGuiStyleVar_ItemSpacing, ImVec2(6, 6));
281
282 const float content_width = ImGui::GetContentRegionAvail().x;
283 const bool wide_layout = content_width >= 680.0f;
284
285 // 0. Toolbar at top
286 RenderToolbar(!wide_layout);
287 ImGui::Separator();
288
289 float content_height =
290 available_height > 0 ? available_height : ImGui::GetContentRegionAvail().y;
291
292 if (wide_layout) {
293 const float sidebar_width =
294 std::clamp(content_width * 0.28f, 220.0f, 320.0f);
295 if (ImGui::BeginChild("##ChatSidebar", ImVec2(sidebar_width, content_height),
296 true, ImGuiWindowFlags_NoScrollbar)) {
297 RenderConversationSidebar(content_height);
298 }
299 ImGui::EndChild();
300
301 ImGui::SameLine();
302 }
303
304 if (ImGui::BeginChild("##ChatMain", ImVec2(0, content_height), false,
305 ImGuiWindowFlags_NoScrollbar)) {
306 const float input_height =
307 ImGui::GetTextLineHeightWithSpacing() * 3.5f + 24.0f;
308 const float history_height =
309 std::max(140.0f, ImGui::GetContentRegionAvail().y - input_height);
310
311 if (ImGui::BeginChild("##ChatHistory", ImVec2(0, history_height), true,
312 ImGuiWindowFlags_NoScrollbar)) {
313 RenderHistory();
314 if (scroll_to_bottom_ ||
315 (auto_scroll_ && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())) {
316 ImGui::SetScrollHereY(1.0f);
317 scroll_to_bottom_ = false;
318 }
319 }
320 ImGui::EndChild();
321
322 RenderInputBox(input_height);
323 }
324 ImGui::EndChild();
325}
326
327void AgentChat::RenderToolbar(bool compact) {
328 const auto& theme = AgentUI::GetTheme();
329
330 {
331 gui::StyleColorGuard btn_guard(ImGuiCol_Button, theme.status_success);
332 if (ImGui::Button(ICON_MD_ADD_COMMENT " New Chat")) {
333 ClearHistory();
334 active_history_path_ = ResolveAgentChatHistoryPath();
335 RefreshConversationList(true);
336 }
337 }
338 ImGui::SameLine();
339
340 if (ImGui::Button(ICON_MD_DELETE_FOREVER " Clear")) {
341 ClearHistory();
342 }
343 ImGui::SameLine();
344
345 const bool history_available = AgentChatHistoryCodec::Available();
346 ImGui::BeginDisabled(!history_available);
347 if (ImGui::Button(ICON_MD_SAVE " Save")) {
348 const std::string filepath =
349 active_history_path_.empty() ? ResolveAgentChatHistoryPath()
350 : active_history_path_.string();
351 if (auto status = SaveHistory(filepath); !status.ok()) {
352 if (toast_manager_) {
353 toast_manager_->Show(
354 "Failed to save history: " + std::string(status.message()),
355 ToastType::kError);
356 }
357 } else if (toast_manager_) {
358 toast_manager_->Show("Chat history saved", ToastType::kSuccess);
359 }
360 RefreshConversationList(true);
361 }
362 ImGui::SameLine();
363
364 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Load")) {
365 const std::string filepath =
366 active_history_path_.empty() ? ResolveAgentChatHistoryPath()
367 : active_history_path_.string();
368 if (auto status = LoadHistory(filepath); !status.ok()) {
369 if (toast_manager_) {
370 toast_manager_->Show(
371 "Failed to load history: " + std::string(status.message()),
372 ToastType::kError);
373 }
374 } else if (toast_manager_) {
375 toast_manager_->Show("Chat history loaded", ToastType::kSuccess);
376 }
377 }
378 ImGui::EndDisabled();
379
380 if (compact && !conversations_.empty()) {
381 ImGui::SameLine();
382 ImGui::SetNextItemWidth(220.0f);
383 const char* current_label = "Current Session";
384 for (const auto& entry : conversations_) {
385 if (entry.is_active) {
386 current_label = entry.title.c_str();
387 break;
388 }
389 }
390 if (ImGui::BeginCombo("##conversation_combo", current_label)) {
391 for (const auto& entry : conversations_) {
392 bool selected = entry.is_active;
393 if (ImGui::Selectable(entry.title.c_str(), selected)) {
394 SelectConversation(entry.path);
395 }
396 if (selected) {
397 ImGui::SetItemDefaultFocus();
398 }
399 }
400 ImGui::EndCombo();
401 }
402 }
403
404 ImGui::SameLine();
405 if (ImGui::Button(ICON_MD_TUNE)) {
406 ImGui::OpenPopup("ChatOptions");
407 }
408 if (ImGui::BeginPopup("ChatOptions")) {
409 ImGui::Checkbox("Auto-scroll", &auto_scroll_);
410 ImGui::Checkbox("Timestamps", &show_timestamps_);
411 ImGui::Checkbox("Reasoning", &show_reasoning_);
412 ImGui::EndPopup();
413 }
414}
415
416void AgentChat::RenderConversationSidebar(float height) {
417 const auto& theme = AgentUI::GetTheme();
418
419 if (!AgentChatHistoryCodec::Available()) {
420 ImGui::TextDisabled("Chat history persistence unavailable.");
421 ImGui::TextDisabled("Build with JSON support to enable sessions.");
422 return;
423 }
424
425 if (context_) {
426 const auto& config = context_->agent_config();
427 ImGui::TextColored(theme.text_secondary_color, "Agent");
428 if (panel_opener_) {
429 if (ImGui::SmallButton(ICON_MD_SETTINGS " Config")) {
430 panel_opener_("agent.configuration");
431 }
432 ImGui::SameLine();
433 if (ImGui::SmallButton(ICON_MD_AUTO_FIX_HIGH " Builder")) {
434 panel_opener_("agent.builder");
435 }
436 }
437 ImGui::TextDisabled("Provider: %s",
438 config.ai_provider.empty() ? "mock"
439 : config.ai_provider.c_str());
440 ImGui::TextDisabled("Model: %s",
441 config.ai_model.empty() ? "not set"
442 : config.ai_model.c_str());
443 ImGui::Spacing();
444 ImGui::Separator();
445 ImGui::Spacing();
446 }
447
448 ImGui::TextColored(theme.text_secondary_color, "Conversations");
449 ImGui::SameLine();
450 if (ImGui::SmallButton(ICON_MD_REFRESH)) {
451 RefreshConversationList(true);
452 }
453 if (ImGui::IsItemHovered()) {
454 ImGui::SetTooltip("Refresh list");
455 }
456
457 ImGui::Spacing();
458 ImGui::InputTextWithHint("##conversation_filter", "Search...",
459 conversation_filter_, sizeof(conversation_filter_));
460 ImGui::Spacing();
461
462 const float list_height = std::max(0.0f, height - 80.0f);
463 if (ImGui::BeginChild("ConversationList", ImVec2(0, list_height), false,
464 ImGuiWindowFlags_NoScrollbar)) {
465 std::string filter = absl::AsciiStrToLower(conversation_filter_);
466 if (conversations_.empty()) {
467 ImGui::TextDisabled("No saved conversations yet.");
468 } else {
469 int index = 0;
470 for (const auto& entry : conversations_) {
471 std::string title_lower = absl::AsciiStrToLower(entry.title);
472 if (!filter.empty() &&
473 title_lower.find(filter) == std::string::npos) {
474 continue;
475 }
476
477 ImGui::PushID(index++);
478 gui::StyleColorGuard selectable_guard({
479 {ImGuiCol_Header,
480 entry.is_active ? theme.status_active : theme.panel_bg_darker},
481 {ImGuiCol_HeaderHovered, theme.panel_bg_color},
482 {ImGuiCol_HeaderActive, theme.status_active}});
483 if (ImGui::Selectable(entry.title.c_str(), entry.is_active,
484 ImGuiSelectableFlags_SpanAllColumns)) {
485 SelectConversation(entry.path);
486 }
487
488 ImGui::TextDisabled("%d msg%s", entry.message_count,
489 entry.message_count == 1 ? "" : "s");
490 if (entry.last_updated != absl::InfinitePast()) {
491 ImGui::SameLine();
492 ImGui::TextDisabled("%s",
493 absl::FormatTime("%b %d, %H:%M",
494 entry.last_updated,
495 absl::LocalTimeZone())
496 .c_str());
497 }
498 ImGui::Spacing();
499 ImGui::Separator();
500 ImGui::PopID();
501 }
502 }
503 }
504 ImGui::EndChild();
505}
506
507void AgentChat::RenderHistory() {
508 const auto& history = agent_service_.GetHistory();
509
510 if (history.empty()) {
511 ImGui::TextDisabled("Start a conversation with the agent...");
512 }
513
514 for (size_t i = 0; i < history.size(); ++i) {
515 RenderMessage(history[i], static_cast<int>(i));
516 if (message_spacing_ > 0) {
517 ImGui::Dummy(ImVec2(0, message_spacing_));
518 }
519 }
520
521 if (waiting_for_response_) {
522 RenderThinkingIndicator();
523 }
524}
525
526void AgentChat::RenderMessage(const cli::agent::ChatMessage& msg, int index) {
527 bool is_user = (msg.sender == cli::agent::ChatMessage::Sender::kUser);
528
529 ImGui::PushID(index);
530
531 // Styling
532 float wrap_width = ImGui::GetContentRegionAvail().x * 0.85f;
533 ImGui::SetCursorPosX(
534 is_user ? (ImGui::GetWindowContentRegionMax().x - wrap_width - 10) : 10);
535
536 ImGui::BeginGroup();
537
538 // Timestamp (if enabled)
539 if (show_timestamps_) {
540 std::string timestamp =
541 absl::FormatTime("%H:%M:%S", msg.timestamp, absl::LocalTimeZone());
542 ImGui::TextColored(gui::GetDisabledColor(), "[%s]",
543 timestamp.c_str());
544 ImGui::SameLine();
545 }
546
547 // Name/Icon
548 if (is_user) {
549 ImGui::TextColored(gui::GetInfoColor(), "%s You",
551 } else {
552 ImGui::TextColored(gui::GetSuccessColor(), "%s Agent",
554 }
555
556 // Message Bubble
557 const auto& theme = AgentUI::GetTheme();
558 ImVec4 bg_col = is_user ? theme.panel_bg_darker : theme.panel_bg_color;
559 {
560 gui::StyleColorGuard bg_guard(ImGuiCol_ChildBg, bg_col);
561 gui::StyleVarGuard rounding_guard(ImGuiStyleVar_ChildRounding, 8.0f);
562
563 std::string content_id = "msg_content_" + std::to_string(index);
564 if (ImGui::BeginChild(content_id.c_str(), ImVec2(wrap_width, 0), true,
565 ImGuiWindowFlags_AlwaysUseWindowPadding)) {
566 // Check if we have table data to render
567 if (!is_user && msg.table_data.has_value()) {
568 RenderTableData(msg.table_data.value());
569 } else if (!is_user && msg.json_pretty.has_value()) {
570 ImGui::TextWrapped("%s", msg.json_pretty.value().c_str());
571 } else {
572 // Parse message for code blocks
573 auto blocks = ParseMessageContent(msg.message);
574 for (const auto& block : blocks) {
575 if (block.type == ContentBlock::Type::kCode) {
576 RenderCodeBlock(block.content, block.language, index);
577 } else {
578 ImGui::TextWrapped("%s", block.content.c_str());
579 }
580 }
581 }
582
583 // Render proposals if any (detect from message or metadata)
584 if (!is_user) {
585 RenderProposalQuickActions(msg, index);
586 }
587
588 // Render tool execution timeline if metadata is available
589 if (!is_user) {
590 RenderToolTimeline(msg);
591 }
592 }
593 ImGui::EndChild();
594 }
595 ImGui::EndGroup();
596
597 ImGui::Spacing();
598 ImGui::PopID();
599}
600
601void AgentChat::RenderThinkingIndicator() {
602 ImGui::Spacing();
603 ImGui::Indent(10);
604 ImGui::TextDisabled("%s Agent is thinking...", ICON_MD_PENDING);
605
606 // Simple pulse animation
607 thinking_animation_ += ImGui::GetIO().DeltaTime;
608 int dots = (int)(thinking_animation_ * 3) % 4;
609 ImGui::SameLine();
610 if (dots == 0)
611 ImGui::Text(".");
612 else if (dots == 1)
613 ImGui::Text("..");
614 else if (dots == 2)
615 ImGui::Text("...");
616
617 ImGui::Unindent(10);
618}
619
620void AgentChat::RenderInputBox(float height) {
621 const auto& theme = AgentUI::GetTheme();
622 if (ImGui::BeginChild("ChatInput", ImVec2(0, height), false,
623 ImGuiWindowFlags_NoScrollbar)) {
624 ImGui::Separator();
625
626 // Input flags
627 ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue |
628 ImGuiInputTextFlags_CtrlEnterForNewLine;
629
630 float button_row_height = ImGui::GetFrameHeightWithSpacing();
631 float input_height =
632 std::max(48.0f,
633 ImGui::GetContentRegionAvail().y - button_row_height - 6.0f);
634
635 ImGui::PushItemWidth(-1);
636 if (ImGui::IsWindowAppearing()) {
637 ImGui::SetKeyboardFocusHere();
638 }
639
640 bool submit = ImGui::InputTextMultiline(
641 "##Input", input_buffer_, sizeof(input_buffer_),
642 ImVec2(0, input_height), flags);
643
644 bool clicked_send = false;
645 {
646 gui::StyleColorGuard send_guard(ImGuiCol_Button, theme.accent_color);
647 clicked_send = ImGui::Button(ICON_MD_SEND " Send", ImVec2(90, 0));
648 }
649 ImGui::SameLine();
650 if (ImGui::Button(ICON_MD_DELETE_FOREVER " Clear")) {
651 input_buffer_[0] = '\0';
652 }
653
654 if (submit || clicked_send) {
655 std::string msg(input_buffer_);
656 while (!msg.empty() &&
657 std::isspace(static_cast<unsigned char>(msg.back()))) {
658 msg.pop_back();
659 }
660
661 if (!msg.empty()) {
662 SendMessage(msg);
663 input_buffer_[0] = '\0';
664 ImGui::SetKeyboardFocusHere(-1);
665 }
666 }
667
668 ImGui::PopItemWidth();
669 }
670 ImGui::EndChild();
671}
672
673void AgentChat::RenderProposalQuickActions(const cli::agent::ChatMessage& msg,
674 int index) {
675 // Simple check for "Proposal:" keyword for now, or metadata if available
676 // In a real implementation, we'd parse the JSON proposal data
677 if (msg.message.find("Proposal:") != std::string::npos) {
678 ImGui::Separator();
679 if (ImGui::Button("View Proposal")) {
680 // Logic to open proposal drawer
681 if (proposal_drawer_) {
682 proposal_drawer_->Show();
683 }
684 }
685 }
686}
687
688void AgentChat::RenderCodeBlock(const std::string& code,
689 const std::string& language, int msg_index) {
690 const auto& theme = AgentUI::GetTheme();
691 gui::StyledChild code_child(
692 absl::StrCat("code_", msg_index).c_str(), ImVec2(0, 0),
693 {.bg = theme.code_bg_color}, true,
694 ImGuiWindowFlags_AlwaysAutoResize);
695 if (code_child) {
696 if (!language.empty()) {
697 ImGui::TextDisabled("%s", language.c_str());
698 ImGui::SameLine();
699 }
700 if (ImGui::Button(ICON_MD_CONTENT_COPY)) {
701 ImGui::SetClipboardText(code.c_str());
702 if (toast_manager_)
703 toast_manager_->Show("Code copied", ToastType::kSuccess);
704 }
705 ImGui::Separator();
706 ImGui::TextUnformatted(code.c_str());
707 }
708}
709
710void AgentChat::UpdateHarnessTelemetry(const AutomationTelemetry& telemetry) {
711 telemetry_history_.push_back(telemetry);
712 // Keep only the last 100 entries to avoid memory growth
713 if (telemetry_history_.size() > 100) {
714 telemetry_history_.erase(telemetry_history_.begin());
715 }
716}
717
718void AgentChat::SetLastPlanSummary(const std::string& summary) {
719 last_plan_summary_ = summary;
720}
721
722std::vector<AgentChat::ContentBlock> AgentChat::ParseMessageContent(
723 const std::string& content) {
724 std::vector<ContentBlock> blocks;
725
726 // Basic markdown code block parser
727 size_t pos = 0;
728 while (pos < content.length()) {
729 size_t code_start = content.find("```", pos);
730 if (code_start == std::string::npos) {
731 // Rest is text
732 blocks.push_back({ContentBlock::Type::kText, content.substr(pos), ""});
733 break;
734 }
735
736 // Add text before code
737 if (code_start > pos) {
738 blocks.push_back({ContentBlock::Type::kText,
739 content.substr(pos, code_start - pos), ""});
740 }
741
742 size_t code_end = content.find("```", code_start + 3);
743 if (code_end == std::string::npos) {
744 // Malformed, treat as text
745 blocks.push_back(
746 {ContentBlock::Type::kText, content.substr(code_start), ""});
747 break;
748 }
749
750 // Extract language
751 std::string language;
752 size_t newline = content.find('\n', code_start + 3);
753 size_t content_start = code_start + 3;
754 if (newline != std::string::npos && newline < code_end) {
755 language = content.substr(code_start + 3, newline - (code_start + 3));
756 content_start = newline + 1;
757 }
758
759 std::string code = content.substr(content_start, code_end - content_start);
760 blocks.push_back({ContentBlock::Type::kCode, code, language});
761
762 pos = code_end + 3;
763 }
764
765 return blocks;
766}
767
768void AgentChat::RenderTableData(
770 if (table.headers.empty()) {
771 return;
772 }
773
774 // Render table
775 if (ImGui::BeginTable("ToolResultTable",
776 static_cast<int>(table.headers.size()),
777 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
778 ImGuiTableFlags_ScrollY)) {
779 // Headers
780 for (const auto& header : table.headers) {
781 ImGui::TableSetupColumn(header.c_str());
782 }
783 ImGui::TableHeadersRow();
784
785 // Rows
786 for (const auto& row : table.rows) {
787 ImGui::TableNextRow();
788 for (size_t col = 0; col < std::min(row.size(), table.headers.size());
789 ++col) {
790 ImGui::TableSetColumnIndex(static_cast<int>(col));
791 ImGui::TextWrapped("%s", row[col].c_str());
792 }
793 }
794
795 ImGui::EndTable();
796 }
797}
798
799void AgentChat::RenderToolTimeline(const cli::agent::ChatMessage& msg) {
800 // Check if we have model metadata with tool information
801 if (!msg.model_metadata.has_value()) {
802 return;
803 }
804
805 const auto& meta = msg.model_metadata.value();
806
807 // Only render if tools were called
808 if (meta.tool_names.empty() && meta.tool_iterations == 0) {
809 return;
810 }
811
812 ImGui::Separator();
813 ImGui::Spacing();
814
815 // Tool timeline header - collapsible
816 const auto& timeline_theme = AgentUI::GetTheme();
817 gui::StyleColorGuard timeline_guard(
818 {{ImGuiCol_Header, timeline_theme.panel_bg_darker},
819 {ImGuiCol_HeaderHovered, timeline_theme.panel_bg_color}});
820
821 std::string header =
822 absl::StrFormat("%s Tools (%d calls, %.2fs)", ICON_MD_BUILD_CIRCLE,
823 meta.tool_iterations, meta.latency_seconds);
824
825 if (ImGui::TreeNode("##ToolTimeline", "%s", header.c_str())) {
826 // List tool names
827 if (!meta.tool_names.empty()) {
828 ImGui::TextDisabled("Tools called:");
829 for (const auto& tool : meta.tool_names) {
830 ImGui::BulletText("%s", tool.c_str());
831 }
832 }
833
834 // Provider/model info
835 ImGui::Spacing();
836 ImGui::TextDisabled("Provider: %s", meta.provider.c_str());
837 if (!meta.model.empty()) {
838 ImGui::TextDisabled("Model: %s", meta.model.c_str());
839 }
840
841 ImGui::TreePop();
842 }
843}
844
845absl::Status AgentChat::LoadHistory(const std::string& filepath) {
846#ifdef YAZE_WITH_JSON
847 auto snapshot_or = AgentChatHistoryCodec::Load(filepath);
848 if (!snapshot_or.ok()) {
849 return snapshot_or.status();
850 }
851 const auto& snapshot = snapshot_or.value();
852 agent_service_.ReplaceHistory(snapshot.history);
853 history_loaded_ = true;
854 scroll_to_bottom_ = true;
855 return absl::OkStatus();
856#else
857 return absl::UnimplementedError("JSON support not available");
858#endif
859}
860
861absl::Status AgentChat::SaveHistory(const std::string& filepath) {
862#ifdef YAZE_WITH_JSON
864 snapshot.history = agent_service_.GetHistory();
865
866 std::filesystem::path path(filepath);
867 std::error_code ec;
868 if (path.has_parent_path()) {
869 std::filesystem::create_directories(path.parent_path(), ec);
870 if (ec) {
871 return absl::InternalError(absl::StrFormat(
872 "Failed to create history directory: %s", ec.message()));
873 }
874 }
875
876 return AgentChatHistoryCodec::Save(path, snapshot);
877#else
878 return absl::UnimplementedError("JSON support not available");
879#endif
880}
881
882} // namespace editor
883} // 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
absl::StatusOr< ChatMessage > SendMessage(const std::string &message)
static absl::StatusOr< Snapshot > Load(const std::filesystem::path &path)
void SendMessage(const std::string &message)
void SetContext(AgentUIContext *context)
void RefreshConversationList(bool force=false)
void Initialize(ToastManager *toast_manager, ProposalDrawer *proposal_drawer)
ToastManager * toast_manager_
Definition agent_chat.h:114
AgentUIContext * context_
Definition agent_chat.h:113
std::filesystem::path active_history_path_
Definition agent_chat.h:136
void SetRomContext(Rom *rom)
std::vector< ConversationEntry > conversations_
Definition agent_chat.h:134
cli::agent::ConversationalAgentService agent_service_
Definition agent_chat.h:119
ProposalDrawer * proposal_drawer_
Definition agent_chat.h:115
absl::Time last_conversation_refresh_
Definition agent_chat.h:135
void HandleAgentResponse(const absl::StatusOr< cli::agent::ChatMessage > &response)
cli::agent::ConversationalAgentService * GetAgentService()
Unified context for agent UI components.
ImGui drawer for displaying and managing agent proposals.
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 style vars.
Definition style_guard.h:68
RAII guard for ImGui child windows with optional styling.
static absl::StatusOr< std::filesystem::path > GetTempDirectory()
Get a temporary directory for the application.
static absl::StatusOr< std::filesystem::path > GetAppDataSubdirectory(const std::string &subdir)
Get a subdirectory within the app data folder.
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_SETTINGS
Definition icons.h:1699
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_BUILD_CIRCLE
Definition icons.h:329
#define ICON_MD_AUTO_FIX_HIGH
Definition icons.h:218
#define ICON_MD_PENDING
Definition icons.h:1398
#define ICON_MD_SEND
Definition icons.h:1683
#define ICON_MD_PERSON
Definition icons.h:1415
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_DELETE_FOREVER
Definition icons.h:531
#define ICON_MD_ADD_COMMENT
Definition icons.h:97
#define ICON_MD_SMART_TOY
Definition icons.h:1781
#define LOG_ERROR(category, format,...)
Definition log.h:109
std::string BuildConversationTitle(const std::filesystem::path &path, const std::vector< cli::agent::ChatMessage > &history)
Definition agent_chat.cc:80
std::string TrimTitle(const std::string &text)
Definition agent_chat.cc:67
std::optional< std::filesystem::path > ResolveAgentSessionsDir()
Definition agent_chat.cc:50
absl::Time FileTimeToAbsl(std::filesystem::file_time_type value)
Definition agent_chat.cc:58
ImVec4 GetSuccessColor()
Definition ui_helpers.cc:48
ImVec4 GetDisabledColor()
Definition ui_helpers.cc:73
ImVec4 GetInfoColor()
Definition ui_helpers.cc:63
std::vector< std::vector< std::string > > rows
std::optional< ModelMetadata > model_metadata
std::optional< std::string > json_pretty
std::vector< cli::agent::ChatMessage > history