#include "views.h" #include "data_http.h" #include #include "imgui.h" #include "implot.h" #include "viz/kpi_card.h" #include "viz/bar_chart.h" #include "viz/pie_chart.h" #include "viz/table_view.h" #include "viz/sparkline.h" #include "core/icons_tabler.h" #include "core/dashboard_panel.h" #include "core/dashboard_grid.h" #include "core/fullscreen_window.h" #include "core/tokens.h" #include "core/page_header.h" #include "core/empty_state.h" #include "core/badge.h" #include "core/button.h" #include "core/icon_button.h" #include "core/toolbar.h" #include "core/modal_dialog.h" #include "core/text_input.h" #include "core/select.h" #include "core/toast.h" #include "core/process_runner.h" #include "core/tree_view.h" #include "core/selectable_text.h" #include #include #include #include #include #include #include // --------------------------------------------------------------------------- // Shared state // --------------------------------------------------------------------------- static std::string g_api_url; static fn_ui::ProcessRunner g_reindex_runner; // --------------------------------------------------------------------------- // Monitor (issue 0086) — local state // --------------------------------------------------------------------------- // Presets de ventana temporal: 1h, 24h, 7d, 30d, All. Indice por defecto = 24h. static const char* kMonitorWindowLabels[] = {"1h", "24h", "7d", "30d", "All"}; static const int kMonitorWindowSecs[] = {3600, 86400, 604800, 2592000, 0}; static int g_monitor_window_idx = 1; // 24h por defecto static bool g_monitor_reload_request = false; static bool g_monitor_ws_connected = false; static long long g_monitor_last_event_ts = 0; // Scatter: ventana solo del eje X. NO afecta KPIs ni queries — solo al plot. static const char* kScatterWindowLabels[] = {"1m", "5m", "15m", "1h", "6h"}; static const int kScatterWindowSecs[] = {60, 300, 900, 3600, 21600}; static int g_scatter_window_idx = 1; // 5m por defecto // Recent Executions: si true, oculta filas con function_id vacio. static bool g_recent_only_registry = false; bool monitor_consume_reload_request() { bool r = g_monitor_reload_request; g_monitor_reload_request = false; return r; } void monitor_set_ws_state(bool connected, long long last_event_ts) { g_monitor_ws_connected = connected; if (last_event_ts > 0) g_monitor_last_event_ts = last_event_ts; } // Formatea un epoch ts en "YYYY-MM-DD HH:MM:SS" local. Si ts == 0 -> "-". static std::string format_ts(long long ts) { if (ts <= 0) return "-"; std::time_t t = static_cast(ts); std::tm tm_buf{}; #if defined(_WIN32) localtime_s(&tm_buf, &t); #else localtime_r(&t, &tm_buf); #endif char buf[32]; std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_buf); return std::string(buf); } // Formatea ts relativo a now: "3s", "2m", "1h", "4d". Para "live indicator". static std::string format_ts_relative(long long ts) { if (ts <= 0) return "never"; std::time_t now = std::time(nullptr); long long diff = static_cast(now) - ts; if (diff < 0) diff = 0; char buf[32]; if (diff < 60) std::snprintf(buf, sizeof(buf), "%llds ago", (long long)diff); else if (diff < 3600) std::snprintf(buf, sizeof(buf), "%lldm ago", (long long)(diff / 60)); else if (diff < 86400) std::snprintf(buf, sizeof(buf), "%lldh ago", (long long)(diff / 3600)); else std::snprintf(buf, sizeof(buf), "%lldd ago", (long long)(diff / 86400)); return std::string(buf); } static fn_ui::ProcessRunner g_add_runner; // Add modal state enum class AddKind : int { App = 0, Analysis = 1, Vault = 2 }; static bool g_show_add = false; static int g_add_kind_idx = static_cast(AddKind::App); static int g_add_project_idx = -1; // -1 = (none / sin project) static int g_add_lang_idx = 0; static int g_add_domain_idx = 0; static char g_add_name[128] = {}; static char g_add_desc[256] = {}; static char g_add_packages[256] = {}; static char g_add_vault_path[512] = {}; void views_set_api_url(const std::string& url) { g_api_url = url; } static std::vector to_cstr(const std::vector& v) { std::vector out; out.reserve(v.size()); for (auto& s : v) out.push_back(s.c_str()); return out; } // Cache del Explorer: forward declarado aqui para que trigger_reload pueda // invalidarlo (la lista completa de funciones se cachea hasta que el usuario // dispara Reload o Reindex). static std::vector g_explorer_funcs; static bool g_explorer_loaded = false; static std::string g_explorer_selected_id; static FunctionDetail g_explorer_detail; static std::vector g_explorer_tests; static int g_explorer_test_idx = 0; static char g_explorer_filter[128] = {}; static int g_explorer_lang_idx = 0; static int g_explorer_domain_idx = 0; static void trigger_reload() { ImGui::GetIO().UserData = reinterpret_cast(1); g_explorer_loaded = false; } // --------------------------------------------------------------------------- // KPI row // --------------------------------------------------------------------------- void draw_kpi_row(const RegistryData& data) { const RegistryStats& stats = data.stats; float tested_pct = stats.total_functions > 0 ? 100.0f * stats.tested_functions / stats.total_functions : 0.0f; float pure_pct = stats.total_functions > 0 ? 100.0f * stats.pure_functions / stats.total_functions : 0.0f; const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX; // Sparkline shared: ultimos 30 dias de creacion de funciones (timeline real // del registry). Si no hay datos cargados, queda vacio y el card mostrara // solo valor + delta placeholder. const float* spark_data = data.date_values.empty() ? nullptr : data.date_values.data(); const int spark_count = static_cast(data.date_values.size()); if (ImGui::BeginTable("##kpi_grid", 4, flags)) { struct KPI { const char* label; float value; const char* fmt; const char* icon; }; const KPI cards[8] = { {"Functions", static_cast(stats.total_functions), "%.0f", TI_FUNCTION}, {"Types", static_cast(stats.total_types), "%.0f", TI_HEXAGON}, {"Apps", static_cast(stats.total_apps), "%.0f", TI_APPS}, {"Analysis", static_cast(stats.total_analysis), "%.0f", TI_FLASK}, {"Unit Tests", static_cast(stats.total_unit_tests), "%.0f", TI_TEST_PIPE}, {"Proposals", static_cast(stats.total_proposals), "%.0f", TI_BULB}, {"Tested", tested_pct, "%.0f%%", TI_CIRCLE_CHECK}, {"Pure", pure_pct, "%.0f%%", TI_HEART}, }; for (int i = 0; i < 8; i++) { if (i % 4 == 0) ImGui::TableNextRow(); ImGui::TableSetColumnIndex(i % 4); kpi_card(cards[i].label, cards[i].value, 0.0f, spark_data, spark_count, cards[i].fmt, cards[i].icon); } ImGui::EndTable(); } } // --------------------------------------------------------------------------- // Charts // --------------------------------------------------------------------------- static bool chart_panel_begin(const char* title, const ImVec2& size) { using namespace fn_tokens; ImGui::PushStyleColor(ImGuiCol_ChildBg, colors::surface); ImGui::PushStyleColor(ImGuiCol_Border, colors::border); ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, radius::md); ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::md, spacing::md)); ImGui::BeginChild(title, size, ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted); ImGui::TextUnformatted(title); ImGui::PopStyleColor(); ImGui::Separator(); return true; } static void chart_panel_end() { ImGui::EndChild(); ImGui::PopStyleVar(3); ImGui::PopStyleColor(2); } void draw_charts(RegistryData& data, float height) { const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX; const float plot_h = height - 48.0f; if (ImGui::BeginTable("##chart_grid", 4, flags)) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); { ImVec2 sz(ImGui::GetContentRegionAvail().x, height); chart_panel_begin("By Language", sz); auto labels = to_cstr(data.lang_labels); if (!labels.empty()) bar_chart("##lang", labels.data(), data.lang_values.data(), static_cast(labels.size()), 0.67f, plot_h); chart_panel_end(); } ImGui::TableSetColumnIndex(1); { ImVec2 sz(ImGui::GetContentRegionAvail().x, height); chart_panel_begin("By Domain", sz); auto labels = to_cstr(data.domain_labels); if (!labels.empty()) bar_chart("##domain", labels.data(), data.domain_values.data(), static_cast(labels.size()), 0.67f, plot_h); chart_panel_end(); } ImGui::TableSetColumnIndex(2); { ImVec2 sz(ImGui::GetContentRegionAvail().x, height); chart_panel_begin("Purity", sz); const char* labels[] = {"Pure", "Impure"}; float values[] = {static_cast(data.stats.pure_functions), static_cast(data.stats.impure_functions)}; pie_chart("##purity", labels, values, 2, 0.0f, plot_h); chart_panel_end(); } ImGui::TableSetColumnIndex(3); { ImVec2 sz(ImGui::GetContentRegionAvail().x, height); chart_panel_begin("Kind", sz); auto labels = to_cstr(data.kind_labels); if (!labels.empty()) pie_chart("##kind", labels.data(), data.kind_values.data(), static_cast(labels.size()), 0.0f, plot_h); chart_panel_end(); } ImGui::EndTable(); } } // --------------------------------------------------------------------------- // Tables // --------------------------------------------------------------------------- void draw_recent_functions(const std::vector& funcs) { if (funcs.empty()) { empty_state("( no data )", "No functions yet", "Run 'fn index' to populate the registry"); return; } const char* headers[] = {"Name", "Lang", "Domain", "Kind", "Purity", "Tested", "Created"}; constexpr int cols = 7; std::vector cell_strings; cell_strings.reserve(funcs.size() * cols); for (auto& f : funcs) { cell_strings.push_back(f.name); cell_strings.push_back(f.lang); cell_strings.push_back(f.domain); cell_strings.push_back(f.kind); cell_strings.push_back(f.purity); cell_strings.push_back(f.tested ? "yes" : "no"); cell_strings.push_back(f.created_at.substr(0, 10)); } auto cells = to_cstr(cell_strings); table_view("##recent", headers, cols, cells.data(), static_cast(funcs.size())); } void draw_apps_list(const std::vector& apps) { if (apps.empty()) { empty_state("( no data )", "No apps registered", "Use the + Add button above or run 'fn sync'"); return; } const char* headers[] = {"Name", "Lang", "Domain", "Framework", "Git", "Description"}; constexpr int cols = 6; std::vector cell_strings; cell_strings.reserve(apps.size() * cols); for (auto& a : apps) { cell_strings.push_back(a.name); cell_strings.push_back(a.lang); cell_strings.push_back(a.domain); cell_strings.push_back(a.framework); // Indicador de git: tiene repo remoto (gitea), solo local, o ninguno. // - "remote": repo_url poblado en el frontmatter del app.md // - "local": hay .git/ en dir_path pero sin repo_url // - "-": ni .git ni repo_url std::string git_status; if (!a.repo_url.empty()) { git_status = "remote"; } else if (!a.dir_path.empty()) { std::error_code ec; if (std::filesystem::exists(std::filesystem::path(a.dir_path) / ".git", ec)) { git_status = "local"; } else { git_status = "-"; } } else { git_status = "-"; } cell_strings.push_back(git_status); cell_strings.push_back(a.description); } auto cells = to_cstr(cell_strings); table_view("##apps", headers, cols, cells.data(), static_cast(apps.size())); } void draw_analysis_list(const std::vector& analyses) { if (analyses.empty()) { empty_state("( no data )", "No analysis yet", "Use the + Add button above with kind = Analysis"); return; } const char* headers[] = {"Name", "Lang", "Domain", "Description"}; constexpr int cols = 4; std::vector cell_strings; cell_strings.reserve(analyses.size() * cols); for (auto& a : analyses) { cell_strings.push_back(a.name); cell_strings.push_back(a.lang); cell_strings.push_back(a.domain); cell_strings.push_back(a.description); } auto cells = to_cstr(cell_strings); table_view("##analysis", headers, cols, cells.data(), static_cast(analyses.size())); } void draw_types_list(const std::vector& types) { if (types.empty()) { empty_state("( no data )", "No types yet", "Types are indexed from the registry alongside functions"); return; } const char* headers[] = {"Name", "Lang", "Domain", "Algebraic", "Description"}; constexpr int cols = 5; std::vector cell_strings; cell_strings.reserve(types.size() * cols); for (auto& t : types) { cell_strings.push_back(t.name); cell_strings.push_back(t.lang); cell_strings.push_back(t.domain); cell_strings.push_back(t.algebraic); cell_strings.push_back(t.description); } auto cells = to_cstr(cell_strings); table_view("##types", headers, cols, cells.data(), static_cast(types.size())); } // --------------------------------------------------------------------------- // Monitor tab (issue 0086) — reads from ops:call_monitor. // Pestana principal del dashboard. Bucle reactivo: construir / ejecutar / // recopilar / analizar / mejorar lo vigila desde aqui. // --------------------------------------------------------------------------- static void draw_monitor_toolbar(RegistryData& data) { fn_ui::toolbar_begin(); // Window preset selector. Si cambia, marcamos reload_request para que // main.cpp recargue solo claude (no toca registry entero). ImGui::TextUnformatted("Window:"); ImGui::SameLine(); ImGui::SetNextItemWidth(110.0f); if (ImGui::BeginCombo("##monitor_window", kMonitorWindowLabels[g_monitor_window_idx])) { const int n = (int)(sizeof(kMonitorWindowLabels) / sizeof(kMonitorWindowLabels[0])); for (int i = 0; i < n; i++) { const bool selected = (i == g_monitor_window_idx); if (ImGui::Selectable(kMonitorWindowLabels[i], selected)) { if (i != g_monitor_window_idx) { g_monitor_window_idx = i; data.claude.window_secs = kMonitorWindowSecs[i]; g_monitor_reload_request = true; } } if (selected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } // Scatter axis window — separado de la ventana de KPIs. ImGui::SameLine(); ImGui::TextUnformatted("Scatter:"); ImGui::SameLine(); ImGui::SetNextItemWidth(90.0f); if (ImGui::BeginCombo("##scatter_window", kScatterWindowLabels[g_scatter_window_idx])) { const int n = (int)(sizeof(kScatterWindowLabels) / sizeof(kScatterWindowLabels[0])); for (int i = 0; i < n; i++) { const bool selected = (i == g_scatter_window_idx); if (ImGui::Selectable(kScatterWindowLabels[i], selected)) { g_scatter_window_idx = i; } if (selected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } ImGui::SameLine(); if (fn_ui::button("Refresh", fn_ui::ButtonVariant::Subtle)) { g_monitor_reload_request = true; } // Live LED: verde si WS conectado, gris si caido. Ts ultimo evento. ImGui::SameLine(); ImGui::Dummy(ImVec2(fn_tokens::spacing::lg, 0)); ImGui::SameLine(); const ImVec4 dot_col = g_monitor_ws_connected ? ImVec4(0.30f, 0.85f, 0.40f, 1.0f) : ImVec4(0.55f, 0.55f, 0.55f, 1.0f); ImGui::TextColored(dot_col, "%s", g_monitor_ws_connected ? TI_POINT : TI_CIRCLE_DOTTED); ImGui::SameLine(); ImGui::TextUnformatted(g_monitor_ws_connected ? "live" : "offline"); if (g_monitor_last_event_ts > 0) { ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); const std::string rel = format_ts_relative(g_monitor_last_event_ts); ImGui::Text("(last event: %s)", rel.c_str()); ImGui::PopStyleColor(); } fn_ui::toolbar_end(); } void draw_monitor(RegistryData& data) { auto& cu = data.claude; // Toolbar siempre visible (date filter + LED) incluso si call_monitor caido. draw_monitor_toolbar(data); ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm)); if (!cu.available) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); ImGui::TextWrapped("%s call_monitor.operations.db no esta accesible.", TI_ALERT_CIRCLE); ImGui::TextWrapped("Inicializa con: ./projects/fn_monitoring/apps/call_monitor/call_monitor init"); ImGui::TextWrapped("Despues `systemctl --user restart sqlite_api` para que el datasource ops:call_monitor sea descubierto."); ImGui::PopStyleColor(); return; } // 7 KPI cards: Calls / MCP / Reg% / Errors / Violations / Copies / Versions // "MCP" = calls Claude lanza via tools registry-aware (mcp / fn_cli_run / // heredoc). "Reg %" = porcentaje del total con function_id no vacio. const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX; if (ImGui::BeginTable("##monitor_kpi", 7, flags)) { struct KPI { const char* label; float value; const char* icon; const char* fmt; }; const KPI cards[7] = { {"Calls", static_cast(cu.total_calls), TI_ACTIVITY, "%.0f"}, {"MCP", static_cast(cu.total_mcp), TI_PLUG_CONNECTED, "%.0f"}, {"Reg %", static_cast(cu.registry_pct), TI_PERCENTAGE, "%.1f%%"}, {"Errors", static_cast(cu.total_errors), TI_ALERT_TRIANGLE, "%.0f"}, {"Violations", static_cast(cu.total_violations), TI_ALERT_CIRCLE, "%.0f"}, {"Copies", static_cast(cu.total_copies), TI_COPY, "%.0f"}, {"Versions", static_cast(cu.total_versions), TI_HISTORY, "%.0f"}, }; ImGui::TableNextRow(); for (int i = 0; i < 7; i++) { ImGui::TableSetColumnIndex(i); kpi_card(cards[i].label, cards[i].value, 0.0f, nullptr, 0, cards[i].fmt, cards[i].icon); } ImGui::EndTable(); } ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md)); // Live scatter — duracion (ms) por ejecucion, X = ts en tiempo real. // Ventana del eje X configurable via combo "Scatter" del toolbar (default // 5 min). Eje Y dinamico: 0..max_dentro_de_ventana + 500ms — asi siempre // hay headroom y el plot se reescala con picos sin perder la base. // ImPlot auto-scrollea fijando el limite derecho a `now` cada frame. // Dos series (ok/error) para colorear sin getter custom. { const double now = static_cast(std::time(nullptr)); const int win_s = kScatterWindowSecs[g_scatter_window_idx]; const double left = now - static_cast(win_s); // Reuse buffers entre frames; reset cada frame es O(N) sobre <=200 filas. static std::vector ok_x, ok_y, err_x, err_y; ok_x.clear(); ok_y.clear(); err_x.clear(); err_y.clear(); ok_x.reserve(cu.recent_executions.size()); ok_y.reserve(cu.recent_executions.size()); double y_max_in_window = 0.0; for (const auto& r : cu.recent_executions) { double x = static_cast(r.ts); double y = static_cast(r.duration_ms); // Solo considera y_max sobre puntos visibles para que el rescale // no se quede pillado en un pico antiguo fuera de ventana. if (x >= left && y > y_max_in_window) y_max_in_window = y; if (r.success) { ok_x.push_back(x); ok_y.push_back(y); } else { err_x.push_back(x); err_y.push_back(y); } } const double y_top = y_max_in_window + 500.0; // Forzar localtime en ImPlot — por defecto formatea como UTC y se // desincroniza con el resto de la app que usa hora local. ImPlot::GetStyle().UseLocalTime = true; if (ImPlot::BeginPlot("##monitor_scatter", ImVec2(-1, 200), ImPlotFlags_NoTitle | ImPlotFlags_NoMouseText)) { // NoHighlight evita el efecto de iluminacion del fondo del eje // cuando el cursor pasa por encima (era visualmente ruidoso). ImPlot::SetupAxis(ImAxis_X1, "time", ImPlotAxisFlags_NoGridLines | ImPlotAxisFlags_NoHighlight); ImPlot::SetupAxisScale(ImAxis_X1, ImPlotScale_Time); ImPlot::SetupAxisLimits(ImAxis_X1, left, now + 5.0, ImPlotCond_Always); ImPlot::SetupAxis(ImAxis_Y1, "duration (ms)", ImPlotAxisFlags_NoHighlight); ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0, y_top, ImPlotCond_Always); // Marcadores. Verde = ok, rojo = error. ImPlot v1.0+ usa ImPlotSpec // en lugar de SetNextMarkerStyle. if (!ok_x.empty()) { ImPlotSpec spec_ok; spec_ok.Marker = ImPlotMarker_Circle; spec_ok.MarkerSize = 4.0f; spec_ok.MarkerFillColor = ImVec4(0.30f, 0.85f, 0.40f, 0.85f); spec_ok.MarkerLineColor = ImVec4(0.30f, 0.85f, 0.40f, 1.0f); spec_ok.LineColor = ImVec4(0.30f, 0.85f, 0.40f, 1.0f); ImPlot::PlotScatter("ok", ok_x.data(), ok_y.data(), static_cast(ok_x.size()), spec_ok); } if (!err_x.empty()) { ImPlotSpec spec_err; spec_err.Marker = ImPlotMarker_Cross; spec_err.MarkerSize = 5.0f; spec_err.MarkerFillColor = ImVec4(0.95f, 0.35f, 0.30f, 0.95f); spec_err.MarkerLineColor = ImVec4(0.95f, 0.35f, 0.30f, 1.0f); spec_err.LineColor = ImVec4(0.95f, 0.35f, 0.30f, 1.0f); ImPlot::PlotScatter("error", err_x.data(), err_y.data(), static_cast(err_x.size()), spec_err); } // Hover tooltip: localiza el punto mas cercano al cursor (en // pixel-space, no plot-space, para que la tolerancia sea uniforme // sin importar la escala del eje Y) y muestra function/tool/ms. if (ImPlot::IsPlotHovered() && !cu.recent_executions.empty()) { const ImVec2 mp_px = ImGui::GetIO().MousePos; const double kHitRadiusPx = 14.0; double best_dist = kHitRadiusPx; const RecentExecutionRow* best = nullptr; for (const auto& r : cu.recent_executions) { double x = static_cast(r.ts); double y = static_cast(r.duration_ms); if (x < left || x > now + 5.0) continue; ImVec2 px = ImPlot::PlotToPixels(x, y); double dx = px.x - mp_px.x; double dy = px.y - mp_px.y; double d = std::sqrt(dx * dx + dy * dy); if (d < best_dist) { best_dist = d; best = &r; } } if (best != nullptr) { ImGui::BeginTooltip(); ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); ImGui::TextUnformatted(format_ts(best->ts).c_str()); ImGui::PopStyleColor(); ImGui::Separator(); ImGui::Text("Function: %s", best->function_id.empty() ? "-" : best->function_id.c_str()); ImGui::Text("Tool: %s", best->tool_used.c_str()); ImGui::Text("Duration: %d ms", best->duration_ms); if (!best->success) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error); ImGui::Text("Error: %s", best->error_class.c_str()); ImGui::PopStyleColor(); } ImGui::EndTooltip(); } } ImPlot::EndPlot(); } } ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md)); // Sub-tabs: Recent Executions (primera) / Top Functions / Violations / Copies if (ImGui::BeginTabBar("##monitor_sub_tabs")) { if (ImGui::BeginTabItem("Recent Executions")) { // Filtro: solo calls a funciones del registry (function_id no vacio). ImGui::Checkbox("Only registry functions", &g_recent_only_registry); ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); int total_shown = 0, total_all = 0; for (const auto& r : cu.recent_executions) { total_all++; if (!g_recent_only_registry || !r.function_id.empty()) total_shown++; } ImGui::Text("(%d/%d)", total_shown, total_all); ImGui::PopStyleColor(); ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs)); if (cu.recent_executions.empty()) { ImGui::TextDisabled("No executions in this window. Try widening (7d/30d/All) or wait for the next call."); } else { const ImGuiTableFlags tf = ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; if (ImGui::BeginTable("##monitor_recent", 6, tf, ImVec2(0, 0))) { ImGui::TableSetupColumn("When"); ImGui::TableSetupColumn("Function"); ImGui::TableSetupColumn("Tool"); ImGui::TableSetupColumn("ms"); ImGui::TableSetupColumn("OK"); ImGui::TableSetupColumn("Error"); ImGui::TableHeadersRow(); for (const auto& r : cu.recent_executions) { if (g_recent_only_registry && r.function_id.empty()) continue; ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(format_ts(r.ts).c_str()); ImGui::TableSetColumnIndex(1); if (!r.function_id.empty()) { // Call de registry — destacada en color normal. ImGui::TextUnformatted(r.function_id.c_str()); } else if (!r.command_snippet.empty()) { // Tool generica (Bash/heredoc): muestra prefijo `$` + snippet // truncado en muted para distinguirla visualmente de calls registry. ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); // Truncado visual a ~80 chars para no romper layout. char buf[88]; std::snprintf(buf, sizeof(buf), "$ %.80s%s", r.command_snippet.c_str(), r.command_snippet.size() > 80 ? "..." : ""); ImGui::TextUnformatted(buf); // Hover tooltip con snippet completo (hasta 200 chars). if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); ImGui::PushTextWrapPos(560.0f); ImGui::TextUnformatted(r.command_snippet.c_str()); ImGui::PopTextWrapPos(); ImGui::EndTooltip(); } ImGui::PopStyleColor(); } else { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); ImGui::TextUnformatted("-"); ImGui::PopStyleColor(); } ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(r.tool_used.c_str()); ImGui::TableSetColumnIndex(3); ImGui::Text("%d", r.duration_ms); ImGui::TableSetColumnIndex(4); if (r.success) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::success); ImGui::TextUnformatted(TI_CHECK); ImGui::PopStyleColor(); } else { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error); ImGui::TextUnformatted(TI_X); ImGui::PopStyleColor(); } ImGui::TableSetColumnIndex(5); ImGui::TextUnformatted(r.error_class.c_str()); } ImGui::EndTable(); } } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Top Functions")) { if (cu.top_functions.empty()) { ImGui::TextDisabled("No function calls recorded yet. Hook fires on next session."); } else { const ImGuiTableFlags tf = ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; if (ImGui::BeginTable("##monitor_top_fn", 6, tf, ImVec2(0, 0))) { ImGui::TableSetupColumn("Function ID"); ImGui::TableSetupColumn("Calls"); ImGui::TableSetupColumn("7d"); ImGui::TableSetupColumn("Errors"); ImGui::TableSetupColumn("Error %"); ImGui::TableSetupColumn("Mean ms"); ImGui::TableHeadersRow(); for (const auto& r : cu.top_functions) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(r.function_id.c_str()); ImGui::TableSetColumnIndex(1); ImGui::Text("%d", r.calls_total); ImGui::TableSetColumnIndex(2); ImGui::Text("%d", r.calls_7d); ImGui::TableSetColumnIndex(3); ImGui::Text("%d", r.errors_total); ImGui::TableSetColumnIndex(4); ImGui::Text("%.1f%%", r.error_rate * 100.0); ImGui::TableSetColumnIndex(5); ImGui::Text("%.0f", r.mean_duration_ms); } ImGui::EndTable(); } } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Violations")) { if (cu.recent_violations.empty()) { ImGui::TextDisabled("No antipattern violations detected."); } else { const ImGuiTableFlags tf = ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; if (ImGui::BeginTable("##monitor_viol", 5, tf, ImVec2(0, 0))) { ImGui::TableSetupColumn("When"); ImGui::TableSetupColumn("Rule"); ImGui::TableSetupColumn("Severity"); ImGui::TableSetupColumn("Function"); ImGui::TableSetupColumn("Snippet"); ImGui::TableHeadersRow(); for (const auto& r : cu.recent_violations) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(format_ts(r.ts).c_str()); ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(r.rule_id.c_str()); ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(r.severity.c_str()); ImGui::TableSetColumnIndex(3); ImGui::TextUnformatted(r.function_id.c_str()); ImGui::TableSetColumnIndex(4); ImGui::TextUnformatted(r.command_snippet.c_str()); } ImGui::EndTable(); } } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Failed Functions")) { // Subset de recent_executions: solo calls que golpean una funcion // del registry Y fallaron. Util para diagnostico: cuales funciones // del registry rompen mas + bug analysis cuando objetivo 1+2 caen. std::vector failed; for (const auto& r : cu.recent_executions) { if (!r.success && !r.function_id.empty()) failed.push_back(&r); } if (failed.empty()) { ImGui::TextDisabled("No registry-function failures in this window. Healthy."); } else { const ImGuiTableFlags tf = ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; if (ImGui::BeginTable("##monitor_failed_fns", 5, tf, ImVec2(0, 0))) { ImGui::TableSetupColumn("When"); ImGui::TableSetupColumn("Function"); ImGui::TableSetupColumn("Tool"); ImGui::TableSetupColumn("Error class"); ImGui::TableSetupColumn("Error snippet"); ImGui::TableHeadersRow(); for (const auto* p : failed) { const auto& r = *p; ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(format_ts(r.ts).c_str()); ImGui::TableSetColumnIndex(1); ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error); ImGui::TextUnformatted(r.function_id.c_str()); ImGui::PopStyleColor(); ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(r.tool_used.c_str()); ImGui::TableSetColumnIndex(3); ImGui::TextUnformatted(r.error_class.c_str()); ImGui::TableSetColumnIndex(4); if (r.error_snippet.empty()) { ImGui::TextDisabled("-"); } else { char buf[120]; std::snprintf(buf, sizeof(buf), "%.110s%s", r.error_snippet.c_str(), r.error_snippet.size() > 110 ? "..." : ""); ImGui::TextUnformatted(buf); if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); ImGui::PushTextWrapPos(560.0f); ImGui::TextUnformatted(r.error_snippet.c_str()); ImGui::PopTextWrapPos(); ImGui::EndTooltip(); } } } ImGui::EndTable(); } } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Copied Code")) { if (cu.copies.empty()) { ImGui::TextDisabled("No copied code detected. Run `fn doctor copied-code` or `call_monitor copied-code`."); } else { const ImGuiTableFlags tf = ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; if (ImGui::BeginTable("##monitor_copies", 5, tf, ImVec2(0, 0))) { ImGui::TableSetupColumn("Kind"); ImGui::TableSetupColumn("Sim"); ImGui::TableSetupColumn("App File"); ImGui::TableSetupColumn("App Function"); ImGui::TableSetupColumn("Registry ID"); ImGui::TableHeadersRow(); for (const auto& r : cu.copies) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(r.kind.c_str()); ImGui::TableSetColumnIndex(1); ImGui::Text("%.2f", r.similarity); ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(r.app_file.c_str()); ImGui::TableSetColumnIndex(3); ImGui::TextUnformatted(r.app_function.c_str()); ImGui::TableSetColumnIndex(4); ImGui::TextUnformatted(r.registry_id.c_str()); } ImGui::EndTable(); } } ImGui::EndTabItem(); } ImGui::EndTabBar(); } } // --------------------------------------------------------------------------- // Projects view // --------------------------------------------------------------------------- static std::string g_selected_project_id = ""; static ProjectDetail g_project_detail; static void refresh_project_detail() { g_project_detail = ProjectDetail{}; if (g_selected_project_id.empty() || g_api_url.empty()) return; load_project_detail_http(g_api_url, g_selected_project_id, g_project_detail); } void draw_projects_list(RegistryData& data) { if (data.projects.empty() && data.orphan_apps == 0 && data.orphan_analyses == 0 && data.orphan_vaults == 0) { empty_state("( no data )", "No projects yet", "Create a project under projects/{name}/ with project.md and reindex"); return; } // Dos columnas: izquierda arbol, derecha detalle. const ImGuiTableFlags flags = ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp; if (!ImGui::BeginTable("##proj_layout", 2, flags)) return; ImGui::TableSetupColumn("tree", ImGuiTableColumnFlags_WidthStretch, 1.0f); ImGui::TableSetupColumn("detail", ImGuiTableColumnFlags_WidthStretch, 2.5f); ImGui::TableNextRow(); // --- Left column: tree --- ImGui::TableSetColumnIndex(0); ImGui::BeginChild("##proj_tree", ImVec2(0, 0), ImGuiChildFlags_Borders); for (const auto& p : data.projects) { bool sel = (g_selected_project_id == p.id); char label[256]; std::snprintf(label, sizeof(label), "%s [%d/%d/%d]", p.name.c_str(), p.apps_count, p.analyses_count, p.vaults_count); fn_ui::tree_leaf(p.id.c_str(), label, sel); if (fn_ui::tree_node_clicked()) { g_selected_project_id = p.id; refresh_project_detail(); } } // Orphans if (data.orphan_apps + data.orphan_analyses + data.orphan_vaults > 0) { bool sel = (g_selected_project_id == "orphans"); char label[128]; std::snprintf(label, sizeof(label), "(orphans) [%d/%d/%d]", data.orphan_apps, data.orphan_analyses, data.orphan_vaults); fn_ui::tree_leaf("orphans", label, sel); if (fn_ui::tree_node_clicked()) { g_selected_project_id = "orphans"; refresh_project_detail(); } } ImGui::EndChild(); // --- Right column: detail --- ImGui::TableSetColumnIndex(1); ImGui::BeginChild("##proj_detail", ImVec2(0, 0), ImGuiChildFlags_Borders); if (g_selected_project_id.empty()) { empty_state("\xe2\x86\x90", "Select a project", "Click a project on the left to see its apps, analyses and vaults"); } else { const auto& d = g_project_detail; ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text); if (!d.name.empty()) ImGui::TextUnformatted(d.name.c_str()); else ImGui::TextUnformatted(g_selected_project_id.c_str()); ImGui::PopStyleColor(); if (!d.description.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); ImGui::TextWrapped("%s", d.description.c_str()); ImGui::PopStyleColor(); } ImGui::Separator(); if (ImGui::BeginTabBar("##proj_tabs")) { char tab_apps[64]; std::snprintf(tab_apps, sizeof(tab_apps), "Apps (%zu)", d.apps.size()); char tab_analyses[64]; std::snprintf(tab_analyses, sizeof(tab_analyses), "Analysis (%zu)", d.analyses.size()); char tab_vaults[64]; std::snprintf(tab_vaults, sizeof(tab_vaults), "Vaults (%zu)", d.vaults.size()); if (ImGui::BeginTabItem(tab_apps)) { draw_apps_list(d.apps); ImGui::EndTabItem(); } if (ImGui::BeginTabItem(tab_analyses)) { draw_analysis_list(d.analyses); ImGui::EndTabItem(); } if (ImGui::BeginTabItem(tab_vaults)) { if (d.vaults.empty()) { empty_state("( no data )", "No vaults in this project", "Use the + Add button with kind = Vault (requires a project)"); } else { const char* headers[] = {"Name", "Path", "Symlink", "Description"}; std::vector cells; cells.reserve(d.vaults.size() * 4); for (auto& v : d.vaults) { cells.push_back(v.name); cells.push_back(v.path); cells.push_back(v.symlink ? "yes" : "no"); cells.push_back(v.description); } auto cp = to_cstr(cells); table_view("##vaults", headers, 4, cp.data(), static_cast(d.vaults.size())); } ImGui::EndTabItem(); } ImGui::EndTabBar(); } } ImGui::EndChild(); ImGui::EndTable(); } // --------------------------------------------------------------------------- // Actions bar (Reindex + Add button) + Add modal // --------------------------------------------------------------------------- static const char* kLangs[] = {"go", "py", "ts", "sh", "cpp"}; static const char* kDomains[] = {"core", "infra", "finance", "datascience", "cybersecurity", "shell", "tui", "pipelines", "browser", "viz", "gfx", "notebook"}; static void submit_add() { if (g_api_url.empty()) { fn_ui::toast_push(fn_ui::ToastKind::Error, "API URL not set"); return; } std::string name = g_add_name; std::string desc = g_add_desc; std::string project; if (g_add_project_idx >= 0) { // g_add_project_idx indexa la lista live de projects (resolved en el modal) // guardada en g_project_ids_cache — ver draw_add_modal } // Resolvemos project en draw_add_modal y lo pasamos via g_add_project_resolved extern std::string g_add_project_resolved; project = g_add_project_resolved; AddKind kind = static_cast(g_add_kind_idx); std::string url = g_api_url; fn_ui::runner_trigger(g_add_runner, [kind, url, name, desc, project, lang_idx = g_add_lang_idx, domain_idx = g_add_domain_idx, packages = std::string(g_add_packages), vault_path = std::string(g_add_vault_path) ](std::string& out) -> bool { std::string body; bool ok = false; switch (kind) { case AddKind::App: ok = http_post_add_app(url, name, kLangs[lang_idx], kDomains[domain_idx], project, desc, body); break; case AddKind::Analysis: ok = http_post_add_analysis(url, name, project, packages, desc, body); break; case AddKind::Vault: ok = http_post_add_vault(url, name, project, vault_path, desc, body); break; } out = body; return ok; }); } std::string g_add_project_resolved = ""; static void draw_add_modal(RegistryData& data) { if (!fn_ui::modal_dialog_begin("Add...", &g_show_add, ImVec2(460, 0))) { fn_ui::modal_dialog_end(); return; } // Kind selector const char* kinds[] = {"App", "Analysis", "Vault"}; fn_ui::select("Kind", &g_add_kind_idx, kinds, 3); // Project selector (del registro vivo) std::vector proj_labels; std::vector proj_ids; for (auto& p : data.projects) { proj_labels.push_back(p.name); proj_ids.push_back(p.id); } std::vector proj_cstr; for (auto& s : proj_labels) proj_cstr.push_back(s.c_str()); AddKind kind = static_cast(g_add_kind_idx); fn_ui::select("Project", &g_add_project_idx, proj_cstr.data(), static_cast(proj_cstr.size()), kind != AddKind::Vault /* vault obliga a project */); g_add_project_resolved = (g_add_project_idx >= 0 && g_add_project_idx < (int)proj_ids.size()) ? proj_ids[g_add_project_idx] : ""; fn_ui::text_input("Name", g_add_name, sizeof(g_add_name), "snake_case, a-z0-9_"); fn_ui::text_input("Description", g_add_desc, sizeof(g_add_desc)); // Campos especificos segun kind if (kind == AddKind::App) { fn_ui::select("Lang", &g_add_lang_idx, kLangs, 5); fn_ui::select("Domain", &g_add_domain_idx, kDomains, 12); } else if (kind == AddKind::Analysis) { fn_ui::text_input("Packages (CSV)", g_add_packages, sizeof(g_add_packages), "polars,scikit-learn,torch"); } else if (kind == AddKind::Vault) { fn_ui::text_input("Path (abs, opcional)", g_add_vault_path, sizeof(g_add_vault_path), "/home/lucas/vaults/my_data"); } ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm)); // Status del runner en curso fn_ui::runner_status(g_add_runner, "Creating..."); // Detectar transicion running->done para cerrar y notificar static fn_ui::RunnerState last_state = fn_ui::RunnerState::Idle; fn_ui::RunnerState now = g_add_runner.state(); if (last_state == fn_ui::RunnerState::Running && (now == fn_ui::RunnerState::Success || now == fn_ui::RunnerState::Error)) { const bool ok = (now == fn_ui::RunnerState::Success); fn_ui::toast_push(ok ? fn_ui::ToastKind::Success : fn_ui::ToastKind::Error, ok ? "Created OK — reloading" : g_add_runner.message().c_str()); if (ok) { g_show_add = false; g_add_name[0] = '\0'; g_add_desc[0] = '\0'; g_add_packages[0] = '\0'; g_add_vault_path[0] = '\0'; trigger_reload(); } } last_state = now; ImGui::Separator(); if (fn_ui::button("Cancel", fn_ui::ButtonVariant::Subtle)) g_show_add = false; ImGui::SameLine(); const bool disabled = g_add_runner.is_busy() || g_add_name[0] == '\0'; if (disabled) ImGui::BeginDisabled(); if (fn_ui::button("Create", fn_ui::ButtonVariant::Primary)) { submit_add(); } if (disabled) ImGui::EndDisabled(); fn_ui::modal_dialog_end(); } static void draw_actions_bar() { if (g_api_url.empty()) return; fn_ui::toolbar_begin(); if (fn_ui::button("Reindex", fn_ui::ButtonVariant::Primary) && !g_reindex_runner.is_busy()) { const std::string url = g_api_url; fn_ui::runner_trigger(g_reindex_runner, [url](std::string& out) -> bool { return http_post_reindex(url, out); }); } ImGui::SameLine(); if (fn_ui::button("+ Add", fn_ui::ButtonVariant::Secondary)) { g_show_add = true; g_add_runner.reset(); } ImGui::SameLine(); if (fn_ui::button("Reload", fn_ui::ButtonVariant::Subtle)) { trigger_reload(); } ImGui::SameLine(); fn_ui::toast_inbox_button("##inbox"); fn_ui::toolbar_end(); // Status del reindex debajo del toolbar static fn_ui::RunnerState last_reindex_state = fn_ui::RunnerState::Idle; fn_ui::RunnerState now = g_reindex_runner.state(); if (now != fn_ui::RunnerState::Idle) { fn_ui::runner_status(g_reindex_runner, "Reindexing..."); } if (last_reindex_state == fn_ui::RunnerState::Running && (now == fn_ui::RunnerState::Success || now == fn_ui::RunnerState::Error)) { const bool ok = (now == fn_ui::RunnerState::Success); fn_ui::toast_push(ok ? fn_ui::ToastKind::Success : fn_ui::ToastKind::Error, g_reindex_runner.message().c_str()); if (ok) trigger_reload(); } last_reindex_state = now; } // --------------------------------------------------------------------------- // Functions Explorer // --------------------------------------------------------------------------- // Pestana con dos columnas: a la izquierda lista filtrable de funciones del // registry; a la derecha codigo + documentacion + metadata de la funcion // seleccionada. La lista se carga en el primer render (lazy) y se cachea — // el boton "Reload" del toolbar fuerza recarga al disparar trigger_reload // (que pone g_explorer_loaded = false). static const char* kExplorerLangs[] = {"all", "go", "py", "ts", "sh", "cpp"}; static const char* kExplorerDomains[] = {"all", "core", "infra", "finance", "datascience", "cybersecurity", "shell", "tui", "pipelines", "browser", "viz", "gfx", "notebook"}; static bool str_contains_ci(const std::string& haystack, const char* needle) { if (!needle || !*needle) return true; std::string h = haystack; std::string n = needle; for (auto& c : h) c = static_cast(std::tolower(static_cast(c))); for (auto& c : n) c = static_cast(std::tolower(static_cast(c))); return h.find(n) != std::string::npos; } static void explorer_select(const std::string& id) { g_explorer_selected_id = id; g_explorer_detail = FunctionDetail{}; g_explorer_tests.clear(); g_explorer_test_idx = 0; if (!g_api_url.empty() && !id.empty()) { load_function_detail_http(g_api_url, id, g_explorer_detail); load_unit_tests_http(g_api_url, id, g_explorer_tests); } } static void explorer_reload_list() { g_explorer_funcs.clear(); g_explorer_loaded = false; if (g_api_url.empty()) return; if (load_all_functions_http(g_api_url, g_explorer_funcs)) { g_explorer_loaded = true; } } void draw_functions_explorer() { if (!g_explorer_loaded) { explorer_reload_list(); } if (g_explorer_funcs.empty()) { empty_state("( no data )", "No functions to explore", "Run 'fn index' or check the API connection"); return; } const ImGuiTableFlags flags = ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp; if (!ImGui::BeginTable("##explorer_layout", 2, flags)) return; ImGui::TableSetupColumn("list", ImGuiTableColumnFlags_WidthStretch, 1.0f); ImGui::TableSetupColumn("detail", ImGuiTableColumnFlags_WidthStretch, 2.4f); ImGui::TableNextRow(); // --- Left: filter + list --- ImGui::TableSetColumnIndex(0); ImGui::BeginChild("##explorer_left", ImVec2(0, 0), ImGuiChildFlags_Borders); fn_ui::text_input("Search", g_explorer_filter, sizeof(g_explorer_filter), "name or description"); ImGui::PushItemWidth(120.0f); ImGui::Combo("##lang", &g_explorer_lang_idx, kExplorerLangs, IM_ARRAYSIZE(kExplorerLangs)); ImGui::SameLine(); ImGui::Combo("##domain", &g_explorer_domain_idx, kExplorerDomains, IM_ARRAYSIZE(kExplorerDomains)); ImGui::PopItemWidth(); ImGui::Spacing(); // Conteo de resultados visibles + lista int visible = 0; ImGui::BeginChild("##explorer_list", ImVec2(0, 0)); for (const auto& f : g_explorer_funcs) { if (g_explorer_lang_idx > 0 && f.lang != kExplorerLangs[g_explorer_lang_idx]) continue; if (g_explorer_domain_idx > 0 && f.domain != kExplorerDomains[g_explorer_domain_idx]) continue; if (g_explorer_filter[0] != '\0' && !str_contains_ci(f.name, g_explorer_filter) && !str_contains_ci(f.description, g_explorer_filter)) continue; visible++; bool sel = (g_explorer_selected_id == f.id); char label[256]; std::snprintf(label, sizeof(label), "%s [%s/%s]", f.name.c_str(), f.lang.c_str(), f.domain.c_str()); if (ImGui::Selectable(label, sel, ImGuiSelectableFlags_AllowOverlap)) { explorer_select(f.id); } } ImGui::EndChild(); (void)visible; ImGui::EndChild(); // --- Right: detail --- ImGui::TableSetColumnIndex(1); ImGui::BeginChild("##explorer_right", ImVec2(0, 0), ImGuiChildFlags_Borders); if (g_explorer_selected_id.empty()) { empty_state("\xe2\x86\x90", "Select a function", "Click a function on the left to see its code and documentation"); } else { const auto& d = g_explorer_detail; // Header: nombre + signature ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text); ImGui::TextUnformatted(d.name.empty() ? g_explorer_selected_id.c_str() : d.name.c_str()); ImGui::PopStyleColor(); // Badges meta (lang/domain/kind/purity/tested) ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); ImGui::Text("%s · %s · %s · %s%s", d.lang.c_str(), d.domain.c_str(), d.kind.c_str(), d.purity.c_str(), d.tested ? " · tested" : ""); ImGui::PopStyleColor(); if (!d.description.empty()) { ImGui::Spacing(); fn_ui::selectable_text_wrapped(d.description.c_str()); } if (!d.signature.empty()) { ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); ImGui::TextUnformatted("Signature:"); ImGui::PopStyleColor(); fn_ui::selectable_text_wrapped(d.signature.c_str()); } ImGui::Separator(); // Tabs Code / Documentation / Notes / Example if (ImGui::BeginTabBar("##fn_detail_tabs")) { if (ImGui::BeginTabItem("Code")) { if (d.code.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim); ImGui::TextUnformatted("(no code stored)"); ImGui::PopStyleColor(); } else { // InputTextMultiline read-only — permite seleccion y copia, // y maneja scroll horizontal/vertical para codigo largo. ImVec2 sz = ImGui::GetContentRegionAvail(); ImGui::InputTextMultiline("##code", const_cast(d.code.c_str()), d.code.size() + 1, sz, ImGuiInputTextFlags_ReadOnly); } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Documentation")) { if (d.documentation.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim); ImGui::TextUnformatted("(no documentation)"); ImGui::PopStyleColor(); } else { ImGui::BeginChild("##doc_scroll", ImVec2(0, 0)); fn_ui::selectable_text_wrapped(d.documentation.c_str()); ImGui::EndChild(); } ImGui::EndTabItem(); } // Tests: caja con dropdown si hay varios + visor del codigo del // test seleccionado. Nombre del tab incluye el conteo para que se // vea de un vistazo si la funcion tiene tests o no. char tests_tab_label[32]; std::snprintf(tests_tab_label, sizeof(tests_tab_label), "Tests (%zu)", g_explorer_tests.size()); if (ImGui::BeginTabItem(tests_tab_label)) { if (g_explorer_tests.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim); ImGui::TextUnformatted("(no unit tests for this function)"); ImGui::PopStyleColor(); } else { // Combo para elegir entre los varios tests (la mayoria // de funciones solo tienen 1, pero algunas tienen varios // ficheros de tests). if (g_explorer_test_idx >= (int)g_explorer_tests.size()) g_explorer_test_idx = 0; std::vector names; names.reserve(g_explorer_tests.size()); for (auto& t : g_explorer_tests) names.push_back(t.name); std::vector name_cstr; for (auto& n : names) name_cstr.push_back(n.c_str()); ImGui::PushItemWidth(-FLT_MIN); ImGui::Combo("##test_select", &g_explorer_test_idx, name_cstr.data(), static_cast(name_cstr.size())); ImGui::PopItemWidth(); const auto& t = g_explorer_tests[g_explorer_test_idx]; ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); ImGui::Text("%s · %s", t.lang.c_str(), t.file_path.empty() ? "(no path)" : t.file_path.c_str()); ImGui::PopStyleColor(); ImGui::Separator(); if (t.code.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim); ImGui::TextUnformatted("(no code stored for this test)"); ImGui::PopStyleColor(); } else { ImVec2 sz = ImGui::GetContentRegionAvail(); ImGui::InputTextMultiline("##test_code", const_cast(t.code.c_str()), t.code.size() + 1, sz, ImGuiInputTextFlags_ReadOnly); } } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Metadata")) { ImGui::BeginChild("##meta_scroll", ImVec2(0, 0)); auto kv = [](const char* k, const std::string& v) { if (v.empty()) return; ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); ImGui::TextUnformatted(k); ImGui::PopStyleColor(); fn_ui::selectable_text_wrapped(v.c_str()); ImGui::Spacing(); }; kv("ID:", d.id); kv("Version:", d.version); kv("File path:", d.file_path); kv("Created:", d.created_at); kv("Returns:", d.returns); kv("Error type:", d.error_type); kv("Uses functions:", d.uses_functions); kv("Uses types:", d.uses_types); kv("Params schema:", d.params_schema); kv("Example:", d.example); kv("Notes:", d.notes); ImGui::EndChild(); ImGui::EndTabItem(); } ImGui::EndTabBar(); } } ImGui::EndChild(); ImGui::EndTable(); } // --------------------------------------------------------------------------- // Main draw // --------------------------------------------------------------------------- void draw_dashboard(RegistryData& data) { // Tema aplicado por fn::run_app() (app_base.h, ThemeMode::FnDark default). // FPS overlay lo dibuja app_base.cpp segun settings().show_fps — no llamarlo aqui. fullscreen_window_begin("##dashboard"); char subtitle[128]; std::snprintf(subtitle, sizeof(subtitle), "%d functions · %d types · %d apps · %d analyses · %zu projects", data.stats.total_functions, data.stats.total_types, data.stats.total_apps, data.stats.total_analysis, data.projects.size()); page_header_begin("fn_registry Dashboard", subtitle); page_header_end(); draw_actions_bar(); ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm)); // Navegacion top-level: "Monitor" es la primera y por defecto (issue 0086). // El bucle reactivo (construir / ejecutar / recopilar / analizar / mejorar) // se vigila desde alli, asi que pega como landing. Las demas son vistas // dedicadas a entidades del registry. if (ImGui::BeginTabBar("##main_tabs", ImGuiTabBarFlags_FittingPolicyScroll)) { if (ImGui::BeginTabItem("Monitor")) { draw_monitor(data); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Dashboard")) { draw_kpi_row(data); ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md)); // Altura del bloque de charts proporcional al espacio disponible: // ~32% del espacio restante despues de KPIs (clamped a [200, 360]). const float remaining_h = ImGui::GetContentRegionAvail().y; float chart_h = remaining_h * 0.40f; if (chart_h < 200.0f) chart_h = 200.0f; if (chart_h > 360.0f) chart_h = 360.0f; draw_charts(data, chart_h); ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md)); // Tabla de funciones recientes para que el Dashboard tenga algo // accionable abajo sin tener que cambiar de pestana. ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); ImGui::TextUnformatted("Recent Functions"); ImGui::PopStyleColor(); draw_recent_functions(data.recent_funcs); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Explorer")) { draw_functions_explorer(); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Projects")) { draw_projects_list(data); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Apps")) { draw_apps_list(data.apps); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Analysis")) { draw_analysis_list(data.analyses); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Types")) { draw_types_list(data.types); ImGui::EndTabItem(); } ImGui::EndTabBar(); } draw_add_modal(data); fullscreen_window_end(); // Toasts encima de todo fn_ui::toast_render(); }