@manny-est/node-red-flowpilot 0.4.0 → 0.5.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 +33 -0
- package/PROJECT-OVERVIEW.md +7 -7
- package/README.md +14 -8
- package/USER-GUIDE.md +27 -10
- package/flowpilot-core.css +44 -0
- package/flowpilot.js +56 -141
- package/lib/build-core-script.js +44 -0
- package/lib/build-system-prompt.js +6 -0
- package/lib/core/apply-review.js +1677 -0
- package/lib/core/history.js +93 -0
- package/lib/core/init.js +1510 -0
- package/lib/core/main.js +1710 -0
- package/lib/core/markdown.js +165 -0
- package/lib/core/modes.js +1567 -0
- package/lib/core/redaction.js +165 -0
- package/lib/core/selection-context.js +211 -0
- package/lib/envelope.js +136 -0
- package/lib/modify-system-prompt.js +3 -1
- package/lib/storage.js +4 -0
- package/package.json +1 -2
- package/flowpilot-core.js +0 -6416
package/lib/core/init.js
ADDED
|
@@ -0,0 +1,1510 @@
|
|
|
1
|
+
// ---- Slash commands ---------------------------------------------------
|
|
2
|
+
// Typed in the compose box and handled entirely client-side: never sent
|
|
3
|
+
// to the model, never recorded in conversation history.
|
|
4
|
+
var HELP_TEXT = "## Captain's briefing\n\n" +
|
|
5
|
+
"You pick the destination, I help you get there. Here's everything on the panel.\n\n" +
|
|
6
|
+
"### Modes (one armed at a time)\n\n" +
|
|
7
|
+
"- **Query** (default) — chat about your flow, ask questions, get explanations. Nothing changes.\n" +
|
|
8
|
+
"- **Generate** — describe a flow and I'll draft it. Review the diff, then Apply.\n" +
|
|
9
|
+
"- **Document** — select node(s) and I'll explain what they do.\n" +
|
|
10
|
+
"- **Modify** — select node(s), tell me what to change, review the diff, then Apply.\n\n" +
|
|
11
|
+
"The compose area glows orange in Query and blue when an Execute mode is armed, so you always know which one you're flying.\n\n" +
|
|
12
|
+
"### Shortcuts\n\n" +
|
|
13
|
+
"- `/help` — show this briefing\n" +
|
|
14
|
+
"- `/generate` — arm Generate mode\n" +
|
|
15
|
+
"- `/document` — arm Document mode\n" +
|
|
16
|
+
"- `/modify` — arm Modify mode\n" +
|
|
17
|
+
"- `/query` — back to Query (disarm)\n" +
|
|
18
|
+
"- `/clear` — start a fresh conversation (clears chat and memory)\n" +
|
|
19
|
+
"- `/history` — open the Flight log (past conversations)\n" +
|
|
20
|
+
"- `/settings` — open the Hangar (providers, behavior, safety)\n\n" +
|
|
21
|
+
"Typing a shortcut with extra text, e.g. `/modify add a debug node`, switches mode and leaves the rest in the box so you can review before sending.\n\n" +
|
|
22
|
+
"- `/demo` — load a sample Generate request (a dad joke flow) into the compose box\n" +
|
|
23
|
+
"- `/feedback` — bug report / feature request info\n" +
|
|
24
|
+
"- `/build` — describe a goal; I'll plan, propose, and walk an iterative build → deploy → debug → review → fix loop with you\n" +
|
|
25
|
+
"- `/compact` — hide labels on the selected node(s) (icon-only); `/expand` restores them. Instant, no AI involved — one Ctrl+Z undoes it.\n" +
|
|
26
|
+
"- `/disable` — disable the selected node(s) (skipped on Deploy); `/enable` re-enables them. Instant, no AI involved — one Ctrl+Z undoes it.\n\n" +
|
|
27
|
+
"### Also worth knowing\n\n" +
|
|
28
|
+
"- Action chips (paper-plane buttons) offer a one-click follow-up — review and send, nothing fires automatically.\n" +
|
|
29
|
+
"- 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" +
|
|
30
|
+
"- \"Pre-flight check\" in the Hangar tests a provider before you rely on it.\n" +
|
|
31
|
+
"- \"Touchdown\"/\"Landed\" notes confirm an applied or imported change. Ctrl+Z undoes an applied change.";
|
|
32
|
+
|
|
33
|
+
// /demo: a ready-made Generate request, used to show off FlowPilot's
|
|
34
|
+
// flow-generation capability end to end (HTTP request, status, debug)
|
|
35
|
+
// with a small, fast-to-generate flow.
|
|
36
|
+
var DEMO_PROMPT = "Using an online dad joke API (e.g. https://icanhazdadjoke.com/ with an \"Accept: application/json\" header), make an API call for a dad joke when triggered by an inject node. Set the node's status to show the joke text, and wire a debug node to output the joke itself.";
|
|
37
|
+
|
|
38
|
+
// /feedback: links back to the repo/issues, shown entirely client-side.
|
|
39
|
+
var FEEDBACK_TEXT = "## Thanks for flying with FlowPilot\n\n" +
|
|
40
|
+
"Bug? Rough edge? Idea for a feature? I'd love to hear about it — the " +
|
|
41
|
+
"human crew reads every report.\n\n" +
|
|
42
|
+
"- **Report an issue**: https://github.com/manny-est/flowpilot/issues\n" +
|
|
43
|
+
"- **Browse the repo**: https://github.com/manny-est/flowpilot\n\n" +
|
|
44
|
+
"A good report travels light but packs the essentials: your Node-RED " +
|
|
45
|
+
"version, the provider/model you're flying with, and the steps to " +
|
|
46
|
+
"reproduce. That's usually enough to get a fix off the ground.\n\n" +
|
|
47
|
+
"Safe travels.";
|
|
48
|
+
|
|
49
|
+
var demoTypeTimer = null;
|
|
50
|
+
|
|
51
|
+
// Registry of all slash commands — drives the autocomplete panel that
|
|
52
|
+
// appears when the user types "/" in the prompt box. Keep in sync with
|
|
53
|
+
// handleSlashCommand below.
|
|
54
|
+
var SLASH_COMMANDS = [
|
|
55
|
+
{ cmd: "/generate", desc: "Switch to Generate mode" },
|
|
56
|
+
{ cmd: "/document", desc: "Switch to Document mode" },
|
|
57
|
+
{ cmd: "/modify", desc: "Switch to Modify mode" },
|
|
58
|
+
{ cmd: "/build", desc: "Start a deploy-verify build loop" },
|
|
59
|
+
{ cmd: "/chat", desc: "Switch to Chat mode" },
|
|
60
|
+
{ cmd: "/query", desc: "Add or toggle a Query intent" },
|
|
61
|
+
{ cmd: "/compact", desc: "Compact labels on selected nodes" },
|
|
62
|
+
{ cmd: "/expand", desc: "Expand labels on selected nodes" },
|
|
63
|
+
{ cmd: "/disable", desc: "Disable selected nodes" },
|
|
64
|
+
{ cmd: "/enable", desc: "Enable selected nodes" },
|
|
65
|
+
{ cmd: "/demo", desc: "Type in a demo prompt" },
|
|
66
|
+
{ cmd: "/help", desc: "Show all available commands" },
|
|
67
|
+
{ cmd: "/feedback", desc: "Show feedback info" }
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
function bindSlashAutocomplete($promptBox) {
|
|
71
|
+
var $wrap = $promptBox.closest(".fp-prompt-wrap");
|
|
72
|
+
var $panel = $('<div id="fp-slash-suggest"></div>').hide();
|
|
73
|
+
$wrap.prepend($panel);
|
|
74
|
+
var activeIndex = -1;
|
|
75
|
+
|
|
76
|
+
function getRows() { return $panel.find(".fp-slash-row"); }
|
|
77
|
+
|
|
78
|
+
function setActive(idx) {
|
|
79
|
+
getRows().removeClass("fp-slash-active");
|
|
80
|
+
activeIndex = idx;
|
|
81
|
+
if (idx >= 0) { getRows().eq(idx).addClass("fp-slash-active"); }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function showPanel(partial) {
|
|
85
|
+
var matches = SLASH_COMMANDS.filter(function (c) {
|
|
86
|
+
return c.cmd.indexOf(partial) === 0;
|
|
87
|
+
});
|
|
88
|
+
if (!matches.length) { $panel.hide(); return; }
|
|
89
|
+
$panel.empty();
|
|
90
|
+
matches.forEach(function (c) {
|
|
91
|
+
$('<div class="fp-slash-row">')
|
|
92
|
+
.append($('<span class="fp-slash-cmd">').text(c.cmd))
|
|
93
|
+
.append($('<span class="fp-slash-desc">').text(c.desc))
|
|
94
|
+
.on("mousedown", function (e) {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
completeWith(c.cmd);
|
|
97
|
+
})
|
|
98
|
+
.appendTo($panel);
|
|
99
|
+
});
|
|
100
|
+
setActive(0);
|
|
101
|
+
$panel.show();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hidePanel() {
|
|
105
|
+
$panel.hide();
|
|
106
|
+
activeIndex = -1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function completeWith(cmd) {
|
|
110
|
+
$promptBox.val(cmd + " ").focus();
|
|
111
|
+
hidePanel();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
$promptBox.on("input.slashcomplete", function () {
|
|
115
|
+
var val = $promptBox.val();
|
|
116
|
+
if (/^\/\S*$/.test(val)) { showPanel(val); } else { hidePanel(); }
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
$promptBox.on("keydown.slashcomplete", function (e) {
|
|
120
|
+
if (!$panel.is(":visible")) { return; }
|
|
121
|
+
var $rows = getRows();
|
|
122
|
+
if (e.key === "ArrowDown") {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
setActive(Math.min(activeIndex + 1, $rows.length - 1));
|
|
125
|
+
} else if (e.key === "ArrowUp") {
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
setActive(Math.max(activeIndex - 1, 0));
|
|
128
|
+
} else if (e.key === "Tab" || e.key === "Enter") {
|
|
129
|
+
if (activeIndex >= 0) {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
completeWith($rows.eq(activeIndex).find(".fp-slash-cmd").text());
|
|
132
|
+
}
|
|
133
|
+
} else if (e.key === "Escape") {
|
|
134
|
+
hidePanel();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
$promptBox.on("blur.slashcomplete", function () {
|
|
139
|
+
setTimeout(function () { hidePanel(); }, 150);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Streams `text` into the prompt box a few characters at a time, as if
|
|
144
|
+
// it were being typed, then calls `onDone`. Disabled while typing so the
|
|
145
|
+
// user doesn't fight the animation; cancels any in-progress run first.
|
|
146
|
+
function typeIntoPrompt(text, onDone) {
|
|
147
|
+
var $promptBox = el("#fp-prompt");
|
|
148
|
+
if (demoTypeTimer) { clearInterval(demoTypeTimer); demoTypeTimer = null; }
|
|
149
|
+
if (!$promptBox.length) { if (onDone) { onDone(); } return; }
|
|
150
|
+
$promptBox.val("").prop("disabled", true);
|
|
151
|
+
var i = 0;
|
|
152
|
+
var CHARS_PER_TICK = 3;
|
|
153
|
+
demoTypeTimer = setInterval(function () {
|
|
154
|
+
i = Math.min(text.length, i + CHARS_PER_TICK);
|
|
155
|
+
$promptBox.val(text.slice(0, i));
|
|
156
|
+
$promptBox.scrollTop($promptBox[0].scrollHeight);
|
|
157
|
+
if (i >= text.length) {
|
|
158
|
+
clearInterval(demoTypeTimer);
|
|
159
|
+
demoTypeTimer = null;
|
|
160
|
+
$promptBox.prop("disabled", false).focus();
|
|
161
|
+
if (onDone) { onDone(); }
|
|
162
|
+
}
|
|
163
|
+
}, 12);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Returns true if `raw` was a recognized "/command" and has been fully
|
|
167
|
+
// handled (compose box updated, mode/panel switched, etc.) — callers
|
|
168
|
+
// should NOT also dispatch it as a normal chat/generate/document/modify
|
|
169
|
+
// send.
|
|
170
|
+
function handleSlashCommand(raw) {
|
|
171
|
+
var trimmed = (raw || "").trim();
|
|
172
|
+
if (trimmed.charAt(0) !== "/") { return false; }
|
|
173
|
+
|
|
174
|
+
var command = trimmed.split(/\s+/)[0].toLowerCase();
|
|
175
|
+
var rest = trimmed.slice(command.length).trim();
|
|
176
|
+
var $promptBox = el("#fp-prompt");
|
|
177
|
+
|
|
178
|
+
switch (command) {
|
|
179
|
+
case "/help":
|
|
180
|
+
case "/?":
|
|
181
|
+
addMessage("assistant", HELP_TEXT);
|
|
182
|
+
if ($promptBox.length) { $promptBox.val(""); }
|
|
183
|
+
break;
|
|
184
|
+
case "/generate":
|
|
185
|
+
armExecuteAction("generate");
|
|
186
|
+
addMessage("assistant", "Cleared for **Generate** — describe the flow you'd like me to build, then send.");
|
|
187
|
+
if ($promptBox.length) { $promptBox.val(rest); }
|
|
188
|
+
break;
|
|
189
|
+
case "/build":
|
|
190
|
+
armExecuteAction("build");
|
|
191
|
+
addMessage("assistant", "Cleared for **Build** — describe the goal. I'll plan, propose a first step, " +
|
|
192
|
+
"and after you apply/deploy/test it, walk through fix cycles with you until it works or we hit the attempt limit.");
|
|
193
|
+
if ($promptBox.length) { $promptBox.val(rest); }
|
|
194
|
+
break;
|
|
195
|
+
case "/document":
|
|
196
|
+
armExecuteAction("document");
|
|
197
|
+
addMessage("assistant", "Cleared for **Document** — select the node(s) you want explained, then send.");
|
|
198
|
+
if ($promptBox.length) { $promptBox.val(rest); }
|
|
199
|
+
break;
|
|
200
|
+
case "/modify":
|
|
201
|
+
armExecuteAction("modify");
|
|
202
|
+
addMessage("assistant", "Cleared for **Modify** — select the node(s) you want changed, describe the change, then send.");
|
|
203
|
+
if ($promptBox.length) { $promptBox.val(rest); }
|
|
204
|
+
break;
|
|
205
|
+
case "/query":
|
|
206
|
+
case "/chat":
|
|
207
|
+
disarmExecuteAction();
|
|
208
|
+
addMessage("assistant", "Back to **Query** — ask away.");
|
|
209
|
+
if ($promptBox.length) { $promptBox.val(rest); }
|
|
210
|
+
break;
|
|
211
|
+
case "/clear":
|
|
212
|
+
clearChat();
|
|
213
|
+
if ($promptBox.length) { $promptBox.val(""); }
|
|
214
|
+
break;
|
|
215
|
+
case "/history":
|
|
216
|
+
showHistory();
|
|
217
|
+
if ($promptBox.length) { $promptBox.val(""); }
|
|
218
|
+
break;
|
|
219
|
+
case "/settings":
|
|
220
|
+
showSettings();
|
|
221
|
+
if ($promptBox.length) { $promptBox.val(""); }
|
|
222
|
+
break;
|
|
223
|
+
case "/demo":
|
|
224
|
+
armExecuteAction("generate");
|
|
225
|
+
addMessage("assistant", "Cleared for **Generate** — loading a demo request into the compose box.");
|
|
226
|
+
typeIntoPrompt(DEMO_PROMPT, function () {
|
|
227
|
+
el("#fp-send").addClass("fp-send-breathe");
|
|
228
|
+
});
|
|
229
|
+
break;
|
|
230
|
+
case "/feedback":
|
|
231
|
+
addMessage("assistant", FEEDBACK_TEXT);
|
|
232
|
+
if ($promptBox.length) { $promptBox.val(""); }
|
|
233
|
+
break;
|
|
234
|
+
// Deterministic, no LLM round-trip: just invokes Node-RED's own
|
|
235
|
+
// native "show/hide selected node labels" action (RED.actions
|
|
236
|
+
// "core:show-selected-node-labels" / "core:hide-selected-node-
|
|
237
|
+
// labels", confirmed present in both NR4 and NR5 — the same
|
|
238
|
+
// action a right-click context menu item triggers). Reusing it
|
|
239
|
+
// outright means group-member expansion, no-op skipping, and
|
|
240
|
+
// batching every affected node into ONE compound undo step are
|
|
241
|
+
// already handled correctly — nothing to reimplement.
|
|
242
|
+
case "/compact":
|
|
243
|
+
case "/expand":
|
|
244
|
+
// These two need a live RED.view selection + RED.actions.invoke
|
|
245
|
+
// — dead in the pop-out's disconnected window. Relay the raw
|
|
246
|
+
// command to the parent and let it run this exact same case
|
|
247
|
+
// for real, instead of duplicating the selection-check/invoke
|
|
248
|
+
// logic here.
|
|
249
|
+
if (isPopoutContext) {
|
|
250
|
+
if (window.opener && !window.opener.closed) {
|
|
251
|
+
try { window.opener.postMessage({ event: "runSlashCommand", command: command }, location.origin); } catch (e) { /* ignore */ }
|
|
252
|
+
}
|
|
253
|
+
if ($promptBox.length) { $promptBox.val(""); }
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
var sel = (RED.view && RED.view.selection) ? RED.view.selection() : null;
|
|
257
|
+
var selCount = (sel && sel.nodes) ? sel.nodes.length : 0;
|
|
258
|
+
if (selCount === 0) {
|
|
259
|
+
addMessage("error", "Select one or more nodes first.");
|
|
260
|
+
if ($promptBox.length) { $promptBox.val(""); }
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
if (command === "/expand") {
|
|
264
|
+
RED.actions.invoke("core:show-selected-node-labels");
|
|
265
|
+
addMessage("assistant", "Touchdown — expanded " + selCount +
|
|
266
|
+
" node label" + (selCount === 1 ? "" : "s") + ". Ctrl+Z to undo.");
|
|
267
|
+
} else {
|
|
268
|
+
RED.actions.invoke("core:hide-selected-node-labels");
|
|
269
|
+
addMessage("assistant", "Touchdown — compacted " + selCount +
|
|
270
|
+
" node label" + (selCount === 1 ? "" : "s") + ". Ctrl+Z to undo.");
|
|
271
|
+
}
|
|
272
|
+
if ($promptBox.length) { $promptBox.val(""); }
|
|
273
|
+
break;
|
|
274
|
+
// Same deterministic pattern as /compact+/expand above, just
|
|
275
|
+
// toggling the "d" (disabled) flag instead of label visibility —
|
|
276
|
+
// Node-RED's own native "core:enable-selected-nodes"/
|
|
277
|
+
// "core:disable-selected-nodes" actions (confirmed present in
|
|
278
|
+
// both NR4 and NR5, the same actions the right-click context menu
|
|
279
|
+
// uses) already batch every affected node into one compound undo
|
|
280
|
+
// step and skip no-ops, so there's nothing to reimplement here.
|
|
281
|
+
case "/disable":
|
|
282
|
+
case "/enable":
|
|
283
|
+
if (isPopoutContext) {
|
|
284
|
+
if (window.opener && !window.opener.closed) {
|
|
285
|
+
try { window.opener.postMessage({ event: "runSlashCommand", command: command }, location.origin); } catch (e) { /* ignore */ }
|
|
286
|
+
}
|
|
287
|
+
if ($promptBox.length) { $promptBox.val(""); }
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
var dSel = (RED.view && RED.view.selection) ? RED.view.selection() : null;
|
|
291
|
+
var dSelCount = (dSel && dSel.nodes) ? dSel.nodes.length : 0;
|
|
292
|
+
if (dSelCount === 0) {
|
|
293
|
+
addMessage("error", "Select one or more nodes first.");
|
|
294
|
+
if ($promptBox.length) { $promptBox.val(""); }
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
if (command === "/enable") {
|
|
298
|
+
RED.actions.invoke("core:enable-selected-nodes");
|
|
299
|
+
addMessage("assistant", "Touchdown — enabled " + dSelCount +
|
|
300
|
+
" node" + (dSelCount === 1 ? "" : "s") + ". Ctrl+Z to undo.");
|
|
301
|
+
} else {
|
|
302
|
+
RED.actions.invoke("core:disable-selected-nodes");
|
|
303
|
+
addMessage("assistant", "Touchdown — disabled " + dSelCount +
|
|
304
|
+
" node" + (dSelCount === 1 ? "" : "s") + ". Ctrl+Z to undo.");
|
|
305
|
+
}
|
|
306
|
+
if ($promptBox.length) { $promptBox.val(""); }
|
|
307
|
+
break;
|
|
308
|
+
default:
|
|
309
|
+
addMessage("assistant", "Unrecognized command `" + command + "`. Type `/help` for the full list.");
|
|
310
|
+
if ($promptBox.length) { $promptBox.val(""); }
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
// ---- Tabbed review + import --------------------------------------------
|
|
319
|
+
// Hands validated flow JSON to Node-RED's own import mechanism — the same
|
|
320
|
+
// path the editor's Import menu uses (confirmed live: RED.view.importNodes
|
|
321
|
+
// exists, attaches the new nodes to the cursor for the user to place with
|
|
322
|
+
// one click, and registers the whole add as a single native undo step).
|
|
323
|
+
// generateIds avoids id clashes with existing nodes, matching the behavior
|
|
324
|
+
// already validated via manual import of a generated flow.
|
|
325
|
+
// onImported: optional, called only after a successful import — used by
|
|
326
|
+
// the /build loop to advance to the "apply" waypoint once the proposal
|
|
327
|
+
// actually lands. Plain Generate passes nothing, so it's a no-op there.
|
|
328
|
+
|
|
329
|
+
// Copies text to the clipboard and gives the triggering button brief
|
|
330
|
+
// "Copied!" feedback. Tries the modern Clipboard API first — but that
|
|
331
|
+
// requires a "secure context" (HTTPS or localhost), which this editor may
|
|
332
|
+
// not be served over — and falls back to the long-supported
|
|
333
|
+
// execCommand("copy") approach via a temporary off-screen textarea, which
|
|
334
|
+
// works regardless of context. Plain clipboard access is a generic browser
|
|
335
|
+
// feature, not an editor capability, so there's nothing Node-RED-specific
|
|
336
|
+
// to reuse here.
|
|
337
|
+
function copyToClipboard($btn, text) {
|
|
338
|
+
var original = $btn.data("fp-label");
|
|
339
|
+
if (original === undefined) {
|
|
340
|
+
original = $btn.text();
|
|
341
|
+
$btn.data("fp-label", original);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function showCopied() {
|
|
345
|
+
$btn.text("Copied!");
|
|
346
|
+
setTimeout(function () { $btn.text(original); }, 1500);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function legacyCopy() {
|
|
350
|
+
var $ta = $("<textarea>").val(text)
|
|
351
|
+
.css({ position: "fixed", top: "-1000px", left: "-1000px" });
|
|
352
|
+
$("body").append($ta);
|
|
353
|
+
$ta[0].select();
|
|
354
|
+
var ok = false;
|
|
355
|
+
try { ok = document.execCommand("copy"); } catch (e) { ok = false; }
|
|
356
|
+
$ta.remove();
|
|
357
|
+
if (ok) { showCopied(); }
|
|
358
|
+
else { addMessage("error", "Couldn't copy to clipboard."); }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
362
|
+
navigator.clipboard.writeText(text).then(showCopied, legacyCopy);
|
|
363
|
+
} else {
|
|
364
|
+
legacyCopy();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Tabbed Summary/JSON review for a generated flow, modeled on the look of
|
|
369
|
+
// Node-RED's Export dialog but built from FlowPilot's own bespoke markup
|
|
370
|
+
// (fp-tab) themed with the editor's --red-ui-* variables — see the handoff
|
|
371
|
+
// doc for why we don't reuse Node-RED's internal tab/export classes.
|
|
372
|
+
// Summary lists each node with type warnings flagged inline plus a
|
|
373
|
+
// detail box; JSON shows the raw, copyable flow. Broken wire references
|
|
374
|
+
// block the "Add to workspace" action (the JSON itself is malformed, not
|
|
375
|
+
// a normal "node not installed" situation); type warnings do not — the
|
|
376
|
+
// user is informed and decides whether to proceed or regenerate.
|
|
377
|
+
// buildGoal: present only for the /build loop's first-step proposal
|
|
378
|
+
// (handleBuildResult) — lets the pop-out's Apply tag carry what
|
|
379
|
+
// startBuildLoop needs (the original goal text) alongside the flow,
|
|
380
|
+
// since plain Generate/Document have no goal/loop to start.
|
|
381
|
+
|
|
382
|
+
// ---- Pop-out window (Phase 8.5 C1) ------------------------------------
|
|
383
|
+
// Mirrors how Node-RED 5's own Debug panel pops out (confirmed against
|
|
384
|
+
// @node-red/nodes/core/common/21-debug.html / debug.js): the SAME
|
|
385
|
+
// renderer (this whole file) loads in both windows; canvas-touching
|
|
386
|
+
// pieces (importGeneratedFlow et al.) stay running in the MAIN window
|
|
387
|
+
// always — the pop-out only ever proxies an intent back via
|
|
388
|
+
// postMessage, never calls RED.* itself. addMessage() and everything it
|
|
389
|
+
// calls runs completely unmodified in either window. Unlike NR5's own
|
|
390
|
+
// reference code (which uses "*" everywhere), every postMessage here
|
|
391
|
+
// pins targetOrigin to location.origin.
|
|
392
|
+
//
|
|
393
|
+
// Slice 1: read-only chat mirror. Slice 2: sending chat from the
|
|
394
|
+
// pop-out (now generalized into the "dispatchSend" intent — see
|
|
395
|
+
// dispatchSend() — covering every mode, not just chat). Slice 3
|
|
396
|
+
// (below): a plain Generate/Document review panel's "Add to
|
|
397
|
+
// workspace" button becomes functional in the pop-out too — Modify
|
|
398
|
+
// and the /build loop are NOT covered by THIS mechanism (Modify's diff
|
|
399
|
+
// is computed against LIVE RED.nodes state, which the pop-out doesn't
|
|
400
|
+
// have; the build loop's import has loop-
|
|
401
|
+
// state follow-up that can't be safely re-triggered from a relayed
|
|
402
|
+
// click) — both stay inert-HTML-only for now, known gaps.
|
|
403
|
+
|
|
404
|
+
// Relays one newly-added top-level #fp-messages child to the pop-out by
|
|
405
|
+
// its rendered HTML. Event handlers don't survive serialization, so the
|
|
406
|
+
// mirrored copy is inert (buttons/links render but do nothing) — exactly
|
|
407
|
+
// right for a display-only v1.
|
|
408
|
+
function relayAppendToPopout(html) {
|
|
409
|
+
if (!popoutWindow || popoutWindow.closed) { return; }
|
|
410
|
+
try {
|
|
411
|
+
popoutWindow.postMessage({ event: "appendMessage", html: html }, location.origin);
|
|
412
|
+
} catch (e) { /* pop-out may be navigating/closing — drop silently */ }
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Relays a removal (e.g. renderLoopStepper replacing the previous
|
|
416
|
+
// stepper, or the pending "typing" indicator being cleared) so the
|
|
417
|
+
// mirror doesn't accumulate stale copies of elements that get replaced
|
|
418
|
+
// in place rather than appended fresh.
|
|
419
|
+
function relayRemoveToPopout(id) {
|
|
420
|
+
if (!popoutWindow || popoutWindow.closed || !id) { return; }
|
|
421
|
+
try {
|
|
422
|
+
popoutWindow.postMessage({ event: "removeMessage", id: id }, location.origin);
|
|
423
|
+
} catch (e) { /* ignore, same as above */ }
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// clearChat()'s el("#fp-messages").empty() removes every bubble at
|
|
427
|
+
// once, but the generic MutationObserver relay only relays a removal
|
|
428
|
+
// when the removed node has an `id` (ordinary chat bubbles don't) —
|
|
429
|
+
// so a bulk clear would silently NOT mirror. Explicit, dedicated event
|
|
430
|
+
// instead of trying to make the generic observer handle bulk removal.
|
|
431
|
+
function relayClearMessagesToPopout() {
|
|
432
|
+
if (!popoutWindow || popoutWindow.closed) { return; }
|
|
433
|
+
try {
|
|
434
|
+
popoutWindow.postMessage({ event: "clearMessages" }, location.origin);
|
|
435
|
+
} catch (e) { /* ignore */ }
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Mirrors the status-strip's live-selection-dependent pieces into the
|
|
439
|
+
// pop-out — sent as plain text/class values, NOT outerHTML, since
|
|
440
|
+
// those elements sit in the SAME .fp-status-strip as the pop-out's own
|
|
441
|
+
// bound Send/Clear buttons (a blind HTML swap would clobber them).
|
|
442
|
+
// Called from the end of updateSelectionStatus()/updateDebugStatus()
|
|
443
|
+
// (both parent-only in practice — only the main window has a live
|
|
444
|
+
// RED.view.selection() to report on). The debug line is rebuilt from
|
|
445
|
+
// attachedDebugMessages directly rather than read from the DOM, since
|
|
446
|
+
// the live element also contains a "preview" link whose text would
|
|
447
|
+
// otherwise bleed into the relayed string (that link's click-through
|
|
448
|
+
// needs live context data and isn't relayed at all — known v1 gap,
|
|
449
|
+
// same as the Preview JSON link below).
|
|
450
|
+
function relayStatusStripToPopout() {
|
|
451
|
+
if (!popoutWindow || popoutWindow.closed) { return; }
|
|
452
|
+
var debugCount = attachedDebugMessages.length;
|
|
453
|
+
var payload = {
|
|
454
|
+
selectionText: el("#fp-selection-status").text(),
|
|
455
|
+
hasSelection: el("#fp-selection-status").hasClass("fp-has-selection"),
|
|
456
|
+
previewVisible: !el("#fp-preview-nodes").hasClass("fp-hidden"),
|
|
457
|
+
sizeText: el("#fp-size-status").text(),
|
|
458
|
+
sizeHidden: el("#fp-size-status").hasClass("fp-hidden"),
|
|
459
|
+
sizeWarn: el("#fp-size-status").hasClass("fp-size-warn"),
|
|
460
|
+
sizeHigh: el("#fp-size-status").hasClass("fp-size-high"),
|
|
461
|
+
secretsHidden: el("#fp-secrets-status").hasClass("fp-hidden"),
|
|
462
|
+
secretsOff: el("#fp-secrets-status").hasClass("fp-secrets-status-off"),
|
|
463
|
+
secretsTitle: el("#fp-secrets-status").attr("title") || "",
|
|
464
|
+
debugHidden: debugCount === 0,
|
|
465
|
+
debugText: debugCount
|
|
466
|
+
? ("🐛 " + debugCount + " debug message" + (debugCount === 1 ? "" : "s") + " attached")
|
|
467
|
+
: ""
|
|
468
|
+
};
|
|
469
|
+
try {
|
|
470
|
+
popoutWindow.postMessage({ event: "statusStripSync", data: payload }, location.origin);
|
|
471
|
+
} catch (e) { /* ignore */ }
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Started once the pop-out is open. Only watches direct children of
|
|
475
|
+
// #fp-messages (childList) — an existing bubble's content changing in
|
|
476
|
+
// place (e.g. a streaming reply filling in) is NOT relayed in v1; the
|
|
477
|
+
// mirror catches up once the next sibling message is added. Known,
|
|
478
|
+
// accepted limitation for the smallest first slice — not a regression
|
|
479
|
+
// for tool-capable providers, which never stream today anyway.
|
|
480
|
+
function startPopoutRelay() {
|
|
481
|
+
var box = el("#fp-messages")[0];
|
|
482
|
+
if (!box || popoutObserver) { return; }
|
|
483
|
+
popoutObserver = new MutationObserver(function (mutations) {
|
|
484
|
+
mutations.forEach(function (m) {
|
|
485
|
+
m.removedNodes.forEach(function (node) {
|
|
486
|
+
if (node.nodeType === 1 && node.id) { relayRemoveToPopout(node.id); }
|
|
487
|
+
});
|
|
488
|
+
m.addedNodes.forEach(function (node) {
|
|
489
|
+
if (node.nodeType === 1) { relayAppendToPopout(node.outerHTML); }
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
popoutObserver.observe(box, { childList: true });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function stopPopoutRelay() {
|
|
497
|
+
if (popoutObserver) { popoutObserver.disconnect(); popoutObserver = null; }
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Opens (or focuses, if already open — named window target) the
|
|
501
|
+
// detached mirror, mirroring 21-debug.html's window.open call exactly
|
|
502
|
+
// (same options string shape). Sends a one-time snapshot of the
|
|
503
|
+
// CURRENT #fp-messages content once the pop-out finishes loading, then
|
|
504
|
+
// starts the live relay for everything after that point.
|
|
505
|
+
function openPopout() {
|
|
506
|
+
if (popoutWindow && !popoutWindow.closed) {
|
|
507
|
+
popoutWindow.focus();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
popoutWindow = window.open(
|
|
511
|
+
document.location.toString().replace(/[?#].*$/, "") + "flowpilot/popout/view.html" + document.location.search,
|
|
512
|
+
"flowpilotPopout",
|
|
513
|
+
"menubar=no,location=no,toolbar=no,chrome,height=700,width=480"
|
|
514
|
+
);
|
|
515
|
+
if (!popoutWindow) { return; }
|
|
516
|
+
popoutWindow.onload = function () {
|
|
517
|
+
var html = el("#fp-messages").length ? el("#fp-messages").html() : "";
|
|
518
|
+
try {
|
|
519
|
+
popoutWindow.postMessage({ event: "initialSync", html: html }, location.origin);
|
|
520
|
+
} catch (e) { /* ignore */ }
|
|
521
|
+
startPopoutRelay();
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Entry point for the pop-out's own page (lib/popout/view.html, loaded
|
|
526
|
+
// via this SAME script). Builds the SAME cockpit shell as the main
|
|
527
|
+
// window's action-bar/compose/status-strip (same ids/classes, so the
|
|
528
|
+
// existing CSS and functions below apply unmodified) and relays any
|
|
529
|
+
// user-initiated close/reopen back via window.opener so the main
|
|
530
|
+
// window's "the pop-out is closed" state (popoutWindow.closed) stays
|
|
531
|
+
// accurate without polling.
|
|
532
|
+
//
|
|
533
|
+
// Full cockpit parity (2026-06-26): arming/disarming Generate/Document/
|
|
534
|
+
// Modify, Query intents, and every slash command except /compact+
|
|
535
|
+
// /expand are pure local state (setArmedExecuteAction/
|
|
536
|
+
// handleSlashCommand/armQueryIntent have zero RED.* calls) and work
|
|
537
|
+
// completely unmodified here — only dispatchSend()'s FINAL dispatch and
|
|
538
|
+
// /compact+/expand need to relay instead of touching dead RED.* state
|
|
539
|
+
// (see isPopoutContext, checked inside those two functions). Settings
|
|
540
|
+
// are loaded independently via the pop-out's OWN loadSettings() call
|
|
541
|
+
// (plain ajaxJson, zero RED.* coupling) — gives a correct Provider-
|
|
542
|
+
// status line and working custom Query intents for free via the
|
|
543
|
+
// existing fillSettings(), which jQuery no-ops harmlessly on the
|
|
544
|
+
// Settings-panel field ids this pop-out doesn't render this slice.
|
|
545
|
+
//
|
|
546
|
+
// conversationId/conversationHistory/attachedDebugMessages/
|
|
547
|
+
// activeBuildLoop stay PARENT-OWNED — this window never keeps its own
|
|
548
|
+
// copies. Sending (any mode) and Clear Chat are relayed asks; the
|
|
549
|
+
// selection-status strip is a relayed MIRROR (relayStatusStripToPopout,
|
|
550
|
+
// called from the parent's updateSelectionStatus/updateDebugStatus).
|
|
551
|
+
|
|
552
|
+
// Slice 3: relayed HTML for a plain Generate/Document review panel
|
|
553
|
+
// (addGeneratedReview tags these with data-fp-apply-flow — see there
|
|
554
|
+
// for why the /build loop's panels are excluded) carries the flow data
|
|
555
|
+
// right in the markup, since outerHTML serializes every attribute.
|
|
556
|
+
// Re-validating/re-rendering in the pop-out isn't needed (and wouldn't
|
|
557
|
+
// work anyway — RED.nodes.getType has no installed types in this
|
|
558
|
+
// window) — the parent already validated everything before the
|
|
559
|
+
// snapshot was relayed. This just rebinds ONE button to ask the parent
|
|
560
|
+
// to do exactly what it would do if the same button were clicked in
|
|
561
|
+
// the sidebar.
|
|
562
|
+
function bindApplyButtons($scope) {
|
|
563
|
+
// appendMessage's scope IS the tagged panel itself (a single newly
|
|
564
|
+
// relayed top-level element); initialSync's scope is the container
|
|
565
|
+
// around many descendants — cover both with filter()+find().
|
|
566
|
+
$scope.filter("[data-fp-apply-flow]").add($scope.find("[data-fp-apply-flow]")).each(function () {
|
|
567
|
+
var $panel = $(this);
|
|
568
|
+
if ($panel.data("fp-apply-bound")) { return; }
|
|
569
|
+
$panel.data("fp-apply-bound", true);
|
|
570
|
+
var flow;
|
|
571
|
+
try { flow = JSON.parse($panel.attr("data-fp-apply-flow")); } catch (e) { return; }
|
|
572
|
+
$panel.find(".fp-review-actions button.red-ui-button-primary").on("click", function () {
|
|
573
|
+
var $btn = $(this);
|
|
574
|
+
$btn.prop("disabled", true).text("Click the canvas to place…");
|
|
575
|
+
if (!window.opener || window.opener.closed) { return; }
|
|
576
|
+
try {
|
|
577
|
+
window.opener.postMessage({ event: "applyGenerated", flow: flow }, location.origin);
|
|
578
|
+
} catch (e) { /* ignore */ }
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Same idea as bindApplyButtons, but for a relayed PLAIN Modify review
|
|
584
|
+
// panel (addModifyReview tags these with data-fp-apply-modify; a
|
|
585
|
+
// /build loop fix gets a separate tag — see bindBuildFixApplyButtons).
|
|
586
|
+
// nodeDiffs/removeNodes/newNodes/newWires already reflect the diff the
|
|
587
|
+
// parent computed against live RED.nodes state at review time; the
|
|
588
|
+
// pop-out doesn't recompute anything, it just asks the parent to run
|
|
589
|
+
// applyInsertions/applyModifications with this exact data, same as a
|
|
590
|
+
// sidebar click would.
|
|
591
|
+
function bindModifyApplyButtons($scope) {
|
|
592
|
+
$scope.filter("[data-fp-apply-modify]").add($scope.find("[data-fp-apply-modify]")).each(function () {
|
|
593
|
+
var $panel = $(this);
|
|
594
|
+
if ($panel.data("fp-apply-modify-bound")) { return; }
|
|
595
|
+
$panel.data("fp-apply-modify-bound", true);
|
|
596
|
+
var applyData;
|
|
597
|
+
try { applyData = JSON.parse($panel.attr("data-fp-apply-modify")); } catch (e) { return; }
|
|
598
|
+
$panel.find(".fp-review-actions button.red-ui-button-primary").on("click", function () {
|
|
599
|
+
var $btn = $(this);
|
|
600
|
+
$btn.prop("disabled", true).text("Applying…");
|
|
601
|
+
if (!window.opener || window.opener.closed) { return; }
|
|
602
|
+
try {
|
|
603
|
+
window.opener.postMessage({ event: "applyModify", data: applyData }, location.origin);
|
|
604
|
+
} catch (e) { /* ignore */ }
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// /build loop, first proposal: addGeneratedReview tags this panel
|
|
610
|
+
// (onImported set AND a buildGoal — plain Generate/Document panels get
|
|
611
|
+
// data-fp-apply-flow instead, bound above) with the flow plus the
|
|
612
|
+
// original goal text. The parent's "applyBuild" handler runs
|
|
613
|
+
// importGeneratedFlow then startBuildLoop with it, exactly like
|
|
614
|
+
// handleBuildResult's own onImported closure would.
|
|
615
|
+
function bindBuildApplyButtons($scope) {
|
|
616
|
+
$scope.filter("[data-fp-apply-build]").add($scope.find("[data-fp-apply-build]")).each(function () {
|
|
617
|
+
var $panel = $(this);
|
|
618
|
+
if ($panel.data("fp-apply-build-bound")) { return; }
|
|
619
|
+
$panel.data("fp-apply-build-bound", true);
|
|
620
|
+
var applyData;
|
|
621
|
+
try { applyData = JSON.parse($panel.attr("data-fp-apply-build")); } catch (e) { return; }
|
|
622
|
+
$panel.find(".fp-review-actions button.red-ui-button-primary").on("click", function () {
|
|
623
|
+
var $btn = $(this);
|
|
624
|
+
$btn.prop("disabled", true).text("Click the canvas to place…");
|
|
625
|
+
if (!window.opener || window.opener.closed) { return; }
|
|
626
|
+
try {
|
|
627
|
+
window.opener.postMessage({ event: "applyBuild", data: applyData }, location.origin);
|
|
628
|
+
} catch (e) { /* ignore */ }
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// /build loop, fix iterations: addModifyReview tags this panel with
|
|
634
|
+
// data-fp-apply-build-fix (instead of data-fp-apply-modify) when
|
|
635
|
+
// buildFixInfo was passed — see applyBuildLoopFix. The parent's
|
|
636
|
+
// "applyBuildFix" handler runs applyInsertions/applyBuildLoopFix
|
|
637
|
+
// with the relayed data, same loop bookkeeping a local click would do.
|
|
638
|
+
function bindBuildFixApplyButtons($scope) {
|
|
639
|
+
$scope.filter("[data-fp-apply-build-fix]").add($scope.find("[data-fp-apply-build-fix]")).each(function () {
|
|
640
|
+
var $panel = $(this);
|
|
641
|
+
if ($panel.data("fp-apply-build-fix-bound")) { return; }
|
|
642
|
+
$panel.data("fp-apply-build-fix-bound", true);
|
|
643
|
+
var applyData;
|
|
644
|
+
try { applyData = JSON.parse($panel.attr("data-fp-apply-build-fix")); } catch (e) { return; }
|
|
645
|
+
$panel.find(".fp-review-actions button.red-ui-button-primary").on("click", function () {
|
|
646
|
+
var $btn = $(this);
|
|
647
|
+
$btn.prop("disabled", true).text("Applying…");
|
|
648
|
+
if (!window.opener || window.opener.closed) { return; }
|
|
649
|
+
try {
|
|
650
|
+
window.opener.postMessage({ event: "applyBuildFix", data: applyData }, location.origin);
|
|
651
|
+
} catch (e) { /* ignore */ }
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// The loop stepper's "Stop build loop" button is relayed the same
|
|
657
|
+
// generic way as any other chat message (renderLoopStepper appends/
|
|
658
|
+
// replaces a #fp-loop-stepper element, which the MutationObserver
|
|
659
|
+
// relay already mirrors via plain add/remove) — but like every other
|
|
660
|
+
// relayed button, it loses its click handler on the way over. Rebind
|
|
661
|
+
// it to ask the parent to do exactly what a local click would.
|
|
662
|
+
function bindStopLoopButton($scope) {
|
|
663
|
+
$scope.filter("#fp-loop-stepper").add($scope.find("#fp-loop-stepper")).each(function () {
|
|
664
|
+
var $stepper = $(this);
|
|
665
|
+
if ($stepper.data("fp-stop-loop-bound")) { return; }
|
|
666
|
+
$stepper.data("fp-stop-loop-bound", true);
|
|
667
|
+
$stepper.find(".fp-loop-actions button").on("click", function () {
|
|
668
|
+
if (!window.opener || window.opener.closed) { return; }
|
|
669
|
+
try {
|
|
670
|
+
window.opener.postMessage({ event: "stopBuildLoop" }, location.origin);
|
|
671
|
+
} catch (e) { /* ignore */ }
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// The Summary/JSON tab toggle (addGeneratedReview, addModifyReview,
|
|
677
|
+
// and anything else using the same .fp-tabs/.fp-tab-panel pattern) is
|
|
678
|
+
// purely local DOM show/hide — unlike Apply, it needs no parent
|
|
679
|
+
// access at all, so this works for EVERY relayed review panel. Lost
|
|
680
|
+
// the same way Apply's original handler did (relayed HTML has no
|
|
681
|
+
// event listeners) — this just rebinds the toggle.
|
|
682
|
+
function bindTabSwitching($scope) {
|
|
683
|
+
$scope.filter(".fp-tabs").add($scope.find(".fp-tabs")).each(function () {
|
|
684
|
+
var $tabs = $(this);
|
|
685
|
+
if ($tabs.data("fp-tabs-bound")) { return; }
|
|
686
|
+
$tabs.data("fp-tabs-bound", true);
|
|
687
|
+
var $tabButtons = $tabs.find(".fp-tab");
|
|
688
|
+
var $panels = $tabs.siblings(".fp-tab-panel");
|
|
689
|
+
$tabButtons.each(function (i) {
|
|
690
|
+
$(this).on("click", function () {
|
|
691
|
+
$tabButtons.removeClass("fp-tab-active");
|
|
692
|
+
$(this).addClass("fp-tab-active");
|
|
693
|
+
$panels.addClass("fp-hidden");
|
|
694
|
+
$panels.eq(i).removeClass("fp-hidden");
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Rebinds the "Attach" buttons in a relayed debug-log panel. The
|
|
701
|
+
// original handlers (closures over debugMessageBuffer entries) don't
|
|
702
|
+
// survive outerHTML serialization — same problem Apply buttons had.
|
|
703
|
+
// Each button carries data-fp-debug-id; clicking relays the entry id
|
|
704
|
+
// to the parent, which finds it in its own debugMessageBuffer and
|
|
705
|
+
// pushes it to attachedDebugMessages (then calls updateDebugStatus,
|
|
706
|
+
// which already calls relayStatusStripToPopout to sync the counter).
|
|
707
|
+
function bindDebugAttachButtons($scope) {
|
|
708
|
+
$scope.filter("[data-fp-debug-id]").add($scope.find("[data-fp-debug-id]")).each(function () {
|
|
709
|
+
var $btn = $(this);
|
|
710
|
+
if ($btn.data("fp-debug-attach-bound")) { return; }
|
|
711
|
+
$btn.data("fp-debug-attach-bound", true);
|
|
712
|
+
var entryId = $btn.attr("data-fp-debug-id");
|
|
713
|
+
$btn.on("click", function () {
|
|
714
|
+
if (!window.opener || window.opener.closed) { return; }
|
|
715
|
+
$btn.prop("disabled", true).text("Attached");
|
|
716
|
+
try { window.opener.postMessage({ event: "attachDebug", entryId: entryId }, location.origin); } catch (e) { /* ignore */ }
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function bindPromptResize() {
|
|
722
|
+
var PROMPT_MIN_HEIGHT = 88;
|
|
723
|
+
var PROMPT_MAX_HEIGHT = 480;
|
|
724
|
+
var dragStartY = null;
|
|
725
|
+
var dragStartHeight = null;
|
|
726
|
+
|
|
727
|
+
function onDrag(e) {
|
|
728
|
+
if (dragStartY === null) { return; }
|
|
729
|
+
var clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
|
730
|
+
var next = dragStartHeight + (dragStartY - clientY);
|
|
731
|
+
next = Math.min(PROMPT_MAX_HEIGHT, Math.max(PROMPT_MIN_HEIGHT, next));
|
|
732
|
+
el("#fp-prompt").css("height", next + "px");
|
|
733
|
+
e.preventDefault();
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function endDrag() {
|
|
737
|
+
dragStartY = null;
|
|
738
|
+
el("#fp-prompt-resize").removeClass("fp-resizing");
|
|
739
|
+
$(document)
|
|
740
|
+
.off("mousemove", onDrag).off("mouseup", endDrag)
|
|
741
|
+
.off("touchmove", onDrag).off("touchend", endDrag);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function startDrag(e) {
|
|
745
|
+
var clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
|
746
|
+
dragStartY = clientY;
|
|
747
|
+
dragStartHeight = el("#fp-prompt")[0].offsetHeight;
|
|
748
|
+
el("#fp-prompt-resize").addClass("fp-resizing");
|
|
749
|
+
$(document)
|
|
750
|
+
.on("mousemove", onDrag).on("mouseup", endDrag)
|
|
751
|
+
.on("touchmove", onDrag).on("touchend", endDrag);
|
|
752
|
+
e.preventDefault();
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
el("#fp-prompt-resize").on("mousedown touchstart", startDrag);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Matches Node-RED 5's own debug pop-out (debug.js) exactly: the dark/
|
|
759
|
+
// light preference lives in localStorage under "view-dark-theme"
|
|
760
|
+
// ("dark" / "auto" / anything else = light), shared with the main
|
|
761
|
+
// window since both are same-origin. red/style.min.css's dark-mode
|
|
762
|
+
// variable overrides are scoped under the SAME nr-theme-dark class the
|
|
763
|
+
// main editor toggles on <html> — without this, the pop-out always
|
|
764
|
+
// rendered light regardless of the editor's actual theme.
|
|
765
|
+
function applyPopoutTheme() {
|
|
766
|
+
var themeVariant = localStorage.getItem("view-dark-theme");
|
|
767
|
+
var isDark = false;
|
|
768
|
+
if (themeVariant === "dark") {
|
|
769
|
+
isDark = true;
|
|
770
|
+
} else if (themeVariant === "auto") {
|
|
771
|
+
isDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
772
|
+
}
|
|
773
|
+
document.documentElement.classList.toggle("nr-theme-dark", isDark);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function initPopout() {
|
|
777
|
+
isPopoutContext = true;
|
|
778
|
+
applyPopoutTheme();
|
|
779
|
+
var content = $(
|
|
780
|
+
'<div id="fp-root">' +
|
|
781
|
+
' <div class="fp-header">' +
|
|
782
|
+
' <div class="fp-header-row">' +
|
|
783
|
+
' <div class="fp-logo">FP</div>' +
|
|
784
|
+
' <div class="fp-heading">' +
|
|
785
|
+
' <div class="fp-title">FlowPilot</div>' +
|
|
786
|
+
' <div class="fp-subtitle">AI flow assistant</div>' +
|
|
787
|
+
' </div>' +
|
|
788
|
+
' <div class="fp-view-buttons">' +
|
|
789
|
+
' <button id="fp-clear-chat" class="red-ui-button red-ui-button-small" type="button" title="Clear chat and start a fresh conversation (resets memory)"><i class="fa fa-eraser"></i></button>' +
|
|
790
|
+
' <button id="fp-recall" class="red-ui-button red-ui-button-small" type="button" title="Recall: search earlier conversations for the text in the prompt box"><i class="fa fa-search"></i></button>' +
|
|
791
|
+
' <button id="fp-debug-log" class="red-ui-button red-ui-button-small" type="button" title="Debug log: view recent Debug sidebar output and attach messages as context"><i class="fa fa-bug"></i></button>' +
|
|
792
|
+
' <button id="fp-show-chat" class="red-ui-button red-ui-button-small" type="button" title="Chat"><i class="fa fa-comments"></i></button>' +
|
|
793
|
+
' <button id="fp-show-history" class="red-ui-button red-ui-button-small" type="button" title="Flight log — past conversations"><i class="fa fa-history"></i></button>' +
|
|
794
|
+
' </div>' +
|
|
795
|
+
' </div>' +
|
|
796
|
+
' </div>' +
|
|
797
|
+
' <div id="fp-chat-panel" class="fp-panel">' +
|
|
798
|
+
' <div id="fp-messages" class="fp-messages"></div>' +
|
|
799
|
+
' <div class="fp-compose">' +
|
|
800
|
+
' <div class="fp-action-bar">' +
|
|
801
|
+
' <div class="fp-action-group">' +
|
|
802
|
+
' <div id="fp-intents" class="fp-intents fp-intents-query"></div>' +
|
|
803
|
+
' </div>' +
|
|
804
|
+
' <div class="fp-action-divider"></div>' +
|
|
805
|
+
' <div class="fp-action-group">' +
|
|
806
|
+
' <div class="fp-intents fp-intents-execute">' +
|
|
807
|
+
' <button id="fp-document" class="red-ui-button red-ui-button-small fp-icon-btn fp-icon-btn-execute" type="button" title="Document — select nodes, optionally add notes, then hit Send to generate a comment-node explanation"><i class="fa fa-file-text-o"></i></button>' +
|
|
808
|
+
' <button id="fp-generate" class="red-ui-button red-ui-button-small fp-icon-btn fp-icon-btn-execute" type="button" title="Generate — describe a flow, then hit Send to draft it"><i class="fa fa-magic"></i></button>' +
|
|
809
|
+
' <button id="fp-modify" class="red-ui-button red-ui-button-small fp-icon-btn fp-icon-btn-execute" type="button" title="Modify — select node(s) in the MAIN window, describe the change, then hit Send"><i class="fa fa-pencil"></i></button>' +
|
|
810
|
+
' </div>' +
|
|
811
|
+
' </div>' +
|
|
812
|
+
' </div>' +
|
|
813
|
+
' <div class="fp-prompt-wrap">' +
|
|
814
|
+
' <textarea id="fp-prompt" placeholder="Select nodes in the main window for context, or just type a question…"></textarea>' +
|
|
815
|
+
' <div id="fp-prompt-resize" class="fp-resize-handle" title="Drag to resize"><i class="fa fa-arrows-v"></i></div>' +
|
|
816
|
+
' </div>' +
|
|
817
|
+
' <div class="fp-status-strip">' +
|
|
818
|
+
' <span id="fp-selection-status" class="fp-selection-status">No nodes selected</span>' +
|
|
819
|
+
' <a href="#" id="fp-preview-nodes" class="fp-preview-link fp-hidden" title="Open this from the main window to see the exact sanitized node JSON">Preview JSON</a>' +
|
|
820
|
+
' <span id="fp-size-status" class="fp-size-status fp-hidden"></span>' +
|
|
821
|
+
' <span id="fp-secrets-status" class="fp-secrets-status fp-hidden" title="Context may include node config and code. Don\'t send credentials or proprietary data. Local/private AI recommended.">⚠</span>' +
|
|
822
|
+
' <span id="fp-debug-status" class="fp-debug-status fp-hidden"></span>' +
|
|
823
|
+
' <a href="#" id="fp-debug-clear" class="fp-hidden" title="Remove all attached debug messages">✕</a>' +
|
|
824
|
+
' <span class="fp-status-spacer"></span>' +
|
|
825
|
+
' <div id="fp-provider-status">Provider: not loaded</div>' +
|
|
826
|
+
' <button id="fp-clear-prompt" class="red-ui-button" type="button" title="Clear prompt box">Clear</button>' +
|
|
827
|
+
' <button id="fp-send" class="red-ui-button red-ui-button-primary" type="button">Send</button>' +
|
|
828
|
+
' </div>' +
|
|
829
|
+
' </div>' +
|
|
830
|
+
' </div>' +
|
|
831
|
+
' <div id="fp-history-panel" class="fp-panel fp-hidden">' +
|
|
832
|
+
' <div class="fp-form">' +
|
|
833
|
+
' <div class="fp-settings-section">Flight log — past conversations</div>' +
|
|
834
|
+
' <div class="fp-consent-hint">Click a conversation to load it back into Chat — ' +
|
|
835
|
+
' new messages continue that conversation\'s memory. Deleting a conversation ' +
|
|
836
|
+
' removes its saved transcript permanently.</div>' +
|
|
837
|
+
' <div class="fp-settings-actions">' +
|
|
838
|
+
' <button id="fp-history-delete-all" class="red-ui-button red-ui-button-small" type="button" title="Delete all saved conversation transcripts permanently"><i class="fa fa-trash"></i> Delete all</button>' +
|
|
839
|
+
' </div>' +
|
|
840
|
+
' <div id="fp-history-list" class="fp-history-list"></div>' +
|
|
841
|
+
' </div>' +
|
|
842
|
+
' </div>' +
|
|
843
|
+
'</div>'
|
|
844
|
+
);
|
|
845
|
+
$("#fp-popout-root").append(content);
|
|
846
|
+
$root = content;
|
|
847
|
+
|
|
848
|
+
// Arming/disarming/Send dispatch reuse the EXISTING functions
|
|
849
|
+
// verbatim — see the comment above this function for why that's
|
|
850
|
+
// safe (pure local state except dispatchSend's final step, which
|
|
851
|
+
// checks isPopoutContext itself).
|
|
852
|
+
el("#fp-generate").on("click", function () { setArmedExecuteAction("generate"); });
|
|
853
|
+
el("#fp-document").on("click", function () { setArmedExecuteAction("document"); });
|
|
854
|
+
el("#fp-modify").on("click", function () { setArmedExecuteAction("modify"); });
|
|
855
|
+
el("#fp-send").on("click", function () { dispatchSend(); });
|
|
856
|
+
el("#fp-prompt").on("keydown", function (e) {
|
|
857
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
858
|
+
e.preventDefault();
|
|
859
|
+
dispatchSend();
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
el("#fp-clear-prompt").on("click", function () {
|
|
863
|
+
el("#fp-prompt").val("").focus();
|
|
864
|
+
});
|
|
865
|
+
// Clear Chat resets PARENT-owned conversationId/conversationHistory/
|
|
866
|
+
// activeBuildLoop — always relayed, never run locally (this window
|
|
867
|
+
// keeps no conversation state of its own to reset).
|
|
868
|
+
el("#fp-clear-chat").on("click", function () {
|
|
869
|
+
if (window.opener && !window.opener.closed) {
|
|
870
|
+
try { window.opener.postMessage({ event: "clearChat" }, location.origin); } catch (e) { /* ignore */ }
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
el("#fp-show-chat").on("click", showChat);
|
|
874
|
+
el("#fp-show-history").on("click", showHistory);
|
|
875
|
+
el("#fp-history-delete-all").on("click", deleteAllConversations);
|
|
876
|
+
el("#fp-recall").on("click", recallSearch);
|
|
877
|
+
el("#fp-debug-log").on("click", function () {
|
|
878
|
+
if (window.opener && !window.opener.closed) {
|
|
879
|
+
try { window.opener.postMessage({ event: "requestDebugBuffer" }, location.origin); } catch (e) { /* ignore */ }
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
el("#fp-debug-clear").on("click", function (ev) {
|
|
883
|
+
ev.preventDefault();
|
|
884
|
+
if (window.opener && !window.opener.closed) {
|
|
885
|
+
try { window.opener.postMessage({ event: "clearAttachedDebug" }, location.origin); } catch (e) { /* ignore */ }
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
el("#fp-preview-nodes").on("click", function (ev) {
|
|
889
|
+
ev.preventDefault();
|
|
890
|
+
addMessage("error", "Preview JSON isn't available in the pop-out yet — open it from the main window.");
|
|
891
|
+
});
|
|
892
|
+
bindPromptResize();
|
|
893
|
+
bindSlashAutocomplete(el("#fp-prompt"));
|
|
894
|
+
|
|
895
|
+
// Built-ins show immediately; loadSettings()'s fillSettings() call
|
|
896
|
+
// re-renders once custom intents (if any) are loaded, same
|
|
897
|
+
// two-step sequence initMainWindow uses.
|
|
898
|
+
renderIntents(el("#fp-intents"));
|
|
899
|
+
loadSettings();
|
|
900
|
+
|
|
901
|
+
window.addEventListener("message", function (evt) {
|
|
902
|
+
if (evt.origin !== location.origin) { return; }
|
|
903
|
+
var data = evt.data || {};
|
|
904
|
+
if (data.event === "initialSync") {
|
|
905
|
+
el("#fp-messages").html(data.html);
|
|
906
|
+
bindApplyButtons(el("#fp-messages"));
|
|
907
|
+
bindModifyApplyButtons(el("#fp-messages"));
|
|
908
|
+
bindBuildApplyButtons(el("#fp-messages"));
|
|
909
|
+
bindBuildFixApplyButtons(el("#fp-messages"));
|
|
910
|
+
bindStopLoopButton(el("#fp-messages"));
|
|
911
|
+
bindTabSwitching(el("#fp-messages"));
|
|
912
|
+
bindDebugAttachButtons(el("#fp-messages"));
|
|
913
|
+
scrollMessagesToBottom(true);
|
|
914
|
+
} else if (data.event === "appendMessage") {
|
|
915
|
+
el("#fp-messages").append(data.html);
|
|
916
|
+
bindApplyButtons(el("#fp-messages").children().last());
|
|
917
|
+
bindModifyApplyButtons(el("#fp-messages").children().last());
|
|
918
|
+
bindBuildApplyButtons(el("#fp-messages").children().last());
|
|
919
|
+
bindBuildFixApplyButtons(el("#fp-messages").children().last());
|
|
920
|
+
bindStopLoopButton(el("#fp-messages").children().last());
|
|
921
|
+
bindTabSwitching(el("#fp-messages").children().last());
|
|
922
|
+
bindDebugAttachButtons(el("#fp-messages").children().last());
|
|
923
|
+
scrollMessagesToBottom();
|
|
924
|
+
} else if (data.event === "removeMessage") {
|
|
925
|
+
el("#" + data.id).remove();
|
|
926
|
+
} else if (data.event === "clearMessages") {
|
|
927
|
+
el("#fp-messages").empty();
|
|
928
|
+
} else if (data.event === "debugBufferSnapshot") {
|
|
929
|
+
showChat();
|
|
930
|
+
var entries = data.entries || [];
|
|
931
|
+
var attachedSet = {};
|
|
932
|
+
(data.attachedIds || []).forEach(function (id) { attachedSet[id] = true; });
|
|
933
|
+
if (!entries.length) {
|
|
934
|
+
addMessage("assistant", "No debug messages captured yet. Trigger a flow with a Debug node wired to the sidebar, then try again.");
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
var $dbgMsg = $("<div>").addClass("fp-message fp-recall");
|
|
938
|
+
$("<div>").addClass("fp-label").text("DEBUG LOG").appendTo($dbgMsg);
|
|
939
|
+
$("<div>").addClass("fp-debug-warning").text("Debug payloads can contain credentials from connected " +
|
|
940
|
+
"systems. Common secret patterns are redacted automatically, but review before attaching.").appendTo($dbgMsg);
|
|
941
|
+
entries.forEach(function (entry) {
|
|
942
|
+
var $item = $("<div>").addClass("fp-recall-item");
|
|
943
|
+
var when = new Date(entry.timestamp).toLocaleTimeString();
|
|
944
|
+
var meta = when + " · " + entry.name + (entry.topic ? " · topic: " + entry.topic : "");
|
|
945
|
+
$("<div>").addClass("fp-recall-meta").text(meta).appendTo($item);
|
|
946
|
+
$("<div>").addClass("fp-recall-text").text(entry.previewValue).appendTo($item);
|
|
947
|
+
var already = !!attachedSet[entry.id];
|
|
948
|
+
var $btn = $("<button>").addClass("fp-recall-use red-ui-button red-ui-button-small")
|
|
949
|
+
.attr("type", "button").prop("disabled", already).text(already ? "Attached" : "Attach");
|
|
950
|
+
$btn.on("click", function () {
|
|
951
|
+
$btn.prop("disabled", true).text("Attached");
|
|
952
|
+
if (window.opener && !window.opener.closed) {
|
|
953
|
+
try { window.opener.postMessage({ event: "attachDebug", entryId: entry.id }, location.origin); } catch (e) { /* ignore */ }
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
$item.append($btn);
|
|
957
|
+
$dbgMsg.append($item);
|
|
958
|
+
});
|
|
959
|
+
el("#fp-messages").append($dbgMsg);
|
|
960
|
+
scrollMessagesToBottom();
|
|
961
|
+
} else if (data.event === "statusStripSync") {
|
|
962
|
+
var s = data.data || {};
|
|
963
|
+
el("#fp-selection-status").text(s.selectionText || "").toggleClass("fp-has-selection", !!s.hasSelection);
|
|
964
|
+
el("#fp-preview-nodes").toggleClass("fp-hidden", !s.previewVisible);
|
|
965
|
+
el("#fp-size-status").text(s.sizeText || "")
|
|
966
|
+
.toggleClass("fp-hidden", !!s.sizeHidden)
|
|
967
|
+
.toggleClass("fp-size-warn", !!s.sizeWarn)
|
|
968
|
+
.toggleClass("fp-size-high", !!s.sizeHigh);
|
|
969
|
+
el("#fp-secrets-status").toggleClass("fp-hidden", !!s.secretsHidden)
|
|
970
|
+
.toggleClass("fp-secrets-status-off", !!s.secretsOff)
|
|
971
|
+
.attr("title", s.secretsTitle || "");
|
|
972
|
+
el("#fp-debug-status").text(s.debugText || "").toggleClass("fp-hidden", !!s.debugHidden);
|
|
973
|
+
el("#fp-debug-clear").toggleClass("fp-hidden", !!s.debugHidden);
|
|
974
|
+
} else if (data.event === "recallResults") {
|
|
975
|
+
hidePending();
|
|
976
|
+
setBusy(false);
|
|
977
|
+
renderRecallResults(data.results || []);
|
|
978
|
+
} else if (data.event === "recallError") {
|
|
979
|
+
hidePending();
|
|
980
|
+
setBusy(false);
|
|
981
|
+
addMessage("error", data.msg || "Recall search failed.");
|
|
982
|
+
} else if (data.event === "conversationList") {
|
|
983
|
+
renderHistoryList(data.conversations || []);
|
|
984
|
+
} else if (data.event === "settingsLoaded") {
|
|
985
|
+
fillSettings(data.settings);
|
|
986
|
+
renderIntents(el("#fp-intents"));
|
|
987
|
+
updateSelectionStatus();
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// ---- Plugin registration --------------------------------------------
|
|
993
|
+
|
|
994
|
+
function initMainWindow() {
|
|
995
|
+
// If onadd has already run this session, do nothing. This is the
|
|
996
|
+
// primary fix for the duplicate #fp-root: the second invocation
|
|
997
|
+
// returns before building or inserting any DOM.
|
|
998
|
+
if (initialised) { return; }
|
|
999
|
+
initialised = true;
|
|
1000
|
+
|
|
1001
|
+
// Belt-and-suspenders: clear any orphan left by a prior version
|
|
1002
|
+
// before this guard existed. Harmless once the guard is in place.
|
|
1003
|
+
try { RED.sidebar.removeTab("flowpilot"); } catch (e) {}
|
|
1004
|
+
$("#fp-root").remove();
|
|
1005
|
+
|
|
1006
|
+
var content = $(
|
|
1007
|
+
'<div id="fp-root">' +
|
|
1008
|
+
' <div class="fp-header">' +
|
|
1009
|
+
' <div class="fp-header-row">' +
|
|
1010
|
+
' <div class="fp-logo">FP</div>' +
|
|
1011
|
+
' <div class="fp-heading">' +
|
|
1012
|
+
' <div class="fp-title">FlowPilot</div>' +
|
|
1013
|
+
' <div class="fp-subtitle">AI flow assistant</div>' +
|
|
1014
|
+
' </div>' +
|
|
1015
|
+
' <div class="fp-view-buttons">' +
|
|
1016
|
+
' <button id="fp-clear-chat" class="red-ui-button red-ui-button-small" type="button" title="Clear chat and start a fresh conversation (resets memory)"><i class="fa fa-eraser"></i></button>' +
|
|
1017
|
+
' <button id="fp-recall" class="red-ui-button red-ui-button-small" type="button" title="Recall: search earlier conversations for the text in the prompt box"><i class="fa fa-search"></i></button>' +
|
|
1018
|
+
' <button id="fp-debug-log" class="red-ui-button red-ui-button-small" type="button" title="Debug log: view recent Debug sidebar output and attach messages as context"><i class="fa fa-bug"></i></button>' +
|
|
1019
|
+
' <button id="fp-show-chat" class="red-ui-button red-ui-button-small" type="button" title="Chat"><i class="fa fa-comments"></i></button>' +
|
|
1020
|
+
' <button id="fp-show-history" class="red-ui-button red-ui-button-small" type="button" title="Flight log — past conversations"><i class="fa fa-history"></i></button>' +
|
|
1021
|
+
' <button id="fp-popout" class="red-ui-button red-ui-button-small" type="button" title="Open in a separate window (read-only mirror — v1)"><i class="fa fa-external-link"></i></button>' +
|
|
1022
|
+
' <button id="fp-show-settings" class="red-ui-button red-ui-button-small" type="button" title="Settings"><i class="fa fa-cog"></i></button>' +
|
|
1023
|
+
' </div>' +
|
|
1024
|
+
' </div>' +
|
|
1025
|
+
' </div>' +
|
|
1026
|
+
|
|
1027
|
+
' <div id="fp-chat-panel" class="fp-panel">' +
|
|
1028
|
+
' <div id="fp-dev-banner" class="fp-warning">' +
|
|
1029
|
+
' <strong>Development/test only.</strong> ' +
|
|
1030
|
+
' Anything you send may leave this Node-RED instance. ' +
|
|
1031
|
+
' Don\'t include credentials or proprietary data; local/private AI recommended.' +
|
|
1032
|
+
' </div>' +
|
|
1033
|
+
' <div id="fp-messages" class="fp-messages"></div>' +
|
|
1034
|
+
' <div class="fp-compose">' +
|
|
1035
|
+
' <div class="fp-action-bar">' +
|
|
1036
|
+
' <div class="fp-action-group">' +
|
|
1037
|
+
' <div id="fp-intents" class="fp-intents fp-intents-query"></div>' +
|
|
1038
|
+
' </div>' +
|
|
1039
|
+
' <div class="fp-action-divider"></div>' +
|
|
1040
|
+
' <div class="fp-action-group">' +
|
|
1041
|
+
' <div class="fp-intents fp-intents-execute">' +
|
|
1042
|
+
' <button id="fp-document" class="red-ui-button red-ui-button-small fp-icon-btn fp-icon-btn-execute" type="button" title="Document — select nodes, optionally add notes, then hit Send to generate a comment-node explanation"><i class="fa fa-file-text-o"></i></button>' +
|
|
1043
|
+
' <button id="fp-generate" class="red-ui-button red-ui-button-small fp-icon-btn fp-icon-btn-execute" type="button" title="Generate — describe a flow, then hit Send to draft it"><i class="fa fa-magic"></i></button>' +
|
|
1044
|
+
' <button id="fp-modify" class="red-ui-button red-ui-button-small fp-icon-btn fp-icon-btn-execute" type="button" title="Modify — select node(s), describe the change, then hit Send"><i class="fa fa-pencil"></i></button>' +
|
|
1045
|
+
' </div>' +
|
|
1046
|
+
' </div>' +
|
|
1047
|
+
' </div>' +
|
|
1048
|
+
' <div class="fp-prompt-wrap">' +
|
|
1049
|
+
' <textarea id="fp-prompt" placeholder="Select nodes for context, or just type a question…"></textarea>' +
|
|
1050
|
+
' <div id="fp-prompt-resize" class="fp-resize-handle" title="Drag to resize"><i class="fa fa-arrows-v"></i></div>' +
|
|
1051
|
+
' </div>' +
|
|
1052
|
+
' <div class="fp-status-strip">' +
|
|
1053
|
+
' <span id="fp-selection-status" class="fp-selection-status">No nodes selected</span>' +
|
|
1054
|
+
' <a href="#" id="fp-preview-nodes" class="fp-preview-link fp-hidden" title="Show the exact sanitized node JSON that will be sent">Preview JSON</a>' +
|
|
1055
|
+
' <span id="fp-size-status" class="fp-size-status fp-hidden"></span>' +
|
|
1056
|
+
' <span id="fp-secrets-status" class="fp-secrets-status fp-hidden" title="Context may include node config and code. Don\'t send credentials or proprietary data. Local/private AI recommended.">⚠</span>' +
|
|
1057
|
+
' <span id="fp-debug-status" class="fp-debug-status fp-hidden"></span>' +
|
|
1058
|
+
' <span class="fp-status-spacer"></span>' +
|
|
1059
|
+
' <div id="fp-provider-status">Provider: not loaded</div>' +
|
|
1060
|
+
' <button id="fp-clear-prompt" class="red-ui-button" type="button" title="Clear prompt box">Clear</button>' +
|
|
1061
|
+
' <button id="fp-send" class="red-ui-button red-ui-button-primary" type="button">Send</button>' +
|
|
1062
|
+
' </div>' +
|
|
1063
|
+
' </div>' +
|
|
1064
|
+
' </div>' +
|
|
1065
|
+
|
|
1066
|
+
' <div id="fp-settings-panel" class="fp-panel fp-hidden">' +
|
|
1067
|
+
' <div class="fp-form">' +
|
|
1068
|
+
|
|
1069
|
+
' <details class="fp-settings-group" open>' +
|
|
1070
|
+
' <summary title="Hangar — where your AI providers are configured">Providers</summary>' +
|
|
1071
|
+
' <div class="fp-warning">' +
|
|
1072
|
+
' <strong>Provider settings.</strong><br>' +
|
|
1073
|
+
' Stored locally under the Node-RED user directory in <code>flowpilot/settings.json</code>.' +
|
|
1074
|
+
' </div>' +
|
|
1075
|
+
' <label>Active provider</label>' +
|
|
1076
|
+
' <select id="fp-provider-select"></select>' +
|
|
1077
|
+
' <div class="fp-provider-actions">' +
|
|
1078
|
+
' <button id="fp-add-provider" class="red-ui-button red-ui-button-small" type="button">+ Add</button>' +
|
|
1079
|
+
' <button id="fp-remove-provider" class="red-ui-button red-ui-button-small" type="button">Remove</button>' +
|
|
1080
|
+
' </div>' +
|
|
1081
|
+
' <label>Provider Name</label>' +
|
|
1082
|
+
' <input id="fp-provider-name" type="text" placeholder="LocalAI">' +
|
|
1083
|
+
' <label>Base URL</label>' +
|
|
1084
|
+
' <input id="fp-base-url" type="text" placeholder="http://localhost:8080">' +
|
|
1085
|
+
' <label>API Key</label>' +
|
|
1086
|
+
' <input id="fp-api-key" type="password" placeholder="Optional">' +
|
|
1087
|
+
' <label>Model</label>' +
|
|
1088
|
+
' <input id="fp-model" type="text" list="fp-model-options" placeholder="Model name for this provider">' +
|
|
1089
|
+
' <datalist id="fp-model-options"></datalist>' +
|
|
1090
|
+
' <div id="fp-models-hint" class="fp-consent-hint fp-hidden"></div>' +
|
|
1091
|
+
' <label>Temperature</label>' +
|
|
1092
|
+
' <input id="fp-temperature" type="number" min="0" max="2" step="0.1" placeholder="0.2">' +
|
|
1093
|
+
' <div class="fp-consent-hint">Controls randomness. Lower (e.g. 0.2) is more ' +
|
|
1094
|
+
' focused and consistent; higher is more creative and varied. 0.2 is a good ' +
|
|
1095
|
+
' default for flow generation.</div>' +
|
|
1096
|
+
' <div class="fp-settings-actions">' +
|
|
1097
|
+
' <button id="fp-test-provider" class="red-ui-button" type="button" title="Pre-flight check — save and send a quick test to this provider">Pre-flight check</button>' +
|
|
1098
|
+
' <button id="fp-refresh-models" class="red-ui-button" type="button" title="Save settings, then fetch this provider\'s model list via GET /v1/models">Refresh models</button>' +
|
|
1099
|
+
' </div>' +
|
|
1100
|
+
' </details>' +
|
|
1101
|
+
|
|
1102
|
+
' <details class="fp-settings-group">' +
|
|
1103
|
+
' <summary>Behavior</summary>' +
|
|
1104
|
+
' <div class="fp-settings-section">System Prompt</div>' +
|
|
1105
|
+
' <textarea id="fp-system-prompt"></textarea>' +
|
|
1106
|
+
' <div class="fp-settings-actions">' +
|
|
1107
|
+
' <button id="fp-reset-system-prompt" class="red-ui-button" type="button" title="Replace this text with FlowPilot\'s current built-in default — useful after an update adds new instructions">Reset to default</button>' +
|
|
1108
|
+
' </div>' +
|
|
1109
|
+
|
|
1110
|
+
' <div class="fp-settings-section">Personality</div>' +
|
|
1111
|
+
' <label>Persona intensity: <span id="fp-persona-value">3</span>/10</label>' +
|
|
1112
|
+
' <input id="fp-persona-intensity" type="range" min="1" max="10" step="1">' +
|
|
1113
|
+
' <div id="fp-persona-label" class="fp-consent-hint"></div>' +
|
|
1114
|
+
' <div class="fp-consent-hint">Chat only. Scales the AI\'s voice at greetings, ' +
|
|
1115
|
+
' capability questions, and brief transitions — 1 is a plain Node-RED engineer, ' +
|
|
1116
|
+
' 10 is a comically over-the-top airline captain who happens to be a Node-RED ' +
|
|
1117
|
+
' expert. Explanations, troubleshooting, and errors always stay plain regardless ' +
|
|
1118
|
+
' of this setting. Generate, Document, and Modify are unaffected.</div>' +
|
|
1119
|
+
|
|
1120
|
+
' <div class="fp-settings-section">Conversation memory</div>' +
|
|
1121
|
+
' <label>Remember last N exchanges</label>' +
|
|
1122
|
+
' <input id="fp-history-max" type="number" min="0" step="1" placeholder="10">' +
|
|
1123
|
+
' <div class="fp-consent-hint">How much of the visible chat is sent back to the ' +
|
|
1124
|
+
' AI as conversation history with each request (0 = no memory). Older messages ' +
|
|
1125
|
+
' drop off and the AI is told when that happens. Clear chat (eraser icon) ' +
|
|
1126
|
+
' resets this entirely.</div>' +
|
|
1127
|
+
' <label class="fp-checkbox-row">' +
|
|
1128
|
+
' <input id="fp-streaming-enabled" type="checkbox"> ' +
|
|
1129
|
+
' Stream chat replies as they generate' +
|
|
1130
|
+
' </label>' +
|
|
1131
|
+
' <div class="fp-consent-hint">Applies to every mode (Chat, Generate, ' +
|
|
1132
|
+
' Document, Modify, Build) the same way — but only when the active ' +
|
|
1133
|
+
' provider doesn\'t support tool/function calling. Tool-capable ' +
|
|
1134
|
+
' providers always wait for the full response regardless of this ' +
|
|
1135
|
+
' setting, since the read-tool loop has no streaming variant.</div>' +
|
|
1136
|
+
|
|
1137
|
+
' <div class="fp-settings-section">Request timeout</div>' +
|
|
1138
|
+
' <label>Give up after (seconds)</label>' +
|
|
1139
|
+
' <input id="fp-request-timeout" type="number" min="5" step="5" placeholder="180">' +
|
|
1140
|
+
' <div class="fp-consent-hint">How long to wait for a provider response before ' +
|
|
1141
|
+
' giving up. Raise this if you\'re running a large local model on slow hardware ' +
|
|
1142
|
+
' (e.g. Ollama without a GPU) and seeing timeout errors.</div>' +
|
|
1143
|
+
|
|
1144
|
+
' <div class="fp-settings-section">Agentic build loop</div>' +
|
|
1145
|
+
' <label>Max build/fix attempts</label>' +
|
|
1146
|
+
' <input id="fp-agent-loop-max-iterations" type="number" min="1" max="20" step="1" placeholder="5">' +
|
|
1147
|
+
' <div class="fp-consent-hint">When using the deploy-verify loop, how many build → deploy → ' +
|
|
1148
|
+
' test → fix cycles to try before stopping with an honest "couldn\'t fully verify" ' +
|
|
1149
|
+
' instead of proposing another fix.</div>' +
|
|
1150
|
+
' <label class="fp-checkbox-row">' +
|
|
1151
|
+
' <input id="fp-loop-hold-step" type="checkbox"> ' +
|
|
1152
|
+
' Hold at each loop checkpoint (wait for confirmation before AI review)' +
|
|
1153
|
+
' </label>' +
|
|
1154
|
+
' <div class="fp-consent-hint">When checked, the loop pauses after auto-attaching debug ' +
|
|
1155
|
+
' output and shows a "Continue review / Stop" prompt before sending to the AI. ' +
|
|
1156
|
+
' When unchecked (default), the loop advances automatically.</div>' +
|
|
1157
|
+
|
|
1158
|
+
' <div class="fp-settings-section">Custom intent buttons</div>' +
|
|
1159
|
+
' <div class="fp-consent-hint">Add your own one-click prompt buttons. ' +
|
|
1160
|
+
' They appear next to the built-in ones above the prompt.</div>' +
|
|
1161
|
+
' <div id="fp-custom-intents" class="fp-custom-intents"></div>' +
|
|
1162
|
+
' <label>New button label</label>' +
|
|
1163
|
+
' <input id="fp-new-intent-label" type="text" placeholder="e.g. Security review">' +
|
|
1164
|
+
' <label>Instruction text</label>' +
|
|
1165
|
+
' <textarea id="fp-new-intent-text" placeholder="What should this button ask the AI to do?"></textarea>' +
|
|
1166
|
+
' <div class="fp-settings-actions">' +
|
|
1167
|
+
' <button id="fp-add-intent" class="red-ui-button" type="button">Add button</button>' +
|
|
1168
|
+
' </div>' +
|
|
1169
|
+
' </details>' +
|
|
1170
|
+
|
|
1171
|
+
' <details class="fp-settings-group">' +
|
|
1172
|
+
' <summary>Context & Safety</summary>' +
|
|
1173
|
+
' <div class="fp-settings-section">Context size warnings</div>' +
|
|
1174
|
+
' <label>Warn above (estimated tokens)</label>' +
|
|
1175
|
+
' <input id="fp-warn-tokens" type="number" min="0" step="500" placeholder="4000">' +
|
|
1176
|
+
' <label>Strong warning above (estimated tokens)</label>' +
|
|
1177
|
+
' <input id="fp-high-tokens" type="number" min="0" step="500" placeholder="8000">' +
|
|
1178
|
+
|
|
1179
|
+
' <div class="fp-settings-section">Risk warnings</div>' +
|
|
1180
|
+
' <label class="fp-checkbox-row">' +
|
|
1181
|
+
' <input id="fp-suppress-warnings" type="checkbox"> ' +
|
|
1182
|
+
' Hide the recurring credentials/size warning bar' +
|
|
1183
|
+
' </label>' +
|
|
1184
|
+
' <div class="fp-consent-hint">To hide the warning, check the box and type ' +
|
|
1185
|
+
' <strong>I understand the risk</strong> below. ' +
|
|
1186
|
+
' Anything you send may leave this Node-RED instance.</div>' +
|
|
1187
|
+
' <input id="fp-suppress-confirm" type="text" placeholder="Type: I understand the risk">' +
|
|
1188
|
+
|
|
1189
|
+
' <div class="fp-settings-section">Redaction</div>' +
|
|
1190
|
+
' <label class="fp-checkbox-row">' +
|
|
1191
|
+
' <input id="fp-redaction-disabled" type="checkbox"> ' +
|
|
1192
|
+
' Disable secret-shaped-value redaction' +
|
|
1193
|
+
' </label>' +
|
|
1194
|
+
' <div class="fp-consent-hint">By default, values that look like secrets ' +
|
|
1195
|
+
' (passwords, tokens, API keys) are replaced with a placeholder before sending. ' +
|
|
1196
|
+
' This opt-out is intended for environments using local or private AIs. Debug ' +
|
|
1197
|
+
' nodes and context may share confidential data, including secret keys, and ' +
|
|
1198
|
+
' credentials. Use at your own risk! Node-RED\'s own credential store is never ' +
|
|
1199
|
+
' sent either way. To disable, check the box and type ' +
|
|
1200
|
+
' <strong>disable redaction</strong> below.</div>' +
|
|
1201
|
+
' <input id="fp-redaction-confirm" type="text" placeholder="Type: disable redaction">' +
|
|
1202
|
+
' </details>' +
|
|
1203
|
+
|
|
1204
|
+
' <div class="fp-settings-actions">' +
|
|
1205
|
+
' <button id="fp-save-settings" class="red-ui-button red-ui-button-primary" type="button">Save settings</button>' +
|
|
1206
|
+
' <span id="fp-save-status" class="fp-save-status fp-hidden"></span>' +
|
|
1207
|
+
' </div>' +
|
|
1208
|
+
' </div>' +
|
|
1209
|
+
' </div>' +
|
|
1210
|
+
|
|
1211
|
+
' <div id="fp-history-panel" class="fp-panel fp-hidden">' +
|
|
1212
|
+
' <div class="fp-form">' +
|
|
1213
|
+
' <div class="fp-settings-section">Flight log — past conversations</div>' +
|
|
1214
|
+
' <div class="fp-consent-hint">Click a conversation to load it back into Chat — ' +
|
|
1215
|
+
' new messages continue that conversation\'s memory. Deleting a conversation ' +
|
|
1216
|
+
' removes its saved transcript permanently.</div>' +
|
|
1217
|
+
' <div class="fp-settings-actions">' +
|
|
1218
|
+
' <button id="fp-history-delete-all" class="red-ui-button red-ui-button-small" type="button" title="Delete all saved conversation transcripts permanently"><i class="fa fa-trash"></i> Delete all</button>' +
|
|
1219
|
+
' </div>' +
|
|
1220
|
+
' <div id="fp-history-list" class="fp-history-list"></div>' +
|
|
1221
|
+
' </div>' +
|
|
1222
|
+
' </div>' +
|
|
1223
|
+
'</div>'
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
// Keep the closure reference to the *inserted* content.
|
|
1227
|
+
$root = content;
|
|
1228
|
+
|
|
1229
|
+
// ---- Settings accordion: only one section expanded at a time.
|
|
1230
|
+
// Bind once on the parent (event delegation) rather than per-summary.
|
|
1231
|
+
content.find(".fp-settings-group > summary").on("click", function () {
|
|
1232
|
+
// Close all other details elements
|
|
1233
|
+
content.find(".fp-settings-group").not($(this).closest(".fp-settings-group")).removeAttr("open");
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
// ---- Bind events here, after the DOM exists. No inline onclick.
|
|
1237
|
+
content.find("#fp-show-chat").on("click", showChat);
|
|
1238
|
+
content.find("#fp-show-settings").on("click", showSettings);
|
|
1239
|
+
content.find("#fp-show-history").on("click", showHistory);
|
|
1240
|
+
content.find("#fp-popout").on("click", openPopout);
|
|
1241
|
+
content.find("#fp-history-delete-all").on("click", deleteAllConversations);
|
|
1242
|
+
content.find("#fp-clear-chat").on("click", clearChat);
|
|
1243
|
+
content.find("#fp-recall").on("click", recallSearch);
|
|
1244
|
+
content.find("#fp-debug-log").on("click", showDebugMessages);
|
|
1245
|
+
content.find("#fp-preview-nodes").on("click", function (ev) {
|
|
1246
|
+
ev.preventDefault();
|
|
1247
|
+
showJsonPreview("Node JSON preview — exactly what will be sent", resolveCurrentSelectionContext());
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
// Subscribe once to the same RED.comms "debug" topic the built-in
|
|
1251
|
+
// Debug sidebar uses, to buffer recent messages locally for
|
|
1252
|
+
// optional attachment (showDebugMessages/attachDebugContext).
|
|
1253
|
+
// Nothing is sent to the backend until the user explicitly
|
|
1254
|
+
// attaches a message and sends a request.
|
|
1255
|
+
try { RED.comms.subscribe("debug", onDebugMessage); } catch (e) { /* comms unavailable */ }
|
|
1256
|
+
|
|
1257
|
+
// Track whether the user is scrolled to the bottom of the chat,
|
|
1258
|
+
// so "Cruising…"/streaming updates only auto-follow when they
|
|
1259
|
+
// haven't scrolled up to read earlier messages.
|
|
1260
|
+
content.find("#fp-messages").on("scroll", function () {
|
|
1261
|
+
fpChatSnappedToBottom = (this.scrollHeight - this.scrollTop - this.clientHeight) <= FP_SCROLL_SNAP_PX;
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
// Delegated: code blocks are injected as raw HTML (renderMarkdown
|
|
1265
|
+
// -> .html()), so individual buttons never exist at bind time.
|
|
1266
|
+
content.find("#fp-messages").on("click", ".fp-code-copy", function () {
|
|
1267
|
+
var $btn = $(this);
|
|
1268
|
+
var $pre = el("#" + $btn.attr("data-code-id"));
|
|
1269
|
+
if ($pre.length) { copyToClipboard($btn, $pre.text()); }
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
content.find("#fp-clear-prompt").on("click", function () {
|
|
1273
|
+
el("#fp-prompt").val("").focus();
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
content.find("#fp-send").on("click", function () {
|
|
1277
|
+
dispatchSend();
|
|
1278
|
+
});
|
|
1279
|
+
content.find("#fp-generate").on("click", function () {
|
|
1280
|
+
setArmedExecuteAction("generate");
|
|
1281
|
+
});
|
|
1282
|
+
content.find("#fp-document").on("click", function () {
|
|
1283
|
+
setArmedExecuteAction("document");
|
|
1284
|
+
});
|
|
1285
|
+
content.find("#fp-modify").on("click", function () {
|
|
1286
|
+
setArmedExecuteAction("modify");
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
// Close the "more query actions" dropdown on any click outside it.
|
|
1290
|
+
// Bound once here (onadd guard above) rather than per-render.
|
|
1291
|
+
$(document).on("click.fpIntentMenu", function () {
|
|
1292
|
+
$(".fp-intent-menu").addClass("fp-hidden");
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
// Provider management.
|
|
1296
|
+
content.find("#fp-provider-select").on("change", function () {
|
|
1297
|
+
switchProvider($(this).val());
|
|
1298
|
+
});
|
|
1299
|
+
content.find("#fp-add-provider").on("click", function () { addProvider(); });
|
|
1300
|
+
content.find("#fp-remove-provider").on("click", function () { removeProvider(); });
|
|
1301
|
+
content.find("#fp-test-provider").on("click", function () { testProvider(); });
|
|
1302
|
+
content.find("#fp-refresh-models").on("click", function () { refreshModels(); });
|
|
1303
|
+
content.find("#fp-reset-system-prompt").on("click", function () { resetSystemPrompt(); });
|
|
1304
|
+
content.find("#fp-persona-intensity").on("input", updatePersonaLabel);
|
|
1305
|
+
|
|
1306
|
+
// Re-enable Test provider live as the user types a model.
|
|
1307
|
+
content.find("#fp-model").on("input", function () {
|
|
1308
|
+
el("#fp-test-provider").prop("disabled", !($(this).val() || "").trim());
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
content.find("#fp-save-settings").on("click", function () {
|
|
1312
|
+
saveSettings(null, true);
|
|
1313
|
+
});
|
|
1314
|
+
content.find("#fp-add-intent").on("click", function () {
|
|
1315
|
+
addCustomIntent();
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
// Enter-to-send (Shift+Enter for newline) on the prompt box.
|
|
1319
|
+
content.find("#fp-prompt").on("keydown", function (e) {
|
|
1320
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
1321
|
+
e.preventDefault();
|
|
1322
|
+
dispatchSend();
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
// /demo's "breathe" cue on Send is meant to draw the eye right
|
|
1327
|
+
// after the prompt is filled in — once the user starts editing
|
|
1328
|
+
// it themselves, the nudge has done its job.
|
|
1329
|
+
content.find("#fp-prompt").on("input", function () {
|
|
1330
|
+
el("#fp-send").removeClass("fp-send-breathe");
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
bindPromptResize();
|
|
1334
|
+
bindSlashAutocomplete(el("#fp-prompt"));
|
|
1335
|
+
|
|
1336
|
+
// Live selection indicator. RED.events fires this whenever the
|
|
1337
|
+
// user changes what's selected on the canvas. While armed, a
|
|
1338
|
+
// new non-empty selection refreshes the pinned context (an empty
|
|
1339
|
+
// selection does NOT clear it — that's what lets follow-ups skip
|
|
1340
|
+
// reselection).
|
|
1341
|
+
RED.events.on("view:selection-changed", function () {
|
|
1342
|
+
if (armedExecuteAction) { pinCurrentSelection(); }
|
|
1343
|
+
updateSelectionStatus();
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
// /build loop: "deploy" only fires on a SUCCESSFUL deploy (no
|
|
1347
|
+
// native "deploy failed" event exists — a failed deploy just
|
|
1348
|
+
// shows the user a notification, nothing programmatic), so a
|
|
1349
|
+
// failed deploy simply leaves the loop waiting at "apply"
|
|
1350
|
+
// rather than advancing — exactly what we want.
|
|
1351
|
+
RED.events.on("deploy", function () {
|
|
1352
|
+
if (activeBuildLoop && activeBuildLoop.waypoint === "apply") {
|
|
1353
|
+
activeBuildLoop.waypoint = "attach";
|
|
1354
|
+
renderLoopStepper(activeBuildLoop);
|
|
1355
|
+
// Start a timer so flows with no debug nodes (e.g. HTTP
|
|
1356
|
+
// endpoints) don't leave the loop stuck silently waiting.
|
|
1357
|
+
buildLoopNoDebugTimer = setTimeout(function () {
|
|
1358
|
+
buildLoopNoDebugTimer = null;
|
|
1359
|
+
if (!activeBuildLoop || activeBuildLoop.waypoint !== "attach") { return; }
|
|
1360
|
+
addMessage("assistant",
|
|
1361
|
+
"No debug output detected yet. If this flow doesn't produce " +
|
|
1362
|
+
"automatic debug output (e.g. it's an HTTP endpoint), trigger " +
|
|
1363
|
+
"it and paste the response here — or describe what happened " +
|
|
1364
|
+
"and I'll review from that.");
|
|
1365
|
+
}, BUILD_LOOP_NO_DEBUG_TIMEOUT_MS);
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
// Pop-out: closing/refreshing the main editor also closes the
|
|
1370
|
+
// detached mirror, so it can never be left open and orphaned —
|
|
1371
|
+
// same as 21-debug.html's beforeunload handling.
|
|
1372
|
+
$(window).on("beforeunload", function () {
|
|
1373
|
+
if (popoutWindow) {
|
|
1374
|
+
try { popoutWindow.close(); } catch (e) { /* ignore */ }
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
// Pop-out child->parent intents: full Send dispatch
|
|
1379
|
+
// ("dispatchSend" — mode + prompt text, the pop-out's own
|
|
1380
|
+
// dispatchSend() relays here instead of calling
|
|
1381
|
+
// generate/document/modify/build/chat locally, since only the
|
|
1382
|
+
// main window has live RED.* context), "/compact"+"/expand"
|
|
1383
|
+
// ("runSlashCommand" — same reason), "import this already-
|
|
1384
|
+
// reviewed Generate/Document flow", "apply this already-
|
|
1385
|
+
// reviewed Modify diff", the /build loop's own apply/fix/stop
|
|
1386
|
+
// intents, and "clear the real conversation" (Clear Chat).
|
|
1387
|
+
// None of these run anything in the pop-out's own window — all
|
|
1388
|
+
// just ask the main window to do exactly what the equivalent
|
|
1389
|
+
// sidebar action would. Replies/confirmations reach the
|
|
1390
|
+
// pop-out via the existing #fp-messages relay, same as any
|
|
1391
|
+
// other new message.
|
|
1392
|
+
window.addEventListener("message", function (evt) {
|
|
1393
|
+
if (evt.origin !== location.origin) { return; }
|
|
1394
|
+
if (evt.source !== popoutWindow) { return; }
|
|
1395
|
+
var data = evt.data || {};
|
|
1396
|
+
if (data.event === "dispatchSend" && data.prompt) {
|
|
1397
|
+
el("#fp-prompt").val(data.prompt);
|
|
1398
|
+
if (data.mode === "generate") { generate(); }
|
|
1399
|
+
else if (data.mode === "build") { buildFlow(); }
|
|
1400
|
+
else if (data.mode === "document") { documentFlow(); }
|
|
1401
|
+
else if (data.mode === "modify") { modifyFlow(); }
|
|
1402
|
+
else { send("chat"); }
|
|
1403
|
+
} else if (data.event === "runSlashCommand" && data.command) {
|
|
1404
|
+
handleSlashCommand(data.command);
|
|
1405
|
+
} else if (data.event === "applyGenerated" && Array.isArray(data.flow)) {
|
|
1406
|
+
importGeneratedFlow(data.flow);
|
|
1407
|
+
} else if (data.event === "applyModify" && data.data) {
|
|
1408
|
+
var ad = data.data;
|
|
1409
|
+
var idMap = {};
|
|
1410
|
+
if (Array.isArray(ad.newNodes) && ad.newNodes.length) {
|
|
1411
|
+
idMap = applyInsertions(ad.newNodes, ad.newWires || [], ad.existingNodeIds || []) || {};
|
|
1412
|
+
}
|
|
1413
|
+
if (ad.hasMutations) {
|
|
1414
|
+
applyModifications(ad.nodeDiffs || [], ad.removeNodes || [], null, idMap);
|
|
1415
|
+
}
|
|
1416
|
+
if (Array.isArray(ad.newGroups) && ad.newGroups.length) {
|
|
1417
|
+
applyGroupChanges(ad.newGroups, idMap);
|
|
1418
|
+
}
|
|
1419
|
+
} else if (data.event === "applyBuild" && data.data && Array.isArray(data.data.flow)) {
|
|
1420
|
+
var bd = data.data;
|
|
1421
|
+
importGeneratedFlow(bd.flow, function (importResult) {
|
|
1422
|
+
startBuildLoop(bd.goal, bd.flow, importResult);
|
|
1423
|
+
});
|
|
1424
|
+
} else if (data.event === "applyBuildFix" && data.data) {
|
|
1425
|
+
var bf = data.data;
|
|
1426
|
+
var fixIdMap = {};
|
|
1427
|
+
if (Array.isArray(bf.newNodes) && bf.newNodes.length) {
|
|
1428
|
+
fixIdMap = applyInsertions(bf.newNodes, bf.newWires || [], bf.existingNodeIds || []) || {};
|
|
1429
|
+
}
|
|
1430
|
+
if (bf.hasMutations) {
|
|
1431
|
+
applyBuildLoopFix(bf.nodeDiffs || [], bf.removeNodes || [], fixIdMap, !!bf.capReached);
|
|
1432
|
+
}
|
|
1433
|
+
if (Array.isArray(bf.newGroups) && bf.newGroups.length) {
|
|
1434
|
+
applyGroupChanges(bf.newGroups, fixIdMap);
|
|
1435
|
+
}
|
|
1436
|
+
} else if (data.event === "stopBuildLoop") {
|
|
1437
|
+
stopBuildLoop("Build loop stopped — applied nodes remain as-is.");
|
|
1438
|
+
} else if (data.event === "clearChat") {
|
|
1439
|
+
clearChat();
|
|
1440
|
+
} else if (data.event === "requestDebugBuffer") {
|
|
1441
|
+
var snapshot = debugMessageBuffer.slice();
|
|
1442
|
+
var attachedIds = attachedDebugMessages.map(function (e) { return e.id; });
|
|
1443
|
+
try { popoutWindow.postMessage({ event: "debugBufferSnapshot", entries: snapshot, attachedIds: attachedIds }, location.origin); } catch (e) { /* ignore */ }
|
|
1444
|
+
} else if (data.event === "attachDebug" && data.entryId) {
|
|
1445
|
+
var found = null;
|
|
1446
|
+
for (var i = 0; i < debugMessageBuffer.length; i++) {
|
|
1447
|
+
if (debugMessageBuffer[i].id === data.entryId) { found = debugMessageBuffer[i]; break; }
|
|
1448
|
+
}
|
|
1449
|
+
if (found) {
|
|
1450
|
+
var alreadyIn = attachedDebugMessages.some(function (e) { return e.id === data.entryId; });
|
|
1451
|
+
if (!alreadyIn) { attachedDebugMessages.push(found); updateDebugStatus(); }
|
|
1452
|
+
}
|
|
1453
|
+
} else if (data.event === "useRecallItem") {
|
|
1454
|
+
if (data.user) { conversationHistory.push({ role: "user", content: String(data.user) }); }
|
|
1455
|
+
if (data.assistant) { conversationHistory.push({ role: "assistant", content: String(data.assistant) }); }
|
|
1456
|
+
updateSelectionStatus();
|
|
1457
|
+
} else if (data.event === "loadConversation" && data.id) {
|
|
1458
|
+
loadConversation(data.id);
|
|
1459
|
+
} else if (data.event === "requestRecallSearch" && data.query) {
|
|
1460
|
+
ajaxJson("POST", "flowpilot/recall", { query: data.query, conversationId: conversationId }, function (result) {
|
|
1461
|
+
try { popoutWindow.postMessage({ event: "recallResults", results: result.results || [] }, location.origin); } catch (e) { /* ignore */ }
|
|
1462
|
+
}, function (msg) {
|
|
1463
|
+
try { popoutWindow.postMessage({ event: "recallError", msg: msg }, location.origin); } catch (e) { /* ignore */ }
|
|
1464
|
+
});
|
|
1465
|
+
} else if (data.event === "requestConversationList") {
|
|
1466
|
+
ajaxJson("GET", "flowpilot/conversations", null, function (result) {
|
|
1467
|
+
try { popoutWindow.postMessage({ event: "conversationList", conversations: result.conversations || [] }, location.origin); } catch (e) { /* ignore */ }
|
|
1468
|
+
}, function () {
|
|
1469
|
+
try { popoutWindow.postMessage({ event: "conversationList", conversations: [] }, location.origin); } catch (e) { /* ignore */ }
|
|
1470
|
+
});
|
|
1471
|
+
} else if (data.event === "deleteConversation" && data.id) {
|
|
1472
|
+
ajaxJson("DELETE", "flowpilot/conversations/" + encodeURIComponent(data.id), null, function () {
|
|
1473
|
+
ajaxJson("GET", "flowpilot/conversations", null, function (result) {
|
|
1474
|
+
try { popoutWindow.postMessage({ event: "conversationList", conversations: result.conversations || [] }, location.origin); } catch (e) { /* ignore */ }
|
|
1475
|
+
});
|
|
1476
|
+
});
|
|
1477
|
+
} else if (data.event === "deleteAllConversations") {
|
|
1478
|
+
ajaxJson("DELETE", "flowpilot/conversations", null, function () {
|
|
1479
|
+
try { popoutWindow.postMessage({ event: "conversationList", conversations: [] }, location.origin); } catch (e) { /* ignore */ }
|
|
1480
|
+
});
|
|
1481
|
+
} else if (data.event === "clearAttachedDebug") {
|
|
1482
|
+
attachedDebugMessages = [];
|
|
1483
|
+
updateDebugStatus();
|
|
1484
|
+
} else if (data.event === "requestSettings") {
|
|
1485
|
+
ajaxJson("GET", "flowpilot/settings", null, function (result) {
|
|
1486
|
+
try { popoutWindow.postMessage({ event: "settingsLoaded", settings: result }, location.origin); } catch (e) { /* ignore */ }
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
// Build intent buttons from built-in INTENTS plus any user-defined
|
|
1492
|
+
// customIntents (loaded from settings). Rebuilt after settings load
|
|
1493
|
+
// /save via renderIntents() so new custom buttons appear without a
|
|
1494
|
+
// reload.
|
|
1495
|
+
renderIntents(content.find("#fp-intents"));
|
|
1496
|
+
|
|
1497
|
+
RED.sidebar.addTab({
|
|
1498
|
+
id: "flowpilot",
|
|
1499
|
+
label: "FlowPilot",
|
|
1500
|
+
name: "FlowPilot",
|
|
1501
|
+
iconClass: "fa fa-paper-plane",
|
|
1502
|
+
content: content
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
loadSettings();
|
|
1506
|
+
updateSelectionStatus();
|
|
1507
|
+
showChat();
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
window.FlowPilotCore = { initMainWindow: initMainWindow, initPopout: initPopout };
|