From b341d5d9ad061185cc05df5029e647dd9559948a Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Mon, 18 May 2026 18:31:17 +0200 Subject: [PATCH] =?UTF-8?q?feat(viz):=20agent=5Fruns=5Ftimeline=20helpers?= =?UTF-8?q?=20=E2=80=94=20pure=20filter/sort/format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../viz/agent_runs_timeline_helpers.cpp | 110 ++++++++++++++++++ .../viz/agent_runs_timeline_helpers.h | 63 ++++++++++ 2 files changed, 173 insertions(+) create mode 100644 cpp/functions/viz/agent_runs_timeline_helpers.cpp create mode 100644 cpp/functions/viz/agent_runs_timeline_helpers.h diff --git a/cpp/functions/viz/agent_runs_timeline_helpers.cpp b/cpp/functions/viz/agent_runs_timeline_helpers.cpp new file mode 100644 index 00000000..2742939c --- /dev/null +++ b/cpp/functions/viz/agent_runs_timeline_helpers.cpp @@ -0,0 +1,110 @@ +#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 diff --git a/cpp/functions/viz/agent_runs_timeline_helpers.h b/cpp/functions/viz/agent_runs_timeline_helpers.h new file mode 100644 index 00000000..abe36d67 --- /dev/null +++ b/cpp/functions/viz/agent_runs_timeline_helpers.h @@ -0,0 +1,63 @@ +#pragma once +// +// agent_runs_timeline_helpers.h — pure helpers for the agent runs timeline panel. +// +// These helpers operate on AgentRun + TimelineFilter from agent_runs_timeline.h +// but never call ImGui. They are unit-testable in isolation (see +// cpp/tests/test_agent_runs_timeline.cpp). +// +// Issue 0118. + +#include "viz/agent_runs_timeline.h" + +#include +#include +#include + +namespace fn_viz { +namespace timeline { + +// True iff `r` matches every active sub-filter: +// - filter.apps empty => any app +// - filter.statuses empty => any status +// - filter.since_ts == 0 => any started_at +// All filters are AND-combined. +bool passes_filter(const AgentRun& r, const TimelineFilter& f); + +// Returns a copy of `runs` with `passes_filter` applied and sorted by +// `started_at` descending (most recent first). Stable for runs with equal +// started_at: ties broken by id ascending. +std::vector filter_and_sort(const std::vector& runs, + const TimelineFilter& f); + +// Human-friendly duration string built from two unix-epoch SECOND timestamps. +// - finished_at == 0 => "running" +// - duration < 60s => "Ns" (e.g. "45s") +// - duration < 3600s => "MmSs" (e.g. "12m05s") +// - else => "HhMm" (e.g. "1h12m") +// Negative or inverted (finished < started) => "—". +std::string format_duration(int64_t started_at, int64_t finished_at); + +// Returns a small integer color token (0..7) per status. The render layer +// maps this token to an actual ImVec4 using fn_tokens::colors. +// +// pending -> 1 (info) +// running -> 1 (info) +// done -> 2 (success) +// validated -> 2 (success) +// merged -> 3 (primary) +// aborted -> 5 (warning) +// failed -> 6 (danger) +// -> 0 (neutral) +int status_color_token(const std::string& status); + +// Returns the TI_* macro string for the given status (already utf-8 encoded +// 3-byte sequence). Render layer uses these directly in ImGui::Text. +std::string status_icon_id(const std::string& status); + +// Hardcoded chip color hex for known app ids. Returns "#5C5F66" (border_strong +// / neutral gray) for unknown apps. Render layer parses the hex to ImVec4. +std::string app_chip_hex(const std::string& app); + +} // namespace timeline +} // namespace fn_viz