@manny-est/node-red-flowpilot 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/README.md +21 -7
- package/USER-GUIDE.md +22 -13
- package/flowpilot-core.css +128 -2
- package/flowpilot.js +382 -27
- package/lib/build-system-prompt.js +26 -4
- package/lib/core/apply-review.js +234 -53
- package/lib/core/init.js +10 -0
- package/lib/core/main.js +92 -6
- package/lib/core/modes.js +698 -24
- package/lib/core/selection-context.js +28 -1
- package/lib/default-system-prompt.js +13 -9
- package/lib/document-system-prompt.js +19 -30
- package/lib/generation-system-prompt.js +24 -40
- package/lib/modify-system-prompt.js +98 -70
- package/lib/prompt-fragments.js +45 -0
- package/lib/provider-anthropic.js +385 -0
- package/lib/provider-openai-compatible.js +12 -3
- package/lib/storage.js +43 -2
- package/lib/validator.js +238 -0
- package/package.json +1 -1
package/lib/core/apply-review.js
CHANGED
|
@@ -28,7 +28,21 @@
|
|
|
28
28
|
// throwing) AND on disk (each string serializes with id
|
|
29
29
|
// undefined -> null, confirmed via a live flows.json). Group
|
|
30
30
|
// membership must only ever change through applyGroupChanges().
|
|
31
|
-
nodes: 1
|
|
31
|
+
nodes: 1,
|
|
32
|
+
// Node appearance/documentation fields and node-type-specific
|
|
33
|
+
// internal defaults that models hallucinate sentinel placeholders
|
|
34
|
+
// for during build-loop reviews. None of these are meaningfully
|
|
35
|
+
// settable via a Modify patch — they're either Appearance-tab
|
|
36
|
+
// metadata or type-internal runtime flags. Adding them here
|
|
37
|
+
// prevents false "redaction" blocks with no actionable remediation.
|
|
38
|
+
// Appearance/docs (all node types):
|
|
39
|
+
info: 1, inputLabels: 1, outputLabels: 1, icon: 1,
|
|
40
|
+
// debug node internal display flags:
|
|
41
|
+
console: 1, tostatus: 1, targetType: 1, statusVal: 1, statusType: 1,
|
|
42
|
+
// function node internal flags:
|
|
43
|
+
noerr: 1, initialize: 1, finalize: 1,
|
|
44
|
+
// mqtt in/out retain handling:
|
|
45
|
+
rh: 1
|
|
32
46
|
};
|
|
33
47
|
|
|
34
48
|
// Sentinel strings written by sanitizeNode (and redactDebugValue) for
|
|
@@ -113,7 +127,7 @@
|
|
|
113
127
|
// internally for t:"add" events) and pushes one compound history entry so
|
|
114
128
|
// a single Ctrl+Z removes both the new nodes and their wires together.
|
|
115
129
|
function applyInsertions(newNodes, newWires, contextNodeIds) {
|
|
116
|
-
if (!newNodes || !newNodes.length) { return; }
|
|
130
|
+
if ((!newNodes || !newNodes.length) && (!newWires || !newWires.length)) { return; }
|
|
117
131
|
|
|
118
132
|
// Determine z (flow-tab id) from the active workspace.
|
|
119
133
|
var z = "";
|
|
@@ -246,6 +260,57 @@
|
|
|
246
260
|
});
|
|
247
261
|
});
|
|
248
262
|
|
|
263
|
+
// Collision-avoidance stagger: independent components that happen to
|
|
264
|
+
// share the same anchor (e.g. several standalone new nodes each
|
|
265
|
+
// wired from/to the SAME existing node — "add 5 debug taps after
|
|
266
|
+
// this http request node") compute identical target coordinates
|
|
267
|
+
// above and land exactly on top of each other. Detect a component
|
|
268
|
+
// whose origin collides with an earlier one and nudge the WHOLE
|
|
269
|
+
// component (preserving its own internal layout) into a small grid
|
|
270
|
+
// offset from the first occupant, so multi-node insertions never
|
|
271
|
+
// pile invisibly at one point.
|
|
272
|
+
var occupiedOrigins = [];
|
|
273
|
+
function collidesWithOccupied(x, y) {
|
|
274
|
+
return occupiedOrigins.some(function (p) {
|
|
275
|
+
return Math.abs(p.x - x) < 20 && Math.abs(p.y - y) < 20;
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
Object.keys(componentAnchors).forEach(function (c) {
|
|
279
|
+
var members = laid.filter(function (n) { return componentOf[n.id] === c; });
|
|
280
|
+
if (!members.length) { return; }
|
|
281
|
+
var baseX = members.reduce(function (m, n) { return Math.min(m, n.x); }, members[0].x);
|
|
282
|
+
var baseY = members.reduce(function (m, n) { return Math.min(m, n.y); }, members[0].y);
|
|
283
|
+
// Walk a single-column grid outward from the natural origin
|
|
284
|
+
// until we find a slot nothing else already occupies — one
|
|
285
|
+
// node per row, never two at the same y (CLAUDE-012: Manny's
|
|
286
|
+
// original spec, which the earlier 2-wide grid still violated
|
|
287
|
+
// by putting a pair at the same vertical level). After 5 nodes
|
|
288
|
+
// in a column, wrap to a new column further right than the
|
|
289
|
+
// first node in the previous one, so wires stay visually
|
|
290
|
+
// distinguishable instead of stacking on top of each other.
|
|
291
|
+
var slot = 0;
|
|
292
|
+
var x = baseX, y = baseY;
|
|
293
|
+
while (collidesWithOccupied(x, y) && slot < 200) {
|
|
294
|
+
slot++;
|
|
295
|
+
x = baseX + Math.floor(slot / 5) * 250;
|
|
296
|
+
y = baseY + (slot % 5) * 90;
|
|
297
|
+
}
|
|
298
|
+
if (slot > 0) {
|
|
299
|
+
var dx = x - baseX, dy = y - baseY;
|
|
300
|
+
members.forEach(function (n) { n.x += dx; n.y += dy; });
|
|
301
|
+
}
|
|
302
|
+
occupiedOrigins.push({ x: x, y: y });
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// B3: snapshot placeholder→type before ids are overwritten, so wire
|
|
306
|
+
// failure messages can name the placeholder id and type rather than the
|
|
307
|
+
// opaque real hex id the node was never given (node not inserted = id
|
|
308
|
+
// exists in idMap but never landed in the live graph).
|
|
309
|
+
var placeholderToType = {};
|
|
310
|
+
laid.forEach(function (n) {
|
|
311
|
+
if (placeholderIds[n.id]) { placeholderToType[n.id] = n.type || "?"; }
|
|
312
|
+
});
|
|
313
|
+
|
|
249
314
|
// Build placeholder-id → real-id map and assign real ids + z.
|
|
250
315
|
var idMap = {};
|
|
251
316
|
laid.forEach(function (n) {
|
|
@@ -338,6 +403,15 @@
|
|
|
338
403
|
? typeDef.outputs
|
|
339
404
|
: (Array.isArray(n.wires) ? n.wires.length : 0);
|
|
340
405
|
}
|
|
406
|
+
// The model's "wires" array can disagree with the node type's
|
|
407
|
+
// real port count (e.g. a hallucinated single empty port on a
|
|
408
|
+
// 0-output type like http response) — the editor draws one
|
|
409
|
+
// anchor per n.wires entry regardless of n.outputs, so a
|
|
410
|
+
// mismatch renders a phantom port. Reconcile the array length
|
|
411
|
+
// to the authoritative n.outputs count now that it's resolved.
|
|
412
|
+
if (!Array.isArray(n.wires)) { n.wires = []; }
|
|
413
|
+
while (n.wires.length < n.outputs) { n.wires.push([]); }
|
|
414
|
+
if (n.wires.length > n.outputs) { n.wires.length = n.outputs; }
|
|
341
415
|
// Apply type-definition defaults for any property the model omitted.
|
|
342
416
|
// This covers required fields (e.g. statusVal/statusType on debug)
|
|
343
417
|
// that oneditsave would normally set, preventing a spurious triangle.
|
|
@@ -396,7 +470,16 @@
|
|
|
396
470
|
var fromNode = findLiveNode(fromId);
|
|
397
471
|
var toNode = findLiveNode(toId);
|
|
398
472
|
if (!fromNode || !toNode) {
|
|
399
|
-
|
|
473
|
+
var _failRef = !fromNode ? wire.from : wire.to;
|
|
474
|
+
var _failMsg;
|
|
475
|
+
if (placeholderIds[_failRef]) {
|
|
476
|
+
_failMsg = "Cannot wire — referenced node was not inserted (" +
|
|
477
|
+
_failRef + " / type: " + (placeholderToType[_failRef] || "?") + ")";
|
|
478
|
+
} else {
|
|
479
|
+
_failMsg = "Cannot wire — node not found in editor: " +
|
|
480
|
+
(!fromNode ? fromId : toId);
|
|
481
|
+
}
|
|
482
|
+
addMessage("error", _failMsg);
|
|
400
483
|
return;
|
|
401
484
|
}
|
|
402
485
|
var fromPort = wire.fromPort || 0;
|
|
@@ -524,12 +607,13 @@
|
|
|
524
607
|
var propertyChanges = [];
|
|
525
608
|
var wiresChanged = false;
|
|
526
609
|
var redactionSkips = 0;
|
|
610
|
+
var redactedKeys = [];
|
|
527
611
|
Object.keys(modNode).forEach(function (k) {
|
|
528
612
|
if (DIFF_SKIP[k]) { return; }
|
|
529
613
|
var newRaw = modNode[k];
|
|
530
614
|
// If the model echoed a sanitizer sentinel, the field is opaque —
|
|
531
615
|
// we can't meaningfully compare or apply it, so skip entirely.
|
|
532
|
-
if (isSanitizeSentinel(newRaw)) { redactionSkips++; return; }
|
|
616
|
+
if (isSanitizeSentinel(newRaw)) { redactionSkips++; redactedKeys.push(k); return; }
|
|
533
617
|
var oldRaw = liveNode ? liveNode[k] : undefined;
|
|
534
618
|
var oldStr, newStr;
|
|
535
619
|
try { oldStr = JSON.stringify(oldRaw); } catch (e) { oldStr = String(oldRaw); }
|
|
@@ -538,7 +622,7 @@
|
|
|
538
622
|
if (k === "wires") { wiresChanged = true; return; }
|
|
539
623
|
propertyChanges.push({ key: k, oldVal: oldRaw, newVal: newRaw });
|
|
540
624
|
});
|
|
541
|
-
return { propertyChanges: propertyChanges, wiresChanged: wiresChanged, redactionSkips: redactionSkips };
|
|
625
|
+
return { propertyChanges: propertyChanges, wiresChanged: wiresChanged, redactionSkips: redactionSkips, redactedKeys: redactedKeys };
|
|
542
626
|
}
|
|
543
627
|
|
|
544
628
|
// Diff outgoing wires for an existing node: compare what's live in the graph
|
|
@@ -642,6 +726,7 @@
|
|
|
642
726
|
propertyChanges: diff.propertyChanges,
|
|
643
727
|
wiresChanged: diff.wiresChanged,
|
|
644
728
|
redactionSkips: diff.redactionSkips,
|
|
729
|
+
redactedKeys: diff.redactedKeys,
|
|
645
730
|
wiresDiff: wiresDiff,
|
|
646
731
|
name: modNode.name || (liveNode && liveNode.name) || "",
|
|
647
732
|
type: modNode.type || (liveNode && liveNode.type) || ""
|
|
@@ -662,7 +747,7 @@
|
|
|
662
747
|
var hasNewNodes = newNodes.length > 0;
|
|
663
748
|
var hasRemoveNodes = removeNodes.length > 0;
|
|
664
749
|
var hasNewGroups = newGroups.length > 0;
|
|
665
|
-
var hasAnyChanges = hasPropChanges || hasWireChanges || hasNewNodes || hasRemoveNodes || hasNewGroups;
|
|
750
|
+
var hasAnyChanges = hasPropChanges || hasWireChanges || hasNewNodes || hasRemoveNodes || hasNewGroups || (newWires && newWires.length > 0);
|
|
666
751
|
var totalRedactionSkips = nodeDiffs.reduce(function (s, d) { return s + (d.redactionSkips || 0); }, 0);
|
|
667
752
|
|
|
668
753
|
var $msg = $("<div>").addClass("fp-message fp-review");
|
|
@@ -715,15 +800,27 @@
|
|
|
715
800
|
});
|
|
716
801
|
if (redactionOnlyDiffs.length > 0) {
|
|
717
802
|
var redSectionTop = hasPropChanges ? "12px" : "0";
|
|
718
|
-
|
|
719
|
-
.
|
|
803
|
+
var totalRedFields = redactionOnlyDiffs.reduce(function (s, d) {
|
|
804
|
+
return s + (Array.isArray(d.redactedKeys) && d.redactedKeys.length ? d.redactedKeys.length : 1);
|
|
805
|
+
}, 0);
|
|
806
|
+
var $redToggle = $("<div>").addClass("fp-review-count")
|
|
807
|
+
.css({ "margin-top": redSectionTop, "cursor": "pointer" })
|
|
808
|
+
.text("▶ Not applied — FlowPilot can’t change these directly (" + totalRedFields + " field(s))")
|
|
720
809
|
.appendTo($summaryPanel);
|
|
810
|
+
var $redContent = $("<div>").addClass("fp-hidden").appendTo($summaryPanel);
|
|
811
|
+
$redToggle.on("click", function () {
|
|
812
|
+
var collapsed = $redContent.hasClass("fp-hidden");
|
|
813
|
+
$redContent.toggleClass("fp-hidden", !collapsed);
|
|
814
|
+
$redToggle.text((collapsed ? "▼" : "▶") + " Not applied — FlowPilot can’t change these directly (" + totalRedFields + " field(s))");
|
|
815
|
+
});
|
|
721
816
|
redactionOnlyDiffs.forEach(function (d) {
|
|
722
|
-
var $section = $("<div>").addClass("fp-diff-node").appendTo($
|
|
817
|
+
var $section = $("<div>").addClass("fp-diff-node").appendTo($redContent);
|
|
723
818
|
var title = d.type + (d.name ? " — \"" + d.name + "\"" : "");
|
|
724
819
|
$("<div>").addClass("fp-diff-node-title").text(title).appendTo($section);
|
|
820
|
+
var keys = Array.isArray(d.redactedKeys) && d.redactedKeys.length ? d.redactedKeys : [];
|
|
821
|
+
var fieldList = keys.length ? keys.join(", ") : "one or more fields";
|
|
725
822
|
$("<div>").addClass("fp-diff-warn")
|
|
726
|
-
.text("
|
|
823
|
+
.text("Cannot change “" + fieldList + "” — FlowPilot cannot read the live value for this field (internal node config). Edit it directly in the node editor if needed.")
|
|
727
824
|
.appendTo($section);
|
|
728
825
|
});
|
|
729
826
|
}
|
|
@@ -845,7 +942,7 @@
|
|
|
845
942
|
if (!hasAnyChanges) {
|
|
846
943
|
if (totalRedactionSkips > 0) {
|
|
847
944
|
$("<div>").addClass("fp-warning")
|
|
848
|
-
.text("
|
|
945
|
+
.text("None of the proposed changes could be applied — all targeted fields are internal node config that FlowPilot cannot read or modify directly. See the field names above and edit them in the node editor if needed.")
|
|
849
946
|
.appendTo($actions);
|
|
850
947
|
} else {
|
|
851
948
|
$("<div>").addClass("fp-review-hint")
|
|
@@ -894,34 +991,49 @@
|
|
|
894
991
|
});
|
|
895
992
|
$msg.attr("data-fp-record-id", _modRecord.id);
|
|
896
993
|
|
|
994
|
+
var hintParts = [];
|
|
995
|
+
if (hasPropChanges || hasWireChanges) { hintParts.push("changes mutate live nodes"); }
|
|
996
|
+
if (hasRemoveNodes) { hintParts.push("removals delete nodes"); }
|
|
997
|
+
if (hasNewGroups) { hintParts.push("groups are created/updated"); }
|
|
998
|
+
if (hasNewNodes) { hintParts.push("insertions add new nodes"); }
|
|
999
|
+
if (!hasNewNodes && newWires && newWires.length > 0) { hintParts.push("connections added"); }
|
|
1000
|
+
var applySub = hintParts.length ? "Review above — " + hintParts.join(", ") + ". Ctrl+Z to undo." : "";
|
|
897
1001
|
var $applyBtn = $("<button>")
|
|
898
|
-
.addClass("
|
|
1002
|
+
.addClass("fp-chip fp-chip-card")
|
|
899
1003
|
.attr("type", "button")
|
|
900
|
-
.text(btnLabel)
|
|
901
1004
|
.on("click", function () {
|
|
902
|
-
$applyBtn.prop("disabled", true)
|
|
1005
|
+
$applyBtn.prop("disabled", true);
|
|
1006
|
+
$applyTitle.text("Applying…");
|
|
903
1007
|
// Insertions run FIRST so their placeholder→real-id map is
|
|
904
1008
|
// available to applyModifications/applyGroupChanges — an
|
|
905
1009
|
// existing node's rewired "wires" (Tier 3) or a new
|
|
906
1010
|
// group's membership may point at a node being inserted
|
|
907
1011
|
// in this same response.
|
|
908
1012
|
var idMap = {};
|
|
909
|
-
|
|
1013
|
+
var hasNewWires = newWires && newWires.length > 0;
|
|
1014
|
+
if (hasNewNodes || hasNewWires) {
|
|
910
1015
|
idMap = applyInsertions(newNodes, newWires, nodes.map(function (n) { return n.id; })) || {};
|
|
911
1016
|
}
|
|
912
|
-
|
|
1017
|
+
// W4 Phase 2: applyCallback also runs the verify read-back,
|
|
1018
|
+
// so it must fire for insertion-only Modifies too (no
|
|
1019
|
+
// property/wire change on an EXISTING node), not just when
|
|
1020
|
+
// hasMutations is true — otherwise "exists"/"wire" checks on
|
|
1021
|
+
// newly inserted nodes would never run.
|
|
1022
|
+
if ((hasMutations || hasNewNodes || hasNewWires) && applyCallback) {
|
|
1023
|
+
applyCallback(nodeDiffs, removeNodes, null, idMap);
|
|
1024
|
+
}
|
|
913
1025
|
if (hasNewGroups) { applyGroupChanges(newGroups, idMap); }
|
|
914
1026
|
if (_modRecord) { _modRecord.state = "applied"; }
|
|
915
|
-
$
|
|
1027
|
+
$applyTitle.text("Done ✓");
|
|
916
1028
|
});
|
|
1029
|
+
$("<span>").addClass("fp-chip-icon")
|
|
1030
|
+
.append($("<i>").addClass(buildFixInfo ? "fa fa-wrench" : "fa fa-check-circle"))
|
|
1031
|
+
.appendTo($applyBtn);
|
|
1032
|
+
var $applyBody = $("<span>").addClass("fp-chip-body").appendTo($applyBtn);
|
|
1033
|
+
var $applyTitle = $("<span>").addClass("fp-chip-title").text(buildFixInfo ? "Apply Fix" : btnLabel).appendTo($applyBody);
|
|
1034
|
+
$("<span>").addClass("fp-chip-sub").text(buildFixInfo && buildFixInfo.capReached ? "Apply final fix and stop the loop" : applySub).appendTo($applyBody);
|
|
1035
|
+
$("<span>").addClass("fp-chip-go").html("›").appendTo($applyBtn);
|
|
917
1036
|
$actions.append($applyBtn);
|
|
918
|
-
var hintParts = [];
|
|
919
|
-
if (hasPropChanges || hasWireChanges) { hintParts.push("changes mutate live nodes"); }
|
|
920
|
-
if (hasRemoveNodes) { hintParts.push("removals delete nodes"); }
|
|
921
|
-
if (hasNewGroups) { hintParts.push("groups are created/updated"); }
|
|
922
|
-
if (hasNewNodes) { hintParts.push("insertions add new nodes"); }
|
|
923
|
-
var hintText = "Review above — " + hintParts.join(", ") + ". Ctrl+Z to undo.";
|
|
924
|
-
$("<span>").addClass("fp-review-hint").text(hintText).appendTo($actions);
|
|
925
1037
|
}
|
|
926
1038
|
|
|
927
1039
|
$box.append($msg);
|
|
@@ -949,7 +1061,17 @@
|
|
|
949
1061
|
|
|
950
1062
|
var oldValues = {};
|
|
951
1063
|
d.propertyChanges.forEach(function (c) { oldValues[c.key] = liveNode[c.key]; });
|
|
952
|
-
d.propertyChanges.forEach(function (c) {
|
|
1064
|
+
d.propertyChanges.forEach(function (c) {
|
|
1065
|
+
// Rewrite placeholder ids that point at newly-inserted config
|
|
1066
|
+
// nodes (e.g. an MQTT out node's broker: "fp-new-0" referring
|
|
1067
|
+
// to a new mqtt-broker in the same response's newNodes).
|
|
1068
|
+
// applyInsertions returns idMap and it flows to us via the
|
|
1069
|
+
// applyCallback chain — idMap[placeholder] is the real id.
|
|
1070
|
+
var val = (idMap && typeof c.newVal === "string" && idMap[c.newVal])
|
|
1071
|
+
? idMap[c.newVal]
|
|
1072
|
+
: c.newVal;
|
|
1073
|
+
liveNode[c.key] = val;
|
|
1074
|
+
});
|
|
953
1075
|
|
|
954
1076
|
// Switch nodes derive their port count from rules.length (the edit
|
|
955
1077
|
// dialog's "outputCount" map assigns each rule its own output and
|
|
@@ -1652,19 +1774,72 @@
|
|
|
1652
1774
|
state: "pending"
|
|
1653
1775
|
});
|
|
1654
1776
|
$msg.attr("data-fp-record-id", _genRecord.id);
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
.
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1777
|
+
if (onImported && buildGoal) {
|
|
1778
|
+
// B1: primary deploy-and-verify chip (accent, full-width) + secondary
|
|
1779
|
+
// plain escape hatch. Only when buildGoal is set — that signals this
|
|
1780
|
+
// is a build-loop context, not just a plain post-import callback.
|
|
1781
|
+
var $primaryChip = $("<button>")
|
|
1782
|
+
.addClass("fp-chip fp-chip-card")
|
|
1783
|
+
.attr("type", "button")
|
|
1784
|
+
.on("click", function () {
|
|
1785
|
+
$primaryChip.prop("disabled", true);
|
|
1786
|
+
$secondaryAddBtn.prop("disabled", true);
|
|
1787
|
+
if (_genRecord) { _genRecord.state = "applied"; }
|
|
1788
|
+
importGeneratedFlow(nodes, onImported);
|
|
1789
|
+
});
|
|
1790
|
+
$("<span>").addClass("fp-chip-icon")
|
|
1791
|
+
.append($("<i>").addClass("fa fa-check-circle"))
|
|
1792
|
+
.appendTo($primaryChip);
|
|
1793
|
+
var $chipBody = $("<span>").addClass("fp-chip-body").appendTo($primaryChip);
|
|
1794
|
+
$("<span>").addClass("fp-chip-title").text("Deploy & verify this works →").appendTo($chipBody);
|
|
1795
|
+
$("<span>").addClass("fp-chip-sub").text("Add to canvas, deploy, then run the verification loop").appendTo($chipBody);
|
|
1796
|
+
$("<span>").addClass("fp-chip-go").html("›").appendTo($primaryChip);
|
|
1797
|
+
$actions.append($primaryChip);
|
|
1798
|
+
var $secondaryAddBtn = $("<button>")
|
|
1799
|
+
.addClass("fp-chip fp-chip-card fp-chip-card-alt")
|
|
1800
|
+
.attr("type", "button")
|
|
1801
|
+
.on("click", function () {
|
|
1802
|
+
$secondaryAddBtn.prop("disabled", true);
|
|
1803
|
+
$primaryChip.prop("disabled", true);
|
|
1804
|
+
if (_genRecord) { _genRecord.state = "applied"; }
|
|
1805
|
+
importGeneratedFlow(nodes, null);
|
|
1806
|
+
});
|
|
1807
|
+
var $altBody = $("<span>").addClass("fp-chip-body").appendTo($secondaryAddBtn);
|
|
1808
|
+
$("<span>").addClass("fp-chip-title").text("Just add to canvas").appendTo($altBody);
|
|
1809
|
+
$("<span>").addClass("fp-chip-sub").text("Skips deploy & verification").appendTo($altBody);
|
|
1810
|
+
$("<span>").addClass("fp-chip-go").html("›").appendTo($secondaryAddBtn);
|
|
1811
|
+
$actions.append($secondaryAddBtn);
|
|
1812
|
+
} else if (onImported) {
|
|
1813
|
+
// Has a post-import callback (e.g. verifyImportedNodes) but no build
|
|
1814
|
+
// loop — chip button that still fires the callback on import.
|
|
1815
|
+
var $addBtn = $("<button>")
|
|
1816
|
+
.addClass("fp-chip fp-chip-card")
|
|
1817
|
+
.attr("type", "button")
|
|
1818
|
+
.on("click", function () {
|
|
1819
|
+
$addBtn.prop("disabled", true);
|
|
1820
|
+
if (_genRecord) { _genRecord.state = "applied"; }
|
|
1821
|
+
importGeneratedFlow(nodes, onImported);
|
|
1822
|
+
});
|
|
1823
|
+
var $addBody = $("<span>").addClass("fp-chip-body").appendTo($addBtn);
|
|
1824
|
+
$("<span>").addClass("fp-chip-title").text("Add to canvas").appendTo($addBody);
|
|
1825
|
+
$("<span>").addClass("fp-chip-sub").text("Click the canvas to place the nodes").appendTo($addBody);
|
|
1826
|
+
$("<span>").addClass("fp-chip-go").html("›").appendTo($addBtn);
|
|
1827
|
+
$actions.append($addBtn);
|
|
1828
|
+
} else {
|
|
1829
|
+
var $addBtn = $("<button>")
|
|
1830
|
+
.addClass("fp-chip fp-chip-card")
|
|
1831
|
+
.attr("type", "button")
|
|
1832
|
+
.on("click", function () {
|
|
1833
|
+
$addBtn.prop("disabled", true);
|
|
1834
|
+
if (_genRecord) { _genRecord.state = "applied"; }
|
|
1835
|
+
importGeneratedFlow(nodes, null);
|
|
1836
|
+
});
|
|
1837
|
+
var $addBody = $("<span>").addClass("fp-chip-body").appendTo($addBtn);
|
|
1838
|
+
$("<span>").addClass("fp-chip-title").text("Add to canvas").appendTo($addBody);
|
|
1839
|
+
$("<span>").addClass("fp-chip-sub").text("Click the canvas to place the nodes").appendTo($addBody);
|
|
1840
|
+
$("<span>").addClass("fp-chip-go").html("›").appendTo($addBtn);
|
|
1841
|
+
$actions.append($addBtn);
|
|
1842
|
+
}
|
|
1668
1843
|
}
|
|
1669
1844
|
|
|
1670
1845
|
$box.append($msg);
|
|
@@ -1867,12 +2042,21 @@
|
|
|
1867
2042
|
: hasMutations ? "Apply Changes"
|
|
1868
2043
|
: hasNewNodes ? "Insert Nodes"
|
|
1869
2044
|
: "Apply Changes";
|
|
1870
|
-
var
|
|
1871
|
-
|
|
2045
|
+
var hintParts = [];
|
|
2046
|
+
if (hasPropChanges || hasWireChanges) { hintParts.push("changes mutate live nodes"); }
|
|
2047
|
+
if (hasRemoveNodes) { hintParts.push("removals delete nodes"); }
|
|
2048
|
+
if (hasNewGroups) { hintParts.push("groups are created/updated"); }
|
|
2049
|
+
if (hasNewNodes) { hintParts.push("insertions add new nodes"); }
|
|
2050
|
+
if (!hasNewNodes && newWires && newWires.length > 0) { hintParts.push("connections added"); }
|
|
2051
|
+
var applySub = hintParts.length ? "Review above — " + hintParts.join(", ") + ". Ctrl+Z to undo." : "";
|
|
2052
|
+
var $applyBtn = $("<button>")
|
|
2053
|
+
.addClass("fp-chip fp-chip-card")
|
|
2054
|
+
.attr("type", "button")
|
|
1872
2055
|
.on("click", function () {
|
|
1873
|
-
$applyBtn.prop("disabled", true)
|
|
2056
|
+
$applyBtn.prop("disabled", true);
|
|
2057
|
+
$applyTitle.text("Applying…");
|
|
1874
2058
|
var idMap = {};
|
|
1875
|
-
if (hasNewNodes) { idMap = applyInsertions(newNodes, newWires, existingIds) || {}; }
|
|
2059
|
+
if (hasNewNodes || (newWires && newWires.length > 0)) { idMap = applyInsertions(newNodes, newWires, existingIds) || {}; }
|
|
1876
2060
|
if (isBuildFix) {
|
|
1877
2061
|
applyBuildLoopFix(nodeDiffs, removeNodes, idMap, capReached);
|
|
1878
2062
|
} else {
|
|
@@ -1880,19 +2064,16 @@
|
|
|
1880
2064
|
if (hasNewGroups) { applyGroupChanges(newGroups, idMap); }
|
|
1881
2065
|
}
|
|
1882
2066
|
if (_newRec) { _newRec.state = "applied"; }
|
|
1883
|
-
$
|
|
2067
|
+
$applyTitle.text("Done ✓");
|
|
1884
2068
|
});
|
|
2069
|
+
$("<span>").addClass("fp-chip-icon")
|
|
2070
|
+
.append($("<i>").addClass(isBuildFix ? "fa fa-wrench" : "fa fa-check-circle"))
|
|
2071
|
+
.appendTo($applyBtn);
|
|
2072
|
+
var $applyBody = $("<span>").addClass("fp-chip-body").appendTo($applyBtn);
|
|
2073
|
+
var $applyTitle = $("<span>").addClass("fp-chip-title").text(isBuildFix ? "Apply Fix" : btnLabel).appendTo($applyBody);
|
|
2074
|
+
$("<span>").addClass("fp-chip-sub").text(isBuildFix && capReached ? "Apply final fix and stop the loop" : applySub).appendTo($applyBody);
|
|
2075
|
+
$("<span>").addClass("fp-chip-go").html("›").appendTo($applyBtn);
|
|
1885
2076
|
$actions.append($applyBtn);
|
|
1886
|
-
var hintParts = [];
|
|
1887
|
-
if (hasPropChanges || hasWireChanges) { hintParts.push("changes mutate live nodes"); }
|
|
1888
|
-
if (hasRemoveNodes) { hintParts.push("removals delete nodes"); }
|
|
1889
|
-
if (hasNewGroups) { hintParts.push("groups are created/updated"); }
|
|
1890
|
-
if (hasNewNodes) { hintParts.push("insertions add new nodes"); }
|
|
1891
|
-
if (hintParts.length) {
|
|
1892
|
-
$("<span>").addClass("fp-review-hint")
|
|
1893
|
-
.text("Review above — " + hintParts.join(", ") + ". Ctrl+Z to undo.")
|
|
1894
|
-
.appendTo($actions);
|
|
1895
|
-
}
|
|
1896
2077
|
}
|
|
1897
2078
|
|
|
1898
2079
|
$box.append($msg);
|
package/lib/core/init.js
CHANGED
|
@@ -1000,8 +1000,14 @@
|
|
|
1000
1000
|
' </div>' +
|
|
1001
1001
|
' <label>Provider Name</label>' +
|
|
1002
1002
|
' <input id="fp-provider-name" type="text" placeholder="LocalAI">' +
|
|
1003
|
+
' <label>Provider type</label>' +
|
|
1004
|
+
' <select id="fp-provider-type">' +
|
|
1005
|
+
' <option value="openai-compatible">OpenAI-compatible (LocalAI, Ollama, SGLang, OpenAI, etc.)</option>' +
|
|
1006
|
+
' <option value="anthropic">Anthropic</option>' +
|
|
1007
|
+
' </select>' +
|
|
1003
1008
|
' <label>Base URL</label>' +
|
|
1004
1009
|
' <input id="fp-base-url" type="text" placeholder="http://localhost:8080">' +
|
|
1010
|
+
' <div id="fp-base-url-hint" class="fp-consent-hint fp-hidden">Leave blank to use api.anthropic.com.</div>' +
|
|
1005
1011
|
' <label>API Key</label>' +
|
|
1006
1012
|
' <input id="fp-api-key" type="password" placeholder="Optional">' +
|
|
1007
1013
|
' <label>Model</label>' +
|
|
@@ -1173,6 +1179,7 @@
|
|
|
1173
1179
|
// Nothing is sent to the backend until the user explicitly
|
|
1174
1180
|
// attaches a message and sends a request.
|
|
1175
1181
|
try { RED.comms.subscribe("debug", onDebugMessage); } catch (e) { /* comms unavailable */ }
|
|
1182
|
+
try { RED.comms.subscribe("status/#", onNodeStatus); } catch (e) { /* comms unavailable */ }
|
|
1176
1183
|
|
|
1177
1184
|
// Track whether the user is scrolled to the bottom of the chat,
|
|
1178
1185
|
// so "Cruising…"/streaming updates only auto-follow when they
|
|
@@ -1216,6 +1223,9 @@
|
|
|
1216
1223
|
content.find("#fp-provider-select").on("change", function () {
|
|
1217
1224
|
switchProvider($(this).val());
|
|
1218
1225
|
});
|
|
1226
|
+
content.find("#fp-provider-type").on("change", function () {
|
|
1227
|
+
toggleAnthropicHint($(this).val());
|
|
1228
|
+
});
|
|
1219
1229
|
content.find("#fp-add-provider").on("click", function () { addProvider(); });
|
|
1220
1230
|
content.find("#fp-remove-provider").on("click", function () { removeProvider(); });
|
|
1221
1231
|
content.find("#fp-test-provider").on("click", function () { testProvider(); });
|
package/lib/core/main.js
CHANGED
|
@@ -81,6 +81,8 @@
|
|
|
81
81
|
case "question":
|
|
82
82
|
if (rec.loopCheckpoint) {
|
|
83
83
|
renderLoopCheckpoint(activeBuildLoop);
|
|
84
|
+
} else if (rec.buildConsentGate) {
|
|
85
|
+
renderBuildConsentGate(rec);
|
|
84
86
|
} else {
|
|
85
87
|
renderClarifyingQuestion(rec.options || []);
|
|
86
88
|
}
|
|
@@ -91,6 +93,9 @@
|
|
|
91
93
|
case "buildStep":
|
|
92
94
|
rerenderBuildStepRecord(rec);
|
|
93
95
|
break;
|
|
96
|
+
case "todo":
|
|
97
|
+
rerenderTodoRecord(rec);
|
|
98
|
+
break;
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
|
|
@@ -141,6 +146,7 @@
|
|
|
141
146
|
var entry = {
|
|
142
147
|
id: nextDebugMessageId++,
|
|
143
148
|
timestamp: Date.now(),
|
|
149
|
+
sourceKind: "debug",
|
|
144
150
|
name: msg.name || msg.id || "(unnamed node)",
|
|
145
151
|
topic: redactedTopic,
|
|
146
152
|
// previewValue: short, for the scannable debug-log list only.
|
|
@@ -173,8 +179,14 @@
|
|
|
173
179
|
// message from one trigger accumulate into attachedDebugMessages
|
|
174
180
|
// before review actually runs, so the model sees the full picture
|
|
175
181
|
// instead of whichever message happened to arrive first.
|
|
182
|
+
// WS4: a Skip decision at the build consent gate excludes this tap's
|
|
183
|
+
// real id from skipCheckpointTapIds — its messages still arrive here
|
|
184
|
+
// (still shown in the debug-log list/updateDebugStatus above) but
|
|
185
|
+
// don't drive the auto-attach/auto-review transition, so the loop
|
|
186
|
+
// falls back to manual confirmation or the honest-timeout instead.
|
|
176
187
|
if (activeBuildLoop && activeBuildLoop.waypoint === "attach" &&
|
|
177
|
-
activeBuildLoop.nodeIds.indexOf(msg.id) !== -1
|
|
188
|
+
activeBuildLoop.nodeIds.indexOf(msg.id) !== -1 &&
|
|
189
|
+
(activeBuildLoop.skipCheckpointTapIds || []).indexOf(msg.id) === -1) {
|
|
178
190
|
attachedDebugMessages.push(entry);
|
|
179
191
|
updateDebugStatus();
|
|
180
192
|
if (buildLoopNoDebugTimer) { clearTimeout(buildLoopNoDebugTimer); buildLoopNoDebugTimer = null; }
|
|
@@ -193,11 +205,76 @@
|
|
|
193
205
|
}
|
|
194
206
|
}
|
|
195
207
|
|
|
208
|
+
// W3: node-status stream evidence path.
|
|
209
|
+
// Fires for every deployed node that sets a status (fill/text via
|
|
210
|
+
// node.status() in user function code, or core nodes like http-request
|
|
211
|
+
// reporting "200 OK" / error text). Topic is "status/<nodeId>".
|
|
212
|
+
// Used as fallback evidence when the loop is waiting at "attach" but no
|
|
213
|
+
// debug node exists (HTTP endpoints, background workers, etc.) — gives
|
|
214
|
+
// the model something real to review instead of the loop hanging until
|
|
215
|
+
// the 20-second honest-timeout fires.
|
|
216
|
+
// Only used when no debug payload has arrived yet; if debug output is
|
|
217
|
+
// coming the richer payload data wins — let that path take over.
|
|
218
|
+
function onNodeStatus(topic, msg) {
|
|
219
|
+
if (!activeBuildLoop || activeBuildLoop.waypoint !== "attach") { return; }
|
|
220
|
+
var nodeId = topic.split("/")[1];
|
|
221
|
+
if (!nodeId || activeBuildLoop.nodeIds.indexOf(nodeId) === -1) { return; }
|
|
222
|
+
// WS4: same Skip-decision gate as onDebugMessage, keyed on the
|
|
223
|
+
// side-effecting node's own real id here (status events report the
|
|
224
|
+
// node itself, not a debug tap wired to it).
|
|
225
|
+
if ((activeBuildLoop.skipCheckpointNodeIds || []).indexOf(nodeId) !== -1) { return; }
|
|
226
|
+
if (attachedDebugMessages.length > 0) { return; }
|
|
227
|
+
|
|
228
|
+
// Skip in-progress ("blue") statuses — e.g. http-request emits
|
|
229
|
+
// fill:"blue" text:"requesting" before any response arrives. Locking
|
|
230
|
+
// in this early status starts the debounce before the actual error or
|
|
231
|
+
// response has a chance to land, so the model reviews a placeholder
|
|
232
|
+
// rather than the real result. Only terminal states (red=error,
|
|
233
|
+
// green=success, or no fill) are useful evidence.
|
|
234
|
+
if (msg && msg.fill === "blue") { return; }
|
|
235
|
+
|
|
236
|
+
var statusText = [msg && msg.fill, msg && msg.text].filter(Boolean).join(" — ");
|
|
237
|
+
if (!statusText) { return; }
|
|
238
|
+
|
|
239
|
+
var entry = {
|
|
240
|
+
id: nextDebugMessageId++,
|
|
241
|
+
timestamp: Date.now(),
|
|
242
|
+
sourceKind: "status",
|
|
243
|
+
name: nodeId + " (node status)",
|
|
244
|
+
topic: "node-status",
|
|
245
|
+
previewValue: statusText,
|
|
246
|
+
value: "Node " + nodeId + " reported status: " + statusText
|
|
247
|
+
};
|
|
248
|
+
attachedDebugMessages.push(entry);
|
|
249
|
+
updateDebugStatus();
|
|
250
|
+
|
|
251
|
+
if (buildLoopNoDebugTimer) { clearTimeout(buildLoopNoDebugTimer); buildLoopNoDebugTimer = null; }
|
|
252
|
+
if (buildLoopAttachTimer) { clearTimeout(buildLoopAttachTimer); }
|
|
253
|
+
buildLoopAttachTimer = setTimeout(function () {
|
|
254
|
+
buildLoopAttachTimer = null;
|
|
255
|
+
if (!activeBuildLoop || activeBuildLoop.waypoint !== "attach") { return; }
|
|
256
|
+
activeBuildLoop.waypoint = "review";
|
|
257
|
+
renderLoopStepper(activeBuildLoop);
|
|
258
|
+
if (currentSettings.loopHoldStep) {
|
|
259
|
+
renderLoopCheckpoint(activeBuildLoop);
|
|
260
|
+
} else {
|
|
261
|
+
runBuildReview(activeBuildLoop);
|
|
262
|
+
}
|
|
263
|
+
}, BUILD_LOOP_ATTACH_DEBOUNCE_MS);
|
|
264
|
+
}
|
|
265
|
+
|
|
196
266
|
// The exact shape sent to the backend (and shown by "Preview debug") —
|
|
197
267
|
// excludes previewValue, which exists only for the debug-log list.
|
|
198
268
|
function buildDebugMessagesForSend() {
|
|
199
269
|
return attachedDebugMessages.map(function (m) {
|
|
200
|
-
return {
|
|
270
|
+
return {
|
|
271
|
+
id: m.id,
|
|
272
|
+
timestamp: m.timestamp,
|
|
273
|
+
sourceKind: m.sourceKind,
|
|
274
|
+
name: m.name,
|
|
275
|
+
topic: m.topic,
|
|
276
|
+
value: m.value
|
|
277
|
+
};
|
|
201
278
|
});
|
|
202
279
|
}
|
|
203
280
|
|
|
@@ -1042,10 +1119,19 @@
|
|
|
1042
1119
|
if (active) { $sel.val(active.id); }
|
|
1043
1120
|
}
|
|
1044
1121
|
|
|
1122
|
+
function toggleAnthropicHint(type) {
|
|
1123
|
+
var isAnthropic = type === "anthropic";
|
|
1124
|
+
el("#fp-base-url-hint").toggleClass("fp-hidden", !isAnthropic);
|
|
1125
|
+
el("#fp-base-url").attr("placeholder", isAnthropic ? "Leave blank for api.anthropic.com" : "http://localhost:8080");
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1045
1128
|
// Write the form fields from a given provider profile.
|
|
1046
1129
|
function fillProviderFields(p) {
|
|
1047
1130
|
p = p || {};
|
|
1048
1131
|
el("#fp-provider-name").val(p.providerName || "");
|
|
1132
|
+
var type = p.type || "openai-compatible";
|
|
1133
|
+
el("#fp-provider-type").val(type);
|
|
1134
|
+
toggleAnthropicHint(type);
|
|
1049
1135
|
el("#fp-base-url").val(p.baseUrl || "");
|
|
1050
1136
|
el("#fp-api-key").val(p.apiKey || "");
|
|
1051
1137
|
el("#fp-model").val(p.model || "");
|
|
@@ -1134,6 +1220,7 @@
|
|
|
1134
1220
|
var ap = activeProvider();
|
|
1135
1221
|
if (!ap) { return; }
|
|
1136
1222
|
ap.providerName = el("#fp-provider-name").val() || "Provider";
|
|
1223
|
+
ap.type = el("#fp-provider-type").val() || "openai-compatible";
|
|
1137
1224
|
ap.baseUrl = el("#fp-base-url").val() || "";
|
|
1138
1225
|
ap.apiKey = el("#fp-api-key").val() || "";
|
|
1139
1226
|
ap.model = el("#fp-model").val() || "";
|
|
@@ -1330,10 +1417,10 @@
|
|
|
1330
1417
|
var payload = collectSettings();
|
|
1331
1418
|
var list = payload.providers || [];
|
|
1332
1419
|
|
|
1333
|
-
// Validation 1: every provider needs a base URL
|
|
1334
|
-
//
|
|
1420
|
+
// Validation 1: every non-Anthropic provider needs a base URL.
|
|
1421
|
+
// Anthropic providers default to api.anthropic.com when baseUrl is blank.
|
|
1335
1422
|
var noUrl = list.filter(function (p) {
|
|
1336
|
-
return !p.baseUrl || !String(p.baseUrl).trim();
|
|
1423
|
+
return p.type !== "anthropic" && (!p.baseUrl || !String(p.baseUrl).trim());
|
|
1337
1424
|
});
|
|
1338
1425
|
if (noUrl.length) {
|
|
1339
1426
|
var urlNames = noUrl.map(function (p) { return p.providerName || "(unnamed)"; }).join(", ");
|
|
@@ -1774,4 +1861,3 @@
|
|
|
1774
1861
|
|
|
1775
1862
|
renderChip("Open Settings", "fa fa-cog", showSettings);
|
|
1776
1863
|
}
|
|
1777
|
-
|