Files
fn_registry/cpp/functions/viz/agent_runs_timeline_helpers.cpp
egutierrez b341d5d9ad feat(viz): agent_runs_timeline helpers — pure filter/sort/format
Issue 0118.

Pure helpers in fn_viz::timeline namespace, free of ImGui:
- passes_filter / filter_and_sort  (multi-select app + status + since_ts)
- format_duration  (running | Ns | MmSSs | HhMMm | —)
- status_color_token / status_icon_id  (status → fn_tokens index / TI_*)
- app_chip_hex  (app id → accent hex, fallback gray)

Designed for unit-test isolation. Render layer (separate commit) consumes
these via agent_runs_timeline.h.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:31:17 +02:00

111 lines
4.2 KiB
C++

#include "viz/agent_runs_timeline_helpers.h"
#include <algorithm>
// 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<std::string>& 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<AgentRun> filter_and_sort(const std::vector<AgentRun>& runs,
const TimelineFilter& f) {
std::vector<AgentRun> 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