feat(0036d): promote kind-aware (Group → clear group_id)

NodeGroups window kind=Group ahora expone un boton SmallButton(TI_ARROW_UP)
por fila que saca la entidad del grupo (group_id = NULL) y dispara
reload del grafo. kind=Table mantiene el comportamiento de issue 0011.

- entity_ops: nueva op `entity_clear_group_id(db, id)` idempotente. Si
  la columna group_id no existe (BD pre-0035a) retorna true como no-op.
  Falla solo si la entidad no existe o SQLite revienta.
- views.cpp: extra columna "promote" en kind=Group, tooltip header
  diferenciado por kind, boton conectado a app.want_clear_group_id_entity.
- main.cpp: handler que ejecuta entity_clear_group_id, marca windows
  como dirty, llama reload_after_mutation y loguea
  `[node_groups] promoted X out of group`.
- gx-cli: flag `node update --clear-group-id` (booleano) y exposicion
  MCP en inputSchema + MCP_DISPATCH defaults para que el agente Echo
  pueda promover via tool calls.
- tests: 3 nuevos CLI (clear, idempotente, combinable con --name) y
  4 MCP (defaults, schema, dispatch end-to-end, idempotente).

WSL: 102 passed (95 base + 7).
Windows: 91 passed, 11 skipped (84 base + 7).

Refs: issues/0036d-promote-kind-aware.md
This commit is contained in:
2026-05-04 01:03:11 +02:00
parent 98e744ea4e
commit f0d8a5ad04
8 changed files with 311 additions and 5 deletions
+23 -3
View File
@@ -2025,6 +2025,13 @@ void views_node_groups_window(AppState& app) {
m.duckdb_path.c_str(), m.table_name.c_str(),
(long long)w.total_rows);
}
// Issue 0036d: tooltip suave que explica el promote segun kind.
if (is_group) {
ImGui::TextDisabled("Promote: saca el nodo del grupo");
} else {
ImGui::TextDisabled(
"Promote: convierte fila DuckDB en entidad del grafo");
}
if (!w.last_error.empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
"ERROR: %s", w.last_error.c_str());
@@ -2033,10 +2040,11 @@ void views_node_groups_window(AppState& app) {
// Tabla — layout depende del kind:
// Table: [id_column] + columns[] + [promoted] (col_count = N+2)
// Group: columns[] (id, name, type_ref, status, updated_at)
// (col_count = N)
// Group: columns[] + [promote] (col_count = N+1)
// (issue 0036d: ultima columna lleva un boton TI_ARROW_UP
// para sacar la entidad del grupo.)
const int col_count = is_group
? (int)m.columns.size()
? (int)m.columns.size() + 1
: (int)m.columns.size() + 2;
ImGuiTableFlags tflags =
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
@@ -2053,6 +2061,9 @@ void views_node_groups_window(AppState& app) {
: ImGuiTableColumnFlags_WidthStretch,
is_id ? 160.0f : 0.0f);
}
// 0036d: columna extra para el boton Promote-out-of-group.
ImGui::TableSetupColumn("promote",
ImGuiTableColumnFlags_WidthFixed, 60.0f);
} else {
ImGui::TableSetupColumn(m.id_column.empty() ? "id" : m.id_column.c_str(),
ImGuiTableColumnFlags_WidthFixed, 100.0f);
@@ -2101,6 +2112,15 @@ void views_node_groups_window(AppState& app) {
if (c < row.values.size())
ImGui::TextUnformatted(row.values[c].c_str());
}
// 0036d: boton Promote-out-of-group en la ultima columna.
ImGui::TableSetColumnIndex(col_count - 1);
if (ImGui::SmallButton(TI_ARROW_UP "##promote_grp")) {
app.want_clear_group_id_entity = true;
app.clear_group_id_entity_id = row.id;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Promote out of group (move to canvas)");
}
} else {
// kind=Table — comportamiento original (DuckDB-backed).
bool is_promoted = !row.promoted_entity_id.empty();