@manny-est/node-red-flowpilot 0.5.2 → 0.6.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -74
- package/USER-GUIDE.md +5 -1
- package/flowpilot-core.css +38 -10
- package/flowpilot.js +1007 -181
- package/lib/agent-contract.js +45 -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 +10 -1
- package/lib/core/init.js +138 -5
- package/lib/core/main.js +664 -16
- package/lib/core/modes.js +882 -100
- package/lib/core/selection-context.js +27 -24
- package/lib/default-system-prompt.js +4 -3
- package/lib/document-system-prompt.js +3 -2
- 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 +13 -2
- package/lib/provider-anthropic.js +11 -8
- package/lib/provider-openai-compatible.js +37 -9
- package/lib/provider-shape-check.js +34 -0
- package/lib/storage.js +128 -21
- package/package.json +1 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------
|
|
4
|
+
// The agent-strategy contract: a "strategy":"agent" turn is only ever
|
|
5
|
+
// allowed to mutate the flow via a WRITE tool call, never via the classic
|
|
6
|
+
// JSON-envelope mutation fields (changes/newNodes/newWires/removeNodes/
|
|
7
|
+
// newGroups). If a model still emits those fields on a turn that made no
|
|
8
|
+
// tool calls, strip them before the response reaches the client and log
|
|
9
|
+
// what was stripped — the two mutation code paths (classic envelope vs.
|
|
10
|
+
// agentic WRITE tools) must stay mutually exclusive per agent turn.
|
|
11
|
+
// Classic-strategy turns and turns that DID make tool calls are untouched
|
|
12
|
+
// by design (guard clause below) — this only fires on the one contract-
|
|
13
|
+
// violating shape it exists to catch.
|
|
14
|
+
// ---------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
const AGENT_MUTATION_FIELDS = ["changes", "newNodes", "newWires", "removeNodes", "newGroups"];
|
|
17
|
+
|
|
18
|
+
function enforceAgentContract(result, execution, hasToolCalls) {
|
|
19
|
+
if (!result || !execution || execution.strategy !== "agent" || hasToolCalls) {
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const strippedFields = [];
|
|
24
|
+
const counts = {};
|
|
25
|
+
AGENT_MUTATION_FIELDS.forEach(function (field) {
|
|
26
|
+
if (!Object.prototype.hasOwnProperty.call(result, field)) { return; }
|
|
27
|
+
strippedFields.push(field);
|
|
28
|
+
counts[field] = Array.isArray(result[field]) ? result[field].length : 1;
|
|
29
|
+
delete result[field];
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (strippedFields.length) {
|
|
33
|
+
result.strippedFields = strippedFields;
|
|
34
|
+
console.warn(
|
|
35
|
+
"[FlowPilot] agent contract stripped mutation fields strategy=%s entry=%s conversationId=%s counts=%s",
|
|
36
|
+
execution.strategy,
|
|
37
|
+
execution.entry,
|
|
38
|
+
execution.conversationId || "none",
|
|
39
|
+
JSON.stringify(counts)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { enforceAgentContract, AGENT_MUTATION_FIELDS };
|
package/lib/build-core-script.js
CHANGED
package/lib/chat-data.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { findMatchingBrace } = require("./envelope");
|
|
4
|
+
|
|
5
|
+
const CHAT_DATA_MARKER = "<<<FLOWPILOT_DATA>>>";
|
|
6
|
+
const CHAT_DATA_MARKER_RE = /<<<\s{0,8}FLOWPILOT_DATA\s{0,8}>>>/i;
|
|
7
|
+
const CHAT_DATA_MARKER_MAX_SCAN = 40;
|
|
8
|
+
|
|
9
|
+
function stripTrailingDataFence(text) {
|
|
10
|
+
return String(text || "")
|
|
11
|
+
.replace(/(?:\r?\n)?```(?:json)?[ \t]*(?:\r?\n)?$/i, "")
|
|
12
|
+
.replace(/\s+$/, "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function stripLeadingDataFence(text) {
|
|
16
|
+
return String(text || "")
|
|
17
|
+
.replace(/^\s*```(?:json)?[ \t]*\r?\n?/i, "")
|
|
18
|
+
.replace(/\s*```[\s\r\n]*$/i, "")
|
|
19
|
+
.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function findChatDataMarker(text) {
|
|
23
|
+
const source = String(text || "");
|
|
24
|
+
const match = CHAT_DATA_MARKER_RE.exec(source);
|
|
25
|
+
if (!match) { return null; }
|
|
26
|
+
return {
|
|
27
|
+
index: match.index,
|
|
28
|
+
marker: match[0]
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseChatDataObject(text) {
|
|
33
|
+
const source = stripLeadingDataFence(text);
|
|
34
|
+
const firstBrace = source.indexOf("{");
|
|
35
|
+
if (firstBrace === -1) { return null; }
|
|
36
|
+
const end = findMatchingBrace(source, firstBrace);
|
|
37
|
+
if (end === -1) { return null; }
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(source.slice(firstBrace, end + 1));
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function stripChatDataFromVisibleText(content) {
|
|
46
|
+
const text = String(content || "");
|
|
47
|
+
const found = findChatDataMarker(text);
|
|
48
|
+
if (!found) { return text; }
|
|
49
|
+
return stripTrailingDataFence(text.slice(0, found.index));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function splitChatDataBlock(content) {
|
|
53
|
+
const text = String(content || "");
|
|
54
|
+
const found = findChatDataMarker(text);
|
|
55
|
+
if (!found) { return { message: text, data: null }; }
|
|
56
|
+
|
|
57
|
+
const message = stripTrailingDataFence(text.slice(0, found.index));
|
|
58
|
+
const data = parseChatDataObject(text.slice(found.index + found.marker.length));
|
|
59
|
+
return { message: message, data: data };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createChatDataStreamSplitter() {
|
|
63
|
+
let held = "";
|
|
64
|
+
let inData = false;
|
|
65
|
+
let dataBuf = "";
|
|
66
|
+
|
|
67
|
+
function push(delta) {
|
|
68
|
+
if (inData) { dataBuf += delta; return ""; }
|
|
69
|
+
|
|
70
|
+
const combined = held + String(delta || "");
|
|
71
|
+
const found = findChatDataMarker(combined);
|
|
72
|
+
if (found) {
|
|
73
|
+
inData = true;
|
|
74
|
+
dataBuf = combined.slice(found.index + found.marker.length);
|
|
75
|
+
held = "";
|
|
76
|
+
return stripTrailingDataFence(combined.slice(0, found.index));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (combined.length <= CHAT_DATA_MARKER_MAX_SCAN) {
|
|
80
|
+
held = combined;
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
held = combined.slice(-CHAT_DATA_MARKER_MAX_SCAN);
|
|
84
|
+
return combined.slice(0, -CHAT_DATA_MARKER_MAX_SCAN);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function finish() {
|
|
88
|
+
const tail = inData ? "" : held;
|
|
89
|
+
held = "";
|
|
90
|
+
const data = inData ? parseChatDataObject(dataBuf) : null;
|
|
91
|
+
return {
|
|
92
|
+
tail: inData ? "" : stripChatDataFromVisibleText(tail),
|
|
93
|
+
data: data
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { push: push, finish: finish };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
CHAT_DATA_MARKER,
|
|
102
|
+
createChatDataStreamSplitter,
|
|
103
|
+
findChatDataMarker,
|
|
104
|
+
splitChatDataBlock,
|
|
105
|
+
stripChatDataFromVisibleText
|
|
106
|
+
};
|
package/lib/core/apply-review.js
CHANGED
|
@@ -126,7 +126,18 @@
|
|
|
126
126
|
// existing nodes. Uses RED.nodes.add (the same path Node-RED's undo uses
|
|
127
127
|
// internally for t:"add" events) and pushes one compound history entry so
|
|
128
128
|
// a single Ctrl+Z removes both the new nodes and their wires together.
|
|
129
|
-
|
|
129
|
+
//
|
|
130
|
+
// historyEvents (optional, CLAUDE-027): when the caller passes a shared
|
|
131
|
+
// array here (an agentic run's own accumulator — see runAgentLoop in
|
|
132
|
+
// modes.js), the compound event below is collected into it INSTEAD of
|
|
133
|
+
// being pushed to RED.history directly, so the run's own flush can fold
|
|
134
|
+
// it together with every other WRITE-tool call this same run made into
|
|
135
|
+
// ONE RED.history entry (RED.history's native t:"multi" wrapper —
|
|
136
|
+
// confirmed via @node-red/editor-client's red.js, e.g. its
|
|
137
|
+
// deleteSelection()). Omitted (undefined), this behaves exactly as
|
|
138
|
+
// before — every other caller (classic Modify/Generate apply, the build
|
|
139
|
+
// loop) still gets its own immediate, standalone push.
|
|
140
|
+
function applyInsertions(newNodes, newWires, contextNodeIds, historyEvents) {
|
|
130
141
|
if ((!newNodes || !newNodes.length) && (!newWires || !newWires.length)) { return; }
|
|
131
142
|
|
|
132
143
|
// Determine z (flow-tab id) from the active workspace.
|
|
@@ -537,7 +548,7 @@
|
|
|
537
548
|
// NB: for t:"add", ev.nodes must be an array of ID STRINGS — NR's undo
|
|
538
549
|
// does RED.nodes.node(ev.nodes[i]) then reads .z, which throws (and
|
|
539
550
|
// silently breaks Ctrl+Z) if given node objects instead of ids.
|
|
540
|
-
|
|
551
|
+
var insertHistoryEvent = {
|
|
541
552
|
t: "add",
|
|
542
553
|
nodes: addedNodes.map(function (n) { return n.id; }),
|
|
543
554
|
links: addedLinks,
|
|
@@ -547,7 +558,8 @@
|
|
|
547
558
|
subflowInputs: [],
|
|
548
559
|
subflowOutputs: [],
|
|
549
560
|
dirty: RED.nodes.dirty()
|
|
550
|
-
}
|
|
561
|
+
};
|
|
562
|
+
if (historyEvents) { historyEvents.push(insertHistoryEvent); } else { RED.history.push(insertHistoryEvent); }
|
|
551
563
|
|
|
552
564
|
RED.nodes.dirty(true);
|
|
553
565
|
RED.view.redraw(true);
|
|
@@ -1044,8 +1056,16 @@
|
|
|
1044
1056
|
// Tier 1 — property changes: mutate live node + {t:"edit"} history entry
|
|
1045
1057
|
// Tier 3 — wire changes: removeLink/addLink + {t:"add", removedLinks} entry
|
|
1046
1058
|
// Tier 4 — node removals: collect links, removeLink, remove node + {t:"delete"} entry
|
|
1047
|
-
// One history entry per node per type so Ctrl+Z steps back through them cleanly
|
|
1048
|
-
|
|
1059
|
+
// One history entry per node per type so Ctrl+Z steps back through them cleanly
|
|
1060
|
+
// — UNLESS historyEvents (optional, CLAUDE-027) is passed: an agentic run's
|
|
1061
|
+
// shared accumulator, into which every event below is collected instead of
|
|
1062
|
+
// pushed immediately, so the run's own flush (runAgentLoop, modes.js) can
|
|
1063
|
+
// fold ALL of this run's WRITE-tool calls together into ONE RED.history
|
|
1064
|
+
// entry via t:"multi" — see applyInsertions' historyEvents doc above for
|
|
1065
|
+
// the same mechanism. Every other caller (classic Modify/Generate apply,
|
|
1066
|
+
// build loop) omits it and keeps this function's original per-node/per-
|
|
1067
|
+
// type granularity unchanged.
|
|
1068
|
+
function applyModifications(nodeDiffs, removeNodes, $applyBtn, idMap, historyEvents) {
|
|
1049
1069
|
idMap = idMap || {};
|
|
1050
1070
|
var propApplied = 0;
|
|
1051
1071
|
var wireNodesApplied = 0;
|
|
@@ -1090,12 +1110,13 @@
|
|
|
1090
1110
|
|
|
1091
1111
|
liveNode.changed = true;
|
|
1092
1112
|
liveNode.dirty = true;
|
|
1093
|
-
|
|
1113
|
+
var editHistoryEvent = {
|
|
1094
1114
|
t: "edit",
|
|
1095
1115
|
node: liveNode,
|
|
1096
1116
|
changes: oldValues,
|
|
1097
1117
|
dirty: RED.nodes.dirty()
|
|
1098
|
-
}
|
|
1118
|
+
};
|
|
1119
|
+
if (historyEvents) { historyEvents.push(editHistoryEvent); } else { RED.history.push(editHistoryEvent); }
|
|
1099
1120
|
propApplied++;
|
|
1100
1121
|
});
|
|
1101
1122
|
|
|
@@ -1151,12 +1172,13 @@
|
|
|
1151
1172
|
});
|
|
1152
1173
|
|
|
1153
1174
|
if (removedLinks.length || addedLinks.length) {
|
|
1154
|
-
|
|
1175
|
+
var wireHistoryEvent = {
|
|
1155
1176
|
t: "add",
|
|
1156
1177
|
links: addedLinks,
|
|
1157
1178
|
removedLinks: removedLinks,
|
|
1158
1179
|
dirty: RED.nodes.dirty()
|
|
1159
|
-
}
|
|
1180
|
+
};
|
|
1181
|
+
if (historyEvents) { historyEvents.push(wireHistoryEvent); } else { RED.history.push(wireHistoryEvent); }
|
|
1160
1182
|
wireNodesApplied++;
|
|
1161
1183
|
}
|
|
1162
1184
|
});
|
|
@@ -1216,7 +1238,7 @@
|
|
|
1216
1238
|
}
|
|
1217
1239
|
}
|
|
1218
1240
|
|
|
1219
|
-
|
|
1241
|
+
var removeHistoryEvent = {
|
|
1220
1242
|
t: "delete",
|
|
1221
1243
|
nodes: isJunction ? [] : [liveNode],
|
|
1222
1244
|
links: connectedLinks,
|
|
@@ -1226,7 +1248,8 @@
|
|
|
1226
1248
|
subflowInputs: [],
|
|
1227
1249
|
subflowOutputs: [],
|
|
1228
1250
|
dirty: RED.nodes.dirty()
|
|
1229
|
-
}
|
|
1251
|
+
};
|
|
1252
|
+
if (historyEvents) { historyEvents.push(removeHistoryEvent); } else { RED.history.push(removeHistoryEvent); }
|
|
1230
1253
|
nodesRemoved++;
|
|
1231
1254
|
});
|
|
1232
1255
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// P10-E (ADR-005): the single implementation of graph truth. Every
|
|
2
|
+
// verification consumer — runSingleVerifyCheck's per-check-type
|
|
3
|
+
// dispatch (modes.js), the WRITE-tool tool_result checks
|
|
4
|
+
// (runChecksForToolResult, main.js), verifyImportedNodes (modes.js),
|
|
5
|
+
// and group_nodes's post-check (main.js) — delegates here instead of
|
|
6
|
+
// re-reading RED.nodes/RED.nodes.eachLink locally. Wires are read
|
|
7
|
+
// exclusively via RED.nodes.eachLink, never node.wires: addLink/
|
|
8
|
+
// removeLink never re-sync a live node's own .wires array mid-session
|
|
9
|
+
// (CLAUDE-010 — the drift this module exists to prevent from
|
|
10
|
+
// recurring). Diff computation (apply-review.js's computeWireDiff and
|
|
11
|
+
// its own eachLink scans) is a different concern — "what changed" vs.
|
|
12
|
+
// "is this true right now" — and stays out of scope per ADR-005.
|
|
13
|
+
|
|
14
|
+
function nodeExists(id) {
|
|
15
|
+
return !!RED.nodes.node(id);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function nodeAbsent(id) {
|
|
19
|
+
return !RED.nodes.node(id);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function propertyEquals(id, key, want) {
|
|
23
|
+
var node = RED.nodes.node(id);
|
|
24
|
+
return !!node && node[key] === want;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Read-only counterpart to propertyEquals, for diagnostics that need to
|
|
28
|
+
// show the actual current value alongside the expected one (CLAUDE-018)
|
|
29
|
+
// rather than just a boolean match. Returns { exists, value } — value is
|
|
30
|
+
// undefined when the node doesn't exist so callers can tell "no node"
|
|
31
|
+
// apart from "property is actually undefined".
|
|
32
|
+
function readProperty(id, key) {
|
|
33
|
+
var node = RED.nodes.node(id);
|
|
34
|
+
return { exists: !!node, value: node ? node[key] : undefined };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function wireExists(fromId, port, toId) {
|
|
38
|
+
var found = false;
|
|
39
|
+
RED.nodes.eachLink(function (l) {
|
|
40
|
+
if (found) { return; }
|
|
41
|
+
if (l.source && l.source.id === fromId &&
|
|
42
|
+
(l.sourcePort || 0) === (port || 0) &&
|
|
43
|
+
l.target && l.target.id === toId) {
|
|
44
|
+
found = true;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function wireAbsent(fromId, port, toId) {
|
|
51
|
+
return !wireExists(fromId, port, toId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ids: a single node id or an array of ids — true only if every one of
|
|
55
|
+
// them currently belongs to groupId (per its live .g ref).
|
|
56
|
+
function groupContains(groupId, ids) {
|
|
57
|
+
var list = Array.isArray(ids) ? ids : [ids];
|
|
58
|
+
if (!list.length) { return false; }
|
|
59
|
+
return list.every(function (id) {
|
|
60
|
+
var node = RED.nodes.node(id);
|
|
61
|
+
return !!node && node.g === groupId;
|
|
62
|
+
});
|
|
63
|
+
}
|
package/lib/core/history.js
CHANGED
|
@@ -17,10 +17,19 @@
|
|
|
17
17
|
return id;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// CLAUDE-029: whether conversationId above came from an existing
|
|
21
|
+
// sessionStorage entry (a page reload continuing a prior conversation)
|
|
22
|
+
// rather than being freshly minted — drives whether page init rehydrates
|
|
23
|
+
// the Chat panel from the server. See rehydrateConversationOnLoad().
|
|
24
|
+
var conversationIdWasRestored = false;
|
|
25
|
+
|
|
20
26
|
var conversationId = (function () {
|
|
21
27
|
try {
|
|
22
28
|
var existing = sessionStorage.getItem("fp-conversation-id");
|
|
23
|
-
if (existing) {
|
|
29
|
+
if (existing) {
|
|
30
|
+
conversationIdWasRestored = true;
|
|
31
|
+
return existing;
|
|
32
|
+
}
|
|
24
33
|
} catch (e) { /* storage unavailable */ }
|
|
25
34
|
return newConversationId();
|
|
26
35
|
})();
|
package/lib/core/init.js
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
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
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
|
|
27
|
+
"- `/refresh` — re-render all messages from the in-memory record store (restores interactive Apply buttons if they were lost).\n" +
|
|
28
|
+
"- `/debug` — toggle debug logging (full prompts/replies/decisions to `flowpilot/debug.log`) on/off. Instant, no AI involved.\n\n" +
|
|
28
29
|
"### Also worth knowing\n\n" +
|
|
29
30
|
"- Action chips (paper-plane buttons) offer a one-click follow-up — review and send, nothing fires automatically.\n" +
|
|
30
31
|
"- 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" +
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
{ cmd: "/disable", desc: "Disable selected nodes" },
|
|
65
66
|
{ cmd: "/enable", desc: "Enable selected nodes" },
|
|
66
67
|
{ cmd: "/refresh", desc: "Re-render all messages from shadow record store" },
|
|
68
|
+
{ cmd: "/debug", desc: "Toggle debug logging on/off" },
|
|
67
69
|
{ cmd: "/demo", desc: "Type in a demo prompt" },
|
|
68
70
|
{ cmd: "/help", desc: "Show all available commands" },
|
|
69
71
|
{ cmd: "/feedback", desc: "Show feedback info" }
|
|
@@ -237,6 +239,17 @@
|
|
|
237
239
|
refreshView();
|
|
238
240
|
if ($promptBox.length) { $promptBox.val(""); }
|
|
239
241
|
break;
|
|
242
|
+
// Deterministic, no AI round-trip: flips the same
|
|
243
|
+
// settings.debugLogging field the Hangar checkbox binds to, via
|
|
244
|
+
// the normal saveSettings() round trip, so the two never drift.
|
|
245
|
+
case "/debug":
|
|
246
|
+
var newDebugState = !el("#fp-debug-logging").prop("checked");
|
|
247
|
+
el("#fp-debug-logging").prop("checked", newDebugState);
|
|
248
|
+
saveSettings(function () {
|
|
249
|
+
addMessage("assistant", "Debug logging is now " + (newDebugState ? "ON" : "OFF") + ".");
|
|
250
|
+
});
|
|
251
|
+
if ($promptBox.length) { $promptBox.val(""); }
|
|
252
|
+
break;
|
|
240
253
|
// Deterministic, no LLM round-trip: just invokes Node-RED's own
|
|
241
254
|
// native "show/hide selected node labels" action (RED.actions
|
|
242
255
|
// "core:show-selected-node-labels" / "core:hide-selected-node-
|
|
@@ -567,7 +580,7 @@
|
|
|
567
580
|
$panel.data("fp-review-apply-bound", true);
|
|
568
581
|
var recordId = parseInt($panel.attr("data-fp-record-id"), 10);
|
|
569
582
|
if (isNaN(recordId)) { return; }
|
|
570
|
-
$panel.find(".fp-review-actions button.
|
|
583
|
+
$panel.find(".fp-review-actions button.fp-chip.fp-chip-card:not(:disabled)").on("click", function () {
|
|
571
584
|
var $btn = $(this);
|
|
572
585
|
if ($btn.prop("disabled")) { return; }
|
|
573
586
|
$btn.prop("disabled", true).text("Applying…");
|
|
@@ -579,6 +592,50 @@
|
|
|
579
592
|
});
|
|
580
593
|
}
|
|
581
594
|
|
|
595
|
+
// Interactive question/consent buttons are cloned into the pop-out as
|
|
596
|
+
// HTML, so their main-window click closures do not survive. Relay the
|
|
597
|
+
// record id plus the selected action back to the parent, where the live
|
|
598
|
+
// record still owns the continuation callback/state.
|
|
599
|
+
function bindRecordActionButtons($scope) {
|
|
600
|
+
$scope.filter("[data-fp-record-id]").add($scope.find("[data-fp-record-id]"))
|
|
601
|
+
.filter("button").each(function () {
|
|
602
|
+
var $btn = $(this);
|
|
603
|
+
if ($btn.data("fp-record-action-bound") || $btn.prop("disabled")) { return; }
|
|
604
|
+
var recordId = parseInt($btn.attr("data-fp-record-id"), 10);
|
|
605
|
+
var action = $btn.attr("data-fp-record-action");
|
|
606
|
+
if (isNaN(recordId) || !action) { return; }
|
|
607
|
+
$btn.data("fp-record-action-bound", true).on("click", function () {
|
|
608
|
+
if (action === "show-other") {
|
|
609
|
+
$btn.closest(".fp-question-row").next(".fp-question-other-row")
|
|
610
|
+
.removeClass("fp-hidden").find("input").focus();
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
var relayAction = action;
|
|
614
|
+
var value = $btn.attr("data-fp-record-value") || "";
|
|
615
|
+
if (action === "answer-other") {
|
|
616
|
+
value = $btn.siblings("input").val().trim();
|
|
617
|
+
if (!value) { return; }
|
|
618
|
+
relayAction = "answer";
|
|
619
|
+
}
|
|
620
|
+
var $questionRow = $btn.closest(".fp-question-row");
|
|
621
|
+
var $otherRow = $btn.closest(".fp-question-other-row");
|
|
622
|
+
$questionRow.find("button, input").prop("disabled", true);
|
|
623
|
+
$questionRow.next(".fp-question-other-row").find("button, input").prop("disabled", true);
|
|
624
|
+
$otherRow.find("button, input").prop("disabled", true);
|
|
625
|
+
$otherRow.prev(".fp-question-row").find("button, input").prop("disabled", true);
|
|
626
|
+
if (!window.opener || window.opener.closed) { return; }
|
|
627
|
+
try {
|
|
628
|
+
window.opener.postMessage({
|
|
629
|
+
event: "resolveRecordAction",
|
|
630
|
+
recordId: recordId,
|
|
631
|
+
action: relayAction,
|
|
632
|
+
value: value
|
|
633
|
+
}, location.origin);
|
|
634
|
+
} catch (e) { /* ignore */ }
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
582
639
|
// The loop stepper's "Stop build loop" button is relayed the same
|
|
583
640
|
// generic way as any other chat message (renderLoopStepper appends/
|
|
584
641
|
// replaces a #fp-loop-stepper element, which the MutationObserver
|
|
@@ -830,6 +887,7 @@
|
|
|
830
887
|
if (data.event === "initialSync") {
|
|
831
888
|
el("#fp-messages").html(data.html);
|
|
832
889
|
bindReviewApplyButtons(el("#fp-messages"));
|
|
890
|
+
bindRecordActionButtons(el("#fp-messages"));
|
|
833
891
|
bindStopLoopButton(el("#fp-messages"));
|
|
834
892
|
bindTabSwitching(el("#fp-messages"));
|
|
835
893
|
bindDebugAttachButtons(el("#fp-messages"));
|
|
@@ -837,6 +895,7 @@
|
|
|
837
895
|
} else if (data.event === "appendMessage") {
|
|
838
896
|
el("#fp-messages").append(data.html);
|
|
839
897
|
bindReviewApplyButtons(el("#fp-messages").children().last());
|
|
898
|
+
bindRecordActionButtons(el("#fp-messages").children().last());
|
|
840
899
|
bindStopLoopButton(el("#fp-messages").children().last());
|
|
841
900
|
bindTabSwitching(el("#fp-messages").children().last());
|
|
842
901
|
bindDebugAttachButtons(el("#fp-messages").children().last());
|
|
@@ -1034,8 +1093,8 @@
|
|
|
1034
1093
|
' </div>' +
|
|
1035
1094
|
|
|
1036
1095
|
' <div class="fp-settings-section">Personality</div>' +
|
|
1037
|
-
' <label>Persona intensity: <span id="fp-persona-value">
|
|
1038
|
-
' <input id="fp-persona-intensity" type="range" min="1" max="
|
|
1096
|
+
' <label>Persona intensity: <span id="fp-persona-value">2</span>/5</label>' +
|
|
1097
|
+
' <input id="fp-persona-intensity" type="range" min="1" max="5" step="1">' +
|
|
1039
1098
|
' <div id="fp-persona-label" class="fp-consent-hint"></div>' +
|
|
1040
1099
|
' <div class="fp-consent-hint">Chat only. Scales the AI\'s voice at greetings, ' +
|
|
1041
1100
|
' capability questions, and brief transitions — 1 is a plain Node-RED engineer, ' +
|
|
@@ -1062,10 +1121,14 @@
|
|
|
1062
1121
|
|
|
1063
1122
|
' <div class="fp-settings-section">Request timeout</div>' +
|
|
1064
1123
|
' <label>Give up after (seconds)</label>' +
|
|
1065
|
-
' <input id="fp-request-timeout" type="number"
|
|
1124
|
+
' <input id="fp-request-timeout" type="number" step="any" placeholder="180">' +
|
|
1066
1125
|
' <div class="fp-consent-hint">How long to wait for a provider response before ' +
|
|
1067
1126
|
' giving up. Raise this if you\'re running a large local model on slow hardware ' +
|
|
1068
1127
|
' (e.g. Ollama without a GPU) and seeing timeout errors.</div>' +
|
|
1128
|
+
' <label>Max tokens per agent turn</label>' +
|
|
1129
|
+
' <input id="fp-agent-turn-max-tokens" type="number" min="1" max="65536" step="1" placeholder="4096">' +
|
|
1130
|
+
' <div class="fp-consent-hint">Hard output limit for each tool-calling agent step. ' +
|
|
1131
|
+
' Classic (no-tools) requests are not capped.</div>' +
|
|
1069
1132
|
|
|
1070
1133
|
' <div class="fp-settings-section">Agentic build loop</div>' +
|
|
1071
1134
|
' <label>Max build/fix attempts</label>' +
|
|
@@ -1125,6 +1188,15 @@
|
|
|
1125
1188
|
' sent either way. To disable, check the box and type ' +
|
|
1126
1189
|
' <strong>disable redaction</strong> below.</div>' +
|
|
1127
1190
|
' <input id="fp-redaction-confirm" type="text" placeholder="Type: disable redaction">' +
|
|
1191
|
+
|
|
1192
|
+
' <div class="fp-settings-section">Debug mode</div>' +
|
|
1193
|
+
' <label class="fp-checkbox-row">' +
|
|
1194
|
+
' <input id="fp-debug-logging" type="checkbox"> ' +
|
|
1195
|
+
' Debug mode' +
|
|
1196
|
+
' </label>' +
|
|
1197
|
+
' <div class="fp-consent-hint">Logs full prompts, replies, and decisions to ' +
|
|
1198
|
+
' <code>flowpilot/debug.log</code> for troubleshooting. Off by default — ' +
|
|
1199
|
+
' this file can get large.</div>' +
|
|
1128
1200
|
' </details>' +
|
|
1129
1201
|
|
|
1130
1202
|
' <div class="fp-settings-actions">' +
|
|
@@ -1287,6 +1359,13 @@
|
|
|
1287
1359
|
RED.events.on("deploy", function () {
|
|
1288
1360
|
if (activeBuildLoop && activeBuildLoop.waypoint === "apply") {
|
|
1289
1361
|
activeBuildLoop.waypoint = "attach";
|
|
1362
|
+
// CLAUDE-025: marks the start of THIS attempt's own
|
|
1363
|
+
// evidence window — see freshBuildLoopEvidence. Anything
|
|
1364
|
+
// that arrived before this (a prior attempt's debug
|
|
1365
|
+
// output, a manual attach, an unrelated still-running
|
|
1366
|
+
// flow) is stale and must not count toward this
|
|
1367
|
+
// attempt's own goal.
|
|
1368
|
+
activeBuildLoop.deployedAt = Date.now();
|
|
1290
1369
|
renderLoopStepper(activeBuildLoop);
|
|
1291
1370
|
// Start a timer so flows with no debug nodes (e.g. HTTP
|
|
1292
1371
|
// endpoints) don't leave the loop stuck silently waiting.
|
|
@@ -1368,6 +1447,36 @@
|
|
|
1368
1447
|
}
|
|
1369
1448
|
rec.state = "applied";
|
|
1370
1449
|
}
|
|
1450
|
+
} else if (data.event === "resolveRecordAction" && typeof data.recordId === "number") {
|
|
1451
|
+
var actionRec = null;
|
|
1452
|
+
for (var ai = 0; ai < messageRecords.length; ai++) {
|
|
1453
|
+
if (messageRecords[ai].id === data.recordId) { actionRec = messageRecords[ai]; break; }
|
|
1454
|
+
}
|
|
1455
|
+
if (actionRec && actionRec.kind === "question" && !actionRec.decision) {
|
|
1456
|
+
if (actionRec.buildConsentGate && (data.action === "proceed" || data.action === "skip")) {
|
|
1457
|
+
actionRec.decision = data.action;
|
|
1458
|
+
runBuildConsentDecision(actionRec, data.action === "proceed");
|
|
1459
|
+
} else if (actionRec.agentToolConsent && (data.action === "proceed" || data.action === "skip")) {
|
|
1460
|
+
actionRec.decision = data.action;
|
|
1461
|
+
if (typeof actionRec.onResume === "function") { actionRec.onResume(data.action === "proceed"); }
|
|
1462
|
+
} else if (actionRec.askUserTool && data.action === "answer") {
|
|
1463
|
+
actionRec.decision = "answered";
|
|
1464
|
+
actionRec.answerText = String(data.value || "");
|
|
1465
|
+
if (typeof actionRec.onAnswer === "function") { actionRec.onAnswer(actionRec.answerText); }
|
|
1466
|
+
} else if (actionRec.loopCheckpoint && (data.action === "continue" || data.action === "stop")) {
|
|
1467
|
+
actionRec.decision = data.action;
|
|
1468
|
+
if (typeof actionRec.onResume === "function") { actionRec.onResume(data.action); }
|
|
1469
|
+
}
|
|
1470
|
+
} else if (actionRec && actionRec.kind === "chip" && actionRec.chipType === "suggestedAction"
|
|
1471
|
+
&& data.action === "apply-suggested-action") {
|
|
1472
|
+
// CLAUDE-028: the redirect chip's own click handler (bound
|
|
1473
|
+
// directly on the button at render time) doesn't survive the
|
|
1474
|
+
// pop-out's innerHTML clone. Rides the same rebind-by-record-id
|
|
1475
|
+
// relay renderAskUserQuestion's quick-reply buttons already use
|
|
1476
|
+
// (bindRecordActionButtons in this file) — no chip-specific wiring
|
|
1477
|
+
// needed on the pop-out side.
|
|
1478
|
+
applySuggestedAction(actionRec.suggestedAction);
|
|
1479
|
+
}
|
|
1371
1480
|
} else if (data.event === "stopBuildLoop") {
|
|
1372
1481
|
stopBuildLoop("Build loop stopped — applied nodes remain as-is.");
|
|
1373
1482
|
} else if (data.event === "clearChat") {
|
|
@@ -1440,6 +1549,30 @@
|
|
|
1440
1549
|
loadSettings();
|
|
1441
1550
|
updateSelectionStatus();
|
|
1442
1551
|
showChat();
|
|
1552
|
+
rehydrateConversationOnLoad();
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// CLAUDE-029: on a page reload, sessionStorage already retains the
|
|
1556
|
+
// conversation-id (see history.js) and the backend already has the full
|
|
1557
|
+
// transcript (History panel proves it's retrievable) — but nothing ever
|
|
1558
|
+
// fetched and rendered it into the Chat panel on init, so a reload left
|
|
1559
|
+
// Chat looking empty even though the conversation was never lost.
|
|
1560
|
+
// Reuses loadConversation() verbatim (same fetch + same addMessage()
|
|
1561
|
+
// render path the History panel's click handler uses) so there is no
|
|
1562
|
+
// second copy of the message-rendering logic to drift out of sync.
|
|
1563
|
+
function rehydrateConversationOnLoad() {
|
|
1564
|
+
if (!conversationIdWasRestored) { return; }
|
|
1565
|
+
loadConversation(conversationId, function (msg, xhr) {
|
|
1566
|
+
// Conversation no longer exists server-side (404) — the
|
|
1567
|
+
// sessionStorage entry is stale, so drop it rather than retry
|
|
1568
|
+
// this same failed fetch on every future reload. Any other
|
|
1569
|
+
// failure (network hiccup, etc.) leaves it in place in case it
|
|
1570
|
+
// was transient. Either way: stay quiet, Chat simply stays
|
|
1571
|
+
// empty exactly as it does today — no error bubble on load.
|
|
1572
|
+
if (xhr && xhr.status === 404) {
|
|
1573
|
+
try { sessionStorage.removeItem("fp-conversation-id"); } catch (e) { /* storage unavailable */ }
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1443
1576
|
}
|
|
1444
1577
|
|
|
1445
1578
|
window.FlowPilotCore = { initMainWindow: initMainWindow, initPopout: initPopout };
|