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 =
63 std::chrono::time_point_cast<std::chrono::system_clock::duration>(
64 value - now_file + now_sys);
65 return absl::FromChrono(converted);
66}
67
68std::string TrimTitle(const std::string& text) {
69 std::string trimmed = std::string(absl::StripAsciiWhitespace(text));
70 if (trimmed.empty()) {
71 return trimmed;
72 }
73 constexpr size_t kMaxLen = 64;
74 if (trimmed.size() > kMaxLen) {
75 trimmed = trimmed.substr(0, kMaxLen - 3);
76 trimmed.append("...");
77 }
78 return trimmed;
79}
80
82 const std::filesystem::path& path,
83 const std::vector<cli::agent::ChatMessage>& history) {
84 for (const auto& msg : history) {
86 !msg.message.empty()) {
87 std::string title = TrimTitle(msg.message);
88 if (!title.empty()) {
89 return title;
90 }
91 }
92 }
93 std::string fallback = path.stem().string();
94 if (!fallback.empty()) {
95 return fallback;
96 }
97 return "Untitled";
98}
99
100} // namespace
101
103 // Default initialization
104}
105
107 ProposalDrawer* proposal_drawer) {
108 toast_manager_ = toast_manager;
109 proposal_drawer_ = proposal_drawer;
110 if (active_history_path_.empty()) {
111 active_history_path_ = ResolveAgentChatHistoryPath();
112 }
113}
114
116 rom_ = rom;
117}
118
120 context_ = context;
121}
122
126
130
133 if (toast_manager_) {
134 toast_manager_->Show("Chat history cleared", ToastType::kInfo);
135 }
136}
137
138void AgentChat::SendMessage(const std::string& message) {
139 if (message.empty())
140 return;
141
143 thinking_animation_ = 0.0f;
145
146 // Send to service
147 auto status = agent_service_.SendMessage(message);
148 HandleAgentResponse(status);
149}
150
152 const absl::StatusOr<cli::agent::ChatMessage>& response) {
153 waiting_for_response_ = false;
154 if (!response.ok()) {
155 if (toast_manager_) {
157 "Agent Error: " + std::string(response.status().message()),
159 }
160 LOG_ERROR("AgentChat", "Agent Error: %s",
161 response.status().ToString().c_str());
162 } else {
164 }
165}
166
169 conversations_.clear();
170 return;
171 }
172 if (!force && last_conversation_refresh_ != absl::InfinitePast()) {
173 absl::Duration since = absl::Now() - last_conversation_refresh_;
174 if (since < absl::Seconds(5)) {
175 return;
176 }
177 }
178
179 if (active_history_path_.empty()) {
180 active_history_path_ = ResolveAgentChatHistoryPath();
181 }
182
183 std::vector<std::filesystem::path> candidates;
184 std::unordered_set<std::string> seen;
185
186 if (!active_history_path_.empty()) {
187 candidates.push_back(active_history_path_);
188 seen.insert(active_history_path_.string());
189 }
190
191 if (auto sessions_dir = ResolveAgentSessionsDir()) {
192 std::error_code ec;
193 if (std::filesystem::exists(*sessions_dir, ec)) {
194 for (const auto& entry :
195 std::filesystem::directory_iterator(*sessions_dir, ec)) {
196 if (ec) {
197 break;
198 }
199 if (!entry.is_regular_file(ec)) {
200 continue;
201 }
202 auto path = entry.path();
203 if (path.extension() != ".json") {
204 continue;
205 }
206 if (!absl::EndsWith(path.stem().string(), "_history")) {
207 continue;
208 }
209 const std::string key = path.string();
210 if (seen.insert(key).second) {
211 candidates.push_back(std::move(path));
212 }
213 }
214 }
215 }
216
217 conversations_.clear();
218 for (const auto& path : candidates) {
219 ConversationEntry entry;
220 entry.path = path;
221 entry.is_active =
222 (!active_history_path_.empty() && path == active_history_path_);
223
224 auto snapshot_or = AgentChatHistoryCodec::Load(path);
225 if (snapshot_or.ok()) {
226 const auto& snapshot = snapshot_or.value();
227 entry.message_count = static_cast<int>(snapshot.history.size());
228 entry.title = BuildConversationTitle(path, snapshot.history);
229 if (!snapshot.history.empty()) {
230 entry.last_updated = snapshot.history.back().timestamp;
231 } else {
232 std::error_code ec;
233 if (std::filesystem::exists(path, ec)) {
234 entry.last_updated =
235 FileTimeToAbsl(std::filesystem::last_write_time(path, ec));
236 }
237 }
238 if (entry.is_active && snapshot.history.empty()) {
239 entry.title = "Current Session";
240 }
241 } else {
242 entry.title = path.stem().string();
243 }
244
245 conversations_.push_back(std::move(entry));
246 }
247
248 std::sort(conversations_.begin(), conversations_.end(),
249 [](const ConversationEntry& a, const ConversationEntry& b) {
250 if (a.is_active != b.is_active) {
251 return a.is_active;
252 }
253 return a.last_updated > b.last_updated;
254 });
255
256 last_conversation_refresh_ = absl::Now();
257}
258
259void AgentChat::SelectConversation(const std::filesystem::path& path) {
260 if (path.empty()) {
261 return;
262 }
263 active_history_path_ = path;
264 auto status = LoadHistory(path.string());
265 if (!status.ok()) {
266 if (toast_manager_) {
267 toast_manager_->Show(std::string(status.message()), ToastType::kError,
268 3.0f);
269 }
270 } else {
271 ScrollToBottom();
272 }
273 RefreshConversationList(true);
274}
275
276void AgentChat::Draw(float available_height) {
277 if (!context_)
278 return;
279
280 RefreshConversationList(false);
281
282 // Chat container
283 gui::StyleVarGuard spacing_guard(ImGuiStyleVar_ItemSpacing, ImVec2(6, 6));
284
285 const float content_width = ImGui::GetContentRegionAvail().x;
286 const bool wide_layout = content_width >= 680.0f;
287
288 // 0. Toolbar at top
289 RenderToolbar(!wide_layout);
290 ImGui::Separator();
291
292 float content_height = available_height > 0
293 ? available_height
294 : ImGui::GetContentRegionAvail().y;
295
296 if (wide_layout) {
297 const float sidebar_width =
298 std::clamp(content_width * 0.28f, 220.0f, 320.0f);
299 if (ImGui::BeginChild("##ChatSidebar",
300 ImVec2(sidebar_width, content_height), true,
301 ImGuiWindowFlags_NoScrollbar)) {
302 RenderConversationSidebar(content_height);
303 }
304 ImGui::EndChild();
305
306 ImGui::SameLine();
307 }
308
309 if (ImGui::BeginChild("##ChatMain", ImVec2(0, content_height), false,
310 ImGuiWindowFlags_NoScrollbar)) {
311 const float input_height =
312 ImGui::GetTextLineHeightWithSpacing() * 3.5f + 24.0f;
313 const float history_height =
314 std::max(140.0f, ImGui::GetContentRegionAvail().y - input_height);
315
316 if (ImGui::BeginChild("##ChatHistory", ImVec2(0, history_height), true,
317 ImGuiWindowFlags_NoScrollbar)) {
318 RenderHistory();
319 if (scroll_to_bottom_ ||
320 (auto_scroll_ && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())) {
321 ImGui::SetScrollHereY(1.0f);
322 scroll_to_bottom_ = false;
323 }
324 }
325 ImGui::EndChild();
326
327 RenderInputBox(input_height);
328 }
329 ImGui::EndChild();
330}
331
332void AgentChat::RenderToolbar(bool compact) {
333 const auto& theme = AgentUI::GetTheme();
334
335 {
336 gui::StyleColorGuard btn_guard(ImGuiCol_Button, theme.status_success);
337 if (ImGui::Button(ICON_MD_ADD_COMMENT " New Chat")) {
338 ClearHistory();
339 active_history_path_ = ResolveAgentChatHistoryPath();
340 RefreshConversationList(true);
341 }
342 }
343 ImGui::SameLine();
344
345 if (ImGui::Button(ICON_MD_DELETE_FOREVER " Clear")) {
346 ClearHistory();
347 }
348 ImGui::SameLine();
349
350 const bool history_available = AgentChatHistoryCodec::Available();
351 ImGui::BeginDisabled(!history_available);
352 if (ImGui::Button(ICON_MD_SAVE " Save")) {
353 const std::string filepath = active_history_path_.empty()
354 ? ResolveAgentChatHistoryPath()
355 : active_history_path_.string();
356 if (auto status = SaveHistory(filepath); !status.ok()) {
357 if (toast_manager_) {
358 toast_manager_->Show(
359 "Failed to save history: " + std::string(status.message()),
360 ToastType::kError);
361 }
362 } else if (toast_manager_) {
363 toast_manager_->Show("Chat history saved", ToastType::kSuccess);
364 }
365 RefreshConversationList(true);
366 }
367 ImGui::SameLine();
368
369 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Load")) {
370 const std::string filepath = active_history_path_.empty()
371 ? ResolveAgentChatHistoryPath()
372 : active_history_path_.string();
373 if (auto status = LoadHistory(filepath); !status.ok()) {
374 if (toast_manager_) {
375 toast_manager_->Show(
376 "Failed to load history: " + std::string(status.message()),
377 ToastType::kError);
378 }
379 } else if (toast_manager_) {
380 toast_manager_->Show("Chat history loaded", ToastType::kSuccess);
381 }
382 }
383 ImGui::EndDisabled();
384
385 if (compact && !conversations_.empty()) {
386 ImGui::SameLine();
387 ImGui::SetNextItemWidth(220.0f);
388 const char* current_label = "Current Session";
389 for (const auto& entry : conversations_) {
390 if (entry.is_active) {
391 current_label = entry.title.c_str();
392 break;
393 }
394 }
395 if (ImGui::BeginCombo("##conversation_combo", current_label)) {
396 for (const auto& entry : conversations_) {
397 bool selected = entry.is_active;
398 if (ImGui::Selectable(entry.title.c_str(), selected)) {
399 SelectConversation(entry.path);
400 }
401 if (selected) {
402 ImGui::SetItemDefaultFocus();
403 }
404 }
405 ImGui::EndCombo();
406 }
407 }
408
409 ImGui::SameLine();
410 if (ImGui::Button(ICON_MD_TUNE)) {
411 ImGui::OpenPopup("ChatOptions");
412 }
413 if (ImGui::BeginPopup("ChatOptions")) {
414 ImGui::Checkbox("Auto-scroll", &auto_scroll_);
415 ImGui::Checkbox("Timestamps", &show_timestamps_);
416 ImGui::Checkbox("Reasoning", &show_reasoning_);
417 ImGui::EndPopup();
418 }
419}
420
421void AgentChat::RenderConversationSidebar(float height) {
422 const auto& theme = AgentUI::GetTheme();
423
424 if (!AgentChatHistoryCodec::Available()) {
425 ImGui::TextDisabled("Chat history persistence unavailable.");
426 ImGui::TextDisabled("Build with JSON support to enable sessions.");
427 return;
428 }
429
430 if (context_) {
431 const auto& config = context_->agent_config();
432 ImGui::TextColored(theme.text_secondary_color, "Agent");
433 if (panel_opener_) {
434 if (ImGui::SmallButton(ICON_MD_SETTINGS " Config")) {
435 panel_opener_("agent.configuration");
436 }
437 ImGui::SameLine();
438 if (ImGui::SmallButton(ICON_MD_AUTO_FIX_HIGH " Builder")) {
439 panel_opener_("agent.builder");
440 }
441 }
442 ImGui::TextDisabled("Provider: %s", config.ai_provider.empty()
443 ? "mock"
444 : config.ai_provider.c_str());
445 ImGui::TextDisabled("Model: %s", config.ai_model.empty()
446 ? "not set"
447 : config.ai_model.c_str());
448 ImGui::Spacing();
449 ImGui::Separator();
450 ImGui::Spacing();
451 }
452
453 ImGui::TextColored(theme.text_secondary_color, "Conversations");
454 ImGui::SameLine();
455 if (ImGui::SmallButton(ICON_MD_REFRESH)) {
456 RefreshConversationList(true);
457 }
458 if (ImGui::IsItemHovered()) {
459 ImGui::SetTooltip("Refresh list");
460 }
461
462 ImGui::Spacing();
463 ImGui::InputTextWithHint("##conversation_filter", "Search...",
464 conversation_filter_, sizeof(conversation_filter_));
465 ImGui::Spacing();
466
467 const float list_height = std::max(0.0f, height - 80.0f);
468 if (ImGui::BeginChild("ConversationList", ImVec2(0, list_height), false,
469 ImGuiWindowFlags_NoScrollbar)) {
470 std::string filter = absl::AsciiStrToLower(conversation_filter_);
471 if (conversations_.empty()) {
472 ImGui::TextDisabled("No saved conversations yet.");
473 } else {
474 int index = 0;
475 for (const auto& entry : conversations_) {
476 std::string title_lower = absl::AsciiStrToLower(entry.title);
477 if (!filter.empty() && title_lower.find(filter) == std::string::npos) {
478 continue;
479 }
480
481 ImGui::PushID(index++);
482 gui::StyleColorGuard selectable_guard(
483 {{ImGuiCol_Header,
484 entry.is_active ? theme.status_active : theme.panel_bg_darker},
485 {ImGuiCol_HeaderHovered, theme.panel_bg_color},
486 {ImGuiCol_HeaderActive, theme.status_active}});
487 if (ImGui::Selectable(entry.title.c_str(), entry.is_active,
488 ImGuiSelectableFlags_SpanAllColumns)) {
489 SelectConversation(entry.path);
490 }
491
492 ImGui::TextDisabled("%d msg%s", entry.message_count,
493 entry.message_count == 1 ? "" : "s");
494 if (entry.last_updated != absl::InfinitePast()) {
495 ImGui::SameLine();
496 ImGui::TextDisabled(
497 "%s", absl::FormatTime("%b %d, %H:%M", entry.last_updated,
498 absl::LocalTimeZone())
499 .c_str());
500 }
501 ImGui::Spacing();
502 ImGui::Separator();
503 ImGui::PopID();
504 }
505 }
506 }
507 ImGui::EndChild();
508}
509
510void AgentChat::RenderHistory() {
511 const auto& history = agent_service_.GetHistory();
512
513 if (history.empty()) {
514 ImGui::TextDisabled("Start a conversation with the agent...");
515 }
516
517 for (size_t i = 0; i < history.size(); ++i) {
518 RenderMessage(history[i], static_cast<int>(i));
519 if (message_spacing_ > 0) {
520 ImGui::Dummy(ImVec2(0, message_spacing_));
521 }
522 }
523
524 if (waiting_for_response_) {
525 RenderThinkingIndicator();
526 }
527}
528
529void AgentChat::RenderMessage(const cli::agent::ChatMessage& msg, int index) {
530 bool is_user = (msg.sender == cli::agent::ChatMessage::Sender::kUser);
531
532 ImGui::PushID(index);
533
534 // Styling
535 float wrap_width = ImGui::GetContentRegionAvail().x * 0.85f;
536 ImGui::SetCursorPosX(
537 is_user ? (ImGui::GetWindowContentRegionMax().x - wrap_width - 10) : 10);
538
539 ImGui::BeginGroup();
540
541 // Timestamp (if enabled)
542 if (show_timestamps_) {
543 std::string timestamp =
544 absl::FormatTime("%H:%M:%S", msg.timestamp, absl::LocalTimeZone());
545 ImGui::TextColored(gui::GetDisabledColor(), "[%s]", timestamp.c_str());
546 ImGui::SameLine();
547 }
548
549 // Name/Icon
550 if (is_user) {
551 ImGui::TextColored(gui::GetInfoColor(), "%s You", ICON_MD_PERSON);
552 } else {
553 ImGui::TextColored(gui::GetSuccessColor(), "%s Agent", ICON_MD_SMART_TOY);
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(
565 content_id.c_str(), ImVec2(wrap_width, 0),
566 ImGuiChildFlags_Borders | ImGuiChildFlags_AlwaysUseWindowPadding,
567 0)) {
568 // Check if we have table data to render
569 if (!is_user && msg.table_data.has_value()) {
570 RenderTableData(msg.table_data.value());
571 } else if (!is_user && msg.json_pretty.has_value()) {
572 ImGui::TextWrapped("%s", msg.json_pretty.value().c_str());
573 } else {
574 // Parse message for code blocks
575 auto blocks = ParseMessageContent(msg.message);
576 for (const auto& block : blocks) {
577 if (block.type == ContentBlock::Type::kCode) {
578 RenderCodeBlock(block.content, block.language, index);
579 } else {
580 ImGui::TextWrapped("%s", block.content.c_str());
581 }
582 }
583 }
584
585 // Render proposals if any (detect from message or metadata)
586 if (!is_user) {
587 RenderProposalQuickActions(msg, index);
588 }
589
590 // Render tool execution timeline if metadata is available
591 if (!is_user) {
592 RenderToolTimeline(msg);
593 }
594 }
595 ImGui::EndChild();
596 }
597 ImGui::EndGroup();
598
599 ImGui::Spacing();
600 ImGui::PopID();
601}
602
603void AgentChat::RenderThinkingIndicator() {
604 ImGui::Spacing();
605 ImGui::Indent(10);
606 ImGui::TextDisabled("%s Agent is thinking...", ICON_MD_PENDING);
607
608 // Simple pulse animation
609 thinking_animation_ += ImGui::GetIO().DeltaTime;
610 int dots = (int)(thinking_animation_ * 3) % 4;
611 ImGui::SameLine();
612 if (dots == 0)
613 ImGui::Text(".");
614 else if (dots == 1)
615 ImGui::Text("..");
616 else if (dots == 2)
617 ImGui::Text("...");
618
619 ImGui::Unindent(10);
620}
621
622void AgentChat::RenderInputBox(float height) {
623 const auto& theme = AgentUI::GetTheme();
624 if (ImGui::BeginChild("ChatInput", ImVec2(0, height), false,
625 ImGuiWindowFlags_NoScrollbar)) {
626 ImGui::Separator();
627
628 // Input flags
629 ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue |
630 ImGuiInputTextFlags_CtrlEnterForNewLine;
631
632 float button_row_height = ImGui::GetFrameHeightWithSpacing();
633 float input_height = std::max(
634 48.0f, ImGui::GetContentRegionAvail().y - button_row_height - 6.0f);
635
636 ImGui::PushItemWidth(-1);
637 if (ImGui::IsWindowAppearing()) {
638 ImGui::SetKeyboardFocusHere();
639 }
640
641 bool submit = ImGui::InputTextMultiline("##Input", input_buffer_,
642 sizeof(input_buffer_),
643 ImVec2(0, input_height), flags);
644
645 bool clicked_send = false;
646 {
647 gui::StyleColorGuard send_guard(ImGuiCol_Button, theme.accent_color);
648 clicked_send = ImGui::Button(ICON_MD_SEND " Send", ImVec2(90, 0));
649 }
650 ImGui::SameLine();
651 if (ImGui::Button(ICON_MD_DELETE_FOREVER " Clear")) {
652 input_buffer_[0] = '\0';
653 }
654
655 if (submit || clicked_send) {
656 std::string msg(input_buffer_);
657 while (!msg.empty() &&
658 std::isspace(static_cast<unsigned char>(msg.back()))) {
659 msg.pop_back();
660 }
661
662 if (!msg.empty()) {
663 SendMessage(msg);
664 input_buffer_[0] = '\0';
665 ImGui::SetKeyboardFocusHere(-1);
666 }
667 }
668
669 ImGui::PopItemWidth();
670 }
671 ImGui::EndChild();
672}
673
674void AgentChat::RenderProposalQuickActions(const cli::agent::ChatMessage& msg,
675 int index) {
676 // Simple check for "Proposal:" keyword for now, or metadata if available
677 // In a real implementation, we'd parse the JSON proposal data
678 if (msg.message.find("Proposal:") != std::string::npos) {
679 ImGui::Separator();
680 if (ImGui::Button("View Proposal")) {
681 // Logic to open proposal drawer
682 if (proposal_drawer_) {
683 proposal_drawer_->Show();
684 }
685 }
686 }
687}
688
689void AgentChat::RenderCodeBlock(const std::string& code,
690 const std::string& language, int msg_index) {
691 const auto& theme = AgentUI::GetTheme();
692 gui::StyledChild code_child(absl::StrCat("code_", msg_index).c_str(),
693 ImVec2(0, 0), {.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:81
std::string TrimTitle(const std::string &text)
Definition agent_chat.cc:68
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