fix(skill_tree): tooltip width fixed + Claude fix button

Tooltip:
- Fija ancho 360px via SetNextWindowSize(Always) + PushTextWrapPos antes de
  BeginTooltip. Soluciona el flash de ventana enorme en primer frame
  cuando TextWrapped no tenia wrap width definido.

Claude fix button (Inspector):
- Aparece solo si status != done (Bucket::Planned o Todo).
- Color violeta-500 destacado.
- On click: lanza terminal externa con
    wt.exe wsl.exe --cd ~/fn_registry -- bash -ic 'claude --dangerously-skip-permissions'
- Fallback: cmd.exe /C start wt.exe ... si CreateProcessA falla.
- Linux: probe x-terminal-emulator/gnome-terminal/konsole/xterm/alacritty/
  kitty hasta encontrar uno disponible.
- Feedback temporal 5s ok/fail tras click.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 20:28:23 +02:00
parent 094e4a73ee
commit c0ad94e050
+96 -1
View File
@@ -1,4 +1,8 @@
#include <imgui.h>
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
#endif
#include "app_base.h"
#include "core/panel_menu.h"
#include "core/icons_tabler.h"
@@ -164,6 +168,56 @@ static double now_seconds() {
return duration_cast<duration<double>>(steady_clock::now().time_since_epoch()).count();
}
// Lanza terminal externa con `claude --dangerously-skip-permissions` en el
// repo fn_registry. En Windows usa Windows Terminal + WSL; en Linux usa el
// primer emulador disponible. Fire-and-forget; no captura output.
static bool spawn_claude_terminal(const fs::path& registry_root) {
#ifdef _WIN32
// wt.exe lanza Windows Terminal. wsl.exe --cd cambia cwd antes del comando.
// Bash -ic para que /etc/profile cargue PATH (claude esta en ~/.local/bin
// o equivalente). Sin comillas el resto del comando.
std::string cmd = "wt.exe new-tab wsl.exe --cd ~/fn_registry -- bash -ic \"claude --dangerously-skip-permissions\"";
STARTUPINFOA si{}; si.cb = sizeof(si);
PROCESS_INFORMATION pi{};
std::string mutable_cmd = cmd; // CreateProcessA needs non-const
BOOL ok = CreateProcessA(nullptr, mutable_cmd.data(), nullptr, nullptr, FALSE,
CREATE_NEW_CONSOLE, nullptr, nullptr, &si, &pi);
if (!ok) {
// Fallback: cmd.exe + start wt.exe (less reliable but works without explicit path).
std::string fb = "cmd.exe /C start \"\" wt.exe wsl.exe --cd ~/fn_registry -- bash -ic \"claude --dangerously-skip-permissions\"";
mutable_cmd = fb;
ok = CreateProcessA(nullptr, mutable_cmd.data(), nullptr, nullptr, FALSE,
CREATE_NEW_CONSOLE, nullptr, nullptr, &si, &pi);
}
if (ok) {
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return true;
}
return false;
#else
// Linux: probar varios emuladores comunes.
const std::string cd = "cd '" + registry_root.string() + "' && exec claude --dangerously-skip-permissions";
const char* candidates[] = {
"x-terminal-emulator", "gnome-terminal", "konsole", "xterm", "alacritty", "kitty",
};
for (const char* term : candidates) {
std::string probe = std::string("command -v ") + term + " >/dev/null 2>&1";
int probe_rc = std::system(probe.c_str());
if (probe_rc != 0) continue;
std::string cmd;
if (std::string(term) == "gnome-terminal") {
cmd = std::string(term) + " -- bash -ic \"" + cd + "\" &";
} else {
cmd = std::string(term) + " -e bash -ic \"" + cd + "\" &";
}
(void)!std::system(cmd.c_str());
return true;
}
return false;
#endif
}
// ---- Scanner ------------------------------------------------------------
static void scan_dir(const fs::path& dir, NodeKind kind, ScanResult& out) {
@@ -614,15 +668,20 @@ static void draw_canvas() {
dl->AddText(tp, IM_COL32(255, 255, 255, 245), lbl);
}
// Tooltip on hover (title).
// Tooltip on hover (title). Fix width up-front so the first frame
// doesnt flash a giant window before reflow.
if (over) {
const float kTipW = 360.0f;
ImGui::SetNextWindowSize(ImVec2(kTipW, 0.0f), ImGuiCond_Always);
ImGui::BeginTooltip();
ImGui::PushTextWrapPos(kTipW - 16.0f);
ImGui::Text("%s %s", n.kind == NodeKind::Issue ? "[ISSUE]" : "[FLOW]", n.id.c_str());
ImGui::TextWrapped("%s", n.title.c_str());
ImGui::TextDisabled("status: %s · ring: %s · domain: %s",
n.status_eff.c_str(),
ring_label(n.ring),
n.domain.empty() ? "?" : n.domain.front().c_str());
ImGui::PopTextWrapPos();
ImGui::EndTooltip();
}
};
@@ -716,6 +775,42 @@ static void draw_inspector() {
for (const auto& r : n.related) ImGui::BulletText("%s", r.c_str());
}
ImGui::Separator();
// === Claude fix: lanza terminal externa con claude ===
static double s_last_launch_t = -1e9;
static bool s_last_launch_ok = true;
static std::string s_last_launch_id;
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");
}
ImGui::PopStyleColor(3);
ImGui::SameLine();
ImGui::TextDisabled("abre terminal externa con `claude --dangerously-skip-permissions` en fn_registry");
} else {
ImGui::TextDisabled("Issue done — no fix necesario.");
}
if (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)");
} else {
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f),
TI_ALERT_TRIANGLE " no se pudo lanzar terminal");
}
}
ImGui::Separator();
ImGui::TextDisabled("Botones [Generate ideas] / [Run autonomous-task] llegan en 0109e/f.");
ImGui::End();