@manny-est/node-red-flowpilot 0.5.0 → 0.5.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 +15 -0
- package/flowpilot-core.css +59 -0
- package/flowpilot.js +85 -11
- package/lib/core/apply-review.js +278 -29
- package/lib/core/init.js +57 -132
- package/lib/core/main.js +68 -1
- package/lib/core/modes.js +180 -57
- package/lib/provider-openai-compatible.js +112 -15
- package/package.json +2 -2
package/lib/core/init.js
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"- `/feedback` — bug report / feature request info\n" +
|
|
24
24
|
"- `/build` — describe a goal; I'll plan, propose, and walk an iterative build → deploy → debug → review → fix loop with you\n" +
|
|
25
25
|
"- `/compact` — hide labels on the selected node(s) (icon-only); `/expand` restores them. Instant, no AI involved — one Ctrl+Z undoes it.\n" +
|
|
26
|
-
"- `/disable` — disable the selected node(s) (skipped on Deploy); `/enable` re-enables them. Instant, no AI involved — one Ctrl+Z undoes it.\n
|
|
26
|
+
"- `/disable` — disable the selected node(s) (skipped on Deploy); `/enable` re-enables them. Instant, no AI involved — one Ctrl+Z undoes it.\n" +
|
|
27
|
+
"- `/refresh` — re-render all messages from the in-memory record store (restores interactive Apply buttons if they were lost).\n\n" +
|
|
27
28
|
"### Also worth knowing\n\n" +
|
|
28
29
|
"- Action chips (paper-plane buttons) offer a one-click follow-up — review and send, nothing fires automatically.\n" +
|
|
29
30
|
"- When I ask a clarifying question, I'll often offer quick-reply buttons (plus \"Other\" for your own answer) — clicking one sends it right away.\n" +
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
{ cmd: "/expand", desc: "Expand labels on selected nodes" },
|
|
63
64
|
{ cmd: "/disable", desc: "Disable selected nodes" },
|
|
64
65
|
{ cmd: "/enable", desc: "Enable selected nodes" },
|
|
66
|
+
{ cmd: "/refresh", desc: "Re-render all messages from shadow record store" },
|
|
65
67
|
{ cmd: "/demo", desc: "Type in a demo prompt" },
|
|
66
68
|
{ cmd: "/help", desc: "Show all available commands" },
|
|
67
69
|
{ cmd: "/feedback", desc: "Show feedback info" }
|
|
@@ -231,6 +233,10 @@
|
|
|
231
233
|
addMessage("assistant", FEEDBACK_TEXT);
|
|
232
234
|
if ($promptBox.length) { $promptBox.val(""); }
|
|
233
235
|
break;
|
|
236
|
+
case "/refresh":
|
|
237
|
+
refreshView();
|
|
238
|
+
if ($promptBox.length) { $promptBox.val(""); }
|
|
239
|
+
break;
|
|
234
240
|
// Deterministic, no LLM round-trip: just invokes Node-RED's own
|
|
235
241
|
// native "show/hide selected node labels" action (RED.actions
|
|
236
242
|
// "core:show-selected-node-labels" / "core:hide-selected-node-
|
|
@@ -549,105 +555,25 @@
|
|
|
549
555
|
// selection-status strip is a relayed MIRROR (relayStatusStripToPopout,
|
|
550
556
|
// called from the parent's updateSelectionStatus/updateDebugStatus).
|
|
551
557
|
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
// snapshot was relayed. This just rebinds ONE button to ask the parent
|
|
560
|
-
// to do exactly what it would do if the same button were clicked in
|
|
561
|
-
// the sidebar.
|
|
562
|
-
function bindApplyButtons($scope) {
|
|
563
|
-
// appendMessage's scope IS the tagged panel itself (a single newly
|
|
564
|
-
// relayed top-level element); initialSync's scope is the container
|
|
565
|
-
// around many descendants — cover both with filter()+find().
|
|
566
|
-
$scope.filter("[data-fp-apply-flow]").add($scope.find("[data-fp-apply-flow]")).each(function () {
|
|
567
|
-
var $panel = $(this);
|
|
568
|
-
if ($panel.data("fp-apply-bound")) { return; }
|
|
569
|
-
$panel.data("fp-apply-bound", true);
|
|
570
|
-
var flow;
|
|
571
|
-
try { flow = JSON.parse($panel.attr("data-fp-apply-flow")); } catch (e) { return; }
|
|
572
|
-
$panel.find(".fp-review-actions button.red-ui-button-primary").on("click", function () {
|
|
573
|
-
var $btn = $(this);
|
|
574
|
-
$btn.prop("disabled", true).text("Click the canvas to place…");
|
|
575
|
-
if (!window.opener || window.opener.closed) { return; }
|
|
576
|
-
try {
|
|
577
|
-
window.opener.postMessage({ event: "applyGenerated", flow: flow }, location.origin);
|
|
578
|
-
} catch (e) { /* ignore */ }
|
|
579
|
-
});
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// Same idea as bindApplyButtons, but for a relayed PLAIN Modify review
|
|
584
|
-
// panel (addModifyReview tags these with data-fp-apply-modify; a
|
|
585
|
-
// /build loop fix gets a separate tag — see bindBuildFixApplyButtons).
|
|
586
|
-
// nodeDiffs/removeNodes/newNodes/newWires already reflect the diff the
|
|
587
|
-
// parent computed against live RED.nodes state at review time; the
|
|
588
|
-
// pop-out doesn't recompute anything, it just asks the parent to run
|
|
589
|
-
// applyInsertions/applyModifications with this exact data, same as a
|
|
590
|
-
// sidebar click would.
|
|
591
|
-
function bindModifyApplyButtons($scope) {
|
|
592
|
-
$scope.filter("[data-fp-apply-modify]").add($scope.find("[data-fp-apply-modify]")).each(function () {
|
|
558
|
+
// Phase 10 0B: all review panels carry data-fp-record-id instead of the
|
|
559
|
+
// old per-kind data-fp-apply-* attribute family. The pop-out posts a
|
|
560
|
+
// recordId to the parent, which looks up the live record and applies from
|
|
561
|
+
// stored payload — no giant JSON blob in the DOM attribute, no separate
|
|
562
|
+
// bind function per review kind.
|
|
563
|
+
function bindReviewApplyButtons($scope) {
|
|
564
|
+
$scope.filter("[data-fp-record-id]").add($scope.find("[data-fp-record-id]")).each(function () {
|
|
593
565
|
var $panel = $(this);
|
|
594
|
-
if ($panel.data("fp-apply-
|
|
595
|
-
$panel.data("fp-apply-
|
|
596
|
-
var
|
|
597
|
-
|
|
566
|
+
if ($panel.data("fp-review-apply-bound")) { return; }
|
|
567
|
+
$panel.data("fp-review-apply-bound", true);
|
|
568
|
+
var recordId = parseInt($panel.attr("data-fp-record-id"), 10);
|
|
569
|
+
if (isNaN(recordId)) { return; }
|
|
598
570
|
$panel.find(".fp-review-actions button.red-ui-button-primary").on("click", function () {
|
|
599
571
|
var $btn = $(this);
|
|
572
|
+
if ($btn.prop("disabled")) { return; }
|
|
600
573
|
$btn.prop("disabled", true).text("Applying…");
|
|
601
574
|
if (!window.opener || window.opener.closed) { return; }
|
|
602
575
|
try {
|
|
603
|
-
window.opener.postMessage({ event: "
|
|
604
|
-
} catch (e) { /* ignore */ }
|
|
605
|
-
});
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// /build loop, first proposal: addGeneratedReview tags this panel
|
|
610
|
-
// (onImported set AND a buildGoal — plain Generate/Document panels get
|
|
611
|
-
// data-fp-apply-flow instead, bound above) with the flow plus the
|
|
612
|
-
// original goal text. The parent's "applyBuild" handler runs
|
|
613
|
-
// importGeneratedFlow then startBuildLoop with it, exactly like
|
|
614
|
-
// handleBuildResult's own onImported closure would.
|
|
615
|
-
function bindBuildApplyButtons($scope) {
|
|
616
|
-
$scope.filter("[data-fp-apply-build]").add($scope.find("[data-fp-apply-build]")).each(function () {
|
|
617
|
-
var $panel = $(this);
|
|
618
|
-
if ($panel.data("fp-apply-build-bound")) { return; }
|
|
619
|
-
$panel.data("fp-apply-build-bound", true);
|
|
620
|
-
var applyData;
|
|
621
|
-
try { applyData = JSON.parse($panel.attr("data-fp-apply-build")); } catch (e) { return; }
|
|
622
|
-
$panel.find(".fp-review-actions button.red-ui-button-primary").on("click", function () {
|
|
623
|
-
var $btn = $(this);
|
|
624
|
-
$btn.prop("disabled", true).text("Click the canvas to place…");
|
|
625
|
-
if (!window.opener || window.opener.closed) { return; }
|
|
626
|
-
try {
|
|
627
|
-
window.opener.postMessage({ event: "applyBuild", data: applyData }, location.origin);
|
|
628
|
-
} catch (e) { /* ignore */ }
|
|
629
|
-
});
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// /build loop, fix iterations: addModifyReview tags this panel with
|
|
634
|
-
// data-fp-apply-build-fix (instead of data-fp-apply-modify) when
|
|
635
|
-
// buildFixInfo was passed — see applyBuildLoopFix. The parent's
|
|
636
|
-
// "applyBuildFix" handler runs applyInsertions/applyBuildLoopFix
|
|
637
|
-
// with the relayed data, same loop bookkeeping a local click would do.
|
|
638
|
-
function bindBuildFixApplyButtons($scope) {
|
|
639
|
-
$scope.filter("[data-fp-apply-build-fix]").add($scope.find("[data-fp-apply-build-fix]")).each(function () {
|
|
640
|
-
var $panel = $(this);
|
|
641
|
-
if ($panel.data("fp-apply-build-fix-bound")) { return; }
|
|
642
|
-
$panel.data("fp-apply-build-fix-bound", true);
|
|
643
|
-
var applyData;
|
|
644
|
-
try { applyData = JSON.parse($panel.attr("data-fp-apply-build-fix")); } catch (e) { return; }
|
|
645
|
-
$panel.find(".fp-review-actions button.red-ui-button-primary").on("click", function () {
|
|
646
|
-
var $btn = $(this);
|
|
647
|
-
$btn.prop("disabled", true).text("Applying…");
|
|
648
|
-
if (!window.opener || window.opener.closed) { return; }
|
|
649
|
-
try {
|
|
650
|
-
window.opener.postMessage({ event: "applyBuildFix", data: applyData }, location.origin);
|
|
576
|
+
window.opener.postMessage({ event: "applyByRecordId", recordId: recordId }, location.origin);
|
|
651
577
|
} catch (e) { /* ignore */ }
|
|
652
578
|
});
|
|
653
579
|
});
|
|
@@ -903,20 +829,14 @@
|
|
|
903
829
|
var data = evt.data || {};
|
|
904
830
|
if (data.event === "initialSync") {
|
|
905
831
|
el("#fp-messages").html(data.html);
|
|
906
|
-
|
|
907
|
-
bindModifyApplyButtons(el("#fp-messages"));
|
|
908
|
-
bindBuildApplyButtons(el("#fp-messages"));
|
|
909
|
-
bindBuildFixApplyButtons(el("#fp-messages"));
|
|
832
|
+
bindReviewApplyButtons(el("#fp-messages"));
|
|
910
833
|
bindStopLoopButton(el("#fp-messages"));
|
|
911
834
|
bindTabSwitching(el("#fp-messages"));
|
|
912
835
|
bindDebugAttachButtons(el("#fp-messages"));
|
|
913
836
|
scrollMessagesToBottom(true);
|
|
914
837
|
} else if (data.event === "appendMessage") {
|
|
915
838
|
el("#fp-messages").append(data.html);
|
|
916
|
-
|
|
917
|
-
bindModifyApplyButtons(el("#fp-messages").children().last());
|
|
918
|
-
bindBuildApplyButtons(el("#fp-messages").children().last());
|
|
919
|
-
bindBuildFixApplyButtons(el("#fp-messages").children().last());
|
|
839
|
+
bindReviewApplyButtons(el("#fp-messages").children().last());
|
|
920
840
|
bindStopLoopButton(el("#fp-messages").children().last());
|
|
921
841
|
bindTabSwitching(el("#fp-messages").children().last());
|
|
922
842
|
bindDebugAttachButtons(el("#fp-messages").children().last());
|
|
@@ -1305,7 +1225,13 @@
|
|
|
1305
1225
|
|
|
1306
1226
|
// Re-enable Test provider live as the user types a model.
|
|
1307
1227
|
content.find("#fp-model").on("input", function () {
|
|
1308
|
-
|
|
1228
|
+
var val = ($(this).val() || "").trim();
|
|
1229
|
+
el("#fp-test-provider").prop("disabled", !val);
|
|
1230
|
+
// Mirror live edits to the provider status so the user can see
|
|
1231
|
+
// what model will be used before hitting Save.
|
|
1232
|
+
var ap = activeProvider();
|
|
1233
|
+
var name = (ap && ap.providerName) || "Provider";
|
|
1234
|
+
el("#fp-provider-status").text("Provider: " + (val ? (name + " / " + val) : name + " (no model)"));
|
|
1309
1235
|
});
|
|
1310
1236
|
|
|
1311
1237
|
content.find("#fp-save-settings").on("click", function () {
|
|
@@ -1402,36 +1328,35 @@
|
|
|
1402
1328
|
else { send("chat"); }
|
|
1403
1329
|
} else if (data.event === "runSlashCommand" && data.command) {
|
|
1404
1330
|
handleSlashCommand(data.command);
|
|
1405
|
-
} else if (data.event === "
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
var idMap = {};
|
|
1410
|
-
if (Array.isArray(ad.newNodes) && ad.newNodes.length) {
|
|
1411
|
-
idMap = applyInsertions(ad.newNodes, ad.newWires || [], ad.existingNodeIds || []) || {};
|
|
1412
|
-
}
|
|
1413
|
-
if (ad.hasMutations) {
|
|
1414
|
-
applyModifications(ad.nodeDiffs || [], ad.removeNodes || [], null, idMap);
|
|
1415
|
-
}
|
|
1416
|
-
if (Array.isArray(ad.newGroups) && ad.newGroups.length) {
|
|
1417
|
-
applyGroupChanges(ad.newGroups, idMap);
|
|
1331
|
+
} else if (data.event === "applyByRecordId" && typeof data.recordId === "number") {
|
|
1332
|
+
var rec = null;
|
|
1333
|
+
for (var ri = 0; ri < messageRecords.length; ri++) {
|
|
1334
|
+
if (messageRecords[ri].id === data.recordId) { rec = messageRecords[ri]; break; }
|
|
1418
1335
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1336
|
+
if (rec && rec.kind === "review" && rec.state !== "applied") {
|
|
1337
|
+
var d0 = rec.sharedApplyData || {};
|
|
1338
|
+
var nd0 = Array.isArray(d0.nodeDiffs) ? d0.nodeDiffs : [];
|
|
1339
|
+
var rn0 = Array.isArray(d0.removeNodes) ? d0.removeNodes : [];
|
|
1340
|
+
var nn0 = Array.isArray(d0.newNodes) ? d0.newNodes : [];
|
|
1341
|
+
var nw0 = Array.isArray(d0.newWires) ? d0.newWires : [];
|
|
1342
|
+
var ng0 = Array.isArray(d0.newGroups) ? d0.newGroups : [];
|
|
1343
|
+
var eids0 = Array.isArray(d0.existingNodeIds) ? d0.existingNodeIds : [];
|
|
1344
|
+
var idMap0 = {};
|
|
1345
|
+
if (rec.subkind === "generate") {
|
|
1346
|
+
importGeneratedFlow(rec.flow || [], null);
|
|
1347
|
+
} else if (rec.subkind === "build-generate") {
|
|
1348
|
+
importGeneratedFlow(rec.flow || [], function (importResult) {
|
|
1349
|
+
startBuildLoop(rec.buildGoal || "", rec.flow || [], importResult);
|
|
1350
|
+
});
|
|
1351
|
+
} else if (rec.subkind === "modify") {
|
|
1352
|
+
if (nn0.length) { idMap0 = applyInsertions(nn0, nw0, eids0) || {}; }
|
|
1353
|
+
if (d0.hasMutations) { applyModifications(nd0, rn0, null, idMap0); }
|
|
1354
|
+
if (ng0.length) { applyGroupChanges(ng0, idMap0); }
|
|
1355
|
+
} else if (rec.subkind === "build-fix") {
|
|
1356
|
+
if (nn0.length) { idMap0 = applyInsertions(nn0, nw0, eids0) || {}; }
|
|
1357
|
+
applyBuildLoopFix(nd0, rn0, idMap0, !!d0.capReached);
|
|
1358
|
+
}
|
|
1359
|
+
rec.state = "applied";
|
|
1435
1360
|
}
|
|
1436
1361
|
} else if (data.event === "stopBuildLoop") {
|
|
1437
1362
|
stopBuildLoop("Build loop stopped — applied nodes remain as-is.");
|
package/lib/core/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
var VERSION = "0.5.
|
|
2
|
+
var VERSION = "0.5.1";
|
|
3
3
|
|
|
4
4
|
// Idempotency guard: Node-RED can invoke a plugin's onadd more than once
|
|
5
5
|
// in a single editor load. Without this, each call builds another #fp-root
|
|
@@ -30,6 +30,70 @@
|
|
|
30
30
|
// settings) is pure local state and needs no flag at all.
|
|
31
31
|
var isPopoutContext = false;
|
|
32
32
|
|
|
33
|
+
// ---------------------------------------------------------------------
|
|
34
|
+
// Shadow record store (Phase 10, Workstream 0A).
|
|
35
|
+
// Every code path that appends DOM to #fp-messages also appends a record
|
|
36
|
+
// here. refreshView() clears the message container and re-renders from
|
|
37
|
+
// records, restoring interactive elements without losing conversation.
|
|
38
|
+
// Records are in-memory only — no sessionStorage, no persistence.
|
|
39
|
+
// ---------------------------------------------------------------------
|
|
40
|
+
var messageRecords = [];
|
|
41
|
+
var _nextRecordId = 0;
|
|
42
|
+
|
|
43
|
+
function addRecord(kind, payload) {
|
|
44
|
+
var rec = { id: _nextRecordId++, ts: Date.now(), kind: kind };
|
|
45
|
+
if (payload) {
|
|
46
|
+
Object.keys(payload).forEach(function (k) { rec[k] = payload[k]; });
|
|
47
|
+
}
|
|
48
|
+
messageRecords.push(rec);
|
|
49
|
+
return rec;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function refreshView() {
|
|
53
|
+
var records = messageRecords.slice();
|
|
54
|
+
var $box = el("#fp-messages");
|
|
55
|
+
if (!$box.length) { return; }
|
|
56
|
+
messageRecords = [];
|
|
57
|
+
$box.empty();
|
|
58
|
+
records.forEach(rerenderRecord);
|
|
59
|
+
scrollMessagesToBottom(true);
|
|
60
|
+
// Sync pop-out: cheapest approach is a full re-sync of the
|
|
61
|
+
// refreshed HTML (same as the initial pop-out open sync).
|
|
62
|
+
if (popoutWindow && !popoutWindow.closed) {
|
|
63
|
+
try {
|
|
64
|
+
popoutWindow.postMessage({
|
|
65
|
+
event: "initialSync",
|
|
66
|
+
html: el("#fp-messages").html()
|
|
67
|
+
}, location.origin);
|
|
68
|
+
} catch (e) { /* ignore */ }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function rerenderRecord(rec) {
|
|
73
|
+
if (!rec) { return; }
|
|
74
|
+
switch (rec.kind) {
|
|
75
|
+
case "chat":
|
|
76
|
+
addMessage(rec.role || "assistant", rec.text || "");
|
|
77
|
+
break;
|
|
78
|
+
case "chip":
|
|
79
|
+
if (rec.chipType === "suggestedAction") { renderActionChip(rec.suggestedAction); }
|
|
80
|
+
break;
|
|
81
|
+
case "question":
|
|
82
|
+
if (rec.loopCheckpoint) {
|
|
83
|
+
renderLoopCheckpoint(activeBuildLoop);
|
|
84
|
+
} else {
|
|
85
|
+
renderClarifyingQuestion(rec.options || []);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
case "review":
|
|
89
|
+
rerenderReviewRecord(rec);
|
|
90
|
+
break;
|
|
91
|
+
case "buildStep":
|
|
92
|
+
rerenderBuildStepRecord(rec);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
33
97
|
// Holds the most recently loaded settings so warning logic can read the
|
|
34
98
|
// user's thresholds and suppression preference without refetching.
|
|
35
99
|
var currentSettings = {};
|
|
@@ -518,6 +582,7 @@
|
|
|
518
582
|
// sees — "start a fresh conversation".
|
|
519
583
|
function clearChat() {
|
|
520
584
|
el("#fp-messages").empty();
|
|
585
|
+
messageRecords = [];
|
|
521
586
|
relayClearMessagesToPopout();
|
|
522
587
|
conversationHistory = [];
|
|
523
588
|
attachedDebugMessages = [];
|
|
@@ -828,9 +893,11 @@
|
|
|
828
893
|
$("<div>").addClass("fp-md").html(renderMarkdown(text || "")).appendTo($msg);
|
|
829
894
|
|
|
830
895
|
$box.append($msg);
|
|
896
|
+
var _rec = addRecord("chat", { role: role, text: text || "" });
|
|
831
897
|
// Sending a message always jumps to the bottom and resumes
|
|
832
898
|
// auto-follow; an incoming message only follows if already snapped.
|
|
833
899
|
scrollMessagesToBottom(role === "user");
|
|
900
|
+
return _rec;
|
|
834
901
|
}
|
|
835
902
|
|
|
836
903
|
// Pending "typing" indicator shown in the thread while awaiting a reply.
|