3#if defined(YAZE_ENABLE_AGENT_CLI)
10#include "absl/strings/str_cat.h"
11#include "absl/strings/str_format.h"
12#include "absl/time/time.h"
15#include "ftxui/component/component.hpp"
16#include "ftxui/component/component_base.hpp"
17#include "ftxui/component/event.hpp"
18#include "ftxui/component/screen_interactive.hpp"
19#include "ftxui/dom/elements.hpp"
20#include "ftxui/dom/table.hpp"
30const std::vector<std::string> kSpinnerFrames = {
"⠋",
"⠙",
"⠹",
"⠸",
"⠼",
31 "⠴",
"⠦",
"⠧",
"⠇",
"⠏"};
33Element RenderPanelPanel(
const std::string& title,
34 const std::vector<Element>& body, Color border_color,
35 bool highlight =
false) {
36 auto panel = window(text(title) | bold, vbox(body));
38 panel = panel | color(border_color) | bgcolor(Color::GrayDark);
40 panel = panel | color(border_color);
45Element RenderLatencySparkline(
const std::vector<double>& data) {
47 return text(
"No latency data yet") | dim;
50 for (
double d : data) {
51 bars.push_back(gauge(d) | flex);
56Element RenderMetricLabel(
const std::string& icon,
const std::string& label,
57 const std::string& value, Color color) {
58 return hbox({text(icon) | ftxui::color(color),
59 text(
" " + label +
": ") | bold,
60 text(value) | ftxui::color(color)});
65 if (rom_context_ !=
nullptr) {
66 agent_service_.SetRomContext(rom_context_);
67 rom_header_ = absl::StrFormat(
"ROM: %s | Size: %d bytes",
68 rom_context_->title(), rom_context_->size());
70 rom_header_ =
"No ROM loaded.";
72 auto status = todo_manager_.Initialize();
73 todo_manager_ready_ = status.ok();
74 InitializeAutocomplete();
75 quick_actions_ = {
"List dungeon entrances",
76 "Show sprite palette summary",
77 "Summarize overworld map",
80 "Search dialogue for 'Master Sword'",
81 "Suggest QA checklist",
90void ChatTUI::SetRomContext(Rom* rom_context) {
91 rom_context_ = rom_context;
92 agent_service_.SetRomContext(rom_context_);
93 if (rom_context_ !=
nullptr) {
94 rom_header_ = absl::StrFormat(
"ROM: %s | Size: %d bytes",
95 rom_context_->title(), rom_context_->size());
97 rom_header_ =
"No ROM loaded.";
101void ChatTUI::InitializeAutocomplete() {
102 autocomplete_engine_.RegisterCommand(
"/help",
"Show help message");
103 autocomplete_engine_.RegisterCommand(
"/exit",
"Exit the chat");
104 autocomplete_engine_.RegisterCommand(
"/quit",
"Exit the chat");
105 autocomplete_engine_.RegisterCommand(
"/clear",
"Clear chat history");
106 autocomplete_engine_.RegisterCommand(
"/rom_info",
"Display ROM information");
107 autocomplete_engine_.RegisterCommand(
"/status",
"Show chat statistics");
110 autocomplete_engine_.RegisterCommand(
"What is",
"Ask a question about ROM");
111 autocomplete_engine_.RegisterCommand(
"How do I",
"Get help with a task");
112 autocomplete_engine_.RegisterCommand(
"Show me",
"Request information");
113 autocomplete_engine_.RegisterCommand(
"List all",
"List items from ROM");
114 autocomplete_engine_.RegisterCommand(
"Find",
"Search for something");
118 auto input_message = std::make_shared<std::string>();
121 auto input_component =
124 auto todo_popup_toggle = [
this] {
127 auto shortcut_palette_toggle = [
this] {
128 ToggleShortcutPalette();
132 input_component = CatchEvent(input_component,
133 [
this, input_message, todo_popup_toggle,
134 shortcut_palette_toggle](
const Event& event) {
135 if (event == Event::Return) {
136 if (input_message->empty())
138 OnSubmit(*input_message);
139 input_message->clear();
142 if (event == Event::Special({20})) {
146 if (event == Event::Special({11})) {
147 shortcut_palette_toggle();
153 auto send_button = Button(
"Send", [
this, input_message] {
154 if (input_message->empty())
156 OnSubmit(*input_message);
157 input_message->clear();
160 auto quick_pick_index = std::make_shared<int>(0);
161 auto quick_pick_menu = Menu(&quick_actions_, quick_pick_index.get());
163 todo_popup_component_ = CreateTodoPopup();
164 shortcut_palette_component_ = BuildShortcutPalette();
167 auto input_container = Container::Horizontal({
172 input_component->TakeFocus();
174 auto main_renderer = Renderer(input_container, [
this, input_component,
175 send_button, quick_pick_menu,
177 const auto& history = agent_service_.GetHistory();
180 std::vector<Element> history_elements;
182 for (
const auto& msg : history) {
184 text(msg.sender == agent::ChatMessage::Sender::kUser ?
"You"
187 color(msg.sender == agent::ChatMessage::Sender::kUser ? Color::Yellow
191 if (msg.table_data.has_value()) {
192 std::vector<std::vector<std::string>> table_rows;
193 if (!msg.table_data->headers.empty()) {
194 table_rows.push_back(msg.table_data->headers);
196 for (
const auto& row : msg.table_data->rows) {
197 table_rows.push_back(row);
199 Table table(table_rows);
200 table.SelectAll().Border(LIGHT);
201 if (!msg.table_data->headers.empty()) {
202 table.SelectRow(0).Decorate(bold);
204 body = table.Render();
205 }
else if (msg.json_pretty.has_value()) {
207 body = paragraphAlignLeft(msg.json_pretty.value());
210 body = paragraphAlignLeft(msg.message);
213 auto message_block = vbox({
219 if (msg.metrics.has_value()) {
220 const auto& metrics = msg.metrics.value();
222 vbox({message_block, separator(),
223 text(absl::StrFormat(
"📊 Turn %d | Elapsed: %.2fs",
225 metrics.total_elapsed_seconds)) |
226 color(Color::Cyan) | dim});
229 history_elements.push_back(message_block | border);
232 Element history_view;
233 if (history.empty()) {
235 vbox({text(
"yaze TUI") | bold | center,
236 text(
"A conversational agent for ROM hacking") | center,
237 separator(), text(
"No messages yet. Start chatting!") | dim}) |
240 history_view = vbox(history_elements) | vscroll_indicator | frame | flex;
244 auto metrics = CurrentMetrics();
246 Element header_line =
247 hbox({text(rom_header_) | bold, filler(),
248 agent_busy_.load() ? text(kSpinnerFrames[spinner_index_.load() %
249 kSpinnerFrames.size()]) |
251 : text(
"✓") | color(Color::GreenLight)});
253 std::vector<Element> info_cards;
254 info_cards.push_back(RenderPanelPanel(
257 RenderMetricLabel(
"🕒",
"Turns",
258 absl::StrFormat(
"%d", metrics.turn_index),
261 "🙋",
"User", absl::StrFormat(
"%d", metrics.total_user_messages),
265 absl::StrFormat(
"%d", metrics.total_agent_messages),
267 RenderMetricLabel(
"🔧",
"Tools",
268 absl::StrFormat(
"%d", metrics.total_tool_calls),
273 info_cards.push_back(RenderPanelPanel(
275 {RenderMetricLabel(
"⚡",
"Last",
276 absl::StrFormat(
"%.2fs", last_response_seconds_),
280 absl::StrFormat(
"%.2fs", metrics.average_latency_seconds),
281 Color::MagentaLight),
282 RenderLatencySparkline(latency_history_)},
283 Color::Magenta, agent_busy_.load()));
285 info_cards.push_back(RenderPanelPanel(
288 text(
"⌨ Enter ↵ Send") | dim,
289 text(
"⌨ Shift+Enter ↩ Multiline") | dim,
290 text(
"⌨ /help, /rom_info, /status") | dim,
291 text(
"⌨ Ctrl+T TODO overlay · Ctrl+K shortcuts · f fullscreen") |
296 Elements layout_elements;
297 layout_elements.push_back(header_line);
298 layout_elements.push_back(separatorLight());
299 layout_elements.push_back(
301 info_cards[0] | flex,
303 info_cards[1] | flex,
305 info_cards[2] | flex,
307 separator(), history_view | bgcolor(Color::Black) | flex}) |
311 layout_elements.push_back(separatorLight());
312 layout_elements.push_back(
313 hbox({text(
"Turns: ") | bold,
314 text(absl::StrFormat(
"%d", metrics.turn_index)), separator(),
315 text(
"User: ") | bold,
316 text(absl::StrFormat(
"%d", metrics.total_user_messages)),
317 separator(), text(
"Agent: ") | bold,
318 text(absl::StrFormat(
"%d", metrics.total_agent_messages)),
319 separator(), text(
"Tools: ") | bold,
320 text(absl::StrFormat(
"%d", metrics.total_tool_calls)), filler(),
321 text(
"Last response " +
322 absl::StrFormat(
"%.2fs", last_response_seconds_)) |
323 color(Color::GrayLight)}) |
324 color(Color::GrayLight));
327 if (last_error_.has_value()) {
328 layout_elements.push_back(separator());
329 layout_elements.push_back(text(absl::StrCat(
"⚠ ERROR: ", *last_error_)) |
334 layout_elements.push_back(separator());
335 layout_elements.push_back(
336 vbox({text(
"Quick Actions") | bold,
337 quick_pick_menu->Render() | frame | size(HEIGHT, EQUAL, 5) | flex,
338 separatorLight(), input_component->Render() | flex}));
339 layout_elements.push_back(hbox({
340 text(
"Press Enter to send | ") | dim,
341 send_button->Render(),
342 text(
" | Tab quick actions · Ctrl+T TODO "
343 "overlay · Ctrl+K shortcuts") |
349 vbox(layout_elements) | borderRounded | bgcolor(Color::Black);
351 if ((todo_popup_visible_ && todo_popup_component_) ||
352 (shortcut_palette_visible_ && shortcut_palette_component_)) {
353 std::vector<Element> overlays;
354 overlays.push_back(base);
355 if (todo_popup_visible_ && todo_popup_component_) {
356 overlays.push_back(todo_popup_component_->Render());
358 if (shortcut_palette_visible_ && shortcut_palette_component_) {
359 overlays.push_back(shortcut_palette_component_->Render());
361 base = dbox(overlays);
367 screen_.Loop(main_renderer);
370void ChatTUI::OnSubmit(
const std::string& message) {
371 if (message.empty()) {
375 if (message ==
"/exit" || message ==
"/quit") {
379 if (message ==
"/clear") {
380 agent_service_.ResetConversation();
384 if (message ==
"/rom_info") {
386 agent_service_.SendMessage(
"Show me information about the loaded ROM");
387 if (!response.ok()) {
388 last_error_ = std::string(response.status().message());
394 if (message ==
"/help") {
395 auto response = agent_service_.SendMessage(
"What commands can I use?");
396 if (!response.ok()) {
397 last_error_ = std::string(response.status().message());
403 if (message ==
"/status") {
404 const auto metrics = agent_service_.GetMetrics();
405 std::string status_message = absl::StrFormat(
407 "- Total Turns: %d\n"
408 "- User Messages: %d\n"
409 "- Agent Messages: %d\n"
413 "- Total Elapsed Time: %.2f seconds\n"
414 "- Average Latency: %.2f seconds",
415 metrics.turn_index, metrics.total_user_messages,
416 metrics.total_agent_messages, metrics.total_tool_calls,
417 metrics.total_commands, metrics.total_proposals,
418 metrics.total_elapsed_seconds, metrics.average_latency_seconds);
421 auto response = agent_service_.SendMessage(status_message);
422 if (!response.ok()) {
423 last_error_ = std::string(response.status().message());
430 LaunchAgentPrompt(message);
433void ChatTUI::LaunchAgentPrompt(
const std::string& prompt) {
434 if (prompt.empty()) {
438 agent_busy_.store(
true);
439 spinner_running_.store(
true);
440 if (!spinner_thread_.joinable()) {
441 spinner_thread_ = std::thread([
this] {
442 while (spinner_running_.load()) {
443 std::this_thread::sleep_for(std::chrono::milliseconds(90));
444 spinner_index_.fetch_add(1);
445 screen_.PostEvent(Event::Custom);
450 last_send_time_ = std::chrono::steady_clock::now();
452 auto future = std::async(std::launch::async, [
this, prompt] {
453 auto response = agent_service_.SendMessage(prompt);
454 if (!response.ok()) {
455 last_error_ = std::string(response.status().message());
460 auto end_time = std::chrono::steady_clock::now();
461 last_response_seconds_ =
462 std::chrono::duration<double>(end_time - last_send_time_).count();
464 latency_history_.push_back(last_response_seconds_);
465 if (latency_history_.size() > 30) {
466 latency_history_.erase(latency_history_.begin());
469 agent_busy_.store(
false);
471 screen_.PostEvent(Event::Custom);
475 std::lock_guard<std::mutex> lock(worker_mutex_);
476 worker_futures_.push_back(std::move(future));
480void ChatTUI::CleanupWorkers() {
481 std::lock_guard<std::mutex> lock(worker_mutex_);
482 for (
auto& future : worker_futures_) {
483 if (future.valid()) {
487 worker_futures_.clear();
492agent::ChatMessage::SessionMetrics ChatTUI::CurrentMetrics()
const {
493 return agent_service_.GetMetrics();
496void ChatTUI::StopSpinner() {
497 spinner_running_.store(
false);
498 if (spinner_thread_.joinable()) {
499 spinner_thread_.join();
503void ChatTUI::ToggleTodoPopup() {
504 if (!todo_popup_component_) {
505 todo_popup_component_ = CreateTodoPopup();
507 todo_popup_visible_ = !todo_popup_visible_;
508 if (todo_popup_visible_ && todo_popup_component_) {
509 screen_.PostEvent(Event::Custom);
513void ChatTUI::ToggleShortcutPalette() {
514 if (!shortcut_palette_component_) {
515 shortcut_palette_component_ = BuildShortcutPalette();
517 shortcut_palette_visible_ = !shortcut_palette_visible_;
518 if (shortcut_palette_visible_) {
519 screen_.PostEvent(Event::Custom);
523ftxui::Component ChatTUI::CreateTodoPopup() {
524 auto refresh_button =
525 Button(
"Refresh", [
this] { screen_.PostEvent(Event::Custom); });
526 auto close_button = Button(
"Close", [
this] {
527 todo_popup_visible_ =
false;
528 screen_.PostEvent(Event::Custom);
531 auto renderer = Renderer([
this, refresh_button, close_button] {
533 if (!todo_manager_ready_) {
534 rows.push_back(text(
"TODO manager unavailable") | color(Color::Red) |
537 auto todos = todo_manager_.GetAllTodos();
539 rows.push_back(text(
"No TODOs tracked") | dim | center);
541 for (const auto& item : todos) {
543 hbox({text(absl::StrFormat(
"[%s]", item.StatusToString())) |
544 color(Color::Yellow),
545 text(
" " + item.description) | flex,
546 text(item.category.empty()
548 : absl::StrCat(
" (", item.category,
")")) |
554 return dbox({window(text(
"📝 TODO Overlay") | bold,
555 vbox({separatorLight(),
556 vbox(rows) | frame | size(HEIGHT, LESS_THAN, 12) |
557 size(WIDTH, LESS_THAN, 70),
559 hbox({refresh_button->Render(), text(
" "),
560 close_button->Render()}) |
562 size(WIDTH, LESS_THAN, 72) | size(HEIGHT, LESS_THAN, 18) |
569ftxui::Component ChatTUI::BuildShortcutPalette() {
570 std::vector<std::pair<std::string, std::string>> shortcuts = {
571 {
"Ctrl+T",
"Toggle TODO overlay"}, {
"Ctrl+K",
"Shortcut palette"},
572 {
"Ctrl+L",
"Clear chat history"}, {
"Ctrl+Shift+S",
"Save transcript"},
573 {
"Ctrl+G",
"Focus quick actions"}, {
"Ctrl+P",
"Command palette"},
574 {
"Ctrl+F",
"Fullscreen chat"}, {
"Esc",
"Back to unified layout"},
577 auto close_button = Button(
"Close", [
this] {
578 shortcut_palette_visible_ =
false;
579 screen_.PostEvent(Event::Custom);
582 auto renderer = Renderer([shortcuts, close_button] {
584 for (
const auto& [combo, desc] : shortcuts) {
586 hbox({text(combo) | bold | color(Color::Cyan), text(
" " + desc)}));
590 {window(text(
"⌨ Shortcuts") | bold,
591 vbox({separatorLight(),
592 vbox(rows) | frame | size(HEIGHT, LESS_THAN, 12) |
593 size(WIDTH, LESS_THAN, 60),
594 separatorLight(), close_button->Render() | center})) |
595 size(WIDTH, LESS_THAN, 64) | size(HEIGHT, LESS_THAN, 16) | center});
601bool ChatTUI::IsPopupOpen()
const {
602 return todo_popup_visible_ || shortcut_palette_visible_;
ChatTUI(Rom *rom_context=nullptr)
Component CreateAutocompleteInput(std::string *input_str, AutocompleteEngine *engine)
Create an input component with autocomplete suggestions.