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:
@@ -9,8 +9,10 @@
|
||||
#include "core/logger.h"
|
||||
#include "core/parse_md_frontmatter.h"
|
||||
#include "core/compute_ring_layout.h"
|
||||
#include "core/http_request.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
@@ -18,8 +20,10 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
@@ -241,6 +245,107 @@ static bool spawn_claude_terminal(const fs::path& registry_root) {
|
||||
#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 ------------------------------------------------------------
|
||||
|
||||
static void scan_dir(const fs::path& dir, NodeKind kind, ScanResult& out) {
|
||||
@@ -1123,7 +1228,12 @@ static void draw_inspector() {
|
||||
|
||||
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 bool s_last_launch_ok = true;
|
||||
static std::string s_last_launch_id;
|
||||
@@ -1131,23 +1241,55 @@ static void draw_inspector() {
|
||||
const Bucket b = status_bucket(n.status_eff);
|
||||
const bool fixable = (b != Bucket::Done);
|
||||
if (fixable) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(168, 85, 247, 200));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(192, 132, 252, 240));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(147, 51, 234, 255));
|
||||
if (ImGui::Button(TI_TERMINAL_2 " Claude fix")) {
|
||||
s_last_launch_ok = spawn_claude_terminal(g_root);
|
||||
s_last_launch_t = now_seconds();
|
||||
s_last_launch_id = n.id;
|
||||
fn_log::log_info("skill_tree: spawn claude terminal for %s -> %s",
|
||||
n.id.c_str(), s_last_launch_ok ? "ok" : "fail");
|
||||
// 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("abre terminal externa con `claude --dangerously-skip-permissions` en fn_registry");
|
||||
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_ButtonHovered, IM_COL32(192, 132, 252, 240));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(147, 51, 234, 255));
|
||||
if (ImGui::Button(TI_TERMINAL_2 " Claude fix")) {
|
||||
s_last_launch_ok = spawn_claude_terminal(g_root);
|
||||
s_last_launch_t = now_seconds();
|
||||
s_last_launch_id = n.id;
|
||||
fn_log::log_info("skill_tree: spawn claude terminal for %s -> %s",
|
||||
n.id.c_str(), s_last_launch_ok ? "ok" : "fail");
|
||||
}
|
||||
ImGui::PopStyleColor(3);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("legacy: abre terminal externa con `claude --dangerously-skip-permissions`");
|
||||
}
|
||||
} else {
|
||||
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) {
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.95f, 0.5f, 1.0f),
|
||||
TI_CHECK " terminal lanzada (revisa Windows Terminal / WSL)");
|
||||
@@ -1385,6 +1527,9 @@ static void render() {
|
||||
g_root = discover_registry_root();
|
||||
fn_log::log_info("skill_tree: discover_registry_root -> '%s'",
|
||||
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();
|
||||
}
|
||||
if (g_show_tree) draw_tree();
|
||||
|
||||
Reference in New Issue
Block a user