c2bdc586a4
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>
111 lines
4.2 KiB
C++
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
|