116 auto input_message = std::make_shared<std::string>();
119 auto input_component =
122 auto todo_popup_toggle = [
this] {
125 auto shortcut_palette_toggle = [
this] {
130 input_component = CatchEvent(input_component,
131 [
this, input_message, todo_popup_toggle,
132 shortcut_palette_toggle](
const Event& event) {
133 if (event == Event::Return) {
134 if (input_message->empty())
137 input_message->clear();
140 if (event == Event::Special({20})) {
144 if (event == Event::Special({11})) {
145 shortcut_palette_toggle();
151 auto send_button = Button(
"Send", [
this, input_message] {
152 if (input_message->empty())
155 input_message->clear();
158 auto quick_pick_index = std::make_shared<int>(0);
159 auto quick_pick_menu = Menu(&
quick_actions_, quick_pick_index.get());
165 auto input_container = Container::Horizontal({
170 input_component->TakeFocus();
172 auto main_renderer = Renderer(input_container, [
this, input_component,
173 send_button, quick_pick_menu,
178 std::vector<Element> history_elements;
180 for (
const auto& msg : history) {
189 if (msg.table_data.has_value()) {
190 std::vector<std::vector<std::string>> table_rows;
191 if (!msg.table_data->headers.empty()) {
192 table_rows.push_back(msg.table_data->headers);
194 for (
const auto& row : msg.table_data->rows) {
195 table_rows.push_back(row);
197 Table table(table_rows);
198 table.SelectAll().Border(LIGHT);
199 if (!msg.table_data->headers.empty()) {
200 table.SelectRow(0).Decorate(bold);
202 body = table.Render();
203 }
else if (msg.json_pretty.has_value()) {
205 body = paragraphAlignLeft(msg.json_pretty.value());
208 body = paragraphAlignLeft(msg.message);
211 auto message_block = vbox({
217 if (msg.metrics.has_value()) {
218 const auto& metrics = msg.metrics.value();
220 vbox({message_block, separator(),
221 text(absl::StrFormat(
"📊 Turn %d | Elapsed: %.2fs",
223 metrics.total_elapsed_seconds)) |
224 color(Color::Cyan) | dim});
227 history_elements.push_back(message_block | border);
230 Element history_view;
231 if (history.empty()) {
233 vbox({text(
"yaze TUI") | bold | center,
234 text(
"A conversational agent for ROM hacking") | center,
235 separator(), text(
"No messages yet. Start chatting!") | dim}) |
238 history_view = vbox(history_elements) | vscroll_indicator | frame | flex;
244 Element header_line =
247 kSpinnerFrames.size()]) |
249 : text(
"✓") | color(Color::GreenLight)});
251 std::vector<Element> info_cards;
252 info_cards.push_back(RenderPanelPanel(
255 RenderMetricLabel(
"🕒",
"Turns",
256 absl::StrFormat(
"%d", metrics.turn_index),
260 absl::StrFormat(
"%d", metrics.total_user_messages),
264 absl::StrFormat(
"%d", metrics.total_agent_messages),
266 RenderMetricLabel(
"🔧",
"Tools",
267 absl::StrFormat(
"%d", metrics.total_tool_calls),
272 info_cards.push_back(RenderPanelPanel(
274 {RenderMetricLabel(
"⚡",
"Last",
279 absl::StrFormat(
"%.2fs", metrics.average_latency_seconds),
280 Color::MagentaLight),
284 info_cards.push_back(RenderPanelPanel(
287 text(
"⌨ Enter ↵ Send") | dim,
288 text(
"⌨ Shift+Enter ↩ Multiline") | dim,
289 text(
"⌨ /help, /rom_info, /status") | dim,
290 text(
"⌨ Ctrl+T TODO overlay · Ctrl+K shortcuts · f fullscreen") |
295 Elements layout_elements;
296 layout_elements.push_back(header_line);
297 layout_elements.push_back(separatorLight());
298 layout_elements.push_back(
300 info_cards[0] | flex,
302 info_cards[1] | flex,
304 info_cards[2] | flex,
306 separator(), history_view | bgcolor(Color::Black) | flex}) |
310 layout_elements.push_back(separatorLight());
311 layout_elements.push_back(
312 hbox({text(
"Turns: ") | bold,
313 text(absl::StrFormat(
"%d", metrics.turn_index)), separator(),
314 text(
"User: ") | bold,
315 text(absl::StrFormat(
"%d", metrics.total_user_messages)),
316 separator(), text(
"Agent: ") | bold,
317 text(absl::StrFormat(
"%d", metrics.total_agent_messages)),
318 separator(), text(
"Tools: ") | bold,
319 text(absl::StrFormat(
"%d", metrics.total_tool_calls)), filler(),
320 text(
"Last response " +
322 color(Color::GrayLight)}) |
323 color(Color::GrayLight));
327 layout_elements.push_back(separator());
328 layout_elements.push_back(text(absl::StrCat(
"⚠ ERROR: ", *
last_error_)) |
333 layout_elements.push_back(separator());
334 layout_elements.push_back(
335 vbox({text(
"Quick Actions") | bold,
336 quick_pick_menu->Render() | frame | size(HEIGHT, EQUAL, 5) | flex,
337 separatorLight(), input_component->Render() | flex}));
338 layout_elements.push_back(hbox({
339 text(
"Press Enter to send | ") | dim,
340 send_button->Render(),
341 text(
" | Tab quick actions · Ctrl+T TODO "
342 "overlay · Ctrl+K shortcuts") |
348 vbox(layout_elements) | borderRounded | bgcolor(Color::Black);
352 std::vector<Element> overlays;
353 overlays.push_back(base);
360 base = dbox(overlays);
523 auto refresh_button =
524 Button(
"Refresh", [
this] {
screen_.PostEvent(Event::Custom); });
525 auto close_button = Button(
"Close", [
this] {
527 screen_.PostEvent(Event::Custom);
530 auto renderer = Renderer([
this, refresh_button, close_button] {
533 rows.push_back(text(
"TODO manager unavailable") | color(Color::Red) |
538 rows.push_back(text(
"No TODOs tracked") | dim | center);
540 for (const auto& item : todos) {
542 hbox({text(absl::StrFormat(
"[%s]", item.StatusToString())) |
543 color(Color::Yellow),
544 text(
" " + item.description) | flex,
545 text(item.category.empty()
547 : absl::StrCat(
" (", item.category,
")")) |
553 return dbox({window(text(
"📝 TODO Overlay") | bold,
554 vbox({separatorLight(),
555 vbox(rows) | frame | size(HEIGHT, LESS_THAN, 12) |
556 size(WIDTH, LESS_THAN, 70),
558 hbox({refresh_button->Render(), text(
" "),
559 close_button->Render()}) |
561 size(WIDTH, LESS_THAN, 72) | size(HEIGHT, LESS_THAN, 18) |
568ftxui::Component ChatTUI::BuildShortcutPalette() {
569 std::vector<std::pair<std::string, std::string>> shortcuts = {
570 {
"Ctrl+T",
"Toggle TODO overlay"}, {
"Ctrl+K",
"Shortcut palette"},
571 {
"Ctrl+L",
"Clear chat history"}, {
"Ctrl+Shift+S",
"Save transcript"},
572 {
"Ctrl+G",
"Focus quick actions"}, {
"Ctrl+P",
"Command palette"},
573 {
"Ctrl+F",
"Fullscreen chat"}, {
"Esc",
"Back to unified layout"},
576 auto close_button = Button(
"Close", [
this] {
577 shortcut_palette_visible_ =
false;
578 screen_.PostEvent(Event::Custom);
581 auto renderer = Renderer([shortcuts, close_button] {
583 for (
const auto& [combo, desc] : shortcuts) {
585 hbox({text(combo) | bold | color(Color::Cyan), text(
" " + desc)}));
589 {window(text(
"⌨ Shortcuts") | bold,
590 vbox({separatorLight(),
591 vbox(rows) | frame | size(HEIGHT, LESS_THAN, 12) |
592 size(WIDTH, LESS_THAN, 60),
593 separatorLight(), close_button->Render() | center})) |
594 size(WIDTH, LESS_THAN, 64) | size(HEIGHT, LESS_THAN, 16) | center});