@manny-est/node-red-flowpilot 0.4.1 → 0.5.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 +42 -0
- package/PROJECT-OVERVIEW.md +7 -7
- package/README.md +14 -8
- package/USER-GUIDE.md +27 -10
- package/flowpilot-core.css +103 -0
- package/flowpilot.js +123 -146
- package/lib/build-core-script.js +44 -0
- package/lib/build-system-prompt.js +6 -0
- package/lib/core/apply-review.js +1926 -0
- package/lib/core/history.js +93 -0
- package/lib/core/init.js +1435 -0
- package/lib/core/main.js +1777 -0
- package/lib/core/markdown.js +165 -0
- package/lib/core/modes.js +1690 -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/provider-openai-compatible.js +112 -15
- package/lib/storage.js +4 -0
- package/package.json +2 -3
- package/flowpilot-core.js +0 -6427
|
@@ -194,7 +194,66 @@ async function chat(settings, messages, options) {
|
|
|
194
194
|
// `data: [DONE]`). Calls onDelta(text) for each content fragment as it
|
|
195
195
|
// arrives and resolves with { content } containing the full concatenated
|
|
196
196
|
// text once the stream ends. Used for chat streaming only.
|
|
197
|
-
|
|
197
|
+
// Splits a streaming content string on <think>...</think> tags so that
|
|
198
|
+
// reasoning text is routed to onReasoning and the actual response text to
|
|
199
|
+
// onContent. Handles tags that arrive split across multiple chunks via a
|
|
200
|
+
// small lookahead buffer. Handles both:
|
|
201
|
+
// - SGLang/Nemotron style: delta.reasoning_content (separate field)
|
|
202
|
+
// - llama.cpp/LocalAI style: <think>...</think> embedded in delta.content
|
|
203
|
+
// The two paths converge at onReasoningDelta in postStream — callers see
|
|
204
|
+
// one uniform reasoning callback regardless of provider format.
|
|
205
|
+
function createThinkTagSplitter(onContent, onReasoning) {
|
|
206
|
+
let phase = "seek"; // "seek" | "think" | "content"
|
|
207
|
+
let buf = "";
|
|
208
|
+
const OPEN = "<think>";
|
|
209
|
+
const CLOSE = "</think>";
|
|
210
|
+
|
|
211
|
+
function push(text) {
|
|
212
|
+
buf += text;
|
|
213
|
+
while (buf.length > 0) {
|
|
214
|
+
if (phase === "seek") {
|
|
215
|
+
const idx = buf.indexOf(OPEN);
|
|
216
|
+
if (idx === -1) {
|
|
217
|
+
// No opening tag visible — emit everything except the last few bytes
|
|
218
|
+
// that might be a partial tag, buffer the rest.
|
|
219
|
+
const safe = buf.length > OPEN.length - 1 ? buf.length - (OPEN.length - 1) : 0;
|
|
220
|
+
if (safe > 0) { onContent(buf.slice(0, safe)); buf = buf.slice(safe); }
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
if (idx > 0) { onContent(buf.slice(0, idx)); }
|
|
224
|
+
buf = buf.slice(idx + OPEN.length);
|
|
225
|
+
phase = "think";
|
|
226
|
+
} else if (phase === "think") {
|
|
227
|
+
const idx = buf.indexOf(CLOSE);
|
|
228
|
+
if (idx === -1) {
|
|
229
|
+
const safe = buf.length > CLOSE.length - 1 ? buf.length - (CLOSE.length - 1) : 0;
|
|
230
|
+
if (safe > 0) { onReasoning(buf.slice(0, safe)); buf = buf.slice(safe); }
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
if (idx > 0) { onReasoning(buf.slice(0, idx)); }
|
|
234
|
+
buf = buf.slice(idx + CLOSE.length);
|
|
235
|
+
// Skip whitespace/newline immediately after </think>
|
|
236
|
+
const trimmed = buf.replace(/^\s+/, "");
|
|
237
|
+
buf = trimmed;
|
|
238
|
+
phase = "content";
|
|
239
|
+
} else {
|
|
240
|
+
onContent(buf);
|
|
241
|
+
buf = "";
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function finish() {
|
|
248
|
+
if (!buf) { return; }
|
|
249
|
+
if (phase === "think") { onReasoning(buf); } else { onContent(buf); }
|
|
250
|
+
buf = "";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { push, finish };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function postStream(urlString, headers, body, timeoutMs, onDelta, onReasoningDelta) {
|
|
198
257
|
return new Promise((resolve, reject) => {
|
|
199
258
|
let url;
|
|
200
259
|
try {
|
|
@@ -233,12 +292,22 @@ function postStream(urlString, headers, body, timeoutMs, onDelta) {
|
|
|
233
292
|
return;
|
|
234
293
|
}
|
|
235
294
|
|
|
236
|
-
let
|
|
295
|
+
let sseBuf = "";
|
|
237
296
|
let full = "";
|
|
297
|
+
// When onReasoningDelta is provided, intercept <think>...</think> from
|
|
298
|
+
// delta.content in addition to the dedicated delta.reasoning_content field
|
|
299
|
+
// (llama.cpp/LocalAI style vs SGLang/Nemotron style — both converge here).
|
|
300
|
+
const thinkSplitter = onReasoningDelta
|
|
301
|
+
? createThinkTagSplitter(
|
|
302
|
+
function (c) { if (firstTokenAt === null) { firstTokenAt = Date.now(); } full += c; onDelta(c); },
|
|
303
|
+
onReasoningDelta
|
|
304
|
+
)
|
|
305
|
+
: null;
|
|
306
|
+
|
|
238
307
|
res.on("data", (chunk) => {
|
|
239
|
-
|
|
240
|
-
const lines =
|
|
241
|
-
|
|
308
|
+
sseBuf += chunk;
|
|
309
|
+
const lines = sseBuf.split("\n");
|
|
310
|
+
sseBuf = lines.pop(); // keep the last (possibly partial) line for next time
|
|
242
311
|
|
|
243
312
|
lines.forEach((line) => {
|
|
244
313
|
line = line.trim();
|
|
@@ -255,17 +324,29 @@ function postStream(urlString, headers, body, timeoutMs, onDelta) {
|
|
|
255
324
|
|
|
256
325
|
if (evt && evt.usage) { usage = evt.usage; }
|
|
257
326
|
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
327
|
+
const deltaObj = evt && evt.choices && evt.choices[0] && evt.choices[0].delta;
|
|
328
|
+
if (deltaObj) {
|
|
329
|
+
if (deltaObj.content) {
|
|
330
|
+
if (thinkSplitter) {
|
|
331
|
+
thinkSplitter.push(deltaObj.content);
|
|
332
|
+
} else {
|
|
333
|
+
if (firstTokenAt === null) { firstTokenAt = Date.now(); }
|
|
334
|
+
full += deltaObj.content;
|
|
335
|
+
onDelta(deltaObj.content);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Separate reasoning_content field (SGLang/Nemotron style). Both
|
|
339
|
+
// paths run in parallel — they are mutually exclusive per provider
|
|
340
|
+
// (llama.cpp puts reasoning in delta.content via <think> tags;
|
|
341
|
+
// SGLang puts it in delta.reasoning_content with delta.content "").
|
|
342
|
+
if (deltaObj.reasoning_content && onReasoningDelta) {
|
|
343
|
+
onReasoningDelta(deltaObj.reasoning_content);
|
|
344
|
+
}
|
|
265
345
|
}
|
|
266
346
|
});
|
|
267
347
|
});
|
|
268
348
|
res.on("end", () => {
|
|
349
|
+
if (thinkSplitter) { thinkSplitter.finish(); }
|
|
269
350
|
const endedAt = Date.now();
|
|
270
351
|
resolve({
|
|
271
352
|
content: full,
|
|
@@ -287,7 +368,7 @@ function postStream(urlString, headers, body, timeoutMs, onDelta) {
|
|
|
287
368
|
});
|
|
288
369
|
}
|
|
289
370
|
|
|
290
|
-
async function chatStream(settings, messages, onDelta) {
|
|
371
|
+
async function chatStream(settings, messages, onDelta, onReasoningDelta) {
|
|
291
372
|
const baseUrl = String(settings.baseUrl || "").replace(/\/+$/, "");
|
|
292
373
|
if (!baseUrl) throw new Error("Provider base URL is required.");
|
|
293
374
|
if (!settings.model) throw new Error("Model is required.");
|
|
@@ -309,7 +390,7 @@ async function chatStream(settings, messages, onDelta) {
|
|
|
309
390
|
temperature,
|
|
310
391
|
stream: true,
|
|
311
392
|
stream_options: { include_usage: true }
|
|
312
|
-
}, settings.requestTimeoutMs || 180000, onDelta);
|
|
393
|
+
}, settings.requestTimeoutMs || 180000, onDelta, onReasoningDelta);
|
|
313
394
|
|
|
314
395
|
return {
|
|
315
396
|
content: result.content || "",
|
|
@@ -377,4 +458,20 @@ async function probeTools(settings) {
|
|
|
377
458
|
}
|
|
378
459
|
}
|
|
379
460
|
|
|
380
|
-
|
|
461
|
+
// ---------------------------------------------------------------------
|
|
462
|
+
// Checks whether the provider's response includes reasoning_content —
|
|
463
|
+
// the separate chain-of-thought field emitted by reasoning models
|
|
464
|
+
// (e.g. Nemotron, DeepSeek-R1, o1-style). Reuses the connectivity test
|
|
465
|
+
// response already obtained by the /test route rather than making a
|
|
466
|
+
// second round-trip: caller passes the raw response object.
|
|
467
|
+
// Returns { isReasoningModel: boolean }.
|
|
468
|
+
// ---------------------------------------------------------------------
|
|
469
|
+
function detectReasoning(rawResponse) {
|
|
470
|
+
const message = rawResponse && rawResponse.choices && rawResponse.choices[0]
|
|
471
|
+
&& rawResponse.choices[0].message;
|
|
472
|
+
const isReasoningModel = !!(message && message.reasoning_content != null
|
|
473
|
+
&& message.reasoning_content !== "");
|
|
474
|
+
return { isReasoningModel };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
module.exports = { chat, chatStream, probeTools, listModels, detectReasoning };
|
package/lib/storage.js
CHANGED
|
@@ -58,6 +58,10 @@ function createStorage(userDir) {
|
|
|
58
58
|
// own hardcoded AGENT_LOOP_MAX_STEPS (a different bound, for a
|
|
59
59
|
// different loop).
|
|
60
60
|
agentLoopMaxIterations: 5,
|
|
61
|
+
// When true, the build loop pauses at the "attach → review" transition
|
|
62
|
+
// and shows a checkpoint question ("Continue with AI review, or stop?")
|
|
63
|
+
// instead of auto-advancing. Default false = original auto-advance behavior.
|
|
64
|
+
loopHoldStep: false,
|
|
61
65
|
// Lets the user silence the recurring secrets/size reminder bar after
|
|
62
66
|
// typing an explicit acknowledgement in settings.
|
|
63
67
|
suppressContextWarnings: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@manny-est/node-red-flowpilot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "FlowPilot for Node-RED - an AI-powered development assistant sidebar",
|
|
5
5
|
"main": "flowpilot.js",
|
|
6
6
|
"keywords": [
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
|
-
"url": "https://github.com/manny-est/flowpilot.git"
|
|
17
|
+
"url": "git+https://github.com/manny-est/flowpilot.git"
|
|
18
18
|
},
|
|
19
19
|
"homepage": "https://github.com/manny-est/flowpilot#readme",
|
|
20
20
|
"bugs": {
|
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
"files": [
|
|
39
39
|
"flowpilot.js",
|
|
40
40
|
"flowpilot.html",
|
|
41
|
-
"flowpilot-core.js",
|
|
42
41
|
"flowpilot-core.css",
|
|
43
42
|
"lib",
|
|
44
43
|
"icons",
|