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),
259 "🙋",
"User", absl::StrFormat(
"%d", metrics.total_user_messages),
263 absl::StrFormat(
"%d", metrics.total_agent_messages),
265 RenderMetricLabel(
"🔧",
"Tools",
266 absl::StrFormat(
"%d", metrics.total_tool_calls),
271 info_cards.push_back(RenderPanelPanel(
273 {RenderMetricLabel(
"⚡",
"Last",
278 absl::StrFormat(
"%.2fs", metrics.average_latency_seconds),
279 Color::MagentaLight),
283 info_cards.push_back(RenderPanelPanel(
286 text(
"⌨ Enter ↵ Send") | dim,
287 text(
"⌨ Shift+Enter ↩ Multiline") | dim,
288 text(
"⌨ /help, /rom_info, /status") | dim,
289 text(
"⌨ Ctrl+T TODO overlay · Ctrl+K shortcuts · f fullscreen") |
294 Elements layout_elements;
295 layout_elements.push_back(header_line);
296 layout_elements.push_back(separatorLight());
297 layout_elements.push_back(
299 info_cards[0] | flex,
301 info_cards[1] | flex,
303 info_cards[2] | flex,
305 separator(), history_view | bgcolor(Color::Black) | flex}) |
309 layout_elements.push_back(separatorLight());
310 layout_elements.push_back(
311 hbox({text(
"Turns: ") | bold,
312 text(absl::StrFormat(
"%d", metrics.turn_index)), separator(),
313 text(
"User: ") | bold,
314 text(absl::StrFormat(
"%d", metrics.total_user_messages)),
315 separator(), text(
"Agent: ") | bold,
316 text(absl::StrFormat(
"%d", metrics.total_agent_messages)),
317 separator(), text(
"Tools: ") | bold,
318 text(absl::StrFormat(
"%d", metrics.total_tool_calls)), filler(),
319 text(
"Last response " +
321 color(Color::GrayLight)}) |
322 color(Color::GrayLight));
326 layout_elements.push_back(separator());
327 layout_elements.push_back(text(absl::StrCat(
"⚠ ERROR: ", *
last_error_)) |
332 layout_elements.push_back(separator());
333 layout_elements.push_back(
334 vbox({text(
"Quick Actions") | bold,
335 quick_pick_menu->Render() | frame | size(HEIGHT, EQUAL, 5) | flex,
336 separatorLight(), input_component->Render() | flex}));
337 layout_elements.push_back(hbox({
338 text(
"Press Enter to send | ") | dim,
339 send_button->Render(),
340 text(
" | Tab quick actions · Ctrl+T TODO "
341 "overlay · Ctrl+K shortcuts") |
347 vbox(layout_elements) | borderRounded | bgcolor(Color::Black);
351 std::vector<Element> overlays;
352 overlays.push_back(base);
359 base = dbox(overlays);
522 auto refresh_button =
523 Button(
"Refresh", [
this] {
screen_.PostEvent(Event::Custom); });
524 auto close_button = Button(
"Close", [
this] {
526 screen_.PostEvent(Event::Custom);
529 auto renderer = Renderer([
this, refresh_button, close_button] {
532 rows.push_back(text(
"TODO manager unavailable") | color(Color::Red) |
537 rows.push_back(text(
"No TODOs tracked") | dim | center);
539 for (const auto& item : todos) {
541 hbox({text(absl::StrFormat(
"[%s]", item.StatusToString())) |
542 color(Color::Yellow),
543 text(
" " + item.description) | flex,
544 text(item.category.empty()
546 : absl::StrCat(
" (", item.category,
")")) |
552 return dbox({window(text(
"📝 TODO Overlay") | bold,
553 vbox({separatorLight(),
554 vbox(rows) | frame | size(HEIGHT, LESS_THAN, 12) |
555 size(WIDTH, LESS_THAN, 70),
557 hbox({refresh_button->Render(), text(
" "),
558 close_button->Render()}) |
560 size(WIDTH, LESS_THAN, 72) | size(HEIGHT, LESS_THAN, 18) |
567ftxui::Component ChatTUI::BuildShortcutPalette() {
568 std::vector<std::pair<std::string, std::string>> shortcuts = {
569 {
"Ctrl+T",
"Toggle TODO overlay"}, {
"Ctrl+K",
"Shortcut palette"},
570 {
"Ctrl+L",
"Clear chat history"}, {
"Ctrl+Shift+S",
"Save transcript"},
571 {
"Ctrl+G",
"Focus quick actions"}, {
"Ctrl+P",
"Command palette"},
572 {
"Ctrl+F",
"Fullscreen chat"}, {
"Esc",
"Back to unified layout"},
575 auto close_button = Button(
"Close", [
this] {
576 shortcut_palette_visible_ =
false;
577 screen_.PostEvent(Event::Custom);
580 auto renderer = Renderer([shortcuts, close_button] {
582 for (
const auto& [combo, desc] : shortcuts) {
584 hbox({text(combo) | bold | color(Color::Cyan), text(
" " + desc)}));
588 {window(text(
"⌨ Shortcuts") | bold,
589 vbox({separatorLight(),
590 vbox(rows) | frame | size(HEIGHT, LESS_THAN, 12) |
591 size(WIDTH, LESS_THAN, 60),
592 separatorLight(), close_button->Render() | center})) |
593 size(WIDTH, LESS_THAN, 64) | size(HEIGHT, LESS_THAN, 16) | center});