ucode-agent 1.53.0 → 1.56.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/README.md +17 -6
- package/THIRD_PARTY_NOTICES.md +34 -0
- package/package.json +64 -65
- package/src/core/loop.js +3067 -3117
- package/src/core/provider.js +1149 -1099
- package/src/core/window.js +84 -11
- package/src/tools/files.js +67 -13
- package/src/tools/fuzzy.js +295 -0
- package/src/tools/index.js +6 -40
- package/src/tools/scaffold.js +502 -528
- package/src/ui/plain.js +8 -2
- package/src/ui/screen.js +66 -16
- package/src/ui/theme.js +22 -2
package/src/ui/plain.js
CHANGED
|
@@ -186,14 +186,20 @@ export class Plain {
|
|
|
186
186
|
for (const line of lines) this.output.write(` ${dim(line)}\n`);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
assistant(text, { closing = false } = {}) {
|
|
190
|
-
const body = closing ? trimAnswer(tidyReply(text)) : tidyReply(text);
|
|
189
|
+
assistant(text, { closing = false, replay = false } = {}) {
|
|
190
|
+
const body = replay ? String(text) : (closing ? trimAnswer(tidyReply(text)) : tidyReply(text));
|
|
191
191
|
const out = render(this.md, body);
|
|
192
192
|
if (!out) return;
|
|
193
193
|
this.stopSpinner();
|
|
194
194
|
this.output.write(`\n${out}\n\n`);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
userMessage(text) {
|
|
198
|
+
this.stopSpinner();
|
|
199
|
+
const first = String(text ?? '').split('\n')[0] ?? '';
|
|
200
|
+
if (first.trim()) this.output.write(`\n${blue('›')} ${first}\n`);
|
|
201
|
+
}
|
|
202
|
+
|
|
197
203
|
narrate(text) {
|
|
198
204
|
const line = asLabel(text);
|
|
199
205
|
if (!line) return;
|
package/src/ui/screen.js
CHANGED
|
@@ -99,6 +99,34 @@ const NEWLINE = String.fromCharCode(10);
|
|
|
99
99
|
const at = (row, col) => `${ESC}[${row};${col}H`;
|
|
100
100
|
const title = (t) => `${ESC}]0;${t}\x07`;
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Every paint is wrapped in these. Autowrap off means a row that is one cell
|
|
104
|
+
* wider than we counted loses its last cell instead of wrapping onto the next
|
|
105
|
+
* row and scrolling the whole frame up — that scroll was the glitch. The
|
|
106
|
+
* synchronized-update pair makes terminals that support it show the frame in
|
|
107
|
+
* one go; the rest ignore it.
|
|
108
|
+
*/
|
|
109
|
+
const PAINT_BEGIN = `${ESC}[?2026h${ESC}[?7l`;
|
|
110
|
+
const PAINT_END = `${ESC}[?7h${ESC}[?2026l`;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Text as it may be drawn: colour codes kept, everything that moves the cursor
|
|
114
|
+
* gone. A carriage return from a CRLF file sent the padding back over the
|
|
115
|
+
* line, a tab took eight cells while it was counted as one, and a clear-screen
|
|
116
|
+
* from a tool's output wiped the frame mid-paint.
|
|
117
|
+
*
|
|
118
|
+
* The SGR colour codes (\x1b[..m) are matched first and kept. An earlier
|
|
119
|
+
* version only excluded them from the CSI branch, so the bare-\x1b fallback
|
|
120
|
+
* stripped the ESC off every colour code and left "[36m" littered across
|
|
121
|
+
* coloured lines — visible only in a real terminal, never in tests, which is
|
|
122
|
+
* why the glitch survived the suite.
|
|
123
|
+
*/
|
|
124
|
+
function printable(text) {
|
|
125
|
+
return String(text)
|
|
126
|
+
.replace(/\t/g, ' ')
|
|
127
|
+
.replace(/\x1b\[[0-9;]*m|(\x1b(?:\[[0-?]*[ -\/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)?|[()#][0-9A-Za-z]|[\x30-\x7e])?|[\x00-\x09\x0b-\x1a\x1c-\x1f\x7f])/g, (m, bad) => (bad ? '' : m));
|
|
128
|
+
}
|
|
129
|
+
|
|
102
130
|
/**
|
|
103
131
|
* Fixed rows below the header: the gap under it, the gap above the input box,
|
|
104
132
|
* the input box's two borders, the blank row inside it, and the status row.
|
|
@@ -181,6 +209,7 @@ export class Screen {
|
|
|
181
209
|
this.onModeChange = null;
|
|
182
210
|
this.spinTimer = null;
|
|
183
211
|
this.paintedBusy = false; // whose turn the frame on screen was drawn for
|
|
212
|
+
this.paintedLines = null; // transcript length at the last paint; growth since then belongs underneath a scrolled-back view
|
|
184
213
|
this.activity = null; // the turn in flight: when it began, how many steps
|
|
185
214
|
this.tick = 0; // animation frames painted, for the spinner
|
|
186
215
|
this.intro = 0; // when the launch sweep began, 0 once it is over
|
|
@@ -292,11 +321,10 @@ export class Screen {
|
|
|
292
321
|
*/
|
|
293
322
|
add(text = '') {
|
|
294
323
|
const width = this.width();
|
|
295
|
-
for (const raw of
|
|
324
|
+
for (const raw of printable(text).split('\n')) {
|
|
296
325
|
if (visLen(raw) <= width) this.lines.push(raw);
|
|
297
326
|
else for (const wrapped of wrapAnsi(raw, width)) this.lines.push(wrapped);
|
|
298
327
|
}
|
|
299
|
-
this.scroll = 0; // new output snaps back to the bottom
|
|
300
328
|
}
|
|
301
329
|
|
|
302
330
|
push(text = '') {
|
|
@@ -329,6 +357,7 @@ export class Screen {
|
|
|
329
357
|
clearScreen() {
|
|
330
358
|
this.lines = [];
|
|
331
359
|
this.scroll = 0;
|
|
360
|
+
this.paintedLines = null;
|
|
332
361
|
this.render();
|
|
333
362
|
}
|
|
334
363
|
|
|
@@ -339,10 +368,14 @@ export class Screen {
|
|
|
339
368
|
* the last thing left on screen, and what the whole session reads like
|
|
340
369
|
* afterwards, so it is cut to eight lines — see trimAnswer.
|
|
341
370
|
*/
|
|
342
|
-
assistant(text, { closing = false } = {}) {
|
|
371
|
+
assistant(text, { closing = false, replay = false, silent = false } = {}) {
|
|
343
372
|
if (!text?.trim()) return;
|
|
373
|
+
// A replayed answer goes back exactly as it was first shown: no tidying,
|
|
374
|
+
// no trimming, no re-read of the request. Tidy-up exists to keep a live
|
|
375
|
+
// answer short; on a resumed one it rewrites history, which is why a
|
|
376
|
+
// resumed session read like only fragments had survived.
|
|
344
377
|
const tidy = tidyReply(text, 4, this.lastPrompt);
|
|
345
|
-
const body = closing ? trimAnswer(tidy) : tidy;
|
|
378
|
+
const body = replay ? String(text) : (closing ? trimAnswer(tidy) : tidy);
|
|
346
379
|
if (!body.trim()) return;
|
|
347
380
|
this.endRun();
|
|
348
381
|
this.add('');
|
|
@@ -355,7 +388,7 @@ export class Screen {
|
|
|
355
388
|
this.add(render(this.md, body));
|
|
356
389
|
|
|
357
390
|
this.add('');
|
|
358
|
-
this.render();
|
|
391
|
+
if (!silent) this.render();
|
|
359
392
|
}
|
|
360
393
|
|
|
361
394
|
/**
|
|
@@ -368,10 +401,11 @@ export class Screen {
|
|
|
368
401
|
* ladder of rules across the page: two horizontal lines per message, each as
|
|
369
402
|
* loud as the input box, none of them saying anything the rail does not.
|
|
370
403
|
*/
|
|
371
|
-
userMessage(text) {
|
|
404
|
+
userMessage(text, { silent = false } = {}) {
|
|
372
405
|
// Kept so the reply can be checked against it: an answer that opens by
|
|
373
406
|
// saying the request back is repeating the line directly above it.
|
|
374
407
|
this.lastPrompt = String(text ?? '');
|
|
408
|
+
this.scroll = 0; // sending something is the one thing that jumps to the bottom
|
|
375
409
|
const room = Math.max(8, this.width() - 2); // the rail and the space after it
|
|
376
410
|
|
|
377
411
|
const rows = [];
|
|
@@ -385,7 +419,7 @@ export class Screen {
|
|
|
385
419
|
for (const row of rows) this.add(`${blue(RAIL)} ${chalk.white(row)}`);
|
|
386
420
|
this.add('');
|
|
387
421
|
this.add('');
|
|
388
|
-
this.render();
|
|
422
|
+
if (!silent) this.render();
|
|
389
423
|
}
|
|
390
424
|
|
|
391
425
|
/**
|
|
@@ -1019,10 +1053,10 @@ export class Screen {
|
|
|
1019
1053
|
const width = this.width();
|
|
1020
1054
|
const liveAt = this.activityRowAt();
|
|
1021
1055
|
this.output.write(
|
|
1022
|
-
HIDE +
|
|
1056
|
+
PAINT_BEGIN + HIDE +
|
|
1023
1057
|
(liveAt ? at(liveAt, 1) + CLEAR_LINE + padVis(this.activityLine(width), width) : '') +
|
|
1024
1058
|
at(this.rows - 1, 1) + CLEAR_LINE + boxRow(this.statusRow(width), width, this.borderPaint()) +
|
|
1025
|
-
at(row, col) + SHOW
|
|
1059
|
+
at(row, col) + SHOW + PAINT_END
|
|
1026
1060
|
);
|
|
1027
1061
|
}
|
|
1028
1062
|
|
|
@@ -1149,6 +1183,7 @@ export class Screen {
|
|
|
1149
1183
|
}
|
|
1150
1184
|
|
|
1151
1185
|
this.pendingPrompt = 'go ahead? [y/N]';
|
|
1186
|
+
this.scroll = 0; // the question has to be on screen to be answered
|
|
1152
1187
|
this.render();
|
|
1153
1188
|
|
|
1154
1189
|
return this.nextLine().then((answer) => {
|
|
@@ -1273,7 +1308,8 @@ export class Screen {
|
|
|
1273
1308
|
const before = this.scroll;
|
|
1274
1309
|
this.scroll = Math.min(Math.max(0, this.scroll + delta), max);
|
|
1275
1310
|
if (this.scroll === before && delta > 0) this.flash('already at the top');
|
|
1276
|
-
|
|
1311
|
+
// One wheel notch arrives as three arrow keys in one chunk; one frame, not three.
|
|
1312
|
+
this.soon();
|
|
1277
1313
|
}
|
|
1278
1314
|
|
|
1279
1315
|
/**
|
|
@@ -1511,6 +1547,20 @@ export class Screen {
|
|
|
1511
1547
|
const width = this.width();
|
|
1512
1548
|
const height = this.viewportHeight();
|
|
1513
1549
|
|
|
1550
|
+
// Scrolled back, the view stays on what is being read while output grows
|
|
1551
|
+
// underneath: every line added since the last paint extends the scroll by
|
|
1552
|
+
// the same amount, so the same rows stay on screen. Snapping to the
|
|
1553
|
+
// bottom on every streamed line fought the wheel sixteen times a second.
|
|
1554
|
+
// A streamed reply truncates its tail and rebuilds it on every delta, so
|
|
1555
|
+
// the growth since the last paint is net — but the tail sits below a
|
|
1556
|
+
// scrolled-back viewport, and net growth still moves the scroll by exactly
|
|
1557
|
+
// the amount that keeps the visible rows put.
|
|
1558
|
+
if (this.scroll > 0 && this.paintedLines != null) {
|
|
1559
|
+
const grown = this.lines.length - this.paintedLines;
|
|
1560
|
+
this.scroll = Math.min(Math.max(0, this.scroll + grown), Math.max(0, this.lines.length - height));
|
|
1561
|
+
}
|
|
1562
|
+
this.paintedLines = this.lines.length;
|
|
1563
|
+
|
|
1514
1564
|
// The live line is the last line of the conversation, not a fixture above
|
|
1515
1565
|
// the input box. Pinned down there it sat at the bottom of the screen while
|
|
1516
1566
|
// the message that started it was at the top, with the empty middle of the
|
|
@@ -1539,13 +1589,13 @@ export class Screen {
|
|
|
1539
1589
|
// The cursor is hidden for the duration of the paint. Without this it is
|
|
1540
1590
|
// dragged through every line as the frame is written, which shows up as a
|
|
1541
1591
|
// dot flickering above the input box on every keystroke.
|
|
1542
|
-
const out = [
|
|
1592
|
+
const out = [PAINT_BEGIN, HIDE];
|
|
1543
1593
|
for (let i = 0; i < this.rows; i++) {
|
|
1544
|
-
out.push(CLEAR_LINE + padVis(frame[i] ?? '', width)
|
|
1594
|
+
out.push(at(i + 1, 1) + CLEAR_LINE + padVis(frame[i] ?? '', width));
|
|
1545
1595
|
}
|
|
1546
1596
|
|
|
1547
1597
|
const [row, col] = this.caret();
|
|
1548
|
-
out.push(at(row, col) + SHOW);
|
|
1598
|
+
out.push(at(row, col) + SHOW + PAINT_END);
|
|
1549
1599
|
this.paintedBusy = this.busy();
|
|
1550
1600
|
this.output.write(out.join(''));
|
|
1551
1601
|
}
|
|
@@ -1637,12 +1687,12 @@ export class Screen {
|
|
|
1637
1687
|
frame[this.rows - 1] = ' '.repeat(Math.max(0, g.cols - visLen(tag) - 2)) + tag;
|
|
1638
1688
|
}
|
|
1639
1689
|
|
|
1640
|
-
const out = [
|
|
1690
|
+
const out = [PAINT_BEGIN, HIDE];
|
|
1641
1691
|
for (let i = 0; i < this.rows; i++) {
|
|
1642
|
-
out.push(CLEAR_LINE + padVis(frame[i], g.cols)
|
|
1692
|
+
out.push(at(i + 1, 1) + CLEAR_LINE + padVis(frame[i], g.cols));
|
|
1643
1693
|
}
|
|
1644
1694
|
const [row, col] = this.caret();
|
|
1645
|
-
out.push(at(row, col) + SHOW);
|
|
1695
|
+
out.push(at(row, col) + SHOW + PAINT_END);
|
|
1646
1696
|
this.paintedBusy = this.busy();
|
|
1647
1697
|
this.output.write(out.join(''));
|
|
1648
1698
|
}
|
package/src/ui/theme.js
CHANGED
|
@@ -174,6 +174,21 @@ export const bare = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, '');
|
|
|
174
174
|
* and a table of every Unicode width would be a megabyte to get the last
|
|
175
175
|
* fraction of a percent right.
|
|
176
176
|
*/
|
|
177
|
+
/**
|
|
178
|
+
* The emoji among the older symbol blocks — ✅ ❌ ⚡ ⭐ ⌛ and friends. They sit
|
|
179
|
+
* between box-drawing characters that are one cell, so they are listed one by
|
|
180
|
+
* one rather than as a range. Counted as one cell, a model's "✅ Done" line
|
|
181
|
+
* ran a column past the edge, wrapped, and scrolled the whole frame.
|
|
182
|
+
*/
|
|
183
|
+
const WIDE_SYMBOLS = new Set([
|
|
184
|
+
0x231a, 0x231b, 0x23e9, 0x23ea, 0x23eb, 0x23ec, 0x23f0, 0x23f3, 0x25fd, 0x25fe,
|
|
185
|
+
0x2614, 0x2615, 0x2648, 0x2649, 0x264a, 0x264b, 0x264c, 0x264d, 0x264e, 0x264f,
|
|
186
|
+
0x2650, 0x2651, 0x2652, 0x2653, 0x267f, 0x2693, 0x26a1, 0x26aa, 0x26ab, 0x26bd,
|
|
187
|
+
0x26be, 0x26c4, 0x26c5, 0x26ce, 0x26d4, 0x26ea, 0x26f2, 0x26f3, 0x26f5, 0x26fa,
|
|
188
|
+
0x26fd, 0x2705, 0x270a, 0x270b, 0x2728, 0x274c, 0x274e, 0x2753, 0x2754, 0x2755,
|
|
189
|
+
0x2757, 0x2795, 0x2796, 0x2797, 0x27b0, 0x27bf, 0x2b1b, 0x2b1c, 0x2b50, 0x2b55,
|
|
190
|
+
]);
|
|
191
|
+
|
|
177
192
|
export function charWidth(code) {
|
|
178
193
|
// Zero: combining marks, joiners, variation selectors.
|
|
179
194
|
if ((code >= 0x0300 && code <= 0x036f)
|
|
@@ -194,10 +209,15 @@ export function charWidth(code) {
|
|
|
194
209
|
|| (code >= 0xfe30 && code <= 0xfe6f)
|
|
195
210
|
|| (code >= 0xff00 && code <= 0xff60)
|
|
196
211
|
|| (code >= 0xffe0 && code <= 0xffe6)
|
|
212
|
+
|| (code >= 0x1f000 && code <= 0x1f02f)
|
|
213
|
+
|| code === 0x1f0cf
|
|
214
|
+
|| (code >= 0x1f18e && code <= 0x1f2ff)
|
|
197
215
|
|| (code >= 0x1f300 && code <= 0x1f64f)
|
|
198
216
|
|| (code >= 0x1f680 && code <= 0x1f6ff)
|
|
199
|
-
|| (code >=
|
|
200
|
-
|| (code >=
|
|
217
|
+
|| (code >= 0x1f7e0 && code <= 0x1f7eb)
|
|
218
|
+
|| (code >= 0x1f900 && code <= 0x1faff)
|
|
219
|
+
|| (code >= 0x20000 && code <= 0x3fffd)
|
|
220
|
+
|| WIDE_SYMBOLS.has(code)) return 2;
|
|
201
221
|
|
|
202
222
|
return 1;
|
|
203
223
|
}
|