// Panels v1+v2: // - Browsers : scan + spawn + kill + seleccion (click selecciona instancia) // - Tabs : lista pestañas via CDP HTTP /json + Focus/Close/New/Select // - Tab Detail : Runtime.evaluate REPL minimo (placeholder upgradeable) // - Network : panel DevTools-like — tabla + filtros + detalle por tab // // Estado cross-panel via g_session() (session_state.h). #include "imgui.h" #include "implot.h" #include "core/icons_tabler.h" #include "core/tokens.h" #include "chrome_scanner.h" #include "chrome_launcher.h" #include "local_api.h" #include "cdp_http.h" #include "session_state.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 # define WIN32_LEAN_AND_MEAN # include #endif namespace navegator { // =========================================================================== // Browsers panel // =========================================================================== namespace { struct BrowsersState { std::mutex mu; std::vector instances; std::chrono::steady_clock::time_point last_scan; std::atomic scanning{false}; std::atomic ever_scanned{false}; char new_profile[128] = "default"; int new_port = 19222; bool new_headless = false; std::string last_error; }; BrowsersState g_browsers; void rescan_async() { if (g_browsers.scanning.exchange(true)) return; std::thread([]{ auto v = scan_chrome_instances(); { std::lock_guard lk(g_browsers.mu); g_browsers.instances = std::move(v); g_browsers.last_scan = std::chrono::steady_clock::now(); } g_browsers.ever_scanned.store(true); g_browsers.scanning.store(false); }).detach(); } std::string default_user_data_dir(const std::string& profile) { #ifdef _WIN32 char buf[MAX_PATH] = {0}; DWORD n = GetEnvironmentVariableA("USERPROFILE", buf, sizeof(buf)); std::string base = (n > 0 && n < sizeof(buf)) ? buf : "C:\\Users\\Public"; return base + "\\AppData\\Local\\navegator_profiles\\" + profile; #else return std::string("/tmp/navegator_profiles/") + profile; #endif } } // anon void render_browsers_panel(bool* p_open) { if (!ImGui::Begin(TI_BROWSER " Browsers", p_open)) { ImGui::End(); return; } auto now = std::chrono::steady_clock::now(); { std::lock_guard lk(g_browsers.mu); if (now - g_browsers.last_scan > std::chrono::seconds(2) && !g_browsers.scanning.load()) { rescan_async(); } } if (g_api_running.load()) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::success); ImGui::Text("API: 127.0.0.1:%d", g_api_port.load()); ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::TextDisabled("(reqs: %d)", g_api_request_count.load()); } else { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error); ImGui::TextUnformatted("API: down"); ImGui::PopStyleColor(); } ImGui::Separator(); if (ImGui::Button(TI_REFRESH " Rescan")) rescan_async(); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); ImGui::TextUnformatted("New profile:"); ImGui::SameLine(); ImGui::SetNextItemWidth(160); ImGui::InputText("##profile", g_browsers.new_profile, sizeof(g_browsers.new_profile)); ImGui::SameLine(); ImGui::TextUnformatted("Port:"); ImGui::SameLine(); ImGui::SetNextItemWidth(80); ImGui::InputInt("##port", &g_browsers.new_port, 0, 0); ImGui::SameLine(); ImGui::Checkbox("Headless", &g_browsers.new_headless); ImGui::SameLine(); if (ImGui::Button(TI_PLAYER_PLAY " Launch")) { LaunchOpts o; o.port = g_browsers.new_port; o.headless = g_browsers.new_headless; std::string profile = g_browsers.new_profile; if (profile.empty()) profile = "default"; o.user_data_dir = default_user_data_dir(profile); auto r = launch_chrome(o); g_browsers.last_error = r.ok ? "" : r.error; std::thread([]{ std::this_thread::sleep_for(std::chrono::milliseconds(800)); rescan_async(); }).detach(); } ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); if (ImGui::Button(TI_X " Kill all navegator")) { kill_chromes_by_userdata("navegator_profiles"); std::thread([]{ std::this_thread::sleep_for(std::chrono::milliseconds(500)); rescan_async(); }).detach(); } if (!g_browsers.last_error.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error); ImGui::TextWrapped("Error: %s", g_browsers.last_error.c_str()); ImGui::PopStyleColor(); } ImGui::Separator(); int sel_port = 0; { std::lock_guard lk(g_session().mu); sel_port = g_session().selected_port; } std::lock_guard lk(g_browsers.mu); if (!g_browsers.ever_scanned.load() && g_browsers.instances.empty()) { ImGui::TextUnformatted("Scanning..."); } else if (g_browsers.instances.empty()) { ImGui::TextDisabled("No Chrome instances with --remote-debugging-port detected."); ImGui::TextDisabled("Lanza una con el formulario de arriba o con scripts/start.sh."); } else { const ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_Sortable; if (ImGui::BeginTable("##browsers", 7, flags)) { ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 22); ImGui::TableSetupColumn("PID", ImGuiTableColumnFlags_WidthFixed, 64); ImGui::TableSetupColumn("Port", ImGuiTableColumnFlags_WidthFixed, 64); ImGui::TableSetupColumn("Profile"); ImGui::TableSetupColumn("Mode", ImGuiTableColumnFlags_WidthFixed, 80); ImGui::TableSetupColumn("user-data-dir"); ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 130); ImGui::TableHeadersRow(); int idx = 0; for (const auto& inst : g_browsers.instances) { ImGui::TableNextRow(); ImGui::PushID(idx); ImGui::TableNextColumn(); bool is_sel = (sel_port == inst.port); if (is_sel) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::primary); ImGui::TextUnformatted(TI_CHECK); ImGui::PopStyleColor(); } else { ImGui::TextUnformatted(""); } ImGui::TableNextColumn(); ImGui::Text("%u", inst.pid); ImGui::TableNextColumn(); ImGui::Text("%d", inst.port); ImGui::TableNextColumn(); ImGui::TextUnformatted(inst.profile_name.empty() ? "(none)" : inst.profile_name.c_str()); ImGui::TableNextColumn(); if (inst.headless) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::warning); ImGui::TextUnformatted("headless"); ImGui::PopStyleColor(); } else { ImGui::TextUnformatted("visible"); } ImGui::TableNextColumn(); ImGui::TextUnformatted(inst.user_data_dir.c_str()); ImGui::TableNextColumn(); if (ImGui::SmallButton(is_sel ? "Selected" : "Select")) { g_session().select_browser(inst.port); } ImGui::SameLine(); if (ImGui::SmallButton("Kill")) { if (!inst.user_data_dir.empty()) { kill_chromes_by_userdata(inst.user_data_dir); } if (sel_port == inst.port) g_session().clear_selection(); std::thread([]{ std::this_thread::sleep_for(std::chrono::milliseconds(500)); rescan_async(); }).detach(); } ImGui::PopID(); ++idx; } ImGui::EndTable(); } } ImGui::End(); } // =========================================================================== // Tabs panel // =========================================================================== namespace { struct TabsUiState { std::atomic refreshing{false}; char new_url_input[1024] = "https://example.com"; char filter[128] = ""; }; TabsUiState g_tabs_ui; void refresh_tabs_async(int port) { if (port <= 0) return; if (g_tabs_ui.refreshing.exchange(true)) return; std::thread([port]{ std::vector v; std::string err; bool ok = cdp_list_tabs(port, v, &err); { std::lock_guard lk(g_session().mu); if (g_session().selected_port == port) { g_session().tabs = std::move(v); g_session().tabs_error = ok ? "" : err; g_session().last_tabs_refresh = std::chrono::steady_clock::now(); } } g_tabs_ui.refreshing.store(false); }).detach(); } } // anon void render_tabs_panel(bool* p_open) { if (!ImGui::Begin(TI_LIST " Tabs", p_open)) { ImGui::End(); return; } int port = 0; std::string sel_tab_id; { std::lock_guard lk(g_session().mu); port = g_session().selected_port; sel_tab_id = g_session().selected_tab_id; } if (port <= 0) { ImGui::TextDisabled("Select a browser in the Browsers panel."); ImGui::End(); return; } auto now = std::chrono::steady_clock::now(); { std::lock_guard lk(g_session().mu); if (now - g_session().last_tabs_refresh > std::chrono::seconds(2) && !g_tabs_ui.refreshing.load()) { refresh_tabs_async(port); } } ImGui::Text("Browser :%d", port); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); if (ImGui::Button(TI_REFRESH " Refresh")) refresh_tabs_async(port); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); ImGui::SetNextItemWidth(280); ImGui::InputTextWithHint("##new_url", "https://...", g_tabs_ui.new_url_input, sizeof(g_tabs_ui.new_url_input)); ImGui::SameLine(); if (ImGui::Button(TI_PLUS " New tab")) { std::string url = g_tabs_ui.new_url_input; std::thread([port, url]{ CdpTab t; std::string err; cdp_new_tab(port, url, t, &err); refresh_tabs_async(port); }).detach(); } ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); ImGui::SetNextItemWidth(200); ImGui::InputTextWithHint("##filter", "filter title/url", g_tabs_ui.filter, sizeof(g_tabs_ui.filter)); { std::lock_guard lk(g_session().mu); if (!g_session().tabs_error.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error); ImGui::TextWrapped("Error: %s", g_session().tabs_error.c_str()); ImGui::PopStyleColor(); } } ImGui::Separator(); std::vector tabs_copy; { std::lock_guard lk(g_session().mu); tabs_copy = g_session().tabs; } if (tabs_copy.empty()) { ImGui::TextDisabled("No tabs (or CDP HTTP not reachable on :%d).", port); ImGui::End(); return; } std::string filter_str = g_tabs_ui.filter; std::transform(filter_str.begin(), filter_str.end(), filter_str.begin(), ::tolower); const ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; if (ImGui::BeginTable("##tabs", 6, flags, ImVec2(0, 0))) { ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 22); ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 70); ImGui::TableSetupColumn("Title"); ImGui::TableSetupColumn("URL"); ImGui::TableSetupColumn("Att.", ImGuiTableColumnFlags_WidthFixed, 40); ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 200); ImGui::TableHeadersRow(); int idx = 0; for (const auto& t : tabs_copy) { if (!filter_str.empty()) { std::string lt = t.title; std::transform(lt.begin(), lt.end(), lt.begin(), ::tolower); std::string lu = t.url; std::transform(lu.begin(), lu.end(), lu.begin(), ::tolower); if (lt.find(filter_str) == std::string::npos && lu.find(filter_str) == std::string::npos) continue; } bool is_sel = (sel_tab_id == t.id); ImGui::TableNextRow(); ImGui::PushID(idx); ImGui::TableNextColumn(); if (is_sel) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::primary); ImGui::TextUnformatted(TI_CHECK); ImGui::PopStyleColor(); } ImGui::TableNextColumn(); ImGui::TextUnformatted(t.type.c_str()); ImGui::TableNextColumn(); ImGui::TextUnformatted(t.title.empty() ? "(no title)" : t.title.c_str()); ImGui::TableNextColumn(); ImGui::TextUnformatted(t.url.c_str()); ImGui::TableNextColumn(); ImGui::TextUnformatted(t.attached ? "yes" : ""); ImGui::TableNextColumn(); if (ImGui::SmallButton("Select")) { if (!t.ws_url.empty()) g_session().select_tab(t.id, t.ws_url); } ImGui::SameLine(); if (ImGui::SmallButton("Focus")) { std::string id = t.id; std::thread([port, id]{ cdp_activate_tab(port, id, nullptr); refresh_tabs_async(port); }).detach(); } ImGui::SameLine(); if (ImGui::SmallButton("Close")) { std::string id = t.id; std::thread([port, id]{ cdp_close_tab(port, id, nullptr); refresh_tabs_async(port); }).detach(); if (is_sel) g_session().select_tab("", ""); } ImGui::PopID(); ++idx; } ImGui::EndTable(); } ImGui::End(); } // =========================================================================== // Tab Detail panel (placeholder funcional) // =========================================================================== namespace { struct TabDetailUiState { char repl_input[4096] = "1+1"; std::string repl_output; std::mutex mu; }; TabDetailUiState g_tab_detail_ui; void tab_detail_eval_async(const std::string& expr) { NetworkSession* net = nullptr; { std::lock_guard lk(g_session().mu); net = g_session().net.get(); if (!net) return; } // No tenemos acceso directo al CdpWs desde NetworkSession publicamente. // Para v1.5 — Tab Detail dedicado abrira su propio CdpWs. De momento, // el panel solo muestra info estatica + tip. (void)expr; } } // anon void render_tab_detail_panel(bool* p_open) { if (!ImGui::Begin(TI_FILE_INFO " Tab Detail", p_open)) { ImGui::End(); return; } std::string sel_id, sel_ws; int port = 0; { std::lock_guard lk(g_session().mu); port = g_session().selected_port; sel_id = g_session().selected_tab_id; sel_ws = g_session().selected_tab_ws_url; } if (sel_id.empty()) { ImGui::TextDisabled("Select a tab in the Tabs panel."); ImGui::End(); return; } ImGui::Text("Browser :%d", port); ImGui::Text("Tab id %s", sel_id.c_str()); ImGui::TextWrapped("WS %s", sel_ws.c_str()); ImGui::Separator(); ImGui::TextWrapped( "Tab Detail (HTML preview + screenshot + Runtime.evaluate REPL) llega " "en v1.5 (issue 0003). El WebSocket esta vivo via Network panel — el " "REPL re-utilizara la misma conexion en una proxima iteracion."); ImGui::End(); } // =========================================================================== // Network panel (DevTools-like) // =========================================================================== namespace { // Filtros chips (tipo recurso). Bitmask sobre ResourceType. struct NetUiState { char filter_text[256] = ""; bool invert_filter = false; bool hide_data_urls = true; bool only_blocked = false; // chips mask. true = mostrar este tipo. start: All on. bool type_doc = true; bool type_css = true; bool type_js = true; bool type_xhr = true; // XHR + Fetch bool type_img = true; bool type_media = true; bool type_font = true; bool type_ws = true; bool type_other = true; bool all_types = true; bool paused = false; int selected_index = -1; // index en snapshot filtrado std::string selected_id; // requestId estable int detail_tab = 0; // 0 headers, 1 payload, 2 response, 3 cookies, 4 timing, 5 ws // Histograma overview (request starts/s). bool show_histogram = true; int histogram_bins = 30; bool reload_ignore_cache = false; }; NetUiState g_net_ui; bool type_passes(const NetUiState& s, ResourceType t) { if (s.all_types) return true; switch (t) { case ResourceType::Document: return s.type_doc; case ResourceType::Stylesheet: return s.type_css; case ResourceType::Script: return s.type_js; case ResourceType::Image: return s.type_img; case ResourceType::Media: return s.type_media; case ResourceType::Font: return s.type_font; case ResourceType::XHR: case ResourceType::Fetch: return s.type_xhr; case ResourceType::WebSocket: case ResourceType::EventSource: return s.type_ws; default: return s.type_other; } } ImVec4 status_color(int status) { if (status == 0) return fn_tokens::colors::text_muted; if (status >= 500) return fn_tokens::colors::error; if (status >= 400) return fn_tokens::colors::warning; if (status >= 300) return fn_tokens::colors::info; return fn_tokens::colors::success; } std::string short_name_from_url(const std::string& url) { if (url.empty()) return ""; size_t scheme_end = url.find("://"); size_t path_start = (scheme_end == std::string::npos) ? 0 : scheme_end + 3; size_t qmark = url.find('?', path_start); std::string path = url.substr(path_start, qmark == std::string::npos ? std::string::npos : qmark - path_start); size_t slash = path.find('/'); if (slash == std::string::npos) return path; std::string after = path.substr(slash); size_t last = after.find_last_of('/'); if (last == std::string::npos || last == after.size() - 1) { // ends with "/" -> use host return path.substr(0, slash); } return after.substr(last + 1); } std::string fmt_size(int64_t b) { char buf[64]; if (b < 1024) std::snprintf(buf, sizeof(buf), "%lld B", (long long)b); else if (b < 1024 * 1024) std::snprintf(buf, sizeof(buf), "%.1f kB", b / 1024.0); else std::snprintf(buf, sizeof(buf), "%.2f MB", b / (1024.0 * 1024.0)); return buf; } std::string fmt_dur_ms(double s) { char buf[32]; if (s <= 0) return "—"; if (s < 1.0) std::snprintf(buf, sizeof(buf), "%.0f ms", s * 1000.0); else std::snprintf(buf, sizeof(buf), "%.2f s", s); return buf; } void copy_to_clipboard(const std::string& s) { ImGui::SetClipboardText(s.c_str()); } std::string build_curl(const NetworkRequest& r) { std::ostringstream os; os << "curl -X " << (r.method.empty() ? "GET" : r.method) << " '" << r.url << "'"; for (const auto& h : r.request_headers) { if (h.name.size() >= 1 && h.name[0] == ':') continue; // pseudo h2 os << " -H '" << h.name << ": " << h.value << "'"; } if (r.has_post_data && !r.post_data.empty()) { os << " --data-raw '" << r.post_data << "'"; } return os.str(); } std::string build_fetch(const NetworkRequest& r) { std::ostringstream os; os << "fetch('" << r.url << "', { method: '" << (r.method.empty() ? "GET" : r.method) << "', headers: {"; bool first = true; for (const auto& h : r.request_headers) { if (h.name.size() >= 1 && h.name[0] == ':') continue; if (!first) os << ", "; first = false; os << "'" << h.name << "': '" << h.value << "'"; } os << "}"; if (r.has_post_data && !r.post_data.empty()) { os << ", body: '" << r.post_data << "'"; } os << "})"; return os.str(); } void draw_filter_chips() { auto chip = [](const char* label, bool* state, ImVec4 color) { if (*state) { ImGui::PushStyleColor(ImGuiCol_Button, color); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, color); } if (ImGui::SmallButton(label)) *state = !*state; if (*state) ImGui::PopStyleColor(2); ImGui::SameLine(); }; if (ImGui::SmallButton(g_net_ui.all_types ? "All*" : "All")) { g_net_ui.all_types = !g_net_ui.all_types; } ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); chip("Doc", &g_net_ui.type_doc, fn_tokens::colors::primary); chip("CSS", &g_net_ui.type_css, fn_tokens::colors::primary); chip("JS", &g_net_ui.type_js, fn_tokens::colors::primary); chip("XHR", &g_net_ui.type_xhr, fn_tokens::colors::primary); chip("Img", &g_net_ui.type_img, fn_tokens::colors::primary); chip("Media", &g_net_ui.type_media, fn_tokens::colors::primary); chip("Font", &g_net_ui.type_font, fn_tokens::colors::primary); chip("WS", &g_net_ui.type_ws, fn_tokens::colors::primary); chip("Other", &g_net_ui.type_other, fn_tokens::colors::primary); ImGui::NewLine(); } void draw_request_detail(const NetworkRequest& r, NetworkSession* net) { if (ImGui::BeginTabBar("##req_detail_tabs")) { if (ImGui::BeginTabItem("Headers")) { ImGui::TextDisabled("General"); ImGui::Text("URL: %s", r.url.c_str()); ImGui::Text("Method: %s", r.method.c_str()); ImGui::Text("Status: %d %s", r.status, r.status_text.c_str()); ImGui::Text("Remote: %s:%d", r.remote_ip.c_str(), r.remote_port); ImGui::Text("Protocol: %s", r.protocol.c_str()); if (r.failed) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error); ImGui::Text("Error: %s%s", r.error_text.c_str(), r.canceled ? " (canceled)" : ""); ImGui::PopStyleColor(); } ImGui::Separator(); ImGui::TextDisabled("Request headers (%d)", (int)r.request_headers.size()); for (const auto& h : r.request_headers) { ImGui::Text("%s:", h.name.c_str()); ImGui::SameLine(); ImGui::TextWrapped("%s", h.value.c_str()); } ImGui::Separator(); ImGui::TextDisabled("Response headers (%d)", (int)r.response_headers.size()); for (const auto& h : r.response_headers) { ImGui::Text("%s:", h.name.c_str()); ImGui::SameLine(); ImGui::TextWrapped("%s", h.value.c_str()); } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Payload")) { if (!r.has_post_data) { ImGui::TextDisabled("(no request body)"); } else { if (ImGui::SmallButton("Copy")) copy_to_clipboard(r.post_data); ImGui::Separator(); ImGui::InputTextMultiline("##postdata", (char*)r.post_data.c_str(), r.post_data.size() + 1, ImVec2(-1, -1), ImGuiInputTextFlags_ReadOnly); } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Response")) { if (r.body_fetched && !r.body_text.empty()) { if (ImGui::SmallButton("Copy")) copy_to_clipboard(r.body_text); ImGui::Separator(); ImGui::InputTextMultiline("##body", (char*)r.body_text.c_str(), r.body_text.size() + 1, ImVec2(-1, -1), ImGuiInputTextFlags_ReadOnly); } else { if (ImGui::Button("Fetch response body")) { if (net) net->request_body(r.id); } ImGui::TextDisabled("(body lazy-loaded via Network.getResponseBody)"); ImGui::TextDisabled("Limitacion conocida: matching id->requestId pendiente — body llega via WS pero no se pinta hasta v1.5."); } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Cookies")) { // Buscar Cookie / Set-Cookie en headers. ImGui::TextDisabled("Sent (Cookie):"); for (const auto& h : r.request_headers) { if (h.name == "Cookie" || h.name == "cookie") { ImGui::TextWrapped("%s", h.value.c_str()); } } ImGui::Separator(); ImGui::TextDisabled("Set (Set-Cookie):"); for (const auto& h : r.response_headers) { if (h.name == "Set-Cookie" || h.name == "set-cookie") { ImGui::TextWrapped("%s", h.value.c_str()); } } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Timing")) { double dur = (r.t_finished > 0 ? r.t_finished : r.t_response) - r.t_started; ImGui::Text("Started: %.3f s", r.t_started); ImGui::Text("Response: %.3f s", r.t_response); ImGui::Text("Finished: %.3f s", r.t_finished); ImGui::Text("Total: %s", fmt_dur_ms(dur).c_str()); ImGui::Separator(); ImGui::Text("CDP timestamps"); ImGui::Text(" requestWillBeSent: %.6f", r.ts_request_will_be_sent); ImGui::Text(" responseReceived: %.6f", r.ts_response_received); ImGui::Text(" loadingFinished: %.6f", r.ts_loading_finished); ImGui::Text(" loadingFailed: %.6f", r.ts_loading_failed); ImGui::EndTabItem(); } if (r.type == ResourceType::WebSocket && ImGui::BeginTabItem("Messages")) { ImGui::Text("Frames: %d", (int)r.ws_frames.size()); ImGui::Separator(); const ImGuiTableFlags f = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY; if (ImGui::BeginTable("##wsframes", 4, f, ImVec2(-1, -1))) { ImGui::TableSetupColumn("Dir", ImGuiTableColumnFlags_WidthFixed, 30); ImGui::TableSetupColumn("Op", ImGuiTableColumnFlags_WidthFixed, 30); ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 80); ImGui::TableSetupColumn("Payload"); ImGui::TableHeadersRow(); for (const auto& wf : r.ws_frames) { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::TextUnformatted(wf.outgoing ? TI_ARROW_UP : TI_ARROW_DOWN); ImGui::TableNextColumn(); ImGui::Text("%d", wf.opcode); ImGui::TableNextColumn(); ImGui::Text("%.3f", wf.time); ImGui::TableNextColumn(); ImGui::TextWrapped("%s", wf.payload.c_str()); } ImGui::EndTable(); } ImGui::EndTabItem(); } ImGui::EndTabBar(); } } void render_network_toolbar(NetworkSession* net) { // Reload page (Page.reload via CDP). Si no hay sesion, deshabilita. if (!net) ImGui::BeginDisabled(); if (ImGui::Button(TI_REFRESH " Reload")) { if (net) net->reload_page(g_net_ui.reload_ignore_cache); } if (!net) ImGui::EndDisabled(); ImGui::SameLine(); ImGui::Checkbox("Bypass cache", &g_net_ui.reload_ignore_cache); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); if (ImGui::Button(TI_TRASH " Clear")) { if (net) net->clear_log(); g_net_ui.selected_id.clear(); g_net_ui.selected_index = -1; } ImGui::SameLine(); if (ImGui::Button(g_net_ui.paused ? (TI_PLAYER_PLAY " Resume") : (TI_PLAYER_PAUSE " Pause"))) { g_net_ui.paused = !g_net_ui.paused; } ImGui::SameLine(); ImGui::Checkbox(TI_CHART_HISTOGRAM " Histogram", &g_net_ui.show_histogram); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); bool preserve = net ? net->preserve_log() : true; if (ImGui::Checkbox("Preserve log", &preserve)) { if (net) net->set_preserve_log(preserve); } ImGui::SameLine(); bool cache_disabled = net ? net->cache_disabled() : false; if (ImGui::Checkbox("Disable cache", &cache_disabled)) { if (net) net->set_cache_disabled(cache_disabled); } ImGui::SameLine(); ImGui::Checkbox("Hide data:", &g_net_ui.hide_data_urls); ImGui::SameLine(); ImGui::Checkbox("Only failed", &g_net_ui.only_blocked); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); ImGui::SetNextItemWidth(220); ImGui::InputTextWithHint("##netfilter", "filter (regex-like substring)", g_net_ui.filter_text, sizeof(g_net_ui.filter_text)); ImGui::SameLine(); ImGui::Checkbox("Invert", &g_net_ui.invert_filter); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); if (ImGui::Button(TI_DOWNLOAD " Export HAR")) { if (net) { std::string har = net->export_har_json(); // Escribir junto al exe. char path[1024]; std::snprintf(path, sizeof(path), "navegator_har_%lld.har", (long long)std::time(nullptr)); FILE* f = std::fopen(path, "w"); if (f) { std::fwrite(har.data(), 1, har.size(), f); std::fclose(f); } } } } } // anon void render_network_panel(bool* p_open) { if (!ImGui::Begin(TI_ACTIVITY " Network", p_open, ImGuiWindowFlags_MenuBar)) { ImGui::End(); return; } NetworkSession* net = nullptr; int port = 0; std::string sel_tab_id; std::string net_err; { std::lock_guard lk(g_session().mu); net = g_session().net.get(); port = g_session().selected_port; sel_tab_id = g_session().selected_tab_id; net_err = g_session().net_error; } if (!net) { if (sel_tab_id.empty()) { ImGui::TextDisabled("Select a tab in the Tabs panel to capture network."); } else { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error); ImGui::TextWrapped("Network session not open: %s", net_err.c_str()); ImGui::PopStyleColor(); } ImGui::End(); return; } // Drenar eventos cada frame (a menos que pause). if (!g_net_ui.paused) net->pump(); render_network_toolbar(net); draw_filter_chips(); ImGui::Separator(); // Snapshot + filtrado. auto reqs = net->snapshot(); // ---------- Histograma overview ---------- if (g_net_ui.show_histogram) { std::vector starts; starts.reserve(reqs.size()); double t_max = 1.0; for (const auto& r : reqs) { if (r->t_started >= 0.0) { starts.push_back(r->t_started); if (r->t_started > t_max) t_max = r->t_started; } } // Tamaño bin dinamico — bins fijo 30, rango 0..t_max. if (ImPlot::BeginPlot("##req_histogram", ImVec2(-1, 100), ImPlotFlags_NoTitle | ImPlotFlags_NoMouseText | ImPlotFlags_NoBoxSelect | ImPlotFlags_NoMenus | ImPlotFlags_NoFrame)) { ImPlot::SetupAxes("t (s)", "reqs/bin", ImPlotAxisFlags_NoMenus, ImPlotAxisFlags_AutoFit | ImPlotAxisFlags_NoMenus); ImPlot::SetupAxisLimits(ImAxis_X1, 0.0, t_max + 0.001, ImPlotCond_Always); if (!starts.empty()) { ImPlot::PlotHistogram("Requests/bin", starts.data(), (int)starts.size(), g_net_ui.histogram_bins, 1.0, ImPlotRange(0.0, t_max + 0.001)); } // Marcadores DOMContentLoaded / Load. auto stats = net->stats(); if (stats.dom_content_loaded > 0) { double x = stats.dom_content_loaded; ImPlot::TagX(x, fn_tokens::colors::info, "DCL"); } if (stats.load_event > 0) { double x = stats.load_event; ImPlot::TagX(x, fn_tokens::colors::success, "L"); } ImPlot::EndPlot(); } ImGui::Separator(); } // ---------- /histograma ---------- std::string filt = g_net_ui.filter_text; std::string filt_lower = filt; std::transform(filt_lower.begin(), filt_lower.end(), filt_lower.begin(), ::tolower); std::vector> filtered; filtered.reserve(reqs.size()); for (auto& r : reqs) { if (g_net_ui.hide_data_urls && r->url.compare(0, 5, "data:") == 0) continue; if (g_net_ui.only_blocked && !r->failed) continue; if (!type_passes(g_net_ui, r->type)) continue; if (!filt_lower.empty()) { std::string lu = r->url; std::transform(lu.begin(), lu.end(), lu.begin(), ::tolower); bool match = (lu.find(filt_lower) != std::string::npos); if (g_net_ui.invert_filter) match = !match; if (!match) continue; } filtered.push_back(r); } // Layout: split top (table) / bottom (detail) cuando hay seleccion. bool has_sel = !g_net_ui.selected_id.empty(); float avail_h = ImGui::GetContentRegionAvail().y; float status_bar_h = ImGui::GetTextLineHeightWithSpacing() + 4.0f; float top_h = has_sel ? std::max(80.0f, (avail_h - status_bar_h) * 0.55f) : (avail_h - status_bar_h); ImGui::BeginChild("##nettable", ImVec2(0, top_h), true); { const ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY | ImGuiTableFlags_Sortable; if (ImGui::BeginTable("##requests", 8, flags)) { ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableSetupColumn("Name"); ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 60); ImGui::TableSetupColumn("Method", ImGuiTableColumnFlags_WidthFixed, 70); ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 70); ImGui::TableSetupColumn("Initiator"); ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed, 90); ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 70); ImGui::TableSetupColumn("Waterfall",ImGuiTableColumnFlags_WidthStretch); ImGui::TableHeadersRow(); // Para waterfall: necesitamos rango total. double t_min = 0.0, t_max = 0.0; for (const auto& r : filtered) { t_max = std::max(t_max, std::max(r->t_finished, r->t_response)); } if (t_max < 1.0) t_max = 1.0; for (size_t i = 0; i < filtered.size(); ++i) { const auto& r = filtered[i]; ImGui::TableNextRow(); ImGui::PushID((int)i); bool is_sel = (g_net_ui.selected_id == r->id); ImGui::TableNextColumn(); std::string name = short_name_from_url(r->url); if (name.empty()) name = "(empty)"; if (ImGui::Selectable(name.c_str(), is_sel, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { g_net_ui.selected_id = r->id; g_net_ui.selected_index = (int)i; } if (ImGui::BeginPopupContextItem("##rowctx")) { if (ImGui::MenuItem("Copy URL")) copy_to_clipboard(r->url); if (ImGui::MenuItem("Copy as cURL")) copy_to_clipboard(build_curl(*r)); if (ImGui::MenuItem("Copy as fetch")) copy_to_clipboard(build_fetch(*r)); ImGui::Separator(); if (ImGui::MenuItem("Block URL (TODO)")) {} ImGui::EndPopup(); } ImGui::TableNextColumn(); if (r->status > 0) { ImGui::PushStyleColor(ImGuiCol_Text, status_color(r->status)); ImGui::Text("%d", r->status); ImGui::PopStyleColor(); } else if (r->failed) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error); ImGui::TextUnformatted("(failed)"); ImGui::PopStyleColor(); } else { ImGui::TextDisabled("..."); } ImGui::TableNextColumn(); ImGui::TextUnformatted(r->method.c_str()); ImGui::TableNextColumn(); ImGui::TextUnformatted(resource_type_label(r->type)); ImGui::TableNextColumn(); if (!r->initiator_url.empty()) { ImGui::TextUnformatted(short_name_from_url(r->initiator_url).c_str()); } else { ImGui::TextDisabled("%s", r->initiator_type.c_str()); } ImGui::TableNextColumn(); if (r->from_cache) { ImGui::TextDisabled("(cache)"); } else { ImGui::TextUnformatted(fmt_size(r->encoded_data_length).c_str()); } ImGui::TableNextColumn(); { double dur = (r->t_finished > 0 ? r->t_finished : r->t_response) - r->t_started; if (dur < 0) dur = 0; ImGui::TextUnformatted(fmt_dur_ms(dur).c_str()); } ImGui::TableNextColumn(); { // mini waterfall bar. ImVec2 cmin = ImGui::GetCursorScreenPos(); ImVec2 avail = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetTextLineHeight()); ImDrawList* dl = ImGui::GetWindowDrawList(); double a = r->t_started / t_max; double b = (r->t_finished > 0 ? r->t_finished : r->t_response) / t_max; if (b < a) b = a; if (b > 1) b = 1; ImVec2 p1(cmin.x + avail.x * (float)a, cmin.y + 2); ImVec2 p2(cmin.x + avail.x * (float)b, cmin.y + avail.y - 2); if (p2.x < p1.x + 2) p2.x = p1.x + 2; ImU32 col = ImGui::ColorConvertFloat4ToU32( r->failed ? fn_tokens::colors::error : (r->finished ? fn_tokens::colors::primary : fn_tokens::colors::warning)); dl->AddRectFilled(p1, p2, col, 2.0f); ImGui::Dummy(avail); } ImGui::PopID(); } ImGui::EndTable(); } } ImGui::EndChild(); // Detail pane (selected request). if (has_sel) { ImGui::BeginChild("##netdetail", ImVec2(0, 0), true); std::shared_ptr sel; for (auto& r : filtered) if (r->id == g_net_ui.selected_id) { sel = r; break; } if (!sel) { for (auto& r : reqs) if (r->id == g_net_ui.selected_id) { sel = r; break; } } if (sel) { draw_request_detail(*sel, net); } else { ImGui::TextDisabled("(request gone — log was cleared)"); } ImGui::EndChild(); } // Status bar auto stats = net->stats(); ImGui::Separator(); ImGui::Text("%d requests", stats.total_requests); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); ImGui::Text("%s transferred", fmt_size(stats.transferred).c_str()); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); ImGui::Text("%s resources", fmt_size(stats.resources).c_str()); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); if (stats.finish_time > 0) ImGui::Text("Finish: %.2f s", stats.finish_time); else ImGui::TextDisabled("Finish: —"); if (stats.dom_content_loaded > 0) { ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); ImGui::Text("DCL: %.2f", stats.dom_content_loaded); } if (stats.load_event > 0) { ImGui::SameLine(); ImGui::Text("L: %.2f", stats.load_event); } ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); { uint64_t fin = net->ws_frames_in(); uint64_t bin = net->ws_bytes_in(); uint64_t bout = net->ws_bytes_out(); bool alive = (fin > 0); ImGui::PushStyleColor(ImGuiCol_Text, alive ? fn_tokens::colors::success : fn_tokens::colors::warning); ImGui::Text("CDP: %s", alive ? "alive" : "no events"); ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::TextDisabled("(%llu frames, in %s / out %s)", (unsigned long long)fin, fmt_size((int64_t)bin).c_str(), fmt_size((int64_t)bout).c_str()); } ImGui::End(); } } // namespace navegator