#include "panels.h" #include "data.h" #include #include #include #include #include #include #include #include "core/icons_tabler.h" namespace kanban { 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); if (p == "media") return IM_COL32(120, 170, 255, 255); if (p == "baja") return IM_COL32(140, 140, 140, 255); 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()); 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); // 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(); ImU32 c = priority_color(iss.priority); ImDrawList* dl = ImGui::GetWindowDrawList(); float r = 5.0f; dl->AddCircleFilled(ImVec2(cur.x + r, cur.y + r + 2), r, c); ImGui::Dummy(ImVec2(2 * r + 4, 2 * r)); ImGui::SameLine(); ImGui::TextColored(ImColor(180, 180, 180, 255), "%s", iss.priority.c_str()); } // 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() > 140) t = t.substr(0, 137) + "..."; ImGui::TextWrapped("%s", t.c_str()); // Bottom: first domain chip if (!iss.domain.empty()) { ImGui::TextColored(ImColor(140, 200, 255, 255), "%s %s", TI_TAG, iss.domain[0].c_str()); } ImGui::EndChild(); ImGui::PopStyleColor(); // Click → load detail if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { refresh_issue_detail(iss.id); } // Drag source if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { ImGui::SetDragDropPayload("KANBAN_ISSUE", iss.id.c_str(), iss.id.size() + 1); ImGui::Text("%s %s", iss.id.c_str(), t.c_str()); 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_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 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& issues, std::vector>& flows) { ImGui::TableNextColumn(); ImGui::PushFont(nullptr); 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(); ImVec2 region = ImGui::GetContentRegionAvail(); ImGui::BeginChild((std::string("col_") + status_key).c_str(), region, false); // Issues first (heavier visual), then flows (lighter). for (const auto* iss : issues) { draw_card(*iss); } 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); patch_issue_status(id, status_key); } ImGui::EndDragDropTarget(); } ImGui::EndChild(); if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* p = ImGui::AcceptDragDropPayload("KANBAN_ISSUE")) { std::string id((const char*)p->Data, p->DataSize - 1); patch_issue_status(id, status_key); } ImGui::EndDragDropTarget(); } } void draw_board() { if (!ImGui::Begin(TI_LAYOUT_KANBAN " Board")) { ImGui::End(); return; } std::vector issue_snap; std::vector 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 g(state().mu); 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 per column. std::vector> issue_buckets(k_n_columns); int filtered_out = 0; 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; } } } } // Count children per flow (issues that reference flow_id). std::map 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>> 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], k_column_icons[c], issue_buckets[c], flow_buckets[c]); } ImGui::EndTable(); } ImGui::End(); } } // namespace kanban