@manny-est/node-red-flowpilot 0.5.2 → 0.6.0
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 +85 -26
- package/README.md +10 -1
- package/USER-GUIDE.md +5 -1
- package/flowpilot-core.css +53 -10
- package/flowpilot-node-entry.js +15 -0
- package/flowpilot.js +1207 -187
- package/lib/agent-contract.js +50 -0
- package/lib/build-core-script.js +1 -0
- package/lib/chat-data.js +106 -0
- package/lib/core/apply-review.js +34 -11
- package/lib/core/graph-truth.js +63 -0
- package/lib/core/history.js +157 -6
- package/lib/core/init.js +223 -21
- package/lib/core/main.js +780 -38
- package/lib/core/modes.js +1098 -112
- package/lib/core/selection-context.js +44 -24
- package/lib/default-system-prompt.js +4 -3
- package/lib/document-system-prompt.js +7 -4
- package/lib/envelope.js +13 -7
- package/lib/generation-system-prompt.js +5 -4
- package/lib/modify-system-prompt.js +64 -12
- package/lib/persona-prompt.js +81 -54
- package/lib/prompt-fragments.js +17 -2
- package/lib/provider-anthropic.js +23 -10
- package/lib/provider-openai-compatible.js +51 -11
- package/lib/provider-shape-check.js +34 -0
- package/lib/storage.js +134 -21
- package/package.json +3 -2
package/lib/core/main.js
CHANGED
|
@@ -35,11 +35,35 @@
|
|
|
35
35
|
// Every code path that appends DOM to #fp-messages also appends a record
|
|
36
36
|
// here. refreshView() clears the message container and re-renders from
|
|
37
37
|
// records, restoring interactive elements without losing conversation.
|
|
38
|
-
// Records are in-memory only — no sessionStorage
|
|
38
|
+
// Records are in-memory only — no full sessionStorage persistence.
|
|
39
|
+
// Only the minimal interrupted-run marker is persisted separately.
|
|
39
40
|
// ---------------------------------------------------------------------
|
|
40
41
|
var messageRecords = [];
|
|
41
42
|
var _nextRecordId = 0;
|
|
42
43
|
|
|
44
|
+
// P10-D1 (ADR-001 R5): per-conversation map of opId -> already-applied
|
|
45
|
+
// WRITE tool result. opId = runId + ":" + call.id (runId minted per
|
|
46
|
+
// runAgentLoop run, modes.js). A repeat opId (duplicate delivery, a
|
|
47
|
+
// retry, or the model repeating a call) returns the SAME result without
|
|
48
|
+
// re-invoking the executor — the graph is mutated at most once per
|
|
49
|
+
// opId. Keyed first by conversationId so switching conversations
|
|
50
|
+
// (clearChat / loading a saved transcript) can't cross-contaminate;
|
|
51
|
+
// cleared for the outgoing conversationId at those same points. In-
|
|
52
|
+
// memory only, matching messageRecords/conversationHistory's existing
|
|
53
|
+
// volatility (ADR-001: "persistence is minimal and schema-ready this
|
|
54
|
+
// phase" — full state-machine persistence is deferred to phase close).
|
|
55
|
+
var appliedOpsByConversation = {};
|
|
56
|
+
|
|
57
|
+
function getAppliedOp(convId, opId) {
|
|
58
|
+
var ops = appliedOpsByConversation[convId];
|
|
59
|
+
return ops ? ops[opId] : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function recordAppliedOp(convId, opId, result) {
|
|
63
|
+
if (!appliedOpsByConversation[convId]) { appliedOpsByConversation[convId] = {}; }
|
|
64
|
+
appliedOpsByConversation[convId][opId] = result;
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
function addRecord(kind, payload) {
|
|
44
68
|
var rec = { id: _nextRecordId++, ts: Date.now(), kind: kind };
|
|
45
69
|
if (payload) {
|
|
@@ -83,6 +107,10 @@
|
|
|
83
107
|
renderLoopCheckpoint(activeBuildLoop);
|
|
84
108
|
} else if (rec.buildConsentGate) {
|
|
85
109
|
renderBuildConsentGate(rec);
|
|
110
|
+
} else if (rec.agentToolConsent) {
|
|
111
|
+
renderAgentToolConsentGate(rec);
|
|
112
|
+
} else if (rec.askUserTool) {
|
|
113
|
+
renderAskUserQuestion(rec);
|
|
86
114
|
} else {
|
|
87
115
|
renderClarifyingQuestion(rec.options || []);
|
|
88
116
|
}
|
|
@@ -223,7 +251,7 @@
|
|
|
223
251
|
// side-effecting node's own real id here (status events report the
|
|
224
252
|
// node itself, not a debug tap wired to it).
|
|
225
253
|
if ((activeBuildLoop.skipCheckpointNodeIds || []).indexOf(nodeId) !== -1) { return; }
|
|
226
|
-
if (
|
|
254
|
+
if (freshBuildLoopEvidence(activeBuildLoop).length > 0) { return; }
|
|
227
255
|
|
|
228
256
|
// Skip in-progress ("blue") statuses — e.g. http-request emits
|
|
229
257
|
// fill:"blue" text:"requesting" before any response arrives. Locking
|
|
@@ -263,6 +291,27 @@
|
|
|
263
291
|
}, BUILD_LOOP_ATTACH_DEBOUNCE_MS);
|
|
264
292
|
}
|
|
265
293
|
|
|
294
|
+
// CLAUDE-025: attachedDebugMessages is deliberately STICKY across turns
|
|
295
|
+
// (see the block comment above — "sticky, like conversationHistory,
|
|
296
|
+
// until removed or Clear Chat"), which is exactly right for ordinary
|
|
297
|
+
// chat/generate/modify context. But the /build loop's own verification
|
|
298
|
+
// step must NOT inherit that stickiness: reviewing against a debug
|
|
299
|
+
// message left over from an earlier Build attempt (or one attached
|
|
300
|
+
// manually via the popout, or a still-running unrelated flow) as if it
|
|
301
|
+
// were evidence for THIS attempt's own goal is how a later attempt can
|
|
302
|
+
// declare Touchdown without ever having deployed or checked its own
|
|
303
|
+
// output. loop.deployedAt is stamped (see init.js's "deploy" listener)
|
|
304
|
+
// the moment THIS attempt's own "apply"->"attach" transition fires, so
|
|
305
|
+
// filtering on it scopes evidence to messages that arrived at or after
|
|
306
|
+
// that specific redeploy — never null (build review only runs once the
|
|
307
|
+
// loop has reached "attach", which requires deployedAt to be set).
|
|
308
|
+
function freshBuildLoopEvidence(loop) {
|
|
309
|
+
if (!loop || typeof loop.deployedAt !== "number") { return []; }
|
|
310
|
+
return attachedDebugMessages.filter(function (m) {
|
|
311
|
+
return m.timestamp >= loop.deployedAt;
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
266
315
|
// The exact shape sent to the backend (and shown by "Preview debug") —
|
|
267
316
|
// excludes previewValue, which exists only for the debug-log list.
|
|
268
317
|
function buildDebugMessagesForSend() {
|
|
@@ -627,7 +676,21 @@
|
|
|
627
676
|
|
|
628
677
|
// ---- View switching -------------------------------------------------
|
|
629
678
|
|
|
679
|
+
// CLAUDE-024: every mode-tab switch disarms whatever Execute action
|
|
680
|
+
// (Generate/Document/Modify) or Query intent was armed before the
|
|
681
|
+
// switch. Without this, arming Modify then tabbing away and back to
|
|
682
|
+
// Chat left armedExecuteAction pointing at the stale mutating action,
|
|
683
|
+
// so a plain question typed on what LOOKS like a fresh Chat tab still
|
|
684
|
+
// dispatched through Modify/Generate/Document on Send — a real
|
|
685
|
+
// unintended-mutation bug (reproduced: an unwanted node deletion).
|
|
686
|
+
// Send must always route to whatever is actually visible.
|
|
687
|
+
function disarmForModeSwitch() {
|
|
688
|
+
disarmExecuteAction();
|
|
689
|
+
disarmQueryIntent();
|
|
690
|
+
}
|
|
691
|
+
|
|
630
692
|
function showChat() {
|
|
693
|
+
disarmForModeSwitch();
|
|
631
694
|
el("#fp-chat-panel").removeClass("fp-hidden");
|
|
632
695
|
el("#fp-settings-panel").addClass("fp-hidden");
|
|
633
696
|
el("#fp-history-panel").addClass("fp-hidden");
|
|
@@ -637,6 +700,7 @@
|
|
|
637
700
|
}
|
|
638
701
|
|
|
639
702
|
function showSettings() {
|
|
703
|
+
disarmForModeSwitch();
|
|
640
704
|
el("#fp-settings-panel").removeClass("fp-hidden");
|
|
641
705
|
el("#fp-chat-panel").addClass("fp-hidden");
|
|
642
706
|
el("#fp-history-panel").addClass("fp-hidden");
|
|
@@ -646,6 +710,7 @@
|
|
|
646
710
|
}
|
|
647
711
|
|
|
648
712
|
function showHistory() {
|
|
713
|
+
disarmForModeSwitch();
|
|
649
714
|
el("#fp-history-panel").removeClass("fp-hidden");
|
|
650
715
|
el("#fp-chat-panel").addClass("fp-hidden");
|
|
651
716
|
el("#fp-settings-panel").addClass("fp-hidden");
|
|
@@ -658,6 +723,11 @@
|
|
|
658
723
|
// Clears the visible chat AND resets the conversation history the model
|
|
659
724
|
// sees — "start a fresh conversation".
|
|
660
725
|
function clearChat() {
|
|
726
|
+
flowpilotStorageLog("log", "clearChat-enter", {
|
|
727
|
+
conversationId: conversationId,
|
|
728
|
+
messageCount: messageRecords.length,
|
|
729
|
+
href: location.href
|
|
730
|
+
});
|
|
661
731
|
el("#fp-messages").empty();
|
|
662
732
|
messageRecords = [];
|
|
663
733
|
relayClearMessagesToPopout();
|
|
@@ -665,7 +735,9 @@
|
|
|
665
735
|
attachedDebugMessages = [];
|
|
666
736
|
activeBuildLoop = null;
|
|
667
737
|
disarmExecuteAction(); // also clears pinnedSelectionIds
|
|
738
|
+
delete appliedOpsByConversation[conversationId];
|
|
668
739
|
conversationId = newConversationId();
|
|
740
|
+
clearRunMarker("clearChat");
|
|
669
741
|
fpChatSnappedToBottom = true;
|
|
670
742
|
updateSelectionStatus();
|
|
671
743
|
updateDebugStatus();
|
|
@@ -778,10 +850,21 @@
|
|
|
778
850
|
// Switches to a past conversation: rebuilds conversationHistory and the
|
|
779
851
|
// visible chat from its saved transcript, and continues using its
|
|
780
852
|
// conversationId so new turns append to the same transcript file.
|
|
781
|
-
|
|
853
|
+
// onError: optional — CLAUDE-029's page-load rehydration passes one to
|
|
854
|
+
// stay quiet (no chat error bubble) and clear a stale sessionStorage
|
|
855
|
+
// entry on 404, instead of the ajaxJson default of addMessage("error", …).
|
|
856
|
+
function loadConversation(id, onError, onLoaded, options) {
|
|
857
|
+
options = options || {};
|
|
858
|
+
flowpilotStorageLog("log", "loadConversation-start", {
|
|
859
|
+
requestedConversationId: id,
|
|
860
|
+
currentConversationId: conversationId,
|
|
861
|
+
href: location.href
|
|
862
|
+
});
|
|
782
863
|
ajaxJson("GET", "flowpilot/conversations/" + encodeURIComponent(id), null, function (data) {
|
|
864
|
+
delete appliedOpsByConversation[conversationId];
|
|
783
865
|
conversationId = id;
|
|
784
|
-
|
|
866
|
+
persistConversationId(id, "loadConversation success");
|
|
867
|
+
if (!options.preserveRunMarker) { clearRunMarker("loadConversation"); }
|
|
785
868
|
|
|
786
869
|
conversationHistory = [];
|
|
787
870
|
relayClearMessagesToPopout();
|
|
@@ -796,7 +879,8 @@
|
|
|
796
879
|
pinnedSelectionIds = null;
|
|
797
880
|
updateSelectionStatus();
|
|
798
881
|
showChat();
|
|
799
|
-
|
|
882
|
+
if (onLoaded) { onLoaded(data); }
|
|
883
|
+
}, onError);
|
|
800
884
|
}
|
|
801
885
|
|
|
802
886
|
// Recall — searches OTHER past conversations' transcripts for the
|
|
@@ -963,7 +1047,9 @@
|
|
|
963
1047
|
if (!$box.length) { return; }
|
|
964
1048
|
|
|
965
1049
|
var label = role === "user" ? "YOU" : role === "error" ? "ERROR" : "FLOWPILOT";
|
|
966
|
-
var cls = "fp-message" + (role === "user" ? " fp-user" :
|
|
1050
|
+
var cls = "fp-message" + (role === "user" ? " fp-user" :
|
|
1051
|
+
role === "error" ? " fp-error" :
|
|
1052
|
+
role === "fp-notice" ? " fp-secondary" : "");
|
|
967
1053
|
|
|
968
1054
|
var $msg = $("<div>").addClass(cls);
|
|
969
1055
|
$("<div>").addClass("fp-label").text(label).appendTo($msg);
|
|
@@ -982,17 +1068,52 @@
|
|
|
982
1068
|
// removed in both the success and error paths so it can't get stuck.
|
|
983
1069
|
// showStop adds a "Stop" button, used by the agent loop
|
|
984
1070
|
// (runAgentChat) so the user can interrupt a multi-step tool-call run.
|
|
1071
|
+
//
|
|
1072
|
+
// fpPendingStartedAt / fpPendingElapsedInterval drive the live elapsed-time
|
|
1073
|
+
// counter (.fp-typing-elapsed). Real timing data (corpus + live browser
|
|
1074
|
+
// testing, ~150+ requests) is bimodal: the overwhelming majority finish in
|
|
1075
|
+
// 2-13s, with rare genuine outliers around 73-77s and nothing observed in
|
|
1076
|
+
// between. FP_PENDING_SLOW_MS picks 25s as the "this is a slow one"
|
|
1077
|
+
// threshold — comfortably past every normal request, comfortably before
|
|
1078
|
+
// the real outliers, so the reassurance text only ever appears when it's
|
|
1079
|
+
// actually warranted.
|
|
1080
|
+
var fpPendingStartedAt = null;
|
|
1081
|
+
var fpPendingElapsedInterval = null;
|
|
1082
|
+
var FP_PENDING_SLOW_MS = 25000;
|
|
1083
|
+
|
|
1084
|
+
function fpFormatElapsed(ms) {
|
|
1085
|
+
var totalSeconds = Math.floor(ms / 1000);
|
|
1086
|
+
var text;
|
|
1087
|
+
if (totalSeconds < 60) {
|
|
1088
|
+
text = totalSeconds + "s";
|
|
1089
|
+
} else {
|
|
1090
|
+
var mins = Math.floor(totalSeconds / 60);
|
|
1091
|
+
var secs = totalSeconds % 60;
|
|
1092
|
+
text = mins + "m " + secs + "s";
|
|
1093
|
+
}
|
|
1094
|
+
if (ms >= FP_PENDING_SLOW_MS) {
|
|
1095
|
+
text += " — still working, some requests take a minute or more";
|
|
1096
|
+
}
|
|
1097
|
+
return text;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
985
1100
|
function showPending(showStop) {
|
|
986
1101
|
var $box = el("#fp-messages");
|
|
987
1102
|
if (!$box.length) { return; }
|
|
988
1103
|
// Guard against duplicates (e.g. fast double-send).
|
|
989
1104
|
$box.find("#fp-pending").remove();
|
|
1105
|
+
if (fpPendingElapsedInterval) {
|
|
1106
|
+
clearInterval(fpPendingElapsedInterval);
|
|
1107
|
+
fpPendingElapsedInterval = null;
|
|
1108
|
+
}
|
|
990
1109
|
|
|
991
1110
|
var $msg = $("<div>").addClass("fp-message").attr("id", "fp-pending");
|
|
992
1111
|
$("<div>").addClass("fp-label").text("FLOWPILOT").appendTo($msg);
|
|
993
1112
|
var $dots = $("<div>").addClass("fp-typing").attr("title", "Working…");
|
|
994
1113
|
$dots.append($("<span>"), $("<span>"), $("<span>"));
|
|
995
1114
|
$dots.append($("<span>").addClass("fp-typing-label").text("Cruising…"));
|
|
1115
|
+
var $elapsed = $("<span>").addClass("fp-typing-elapsed");
|
|
1116
|
+
$dots.append($elapsed);
|
|
996
1117
|
if (showStop) {
|
|
997
1118
|
$dots.append($("<button>")
|
|
998
1119
|
.addClass("fp-agent-stop red-ui-button red-ui-button-small")
|
|
@@ -1000,6 +1121,7 @@
|
|
|
1000
1121
|
.text("Stop")
|
|
1001
1122
|
.on("click", function () {
|
|
1002
1123
|
fpAgentStopRequested = true;
|
|
1124
|
+
if (fpCurrentAgentRequest) { fpCurrentAgentRequest.abort(); }
|
|
1003
1125
|
$(this).prop("disabled", true).text("Stopping…");
|
|
1004
1126
|
}));
|
|
1005
1127
|
}
|
|
@@ -1007,10 +1129,28 @@
|
|
|
1007
1129
|
|
|
1008
1130
|
$box.append($msg);
|
|
1009
1131
|
scrollMessagesToBottom();
|
|
1132
|
+
|
|
1133
|
+
fpPendingStartedAt = Date.now();
|
|
1134
|
+
fpPendingElapsedInterval = setInterval(function () {
|
|
1135
|
+
var $label = el("#fp-pending .fp-typing-elapsed");
|
|
1136
|
+
if (!$label.length) {
|
|
1137
|
+
// Panel closed / #fp-pending gone without hidePending firing;
|
|
1138
|
+
// stop polling instead of leaking the interval forever.
|
|
1139
|
+
clearInterval(fpPendingElapsedInterval);
|
|
1140
|
+
fpPendingElapsedInterval = null;
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
$label.text(" · " + fpFormatElapsed(Date.now() - fpPendingStartedAt));
|
|
1144
|
+
}, 1000);
|
|
1010
1145
|
}
|
|
1011
1146
|
|
|
1012
1147
|
function hidePending() {
|
|
1013
1148
|
el("#fp-messages").find("#fp-pending").remove();
|
|
1149
|
+
if (fpPendingElapsedInterval) {
|
|
1150
|
+
clearInterval(fpPendingElapsedInterval);
|
|
1151
|
+
fpPendingElapsedInterval = null;
|
|
1152
|
+
}
|
|
1153
|
+
fpPendingStartedAt = null;
|
|
1014
1154
|
}
|
|
1015
1155
|
|
|
1016
1156
|
// Updates the narration text shown in the pending indicator while the
|
|
@@ -1071,7 +1211,7 @@
|
|
|
1071
1211
|
}
|
|
1072
1212
|
|
|
1073
1213
|
function ajaxJson(method, url, payload, onSuccess, onError) {
|
|
1074
|
-
$.ajax({
|
|
1214
|
+
return $.ajax({
|
|
1075
1215
|
url: flowpilotUrl(url),
|
|
1076
1216
|
method: method,
|
|
1077
1217
|
contentType: "application/json",
|
|
@@ -1084,7 +1224,14 @@
|
|
|
1084
1224
|
},
|
|
1085
1225
|
success: onSuccess,
|
|
1086
1226
|
error: function (xhr) {
|
|
1087
|
-
|
|
1227
|
+
// Prefer .message (human-readable text) over .error (a
|
|
1228
|
+
// machine code like "provider_unconfirmed" or
|
|
1229
|
+
// "agent_strategy_unavailable") when both are present — a
|
|
1230
|
+
// plain-string-only error response (the common case,
|
|
1231
|
+
// {error:"..."} with no .message) is untouched, since
|
|
1232
|
+
// .message is simply absent there.
|
|
1233
|
+
var msg = (xhr.responseJSON && xhr.responseJSON.message) ||
|
|
1234
|
+
(xhr.responseJSON && xhr.responseJSON.error) ||
|
|
1088
1235
|
xhr.responseText || xhr.statusText || "Unknown error";
|
|
1089
1236
|
if (onError) { onError(msg, xhr); }
|
|
1090
1237
|
else { addMessage("error", msg); }
|
|
@@ -1125,7 +1272,11 @@
|
|
|
1125
1272
|
el("#fp-base-url").attr("placeholder", isAnthropic ? "Leave blank for api.anthropic.com" : "http://localhost:8080");
|
|
1126
1273
|
}
|
|
1127
1274
|
|
|
1128
|
-
// Write the form fields from a given provider profile.
|
|
1275
|
+
// Write the form fields from a given provider profile. p.apiKey is
|
|
1276
|
+
// never a real key here — the server masks it to a sentinel/"" on every
|
|
1277
|
+
// response (see maskProviderSecrets, flowpilot.js) — so leaving the
|
|
1278
|
+
// field untouched on save round-trips harmlessly (reconcileProviderSecrets,
|
|
1279
|
+
// lib/storage.js, keeps the real stored key).
|
|
1129
1280
|
function fillProviderFields(p) {
|
|
1130
1281
|
p = p || {};
|
|
1131
1282
|
el("#fp-provider-name").val(p.providerName || "");
|
|
@@ -1134,20 +1285,24 @@
|
|
|
1134
1285
|
toggleAnthropicHint(type);
|
|
1135
1286
|
el("#fp-base-url").val(p.baseUrl || "");
|
|
1136
1287
|
el("#fp-api-key").val(p.apiKey || "");
|
|
1288
|
+
el("#fp-api-key").attr("placeholder", p.hasApiKey ? "Saved — leave to keep, retype to change" : "Optional");
|
|
1137
1289
|
el("#fp-model").val(p.model || "");
|
|
1290
|
+
el("#fp-num-ctx").val(p.numCtx !== undefined ? p.numCtx : 0);
|
|
1138
1291
|
el("#fp-temperature").val(p.temperature !== undefined ? p.temperature : 0.2);
|
|
1139
1292
|
// Test provider is disabled until this provider has a model.
|
|
1140
1293
|
el("#fp-test-provider").prop("disabled", !(p.model && String(p.model).trim()));
|
|
1141
1294
|
}
|
|
1142
1295
|
|
|
1143
|
-
//
|
|
1144
|
-
//
|
|
1296
|
+
// CLAUDE-032: 5 discrete levels (was 1-10 with sparse interpolated
|
|
1297
|
+
// anchors — live testing found no discernible voice difference across
|
|
1298
|
+
// the old scale). Labels match lib/persona-prompt.js's PERSONA_LEVELS
|
|
1299
|
+
// exactly, one label per slider position, no interpolation.
|
|
1145
1300
|
function personaLabelFor(n) {
|
|
1146
1301
|
n = Number(n);
|
|
1147
1302
|
if (n <= 1) { return "Plain engineer — no aviation language at all."; }
|
|
1148
|
-
if (n
|
|
1149
|
-
if (n
|
|
1150
|
-
if (n
|
|
1303
|
+
if (n === 2) { return "Subtle co-pilot (default) — light, occasional flavor."; }
|
|
1304
|
+
if (n === 3) { return "Noticeable captain energy — a sentence or two, every time."; }
|
|
1305
|
+
if (n === 4) { return "Heavy captain energy — leans hard into the bit."; }
|
|
1151
1306
|
return "Full captain — comically over-the-top.";
|
|
1152
1307
|
}
|
|
1153
1308
|
|
|
@@ -1157,32 +1312,86 @@
|
|
|
1157
1312
|
el("#fp-persona-label").text(personaLabelFor(n));
|
|
1158
1313
|
}
|
|
1159
1314
|
|
|
1160
|
-
function
|
|
1315
|
+
function hideUpdateBanner() {
|
|
1316
|
+
el("#fp-update-banner").addClass("fp-hidden").empty();
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function showUpdateBanner(data) {
|
|
1320
|
+
var $banner = el("#fp-update-banner");
|
|
1321
|
+
if (!$banner.length) { return; }
|
|
1322
|
+
$banner.empty().removeClass("fp-hidden");
|
|
1323
|
+
$banner.append(document.createTextNode("Update available (v" + data.latestVersion + ") — see Palette Manager to install. "));
|
|
1324
|
+
$("<a>").attr("href", "#").text("Dismiss").on("click", function (ev) {
|
|
1325
|
+
ev.preventDefault();
|
|
1326
|
+
try {
|
|
1327
|
+
sessionStorage.setItem("fp-update-dismissed-" + data.latestVersion, "1");
|
|
1328
|
+
} catch (e) { /* storage unavailable */ }
|
|
1329
|
+
hideUpdateBanner();
|
|
1330
|
+
}).appendTo($banner);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function checkForFlowPilotUpdates() {
|
|
1334
|
+
if (isPopoutContext) { return; }
|
|
1335
|
+
ajaxJson("GET", "flowpilot/update-check", null, function (data) {
|
|
1336
|
+
if (!data || data.enabled === false || data.updateAvailable === false) {
|
|
1337
|
+
hideUpdateBanner();
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
try {
|
|
1341
|
+
if (sessionStorage.getItem("fp-update-dismissed-" + data.latestVersion)) {
|
|
1342
|
+
hideUpdateBanner();
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
} catch (e) { /* storage unavailable */ }
|
|
1346
|
+
showUpdateBanner(data);
|
|
1347
|
+
}, function () {
|
|
1348
|
+
hideUpdateBanner();
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function fillSettings(settings, reason) {
|
|
1161
1353
|
settings = settings || {};
|
|
1162
1354
|
currentSettings = settings;
|
|
1163
1355
|
|
|
1164
1356
|
renderProviderDropdown();
|
|
1165
1357
|
fillProviderFields(activeProvider());
|
|
1166
1358
|
|
|
1359
|
+
el("#fp-flowpilot-version").text("FlowPilot v" + (settings.flowpilotVersion || "unknown"));
|
|
1360
|
+
var systemPromptCurrent = el("#fp-system-prompt").val();
|
|
1361
|
+
var systemPromptIncoming = settings.systemPrompt || "";
|
|
1362
|
+
console.log("[FlowPilot][G3] fillSettings systemPrompt", {
|
|
1363
|
+
reason: reason || "unspecified",
|
|
1364
|
+
current: systemPromptCurrent,
|
|
1365
|
+
incoming: settings.systemPrompt,
|
|
1366
|
+
writing: systemPromptIncoming,
|
|
1367
|
+
differs: systemPromptCurrent !== systemPromptIncoming
|
|
1368
|
+
});
|
|
1167
1369
|
el("#fp-system-prompt").val(settings.systemPrompt || "");
|
|
1168
|
-
el("#fp-persona-intensity").val(settings.personaIntensity !== undefined ? settings.personaIntensity :
|
|
1370
|
+
el("#fp-persona-intensity").val(settings.personaIntensity !== undefined ? settings.personaIntensity : 2);
|
|
1169
1371
|
updatePersonaLabel();
|
|
1170
1372
|
el("#fp-warn-tokens").val(settings.contextWarnTokens || 4000);
|
|
1171
1373
|
el("#fp-high-tokens").val(settings.contextHighTokens || 8000);
|
|
1172
1374
|
el("#fp-history-max").val(settings.historyMaxExchanges !== undefined ? settings.historyMaxExchanges : 10);
|
|
1173
1375
|
el("#fp-streaming-enabled").prop("checked", !!settings.streamingEnabled);
|
|
1174
|
-
el("#fp-request-timeout").val(
|
|
1376
|
+
el("#fp-request-timeout").val(settings.requestTimeoutMs !== undefined ? (settings.requestTimeoutMs / 1000) : 180);
|
|
1377
|
+
el("#fp-check-for-updates").prop("checked", settings.checkForUpdates !== false);
|
|
1378
|
+
el("#fp-agent-turn-max-tokens").val(settings.agentTurnMaxTokens !== undefined ? settings.agentTurnMaxTokens : 4096);
|
|
1379
|
+
el("#fp-agent-loop-token-ceiling").val(settings.agentLoopTokenCeiling !== undefined ? settings.agentLoopTokenCeiling : 50000);
|
|
1175
1380
|
el("#fp-agent-loop-max-iterations").val(settings.agentLoopMaxIterations !== undefined ? settings.agentLoopMaxIterations : 5);
|
|
1176
1381
|
el("#fp-loop-hold-step").prop("checked", !!settings.loopHoldStep);
|
|
1177
1382
|
el("#fp-suppress-warnings").prop("checked", !!settings.suppressContextWarnings);
|
|
1178
1383
|
el("#fp-redaction-disabled").prop("checked", settings.redactionEnabled === false);
|
|
1384
|
+
el("#fp-debug-logging").prop("checked", !!settings.debugLogging);
|
|
1179
1385
|
|
|
1180
|
-
//
|
|
1181
|
-
//
|
|
1386
|
+
// Plain checkbox-driven dev/test warning icon visibility; this warning
|
|
1387
|
+
// no longer uses a type-to-confirm acknowledgement.
|
|
1182
1388
|
if (settings.suppressContextWarnings) {
|
|
1183
|
-
el("#fp-dev-
|
|
1389
|
+
el("#fp-dev-warning-status").addClass("fp-hidden");
|
|
1184
1390
|
} else {
|
|
1185
|
-
el("#fp-dev-
|
|
1391
|
+
el("#fp-dev-warning-status").removeClass("fp-hidden");
|
|
1392
|
+
}
|
|
1393
|
+
if (settings.checkForUpdates === false) {
|
|
1394
|
+
hideUpdateBanner();
|
|
1186
1395
|
}
|
|
1187
1396
|
|
|
1188
1397
|
// Custom intents may have changed; rebuild buttons and the editor list.
|
|
@@ -1224,21 +1433,21 @@
|
|
|
1224
1433
|
ap.baseUrl = el("#fp-base-url").val() || "";
|
|
1225
1434
|
ap.apiKey = el("#fp-api-key").val() || "";
|
|
1226
1435
|
ap.model = el("#fp-model").val() || "";
|
|
1436
|
+
ap.numCtx = Math.max(0, Number(el("#fp-num-ctx").val() || 0));
|
|
1227
1437
|
ap.temperature = Number(el("#fp-temperature").val() || 0.2);
|
|
1228
1438
|
currentSettings.providers = list;
|
|
1229
1439
|
}
|
|
1230
1440
|
|
|
1231
1441
|
function collectSettings() {
|
|
1232
|
-
//
|
|
1233
|
-
//
|
|
1234
|
-
|
|
1235
|
-
var
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
//
|
|
1239
|
-
//
|
|
1240
|
-
//
|
|
1241
|
-
// "off-able, not off-by-accident".
|
|
1442
|
+
// Plain checkbox — no confirmation phrase required (Manny's explicit
|
|
1443
|
+
// call: excessive friction for this particular warning, unlike the
|
|
1444
|
+
// redaction opt-out below which stays gated).
|
|
1445
|
+
var suppress = el("#fp-suppress-warnings").prop("checked");
|
|
1446
|
+
|
|
1447
|
+
// Same type-to-confirm gate as suppressContextWarnings used to have,
|
|
1448
|
+
// and for the same reason: the confirm box is never pre-filled from
|
|
1449
|
+
// settings, so disabling redaction stays off unless re-confirmed on
|
|
1450
|
+
// every save — "off-able, not off-by-accident".
|
|
1242
1451
|
var wantRedactionOff = el("#fp-redaction-disabled").prop("checked");
|
|
1243
1452
|
var redactionTyped = (el("#fp-redaction-confirm").val() || "").trim();
|
|
1244
1453
|
var redactionEnabled = !(wantRedactionOff && redactionTyped === "disable redaction");
|
|
@@ -1250,28 +1459,38 @@
|
|
|
1250
1459
|
if (!isFinite(historyMax) || historyMax < 0) { historyMax = 10; }
|
|
1251
1460
|
|
|
1252
1461
|
var requestTimeoutSec = Number(el("#fp-request-timeout").val());
|
|
1253
|
-
if (!isFinite(requestTimeoutSec) || requestTimeoutSec < 5) { requestTimeoutSec = 180; }
|
|
1254
1462
|
|
|
1255
1463
|
var personaIntensity = Number(el("#fp-persona-intensity").val());
|
|
1256
|
-
if (!isFinite(personaIntensity) || personaIntensity < 1 || personaIntensity >
|
|
1464
|
+
if (!isFinite(personaIntensity) || personaIntensity < 1 || personaIntensity > 5) { personaIntensity = 2; }
|
|
1257
1465
|
|
|
1258
1466
|
var agentLoopMaxIterations = Number(el("#fp-agent-loop-max-iterations").val());
|
|
1259
1467
|
if (!isFinite(agentLoopMaxIterations) || agentLoopMaxIterations < 1) { agentLoopMaxIterations = 5; }
|
|
1260
1468
|
|
|
1469
|
+
var agentTurnMaxTokens = Number(el("#fp-agent-turn-max-tokens").val());
|
|
1470
|
+
var agentLoopTokenCeiling = Number(el("#fp-agent-loop-token-ceiling").val());
|
|
1471
|
+
var systemPrompt = el("#fp-system-prompt").val();
|
|
1472
|
+
console.log("[FlowPilot][G3] saveSettings reading systemPrompt", {
|
|
1473
|
+
value: systemPrompt
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1261
1476
|
return {
|
|
1262
1477
|
providers: providersList(),
|
|
1263
1478
|
activeProviderId: currentSettings.activeProviderId,
|
|
1264
|
-
systemPrompt:
|
|
1479
|
+
systemPrompt: systemPrompt,
|
|
1265
1480
|
personaIntensity: personaIntensity,
|
|
1266
1481
|
contextWarnTokens: Number(el("#fp-warn-tokens").val() || 4000),
|
|
1267
1482
|
contextHighTokens: Number(el("#fp-high-tokens").val() || 8000),
|
|
1268
1483
|
historyMaxExchanges: historyMax,
|
|
1269
1484
|
streamingEnabled: el("#fp-streaming-enabled").prop("checked"),
|
|
1270
1485
|
requestTimeoutMs: Math.round(requestTimeoutSec * 1000),
|
|
1486
|
+
checkForUpdates: el("#fp-check-for-updates").prop("checked"),
|
|
1487
|
+
agentTurnMaxTokens: agentTurnMaxTokens,
|
|
1488
|
+
agentLoopTokenCeiling: agentLoopTokenCeiling,
|
|
1271
1489
|
agentLoopMaxIterations: agentLoopMaxIterations,
|
|
1272
1490
|
loopHoldStep: el("#fp-loop-hold-step").prop("checked"),
|
|
1273
1491
|
suppressContextWarnings: suppress,
|
|
1274
1492
|
redactionEnabled: redactionEnabled,
|
|
1493
|
+
debugLogging: el("#fp-debug-logging").prop("checked"),
|
|
1275
1494
|
customIntents: Array.isArray(currentSettings.customIntents)
|
|
1276
1495
|
? currentSettings.customIntents : []
|
|
1277
1496
|
};
|
|
@@ -1307,6 +1526,7 @@
|
|
|
1307
1526
|
baseUrl: "http://localhost:8080",
|
|
1308
1527
|
apiKey: "",
|
|
1309
1528
|
model: "",
|
|
1529
|
+
numCtx: 0,
|
|
1310
1530
|
temperature: 0.2
|
|
1311
1531
|
});
|
|
1312
1532
|
currentSettings.providers = list;
|
|
@@ -1400,7 +1620,7 @@
|
|
|
1400
1620
|
return;
|
|
1401
1621
|
}
|
|
1402
1622
|
ajaxJson("GET", "flowpilot/settings", null, function (data) {
|
|
1403
|
-
fillSettings(data);
|
|
1623
|
+
fillSettings(data, "loadSettings GET response");
|
|
1404
1624
|
maybeShowFirstRun(data);
|
|
1405
1625
|
updateSelectionStatus();
|
|
1406
1626
|
}, function (msg) {
|
|
@@ -1417,6 +1637,28 @@
|
|
|
1417
1637
|
var payload = collectSettings();
|
|
1418
1638
|
var list = payload.providers || [];
|
|
1419
1639
|
|
|
1640
|
+
if (!isFinite(payload.requestTimeoutMs) || payload.requestTimeoutMs <= 0) {
|
|
1641
|
+
var timeoutMsg = "Cannot save: request timeout must be a positive number of seconds.";
|
|
1642
|
+
addMessage("error", timeoutMsg);
|
|
1643
|
+
if (announce) { showSaveStatus(timeoutMsg, true); }
|
|
1644
|
+
showSettings();
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
if (!isFinite(payload.agentTurnMaxTokens) || payload.agentTurnMaxTokens <= 0 || Math.floor(payload.agentTurnMaxTokens) !== payload.agentTurnMaxTokens) {
|
|
1648
|
+
var tokenMsg = "Cannot save: max tokens per model response (agent step) must be a positive whole number.";
|
|
1649
|
+
addMessage("error", tokenMsg);
|
|
1650
|
+
if (announce) { showSaveStatus(tokenMsg, true); }
|
|
1651
|
+
showSettings();
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
if (!isFinite(payload.agentLoopTokenCeiling) || payload.agentLoopTokenCeiling <= 0 || Math.floor(payload.agentLoopTokenCeiling) !== payload.agentLoopTokenCeiling) {
|
|
1655
|
+
var ceilingMsg = "Cannot save: max total tokens per agent turn must be a positive whole number.";
|
|
1656
|
+
addMessage("error", ceilingMsg);
|
|
1657
|
+
if (announce) { showSaveStatus(ceilingMsg, true); }
|
|
1658
|
+
showSettings();
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1420
1662
|
// Validation 1: every non-Anthropic provider needs a base URL.
|
|
1421
1663
|
// Anthropic providers default to api.anthropic.com when baseUrl is blank.
|
|
1422
1664
|
var noUrl = list.filter(function (p) {
|
|
@@ -1470,7 +1712,7 @@
|
|
|
1470
1712
|
}
|
|
1471
1713
|
|
|
1472
1714
|
ajaxJson("POST", "flowpilot/settings", payload, function (data) {
|
|
1473
|
-
fillSettings(data);
|
|
1715
|
+
fillSettings(data, "saveSettings POST response");
|
|
1474
1716
|
addMessage("assistant", "Settings saved.");
|
|
1475
1717
|
if (announce) { showSaveStatus("Settings saved."); }
|
|
1476
1718
|
updateSelectionStatus();
|
|
@@ -1649,6 +1891,470 @@
|
|
|
1649
1891
|
return Object.assign({ selected: true }, context);
|
|
1650
1892
|
}
|
|
1651
1893
|
|
|
1894
|
+
// ---------------------------------------------------------------------
|
|
1895
|
+
// W7 — WRITE tools (§16 of FlowPilot-Phase10-Rescope-Scoping.md).
|
|
1896
|
+
//
|
|
1897
|
+
// Exact argument schemas as landed by CODEX-005 (coordinated via
|
|
1898
|
+
// mailbox, 2026-07-26 19:28/19:37 UTC — these are NOT a client-side
|
|
1899
|
+
// guess): CODEX-005 deliberately reused the EXISTING Modify-envelope
|
|
1900
|
+
// field names/shapes so the client applies WRITE tool calls through
|
|
1901
|
+
// the SAME pipeline as the ordinary (non-agentic) Modify apply path —
|
|
1902
|
+
// apply_step: { summary, changes?: [{id, set}], newNodes?: [{id,
|
|
1903
|
+
// type, ...}], newWires?: [{from, fromPort, to}] }
|
|
1904
|
+
// `changes[].set` is a sparse property patch, `newNodes`/`newWires`
|
|
1905
|
+
// are byte-identical in shape to the top-level Modify envelope's
|
|
1906
|
+
// own fields (applyInsertions already accepts them directly).
|
|
1907
|
+
// "One call is one todo item even when these arrays contain a
|
|
1908
|
+
// small bundle" (§16 point 2) — arrays may hold more than one
|
|
1909
|
+
// entry in a single call.
|
|
1910
|
+
// remove_step: { summary, nodeId }
|
|
1911
|
+
// rename_node: { summary, nodeId, name }
|
|
1912
|
+
// ask_user: { question, options? } — tier "write-safe", non-mutating,
|
|
1913
|
+
// handled entirely in the agent loop (modes.js), never reaches
|
|
1914
|
+
// here in normal flow.
|
|
1915
|
+
//
|
|
1916
|
+
// Tier ("write-gated" vs "write-safe") arrives OUT OF BAND per call as
|
|
1917
|
+
// data.toolTiers[call.id] (modes.js reads this), not by tool name — a
|
|
1918
|
+
// write-gated tier means the call is ELIGIBLE for consent-gating, not
|
|
1919
|
+
// that every call of that tool prompts (CODEX-005's own clarification):
|
|
1920
|
+
// §16 point 4 still requires classifying the ACTUAL touched node
|
|
1921
|
+
// type(s) against SAFE_NODE_TYPES before deciding to gate.
|
|
1922
|
+
//
|
|
1923
|
+
// tool_result shape (§16 point 3 — mirrors the existing verifySteps
|
|
1924
|
+
// check vocabulary, scoped strictly to this call's own touched ids):
|
|
1925
|
+
// { requested, checks: [{check, nodeId|fromId/toId, prop?, expected?,
|
|
1926
|
+
// pass}], allPass, error? }
|
|
1927
|
+
// ---------------------------------------------------------------------
|
|
1928
|
+
|
|
1929
|
+
// Mirrors flowpilot.js's SAFE_NODE_TYPES verbatim (§16 point 4: "reuse
|
|
1930
|
+
// SAFE_NODE_TYPES/classifyFlowNodes verbatim"). This is a client-side
|
|
1931
|
+
// COPY, not a shared module — lib/core/*.js has no require/import, and
|
|
1932
|
+
// the server's set lives in a separate Node.js process. Must be kept in
|
|
1933
|
+
// sync by hand; flagged in the mailbox report as a drift risk to watch
|
|
1934
|
+
// if the server-side list ever changes.
|
|
1935
|
+
var WRITE_GATE_SAFE_NODE_TYPES = new Set([
|
|
1936
|
+
"inject", "function", "change", "switch", "filter", "json", "xml", "csv",
|
|
1937
|
+
"base64", "html", "split", "join", "sort", "batch", "debug", "status",
|
|
1938
|
+
"comment", "link in", "link out", "link call", "junction"
|
|
1939
|
+
]);
|
|
1940
|
+
|
|
1941
|
+
// Collects the node type(s) a WRITE tool call would touch — a NEW
|
|
1942
|
+
// node's own declared type, or an EXISTING node's live type looked up
|
|
1943
|
+
// via findLiveNode (apply-review.js, same closure). Used by
|
|
1944
|
+
// writeToolCallNeedsConsent (modes.js) to decide whether to gate.
|
|
1945
|
+
function collectWriteToolTouchedTypes(name, args) {
|
|
1946
|
+
args = args || {};
|
|
1947
|
+
var types = [];
|
|
1948
|
+
function addLiveType(id) {
|
|
1949
|
+
if (!id) { return; }
|
|
1950
|
+
var live = findLiveNode(id);
|
|
1951
|
+
if (live) { types.push(live.type); }
|
|
1952
|
+
}
|
|
1953
|
+
switch (name) {
|
|
1954
|
+
case "apply_step":
|
|
1955
|
+
(Array.isArray(args.newNodes) ? args.newNodes : []).forEach(function (n) {
|
|
1956
|
+
if (n && n.type) { types.push(n.type); }
|
|
1957
|
+
});
|
|
1958
|
+
(Array.isArray(args.newWires) ? args.newWires : []).forEach(function (w) {
|
|
1959
|
+
if (!w) { return; }
|
|
1960
|
+
addLiveType(w.from);
|
|
1961
|
+
addLiveType(w.to);
|
|
1962
|
+
});
|
|
1963
|
+
(Array.isArray(args.changes) ? args.changes : []).forEach(function (c) {
|
|
1964
|
+
if (c) { addLiveType(c.id); }
|
|
1965
|
+
});
|
|
1966
|
+
break;
|
|
1967
|
+
case "remove_step":
|
|
1968
|
+
addLiveType(args.nodeId);
|
|
1969
|
+
break;
|
|
1970
|
+
case "rename_node":
|
|
1971
|
+
addLiveType(args.nodeId);
|
|
1972
|
+
break;
|
|
1973
|
+
}
|
|
1974
|
+
return types;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
// §16 point 4: gate ONLY when the call's tier is "write-gated" AND it
|
|
1978
|
+
// touches a node type outside SAFE_NODE_TYPES; autonomously apply
|
|
1979
|
+
// everything else (including every "write-safe" call, e.g. ask_user,
|
|
1980
|
+
// which never reaches this function at all in practice — see
|
|
1981
|
+
// handleStep). An id that doesn't resolve to a live node at all
|
|
1982
|
+
// (hallucinated id, or a brand-new node referenced by a wire before it
|
|
1983
|
+
// exists) can't be proven safe, so it's treated conservatively as
|
|
1984
|
+
// side-effecting — mirrors WS4's classifyFlowNodes default (unknown =>
|
|
1985
|
+
// sideEffecting).
|
|
1986
|
+
function writeToolCallNeedsConsent(tier, name, args) {
|
|
1987
|
+
if (tier !== "write-gated") { return false; }
|
|
1988
|
+
var types = collectWriteToolTouchedTypes(name, args);
|
|
1989
|
+
if (!types.length) { return true; }
|
|
1990
|
+
return types.some(function (t) { return !WRITE_GATE_SAFE_NODE_TYPES.has(t); });
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// Builds the tool_result envelope from a checks array already carrying
|
|
1994
|
+
// .pass — shared tail for all three WRITE tool executors below.
|
|
1995
|
+
function buildWriteToolResult(requestedArgs, checks, extra) {
|
|
1996
|
+
var allPass = checks.length > 0 && checks.every(function (c) { return c.pass; });
|
|
1997
|
+
return Object.assign({ requested: requestedArgs, checks: checks, allPass: allPass }, extra || {});
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
// Runs the verifySteps-style check vocabulary (runSingleVerifyCheck,
|
|
2001
|
+
// modes.js, same closure) against a list of steps in this call's own
|
|
2002
|
+
// shape, scoped strictly to the touched id(s) — never a flow-wide
|
|
2003
|
+
// snapshot (§16 point 3).
|
|
2004
|
+
function runChecksForToolResult(steps, idMap) {
|
|
2005
|
+
var checks = [];
|
|
2006
|
+
(steps || []).forEach(function (step) {
|
|
2007
|
+
var result = runSingleVerifyCheck(step, idMap);
|
|
2008
|
+
if (!result) { return; }
|
|
2009
|
+
checks.push(Object.assign({}, step, { pass: result.ok }));
|
|
2010
|
+
});
|
|
2011
|
+
return checks;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// apply_step: newNodes/newWires go through applyInsertions — the SAME
|
|
2015
|
+
// layout/collision-avoidance/wiring path Generate/Modify insertions
|
|
2016
|
+
// already use, since CODEX-005 matched that exact field shape — then
|
|
2017
|
+
// changes[].set (sparse property patches on EXISTING nodes) goes
|
|
2018
|
+
// through applyModifications's Tier 1, using the resulting idMap so a
|
|
2019
|
+
// change value can reference a placeholder id from newNodes in the
|
|
2020
|
+
// SAME call (mirrors the existing "insertions run first so their
|
|
2021
|
+
// placeholder→real-id map is available" ordering in
|
|
2022
|
+
// addModifyReview's apply button). `changes[].set.wires` is the ONE
|
|
2023
|
+
// exception: CLAUDE-009-fix — an existing node's wiring is never a
|
|
2024
|
+
// generic property (mirrors computeNodeDiff's `if (k === "wires") {
|
|
2025
|
+
// wiresChanged = true; return; }`), so it's split out and routed
|
|
2026
|
+
// through computeWireDiff/Tier 3 (RED.nodes.addLink/removeLink,
|
|
2027
|
+
// canWire port validation) instead of a raw `liveNode.wires = val`
|
|
2028
|
+
// assignment, which would "succeed" without ever updating the link
|
|
2029
|
+
// registry the canvas and RED.nodes.eachLink actually read from.
|
|
2030
|
+
function executeApplyStepTool(args, runIdMap, runHistoryEvents) {
|
|
2031
|
+
args = args || {};
|
|
2032
|
+
var changes = Array.isArray(args.changes) ? args.changes : [];
|
|
2033
|
+
var newNodes = Array.isArray(args.newNodes) ? args.newNodes : [];
|
|
2034
|
+
var newWires = Array.isArray(args.newWires) ? args.newWires : [];
|
|
2035
|
+
|
|
2036
|
+
var idMap = {};
|
|
2037
|
+
var steps = [];
|
|
2038
|
+
|
|
2039
|
+
// CLAUDE-013: resolves a placeholder id (e.g. "fp-new-2") through
|
|
2040
|
+
// this call's own idMap first (ids it just created via
|
|
2041
|
+
// applyInsertions), then through runIdMap (ids created by an
|
|
2042
|
+
// EARLIER WRITE-tool call in the same agent run) — so referencing a
|
|
2043
|
+
// node created two tool calls ago works the same as referencing one
|
|
2044
|
+
// created in this call.
|
|
2045
|
+
function resolveId(id) {
|
|
2046
|
+
if (typeof id !== "string") { return id; }
|
|
2047
|
+
if (idMap && idMap[id]) { return idMap[id]; }
|
|
2048
|
+
if (runIdMap && runIdMap[id]) { return runIdMap[id]; }
|
|
2049
|
+
return id;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
if (newNodes.length || newWires.length) {
|
|
2053
|
+
idMap = applyInsertions(newNodes, newWires, [], runHistoryEvents) || {};
|
|
2054
|
+
newNodes.forEach(function (n) {
|
|
2055
|
+
if (n && n.id) { steps.push({ check: "exists", nodeId: n.id }); }
|
|
2056
|
+
});
|
|
2057
|
+
newWires.forEach(function (w) {
|
|
2058
|
+
if (w && w.from && w.to) { steps.push({ check: "wire", fromId: w.from, fromPort: w.fromPort || 0, toId: w.to }); }
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
if (changes.length) {
|
|
2063
|
+
var propDiffs = changes.filter(function (c) { return c && c.id && c.set && typeof c.set === "object"; })
|
|
2064
|
+
.map(function (c) {
|
|
2065
|
+
var nodeId = resolveId(c.id);
|
|
2066
|
+
var live = findLiveNode(nodeId);
|
|
2067
|
+
var hasWireChange = Object.prototype.hasOwnProperty.call(c.set, "wires");
|
|
2068
|
+
var propertyChanges = Object.keys(c.set).filter(function (k) { return k !== "wires"; })
|
|
2069
|
+
.map(function (k) {
|
|
2070
|
+
return { key: k, oldVal: live ? live[k] : undefined, newVal: c.set[k] };
|
|
2071
|
+
});
|
|
2072
|
+
var wiresDiff = { toAdd: [], toRemove: [] };
|
|
2073
|
+
if (hasWireChange && live) {
|
|
2074
|
+
// The tool-call path has no "flow the model had in
|
|
2075
|
+
// context" boundary the way the classic envelope's
|
|
2076
|
+
// validTargetIds does (scoped to the returned "flow"
|
|
2077
|
+
// array) — the model only sends this one node's
|
|
2078
|
+
// desired wires, not a full flow. Per sr-dev's
|
|
2079
|
+
// guidance, treat every id the diff could actually
|
|
2080
|
+
// flag for removal as valid: computeWireDiff only
|
|
2081
|
+
// ever consults validTargetIds for ids already found
|
|
2082
|
+
// among this node's CURRENT live targets, so seeding
|
|
2083
|
+
// validTargetIds from those current targets is
|
|
2084
|
+
// equivalent to "always valid" without needing a
|
|
2085
|
+
// magic always-true object.
|
|
2086
|
+
var validTargetIds = {};
|
|
2087
|
+
RED.nodes.eachLink(function (l) {
|
|
2088
|
+
if (l.source && l.source.id === nodeId && l.target) { validTargetIds[l.target.id] = true; }
|
|
2089
|
+
});
|
|
2090
|
+
wiresDiff = computeWireDiff(nodeId, c.set.wires, validTargetIds);
|
|
2091
|
+
}
|
|
2092
|
+
return {
|
|
2093
|
+
modNode: { id: nodeId },
|
|
2094
|
+
propertyChanges: propertyChanges,
|
|
2095
|
+
wiresChanged: hasWireChange && !!live,
|
|
2096
|
+
wiresDiff: wiresDiff
|
|
2097
|
+
};
|
|
2098
|
+
});
|
|
2099
|
+
// CLAUDE-013: applyModifications's own Tier 1 substitution
|
|
2100
|
+
// (apply-review.js) resolves c.set[k] placeholder values through
|
|
2101
|
+
// whatever idMap it's given — passing the call-local idMap alone
|
|
2102
|
+
// would WRITE the raw unresolved placeholder string onto the live
|
|
2103
|
+
// node (e.g. an mqtt-out's "broker" left as "fp-new-broker")
|
|
2104
|
+
// whenever the referenced node was created by an EARLIER call
|
|
2105
|
+
// this run, even though the verify step below correctly expects
|
|
2106
|
+
// the real id — a live-value/verify-step mismatch caught by
|
|
2107
|
+
// testing, not named explicitly in the ticket's cited line
|
|
2108
|
+
// numbers. Merge runIdMap in so the actual mutation and the
|
|
2109
|
+
// verify step agree.
|
|
2110
|
+
if (propDiffs.length) { applyModifications(propDiffs, [], null, Object.assign({}, runIdMap, idMap), runHistoryEvents); }
|
|
2111
|
+
changes.forEach(function (c) {
|
|
2112
|
+
if (!c || !c.id || !c.set || typeof c.set !== "object") { return; }
|
|
2113
|
+
var nodeId = resolveId(c.id);
|
|
2114
|
+
Object.keys(c.set).filter(function (k) { return k !== "wires"; }).forEach(function (k) {
|
|
2115
|
+
// Resolve through idMap/runIdMap the SAME way the diff
|
|
2116
|
+
// above just did, so a set value referencing a
|
|
2117
|
+
// placeholder from THIS call's own newNodes (e.g. an
|
|
2118
|
+
// mqtt-out's "broker" pointing at a new mqtt-broker) OR
|
|
2119
|
+
// from an EARLIER call this run is checked against what
|
|
2120
|
+
// actually landed, not the raw unresolved placeholder
|
|
2121
|
+
// string.
|
|
2122
|
+
var expected = resolveId(c.set[k]);
|
|
2123
|
+
steps.push({ check: "property", nodeId: nodeId, prop: k, expected: expected });
|
|
2124
|
+
});
|
|
2125
|
+
if (Object.prototype.hasOwnProperty.call(c.set, "wires")) {
|
|
2126
|
+
// Emit one "wire" check per desired (port, target) pair —
|
|
2127
|
+
// the same check shape/granularity newWires already uses
|
|
2128
|
+
// above — so the model's requested final wiring state is
|
|
2129
|
+
// verified against the live graph, not a property key.
|
|
2130
|
+
var desiredWires = Array.isArray(c.set.wires) ? c.set.wires : [];
|
|
2131
|
+
desiredWires.forEach(function (targets, port) {
|
|
2132
|
+
(Array.isArray(targets) ? targets : []).forEach(function (targetId) {
|
|
2133
|
+
steps.push({ check: "wire", fromId: nodeId, fromPort: port, toId: resolveId(targetId) });
|
|
2134
|
+
});
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
if (!steps.length) {
|
|
2141
|
+
return buildWriteToolResult(args, [], { error: "apply_step call had nothing to apply" });
|
|
2142
|
+
}
|
|
2143
|
+
var checks = runChecksForToolResult(steps, idMap);
|
|
2144
|
+
var extra = {};
|
|
2145
|
+
if (idMap && Object.keys(idMap).length) { extra.idMap = idMap; }
|
|
2146
|
+
return buildWriteToolResult(args, checks, extra);
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
function executeRemoveStepTool(args, runIdMap, runHistoryEvents) {
|
|
2150
|
+
args = args || {};
|
|
2151
|
+
if (!args.nodeId) {
|
|
2152
|
+
return buildWriteToolResult(args, [], { error: "remove_step requires nodeId" });
|
|
2153
|
+
}
|
|
2154
|
+
// CLAUDE-013: args.nodeId may be a placeholder minted by an EARLIER
|
|
2155
|
+
// WRITE-tool call this run (e.g. "insert node, then remove it").
|
|
2156
|
+
var nodeId = (runIdMap && typeof args.nodeId === "string" && runIdMap[args.nodeId]) || args.nodeId;
|
|
2157
|
+
applyModifications([], [nodeId], null, {}, runHistoryEvents);
|
|
2158
|
+
var checks = runChecksForToolResult([{ check: "absent", nodeId: nodeId }], {});
|
|
2159
|
+
return buildWriteToolResult(args, checks);
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
function executeRenameNodeTool(args, runIdMap, runHistoryEvents) {
|
|
2163
|
+
args = args || {};
|
|
2164
|
+
if (!args.nodeId || typeof args.name !== "string") {
|
|
2165
|
+
return buildWriteToolResult(args, [], { error: "rename_node requires nodeId and name" });
|
|
2166
|
+
}
|
|
2167
|
+
// CLAUDE-013: same placeholder resolution as executeRemoveStepTool.
|
|
2168
|
+
var nodeId = (runIdMap && typeof args.nodeId === "string" && runIdMap[args.nodeId]) || args.nodeId;
|
|
2169
|
+
var live = findLiveNode(nodeId);
|
|
2170
|
+
var diff = [{
|
|
2171
|
+
modNode: { id: nodeId },
|
|
2172
|
+
propertyChanges: [{ key: "name", oldVal: live ? live.name : undefined, newVal: args.name }],
|
|
2173
|
+
wiresChanged: false,
|
|
2174
|
+
wiresDiff: { toAdd: [], toRemove: [] }
|
|
2175
|
+
}];
|
|
2176
|
+
applyModifications(diff, [], null, {}, runHistoryEvents);
|
|
2177
|
+
var checks = runChecksForToolResult([{ check: "property", nodeId: nodeId, prop: "name", expected: args.name }], {});
|
|
2178
|
+
return buildWriteToolResult(args, checks);
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
function executeRedirectModeTool(args) {
|
|
2182
|
+
args = args || {};
|
|
2183
|
+
if (["generate", "document", "chat"].indexOf(args.mode) === -1) {
|
|
2184
|
+
return { error: "redirect_mode requires mode = generate, document, or chat" };
|
|
2185
|
+
}
|
|
2186
|
+
if (typeof args.prompt !== "string" || !args.prompt.trim()) {
|
|
2187
|
+
return { error: "redirect_mode requires prompt" };
|
|
2188
|
+
}
|
|
2189
|
+
if (typeof args.explanation !== "string" || !args.explanation.trim()) {
|
|
2190
|
+
return { error: "redirect_mode requires explanation" };
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
var result = {
|
|
2194
|
+
redirected: true,
|
|
2195
|
+
explanation: args.explanation.trim(),
|
|
2196
|
+
suggestedAction: {
|
|
2197
|
+
mode: args.mode,
|
|
2198
|
+
prompt: args.prompt.trim()
|
|
2199
|
+
}
|
|
2200
|
+
};
|
|
2201
|
+
if (typeof args.selectionHint === "string" && args.selectionHint.trim()) {
|
|
2202
|
+
result.suggestedAction.selectionHint = args.selectionHint.trim();
|
|
2203
|
+
}
|
|
2204
|
+
if (args.targetNodeIds === "all" || args.targetNodeIds === "instance") {
|
|
2205
|
+
result.suggestedAction.targetNodeIds = args.targetNodeIds;
|
|
2206
|
+
} else if (Array.isArray(args.targetNodeIds)) {
|
|
2207
|
+
var ids = args.targetNodeIds
|
|
2208
|
+
.filter(function (id) { return typeof id === "string" && id.trim(); })
|
|
2209
|
+
.map(function (id) { return id.trim(); });
|
|
2210
|
+
if (ids.length) { result.suggestedAction.targetNodeIds = ids; }
|
|
2211
|
+
}
|
|
2212
|
+
return result;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// group_nodes (ADR-003 R3a / P10-B3): wraps the exact
|
|
2216
|
+
// RED.group.createGroup path already proven in apply-review.js's
|
|
2217
|
+
// applyGroupChanges create branch (~1356-1362) — no new group logic.
|
|
2218
|
+
// Pre-validates every id via findLiveNode (the established resolver
|
|
2219
|
+
// used by the other WRITE tool executors above, a superset of
|
|
2220
|
+
// RED.nodes.node that also resolves groups/junctions — needed here
|
|
2221
|
+
// specifically to detect a group id among nodeIds). "No partial
|
|
2222
|
+
// group": any missing id is an error, not a partial create. A
|
|
2223
|
+
// resolved id that's itself a group (nesting) or already belongs to a
|
|
2224
|
+
// group (would require editing that group's membership) is out of
|
|
2225
|
+
// scope for this minimal tool per ADR-003 and reported as
|
|
2226
|
+
// unsupported_operation instead of attempted.
|
|
2227
|
+
function executeGroupNodesTool(args, runIdMap, runHistoryEvents) {
|
|
2228
|
+
args = args || {};
|
|
2229
|
+
var name = typeof args.name === "string" ? args.name : "";
|
|
2230
|
+
var nodeIds = Array.isArray(args.nodeIds) ? args.nodeIds : [];
|
|
2231
|
+
if (!name || !nodeIds.length) {
|
|
2232
|
+
return buildWriteToolResult(args, [], { error: "group_nodes requires name and at least one nodeId" });
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// CLAUDE-013: this is the exact call shape that surfaced the "node
|
|
2236
|
+
// not found: fp-new-2" error — a node created by an EARLIER
|
|
2237
|
+
// apply_step call in the same run, referenced here by its
|
|
2238
|
+
// placeholder id, with no idMap of its own to resolve against.
|
|
2239
|
+
var resolvedIds = nodeIds.map(function (id) {
|
|
2240
|
+
return (runIdMap && typeof id === "string" && runIdMap[id]) || id;
|
|
2241
|
+
});
|
|
2242
|
+
var uniqueIds = resolvedIds.filter(function (id, i) { return resolvedIds.indexOf(id) === i; });
|
|
2243
|
+
var missing = [], nested = [], alreadyGrouped = [], resolved = [];
|
|
2244
|
+
uniqueIds.forEach(function (id) {
|
|
2245
|
+
var live = findLiveNode(id);
|
|
2246
|
+
if (!live) { missing.push(id); return; }
|
|
2247
|
+
if (live.type === "group") { nested.push(id); return; }
|
|
2248
|
+
if (live.g) { alreadyGrouped.push(id); return; }
|
|
2249
|
+
resolved.push(live);
|
|
2250
|
+
});
|
|
2251
|
+
|
|
2252
|
+
if (missing.length) {
|
|
2253
|
+
return buildWriteToolResult(args, [], { error: "group_nodes: node id(s) not found: " + missing.join(", ") });
|
|
2254
|
+
}
|
|
2255
|
+
if (nested.length || alreadyGrouped.length) {
|
|
2256
|
+
var reasonParts = [];
|
|
2257
|
+
if (nested.length) { reasonParts.push("already a group: " + nested.join(", ")); }
|
|
2258
|
+
if (alreadyGrouped.length) { reasonParts.push("already in another group: " + alreadyGrouped.join(", ")); }
|
|
2259
|
+
return buildWriteToolResult(args, [], {
|
|
2260
|
+
unsupported: true,
|
|
2261
|
+
operation: "group_nodes",
|
|
2262
|
+
reason: "Nested groups and existing-group membership edits aren't supported (" + reasonParts.join("; ") + ").",
|
|
2263
|
+
available: ["apply_step", "remove_step", "rename_node", "group_nodes"]
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
// CLAUDE-026: all resolved nodes must share one tab. Node-RED core's
|
|
2268
|
+
// own RED.group.createGroup (confirmed via source) registers an
|
|
2269
|
+
// EMPTY group via RED.nodes.addGroup() FIRST, using resolved[0].z,
|
|
2270
|
+
// and only THEN populates it via addToGroup — which validates every
|
|
2271
|
+
// node's .z matches and throws if not. createGroup catches that
|
|
2272
|
+
// throw itself, RED.notifies it, and returns undefined — but never
|
|
2273
|
+
// undoes the addGroup() call, so a mismatched-z request leaves a
|
|
2274
|
+
// real, empty, orphaned group on resolved[0]'s tab every time.
|
|
2275
|
+
// Checking up front avoids ever creating that orphan for this
|
|
2276
|
+
// (deterministic, reproducible-by-inspection) cause.
|
|
2277
|
+
var groupZ = resolved.length ? resolved[0].z : null;
|
|
2278
|
+
var mismatchedZ = resolved.some(function (n) { return n.z !== groupZ; });
|
|
2279
|
+
if (mismatchedZ) {
|
|
2280
|
+
return buildWriteToolResult(args, [], { error: "group_nodes: all nodes must be on the same tab to be grouped" });
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
// Belt-and-suspenders for any OTHER way createGroup can fail after
|
|
2284
|
+
// already registering that empty shell (a live QA run hit one:
|
|
2285
|
+
// "Node type not installed: group" on 1 of 3 attempts, whose exact
|
|
2286
|
+
// trigger wasn't pinned down) — snapshot the groups already on this
|
|
2287
|
+
// tab before each attempt, and if createGroup comes back falsy,
|
|
2288
|
+
// diff RED.nodes.groups(z) against the snapshot to find and remove
|
|
2289
|
+
// (via RED.group.ungroup — the same full-removal path
|
|
2290
|
+
// applyGroupChanges' disband branch already uses, confirmed safe on
|
|
2291
|
+
// a zero-member group) exactly what OUR call just orphaned, never
|
|
2292
|
+
// anything the user made themselves. One retry after cleanup in
|
|
2293
|
+
// case the underlying cause was transient.
|
|
2294
|
+
function attemptCreateGroup() {
|
|
2295
|
+
var before = {};
|
|
2296
|
+
RED.nodes.groups(groupZ).forEach(function (g) { before[g.id] = true; });
|
|
2297
|
+
var group;
|
|
2298
|
+
var caught = null;
|
|
2299
|
+
try {
|
|
2300
|
+
group = RED.group.createGroup(resolved);
|
|
2301
|
+
} catch (e) {
|
|
2302
|
+
caught = e;
|
|
2303
|
+
}
|
|
2304
|
+
if (!group) {
|
|
2305
|
+
RED.nodes.groups(groupZ).forEach(function (g) {
|
|
2306
|
+
if (!before[g.id]) { RED.group.ungroup(g); }
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
return { group: group, error: caught };
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
var attempt = attemptCreateGroup();
|
|
2313
|
+
if (!attempt.group) { attempt = attemptCreateGroup(); }
|
|
2314
|
+
if (!attempt.group) {
|
|
2315
|
+
var errMsg = (attempt.error && attempt.error.message) || attempt.error || "createGroup returned nothing";
|
|
2316
|
+
return buildWriteToolResult(args, [], { error: "Failed to create group: " + errMsg });
|
|
2317
|
+
}
|
|
2318
|
+
var newGroup = attempt.group;
|
|
2319
|
+
newGroup.name = name;
|
|
2320
|
+
RED.group.markDirty(newGroup);
|
|
2321
|
+
// CLAUDE-027: collected into this run's shared accumulator (same
|
|
2322
|
+
// mechanism as applyInsertions/applyModifications above) rather than
|
|
2323
|
+
// pushed straight to RED.history, so this run's flush can fold it
|
|
2324
|
+
// together with the rest of this run's WRITE-tool calls into ONE
|
|
2325
|
+
// undo entry via t:"multi".
|
|
2326
|
+
var createGroupHistoryEvent = { t: "createGroup", groups: [newGroup], dirty: RED.nodes.dirty() };
|
|
2327
|
+
if (runHistoryEvents) { runHistoryEvents.push(createGroupHistoryEvent); } else { RED.history.push(createGroupHistoryEvent); }
|
|
2328
|
+
// markDirty alone doesn't force the editor to recompute the new
|
|
2329
|
+
// group's visible boundary immediately (apply-review.js's
|
|
2330
|
+
// applyInsertions already redraws after every insertion for the
|
|
2331
|
+
// same reason) — without this the group stays invisible until some
|
|
2332
|
+
// unrelated user action (zoom, node move) triggers a real redraw.
|
|
2333
|
+
RED.view.redraw(true);
|
|
2334
|
+
|
|
2335
|
+
// CLAUDE-020: group_nodes succeeded silently — no chat confirmation,
|
|
2336
|
+
// unlike its sibling WRITE-tool executor (apply-review.js's
|
|
2337
|
+
// applyInsertions), which reports a "Touchdown" note on every
|
|
2338
|
+
// successful insertion. Mirror that here so a Build-plan step that
|
|
2339
|
+
// groups nodes is actually confirmed in the chat, not just visible
|
|
2340
|
+
// on the canvas.
|
|
2341
|
+
var groupedNote = "Touchdown — created group \"" + name + "\" (" +
|
|
2342
|
+
resolved.length + " node(s)). Ctrl+Z to undo.";
|
|
2343
|
+
addMessage("assistant", groupedNote);
|
|
2344
|
+
pushHistory("assistant", groupedNote);
|
|
2345
|
+
updateSelectionStatus();
|
|
2346
|
+
|
|
2347
|
+
var steps = [
|
|
2348
|
+
{ check: "exists", nodeId: newGroup.id },
|
|
2349
|
+
{ check: "property", nodeId: newGroup.id, prop: "name", expected: name }
|
|
2350
|
+
];
|
|
2351
|
+
resolved.forEach(function (n) {
|
|
2352
|
+
steps.push({ check: "property", nodeId: n.id, prop: "g", expected: newGroup.id });
|
|
2353
|
+
});
|
|
2354
|
+
var checks = runChecksForToolResult(steps, {});
|
|
2355
|
+
return buildWriteToolResult(args, checks, { groupId: newGroup.id });
|
|
2356
|
+
}
|
|
2357
|
+
|
|
1652
2358
|
// Per-step narration: a short human-readable description of what a tool
|
|
1653
2359
|
// call is about to do, shown in the pending indicator (see runAgentChat).
|
|
1654
2360
|
function describeAgentToolCall(name, args) {
|
|
@@ -1666,6 +2372,18 @@
|
|
|
1666
2372
|
return "Checking the debug log…";
|
|
1667
2373
|
case "get_selection":
|
|
1668
2374
|
return "Checking the current selection…";
|
|
2375
|
+
case "apply_step":
|
|
2376
|
+
return "Applying step" + (args.summary ? ": " + args.summary : "") + "…";
|
|
2377
|
+
case "remove_step":
|
|
2378
|
+
return "Removing node" + (args.summary ? ": " + args.summary : " " + JSON.stringify(args.nodeId || "?")) + "…";
|
|
2379
|
+
case "rename_node":
|
|
2380
|
+
return "Renaming node to " + JSON.stringify(args.name || "?") + "…";
|
|
2381
|
+
case "group_nodes":
|
|
2382
|
+
return "Creating group " + JSON.stringify(args.name || "?") + "…";
|
|
2383
|
+
case "redirect_mode":
|
|
2384
|
+
return "Redirecting to " + JSON.stringify(args.mode || "?") + " mode…";
|
|
2385
|
+
case "ask_user":
|
|
2386
|
+
return "Asking a clarifying question…";
|
|
1669
2387
|
default:
|
|
1670
2388
|
return "Running " + (name || "a tool") + "…";
|
|
1671
2389
|
}
|
|
@@ -1679,7 +2397,7 @@
|
|
|
1679
2397
|
catch (e) { return {}; }
|
|
1680
2398
|
}
|
|
1681
2399
|
|
|
1682
|
-
function executeAgentToolCall(call) {
|
|
2400
|
+
function executeAgentToolCall(call, runIdMap, runHistoryEvents) {
|
|
1683
2401
|
var name = call && call.function && call.function.name;
|
|
1684
2402
|
var args = parseToolCallArgs(call);
|
|
1685
2403
|
switch (name) {
|
|
@@ -1695,6 +2413,21 @@
|
|
|
1695
2413
|
return executeReadDebugTool(args);
|
|
1696
2414
|
case "get_selection":
|
|
1697
2415
|
return executeGetSelectionTool();
|
|
2416
|
+
case "apply_step":
|
|
2417
|
+
return executeApplyStepTool(args, runIdMap, runHistoryEvents);
|
|
2418
|
+
case "remove_step":
|
|
2419
|
+
return executeRemoveStepTool(args, runIdMap, runHistoryEvents);
|
|
2420
|
+
case "rename_node":
|
|
2421
|
+
return executeRenameNodeTool(args, runIdMap, runHistoryEvents);
|
|
2422
|
+
case "group_nodes":
|
|
2423
|
+
return executeGroupNodesTool(args, runIdMap, runHistoryEvents);
|
|
2424
|
+
case "redirect_mode":
|
|
2425
|
+
return executeRedirectModeTool(args);
|
|
2426
|
+
case "ask_user":
|
|
2427
|
+
// Non-mutating and always intercepted by the agent loop
|
|
2428
|
+
// (modes.js's handleStep) before reaching here — this is a
|
|
2429
|
+
// safe fallback only, never expected in normal operation.
|
|
2430
|
+
return { error: "ask_user must be answered via the loop's question UI, not executed directly" };
|
|
1698
2431
|
default:
|
|
1699
2432
|
return { error: "Unknown tool: " + name };
|
|
1700
2433
|
}
|
|
@@ -1774,6 +2507,9 @@
|
|
|
1774
2507
|
} else {
|
|
1775
2508
|
var warnAt = Number(currentSettings.contextWarnTokens) || 4000;
|
|
1776
2509
|
var highAt = Number(currentSettings.contextHighTokens) || 8000;
|
|
2510
|
+
var ap = activeProvider();
|
|
2511
|
+
var numCtx = ap ? Number(ap.numCtx) : 0;
|
|
2512
|
+
var hasNumCtx = isFinite(numCtx) && numCtx > 0;
|
|
1777
2513
|
|
|
1778
2514
|
var parts = [];
|
|
1779
2515
|
if (contextTokens) { parts.push("context ~" + contextTokens.toLocaleString()); }
|
|
@@ -1784,9 +2520,15 @@
|
|
|
1784
2520
|
}
|
|
1785
2521
|
var sizeText = "~" + tokens.toLocaleString() + " tokens" +
|
|
1786
2522
|
(parts.length ? " (" + parts.join(", ") + ")" : "");
|
|
2523
|
+
if (hasNumCtx) {
|
|
2524
|
+
sizeText += " — " + Math.round((tokens / numCtx) * 100) + "% of " +
|
|
2525
|
+
(ap.providerName || "this provider") + "'s " + numCtx.toLocaleString() +
|
|
2526
|
+
" context window";
|
|
2527
|
+
}
|
|
2528
|
+
var nearProviderLimit = hasNumCtx && tokens >= numCtx * 0.8;
|
|
1787
2529
|
|
|
1788
2530
|
$size.removeClass("fp-hidden fp-size-warn fp-size-high");
|
|
1789
|
-
if (tokens >= highAt) {
|
|
2531
|
+
if (tokens >= highAt || nearProviderLimit) {
|
|
1790
2532
|
$size.text(sizeText + " — large; may exceed smaller local models. " +
|
|
1791
2533
|
"Consider selecting fewer nodes, clearing chat history, or splitting your request.")
|
|
1792
2534
|
.addClass("fp-size-high");
|