@manny-est/node-red-flowpilot 0.5.1 → 0.6.0-beta.1
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 +58 -0
- package/README.md +21 -7
- package/USER-GUIDE.md +27 -14
- package/flowpilot-core.css +159 -5
- package/flowpilot.js +1353 -172
- package/lib/agent-contract.js +45 -0
- package/lib/build-core-script.js +1 -0
- package/lib/build-system-prompt.js +26 -4
- package/lib/chat-data.js +106 -0
- package/lib/core/apply-review.js +268 -64
- package/lib/core/graph-truth.js +63 -0
- package/lib/core/history.js +10 -1
- package/lib/core/init.js +148 -5
- package/lib/core/main.js +755 -21
- package/lib/core/modes.js +1523 -67
- package/lib/core/selection-context.js +31 -1
- package/lib/default-system-prompt.js +17 -12
- package/lib/document-system-prompt.js +22 -32
- package/lib/envelope.js +13 -7
- package/lib/generation-system-prompt.js +29 -44
- package/lib/modify-system-prompt.js +152 -72
- package/lib/persona-prompt.js +81 -54
- package/lib/prompt-fragments.js +56 -0
- package/lib/provider-anthropic.js +388 -0
- package/lib/provider-openai-compatible.js +49 -12
- package/lib/provider-shape-check.js +34 -0
- package/lib/storage.js +160 -12
- 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
|
|
@@ -112,8 +126,19 @@
|
|
|
112
126
|
// existing nodes. Uses RED.nodes.add (the same path Node-RED's undo uses
|
|
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
|
-
|
|
116
|
-
|
|
129
|
+
//
|
|
130
|
+
// historyEvents (optional, CLAUDE-027): when the caller passes a shared
|
|
131
|
+
// array here (an agentic run's own accumulator — see runAgentLoop in
|
|
132
|
+
// modes.js), the compound event below is collected into it INSTEAD of
|
|
133
|
+
// being pushed to RED.history directly, so the run's own flush can fold
|
|
134
|
+
// it together with every other WRITE-tool call this same run made into
|
|
135
|
+
// ONE RED.history entry (RED.history's native t:"multi" wrapper —
|
|
136
|
+
// confirmed via @node-red/editor-client's red.js, e.g. its
|
|
137
|
+
// deleteSelection()). Omitted (undefined), this behaves exactly as
|
|
138
|
+
// before — every other caller (classic Modify/Generate apply, the build
|
|
139
|
+
// loop) still gets its own immediate, standalone push.
|
|
140
|
+
function applyInsertions(newNodes, newWires, contextNodeIds, historyEvents) {
|
|
141
|
+
if ((!newNodes || !newNodes.length) && (!newWires || !newWires.length)) { return; }
|
|
117
142
|
|
|
118
143
|
// Determine z (flow-tab id) from the active workspace.
|
|
119
144
|
var z = "";
|
|
@@ -246,6 +271,57 @@
|
|
|
246
271
|
});
|
|
247
272
|
});
|
|
248
273
|
|
|
274
|
+
// Collision-avoidance stagger: independent components that happen to
|
|
275
|
+
// share the same anchor (e.g. several standalone new nodes each
|
|
276
|
+
// wired from/to the SAME existing node — "add 5 debug taps after
|
|
277
|
+
// this http request node") compute identical target coordinates
|
|
278
|
+
// above and land exactly on top of each other. Detect a component
|
|
279
|
+
// whose origin collides with an earlier one and nudge the WHOLE
|
|
280
|
+
// component (preserving its own internal layout) into a small grid
|
|
281
|
+
// offset from the first occupant, so multi-node insertions never
|
|
282
|
+
// pile invisibly at one point.
|
|
283
|
+
var occupiedOrigins = [];
|
|
284
|
+
function collidesWithOccupied(x, y) {
|
|
285
|
+
return occupiedOrigins.some(function (p) {
|
|
286
|
+
return Math.abs(p.x - x) < 20 && Math.abs(p.y - y) < 20;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
Object.keys(componentAnchors).forEach(function (c) {
|
|
290
|
+
var members = laid.filter(function (n) { return componentOf[n.id] === c; });
|
|
291
|
+
if (!members.length) { return; }
|
|
292
|
+
var baseX = members.reduce(function (m, n) { return Math.min(m, n.x); }, members[0].x);
|
|
293
|
+
var baseY = members.reduce(function (m, n) { return Math.min(m, n.y); }, members[0].y);
|
|
294
|
+
// Walk a single-column grid outward from the natural origin
|
|
295
|
+
// until we find a slot nothing else already occupies — one
|
|
296
|
+
// node per row, never two at the same y (CLAUDE-012: Manny's
|
|
297
|
+
// original spec, which the earlier 2-wide grid still violated
|
|
298
|
+
// by putting a pair at the same vertical level). After 5 nodes
|
|
299
|
+
// in a column, wrap to a new column further right than the
|
|
300
|
+
// first node in the previous one, so wires stay visually
|
|
301
|
+
// distinguishable instead of stacking on top of each other.
|
|
302
|
+
var slot = 0;
|
|
303
|
+
var x = baseX, y = baseY;
|
|
304
|
+
while (collidesWithOccupied(x, y) && slot < 200) {
|
|
305
|
+
slot++;
|
|
306
|
+
x = baseX + Math.floor(slot / 5) * 250;
|
|
307
|
+
y = baseY + (slot % 5) * 90;
|
|
308
|
+
}
|
|
309
|
+
if (slot > 0) {
|
|
310
|
+
var dx = x - baseX, dy = y - baseY;
|
|
311
|
+
members.forEach(function (n) { n.x += dx; n.y += dy; });
|
|
312
|
+
}
|
|
313
|
+
occupiedOrigins.push({ x: x, y: y });
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// B3: snapshot placeholder→type before ids are overwritten, so wire
|
|
317
|
+
// failure messages can name the placeholder id and type rather than the
|
|
318
|
+
// opaque real hex id the node was never given (node not inserted = id
|
|
319
|
+
// exists in idMap but never landed in the live graph).
|
|
320
|
+
var placeholderToType = {};
|
|
321
|
+
laid.forEach(function (n) {
|
|
322
|
+
if (placeholderIds[n.id]) { placeholderToType[n.id] = n.type || "?"; }
|
|
323
|
+
});
|
|
324
|
+
|
|
249
325
|
// Build placeholder-id → real-id map and assign real ids + z.
|
|
250
326
|
var idMap = {};
|
|
251
327
|
laid.forEach(function (n) {
|
|
@@ -338,6 +414,15 @@
|
|
|
338
414
|
? typeDef.outputs
|
|
339
415
|
: (Array.isArray(n.wires) ? n.wires.length : 0);
|
|
340
416
|
}
|
|
417
|
+
// The model's "wires" array can disagree with the node type's
|
|
418
|
+
// real port count (e.g. a hallucinated single empty port on a
|
|
419
|
+
// 0-output type like http response) — the editor draws one
|
|
420
|
+
// anchor per n.wires entry regardless of n.outputs, so a
|
|
421
|
+
// mismatch renders a phantom port. Reconcile the array length
|
|
422
|
+
// to the authoritative n.outputs count now that it's resolved.
|
|
423
|
+
if (!Array.isArray(n.wires)) { n.wires = []; }
|
|
424
|
+
while (n.wires.length < n.outputs) { n.wires.push([]); }
|
|
425
|
+
if (n.wires.length > n.outputs) { n.wires.length = n.outputs; }
|
|
341
426
|
// Apply type-definition defaults for any property the model omitted.
|
|
342
427
|
// This covers required fields (e.g. statusVal/statusType on debug)
|
|
343
428
|
// that oneditsave would normally set, preventing a spurious triangle.
|
|
@@ -396,7 +481,16 @@
|
|
|
396
481
|
var fromNode = findLiveNode(fromId);
|
|
397
482
|
var toNode = findLiveNode(toId);
|
|
398
483
|
if (!fromNode || !toNode) {
|
|
399
|
-
|
|
484
|
+
var _failRef = !fromNode ? wire.from : wire.to;
|
|
485
|
+
var _failMsg;
|
|
486
|
+
if (placeholderIds[_failRef]) {
|
|
487
|
+
_failMsg = "Cannot wire — referenced node was not inserted (" +
|
|
488
|
+
_failRef + " / type: " + (placeholderToType[_failRef] || "?") + ")";
|
|
489
|
+
} else {
|
|
490
|
+
_failMsg = "Cannot wire — node not found in editor: " +
|
|
491
|
+
(!fromNode ? fromId : toId);
|
|
492
|
+
}
|
|
493
|
+
addMessage("error", _failMsg);
|
|
400
494
|
return;
|
|
401
495
|
}
|
|
402
496
|
var fromPort = wire.fromPort || 0;
|
|
@@ -454,7 +548,7 @@
|
|
|
454
548
|
// NB: for t:"add", ev.nodes must be an array of ID STRINGS — NR's undo
|
|
455
549
|
// does RED.nodes.node(ev.nodes[i]) then reads .z, which throws (and
|
|
456
550
|
// silently breaks Ctrl+Z) if given node objects instead of ids.
|
|
457
|
-
|
|
551
|
+
var insertHistoryEvent = {
|
|
458
552
|
t: "add",
|
|
459
553
|
nodes: addedNodes.map(function (n) { return n.id; }),
|
|
460
554
|
links: addedLinks,
|
|
@@ -464,7 +558,8 @@
|
|
|
464
558
|
subflowInputs: [],
|
|
465
559
|
subflowOutputs: [],
|
|
466
560
|
dirty: RED.nodes.dirty()
|
|
467
|
-
}
|
|
561
|
+
};
|
|
562
|
+
if (historyEvents) { historyEvents.push(insertHistoryEvent); } else { RED.history.push(insertHistoryEvent); }
|
|
468
563
|
|
|
469
564
|
RED.nodes.dirty(true);
|
|
470
565
|
RED.view.redraw(true);
|
|
@@ -524,12 +619,13 @@
|
|
|
524
619
|
var propertyChanges = [];
|
|
525
620
|
var wiresChanged = false;
|
|
526
621
|
var redactionSkips = 0;
|
|
622
|
+
var redactedKeys = [];
|
|
527
623
|
Object.keys(modNode).forEach(function (k) {
|
|
528
624
|
if (DIFF_SKIP[k]) { return; }
|
|
529
625
|
var newRaw = modNode[k];
|
|
530
626
|
// If the model echoed a sanitizer sentinel, the field is opaque —
|
|
531
627
|
// we can't meaningfully compare or apply it, so skip entirely.
|
|
532
|
-
if (isSanitizeSentinel(newRaw)) { redactionSkips++; return; }
|
|
628
|
+
if (isSanitizeSentinel(newRaw)) { redactionSkips++; redactedKeys.push(k); return; }
|
|
533
629
|
var oldRaw = liveNode ? liveNode[k] : undefined;
|
|
534
630
|
var oldStr, newStr;
|
|
535
631
|
try { oldStr = JSON.stringify(oldRaw); } catch (e) { oldStr = String(oldRaw); }
|
|
@@ -538,7 +634,7 @@
|
|
|
538
634
|
if (k === "wires") { wiresChanged = true; return; }
|
|
539
635
|
propertyChanges.push({ key: k, oldVal: oldRaw, newVal: newRaw });
|
|
540
636
|
});
|
|
541
|
-
return { propertyChanges: propertyChanges, wiresChanged: wiresChanged, redactionSkips: redactionSkips };
|
|
637
|
+
return { propertyChanges: propertyChanges, wiresChanged: wiresChanged, redactionSkips: redactionSkips, redactedKeys: redactedKeys };
|
|
542
638
|
}
|
|
543
639
|
|
|
544
640
|
// Diff outgoing wires for an existing node: compare what's live in the graph
|
|
@@ -642,6 +738,7 @@
|
|
|
642
738
|
propertyChanges: diff.propertyChanges,
|
|
643
739
|
wiresChanged: diff.wiresChanged,
|
|
644
740
|
redactionSkips: diff.redactionSkips,
|
|
741
|
+
redactedKeys: diff.redactedKeys,
|
|
645
742
|
wiresDiff: wiresDiff,
|
|
646
743
|
name: modNode.name || (liveNode && liveNode.name) || "",
|
|
647
744
|
type: modNode.type || (liveNode && liveNode.type) || ""
|
|
@@ -662,7 +759,7 @@
|
|
|
662
759
|
var hasNewNodes = newNodes.length > 0;
|
|
663
760
|
var hasRemoveNodes = removeNodes.length > 0;
|
|
664
761
|
var hasNewGroups = newGroups.length > 0;
|
|
665
|
-
var hasAnyChanges = hasPropChanges || hasWireChanges || hasNewNodes || hasRemoveNodes || hasNewGroups;
|
|
762
|
+
var hasAnyChanges = hasPropChanges || hasWireChanges || hasNewNodes || hasRemoveNodes || hasNewGroups || (newWires && newWires.length > 0);
|
|
666
763
|
var totalRedactionSkips = nodeDiffs.reduce(function (s, d) { return s + (d.redactionSkips || 0); }, 0);
|
|
667
764
|
|
|
668
765
|
var $msg = $("<div>").addClass("fp-message fp-review");
|
|
@@ -715,15 +812,27 @@
|
|
|
715
812
|
});
|
|
716
813
|
if (redactionOnlyDiffs.length > 0) {
|
|
717
814
|
var redSectionTop = hasPropChanges ? "12px" : "0";
|
|
718
|
-
|
|
719
|
-
.
|
|
815
|
+
var totalRedFields = redactionOnlyDiffs.reduce(function (s, d) {
|
|
816
|
+
return s + (Array.isArray(d.redactedKeys) && d.redactedKeys.length ? d.redactedKeys.length : 1);
|
|
817
|
+
}, 0);
|
|
818
|
+
var $redToggle = $("<div>").addClass("fp-review-count")
|
|
819
|
+
.css({ "margin-top": redSectionTop, "cursor": "pointer" })
|
|
820
|
+
.text("▶ Not applied — FlowPilot can’t change these directly (" + totalRedFields + " field(s))")
|
|
720
821
|
.appendTo($summaryPanel);
|
|
822
|
+
var $redContent = $("<div>").addClass("fp-hidden").appendTo($summaryPanel);
|
|
823
|
+
$redToggle.on("click", function () {
|
|
824
|
+
var collapsed = $redContent.hasClass("fp-hidden");
|
|
825
|
+
$redContent.toggleClass("fp-hidden", !collapsed);
|
|
826
|
+
$redToggle.text((collapsed ? "▼" : "▶") + " Not applied — FlowPilot can’t change these directly (" + totalRedFields + " field(s))");
|
|
827
|
+
});
|
|
721
828
|
redactionOnlyDiffs.forEach(function (d) {
|
|
722
|
-
var $section = $("<div>").addClass("fp-diff-node").appendTo($
|
|
829
|
+
var $section = $("<div>").addClass("fp-diff-node").appendTo($redContent);
|
|
723
830
|
var title = d.type + (d.name ? " — \"" + d.name + "\"" : "");
|
|
724
831
|
$("<div>").addClass("fp-diff-node-title").text(title).appendTo($section);
|
|
832
|
+
var keys = Array.isArray(d.redactedKeys) && d.redactedKeys.length ? d.redactedKeys : [];
|
|
833
|
+
var fieldList = keys.length ? keys.join(", ") : "one or more fields";
|
|
725
834
|
$("<div>").addClass("fp-diff-warn")
|
|
726
|
-
.text("
|
|
835
|
+
.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
836
|
.appendTo($section);
|
|
728
837
|
});
|
|
729
838
|
}
|
|
@@ -845,7 +954,7 @@
|
|
|
845
954
|
if (!hasAnyChanges) {
|
|
846
955
|
if (totalRedactionSkips > 0) {
|
|
847
956
|
$("<div>").addClass("fp-warning")
|
|
848
|
-
.text("
|
|
957
|
+
.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
958
|
.appendTo($actions);
|
|
850
959
|
} else {
|
|
851
960
|
$("<div>").addClass("fp-review-hint")
|
|
@@ -894,34 +1003,49 @@
|
|
|
894
1003
|
});
|
|
895
1004
|
$msg.attr("data-fp-record-id", _modRecord.id);
|
|
896
1005
|
|
|
1006
|
+
var hintParts = [];
|
|
1007
|
+
if (hasPropChanges || hasWireChanges) { hintParts.push("changes mutate live nodes"); }
|
|
1008
|
+
if (hasRemoveNodes) { hintParts.push("removals delete nodes"); }
|
|
1009
|
+
if (hasNewGroups) { hintParts.push("groups are created/updated"); }
|
|
1010
|
+
if (hasNewNodes) { hintParts.push("insertions add new nodes"); }
|
|
1011
|
+
if (!hasNewNodes && newWires && newWires.length > 0) { hintParts.push("connections added"); }
|
|
1012
|
+
var applySub = hintParts.length ? "Review above — " + hintParts.join(", ") + ". Ctrl+Z to undo." : "";
|
|
897
1013
|
var $applyBtn = $("<button>")
|
|
898
|
-
.addClass("
|
|
1014
|
+
.addClass("fp-chip fp-chip-card")
|
|
899
1015
|
.attr("type", "button")
|
|
900
|
-
.text(btnLabel)
|
|
901
1016
|
.on("click", function () {
|
|
902
|
-
$applyBtn.prop("disabled", true)
|
|
1017
|
+
$applyBtn.prop("disabled", true);
|
|
1018
|
+
$applyTitle.text("Applying…");
|
|
903
1019
|
// Insertions run FIRST so their placeholder→real-id map is
|
|
904
1020
|
// available to applyModifications/applyGroupChanges — an
|
|
905
1021
|
// existing node's rewired "wires" (Tier 3) or a new
|
|
906
1022
|
// group's membership may point at a node being inserted
|
|
907
1023
|
// in this same response.
|
|
908
1024
|
var idMap = {};
|
|
909
|
-
|
|
1025
|
+
var hasNewWires = newWires && newWires.length > 0;
|
|
1026
|
+
if (hasNewNodes || hasNewWires) {
|
|
910
1027
|
idMap = applyInsertions(newNodes, newWires, nodes.map(function (n) { return n.id; })) || {};
|
|
911
1028
|
}
|
|
912
|
-
|
|
1029
|
+
// W4 Phase 2: applyCallback also runs the verify read-back,
|
|
1030
|
+
// so it must fire for insertion-only Modifies too (no
|
|
1031
|
+
// property/wire change on an EXISTING node), not just when
|
|
1032
|
+
// hasMutations is true — otherwise "exists"/"wire" checks on
|
|
1033
|
+
// newly inserted nodes would never run.
|
|
1034
|
+
if ((hasMutations || hasNewNodes || hasNewWires) && applyCallback) {
|
|
1035
|
+
applyCallback(nodeDiffs, removeNodes, null, idMap);
|
|
1036
|
+
}
|
|
913
1037
|
if (hasNewGroups) { applyGroupChanges(newGroups, idMap); }
|
|
914
1038
|
if (_modRecord) { _modRecord.state = "applied"; }
|
|
915
|
-
$
|
|
1039
|
+
$applyTitle.text("Done ✓");
|
|
916
1040
|
});
|
|
1041
|
+
$("<span>").addClass("fp-chip-icon")
|
|
1042
|
+
.append($("<i>").addClass(buildFixInfo ? "fa fa-wrench" : "fa fa-check-circle"))
|
|
1043
|
+
.appendTo($applyBtn);
|
|
1044
|
+
var $applyBody = $("<span>").addClass("fp-chip-body").appendTo($applyBtn);
|
|
1045
|
+
var $applyTitle = $("<span>").addClass("fp-chip-title").text(buildFixInfo ? "Apply Fix" : btnLabel).appendTo($applyBody);
|
|
1046
|
+
$("<span>").addClass("fp-chip-sub").text(buildFixInfo && buildFixInfo.capReached ? "Apply final fix and stop the loop" : applySub).appendTo($applyBody);
|
|
1047
|
+
$("<span>").addClass("fp-chip-go").html("›").appendTo($applyBtn);
|
|
917
1048
|
$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
1049
|
}
|
|
926
1050
|
|
|
927
1051
|
$box.append($msg);
|
|
@@ -932,8 +1056,16 @@
|
|
|
932
1056
|
// Tier 1 — property changes: mutate live node + {t:"edit"} history entry
|
|
933
1057
|
// Tier 3 — wire changes: removeLink/addLink + {t:"add", removedLinks} entry
|
|
934
1058
|
// Tier 4 — node removals: collect links, removeLink, remove node + {t:"delete"} entry
|
|
935
|
-
// One history entry per node per type so Ctrl+Z steps back through them cleanly
|
|
936
|
-
|
|
1059
|
+
// One history entry per node per type so Ctrl+Z steps back through them cleanly
|
|
1060
|
+
// — UNLESS historyEvents (optional, CLAUDE-027) is passed: an agentic run's
|
|
1061
|
+
// shared accumulator, into which every event below is collected instead of
|
|
1062
|
+
// pushed immediately, so the run's own flush (runAgentLoop, modes.js) can
|
|
1063
|
+
// fold ALL of this run's WRITE-tool calls together into ONE RED.history
|
|
1064
|
+
// entry via t:"multi" — see applyInsertions' historyEvents doc above for
|
|
1065
|
+
// the same mechanism. Every other caller (classic Modify/Generate apply,
|
|
1066
|
+
// build loop) omits it and keeps this function's original per-node/per-
|
|
1067
|
+
// type granularity unchanged.
|
|
1068
|
+
function applyModifications(nodeDiffs, removeNodes, $applyBtn, idMap, historyEvents) {
|
|
937
1069
|
idMap = idMap || {};
|
|
938
1070
|
var propApplied = 0;
|
|
939
1071
|
var wireNodesApplied = 0;
|
|
@@ -949,7 +1081,17 @@
|
|
|
949
1081
|
|
|
950
1082
|
var oldValues = {};
|
|
951
1083
|
d.propertyChanges.forEach(function (c) { oldValues[c.key] = liveNode[c.key]; });
|
|
952
|
-
d.propertyChanges.forEach(function (c) {
|
|
1084
|
+
d.propertyChanges.forEach(function (c) {
|
|
1085
|
+
// Rewrite placeholder ids that point at newly-inserted config
|
|
1086
|
+
// nodes (e.g. an MQTT out node's broker: "fp-new-0" referring
|
|
1087
|
+
// to a new mqtt-broker in the same response's newNodes).
|
|
1088
|
+
// applyInsertions returns idMap and it flows to us via the
|
|
1089
|
+
// applyCallback chain — idMap[placeholder] is the real id.
|
|
1090
|
+
var val = (idMap && typeof c.newVal === "string" && idMap[c.newVal])
|
|
1091
|
+
? idMap[c.newVal]
|
|
1092
|
+
: c.newVal;
|
|
1093
|
+
liveNode[c.key] = val;
|
|
1094
|
+
});
|
|
953
1095
|
|
|
954
1096
|
// Switch nodes derive their port count from rules.length (the edit
|
|
955
1097
|
// dialog's "outputCount" map assigns each rule its own output and
|
|
@@ -968,12 +1110,13 @@
|
|
|
968
1110
|
|
|
969
1111
|
liveNode.changed = true;
|
|
970
1112
|
liveNode.dirty = true;
|
|
971
|
-
|
|
1113
|
+
var editHistoryEvent = {
|
|
972
1114
|
t: "edit",
|
|
973
1115
|
node: liveNode,
|
|
974
1116
|
changes: oldValues,
|
|
975
1117
|
dirty: RED.nodes.dirty()
|
|
976
|
-
}
|
|
1118
|
+
};
|
|
1119
|
+
if (historyEvents) { historyEvents.push(editHistoryEvent); } else { RED.history.push(editHistoryEvent); }
|
|
977
1120
|
propApplied++;
|
|
978
1121
|
});
|
|
979
1122
|
|
|
@@ -1029,12 +1172,13 @@
|
|
|
1029
1172
|
});
|
|
1030
1173
|
|
|
1031
1174
|
if (removedLinks.length || addedLinks.length) {
|
|
1032
|
-
|
|
1175
|
+
var wireHistoryEvent = {
|
|
1033
1176
|
t: "add",
|
|
1034
1177
|
links: addedLinks,
|
|
1035
1178
|
removedLinks: removedLinks,
|
|
1036
1179
|
dirty: RED.nodes.dirty()
|
|
1037
|
-
}
|
|
1180
|
+
};
|
|
1181
|
+
if (historyEvents) { historyEvents.push(wireHistoryEvent); } else { RED.history.push(wireHistoryEvent); }
|
|
1038
1182
|
wireNodesApplied++;
|
|
1039
1183
|
}
|
|
1040
1184
|
});
|
|
@@ -1094,7 +1238,7 @@
|
|
|
1094
1238
|
}
|
|
1095
1239
|
}
|
|
1096
1240
|
|
|
1097
|
-
|
|
1241
|
+
var removeHistoryEvent = {
|
|
1098
1242
|
t: "delete",
|
|
1099
1243
|
nodes: isJunction ? [] : [liveNode],
|
|
1100
1244
|
links: connectedLinks,
|
|
@@ -1104,7 +1248,8 @@
|
|
|
1104
1248
|
subflowInputs: [],
|
|
1105
1249
|
subflowOutputs: [],
|
|
1106
1250
|
dirty: RED.nodes.dirty()
|
|
1107
|
-
}
|
|
1251
|
+
};
|
|
1252
|
+
if (historyEvents) { historyEvents.push(removeHistoryEvent); } else { RED.history.push(removeHistoryEvent); }
|
|
1108
1253
|
nodesRemoved++;
|
|
1109
1254
|
});
|
|
1110
1255
|
|
|
@@ -1652,19 +1797,72 @@
|
|
|
1652
1797
|
state: "pending"
|
|
1653
1798
|
});
|
|
1654
1799
|
$msg.attr("data-fp-record-id", _genRecord.id);
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
.
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1800
|
+
if (onImported && buildGoal) {
|
|
1801
|
+
// B1: primary deploy-and-verify chip (accent, full-width) + secondary
|
|
1802
|
+
// plain escape hatch. Only when buildGoal is set — that signals this
|
|
1803
|
+
// is a build-loop context, not just a plain post-import callback.
|
|
1804
|
+
var $primaryChip = $("<button>")
|
|
1805
|
+
.addClass("fp-chip fp-chip-card")
|
|
1806
|
+
.attr("type", "button")
|
|
1807
|
+
.on("click", function () {
|
|
1808
|
+
$primaryChip.prop("disabled", true);
|
|
1809
|
+
$secondaryAddBtn.prop("disabled", true);
|
|
1810
|
+
if (_genRecord) { _genRecord.state = "applied"; }
|
|
1811
|
+
importGeneratedFlow(nodes, onImported);
|
|
1812
|
+
});
|
|
1813
|
+
$("<span>").addClass("fp-chip-icon")
|
|
1814
|
+
.append($("<i>").addClass("fa fa-check-circle"))
|
|
1815
|
+
.appendTo($primaryChip);
|
|
1816
|
+
var $chipBody = $("<span>").addClass("fp-chip-body").appendTo($primaryChip);
|
|
1817
|
+
$("<span>").addClass("fp-chip-title").text("Deploy & verify this works →").appendTo($chipBody);
|
|
1818
|
+
$("<span>").addClass("fp-chip-sub").text("Add to canvas, deploy, then run the verification loop").appendTo($chipBody);
|
|
1819
|
+
$("<span>").addClass("fp-chip-go").html("›").appendTo($primaryChip);
|
|
1820
|
+
$actions.append($primaryChip);
|
|
1821
|
+
var $secondaryAddBtn = $("<button>")
|
|
1822
|
+
.addClass("fp-chip fp-chip-card fp-chip-card-alt")
|
|
1823
|
+
.attr("type", "button")
|
|
1824
|
+
.on("click", function () {
|
|
1825
|
+
$secondaryAddBtn.prop("disabled", true);
|
|
1826
|
+
$primaryChip.prop("disabled", true);
|
|
1827
|
+
if (_genRecord) { _genRecord.state = "applied"; }
|
|
1828
|
+
importGeneratedFlow(nodes, null);
|
|
1829
|
+
});
|
|
1830
|
+
var $altBody = $("<span>").addClass("fp-chip-body").appendTo($secondaryAddBtn);
|
|
1831
|
+
$("<span>").addClass("fp-chip-title").text("Just add to canvas").appendTo($altBody);
|
|
1832
|
+
$("<span>").addClass("fp-chip-sub").text("Skips deploy & verification").appendTo($altBody);
|
|
1833
|
+
$("<span>").addClass("fp-chip-go").html("›").appendTo($secondaryAddBtn);
|
|
1834
|
+
$actions.append($secondaryAddBtn);
|
|
1835
|
+
} else if (onImported) {
|
|
1836
|
+
// Has a post-import callback (e.g. verifyImportedNodes) but no build
|
|
1837
|
+
// loop — chip button that still fires the callback on import.
|
|
1838
|
+
var $addBtn = $("<button>")
|
|
1839
|
+
.addClass("fp-chip fp-chip-card")
|
|
1840
|
+
.attr("type", "button")
|
|
1841
|
+
.on("click", function () {
|
|
1842
|
+
$addBtn.prop("disabled", true);
|
|
1843
|
+
if (_genRecord) { _genRecord.state = "applied"; }
|
|
1844
|
+
importGeneratedFlow(nodes, onImported);
|
|
1845
|
+
});
|
|
1846
|
+
var $addBody = $("<span>").addClass("fp-chip-body").appendTo($addBtn);
|
|
1847
|
+
$("<span>").addClass("fp-chip-title").text("Add to canvas").appendTo($addBody);
|
|
1848
|
+
$("<span>").addClass("fp-chip-sub").text("Click the canvas to place the nodes").appendTo($addBody);
|
|
1849
|
+
$("<span>").addClass("fp-chip-go").html("›").appendTo($addBtn);
|
|
1850
|
+
$actions.append($addBtn);
|
|
1851
|
+
} else {
|
|
1852
|
+
var $addBtn = $("<button>")
|
|
1853
|
+
.addClass("fp-chip fp-chip-card")
|
|
1854
|
+
.attr("type", "button")
|
|
1855
|
+
.on("click", function () {
|
|
1856
|
+
$addBtn.prop("disabled", true);
|
|
1857
|
+
if (_genRecord) { _genRecord.state = "applied"; }
|
|
1858
|
+
importGeneratedFlow(nodes, null);
|
|
1859
|
+
});
|
|
1860
|
+
var $addBody = $("<span>").addClass("fp-chip-body").appendTo($addBtn);
|
|
1861
|
+
$("<span>").addClass("fp-chip-title").text("Add to canvas").appendTo($addBody);
|
|
1862
|
+
$("<span>").addClass("fp-chip-sub").text("Click the canvas to place the nodes").appendTo($addBody);
|
|
1863
|
+
$("<span>").addClass("fp-chip-go").html("›").appendTo($addBtn);
|
|
1864
|
+
$actions.append($addBtn);
|
|
1865
|
+
}
|
|
1668
1866
|
}
|
|
1669
1867
|
|
|
1670
1868
|
$box.append($msg);
|
|
@@ -1867,12 +2065,21 @@
|
|
|
1867
2065
|
: hasMutations ? "Apply Changes"
|
|
1868
2066
|
: hasNewNodes ? "Insert Nodes"
|
|
1869
2067
|
: "Apply Changes";
|
|
1870
|
-
var
|
|
1871
|
-
|
|
2068
|
+
var hintParts = [];
|
|
2069
|
+
if (hasPropChanges || hasWireChanges) { hintParts.push("changes mutate live nodes"); }
|
|
2070
|
+
if (hasRemoveNodes) { hintParts.push("removals delete nodes"); }
|
|
2071
|
+
if (hasNewGroups) { hintParts.push("groups are created/updated"); }
|
|
2072
|
+
if (hasNewNodes) { hintParts.push("insertions add new nodes"); }
|
|
2073
|
+
if (!hasNewNodes && newWires && newWires.length > 0) { hintParts.push("connections added"); }
|
|
2074
|
+
var applySub = hintParts.length ? "Review above — " + hintParts.join(", ") + ". Ctrl+Z to undo." : "";
|
|
2075
|
+
var $applyBtn = $("<button>")
|
|
2076
|
+
.addClass("fp-chip fp-chip-card")
|
|
2077
|
+
.attr("type", "button")
|
|
1872
2078
|
.on("click", function () {
|
|
1873
|
-
$applyBtn.prop("disabled", true)
|
|
2079
|
+
$applyBtn.prop("disabled", true);
|
|
2080
|
+
$applyTitle.text("Applying…");
|
|
1874
2081
|
var idMap = {};
|
|
1875
|
-
if (hasNewNodes) { idMap = applyInsertions(newNodes, newWires, existingIds) || {}; }
|
|
2082
|
+
if (hasNewNodes || (newWires && newWires.length > 0)) { idMap = applyInsertions(newNodes, newWires, existingIds) || {}; }
|
|
1876
2083
|
if (isBuildFix) {
|
|
1877
2084
|
applyBuildLoopFix(nodeDiffs, removeNodes, idMap, capReached);
|
|
1878
2085
|
} else {
|
|
@@ -1880,19 +2087,16 @@
|
|
|
1880
2087
|
if (hasNewGroups) { applyGroupChanges(newGroups, idMap); }
|
|
1881
2088
|
}
|
|
1882
2089
|
if (_newRec) { _newRec.state = "applied"; }
|
|
1883
|
-
$
|
|
2090
|
+
$applyTitle.text("Done ✓");
|
|
1884
2091
|
});
|
|
2092
|
+
$("<span>").addClass("fp-chip-icon")
|
|
2093
|
+
.append($("<i>").addClass(isBuildFix ? "fa fa-wrench" : "fa fa-check-circle"))
|
|
2094
|
+
.appendTo($applyBtn);
|
|
2095
|
+
var $applyBody = $("<span>").addClass("fp-chip-body").appendTo($applyBtn);
|
|
2096
|
+
var $applyTitle = $("<span>").addClass("fp-chip-title").text(isBuildFix ? "Apply Fix" : btnLabel).appendTo($applyBody);
|
|
2097
|
+
$("<span>").addClass("fp-chip-sub").text(isBuildFix && capReached ? "Apply final fix and stop the loop" : applySub).appendTo($applyBody);
|
|
2098
|
+
$("<span>").addClass("fp-chip-go").html("›").appendTo($applyBtn);
|
|
1885
2099
|
$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
2100
|
}
|
|
1897
2101
|
|
|
1898
2102
|
$box.append($msg);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// P10-E (ADR-005): the single implementation of graph truth. Every
|
|
2
|
+
// verification consumer — runSingleVerifyCheck's per-check-type
|
|
3
|
+
// dispatch (modes.js), the WRITE-tool tool_result checks
|
|
4
|
+
// (runChecksForToolResult, main.js), verifyImportedNodes (modes.js),
|
|
5
|
+
// and group_nodes's post-check (main.js) — delegates here instead of
|
|
6
|
+
// re-reading RED.nodes/RED.nodes.eachLink locally. Wires are read
|
|
7
|
+
// exclusively via RED.nodes.eachLink, never node.wires: addLink/
|
|
8
|
+
// removeLink never re-sync a live node's own .wires array mid-session
|
|
9
|
+
// (CLAUDE-010 — the drift this module exists to prevent from
|
|
10
|
+
// recurring). Diff computation (apply-review.js's computeWireDiff and
|
|
11
|
+
// its own eachLink scans) is a different concern — "what changed" vs.
|
|
12
|
+
// "is this true right now" — and stays out of scope per ADR-005.
|
|
13
|
+
|
|
14
|
+
function nodeExists(id) {
|
|
15
|
+
return !!RED.nodes.node(id);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function nodeAbsent(id) {
|
|
19
|
+
return !RED.nodes.node(id);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function propertyEquals(id, key, want) {
|
|
23
|
+
var node = RED.nodes.node(id);
|
|
24
|
+
return !!node && node[key] === want;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Read-only counterpart to propertyEquals, for diagnostics that need to
|
|
28
|
+
// show the actual current value alongside the expected one (CLAUDE-018)
|
|
29
|
+
// rather than just a boolean match. Returns { exists, value } — value is
|
|
30
|
+
// undefined when the node doesn't exist so callers can tell "no node"
|
|
31
|
+
// apart from "property is actually undefined".
|
|
32
|
+
function readProperty(id, key) {
|
|
33
|
+
var node = RED.nodes.node(id);
|
|
34
|
+
return { exists: !!node, value: node ? node[key] : undefined };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function wireExists(fromId, port, toId) {
|
|
38
|
+
var found = false;
|
|
39
|
+
RED.nodes.eachLink(function (l) {
|
|
40
|
+
if (found) { return; }
|
|
41
|
+
if (l.source && l.source.id === fromId &&
|
|
42
|
+
(l.sourcePort || 0) === (port || 0) &&
|
|
43
|
+
l.target && l.target.id === toId) {
|
|
44
|
+
found = true;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function wireAbsent(fromId, port, toId) {
|
|
51
|
+
return !wireExists(fromId, port, toId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ids: a single node id or an array of ids — true only if every one of
|
|
55
|
+
// them currently belongs to groupId (per its live .g ref).
|
|
56
|
+
function groupContains(groupId, ids) {
|
|
57
|
+
var list = Array.isArray(ids) ? ids : [ids];
|
|
58
|
+
if (!list.length) { return false; }
|
|
59
|
+
return list.every(function (id) {
|
|
60
|
+
var node = RED.nodes.node(id);
|
|
61
|
+
return !!node && node.g === groupId;
|
|
62
|
+
});
|
|
63
|
+
}
|
package/lib/core/history.js
CHANGED
|
@@ -17,10 +17,19 @@
|
|
|
17
17
|
return id;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// CLAUDE-029: whether conversationId above came from an existing
|
|
21
|
+
// sessionStorage entry (a page reload continuing a prior conversation)
|
|
22
|
+
// rather than being freshly minted — drives whether page init rehydrates
|
|
23
|
+
// the Chat panel from the server. See rehydrateConversationOnLoad().
|
|
24
|
+
var conversationIdWasRestored = false;
|
|
25
|
+
|
|
20
26
|
var conversationId = (function () {
|
|
21
27
|
try {
|
|
22
28
|
var existing = sessionStorage.getItem("fp-conversation-id");
|
|
23
|
-
if (existing) {
|
|
29
|
+
if (existing) {
|
|
30
|
+
conversationIdWasRestored = true;
|
|
31
|
+
return existing;
|
|
32
|
+
}
|
|
24
33
|
} catch (e) { /* storage unavailable */ }
|
|
25
34
|
return newConversationId();
|
|
26
35
|
})();
|