chore: auto-commit (8 archivos)

- backend/handlers.go
- data.cpp
- data.h
- main.cpp
- panel_board.cpp
- panel_filters.cpp
- appicon.ico
- backend/kanban_cpp_backend.exe

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 19:38:15 +02:00
parent 255e8dcf71
commit 98bf278472
8 changed files with 374 additions and 51 deletions
+203 -33
View File
@@ -3,17 +3,29 @@
#include <imgui.h>
#include <imgui_internal.h>
#include <map>
#include <mutex>
#include <string>
#include <utility>
#include <vector>
#include "core/icons_tabler.h"
namespace kanban {
static const char* k_columns[] = {"pendiente", "in-progress", "bloqueado", "completado"};
static const char* k_columns[] = {"ideas", "pendiente", "in-progress", "completado"};
static const char* k_column_icons[] = {TI_BULB, TI_CLIPBOARD, TI_TOOLS, TI_CIRCLE_CHECK};
static const int k_n_columns = sizeof(k_columns) / sizeof(k_columns[0]);
// Map flow status (free-form) → one of the 4 board columns.
static int flow_column_index(const std::string& s) {
if (s == "draft" || s == "ideas") return 0;
if (s == "active" || s == "in-progress") return 2;
if (s == "done" || s == "completed" || s == "completado") return 3;
// pending, paused, "" → pendiente
return 1;
}
static ImU32 priority_color(const std::string& p) {
if (p == "critica") return IM_COL32(255, 80, 80, 255);
if (p == "alta") return IM_COL32(255, 165, 0, 255);
@@ -22,14 +34,25 @@ static ImU32 priority_color(const std::string& p) {
return IM_COL32(180, 180, 180, 255);
}
static ImU32 status_tint(const std::string& s) {
if (s == "bloqueado") return IM_COL32(120, 60, 60, 200);
if (s == "completado") return IM_COL32( 60, 110, 60, 100);
if (s == "in-progress") return IM_COL32( 70, 90, 140, 100);
if (s == "ideas") return IM_COL32(110, 90, 140, 100);
return IM_COL32(60, 60, 70, 100);
}
static void draw_card(const Issue& iss) {
ImGui::PushID(iss.id.c_str());
ImVec2 size(0.0f, 64.0f);
const float card_h = 110.0f;
ImVec2 size(0.0f, card_h);
ImU32 bg = status_tint(iss.status);
ImGui::PushStyleColor(ImGuiCol_ChildBg, bg);
ImGui::BeginChild("card", size, true, ImGuiWindowFlags_NoScrollbar);
// Header: id + priority badge
ImGui::TextColored(ImColor(180, 180, 180, 255), "%s", iss.id.c_str());
// Top row: id · priority dot · agent icon (right-aligned) · block badge if blocked
ImGui::TextColored(ImColor(200, 200, 200, 255), "%s", iss.id.c_str());
ImGui::SameLine();
{
ImVec2 cur = ImGui::GetCursorScreenPos();
@@ -39,22 +62,42 @@ static void draw_card(const Issue& iss) {
dl->AddCircleFilled(ImVec2(cur.x + r, cur.y + r + 2), r, c);
ImGui::Dummy(ImVec2(2 * r + 4, 2 * r));
ImGui::SameLine();
ImGui::TextUnformatted(iss.priority.c_str());
ImGui::TextColored(ImColor(180, 180, 180, 255), "%s", iss.priority.c_str());
}
// Title (truncate to ~60 chars)
// Right-aligned indicators (agent active + blocked).
{
std::string indicators;
bool active = is_agent_active(iss.id);
if (active) indicators += TI_ROBOT;
if (iss.status == "bloqueado") { if (!indicators.empty()) indicators += " "; indicators += TI_LOCK; }
if (!indicators.empty()) {
ImVec2 ts = ImGui::CalcTextSize(indicators.c_str());
float avail = ImGui::GetContentRegionAvail().x;
ImGui::SameLine(0, avail - ts.x - 4);
if (active) {
ImGui::TextColored(ImColor(120, 220, 120, 255), "%s", indicators.c_str());
} else {
ImGui::TextColored(ImColor(220, 120, 120, 255), "%s", indicators.c_str());
}
}
}
// Title (2-3 lines)
std::string t = iss.title;
if (t.size() > 70) t = t.substr(0, 67) + "...";
if (t.size() > 140) t = t.substr(0, 137) + "...";
ImGui::TextWrapped("%s", t.c_str());
// First domain chip
// Bottom: first domain chip
if (!iss.domain.empty()) {
ImGui::TextColored(ImColor(140, 200, 255, 255), TI_TAG " %s", iss.domain[0].c_str());
ImGui::TextColored(ImColor(140, 200, 255, 255), "%s %s", TI_TAG, iss.domain[0].c_str());
}
ImGui::EndChild();
ImGui::PopStyleColor();
if (ImGui::IsItemClicked()) {
// Click → load detail
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
refresh_issue_detail(iss.id);
}
@@ -65,26 +108,106 @@ static void draw_card(const Issue& iss) {
ImGui::EndDragDropSource();
}
// Right-click context menu
if (ImGui::BeginPopupContextItem("card_ctx")) {
if (iss.status == "bloqueado") {
if (ImGui::MenuItem(TI_LOCK_OPEN " Unblock (→ pendiente)")) {
patch_issue_status(iss.id, "pendiente");
}
} else {
if (ImGui::MenuItem(TI_LOCK " Block this issue")) {
patch_issue_status(iss.id, "bloqueado");
}
}
ImGui::Separator();
if (ImGui::MenuItem(TI_PLAYER_PLAY " Launch agent (fix-issue)", nullptr, false, !is_agent_active(iss.id))) {
launch_agent(iss.id);
}
if (is_agent_active(iss.id)) {
ImGui::TextDisabled(TI_ROBOT " agent already running");
}
ImGui::Separator();
if (ImGui::MenuItem("Move → ideas")) patch_issue_status(iss.id, "ideas");
if (ImGui::MenuItem("Move → pendiente")) patch_issue_status(iss.id, "pendiente");
if (ImGui::MenuItem("Move → in-progress")) patch_issue_status(iss.id, "in-progress");
if (ImGui::MenuItem("Move → completado")) patch_issue_status(iss.id, "completado");
ImGui::EndPopup();
}
ImGui::PopID();
}
static void draw_column(const char* status_key, std::vector<const Issue*>& items) {
static void draw_flow_card(const Flow& fl, int child_issue_count) {
ImGui::PushID(("flow_" + fl.id).c_str());
const float card_h = 88.0f;
ImVec2 size(0.0f, card_h);
ImU32 bg = IM_COL32(80, 60, 110, 100);
ImGui::PushStyleColor(ImGuiCol_ChildBg, bg);
ImGui::BeginChild("flow_card", size, true, ImGuiWindowFlags_NoScrollbar);
ImGui::TextColored(ImColor(190, 170, 230, 255), "%s flow %s", TI_GIT_BRANCH, fl.id.c_str());
{
ImVec2 ts = ImGui::CalcTextSize("999 issues");
float avail = ImGui::GetContentRegionAvail().x;
ImGui::SameLine(0, avail - ts.x - 4);
ImGui::TextDisabled("%d issues", child_issue_count);
}
std::string t = fl.title;
if (t.size() > 140) t = t.substr(0, 137) + "...";
ImGui::TextWrapped("%s", t.c_str());
if (!fl.tags.empty()) {
ImGui::TextColored(ImColor(140, 200, 255, 255), "%s %s", TI_TAG, fl.tags[0].c_str());
}
ImGui::EndChild();
ImGui::PopStyleColor();
// Click → set selected_issue_id to first child issue (best UX guess).
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
std::lock_guard<std::mutex> g(state().mu);
for (const auto& iss : state().issues) {
if (iss.flow == fl.id) {
state().selected_issue_id = iss.id;
state().selected_issue_detail = iss;
break;
}
}
}
if (ImGui::BeginPopupContextItem("flow_ctx")) {
ImGui::TextDisabled("flow %s — %d issues", fl.id.c_str(), child_issue_count);
ImGui::Separator();
ImGui::TextDisabled("(no actions; edit %s manually)", fl.file_path.c_str());
ImGui::EndPopup();
}
ImGui::PopID();
}
static void draw_column(const char* status_key, const char* icon,
std::vector<const Issue*>& issues,
std::vector<std::pair<const Flow*, int>>& flows) {
ImGui::TableNextColumn();
// Column header with count
ImGui::PushFont(nullptr);
ImGui::TextColored(ImColor(220, 220, 220, 255), "%s (%zu)", status_key, items.size());
size_t total = issues.size() + flows.size();
ImGui::TextColored(ImColor(220, 220, 220, 255), "%s %s (%zu)", icon, status_key, total);
ImGui::PopFont();
ImGui::Separator();
// Drop target on the whole column
ImVec2 region = ImGui::GetContentRegionAvail();
ImGui::BeginChild((std::string("col_") + status_key).c_str(), region, false);
for (const auto* iss : items) {
// Issues first (heavier visual), then flows (lighter).
for (const auto* iss : issues) {
draw_card(*iss);
}
// Bottom drop zone
ImGui::InvisibleButton("col_drop", ImVec2(-1, 16));
for (const auto& [fl, cnt] : flows) {
draw_flow_card(*fl, cnt);
}
ImGui::InvisibleButton("col_drop", ImVec2(-1, 24));
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* p = ImGui::AcceptDragDropPayload("KANBAN_ISSUE")) {
std::string id((const char*)p->Data, p->DataSize - 1);
@@ -109,37 +232,84 @@ void draw_board() {
return;
}
// Snapshot under lock
std::vector<Issue> snapshot;
std::vector<Issue> issue_snap;
std::vector<Flow> flow_snap;
bool agent_up = false;
int agent_running_count = 0;
std::string last_launch;
bool show_issues_flag = true;
bool show_flows_flag = true;
{
std::lock_guard<std::mutex> g(state().mu);
snapshot = state().issues;
issue_snap = state().issues;
flow_snap = state().flows;
agent_up = state().agent_runner_up;
agent_running_count = (int)state().agent_active.size();
last_launch = state().last_launch_msg;
show_issues_flag = state().filters.show_issues;
show_flows_flag = state().filters.show_flows;
}
// Bucket issues by status
std::vector<std::vector<const Issue*>> buckets(k_n_columns);
// Bucket issues per column.
std::vector<std::vector<const Issue*>> issue_buckets(k_n_columns);
int filtered_out = 0;
for (const auto& iss : snapshot) {
if (!passes_filters(iss)) {
filtered_out++;
continue;
}
for (int c = 0; c < k_n_columns; ++c) {
if (iss.status == k_columns[c]) {
buckets[c].push_back(&iss);
break;
if (show_issues_flag) {
for (const auto& iss : issue_snap) {
if (!passes_filters(iss)) {
filtered_out++;
continue;
}
std::string s = iss.status;
if (s == "bloqueado") s = "pendiente";
for (int c = 0; c < k_n_columns; ++c) {
if (s == k_columns[c]) {
issue_buckets[c].push_back(&iss);
break;
}
}
}
}
ImGui::Text("%zu total — %d filtered out", snapshot.size(), filtered_out);
// Count children per flow (issues that reference flow_id).
std::map<std::string, int> flow_child_count;
for (const auto& iss : issue_snap) {
if (!iss.flow.empty()) flow_child_count[iss.flow]++;
}
// Bucket flows per column using flow_column_index.
std::vector<std::vector<std::pair<const Flow*, int>>> flow_buckets(k_n_columns);
if (show_flows_flag) {
for (const auto& fl : flow_snap) {
int col = flow_column_index(fl.status);
int cnt = flow_child_count.count(fl.id) ? flow_child_count[fl.id] : 0;
flow_buckets[col].push_back({&fl, cnt});
}
}
// Header line
size_t total_visible = 0;
for (int c = 0; c < k_n_columns; ++c) total_visible += issue_buckets[c].size() + flow_buckets[c].size();
ImGui::Text("%zu issues + %zu flows — %d filtered out — visible %zu",
issue_snap.size(), flow_snap.size(), filtered_out, total_visible);
ImGui::SameLine();
ImGui::TextDisabled(" | ");
ImGui::SameLine();
if (agent_up) {
ImGui::TextColored(ImColor(120, 220, 120, 255), TI_ROBOT " agent_runner_api OK — %d active", agent_running_count);
} else {
ImGui::TextColored(ImColor(220, 140, 140, 255), TI_ROBOT " agent_runner_api offline");
}
if (!last_launch.empty()) {
ImGui::SameLine();
ImGui::TextDisabled(" | %s", last_launch.c_str());
}
ImGui::Separator();
if (ImGui::BeginTable("board_table", k_n_columns,
ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchSame)) {
ImGui::TableNextRow();
for (int c = 0; c < k_n_columns; ++c) {
draw_column(k_columns[c], buckets[c]);
draw_column(k_columns[c], k_column_icons[c], issue_buckets[c], flow_buckets[c]);
}
ImGui::EndTable();
}