#include "viz/agent_runs_timeline_helpers.h" #include // Tabler icon macros live in core/icons_tabler.h. They are short (3-byte // utf-8) string literals like "\xee\xa9\x9e". We don't depend on that header // here directly to keep helpers buildable without the icon font tree — but // the values below are pulled from cpp/functions/core/icons_tabler.h // (Tabler v3.41). If a glyph moves upstream, update this table. namespace fn_viz { namespace timeline { namespace { // Mirror of icons_tabler.h entries used by the timeline. Kept inline so the // helpers TU compiles standalone (tests don't pull the full icon font). constexpr const char* TI_CLOCK_ = "\xee\xa9\xb0"; // U+EA70 constexpr const char* TI_LOADER_ = "\xee\xb2\xa3"; // U+ECA3 spinning loader constexpr const char* TI_CIRCLE_CHECK_ = "\xee\xa9\xa7"; // U+EA67 constexpr const char* TI_CHECKS_ = "\xee\xae\xaa"; // U+EBAA constexpr const char* TI_GIT_MERGE_ = "\xee\xaa\xb5"; // U+EAB5 constexpr const char* TI_BAN_ = "\xee\xa8\xae"; // U+EA2E constexpr const char* TI_CIRCLE_X_ = "\xee\xa9\xaa"; // U+EA6A constexpr const char* TI_HOURGLASS_ = "\xee\xbe\x93"; // U+EF93 bool contains(const std::vector& v, const std::string& needle) { for (const auto& s : v) { if (s == needle) return true; } return false; } } // namespace bool passes_filter(const AgentRun& r, const TimelineFilter& f) { if (!f.apps.empty() && !contains(f.apps, r.app)) return false; if (!f.statuses.empty() && !contains(f.statuses, r.status)) return false; if (f.since_ts > 0 && r.started_at < f.since_ts) return false; return true; } std::vector filter_and_sort(const std::vector& runs, const TimelineFilter& f) { std::vector out; out.reserve(runs.size()); for (const auto& r : runs) { if (passes_filter(r, f)) out.push_back(r); } std::sort(out.begin(), out.end(), [](const AgentRun& a, const AgentRun& b) { if (a.started_at != b.started_at) return a.started_at > b.started_at; return a.id < b.id; }); return out; } std::string format_duration(int64_t started_at, int64_t finished_at) { if (finished_at == 0) return "running"; int64_t dur = finished_at - started_at; if (dur < 0) return "—"; char buf[32]; if (dur < 60) { std::snprintf(buf, sizeof(buf), "%llds", (long long)dur); } else if (dur < 3600) { long long m = dur / 60; long long s = dur % 60; std::snprintf(buf, sizeof(buf), "%lldm%02llds", m, s); } else { long long h = dur / 3600; long long m = (dur % 3600) / 60; std::snprintf(buf, sizeof(buf), "%lldh%02lldm", h, m); } return std::string(buf); } int status_color_token(const std::string& status) { if (status == "pending") return 1; // info if (status == "running") return 1; // info if (status == "done") return 2; // success if (status == "validated") return 2; // success if (status == "merged") return 3; // primary if (status == "aborted") return 5; // warning if (status == "failed") return 6; // danger return 0; // neutral } std::string status_icon_id(const std::string& status) { if (status == "pending") return TI_CLOCK_; if (status == "running") return TI_LOADER_; if (status == "done") return TI_CIRCLE_CHECK_; if (status == "validated") return TI_CHECKS_; if (status == "merged") return TI_GIT_MERGE_; if (status == "aborted") return TI_BAN_; if (status == "failed") return TI_CIRCLE_X_; return TI_HOURGLASS_; } std::string app_chip_hex(const std::string& app) { // Hardcoded palette. Keep aligned with the apps' icon.accent in app.md. if (app == "kanban_cpp") return "#a855f7"; // violet if (app == "skill_tree") return "#0ea5e9"; // sky if (app == "graph_explorer") return "#16a34a"; // green if (app == "shaders_lab") return "#f97316"; // orange if (app == "registry_dashboard") return "#0ea5e9"; return "#5C5F66"; // neutral gray (matches fn_tokens::border_strong) } } // namespace timeline } // namespace fn_viz