@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/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\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
- // Slice 3: relayed HTML for a plain Generate/Document review panel
553
- // (addGeneratedReview tags these with data-fp-apply-flow see there
554
- // for why the /build loop's panels are excluded) carries the flow data
555
- // right in the markup, since outerHTML serializes every attribute.
556
- // Re-validating/re-rendering in the pop-out isn't needed (and wouldn't
557
- // work anyway — RED.nodes.getType has no installed types in this
558
- // window) — the parent already validated everything before the
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-modify-bound")) { return; }
595
- $panel.data("fp-apply-modify-bound", true);
596
- var applyData;
597
- try { applyData = JSON.parse($panel.attr("data-fp-apply-modify")); } catch (e) { return; }
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: "applyModify", data: applyData }, location.origin);
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
- bindApplyButtons(el("#fp-messages"));
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
- bindApplyButtons(el("#fp-messages").children().last());
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
- el("#fp-test-provider").prop("disabled", !($(this).val() || "").trim());
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 === "applyGenerated" && Array.isArray(data.flow)) {
1406
- importGeneratedFlow(data.flow);
1407
- } else if (data.event === "applyModify" && data.data) {
1408
- var ad = data.data;
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
- } else if (data.event === "applyBuild" && data.data && Array.isArray(data.data.flow)) {
1420
- var bd = data.data;
1421
- importGeneratedFlow(bd.flow, function (importResult) {
1422
- startBuildLoop(bd.goal, bd.flow, importResult);
1423
- });
1424
- } else if (data.event === "applyBuildFix" && data.data) {
1425
- var bf = data.data;
1426
- var fixIdMap = {};
1427
- if (Array.isArray(bf.newNodes) && bf.newNodes.length) {
1428
- fixIdMap = applyInsertions(bf.newNodes, bf.newWires || [], bf.existingNodeIds || []) || {};
1429
- }
1430
- if (bf.hasMutations) {
1431
- applyBuildLoopFix(bf.nodeDiffs || [], bf.removeNodes || [], fixIdMap, !!bf.capReached);
1432
- }
1433
- if (Array.isArray(bf.newGroups) && bf.newGroups.length) {
1434
- applyGroupChanges(bf.newGroups, fixIdMap);
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.0";
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.