feat: Add Vim Mode and Autocomplete Features to Simple Chat

- Implemented vim-style line editing in the simple chat interface, allowing users to navigate and edit text using familiar vim commands.
- Introduced an autocomplete system in the FTXUI chat, providing real-time command suggestions and fuzzy matching for improved user experience.
- Updated documentation to reflect new features and usage instructions for vim mode and autocomplete functionality.
- Enhanced the TUI with autocomplete UI components for better interaction and command input.
This commit is contained in:
scawful
2025-10-06 00:54:15 -04:00
parent 939df9fa3d
commit be571e1b4f
11 changed files with 326 additions and 163 deletions

View File

@@ -1,4 +1,5 @@
#include <ftxui/component/component.hpp>
#include <ftxui/component/screen_interactive.hpp>
#include <ftxui/dom/elements.hpp>
#include "cli/util/autocomplete.h"
#include "cli/tui/tui.h"
@@ -46,78 +47,20 @@ Component CreateAutocompleteInput(std::string* input_str,
}
Component CreateQuickActionMenu(ScreenInteractive& screen) {
// Note: This function is a placeholder for future quick action menu integration.
// Currently not used in the TUI, but kept for API compatibility.
static int selected = 0;
static const std::vector<std::string> actions = {
"📖 Read hex at address",
"🎨 View palette colors",
"🔍 Search hex pattern",
"📊 Analyze palette",
"💾 ROM info",
"Quick Actions Menu - Not Yet Implemented",
"⬅️ Back",
};
auto menu = Menu(&actions, &selected);
return CatchEvent(menu, [&](Event e) {
if (e == Event::Return) {
switch (selected) {
case 0: {
// Quick hex read
std::cout << "\n📖 Quick Hex Read\n";
std::cout << "Address (hex): ";
std::string addr;
std::getline(std::cin, addr);
if (!addr.empty()) {
agent::HandleHexRead({"--address=" + addr, "--length=16", "--format=both"},
&app_context.rom);
}
break;
}
case 1: {
std::cout << "\n🎨 Quick Palette View\n";
std::cout << "Group (0-7): ";
std::string group;
std::getline(std::cin, group);
std::cout << "Palette (0-7): ";
std::string pal;
std::getline(std::cin, pal);
agent::HandlePaletteGetColors({"--group=" + group, "--palette=" + pal, "--format=hex"},
&app_context.rom);
break;
}
case 2: {
std::cout << "\n🔍 Hex Pattern Search\n";
std::cout << "Pattern (e.g. FF 00 ?? 12): ";
std::string pattern;
std::getline(std::cin, pattern);
agent::HandleHexSearch({"--pattern=" + pattern}, &app_context.rom);
break;
}
case 3: {
std::cout << "\n📊 Palette Analysis\n";
std::cout << "Group/Palette (e.g. 0/0): ";
std::string id;
std::getline(std::cin, id);
agent::HandlePaletteAnalyze({"--type=palette", "--id=" + id}, &app_context.rom);
break;
}
case 4: {
if (app_context.rom.is_loaded()) {
std::cout << "\n💾 ROM Information\n";
std::cout << "Title: " << app_context.rom.title() << "\n";
std::cout << "Size: " << app_context.rom.size() << " bytes\n";
} else {
std::cout << "\n⚠️ No ROM loaded\n";
}
std::cout << "\nPress Enter to continue...";
std::cin.get();
break;
}
case 5:
app_context.current_layout = LayoutID::kMainMenu;
screen.ExitLoopClosure()();
return true;
}
if (e == Event::Return && selected == 1) {
screen.ExitLoopClosure()();
return true;
}
return false;
});

View File

@@ -0,0 +1,33 @@
#ifndef YAZE_CLI_TUI_AUTOCOMPLETE_UI_H_
#define YAZE_CLI_TUI_AUTOCOMPLETE_UI_H_
#include <ftxui/component/component.hpp>
#include <ftxui/component/screen_interactive.hpp>
#include "cli/util/autocomplete.h"
namespace yaze {
namespace cli {
/**
* @brief Create an input component with autocomplete suggestions
*
* @param input_str Pointer to the input string
* @param engine Pointer to the autocomplete engine
* @return ftxui::Component Input component with autocomplete dropdown
*/
ftxui::Component CreateAutocompleteInput(std::string* input_str,
AutocompleteEngine* engine);
/**
* @brief Create a quick action menu for common ROM operations
*
* @param screen The screen interactive reference
* @return ftxui::Component Menu component with quick actions
*/
ftxui::Component CreateQuickActionMenu(ftxui::ScreenInteractive& screen);
} // namespace cli
} // namespace yaze
#endif // YAZE_CLI_TUI_AUTOCOMPLETE_UI_H_

View File

@@ -11,6 +11,8 @@
#include "ftxui/component/screen_interactive.hpp"
#include "ftxui/dom/elements.hpp"
#include "ftxui/dom/table.hpp"
#include "app/rom.h"
#include "autocomplete_ui.h"
namespace yaze {
namespace cli {
@@ -21,133 +23,192 @@ using namespace ftxui;
ChatTUI::ChatTUI(Rom* rom_context) : rom_context_(rom_context) {
if (rom_context_ != nullptr) {
agent_service_.SetRomContext(rom_context_);
rom_header_ = absl::StrFormat("ROM: %s | Size: %d bytes", rom_context_->title(), rom_context_->size());
} else {
rom_header_ = "No ROM loaded.";
}
InitializeAutocomplete();
}
void ChatTUI::SetRomContext(Rom* rom_context) {
rom_context_ = rom_context;
agent_service_.SetRomContext(rom_context_);
if (rom_context_ != nullptr) {
rom_header_ = absl::StrFormat("ROM: %s | Size: %d bytes", rom_context_->title(), rom_context_->size());
} else {
rom_header_ = "No ROM loaded.";
}
}
void ChatTUI::InitializeAutocomplete() {
autocomplete_engine_.RegisterCommand("/help", "Show help message.");
autocomplete_engine_.RegisterCommand("/exit", "Exit the chat.");
autocomplete_engine_.RegisterCommand("/clear", "Clear chat history.");
autocomplete_engine_.RegisterCommand("/rom_info", "Display info about the loaded ROM.");
}
void ChatTUI::Run() {
auto input = Input(&input_message_, "Enter your message...");
input = CatchEvent(input, [this](Event event) {
std::string input_message;
auto input_component = CreateAutocompleteInput(&input_message, &autocomplete_engine_);
input_component->TakeFocus();
auto send_button = Button("Send", [&] {
if (input_message.empty()) return;
OnSubmit(input_message);
input_message.clear();
});
// Handle 'Enter' key in the input field.
input_component = CatchEvent(input_component, [&](Event event) {
if (event == Event::Return) {
OnSubmit();
if (input_message.empty()) return true;
OnSubmit(input_message);
input_message.clear();
return true;
}
return false;
});
auto button = Button("Send", [this] { OnSubmit(); });
int selected_message = 0;
auto history_container = Container::Vertical({}, &selected_message);
auto controls = Container::Horizontal({input, button});
auto layout = Container::Vertical({controls});
auto main_container = Container::Vertical({
history_container,
input_component,
});
auto renderer = Renderer(layout, [this, input, button] {
Elements message_blocks;
auto main_renderer = Renderer(main_container, [&] {
const auto& history = agent_service_.GetHistory();
message_blocks.reserve(history.size());
if (history.size() != history_container->ChildCount()) {
history_container->DetachAllChildren();
for (const auto& msg : history) {
Element header = text(msg.sender == agent::ChatMessage::Sender::kUser
? "You"
: "Agent") |
bold |
color(msg.sender == agent::ChatMessage::Sender::kUser
? Color::Yellow
: Color::Green);
for (const auto& msg : history) {
Element header = text(msg.sender == agent::ChatMessage::Sender::kUser
? "You"
: "Agent") |
bold |
color(msg.sender == agent::ChatMessage::Sender::kUser
? Color::Yellow
: Color::Green);
Element body;
if (msg.table_data.has_value()) {
std::vector<std::vector<std::string>> table_rows;
table_rows.reserve(msg.table_data->rows.size() + 1);
table_rows.push_back(msg.table_data->headers);
for (const auto& row : msg.table_data->rows) {
table_rows.push_back(row);
Element body;
if (msg.table_data.has_value()) {
std::vector<std::vector<std::string>> table_rows;
table_rows.reserve(msg.table_data->rows.size() + 1);
table_rows.push_back(msg.table_data->headers);
for (const auto& row : msg.table_data->rows) {
table_rows.push_back(row);
}
Table table(table_rows);
table.SelectAll().Border(LIGHT);
if (!table_rows.empty()) {
table.SelectRow(0).Decorate(bold);
}
body = table.Render();
} else if (msg.json_pretty.has_value()) {
body = paragraph(msg.json_pretty.value());
} else {
body = paragraph(msg.message);
}
Table table(table_rows);
table.SelectAll().Border(LIGHT);
table.SelectAll().SeparatorVertical(LIGHT);
table.SelectAll().SeparatorHorizontal(LIGHT);
if (!table_rows.empty()) {
table.SelectRow(0).Decorate(bold);
Elements block = {header, hbox({text(" "), body})};
if (msg.metrics.has_value()) {
const auto& metrics = msg.metrics.value();
block.push_back(text(absl::StrFormat(
" 📊 Turn %d — users:%d agents:%d tools:%d commands:%d proposals:%d elapsed %.2fs avg %.2fs",
metrics.turn_index, metrics.total_user_messages,
metrics.total_agent_messages, metrics.total_tool_calls,
metrics.total_commands, metrics.total_proposals,
metrics.total_elapsed_seconds,
metrics.average_latency_seconds)) | color(Color::Cyan));
}
body = table.Render();
} else if (msg.json_pretty.has_value()) {
body = paragraph(msg.json_pretty.value());
} else {
body = paragraph(msg.message);
block.push_back(separator());
history_container->Add(Renderer([block = vbox(block)] { return block; }));
}
Elements block = {header, hbox({text(" "), body})};
if (msg.metrics.has_value()) {
const auto& metrics = msg.metrics.value();
block.push_back(text(absl::StrFormat(
" 📊 Turn %d — users:%d agents:%d tools:%d commands:%d proposals:%d elapsed %.2fs avg %.2fs",
metrics.turn_index, metrics.total_user_messages,
metrics.total_agent_messages, metrics.total_tool_calls,
metrics.total_commands, metrics.total_proposals,
metrics.total_elapsed_seconds,
metrics.average_latency_seconds)) |
color(Color::Cyan));
}
block.push_back(separator());
message_blocks.push_back(vbox(block));
selected_message = history.empty() ? 0 : history.size() - 1;
}
if (message_blocks.empty()) {
message_blocks.push_back(text("No messages yet. Start chatting!") | dim);
auto history_view = history_container->Render() | flex | frame;
if (history.empty()) {
history_view = vbox({text("No messages yet. Start chatting!") | dim}) | flex | center;
}
const auto metrics = agent_service_.GetMetrics();
Element metrics_bar = text(absl::StrFormat(
"Turns:%d Users:%d Agents:%d Tools:%d Commands:%d Proposals:%d Elapsed:%.2fs avg %.2fs",
"Turns:%d Users:%d Agents:%d Tools:%d Commands:%d Proposals:%d Elapsed:%.2fs avg %.2fs",
metrics.turn_index, metrics.total_user_messages,
metrics.total_agent_messages, metrics.total_tool_calls,
metrics.total_commands, metrics.total_proposals,
metrics.total_elapsed_seconds, metrics.average_latency_seconds)) |
color(Color::Cyan);
Elements content{
vbox(message_blocks) | flex | frame,
separator(),
};
auto input_view = hbox({
text("You: ") | bold,
input_component->Render() | flex,
text(" "),
send_button->Render(),
});
Element error_view;
if (last_error_.has_value()) {
content.push_back(text(absl::StrCat("", *last_error_)) |
color(Color::Red));
content.push_back(separator());
error_view = text(absl::StrCat("", *last_error_)) | color(Color::Red);
}
content.push_back(metrics_bar);
content.push_back(separator());
content.push_back(hbox({
text("You: ") | bold,
input->Render() | flex,
text(" "),
button->Render(),
}));
return vbox(content) | border;
return gridbox({
{text(rom_header_) | center},
{separator()},
{history_view},
{separator()},
{metrics_bar},
{error_view ? separator() : filler()},
{error_view ? error_view : filler()},
{separator()},
{input_view},
}) | border;
});
screen_.Loop(renderer);
screen_.Loop(main_renderer);
}
void ChatTUI::OnSubmit() {
if (input_message_.empty()) {
void ChatTUI::OnSubmit(const std::string& message) {
if (message.empty()) {
return;
}
auto response = agent_service_.SendMessage(input_message_);
if (message == "/exit") {
screen_.Exit();
return;
}
if (message == "/clear") {
agent_service_.ResetConversation();
return;
}
if (message == "/rom_info") {
// Send ROM info as a user message to get a response
auto response = agent_service_.SendMessage("Show me information about the loaded ROM");
if (!response.ok()) {
last_error_ = response.status().message();
} else {
last_error_.reset();
}
return;
}
if (message == "/help") {
// Send help request as a user message
auto response = agent_service_.SendMessage("What commands can I use?");
if (!response.ok()) {
last_error_ = response.status().message();
} else {
last_error_.reset();
}
return;
}
auto response = agent_service_.SendMessage(message);
if (!response.ok()) {
last_error_ = response.status().message();
} else {
last_error_.reset();
}
input_message_.clear();
}
} // namespace tui

View File

@@ -7,6 +7,8 @@
#include "ftxui/component/screen_interactive.hpp"
#include "cli/service/agent/conversational_agent_service.h"
#include "cli/util/autocomplete.h"
namespace yaze {
class Rom;
@@ -21,13 +23,15 @@ class ChatTUI {
void SetRomContext(Rom* rom_context);
private:
void OnSubmit();
void OnSubmit(const std::string& message);
void InitializeAutocomplete();
ftxui::ScreenInteractive screen_ = ftxui::ScreenInteractive::Fullscreen();
std::string input_message_;
agent::ConversationalAgentService agent_service_;
Rom* rom_context_ = nullptr;
std::optional<std::string> last_error_;
AutocompleteEngine autocomplete_engine_;
std::string rom_header_;
};
} // namespace tui