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