feat: Launch workflow boton via agent_runner_api (issue 0116)
- Anade boton 'Launch workflow' (TI_ROCKET) que hace POST async a http://localhost:8486/api/runs con {issue_id, mode:'fix-issue'}. - HTTP async via std::thread + fn_http::request (de http_request_cpp_core) con timeout 3s. NO bloquea el frame. - Feature flag 'legacy_claude_fix' (default OFF) controla la visibilidad del boton 'Claude fix' legacy (terminal externa + claude --dangerously- skip-permissions). Flag leido al arrancar desde dev/feature_flags.json. - Toast 3s con run_id devuelto por el API (o error si :8486 down / transport fail / HTTP non-2xx). Render thread-safe con mutex. - CMakeLists.txt linkea cpp/functions/core/http_request.cpp. - app.md: version 0.1.0 -> 0.2.0, uses_functions anade http_request_cpp_core, capability growth log con entrada v0.2.0.
This commit is contained in:
@@ -2,6 +2,7 @@ add_imgui_app(skill_tree
|
|||||||
main.cpp
|
main.cpp
|
||||||
${CMAKE_SOURCE_DIR}/functions/core/parse_md_frontmatter.cpp
|
${CMAKE_SOURCE_DIR}/functions/core/parse_md_frontmatter.cpp
|
||||||
${CMAKE_SOURCE_DIR}/functions/core/compute_ring_layout.cpp
|
${CMAKE_SOURCE_DIR}/functions/core/compute_ring_layout.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/functions/core/http_request.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(skill_tree PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(skill_tree PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
name: skill_tree
|
name: skill_tree
|
||||||
lang: cpp
|
lang: cpp
|
||||||
domain: tools
|
domain: tools
|
||||||
|
version: 0.2.0
|
||||||
description: "Mapa interactivo de issues+flows en anillos concentricos por estado, click para spawn agentes"
|
description: "Mapa interactivo de issues+flows en anillos concentricos por estado, click para spawn agentes"
|
||||||
tags: [dashboard, meta, imgui]
|
tags: [dashboard, meta, imgui]
|
||||||
icon:
|
icon:
|
||||||
@@ -10,6 +11,7 @@ icon:
|
|||||||
uses_functions:
|
uses_functions:
|
||||||
- parse_md_frontmatter_cpp_core
|
- parse_md_frontmatter_cpp_core
|
||||||
- compute_ring_layout_cpp_core
|
- compute_ring_layout_cpp_core
|
||||||
|
- http_request_cpp_core
|
||||||
uses_types: []
|
uses_types: []
|
||||||
framework: "imgui"
|
framework: "imgui"
|
||||||
entry_point: "main.cpp"
|
entry_point: "main.cpp"
|
||||||
@@ -51,3 +53,14 @@ cd cpp && cmake --build build --target skill_tree -j
|
|||||||
## Estado
|
## Estado
|
||||||
|
|
||||||
MVP fase A — sub-issue 0109a: shell + parsers issues/flows. Sin render de grafo todavia.
|
MVP fase A — sub-issue 0109a: shell + parsers issues/flows. Sin render de grafo todavia.
|
||||||
|
|
||||||
|
|
||||||
|
## Capability growth log
|
||||||
|
|
||||||
|
Una linea por bump SemVer. Bump-type segun `.claude/commands/version.md`:
|
||||||
|
- `major`: breaking observable (CLI args, schema BBDD propia, formato wire).
|
||||||
|
- `minor`: feature aditiva (nuevo panel, endpoint, opcion).
|
||||||
|
- `patch`: bugfix sin cambio observable.
|
||||||
|
|
||||||
|
- v0.1.0 (2026-05-18) — baseline.
|
||||||
|
- v0.2.0 (2026-05-18) — boton `Launch workflow` (async POST :8486/api/runs via fn_http::request) + feature flag `legacy_claude_fix` que oculta el boton `Claude fix` (terminal externa). Toast con run_id 3s. Issue 0116.
|
||||||
|
|||||||
@@ -9,8 +9,10 @@
|
|||||||
#include "core/logger.h"
|
#include "core/logger.h"
|
||||||
#include "core/parse_md_frontmatter.h"
|
#include "core/parse_md_frontmatter.h"
|
||||||
#include "core/compute_ring_layout.h"
|
#include "core/compute_ring_layout.h"
|
||||||
|
#include "core/http_request.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
@@ -18,8 +20,10 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -241,6 +245,107 @@ static bool spawn_claude_terminal(const fs::path& registry_root) {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Feature flag (legacy_claude_fix) -----------------------------------
|
||||||
|
//
|
||||||
|
// Reads dev/feature_flags.json once at startup, caches the boolean. Minimal
|
||||||
|
// substring parser (no JSON library dependency at this scope — keeps the
|
||||||
|
// boot path tiny). Looks for "legacy_claude_fix" followed by "enabled":true
|
||||||
|
// inside the same object block. Defaults to false on any parse hiccup.
|
||||||
|
|
||||||
|
static bool g_legacy_claude_fix_flag = false;
|
||||||
|
|
||||||
|
static void load_feature_flags(const fs::path& registry_root) {
|
||||||
|
fs::path p = registry_root / "dev" / "feature_flags.json";
|
||||||
|
std::ifstream f(p);
|
||||||
|
if (!f.is_open()) {
|
||||||
|
g_legacy_claude_fix_flag = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::stringstream ss; ss << f.rdbuf();
|
||||||
|
std::string s = ss.str();
|
||||||
|
auto k = s.find("\"legacy_claude_fix\"");
|
||||||
|
if (k == std::string::npos) { g_legacy_claude_fix_flag = false; return; }
|
||||||
|
// Scan forward until the next closing brace; look for "enabled": true.
|
||||||
|
auto end = s.find('}', k);
|
||||||
|
if (end == std::string::npos) end = s.size();
|
||||||
|
auto en = s.find("\"enabled\"", k);
|
||||||
|
if (en == std::string::npos || en > end) { g_legacy_claude_fix_flag = false; return; }
|
||||||
|
auto colon = s.find(':', en);
|
||||||
|
if (colon == std::string::npos) { g_legacy_claude_fix_flag = false; return; }
|
||||||
|
// Skip whitespace and check for "true".
|
||||||
|
size_t i = colon + 1;
|
||||||
|
while (i < s.size() && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r')) ++i;
|
||||||
|
g_legacy_claude_fix_flag = (i + 4 <= s.size() && s.compare(i, 4, "true") == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Launch workflow (agent_runner_api POST) ----------------------------
|
||||||
|
//
|
||||||
|
// Async HTTP POST to http://localhost:8486/api/runs with
|
||||||
|
// { "issue_id": "<id>", "mode": "fix-issue" }. Toast carries the resulting
|
||||||
|
// run_id (or error) for 3 seconds. Uses fn_http::request on a detached
|
||||||
|
// std::thread so the ImGui frame is never blocked.
|
||||||
|
|
||||||
|
struct LaunchToast {
|
||||||
|
std::mutex mu;
|
||||||
|
std::string text; // human-readable message
|
||||||
|
bool ok = true;
|
||||||
|
double expires_at = -1.0; // seconds since steady_clock epoch
|
||||||
|
};
|
||||||
|
|
||||||
|
static LaunchToast g_launch_toast;
|
||||||
|
|
||||||
|
static void show_launch_toast(const std::string& msg, bool ok, double ttl_s = 3.0) {
|
||||||
|
std::lock_guard<std::mutex> lk(g_launch_toast.mu);
|
||||||
|
g_launch_toast.text = msg;
|
||||||
|
g_launch_toast.ok = ok;
|
||||||
|
g_launch_toast.expires_at = now_seconds() + ttl_s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string extract_json_string_field(const std::string& body, const std::string& key) {
|
||||||
|
std::string needle = "\"" + key + "\"";
|
||||||
|
auto k = body.find(needle);
|
||||||
|
if (k == std::string::npos) return {};
|
||||||
|
auto colon = body.find(':', k);
|
||||||
|
if (colon == std::string::npos) return {};
|
||||||
|
auto q1 = body.find('"', colon + 1);
|
||||||
|
if (q1 == std::string::npos) return {};
|
||||||
|
auto q2 = body.find('"', q1 + 1);
|
||||||
|
if (q2 == std::string::npos) return {};
|
||||||
|
return body.substr(q1 + 1, q2 - q1 - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void launch_workflow_async(const std::string& issue_id) {
|
||||||
|
show_launch_toast(std::string(TI_LOADER " launching workflow for ") + issue_id + "...", true, 30.0);
|
||||||
|
std::thread([issue_id]() {
|
||||||
|
fn_http::Request req;
|
||||||
|
req.method = "POST";
|
||||||
|
req.url = "http://localhost:8486/api/runs";
|
||||||
|
req.headers.push_back({"Content-Type", "application/json"});
|
||||||
|
// Minimal hand-rolled JSON — both fields are simple ASCII slugs.
|
||||||
|
req.body = "{\"issue_id\":\"" + issue_id + "\",\"mode\":\"fix-issue\"}";
|
||||||
|
req.timeout_ms = 3000;
|
||||||
|
fn_http::Response resp = fn_http::request(req);
|
||||||
|
if (resp.status == 0) {
|
||||||
|
show_launch_toast(std::string("agent_runner_api unreachable: ") + resp.error, false);
|
||||||
|
fn_log::log_warn("skill_tree: launch_workflow %s -> transport error: %s",
|
||||||
|
issue_id.c_str(), resp.error.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (resp.status >= 200 && resp.status < 300) {
|
||||||
|
std::string run_id = extract_json_string_field(resp.body, "run_id");
|
||||||
|
if (run_id.empty()) run_id = extract_json_string_field(resp.body, "id");
|
||||||
|
std::string msg = "run_id=" + (run_id.empty() ? std::string("(unknown)") : run_id);
|
||||||
|
show_launch_toast(msg, true);
|
||||||
|
fn_log::log_info("skill_tree: launch_workflow %s -> %s", issue_id.c_str(), msg.c_str());
|
||||||
|
} else {
|
||||||
|
char buf[128]; std::snprintf(buf, sizeof(buf), "HTTP %d", resp.status);
|
||||||
|
show_launch_toast(std::string(buf) + ": " + resp.body.substr(0, 80), false);
|
||||||
|
fn_log::log_warn("skill_tree: launch_workflow %s -> %s body=%s",
|
||||||
|
issue_id.c_str(), buf, resp.body.c_str());
|
||||||
|
}
|
||||||
|
}).detach();
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Scanner ------------------------------------------------------------
|
// ---- Scanner ------------------------------------------------------------
|
||||||
|
|
||||||
static void scan_dir(const fs::path& dir, NodeKind kind, ScanResult& out) {
|
static void scan_dir(const fs::path& dir, NodeKind kind, ScanResult& out) {
|
||||||
@@ -1123,7 +1228,12 @@ static void draw_inspector() {
|
|||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
// === Claude fix: lanza terminal externa con claude ===
|
// === Launch workflow / Claude fix ===
|
||||||
|
//
|
||||||
|
// Default path: POST to http://localhost:8486/api/runs and let the
|
||||||
|
// agent_runner_api spawn the worker headless. Legacy path (terminal
|
||||||
|
// external + interactive claude CLI) sits behind feature flag
|
||||||
|
// `legacy_claude_fix` in dev/feature_flags.json. Default OFF (issue 0116).
|
||||||
static double s_last_launch_t = -1e9;
|
static double s_last_launch_t = -1e9;
|
||||||
static bool s_last_launch_ok = true;
|
static bool s_last_launch_ok = true;
|
||||||
static std::string s_last_launch_id;
|
static std::string s_last_launch_id;
|
||||||
@@ -1131,6 +1241,20 @@ static void draw_inspector() {
|
|||||||
const Bucket b = status_bucket(n.status_eff);
|
const Bucket b = status_bucket(n.status_eff);
|
||||||
const bool fixable = (b != Bucket::Done);
|
const bool fixable = (b != Bucket::Done);
|
||||||
if (fixable) {
|
if (fixable) {
|
||||||
|
// Launch workflow (always available): async POST to agent_runner_api.
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 16, 185, 129, 220));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 52, 211, 153, 250));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 6, 148, 110, 255));
|
||||||
|
if (ImGui::Button(TI_ROCKET " Launch workflow")) {
|
||||||
|
launch_workflow_async(n.id);
|
||||||
|
fn_log::log_info("skill_tree: launch_workflow requested for %s", n.id.c_str());
|
||||||
|
}
|
||||||
|
ImGui::PopStyleColor(3);
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextDisabled("POST :8486/api/runs (fix-issue) — agent corre headless");
|
||||||
|
|
||||||
|
// Legacy Claude fix: only rendered when feature flag is ON.
|
||||||
|
if (g_legacy_claude_fix_flag) {
|
||||||
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(168, 85, 247, 200));
|
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(168, 85, 247, 200));
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(192, 132, 252, 240));
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(192, 132, 252, 240));
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(147, 51, 234, 255));
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(147, 51, 234, 255));
|
||||||
@@ -1143,11 +1267,29 @@ static void draw_inspector() {
|
|||||||
}
|
}
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextDisabled("abre terminal externa con `claude --dangerously-skip-permissions` en fn_registry");
|
ImGui::TextDisabled("legacy: abre terminal externa con `claude --dangerously-skip-permissions`");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ImGui::TextDisabled("Issue done — no fix necesario.");
|
ImGui::TextDisabled("Issue done — no fix necesario.");
|
||||||
}
|
}
|
||||||
if (s_last_launch_id == n.id && (now_seconds() - s_last_launch_t) < 5.0) {
|
|
||||||
|
// Toast del Launch workflow (run_id / error). Thread-safe lectura.
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(g_launch_toast.mu);
|
||||||
|
if (!g_launch_toast.text.empty() && now_seconds() < g_launch_toast.expires_at) {
|
||||||
|
if (g_launch_toast.ok) {
|
||||||
|
ImGui::TextColored(ImVec4(0.4f, 0.95f, 0.5f, 1.0f),
|
||||||
|
TI_CHECK " %s", g_launch_toast.text.c_str());
|
||||||
|
} else {
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f),
|
||||||
|
TI_ALERT_TRIANGLE " %s", g_launch_toast.text.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toast legacy Claude fix.
|
||||||
|
if (g_legacy_claude_fix_flag &&
|
||||||
|
s_last_launch_id == n.id && (now_seconds() - s_last_launch_t) < 5.0) {
|
||||||
if (s_last_launch_ok) {
|
if (s_last_launch_ok) {
|
||||||
ImGui::TextColored(ImVec4(0.4f, 0.95f, 0.5f, 1.0f),
|
ImGui::TextColored(ImVec4(0.4f, 0.95f, 0.5f, 1.0f),
|
||||||
TI_CHECK " terminal lanzada (revisa Windows Terminal / WSL)");
|
TI_CHECK " terminal lanzada (revisa Windows Terminal / WSL)");
|
||||||
@@ -1385,6 +1527,9 @@ static void render() {
|
|||||||
g_root = discover_registry_root();
|
g_root = discover_registry_root();
|
||||||
fn_log::log_info("skill_tree: discover_registry_root -> '%s'",
|
fn_log::log_info("skill_tree: discover_registry_root -> '%s'",
|
||||||
g_root.empty() ? "(empty)" : g_root.string().c_str());
|
g_root.empty() ? "(empty)" : g_root.string().c_str());
|
||||||
|
load_feature_flags(g_root);
|
||||||
|
fn_log::log_info("skill_tree: legacy_claude_fix flag = %s",
|
||||||
|
g_legacy_claude_fix_flag ? "ON" : "OFF");
|
||||||
reload_scan();
|
reload_scan();
|
||||||
}
|
}
|
||||||
if (g_show_tree) draw_tree();
|
if (g_show_tree) draw_tree();
|
||||||
|
|||||||
Reference in New Issue
Block a user