@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/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 () {
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-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; }
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: "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());
@@ -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
- el("#fp-test-provider").prop("disabled", !($(this).val() || "").trim());
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 === "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);
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 (Array.isArray(ad.newGroups) && ad.newGroups.length) {
1417
- applyGroupChanges(ad.newGroups, idMap);
1418
- }
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);
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.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,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 { id: m.id, timestamp: m.timestamp, name: m.name, topic: m.topic, value: m.value };
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 (the one field a
1267
- // provider cannot function without).
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
-