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
8#include "absl/strings/str_format.h"
9#include "absl/time/time.h"
12#include "app/gui/core/icons.h"
13#include "app/gui/core/style.h"
15#include "imgui/imgui.h"
16#include "imgui/misc/cpp/imgui_stdlib.h"
17#include "util/log.h"
18#include "util/platform_paths.h"
19
20#ifdef YAZE_WITH_JSON
21#include "nlohmann/json.hpp"
22#endif
23
24namespace yaze {
25namespace editor {
26
27namespace {
28
30 auto agent_dir = util::PlatformPaths::GetAppDataSubdirectory("agent");
31 if (agent_dir.ok()) {
32 return (*agent_dir / "agent_chat_history.json").string();
33 }
35 if (docs_dir.ok()) {
36 return (*docs_dir / "agent_chat_history.json").string();
37 }
39 if (temp_dir.ok()) {
40 return (*temp_dir / "agent_chat_history.json").string();
41 }
42 return (std::filesystem::current_path() / "agent_chat_history.json").string();
43}
44
45} // namespace
46
48 // Default initialization
49}
50
52 ProposalDrawer* proposal_drawer) {
53 toast_manager_ = toast_manager;
54 proposal_drawer_ = proposal_drawer;
55}
56
58 rom_ = rom;
59}
60
62 context_ = context;
63}
64
68
72
75 if (toast_manager_) {
76 toast_manager_->Show("Chat history cleared", ToastType::kInfo);
77 }
78}
79
80void AgentChat::SendMessage(const std::string& message) {
81 if (message.empty())
82 return;
83
87
88 // Send to service
89 auto status = agent_service_.SendMessage(message);
90 HandleAgentResponse(status);
91}
92
94 const absl::StatusOr<cli::agent::ChatMessage>& response) {
96 if (!response.ok()) {
97 if (toast_manager_) {
99 "Agent Error: " + std::string(response.status().message()),
101 }
102 LOG_ERROR("AgentChat", "Agent Error: %s",
103 response.status().ToString().c_str());
104 } else {
106 }
107}
108
109void AgentChat::Draw(float available_height) {
110 if (!context_)
111 return;
112
113 // Chat container
114 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 4));
115
116 // 0. Toolbar at top
118 ImGui::Separator();
119
120 // 1. History Area (Available space - Input height - Toolbar height)
121 float input_height = ImGui::GetTextLineHeightWithSpacing() * 4 + 20.0f;
122 float toolbar_height = ImGui::GetFrameHeightWithSpacing() + 8.0f;
123 float history_height =
124 available_height > 0 ? (available_height - input_height - toolbar_height)
125 : -input_height - toolbar_height;
126
127 if (ImGui::BeginChild("##ChatHistory", ImVec2(0, history_height), true)) {
129 // Handle auto-scroll
130 if (scroll_to_bottom_ ||
131 (auto_scroll_ && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())) {
132 ImGui::SetScrollHereY(1.0f);
133 scroll_to_bottom_ = false;
134 }
135 }
136 ImGui::EndChild();
137
138 // 2. Input Area
140
141 ImGui::PopStyleVar();
142}
143
145 if (ImGui::Button(ICON_MD_DELETE_FOREVER " Clear")) {
146 ClearHistory();
147 }
148 ImGui::SameLine();
149
150 if (ImGui::Button(ICON_MD_SAVE " Save")) {
151 std::string filepath = ResolveAgentChatHistoryPath();
152 if (auto status = SaveHistory(filepath); !status.ok()) {
153 if (toast_manager_) {
155 "Failed to save history: " + std::string(status.message()),
157 }
158 } else {
159 if (toast_manager_) {
160 toast_manager_->Show("Chat history saved", ToastType::kSuccess);
161 }
162 }
163 }
164 ImGui::SameLine();
165
166 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Load")) {
167 std::string filepath = ResolveAgentChatHistoryPath();
168 if (auto status = LoadHistory(filepath); !status.ok()) {
169 if (toast_manager_) {
171 "Failed to load history: " + std::string(status.message()),
173 }
174 } else {
175 if (toast_manager_) {
176 toast_manager_->Show("Chat history loaded", ToastType::kSuccess);
177 }
178 }
179 }
180
181 ImGui::SameLine();
182 ImGui::Checkbox("Auto-scroll", &auto_scroll_);
183
184 ImGui::SameLine();
185 ImGui::Checkbox("Timestamps", &show_timestamps_);
186
187 ImGui::SameLine();
188 ImGui::Checkbox("Reasoning", &show_reasoning_);
189}
190
192 const auto& history = agent_service_.GetHistory();
193
194 if (history.empty()) {
195 ImGui::TextDisabled("Start a conversation with the agent...");
196 }
197
198 for (size_t i = 0; i < history.size(); ++i) {
199 RenderMessage(history[i], static_cast<int>(i));
200 if (message_spacing_ > 0) {
201 ImGui::Dummy(ImVec2(0, message_spacing_));
202 }
203 }
204
207 }
208}
209
211 bool is_user = (msg.sender == cli::agent::ChatMessage::Sender::kUser);
212
213 ImGui::PushID(index);
214
215 // Styling
216 float wrap_width = ImGui::GetContentRegionAvail().x * 0.85f;
217 ImGui::SetCursorPosX(
218 is_user ? (ImGui::GetWindowContentRegionMax().x - wrap_width - 10) : 10);
219
220 ImGui::BeginGroup();
221
222 // Timestamp (if enabled)
223 if (show_timestamps_) {
224 std::string timestamp =
225 absl::FormatTime("%H:%M:%S", msg.timestamp, absl::LocalTimeZone());
226 ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "[%s]",
227 timestamp.c_str());
228 ImGui::SameLine();
229 }
230
231 // Name/Icon
232 if (is_user) {
233 ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "%s You",
235 } else {
236 ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.6f, 1.0f), "%s Agent",
238 }
239
240 // Message Bubble
241 ImVec4 bg_col = is_user ? ImVec4(0.2f, 0.2f, 0.25f, 1.0f)
242 : ImVec4(0.25f, 0.25f, 0.25f, 1.0f);
243 ImGui::PushStyleColor(ImGuiCol_ChildBg, bg_col);
244 ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 8.0f);
245
246 std::string content_id = "msg_content_" + std::to_string(index);
247 if (ImGui::BeginChild(content_id.c_str(), ImVec2(wrap_width, 0), true,
248 ImGuiWindowFlags_AlwaysUseWindowPadding)) {
249 // Check if we have table data to render
250 if (!is_user && msg.table_data.has_value()) {
251 RenderTableData(msg.table_data.value());
252 } else if (!is_user && msg.json_pretty.has_value()) {
253 ImGui::TextWrapped("%s", msg.json_pretty.value().c_str());
254 } else {
255 // Parse message for code blocks
256 auto blocks = ParseMessageContent(msg.message);
257 for (const auto& block : blocks) {
258 if (block.type == ContentBlock::Type::kCode) {
259 RenderCodeBlock(block.content, block.language, index);
260 } else {
261 ImGui::TextWrapped("%s", block.content.c_str());
262 }
263 }
264 }
265
266 // Render proposals if any (detect from message or metadata)
267 if (!is_user) {
268 RenderProposalQuickActions(msg, index);
269 }
270
271 // Render tool execution timeline if metadata is available
272 if (!is_user) {
274 }
275 }
276 ImGui::EndChild();
277
278 ImGui::PopStyleVar();
279 ImGui::PopStyleColor();
280 ImGui::EndGroup();
281
282 ImGui::Spacing();
283 ImGui::PopID();
284}
285
287 ImGui::Spacing();
288 ImGui::Indent(10);
289 ImGui::TextDisabled("%s Agent is thinking...", ICON_MD_PENDING);
290
291 // Simple pulse animation
292 thinking_animation_ += ImGui::GetIO().DeltaTime;
293 int dots = (int)(thinking_animation_ * 3) % 4;
294 ImGui::SameLine();
295 if (dots == 0)
296 ImGui::Text(".");
297 else if (dots == 1)
298 ImGui::Text("..");
299 else if (dots == 2)
300 ImGui::Text("...");
301
302 ImGui::Unindent(10);
303}
304
306 ImGui::Separator();
307
308 // Input flags
309 ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue |
310 ImGuiInputTextFlags_CtrlEnterForNewLine;
311
312 ImGui::PushItemWidth(-1);
313 if (ImGui::IsWindowAppearing()) {
314 ImGui::SetKeyboardFocusHere();
315 }
316
317 bool submit = ImGui::InputTextMultiline(
318 "##Input", input_buffer_, sizeof(input_buffer_), ImVec2(0, 0), flags);
319
320 if (submit) {
321 std::string msg(input_buffer_);
322 // Trim whitespace
323 while (!msg.empty() && std::isspace(msg.back()))
324 msg.pop_back();
325
326 if (!msg.empty()) {
327 SendMessage(msg);
328 input_buffer_[0] = '\0';
329 ImGui::SetKeyboardFocusHere(-1); // Refocus
330 }
331 }
332
333 ImGui::PopItemWidth();
334}
335
337 int index) {
338 // Simple check for "Proposal:" keyword for now, or metadata if available
339 // In a real implementation, we'd parse the JSON proposal data
340 if (msg.message.find("Proposal:") != std::string::npos) {
341 ImGui::Separator();
342 if (ImGui::Button("View Proposal")) {
343 // Logic to open proposal drawer
344 if (proposal_drawer_) {
346 }
347 }
348 }
349}
350
351void AgentChat::RenderCodeBlock(const std::string& code,
352 const std::string& language, int msg_index) {
353 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.1f, 0.1f, 0.1f, 1.0f));
354 if (ImGui::BeginChild(absl::StrCat("code_", msg_index).c_str(), ImVec2(0, 0),
355 true, ImGuiWindowFlags_AlwaysAutoResize)) {
356 if (!language.empty()) {
357 ImGui::TextDisabled("%s", language.c_str());
358 ImGui::SameLine();
359 }
360 if (ImGui::Button(ICON_MD_CONTENT_COPY)) {
361 ImGui::SetClipboardText(code.c_str());
362 if (toast_manager_)
363 toast_manager_->Show("Code copied", ToastType::kSuccess);
364 }
365 ImGui::Separator();
366 ImGui::TextUnformatted(code.c_str());
367 }
368 ImGui::EndChild();
369 ImGui::PopStyleColor();
370}
371
373 telemetry_history_.push_back(telemetry);
374 // Keep only the last 100 entries to avoid memory growth
375 if (telemetry_history_.size() > 100) {
377 }
378}
379
380void AgentChat::SetLastPlanSummary(const std::string& summary) {
381 last_plan_summary_ = summary;
382}
383
384std::vector<AgentChat::ContentBlock> AgentChat::ParseMessageContent(
385 const std::string& content) {
386 std::vector<ContentBlock> blocks;
387
388 // Basic markdown code block parser
389 size_t pos = 0;
390 while (pos < content.length()) {
391 size_t code_start = content.find("```", pos);
392 if (code_start == std::string::npos) {
393 // Rest is text
394 blocks.push_back({ContentBlock::Type::kText, content.substr(pos), ""});
395 break;
396 }
397
398 // Add text before code
399 if (code_start > pos) {
400 blocks.push_back({ContentBlock::Type::kText,
401 content.substr(pos, code_start - pos), ""});
402 }
403
404 size_t code_end = content.find("```", code_start + 3);
405 if (code_end == std::string::npos) {
406 // Malformed, treat as text
407 blocks.push_back(
408 {ContentBlock::Type::kText, content.substr(code_start), ""});
409 break;
410 }
411
412 // Extract language
413 std::string language;
414 size_t newline = content.find('\n', code_start + 3);
415 size_t content_start = code_start + 3;
416 if (newline != std::string::npos && newline < code_end) {
417 language = content.substr(code_start + 3, newline - (code_start + 3));
418 content_start = newline + 1;
419 }
420
421 std::string code = content.substr(content_start, code_end - content_start);
422 blocks.push_back({ContentBlock::Type::kCode, code, language});
423
424 pos = code_end + 3;
425 }
426
427 return blocks;
428}
429
432 if (table.headers.empty()) {
433 return;
434 }
435
436 // Render table
437 if (ImGui::BeginTable("ToolResultTable",
438 static_cast<int>(table.headers.size()),
439 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
440 ImGuiTableFlags_ScrollY)) {
441 // Headers
442 for (const auto& header : table.headers) {
443 ImGui::TableSetupColumn(header.c_str());
444 }
445 ImGui::TableHeadersRow();
446
447 // Rows
448 for (const auto& row : table.rows) {
449 ImGui::TableNextRow();
450 for (size_t col = 0; col < std::min(row.size(), table.headers.size());
451 ++col) {
452 ImGui::TableSetColumnIndex(static_cast<int>(col));
453 ImGui::TextWrapped("%s", row[col].c_str());
454 }
455 }
456
457 ImGui::EndTable();
458 }
459}
460
462 // Check if we have model metadata with tool information
463 if (!msg.model_metadata.has_value()) {
464 return;
465 }
466
467 const auto& meta = msg.model_metadata.value();
468
469 // Only render if tools were called
470 if (meta.tool_names.empty() && meta.tool_iterations == 0) {
471 return;
472 }
473
474 ImGui::Separator();
475 ImGui::Spacing();
476
477 // Tool timeline header - collapsible
478 ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.15f, 0.15f, 0.18f, 1.0f));
479 ImGui::PushStyleColor(ImGuiCol_HeaderHovered,
480 ImVec4(0.2f, 0.2f, 0.25f, 1.0f));
481
482 std::string header =
483 absl::StrFormat("%s Tools (%d calls, %.2fs)", ICON_MD_BUILD_CIRCLE,
484 meta.tool_iterations, meta.latency_seconds);
485
486 if (ImGui::TreeNode("##ToolTimeline", "%s", header.c_str())) {
487 // List tool names
488 if (!meta.tool_names.empty()) {
489 ImGui::TextDisabled("Tools called:");
490 for (const auto& tool : meta.tool_names) {
491 ImGui::BulletText("%s", tool.c_str());
492 }
493 }
494
495 // Provider/model info
496 ImGui::Spacing();
497 ImGui::TextDisabled("Provider: %s", meta.provider.c_str());
498 if (!meta.model.empty()) {
499 ImGui::TextDisabled("Model: %s", meta.model.c_str());
500 }
501
502 ImGui::TreePop();
503 }
504
505 ImGui::PopStyleColor(2);
506}
507
508absl::Status AgentChat::LoadHistory(const std::string& filepath) {
509#ifdef YAZE_WITH_JSON
510 std::ifstream file(filepath);
511 if (!file.is_open()) {
512 return absl::NotFoundError(
513 absl::StrFormat("Could not open file: %s", filepath));
514 }
515
516 try {
517 nlohmann::json j;
518 file >> j;
519
520 // Parse and load messages
521 // Note: This would require exposing a LoadHistory method in
522 // ConversationalAgentService. For now, we'll just return success.
523 // TODO: Implement full history restoration when service supports it.
524
525 return absl::OkStatus();
526 } catch (const nlohmann::json::exception& e) {
527 return absl::InvalidArgumentError(
528 absl::StrFormat("Failed to parse JSON: %s", e.what()));
529 }
530#else
531 return absl::UnimplementedError("JSON support not available");
532#endif
533}
534
535absl::Status AgentChat::SaveHistory(const std::string& filepath) {
536#ifdef YAZE_WITH_JSON
537 // Create directory if needed
538 std::filesystem::path path(filepath);
539 if (path.has_parent_path()) {
540 std::error_code ec;
541 std::filesystem::create_directories(path.parent_path(), ec);
542 if (ec) {
543 return absl::InternalError(absl::StrFormat(
544 "Failed to create history directory: %s", ec.message()));
545 }
546 }
547
548 std::ofstream file(filepath);
549 if (!file.is_open()) {
550 return absl::InternalError(
551 absl::StrFormat("Could not create file: %s", filepath));
552 }
553
554 try {
555 nlohmann::json j;
556 const auto& history = agent_service_.GetHistory();
557
558 j["version"] = 1;
559 j["messages"] = nlohmann::json::array();
560
561 for (const auto& msg : history) {
562 nlohmann::json msg_json;
563 msg_json["sender"] =
564 (msg.sender == cli::agent::ChatMessage::Sender::kUser) ? "user"
565 : "agent";
566 msg_json["message"] = msg.message;
567 msg_json["timestamp"] = absl::FormatTime(msg.timestamp);
568 j["messages"].push_back(msg_json);
569 }
570
571 file << j.dump(2); // Pretty print with 2-space indent
572
573 return absl::OkStatus();
574 } catch (const nlohmann::json::exception& e) {
575 return absl::InternalError(
576 absl::StrFormat("Failed to serialize JSON: %s", e.what()));
577 }
578#else
579 return absl::UnimplementedError("JSON support not available");
580#endif
581}
582
583} // namespace editor
584} // 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
absl::StatusOr< ChatMessage > SendMessage(const std::string &message)
const std::vector< ChatMessage > & GetHistory() const
std::vector< ContentBlock > ParseMessageContent(const std::string &content)
void RenderCodeBlock(const std::string &code, const std::string &language, int msg_index)
absl::Status LoadHistory(const std::string &filepath)
void SendMessage(const std::string &message)
Definition agent_chat.cc:80
void RenderTableData(const cli::agent::ChatMessage::TableData &table)
void SetContext(AgentUIContext *context)
Definition agent_chat.cc:61
void RenderToolTimeline(const cli::agent::ChatMessage &msg)
std::string last_plan_summary_
Definition agent_chat.h:128
void Initialize(ToastManager *toast_manager, ProposalDrawer *proposal_drawer)
Definition agent_chat.cc:51
std::vector< AutomationTelemetry > telemetry_history_
Definition agent_chat.h:127
ToastManager * toast_manager_
Definition agent_chat.h:107
AgentUIContext * context_
Definition agent_chat.h:106
void UpdateHarnessTelemetry(const AutomationTelemetry &telemetry)
void SetRomContext(Rom *rom)
Definition agent_chat.cc:57
void SetLastPlanSummary(const std::string &summary)
void Draw(float available_height=0.0f)
cli::agent::ConversationalAgentService agent_service_
Definition agent_chat.h:112
void RenderMessage(const cli::agent::ChatMessage &msg, int index)
ProposalDrawer * proposal_drawer_
Definition agent_chat.h:108
void RenderProposalQuickActions(const cli::agent::ChatMessage &msg, int index)
void HandleAgentResponse(const absl::StatusOr< cli::agent::ChatMessage > &response)
Definition agent_chat.cc:93
cli::agent::ConversationalAgentService * GetAgentService()
Definition agent_chat.cc:65
absl::Status SaveHistory(const std::string &filepath)
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)
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.
static absl::StatusOr< std::filesystem::path > GetUserDocumentsSubdirectory(const std::string &subdir)
Get a subdirectory within the user documents folder.
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_BUILD_CIRCLE
Definition icons.h:329
#define ICON_MD_PENDING
Definition icons.h:1398
#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_SMART_TOY
Definition icons.h:1781
#define LOG_ERROR(category, format,...)
Definition log.h:109
std::vector< std::vector< std::string > > rows
std::optional< ModelMetadata > model_metadata
std::optional< std::string > json_pretty