@krmxd/onegpt 0.2.1-beta → 0.2.2-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 +5 -2
- package/src/render.js +151 -39
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -473,11 +473,14 @@ function diffLines(oldStr, newStr) {
|
|
|
473
473
|
}
|
|
474
474
|
|
|
475
475
|
function codeMarkdown(ev) {
|
|
476
|
+
// The fence info carries the file so renderers can show a titled,
|
|
477
|
+
// macOS-style code box: ```lang path=folder/file.ext
|
|
478
|
+
const rel = String(ev.path || "").replace(/^\/+/, "");
|
|
476
479
|
if (ev.action === "EDITED" && ev.old_content !== undefined) {
|
|
477
|
-
return "```diff\n" + diffLines(ev.old_content, ev.content).join("\n") + "\n```";
|
|
480
|
+
return "```diff path=" + rel + "\n" + diffLines(ev.old_content, ev.content).join("\n") + "\n```";
|
|
478
481
|
}
|
|
479
482
|
const lang = detectLang(ev.path);
|
|
480
|
-
return "```" + lang + "\n" + ev.content + "\n```";
|
|
483
|
+
return "```" + lang + " path=" + rel + "\n" + ev.content + "\n```";
|
|
481
484
|
}
|
|
482
485
|
|
|
483
486
|
class Agent {
|
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
|
+
if (!skipFlush && this.buf) {
|
|
279
|
+
// Hand back the un-styled tail so nothing is lost visually.
|
|
280
|
+
this.out.write("\n" + this.buf.slice(-4000) + "\n");
|
|
281
|
+
} else {
|
|
282
|
+
this.out.write("\n");
|
|
283
|
+
}
|
|
284
|
+
this.buf = "";
|
|
285
|
+
}
|
|
286
|
+
|
|
177
287
|
finish() {
|
|
178
288
|
if (this.closed) return;
|
|
179
289
|
this.closed = true;
|
|
180
290
|
if (this.timer) { clearTimeout(this.timer); this.timer = null; }
|
|
181
|
-
if (!this.tty) return;
|
|
291
|
+
if (!this.tty || this.degraded) 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 };
|