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:
+203
-33
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user