@lzhzzzzwill/cofos 1.2.4 → 1.3.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/README.md +16 -0
- package/bin/cofos.js +253 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ COFOS CLI is a standalone Node.js command line wrapper around the COFOS Python r
|
|
|
6
6
|
npx @lzhzzzzwill/cofos
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
By default COFOS uses a plain terminal interface, so normal terminal scrollback, mouse selection, copy, and `Ctrl+C` work as expected. The fixed-input full-screen interface is still available as an experimental mode with `--tui`.
|
|
10
|
+
|
|
9
11
|
## Runtime Model
|
|
10
12
|
|
|
11
13
|
The standalone npm package includes a minimal Python runtime under `runtime/`:
|
|
@@ -69,6 +71,18 @@ Disable RAG for a session:
|
|
|
69
71
|
npx @lzhzzzzwill/cofos --no-rag
|
|
70
72
|
```
|
|
71
73
|
|
|
74
|
+
Ask once and exit, useful for scripts and smoke tests:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx @lzhzzzzwill/cofos -p "What ROS does TT-T-COF generate under visible-light photocatalysis?"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Use the experimental fixed-input TUI:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npx @lzhzzzzwill/cofos --tui
|
|
84
|
+
```
|
|
85
|
+
|
|
72
86
|
## Startup Behavior
|
|
73
87
|
|
|
74
88
|
On startup, the CLI does the following:
|
|
@@ -118,6 +132,8 @@ cofos \
|
|
|
118
132
|
| `--cache-dir` | Folder for model, KG/BM25, and parsed PDF cache; defaults to `./.cofos` |
|
|
119
133
|
| `--rebuild-pdf-index` | Force PDF parsing and BM25 rebuild |
|
|
120
134
|
| `--top-k` | Retrieval top-k |
|
|
135
|
+
| `-p`, `--prompt` | Ask one question and exit |
|
|
136
|
+
| `--tui` | Use the experimental fixed-input full-screen TUI |
|
|
121
137
|
| `--max-new-tokens` | Override generation length |
|
|
122
138
|
| `--no-rag` | Run without KG/BM25 retrieval |
|
|
123
139
|
|
package/bin/cofos.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
-
import
|
|
3
|
+
import { createInterface, emitKeypressEvents } from "node:readline";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -134,6 +134,12 @@ function slashHint(line = "/") {
|
|
|
134
134
|
return `\n Commands\n${rows}\n Press Tab to complete.\n`;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
function slashInlineHint(line = "/") {
|
|
138
|
+
const hits = slashMatches(line);
|
|
139
|
+
if (!hits.length) return "";
|
|
140
|
+
return hits.map(({ cmd, desc }) => ` ${C}${cmd}${R} ${desc}`).join("\n");
|
|
141
|
+
}
|
|
142
|
+
|
|
137
143
|
/* ─── helpers ─── */
|
|
138
144
|
|
|
139
145
|
function cmdExist(c) { return spawnSync(c, ["--version"], { encoding: "utf-8", stdio: "pipe" }).status === 0; }
|
|
@@ -154,12 +160,15 @@ function ensureDeps(py, log = console.log) {
|
|
|
154
160
|
|
|
155
161
|
/* ─── TUI ─── */
|
|
156
162
|
|
|
157
|
-
function createTui() {
|
|
163
|
+
async function createTui() {
|
|
164
|
+
const { default: blessed } = await import("blessed");
|
|
158
165
|
const screen = blessed.screen({
|
|
159
166
|
smartCSR: true,
|
|
160
167
|
fullUnicode: true,
|
|
161
168
|
title: "COFOS",
|
|
169
|
+
mouse: false,
|
|
162
170
|
});
|
|
171
|
+
screen.program.disableMouse();
|
|
163
172
|
|
|
164
173
|
const history = blessed.box({
|
|
165
174
|
top: 0,
|
|
@@ -170,7 +179,7 @@ function createTui() {
|
|
|
170
179
|
alwaysScroll: true,
|
|
171
180
|
keys: true,
|
|
172
181
|
vi: true,
|
|
173
|
-
mouse:
|
|
182
|
+
mouse: false,
|
|
174
183
|
tags: true,
|
|
175
184
|
scrollbar: { ch: " ", track: { bg: "black" }, style: { bg: "cyan" } },
|
|
176
185
|
});
|
|
@@ -213,6 +222,12 @@ function createTui() {
|
|
|
213
222
|
let suggestionMatches = [];
|
|
214
223
|
let selectedSuggestion = 0;
|
|
215
224
|
|
|
225
|
+
function renderBuffer(wasAtBottom = true) {
|
|
226
|
+
history.setContent(buffer);
|
|
227
|
+
if (wasAtBottom) history.setScrollPerc(100);
|
|
228
|
+
screen.render();
|
|
229
|
+
}
|
|
230
|
+
|
|
216
231
|
function append(text = "") {
|
|
217
232
|
const wasAtBottom = history.getScrollPerc() >= 98;
|
|
218
233
|
const data = String(text).replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
|
|
@@ -226,9 +241,7 @@ function createTui() {
|
|
|
226
241
|
}
|
|
227
242
|
}
|
|
228
243
|
if (buffer.length > 250000) buffer = buffer.slice(-220000);
|
|
229
|
-
|
|
230
|
-
if (wasAtBottom) history.setScrollPerc(100);
|
|
231
|
-
screen.render();
|
|
244
|
+
renderBuffer(wasAtBottom);
|
|
232
245
|
}
|
|
233
246
|
|
|
234
247
|
function hideSuggestions() {
|
|
@@ -326,25 +339,100 @@ function createTui() {
|
|
|
326
339
|
screen.render();
|
|
327
340
|
|
|
328
341
|
return {
|
|
342
|
+
supportsReplace: true,
|
|
329
343
|
screen,
|
|
330
344
|
input,
|
|
331
345
|
append,
|
|
332
346
|
print: (text = "") => append(String(text) + "\n"),
|
|
333
|
-
clear: () => { buffer = "";
|
|
347
|
+
clear: () => { buffer = ""; renderBuffer(true); },
|
|
348
|
+
length: () => buffer.length,
|
|
349
|
+
replaceFrom: (index, text = "") => { buffer = buffer.slice(0, Math.max(0, index)) + String(text); renderBuffer(true); },
|
|
334
350
|
setSubmitHandler: (fn) => { onSubmit = fn; },
|
|
335
351
|
setExitHandler: (fn) => { onExit = fn; },
|
|
336
352
|
focus: () => { input.focus(); screen.render(); },
|
|
337
353
|
setPrompt: (label) => { input.setLabel(` ${label} `); screen.render(); },
|
|
338
|
-
destroy: () =>
|
|
354
|
+
destroy: () => {
|
|
355
|
+
try { screen.program.disableMouse(); } catch (_) {}
|
|
356
|
+
screen.destroy();
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function blessedToAnsi(text) {
|
|
362
|
+
return String(text)
|
|
363
|
+
.replace(/\{bold\}/g, B)
|
|
364
|
+
.replace(/\{\/bold\}/g, R)
|
|
365
|
+
.replace(/\{cyan-fg\}/g, C)
|
|
366
|
+
.replace(/\{yellow-fg\}/g, Y)
|
|
367
|
+
.replace(/\{green-fg\}/g, GR)
|
|
368
|
+
.replace(/\{gray-fg\}/g, G)
|
|
369
|
+
.replace(/\{red-fg\}/g, "\x1b[91m")
|
|
370
|
+
.replace(/\{black-fg\}|\{yellow-bg\}|\{\/yellow-bg\}|\{\/black-fg\}/g, "")
|
|
371
|
+
.replace(/\{\/(?:cyan-fg|yellow-fg|green-fg|gray-fg|red-fg)\}/g, R)
|
|
372
|
+
.replace(/[{}]/g, "");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function createPlainUi() {
|
|
376
|
+
const rl = createInterface({
|
|
377
|
+
input: process.stdin,
|
|
378
|
+
output: process.stdout,
|
|
379
|
+
terminal: true,
|
|
380
|
+
completer: (line) => slashCompletions(line),
|
|
381
|
+
});
|
|
382
|
+
let onSubmit = () => {};
|
|
383
|
+
let onExit = () => {};
|
|
384
|
+
let prompt = "cofos";
|
|
385
|
+
let slashHintShown = false;
|
|
386
|
+
|
|
387
|
+
function setPrompt(label) {
|
|
388
|
+
prompt = label;
|
|
389
|
+
rl.setPrompt(`${C}${prompt}${R} `);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
setPrompt(prompt);
|
|
393
|
+
if (process.stdin.isTTY) {
|
|
394
|
+
emitKeypressEvents(process.stdin, rl);
|
|
395
|
+
process.stdin.on("keypress", () => {
|
|
396
|
+
setTimeout(() => {
|
|
397
|
+
const line = rl.line || "";
|
|
398
|
+
if (line === "/" && !slashHintShown) {
|
|
399
|
+
slashHintShown = true;
|
|
400
|
+
process.stdout.write(`\n${slashInlineHint("/")}\n`);
|
|
401
|
+
rl.prompt(true);
|
|
402
|
+
} else if (!line.startsWith("/")) {
|
|
403
|
+
slashHintShown = false;
|
|
404
|
+
}
|
|
405
|
+
}, 0);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
rl.on("line", (line) => {
|
|
409
|
+
slashHintShown = false;
|
|
410
|
+
onSubmit(line);
|
|
411
|
+
});
|
|
412
|
+
rl.on("SIGINT", () => onExit());
|
|
413
|
+
rl.on("close", () => onExit());
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
supportsReplace: false,
|
|
417
|
+
append: (text = "") => process.stdout.write(blessedToAnsi(text)),
|
|
418
|
+
print: (text = "") => console.log(blessedToAnsi(text)),
|
|
419
|
+
clear: () => console.clear(),
|
|
420
|
+
length: () => 0,
|
|
421
|
+
replaceFrom: (_index, text = "") => process.stdout.write(blessedToAnsi(text)),
|
|
422
|
+
setSubmitHandler: (fn) => { onSubmit = fn; },
|
|
423
|
+
setExitHandler: (fn) => { onExit = fn; },
|
|
424
|
+
focus: () => rl.prompt(),
|
|
425
|
+
setPrompt,
|
|
426
|
+
destroy: () => { try { rl.close(); } catch (_) {} },
|
|
339
427
|
};
|
|
340
428
|
}
|
|
341
429
|
|
|
342
430
|
/* ─── main ─── */
|
|
343
431
|
|
|
344
|
-
function main() {
|
|
432
|
+
async function main() {
|
|
345
433
|
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
346
434
|
console.log("\n COFOS CLI — 9B + RAG chat (Willlzh/COFOS)\n");
|
|
347
|
-
console.log(" Usage: cofos [--model PATH] [--config PATH] [--pdf-dir DIR] [--cache-dir DIR] [--top-k N] [--no-rag]\n");
|
|
435
|
+
console.log(" Usage: cofos [--tui] [-p QUESTION] [--model PATH] [--config PATH] [--pdf-dir DIR] [--cache-dir DIR] [--top-k N] [--no-rag]\n");
|
|
348
436
|
console.log(" Commands in chat:");
|
|
349
437
|
console.log(" /help Show this help");
|
|
350
438
|
console.log(" /exit Quit");
|
|
@@ -356,6 +444,9 @@ function main() {
|
|
|
356
444
|
console.log(" Startup options:");
|
|
357
445
|
console.log(" --pdf-dir DIR Add PDFs from DIR as extra BM25 evidence");
|
|
358
446
|
console.log(" --cache-dir DIR Store model/data cache here (default: ./.cofos)");
|
|
447
|
+
console.log(" --max-new-tokens N Override generation length");
|
|
448
|
+
console.log(" --tui Use the experimental fixed-input TUI");
|
|
449
|
+
console.log(" -p, --prompt TEXT Ask once and exit");
|
|
359
450
|
console.log(" --rebuild-pdf-index Force reparsing PDFs and rebuilding BM25\n");
|
|
360
451
|
console.log(" Multi-line: end a line with \\ to continue on the next line.\n");
|
|
361
452
|
process.exit(0);
|
|
@@ -368,8 +459,11 @@ function main() {
|
|
|
368
459
|
let configPath = DEFAULT_CONFIG;
|
|
369
460
|
let topK = "5";
|
|
370
461
|
let pdfDir = null;
|
|
462
|
+
let maxNewTokens = null;
|
|
371
463
|
let cacheDir = process.env.COFOS_CACHE_DIR ? resolve(process.env.COFOS_CACHE_DIR) : resolve(process.cwd(), ".cofos");
|
|
372
464
|
let rebuildPdfIndex = false;
|
|
465
|
+
let useTui = false;
|
|
466
|
+
let singlePrompt = null;
|
|
373
467
|
const backendArgs = [chatScript];
|
|
374
468
|
|
|
375
469
|
const die = (msg) => { console.error("[cofos] " + msg); process.exit(1); };
|
|
@@ -393,10 +487,17 @@ function main() {
|
|
|
393
487
|
} else if (arg === "--pdf-dir") {
|
|
394
488
|
pdfDir = resolve(value(++i, arg));
|
|
395
489
|
if (!existsSync(pdfDir)) die(`--pdf-dir does not exist: ${pdfDir}`);
|
|
490
|
+
} else if (arg === "--max-new-tokens") {
|
|
491
|
+
maxNewTokens = value(++i, arg);
|
|
492
|
+
if (!/^\d+$/.test(maxNewTokens) || Number(maxNewTokens) < 1) die(`--max-new-tokens must be a positive integer, got "${maxNewTokens}"`);
|
|
396
493
|
} else if (arg === "--cache-dir") {
|
|
397
494
|
cacheDir = resolve(value(++i, arg));
|
|
398
495
|
} else if (arg === "--rebuild-pdf-index") {
|
|
399
496
|
rebuildPdfIndex = true;
|
|
497
|
+
} else if (arg === "--tui") {
|
|
498
|
+
useTui = true;
|
|
499
|
+
} else if (arg === "-p" || arg === "--prompt") {
|
|
500
|
+
singlePrompt = value(++i, arg);
|
|
400
501
|
} else if (arg === "--no-rag") {
|
|
401
502
|
backendArgs.push("--no-rag");
|
|
402
503
|
} else {
|
|
@@ -405,14 +506,15 @@ function main() {
|
|
|
405
506
|
}
|
|
406
507
|
backendArgs.push("--merged-model-path", modelPath, "--config", configPath, "--top-k", topK);
|
|
407
508
|
if (pdfDir) backendArgs.push("--pdf-dir", pdfDir);
|
|
509
|
+
if (maxNewTokens) backendArgs.push("--max-new-tokens", maxNewTokens);
|
|
408
510
|
if (rebuildPdfIndex) backendArgs.push("--rebuild-pdf-index");
|
|
409
511
|
|
|
410
512
|
const py = findPy();
|
|
411
513
|
if (!py) die("Python not found. Install Python 3.9+ and re-run.");
|
|
412
514
|
if (!existsSync(chatScript)) die(`Missing script: ${chatScript}`);
|
|
413
515
|
|
|
414
|
-
const ui = createTui();
|
|
415
|
-
ui.print(plain(render({ modelPath, cacheDir, pdfDir })));
|
|
516
|
+
const ui = useTui ? await createTui() : createPlainUi();
|
|
517
|
+
ui.print(useTui ? plain(render({ modelPath, cacheDir, pdfDir })) : render({ modelPath, cacheDir, pdfDir }));
|
|
416
518
|
ensureDeps(py, (msg) => ui.print(msg));
|
|
417
519
|
ui.print(" Loading COFOS 9B + RAG backend from Hugging Face … first run may take a while");
|
|
418
520
|
if (pdfDir) ui.print(" Local PDF RAG: enabled for this session");
|
|
@@ -439,7 +541,8 @@ function main() {
|
|
|
439
541
|
proc.stderr.setEncoding("utf-8");
|
|
440
542
|
|
|
441
543
|
let buf = "", ready = false, generating = false, responseOpen = false, commandOpen = false, closed = false;
|
|
442
|
-
let
|
|
544
|
+
let currentAnswer = "", assistantStart = -1, plainAssistantMode = "undecided", plainAssistantPending = "";
|
|
545
|
+
let plainReasonBuffer = "", plainReasonPrinted = 0;
|
|
443
546
|
const inputQ = [];
|
|
444
547
|
let mlBuf = [];
|
|
445
548
|
|
|
@@ -449,6 +552,7 @@ function main() {
|
|
|
449
552
|
try { proc.stdin.end(); } catch (_) {}
|
|
450
553
|
try { proc.kill("SIGTERM"); } catch (_) {}
|
|
451
554
|
ui.destroy();
|
|
555
|
+
process.exit(0);
|
|
452
556
|
}
|
|
453
557
|
|
|
454
558
|
ui.setExitHandler(shutdown);
|
|
@@ -479,7 +583,7 @@ function main() {
|
|
|
479
583
|
input = input.trim();
|
|
480
584
|
if (!input) return false;
|
|
481
585
|
|
|
482
|
-
if (/^\/(exit|quit)$/.test(input)) {
|
|
586
|
+
if (/^\/(exit|quit)$/.test(input)) { shutdown(); return true; }
|
|
483
587
|
if (/^\/(help)$/.test(input)) {
|
|
484
588
|
ui.print(slashHint("/") + "\n Add local papers to RAG:\n cofos --pdf-dir ./new_pdfs\n cofos --pdf-dir ./new_pdfs --rebuild-pdf-index\n\n Multi-line: end a line with \\ to continue.");
|
|
485
589
|
return false;
|
|
@@ -492,13 +596,25 @@ function main() {
|
|
|
492
596
|
ui.clear();
|
|
493
597
|
mlBuf = [];
|
|
494
598
|
responseOpen = false;
|
|
599
|
+
assistantStart = -1;
|
|
600
|
+
currentAnswer = "";
|
|
601
|
+
plainAssistantMode = "undecided";
|
|
602
|
+
plainAssistantPending = "";
|
|
603
|
+
plainReasonBuffer = "";
|
|
604
|
+
plainReasonPrinted = 0;
|
|
495
605
|
commandOpen = true;
|
|
496
606
|
proc.stdin.write(input + "\n");
|
|
497
607
|
generating = true;
|
|
498
608
|
return true;
|
|
499
609
|
}
|
|
500
|
-
if (/^\/(topk)(?:\s+\d+)?$/.test(input) || /^\/rag\s+(?:on|off)$/.test(input)) {
|
|
610
|
+
if (/^\/(save)$/.test(input) || /^\/(topk)(?:\s+\d+)?$/.test(input) || /^\/rag\s+(?:on|off)$/.test(input)) {
|
|
501
611
|
responseOpen = false;
|
|
612
|
+
assistantStart = -1;
|
|
613
|
+
currentAnswer = "";
|
|
614
|
+
plainAssistantMode = "undecided";
|
|
615
|
+
plainAssistantPending = "";
|
|
616
|
+
plainReasonBuffer = "";
|
|
617
|
+
plainReasonPrinted = 0;
|
|
502
618
|
commandOpen = true;
|
|
503
619
|
proc.stdin.write(input + "\n");
|
|
504
620
|
generating = true;
|
|
@@ -515,6 +631,12 @@ function main() {
|
|
|
515
631
|
|
|
516
632
|
ui.print("{yellow-fg}●{/yellow-fg} " + input);
|
|
517
633
|
responseOpen = false;
|
|
634
|
+
assistantStart = -1;
|
|
635
|
+
currentAnswer = "";
|
|
636
|
+
plainAssistantMode = "undecided";
|
|
637
|
+
plainAssistantPending = "";
|
|
638
|
+
plainReasonBuffer = "";
|
|
639
|
+
plainReasonPrinted = 0;
|
|
518
640
|
generating = true;
|
|
519
641
|
proc.stdin.write(input + "\n");
|
|
520
642
|
return true;
|
|
@@ -527,39 +649,104 @@ function main() {
|
|
|
527
649
|
|
|
528
650
|
proc.stderr.on("data", (chunk) => ui.append(chunk));
|
|
529
651
|
|
|
530
|
-
function
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
652
|
+
function escapeTags(text) {
|
|
653
|
+
return String(text).replace(/[{}]/g, "");
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function formatAssistant(answer) {
|
|
657
|
+
const raw = escapeTags(answer).trimStart();
|
|
658
|
+
if (!raw) return "";
|
|
659
|
+
if (raw.includes("</think>")) {
|
|
660
|
+
const [reason, ...answerParts] = raw.split("</think>");
|
|
661
|
+
const cleanReason = reason.replace(/^<think>\s*/i, "").trim();
|
|
662
|
+
const finalAnswer = answerParts.join("</think>").trimStart();
|
|
663
|
+
return finalAnswer
|
|
664
|
+
? "{gray-fg}- " + cleanReason + "{/gray-fg}\n{green-fg}{bold}◆{/bold}{/green-fg} " + finalAnswer
|
|
665
|
+
: "{gray-fg}- " + cleanReason + "{/gray-fg}";
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const paragraphs = raw.split(/\n\s*\n/);
|
|
669
|
+
const reasonRe = /^(The user|Okay[,,]|I need|We need|I should|I'll|Let me|Since the input|In COF nomenclature|Based on the provided information[,,]? I can|用户|让我)/i;
|
|
670
|
+
let reasonCount = 0;
|
|
671
|
+
while (reasonCount < paragraphs.length && reasonRe.test(paragraphs[reasonCount].trim())) reasonCount += 1;
|
|
672
|
+
if (reasonCount > 0) {
|
|
673
|
+
const reason = paragraphs.slice(0, reasonCount).join("\n\n").trim();
|
|
674
|
+
const finalAnswer = paragraphs.slice(reasonCount).join("\n\n").trimStart();
|
|
675
|
+
return finalAnswer
|
|
676
|
+
? "{gray-fg}- " + reason + "{/gray-fg}\n{green-fg}{bold}◆{/bold}{/green-fg} " + finalAnswer
|
|
677
|
+
: "{gray-fg}- " + reason + "{/gray-fg}";
|
|
678
|
+
}
|
|
679
|
+
return "{green-fg}{bold}◆{/bold}{/green-fg} " + raw;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const reasonRe = /^(The user|Okay[,,]|I need|We need|I should|I'll|Let me|Since the input|In COF nomenclature|Based on the provided information[,,]? I can|用户|让我)/i;
|
|
683
|
+
|
|
684
|
+
function redrawAssistant() {
|
|
685
|
+
if (assistantStart < 0 || !ui.supportsReplace) return;
|
|
686
|
+
ui.replaceFrom(assistantStart, formatAssistant(currentAnswer));
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function streamPlainAssistant(text, final = false) {
|
|
690
|
+
if (ui.supportsReplace) return;
|
|
691
|
+
plainAssistantPending += text;
|
|
692
|
+
if (plainAssistantMode === "undecided") {
|
|
693
|
+
const start = plainAssistantPending.trimStart();
|
|
694
|
+
if (!start && !final) return;
|
|
695
|
+
if (start.startsWith("<think>") || reasonRe.test(start)) {
|
|
696
|
+
plainAssistantMode = "reason";
|
|
697
|
+
plainReasonBuffer = start.replace(/^<think>\s*/i, "");
|
|
698
|
+
plainReasonPrinted = 0;
|
|
699
|
+
plainAssistantPending = "";
|
|
700
|
+
ui.append(`${G}-${R} ${G}`);
|
|
701
|
+
} else if (final || start.length >= 12 || /\s/.test(start)) {
|
|
702
|
+
plainAssistantMode = "answer";
|
|
703
|
+
plainAssistantPending = start;
|
|
704
|
+
ui.append(`${GR}${B}◆${R} `);
|
|
541
705
|
} else {
|
|
542
|
-
|
|
706
|
+
return;
|
|
543
707
|
}
|
|
544
|
-
responseBuffer = "";
|
|
545
|
-
return;
|
|
546
708
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
709
|
+
|
|
710
|
+
if (plainAssistantMode === "reason") {
|
|
711
|
+
plainReasonBuffer += plainAssistantPending;
|
|
712
|
+
plainAssistantPending = "";
|
|
713
|
+
const thinkEnd = plainReasonBuffer.indexOf("</think>");
|
|
714
|
+
const paraEnd = plainReasonBuffer.search(/\n\s*\n/);
|
|
715
|
+
const split = thinkEnd !== -1 ? thinkEnd : paraEnd;
|
|
716
|
+
if (split !== -1) {
|
|
717
|
+
const reason = plainReasonBuffer.slice(plainReasonPrinted, split).replace(/<\/think>/gi, "");
|
|
718
|
+
const restStart = thinkEnd !== -1 ? split + "</think>".length : split;
|
|
719
|
+
const rest = plainReasonBuffer.slice(restStart).trimStart();
|
|
720
|
+
if (reason) ui.append(reason);
|
|
721
|
+
ui.append(`${R}\n${GR}${B}◆${R} `);
|
|
722
|
+
plainAssistantMode = "answer";
|
|
723
|
+
plainAssistantPending = rest;
|
|
724
|
+
plainReasonBuffer = "";
|
|
725
|
+
plainReasonPrinted = 0;
|
|
555
726
|
} else {
|
|
556
|
-
|
|
727
|
+
const printableUntil = final ? plainReasonBuffer.length : Math.max(0, plainReasonBuffer.length - 2);
|
|
728
|
+
if (printableUntil > plainReasonPrinted) {
|
|
729
|
+
ui.append(plainReasonBuffer.slice(plainReasonPrinted, printableUntil));
|
|
730
|
+
plainReasonPrinted = printableUntil;
|
|
731
|
+
}
|
|
732
|
+
if (!final) return;
|
|
733
|
+
if (plainReasonPrinted < plainReasonBuffer.length) ui.append(plainReasonBuffer.slice(plainReasonPrinted));
|
|
734
|
+
plainReasonBuffer = "";
|
|
735
|
+
plainReasonPrinted = 0;
|
|
736
|
+
ui.append(R);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (plainAssistantMode === "answer") {
|
|
742
|
+
if (plainAssistantPending) {
|
|
743
|
+
ui.append(plainAssistantPending);
|
|
744
|
+
plainAssistantPending = "";
|
|
557
745
|
}
|
|
558
|
-
return;
|
|
559
746
|
}
|
|
560
|
-
ui.append(text);
|
|
561
747
|
}
|
|
562
748
|
|
|
749
|
+
|
|
563
750
|
proc.stdout.on("data", (chunk) => {
|
|
564
751
|
buf += chunk;
|
|
565
752
|
if (!ready) {
|
|
@@ -568,6 +755,8 @@ function main() {
|
|
|
568
755
|
ready = true;
|
|
569
756
|
buf = buf.slice(i + READY.length);
|
|
570
757
|
if (buf[0] === "\n") buf = buf.slice(1);
|
|
758
|
+
if (singlePrompt) inputQ.push(singlePrompt);
|
|
759
|
+
else ui.focus();
|
|
571
760
|
drain();
|
|
572
761
|
}
|
|
573
762
|
return;
|
|
@@ -583,7 +772,8 @@ function main() {
|
|
|
583
772
|
if (buf[0] === "\n") buf = buf.slice(1);
|
|
584
773
|
generating = false;
|
|
585
774
|
commandOpen = false;
|
|
586
|
-
|
|
775
|
+
if (singlePrompt && inputQ.length === 0) shutdown();
|
|
776
|
+
else ui.focus();
|
|
587
777
|
drain();
|
|
588
778
|
return;
|
|
589
779
|
}
|
|
@@ -598,29 +788,33 @@ function main() {
|
|
|
598
788
|
buf = buf.slice(ri + RESPONSE.length);
|
|
599
789
|
if (buf[0] === "\n") buf = buf.slice(1);
|
|
600
790
|
responseOpen = true;
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
791
|
+
assistantStart = ui.supportsReplace ? ui.length() : 0;
|
|
792
|
+
currentAnswer = "";
|
|
793
|
+
plainAssistantMode = "undecided";
|
|
794
|
+
plainAssistantPending = "";
|
|
795
|
+
plainReasonBuffer = "";
|
|
796
|
+
plainReasonPrinted = 0;
|
|
605
797
|
}
|
|
606
798
|
|
|
607
799
|
const di = buf.indexOf(DONE);
|
|
608
800
|
if (di !== -1) {
|
|
609
801
|
const pre = buf.slice(0, di);
|
|
610
|
-
if (pre)
|
|
802
|
+
if (pre) { currentAnswer += pre; if (ui.supportsReplace) redrawAssistant(); else streamPlainAssistant(pre); }
|
|
611
803
|
buf = buf.slice(di + DONE.length);
|
|
612
804
|
if (buf[0] === "\n") buf = buf.slice(1);
|
|
613
|
-
if (
|
|
805
|
+
if (ui.supportsReplace) redrawAssistant();
|
|
806
|
+
else streamPlainAssistant("", true);
|
|
614
807
|
generating = false;
|
|
615
808
|
responseOpen = false;
|
|
616
|
-
ui.append("\n");
|
|
617
|
-
|
|
809
|
+
if (ui.supportsReplace) ui.append("\n");
|
|
810
|
+
if (singlePrompt && inputQ.length === 0) shutdown();
|
|
811
|
+
else ui.focus();
|
|
618
812
|
drain();
|
|
619
813
|
} else {
|
|
620
814
|
const keep = DONE.length - 1;
|
|
621
815
|
if (buf.length > keep) {
|
|
622
816
|
const out = buf.slice(0, -keep);
|
|
623
|
-
if (out)
|
|
817
|
+
if (out) { currentAnswer += out; if (ui.supportsReplace) redrawAssistant(); else streamPlainAssistant(out); }
|
|
624
818
|
buf = buf.slice(-keep);
|
|
625
819
|
}
|
|
626
820
|
}
|
|
@@ -628,12 +822,18 @@ function main() {
|
|
|
628
822
|
|
|
629
823
|
proc.on("error", (e) => { ui.print("[cofos] Python error: " + e.message); shutdown(); });
|
|
630
824
|
proc.on("exit", (c) => {
|
|
631
|
-
closed
|
|
632
|
-
|
|
633
|
-
ui.
|
|
825
|
+
if (closed) return;
|
|
826
|
+
ui.print(c && c !== 0 ? "[cofos] Python exited with code " + c : "[cofos] Python backend exited.");
|
|
827
|
+
ui.print("Press Ctrl+C to close.");
|
|
828
|
+
ui.focus();
|
|
634
829
|
process.exitCode = c ?? 0;
|
|
635
830
|
});
|
|
636
831
|
|
|
637
832
|
}
|
|
638
833
|
|
|
834
|
+
process.on("uncaughtException", (err) => {
|
|
835
|
+
try { process.stderr.write(`[cofos] Uncaught error: ${err?.stack || err}\n`); } catch (_) {}
|
|
836
|
+
process.exit(1);
|
|
837
|
+
});
|
|
838
|
+
|
|
639
839
|
main();
|