@manny-est/node-red-flowpilot 0.5.0 → 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 +94 -0
- package/README.md +21 -7
- package/USER-GUIDE.md +22 -13
- package/flowpilot-core.css +187 -2
- package/flowpilot.js +462 -33
- package/lib/build-system-prompt.js +26 -4
- package/lib/core/apply-review.js +494 -64
- package/lib/core/init.js +67 -132
- package/lib/core/main.js +160 -7
- package/lib/core/modes.js +876 -79
- 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 +122 -16
- package/lib/storage.js +43 -2
- package/lib/validator.js +238 -0
- 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 () {
|
|
593
|
-
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; }
|
|
598
|
-
$panel.find(".fp-review-actions button.red-ui-button-primary").on("click", function () {
|
|
599
|
-
var $btn = $(this);
|
|
600
|
-
$btn.prop("disabled", true).text("Applying…");
|
|
601
|
-
if (!window.opener || window.opener.closed) { return; }
|
|
602
|
-
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 () {
|
|
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 () {
|
|
640
565
|
var $panel = $(this);
|
|
641
|
-
if ($panel.data("fp-apply-
|
|
642
|
-
$panel.data("fp-apply-
|
|
643
|
-
var
|
|
644
|
-
|
|
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; }
|
|
645
570
|
$panel.find(".fp-review-actions button.red-ui-button-primary").on("click", function () {
|
|
646
571
|
var $btn = $(this);
|
|
572
|
+
if ($btn.prop("disabled")) { return; }
|
|
647
573
|
$btn.prop("disabled", true).text("Applying…");
|
|
648
574
|
if (!window.opener || window.opener.closed) { return; }
|
|
649
575
|
try {
|
|
650
|
-
window.opener.postMessage({ event: "
|
|
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());
|
|
@@ -1080,8 +1000,14 @@
|
|
|
1080
1000
|
' </div>' +
|
|
1081
1001
|
' <label>Provider Name</label>' +
|
|
1082
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>' +
|
|
1083
1008
|
' <label>Base URL</label>' +
|
|
1084
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>' +
|
|
1085
1011
|
' <label>API Key</label>' +
|
|
1086
1012
|
' <input id="fp-api-key" type="password" placeholder="Optional">' +
|
|
1087
1013
|
' <label>Model</label>' +
|
|
@@ -1253,6 +1179,7 @@
|
|
|
1253
1179
|
// Nothing is sent to the backend until the user explicitly
|
|
1254
1180
|
// attaches a message and sends a request.
|
|
1255
1181
|
try { RED.comms.subscribe("debug", onDebugMessage); } catch (e) { /* comms unavailable */ }
|
|
1182
|
+
try { RED.comms.subscribe("status/#", onNodeStatus); } catch (e) { /* comms unavailable */ }
|
|
1256
1183
|
|
|
1257
1184
|
// Track whether the user is scrolled to the bottom of the chat,
|
|
1258
1185
|
// so "Cruising…"/streaming updates only auto-follow when they
|
|
@@ -1296,6 +1223,9 @@
|
|
|
1296
1223
|
content.find("#fp-provider-select").on("change", function () {
|
|
1297
1224
|
switchProvider($(this).val());
|
|
1298
1225
|
});
|
|
1226
|
+
content.find("#fp-provider-type").on("change", function () {
|
|
1227
|
+
toggleAnthropicHint($(this).val());
|
|
1228
|
+
});
|
|
1299
1229
|
content.find("#fp-add-provider").on("click", function () { addProvider(); });
|
|
1300
1230
|
content.find("#fp-remove-provider").on("click", function () { removeProvider(); });
|
|
1301
1231
|
content.find("#fp-test-provider").on("click", function () { testProvider(); });
|
|
@@ -1305,7 +1235,13 @@
|
|
|
1305
1235
|
|
|
1306
1236
|
// Re-enable Test provider live as the user types a model.
|
|
1307
1237
|
content.find("#fp-model").on("input", function () {
|
|
1308
|
-
|
|
1238
|
+
var val = ($(this).val() || "").trim();
|
|
1239
|
+
el("#fp-test-provider").prop("disabled", !val);
|
|
1240
|
+
// Mirror live edits to the provider status so the user can see
|
|
1241
|
+
// what model will be used before hitting Save.
|
|
1242
|
+
var ap = activeProvider();
|
|
1243
|
+
var name = (ap && ap.providerName) || "Provider";
|
|
1244
|
+
el("#fp-provider-status").text("Provider: " + (val ? (name + " / " + val) : name + " (no model)"));
|
|
1309
1245
|
});
|
|
1310
1246
|
|
|
1311
1247
|
content.find("#fp-save-settings").on("click", function () {
|
|
@@ -1402,36 +1338,35 @@
|
|
|
1402
1338
|
else { send("chat"); }
|
|
1403
1339
|
} else if (data.event === "runSlashCommand" && data.command) {
|
|
1404
1340
|
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);
|
|
1341
|
+
} else if (data.event === "applyByRecordId" && typeof data.recordId === "number") {
|
|
1342
|
+
var rec = null;
|
|
1343
|
+
for (var ri = 0; ri < messageRecords.length; ri++) {
|
|
1344
|
+
if (messageRecords[ri].id === data.recordId) { rec = messageRecords[ri]; break; }
|
|
1415
1345
|
}
|
|
1416
|
-
if (
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1346
|
+
if (rec && rec.kind === "review" && rec.state !== "applied") {
|
|
1347
|
+
var d0 = rec.sharedApplyData || {};
|
|
1348
|
+
var nd0 = Array.isArray(d0.nodeDiffs) ? d0.nodeDiffs : [];
|
|
1349
|
+
var rn0 = Array.isArray(d0.removeNodes) ? d0.removeNodes : [];
|
|
1350
|
+
var nn0 = Array.isArray(d0.newNodes) ? d0.newNodes : [];
|
|
1351
|
+
var nw0 = Array.isArray(d0.newWires) ? d0.newWires : [];
|
|
1352
|
+
var ng0 = Array.isArray(d0.newGroups) ? d0.newGroups : [];
|
|
1353
|
+
var eids0 = Array.isArray(d0.existingNodeIds) ? d0.existingNodeIds : [];
|
|
1354
|
+
var idMap0 = {};
|
|
1355
|
+
if (rec.subkind === "generate") {
|
|
1356
|
+
importGeneratedFlow(rec.flow || [], null);
|
|
1357
|
+
} else if (rec.subkind === "build-generate") {
|
|
1358
|
+
importGeneratedFlow(rec.flow || [], function (importResult) {
|
|
1359
|
+
startBuildLoop(rec.buildGoal || "", rec.flow || [], importResult);
|
|
1360
|
+
});
|
|
1361
|
+
} else if (rec.subkind === "modify") {
|
|
1362
|
+
if (nn0.length) { idMap0 = applyInsertions(nn0, nw0, eids0) || {}; }
|
|
1363
|
+
if (d0.hasMutations) { applyModifications(nd0, rn0, null, idMap0); }
|
|
1364
|
+
if (ng0.length) { applyGroupChanges(ng0, idMap0); }
|
|
1365
|
+
} else if (rec.subkind === "build-fix") {
|
|
1366
|
+
if (nn0.length) { idMap0 = applyInsertions(nn0, nw0, eids0) || {}; }
|
|
1367
|
+
applyBuildLoopFix(nd0, rn0, idMap0, !!d0.capReached);
|
|
1368
|
+
}
|
|
1369
|
+
rec.state = "applied";
|
|
1435
1370
|
}
|
|
1436
1371
|
} else if (data.event === "stopBuildLoop") {
|
|
1437
1372
|
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,75 @@
|
|
|
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 if (rec.buildConsentGate) {
|
|
85
|
+
renderBuildConsentGate(rec);
|
|
86
|
+
} else {
|
|
87
|
+
renderClarifyingQuestion(rec.options || []);
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
case "review":
|
|
91
|
+
rerenderReviewRecord(rec);
|
|
92
|
+
break;
|
|
93
|
+
case "buildStep":
|
|
94
|
+
rerenderBuildStepRecord(rec);
|
|
95
|
+
break;
|
|
96
|
+
case "todo":
|
|
97
|
+
rerenderTodoRecord(rec);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
33
102
|
// Holds the most recently loaded settings so warning logic can read the
|
|
34
103
|
// user's thresholds and suppression preference without refetching.
|
|
35
104
|
var currentSettings = {};
|
|
@@ -77,6 +146,7 @@
|
|
|
77
146
|
var entry = {
|
|
78
147
|
id: nextDebugMessageId++,
|
|
79
148
|
timestamp: Date.now(),
|
|
149
|
+
sourceKind: "debug",
|
|
80
150
|
name: msg.name || msg.id || "(unnamed node)",
|
|
81
151
|
topic: redactedTopic,
|
|
82
152
|
// previewValue: short, for the scannable debug-log list only.
|
|
@@ -109,8 +179,14 @@
|
|
|
109
179
|
// message from one trigger accumulate into attachedDebugMessages
|
|
110
180
|
// before review actually runs, so the model sees the full picture
|
|
111
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.
|
|
112
187
|
if (activeBuildLoop && activeBuildLoop.waypoint === "attach" &&
|
|
113
|
-
activeBuildLoop.nodeIds.indexOf(msg.id) !== -1
|
|
188
|
+
activeBuildLoop.nodeIds.indexOf(msg.id) !== -1 &&
|
|
189
|
+
(activeBuildLoop.skipCheckpointTapIds || []).indexOf(msg.id) === -1) {
|
|
114
190
|
attachedDebugMessages.push(entry);
|
|
115
191
|
updateDebugStatus();
|
|
116
192
|
if (buildLoopNoDebugTimer) { clearTimeout(buildLoopNoDebugTimer); buildLoopNoDebugTimer = null; }
|
|
@@ -129,11 +205,76 @@
|
|
|
129
205
|
}
|
|
130
206
|
}
|
|
131
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
|
+
|
|
132
266
|
// The exact shape sent to the backend (and shown by "Preview debug") —
|
|
133
267
|
// excludes previewValue, which exists only for the debug-log list.
|
|
134
268
|
function buildDebugMessagesForSend() {
|
|
135
269
|
return attachedDebugMessages.map(function (m) {
|
|
136
|
-
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
|
+
};
|
|
137
278
|
});
|
|
138
279
|
}
|
|
139
280
|
|
|
@@ -518,6 +659,7 @@
|
|
|
518
659
|
// sees — "start a fresh conversation".
|
|
519
660
|
function clearChat() {
|
|
520
661
|
el("#fp-messages").empty();
|
|
662
|
+
messageRecords = [];
|
|
521
663
|
relayClearMessagesToPopout();
|
|
522
664
|
conversationHistory = [];
|
|
523
665
|
attachedDebugMessages = [];
|
|
@@ -828,9 +970,11 @@
|
|
|
828
970
|
$("<div>").addClass("fp-md").html(renderMarkdown(text || "")).appendTo($msg);
|
|
829
971
|
|
|
830
972
|
$box.append($msg);
|
|
973
|
+
var _rec = addRecord("chat", { role: role, text: text || "" });
|
|
831
974
|
// Sending a message always jumps to the bottom and resumes
|
|
832
975
|
// auto-follow; an incoming message only follows if already snapped.
|
|
833
976
|
scrollMessagesToBottom(role === "user");
|
|
977
|
+
return _rec;
|
|
834
978
|
}
|
|
835
979
|
|
|
836
980
|
// Pending "typing" indicator shown in the thread while awaiting a reply.
|
|
@@ -975,10 +1119,19 @@
|
|
|
975
1119
|
if (active) { $sel.val(active.id); }
|
|
976
1120
|
}
|
|
977
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
|
+
|
|
978
1128
|
// Write the form fields from a given provider profile.
|
|
979
1129
|
function fillProviderFields(p) {
|
|
980
1130
|
p = p || {};
|
|
981
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);
|
|
982
1135
|
el("#fp-base-url").val(p.baseUrl || "");
|
|
983
1136
|
el("#fp-api-key").val(p.apiKey || "");
|
|
984
1137
|
el("#fp-model").val(p.model || "");
|
|
@@ -1067,6 +1220,7 @@
|
|
|
1067
1220
|
var ap = activeProvider();
|
|
1068
1221
|
if (!ap) { return; }
|
|
1069
1222
|
ap.providerName = el("#fp-provider-name").val() || "Provider";
|
|
1223
|
+
ap.type = el("#fp-provider-type").val() || "openai-compatible";
|
|
1070
1224
|
ap.baseUrl = el("#fp-base-url").val() || "";
|
|
1071
1225
|
ap.apiKey = el("#fp-api-key").val() || "";
|
|
1072
1226
|
ap.model = el("#fp-model").val() || "";
|
|
@@ -1263,10 +1417,10 @@
|
|
|
1263
1417
|
var payload = collectSettings();
|
|
1264
1418
|
var list = payload.providers || [];
|
|
1265
1419
|
|
|
1266
|
-
// Validation 1: every provider needs a base URL
|
|
1267
|
-
//
|
|
1420
|
+
// Validation 1: every non-Anthropic provider needs a base URL.
|
|
1421
|
+
// Anthropic providers default to api.anthropic.com when baseUrl is blank.
|
|
1268
1422
|
var noUrl = list.filter(function (p) {
|
|
1269
|
-
return !p.baseUrl || !String(p.baseUrl).trim();
|
|
1423
|
+
return p.type !== "anthropic" && (!p.baseUrl || !String(p.baseUrl).trim());
|
|
1270
1424
|
});
|
|
1271
1425
|
if (noUrl.length) {
|
|
1272
1426
|
var urlNames = noUrl.map(function (p) { return p.providerName || "(unnamed)"; }).join(", ");
|
|
@@ -1707,4 +1861,3 @@
|
|
|
1707
1861
|
|
|
1708
1862
|
renderChip("Open Settings", "fa fa-cog", showSettings);
|
|
1709
1863
|
}
|
|
1710
|
-
|