@krmxd/onegpt 0.2.1-beta → 0.2.3-beta
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/package.json +1 -1
- package/src/agent.js +63 -3
- package/src/render.js +150 -38
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -10,6 +10,41 @@ const path = require("path");
|
|
|
10
10
|
const { getConfig } = require("./config");
|
|
11
11
|
const { getSharedProvider } = require("./ollama");
|
|
12
12
|
const { ToolRegistry } = require("./tools");
|
|
13
|
+
const { spawnSync } = require("child_process");
|
|
14
|
+
|
|
15
|
+
// --- auto syntax check for AI-written code -------------------------------
|
|
16
|
+
const CHECKABLE_EXT = new Set([".py", ".js", ".mjs", ".cjs"]);
|
|
17
|
+
|
|
18
|
+
function _pick(binaries, args, timeoutMs) {
|
|
19
|
+
for (const bin of binaries) {
|
|
20
|
+
try {
|
|
21
|
+
const r = spawnSync(bin, args, { timeout: timeoutMs });
|
|
22
|
+
if (r.status === 0) return null;
|
|
23
|
+
if (r.error && r.error.code === "ENOENT") continue;
|
|
24
|
+
const err = (r.stderr ? r.stderr.toString() : "")
|
|
25
|
+
|| (r.stdout ? r.stdout.toString() : "") || `exit ${r.status}`;
|
|
26
|
+
return err.split("\n").filter((l) => l.trim()).slice(0, 4).join("\n").slice(0, 500);
|
|
27
|
+
} catch { /* try next */ }
|
|
28
|
+
}
|
|
29
|
+
return null; // no interpreter available - skip silently
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function syntaxCheckError(filePath) {
|
|
33
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
34
|
+
if (!CHECKABLE_EXT.has(ext)) return null;
|
|
35
|
+
if (!fs.existsSync(filePath)) return null;
|
|
36
|
+
if (ext === ".py") {
|
|
37
|
+
return _pick(["python3", "python"], ["-m", "py_compile", filePath], 15000);
|
|
38
|
+
}
|
|
39
|
+
return _pick([process.execPath || "node", "node"], ["--check", filePath], 15000);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function autoCheckMsg(p, err) {
|
|
43
|
+
// Loop-break redirect style: tiny models need the exact shape again.
|
|
44
|
+
return "[auto-check] The file you just wrote FAILED its syntax check:\n" +
|
|
45
|
+
`${p}\n${err}\n` +
|
|
46
|
+
"Rewrite the ENTIRE file correctly NOW with write_file (bare JSON tool call), fixing every reported error. Do not explain.";
|
|
47
|
+
}
|
|
13
48
|
|
|
14
49
|
const TOOL_RE = /\x00TOOL:([^:]*):([^:]*):(.*?)\x00/g;
|
|
15
50
|
const USAGE_RE = /\x00USAGE:(.*?)\x00/;
|
|
@@ -473,11 +508,14 @@ function diffLines(oldStr, newStr) {
|
|
|
473
508
|
}
|
|
474
509
|
|
|
475
510
|
function codeMarkdown(ev) {
|
|
511
|
+
// The fence info carries the file so renderers can show a titled,
|
|
512
|
+
// macOS-style code box: ```lang path=folder/file.ext
|
|
513
|
+
const rel = String(ev.path || "").replace(/^\/+/, "");
|
|
476
514
|
if (ev.action === "EDITED" && ev.old_content !== undefined) {
|
|
477
|
-
return "```diff\n" + diffLines(ev.old_content, ev.content).join("\n") + "\n```";
|
|
515
|
+
return "```diff path=" + rel + "\n" + diffLines(ev.old_content, ev.content).join("\n") + "\n```";
|
|
478
516
|
}
|
|
479
517
|
const lang = detectLang(ev.path);
|
|
480
|
-
return "```" + lang + "\n" + ev.content + "\n```";
|
|
518
|
+
return "```" + lang + " path=" + rel + "\n" + ev.content + "\n```";
|
|
481
519
|
}
|
|
482
520
|
|
|
483
521
|
class Agent {
|
|
@@ -567,6 +605,7 @@ class Agent {
|
|
|
567
605
|
let full = "";
|
|
568
606
|
let rounds = 0;
|
|
569
607
|
let nudges = 0;
|
|
608
|
+
this._autoChecks = 0;
|
|
570
609
|
let lastSig = null;
|
|
571
610
|
let loopBreaks = 0;
|
|
572
611
|
const doneBits = [];
|
|
@@ -630,6 +669,15 @@ class Agent {
|
|
|
630
669
|
doneBits.push(result.output.split("\n")[0].slice(0, 90));
|
|
631
670
|
const ev = codeEvent(tc, pre);
|
|
632
671
|
if (ev) full += (full.trim() ? "\n\n" : "") + codeMarkdown(ev);
|
|
672
|
+
// Auto syntax check: broken AI-written code gets one bounce-back.
|
|
673
|
+
if (ev && (tc.name === "write_file" || tc.name === "edit_file")
|
|
674
|
+
&& this._autoChecks < 2) {
|
|
675
|
+
const err = syntaxCheckError(ev.path);
|
|
676
|
+
if (err) {
|
|
677
|
+
this._autoChecks++;
|
|
678
|
+
this.history.push({ role: "user", content: autoCheckMsg(ev.path, err), ts: Date.now() / 1000 });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
633
681
|
}
|
|
634
682
|
}
|
|
635
683
|
} else {
|
|
@@ -679,6 +727,7 @@ class Agent {
|
|
|
679
727
|
this.history.push({ role: "user", content: userInput, ts: Date.now() / 1000 });
|
|
680
728
|
let rounds = 0;
|
|
681
729
|
let nudges = 0;
|
|
730
|
+
this._autoChecks = 0;
|
|
682
731
|
const tStart = Date.now();
|
|
683
732
|
let ttft = 0;
|
|
684
733
|
let nTokens = 0;
|
|
@@ -787,6 +836,16 @@ class Agent {
|
|
|
787
836
|
}
|
|
788
837
|
const ev = codeEvent(tc, pre);
|
|
789
838
|
if (ev) yield "\n" + codeMarkdown(ev) + "\n";
|
|
839
|
+
// Auto syntax check: broken AI-written code gets one bounce-back.
|
|
840
|
+
if (ev && (tc.name === "write_file" || tc.name === "edit_file")
|
|
841
|
+
&& this._autoChecks < 2) {
|
|
842
|
+
const err = syntaxCheckError(ev.path);
|
|
843
|
+
if (err) {
|
|
844
|
+
this._autoChecks++;
|
|
845
|
+
yield `\n[auto-check] ${path.basename(ev.path)} has a syntax error - fixing...\n`;
|
|
846
|
+
this.history.push({ role: "user", content: autoCheckMsg(ev.path, err), ts: Date.now() / 1000 });
|
|
847
|
+
}
|
|
848
|
+
}
|
|
790
849
|
}
|
|
791
850
|
}
|
|
792
851
|
} else {
|
|
@@ -906,4 +965,5 @@ class Agent {
|
|
|
906
965
|
}
|
|
907
966
|
}
|
|
908
967
|
|
|
909
|
-
module.exports = { Agent, extractJsonTools, repairToolJson, wantsAction,
|
|
968
|
+
module.exports = { Agent, extractJsonTools, repairToolJson, wantsAction,
|
|
969
|
+
repliedWithoutActing, stalledHalfway, syntaxCheckError, autoCheckMsg };
|
package/src/render.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
// Terminal chat renderer, opencode-style:
|
|
4
|
-
// - live-updating output (SmoothPrinter repaints in place
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// - live-updating output (SmoothPrinter repaints in place; every line is
|
|
5
|
+
// hard-truncated with WIDE-CHAR-AWARE measuring so the terminal never
|
|
6
|
+
// soft-wraps behind our back - that desync used to duplicate text)
|
|
7
|
+
// - fenced code blocks drawn as rounded boxes with macOS-style
|
|
8
|
+
// traffic-light circles and the FILE TITLE when known
|
|
7
9
|
// - light inline styling: bold/italic, `inline code`, headings, links
|
|
8
|
-
// Everything degrades
|
|
10
|
+
// Everything degrades gracefully: non-TTY or a mid-stream resize falls
|
|
11
|
+
// back to plain passthrough instead of ever duplicating output.
|
|
9
12
|
|
|
10
13
|
const ESC = "\x1b";
|
|
11
14
|
const C = {
|
|
@@ -16,23 +19,48 @@ const C = {
|
|
|
16
19
|
|
|
17
20
|
function stripAnsi(s) { return s.replace(/\x1b\[[0-9;]*m/g, ""); }
|
|
18
21
|
|
|
22
|
+
const WIDE_RANGES = [
|
|
23
|
+
[0x1100, 0x115f], [0x2e80, 0x303e], [0x3041, 0x33ff],
|
|
24
|
+
[0x3400, 0x4dbf], [0x4e00, 0x9fff], [0xa000, 0xa4cf],
|
|
25
|
+
[0xac00, 0xd7a3], [0xf900, 0xfaff], [0xfe30, 0xfe6f],
|
|
26
|
+
[0xff00, 0xff60], [0xffe0, 0xffe6], [0x1f300, 0x1f64f],
|
|
27
|
+
[0x1f900, 0x1f9ff], [0x20000, 0x3fffd],
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function charWidth(cp) {
|
|
31
|
+
if (cp === 0) return 0;
|
|
32
|
+
for (const [lo, hi] of WIDE_RANGES) {
|
|
33
|
+
if (cp >= lo && cp <= hi) return 2;
|
|
34
|
+
}
|
|
35
|
+
return cp > 0xffff ? 2 : 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Display width of a string as the terminal will draw it (ANSI skipped).
|
|
39
|
+
function visibleWidth(s) {
|
|
40
|
+
let w = 0;
|
|
41
|
+
const plain = stripAnsi(s);
|
|
42
|
+
for (const ch of plain) w += charWidth(ch.codePointAt(0));
|
|
43
|
+
return w;
|
|
44
|
+
}
|
|
45
|
+
|
|
19
46
|
function truncateVisible(s, max) {
|
|
20
47
|
if (visibleWidth(s) <= max) return s;
|
|
21
48
|
let out = "", w = 0;
|
|
22
49
|
for (let i = 0; i < s.length; i++) {
|
|
23
50
|
if (s[i] === ESC && s[i + 1] === "[") {
|
|
24
|
-
const m =
|
|
51
|
+
const m = /^\x1b\[[0-9;]*m/.exec(s.slice(i));
|
|
25
52
|
if (m) { out += m[0]; i += m[0].length - 1; continue; }
|
|
26
53
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
out
|
|
54
|
+
const cp = s.codePointAt(i);
|
|
55
|
+
const cw = charWidth(cp);
|
|
56
|
+
if (w + cw > max - 1) return out + C.dim + "…" + C.reset;
|
|
57
|
+
w += cw;
|
|
58
|
+
out += String.fromCodePoint(cp);
|
|
59
|
+
if (cp > 0xffff) i++;
|
|
30
60
|
}
|
|
31
61
|
return out;
|
|
32
62
|
}
|
|
33
63
|
|
|
34
|
-
function visibleWidth(s) { return stripAnsi(s).length; }
|
|
35
|
-
|
|
36
64
|
const KEYWORDS = new Set(("const let var function return if else for while class import " +
|
|
37
65
|
"from export default async await new try catch throw switch case break continue " +
|
|
38
66
|
"def lambda elif None True False self public private static void int float str " +
|
|
@@ -63,15 +91,64 @@ function padVisible(s, width) {
|
|
|
63
91
|
return s + " ".repeat(Math.max(0, pad));
|
|
64
92
|
}
|
|
65
93
|
|
|
66
|
-
|
|
94
|
+
const EXT_LANG = {
|
|
95
|
+
html: "html", htm: "html", css: "css", js: "javascript", mjs: "javascript",
|
|
96
|
+
cjs: "javascript", jsx: "jsx", ts: "typescript", tsx: "tsx", py: "python",
|
|
97
|
+
json: "json", md: "markdown", sh: "bash", bash: "bash", yml: "yaml",
|
|
98
|
+
yaml: "yaml", txt: "text", csv: "text", go: "go", rs: "rust", java: "java",
|
|
99
|
+
c: "c", h: "c", cpp: "cpp", cs: "csharp", rb: "ruby", php: "php",
|
|
100
|
+
sql: "sql", kt: "kotlin", swift: "swift", dart: "dart", lua: "lua",
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
function langFromFilename(name) {
|
|
104
|
+
const ext = (/\.([a-z0-9]+)$/i.exec(name) || [])[1];
|
|
105
|
+
return ext ? (EXT_LANG[ext.toLowerCase()] || ext.toLowerCase()) : "";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Fence info strings may carry the file title:
|
|
110
|
+
* ```js | ```js path=src/app.py | ```index.html
|
|
111
|
+
* Returns {lang, title}.
|
|
112
|
+
*/
|
|
113
|
+
function parseFenceInfo(info) {
|
|
114
|
+
const tokens = String(info || "").trim().split(/\s+/).filter(Boolean);
|
|
115
|
+
let lang = "", title = "";
|
|
116
|
+
for (const tok of tokens) {
|
|
117
|
+
const m = /^(?:path|file)=(.+)$/.exec(tok);
|
|
118
|
+
if (m) { title = m[1]; continue; }
|
|
119
|
+
if (!lang) lang = tok;
|
|
120
|
+
}
|
|
121
|
+
if (title) {
|
|
122
|
+
// A bare filename as the language slot also means title.
|
|
123
|
+
if (!lang && /\.[a-z0-9]+$/i.test(tokens[0] || "")) lang = "";
|
|
124
|
+
lang = lang || langFromFilename(title);
|
|
125
|
+
} else if (lang && /\.[a-z0-9]{1,8}$/i.test(lang) && !lang.includes("(")) {
|
|
126
|
+
// ```index.html style - treat as a filename
|
|
127
|
+
title = lang;
|
|
128
|
+
lang = langFromFilename(lang);
|
|
129
|
+
}
|
|
130
|
+
return { lang, title };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function renderCodeBox(info, lines, width) {
|
|
67
134
|
const inner = Math.max(20, width - 4);
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
135
|
+
const { lang, title } = typeof info === "string" ? parseFenceInfo(info) : info;
|
|
136
|
+
// macOS window chrome: three traffic lights, then title (file name when
|
|
137
|
+
// known, otherwise the language tag).
|
|
138
|
+
const lights = `${C.red}●${C.reset} ${C.yellow}●${C.reset} ${C.green}●${C.reset}`;
|
|
139
|
+
const label = title
|
|
140
|
+
? `${lights} ${C.dim}─${C.reset} ${C.bold}${C.cyan}${truncateVisible(title, Math.max(6, inner - 12))}${C.reset}`
|
|
141
|
+
: (lang
|
|
142
|
+
? `${lights} ${C.dim}─${C.reset} ${C.bold}${lang}${C.reset}`
|
|
143
|
+
: lights);
|
|
144
|
+
const used = visibleWidth(label);
|
|
145
|
+
// Top border must fit within `width` like every other line:
|
|
146
|
+
// "╭─ " (3) + label + " " (1) + dash <= width
|
|
147
|
+
const dash = C.dim + "─".repeat(Math.max(1, width - used - 5)) + C.reset;
|
|
148
|
+
const top = truncateVisible(`╭─ ${label} ${dash}`, width);
|
|
72
149
|
const body = lines.map((l) =>
|
|
73
150
|
`${C.dim}│${C.reset} ` + padVisible(truncateVisible(tintCode(l), inner), inner) + ` ${C.dim}│${C.reset}`);
|
|
74
|
-
return [top
|
|
151
|
+
return [top, ...body,
|
|
75
152
|
C.dim + "╰" + "─".repeat(inner + 2) + "╯" + C.reset];
|
|
76
153
|
}
|
|
77
154
|
|
|
@@ -86,35 +163,35 @@ function renderInline(s) {
|
|
|
86
163
|
/**
|
|
87
164
|
* Render a full assistant message for the terminal.
|
|
88
165
|
* Fenced blocks become rounded boxes; prose gets light markdown styling.
|
|
89
|
-
*
|
|
166
|
+
* EVERY emitted line is guaranteed to fit within `width` display cells,
|
|
167
|
+
* so the terminal never soft-wraps and repaint math stays exact.
|
|
90
168
|
*/
|
|
91
169
|
function renderMarkdown(text, width = 100) {
|
|
92
|
-
width = Math.max(40, Math.min(
|
|
170
|
+
width = Math.max(40, Math.min(200, width));
|
|
93
171
|
const src = String(text ?? "").split("\n");
|
|
94
172
|
const out = [];
|
|
95
|
-
let inFence = false,
|
|
173
|
+
let inFence = false, info = "", buf = [];
|
|
96
174
|
const flushFence = () => {
|
|
97
|
-
out.push(...renderCodeBox(
|
|
98
|
-
buf = [];
|
|
175
|
+
out.push(...renderCodeBox(info, buf, width));
|
|
176
|
+
buf = []; info = "";
|
|
99
177
|
};
|
|
100
178
|
for (const line of src) {
|
|
101
179
|
const m = /^\s*```(.*)$/.exec(line);
|
|
102
180
|
if (m) {
|
|
103
181
|
if (inFence) { flushFence(); inFence = false; }
|
|
104
|
-
else { inFence = true;
|
|
182
|
+
else { inFence = true; info = m[1]; }
|
|
105
183
|
continue;
|
|
106
184
|
}
|
|
107
185
|
if (inFence) { buf.push(line); continue; }
|
|
108
186
|
const h = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
109
187
|
if (h) {
|
|
110
|
-
out.push(C.bold + C.cyan + h[2] + C.reset);
|
|
188
|
+
out.push(truncateVisible(C.bold + C.cyan + h[2] + C.reset, width));
|
|
111
189
|
continue;
|
|
112
190
|
}
|
|
113
191
|
if (/^\s*(-{3,}|={3,}|\*{3,})\s*$/.test(line)) {
|
|
114
|
-
out.push(C.dim + "─".repeat(width) + C.reset);
|
|
192
|
+
out.push(truncateVisible(C.dim + "─".repeat(width) + C.reset, width));
|
|
115
193
|
continue;
|
|
116
194
|
}
|
|
117
|
-
if (/^\s*>\s?/.test(line)) { out.push(C.dim + line + C.reset); continue; }
|
|
118
195
|
out.push(truncateVisible(renderInline(line), width));
|
|
119
196
|
}
|
|
120
197
|
if (inFence) flushFence();
|
|
@@ -124,15 +201,17 @@ function renderMarkdown(text, width = 100) {
|
|
|
124
201
|
const MAX_PREVIEW_CHARS = 60000;
|
|
125
202
|
|
|
126
203
|
/**
|
|
127
|
-
* In-place repainting stream target.
|
|
128
|
-
*
|
|
204
|
+
* In-place repainting stream target.
|
|
205
|
+
* TTY: buffers chunks and repaints the rendered message every ~60ms.
|
|
206
|
+
* Non-TTY / resized mid-stream / absurdly tall paints: raw passthrough,
|
|
207
|
+
* because a WRONG erase duplicates text ("spamming") - never risk it.
|
|
129
208
|
*/
|
|
130
209
|
class SmoothPrinter {
|
|
131
210
|
constructor(out = process.stdout) {
|
|
132
211
|
this.out = out;
|
|
133
212
|
this.tty = !!(out.isTTY && process.env.TERM !== "dumb"
|
|
134
213
|
&& !process.env.OGPT_PLAIN);
|
|
135
|
-
this.
|
|
214
|
+
this.widthOf = () => Math.max(40, Math.min(200, (out.columns || 100) - 2));
|
|
136
215
|
}
|
|
137
216
|
|
|
138
217
|
begin() {
|
|
@@ -140,19 +219,21 @@ class SmoothPrinter {
|
|
|
140
219
|
this.drawn = 0;
|
|
141
220
|
this.lastPaint = 0;
|
|
142
221
|
this.timer = null;
|
|
222
|
+
this.closed = false;
|
|
223
|
+
this.degraded = !this.tty;
|
|
224
|
+
this.paintWidth = null;
|
|
143
225
|
if (!this.tty) return;
|
|
144
|
-
// Hide the cursor while we repaint to avoid flicker.
|
|
145
226
|
this.out.write(`${ESC}[?25l`);
|
|
146
227
|
}
|
|
147
228
|
|
|
148
229
|
feed(chunk) {
|
|
149
|
-
if (
|
|
230
|
+
if (this.degraded) { this.out.write(chunk); return; }
|
|
150
231
|
this.buf += chunk;
|
|
151
232
|
if (this.buf.length > MAX_PREVIEW_CHARS * 1.5) {
|
|
152
233
|
this.buf = this.buf.slice(-MAX_PREVIEW_CHARS);
|
|
153
234
|
}
|
|
154
235
|
if (!this.timer) {
|
|
155
|
-
this.timer = setTimeout(() => { this.timer = null; this._paint(); },
|
|
236
|
+
this.timer = setTimeout(() => { this.timer = null; this._paint(); }, 60);
|
|
156
237
|
}
|
|
157
238
|
}
|
|
158
239
|
|
|
@@ -162,25 +243,55 @@ class SmoothPrinter {
|
|
|
162
243
|
}
|
|
163
244
|
|
|
164
245
|
_paint() {
|
|
165
|
-
if (
|
|
246
|
+
if (this.closed || this.degraded) return;
|
|
166
247
|
const now = Date.now();
|
|
167
|
-
if (now - this.lastPaint <
|
|
168
|
-
this.
|
|
248
|
+
if (now - this.lastPaint < 50) return;
|
|
249
|
+
const cols = this.out.columns;
|
|
250
|
+
// Terminal resized since the last paint: old rows were wrapped at a
|
|
251
|
+
// different width, so erasing would leave ghosts -> degrade safely.
|
|
252
|
+
if (this.paintWidth !== null && cols && cols !== this.paintWidth) {
|
|
253
|
+
this._degrade();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const width = this.widthOf();
|
|
169
257
|
let preview = this.buf;
|
|
170
258
|
if (preview.length > MAX_PREVIEW_CHARS) preview = preview.slice(-MAX_PREVIEW_CHARS);
|
|
171
|
-
const rendered = renderMarkdown(preview,
|
|
259
|
+
const rendered = renderMarkdown(preview, width);
|
|
260
|
+
const lines = rendered.split("\n");
|
|
261
|
+
const rows = Math.max(0, lines.length - 1);
|
|
262
|
+
const maxRows = (this.out.rows || 2000) - 1;
|
|
263
|
+
if (rows > maxRows) {
|
|
264
|
+
// Taller than the screen: scrolling would already have eaten the top
|
|
265
|
+
// rows - repainting can't reach them. Degrade instead of duplicating.
|
|
266
|
+
this._degrade(true);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
this._erase();
|
|
172
270
|
this.out.write(rendered);
|
|
173
|
-
this.drawn =
|
|
271
|
+
this.drawn = rows;
|
|
272
|
+
this.paintWidth = cols || width;
|
|
174
273
|
this.lastPaint = now;
|
|
175
274
|
}
|
|
176
275
|
|
|
276
|
+
_degrade(skipFlush) {
|
|
277
|
+
this.degraded = true;
|
|
278
|
+
// Do NOT reprint the buffer here - part of it is already on screen
|
|
279
|
+
// and repainting would duplicate it. Just mark the seam and let the
|
|
280
|
+
// remaining chunks stream raw.
|
|
281
|
+
this.out.write("\n");
|
|
282
|
+
this.buf = "";
|
|
283
|
+
}
|
|
284
|
+
|
|
177
285
|
finish() {
|
|
178
286
|
if (this.closed) return;
|
|
179
287
|
this.closed = true;
|
|
180
288
|
if (this.timer) { clearTimeout(this.timer); this.timer = null; }
|
|
181
289
|
if (!this.tty) return;
|
|
290
|
+
// Always un-hide the cursor, degraded or not.
|
|
291
|
+
if (this.degraded) { this.out.write(`${ESC}[?25h`); return; }
|
|
182
292
|
this._erase();
|
|
183
|
-
|
|
293
|
+
const width = this.widthOf();
|
|
294
|
+
this.out.write(renderMarkdown(this.buf, width) + "\n");
|
|
184
295
|
this.out.write(`${ESC}[?25h`);
|
|
185
296
|
}
|
|
186
297
|
|
|
@@ -193,4 +304,5 @@ class SmoothPrinter {
|
|
|
193
304
|
}
|
|
194
305
|
}
|
|
195
306
|
|
|
196
|
-
module.exports = { renderMarkdown, SmoothPrinter, tintCode, visibleWidth
|
|
307
|
+
module.exports = { renderMarkdown, SmoothPrinter, tintCode, visibleWidth,
|
|
308
|
+
parseFenceInfo, truncateVisible };
|