diff --git a/data.h b/data.h index 1a2693b..1c91015 100644 --- a/data.h +++ b/data.h @@ -176,6 +176,8 @@ struct RecentExecutionRow { int duration_ms = 0; bool success = true; std::string error_class; + std::string error_snippet; // texto de error si success=false + std::string command_snippet; // raw command (truncado/redactado) cuando function_id vacio std::string session_id; }; @@ -186,6 +188,12 @@ struct ClaudeUsageData { int total_violations = 0; int total_copies = 0; int total_versions = 0; + // Calls que Claude lanza via tools registry-aware: MCP del registry + // (mcp), `fn run` (fn_cli_run) o heredocs python. Excluye bash plano. + int total_mcp = 0; + // % de filas en la ventana con function_id != '' — es decir, calls que + // golpean una funcion registrada. Indicador clave de adopcion del registry. + double registry_pct = 0.0; std::vector top_functions; // top 20 by calls_total std::vector recent_violations; // last 20 std::vector copies; diff --git a/data_http.cpp b/data_http.cpp index 4d6ed03..4c3bb9d 100644 --- a/data_http.cpp +++ b/data_http.cpp @@ -612,12 +612,38 @@ bool load_claude_usage_http(const std::string& api_url, RegistryData& out, const std::string sql_viol = "SELECT COUNT(*) FROM violations" + wf_viol; out.claude.total_violations = extract_int(call_monitor_query(cli, sql_viol.c_str())); } + // MCP / fn run / heredoc — herramientas registry-aware. Cubre las + // variantes vistas en produccion: mcp, mcp_fn_search, mcp_fn_run, + // fn_cli_run, fn_run_cli, heredoc, heredoc_py. + static const char* kRegistryAwareToolCond = + "(tool_used LIKE 'mcp%' OR tool_used LIKE 'heredoc%' " + "OR tool_used IN ('fn_cli_run','fn_run_cli'))"; + { + const std::string wf_and = wf_calls.empty() + ? std::string(" WHERE ") + kRegistryAwareToolCond + : std::string(wf_calls + " AND " + kRegistryAwareToolCond); + const std::string sql = "SELECT COUNT(*) FROM calls" + wf_and; + out.claude.total_mcp = extract_int(call_monitor_query(cli, sql.c_str())); + } + // % calls que llamaron a una funcion del registry (function_id no vacio). + { + const std::string wf_and = wf_calls.empty() + ? std::string(" WHERE function_id != '' ") + : std::string(wf_calls + " AND function_id != '' "); + const std::string sql = "SELECT COUNT(*) FROM calls" + wf_and; + int reg_hits = extract_int(call_monitor_query(cli, sql.c_str())); + out.claude.registry_pct = (out.claude.total_calls > 0) + ? 100.0 * static_cast(reg_hits) / static_cast(out.claude.total_calls) + : 0.0; + } out.claude.total_copies = extract_int(call_monitor_query(cli, "SELECT COUNT(*) FROM copied_code")); out.claude.total_versions = extract_int(call_monitor_query(cli, "SELECT COUNT(*) FROM function_versions")); // Recent executions (calls table) ordenada por ts DESC { - std::string sql = "SELECT id, ts, function_id, tool_used, duration_ms, success, error_class, session_id " + std::string sql = "SELECT id, ts, function_id, tool_used, duration_ms, success, error_class, session_id, " + "COALESCE(command_snippet,'') AS command_snippet, " + "COALESCE(error_snippet,'') AS error_snippet " "FROM calls" + wf_calls + " ORDER BY ts DESC LIMIT 100"; json rx = call_monitor_query(cli, sql.c_str()); if (rx.is_object() && rx.contains("rows")) { @@ -630,8 +656,10 @@ bool load_claude_usage_http(const std::string& api_url, RegistryData& out, row.tool_used = extract_str(r, 3); row.duration_ms = extract_row_int(r, 4); row.success = extract_row_int(r, 5) != 0; - row.error_class = extract_str(r, 6); - row.session_id = extract_str(r, 7); + row.error_class = extract_str(r, 6); + row.session_id = extract_str(r, 7); + row.command_snippet = extract_str(r, 8); + row.error_snippet = extract_str(r, 9); if (row.id > mx) mx = row.id; out.claude.recent_executions.push_back(row); } diff --git a/main.cpp b/main.cpp index 887da31..4222e86 100644 --- a/main.cpp +++ b/main.cpp @@ -86,6 +86,8 @@ static bool apply_ws_message(const std::string& raw) { std::vector incoming; incoming.reserve(msg["calls"].size()); int new_errors = 0; + int new_mcp = 0; + int new_reg_hits = 0; for (const auto& c : msg["calls"]) { RecentExecutionRow row; row.id = c.value("id", 0LL); @@ -94,15 +96,39 @@ static bool apply_ws_message(const std::string& raw) { row.tool_used = c.value("tool_used", ""); row.duration_ms = c.value("duration_ms", 0); row.success = c.value("success", true); - row.error_class = c.value("error_class", ""); - row.session_id = c.value("session_id", ""); + row.error_class = c.value("error_class", ""); + row.error_snippet = c.value("error_snippet", ""); + row.command_snippet = c.value("command_snippet", ""); + row.session_id = c.value("session_id", ""); if (!row.success) new_errors++; + // Registry-aware tools: mcp*, heredoc*, fn_cli_run, fn_run_cli. + const auto starts_with = [](const std::string& s, const char* p) { + size_t lp = std::strlen(p); + return s.size() >= lp && std::memcmp(s.c_str(), p, lp) == 0; + }; + if (starts_with(row.tool_used, "mcp") || + starts_with(row.tool_used, "heredoc") || + row.tool_used == "fn_cli_run" || + row.tool_used == "fn_run_cli") { + new_mcp++; + } + if (!row.function_id.empty()) new_reg_hits++; incoming.push_back(std::move(row)); } if (type == "delta") { g_data.claude.total_calls += static_cast(incoming.size()); g_data.claude.total_errors += new_errors; + g_data.claude.total_mcp += new_mcp; + // registry_pct se recalcula sobre el total acumulado tras el delta. + // Aproximacion: prev_pct * prev_total + new_reg_hits / new_total. + int prev_total = g_data.claude.total_calls - static_cast(incoming.size()); + int prev_hits = static_cast(g_data.claude.registry_pct * + static_cast(prev_total) / 100.0 + 0.5); + int total_hits = prev_hits + new_reg_hits; + g_data.claude.registry_pct = (g_data.claude.total_calls > 0) + ? 100.0 * static_cast(total_hits) / static_cast(g_data.claude.total_calls) + : 0.0; } // Prepend (newer al frente). Para delta: filas vienen ASC del server, diff --git a/views.cpp b/views.cpp index 66e0fac..3ab1f88 100644 --- a/views.cpp +++ b/views.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -53,6 +54,14 @@ 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; @@ -393,6 +402,23 @@ static void draw_monitor_toolbar(RegistryData& data) { 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; @@ -435,29 +461,159 @@ void draw_monitor(RegistryData& data) { return; } - // 5 KPI cards: Calls / Errors / Violations / Copies / Versions + // 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", 5, flags)) { - struct KPI { const char* label; float value; const char* icon; }; - const KPI cards[5] = { - {"Calls", static_cast(cu.total_calls), TI_ACTIVITY}, - {"Errors", static_cast(cu.total_errors), TI_ALERT_TRIANGLE}, - {"Violations", static_cast(cu.total_violations), TI_ALERT_CIRCLE}, - {"Copies", static_cast(cu.total_copies), TI_COPY}, - {"Versions", static_cast(cu.total_versions), TI_HISTORY}, + 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 < 5; i++) { + for (int i = 0; i < 7; i++) { ImGui::TableSetColumnIndex(i); - kpi_card(cards[i].label, cards[i].value, 0.0f, nullptr, 0, "%.0f", cards[i].icon); + 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 { @@ -472,11 +628,38 @@ void draw_monitor(RegistryData& data) { 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); - ImGui::TextUnformatted(r.function_id.empty() ? "-" : r.function_id.c_str()); + 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); @@ -553,6 +736,59 @@ void draw_monitor(RegistryData& data) { } 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`.");