ucode-agent 1.29.0 → 1.30.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/package.json +1 -1
- package/src/ui/activity.js +98 -2
- package/src/ui/screen.js +69 -41
- package/src/ui/theme.js +68 -3
package/package.json
CHANGED
package/src/ui/activity.js
CHANGED
|
@@ -161,6 +161,102 @@ export function bannerSweep(line, row, rows, elapsed, { level = chalk.level } =
|
|
|
161
161
|
return out;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The bar that says work is happening, with no claim about how much is left.
|
|
167
|
+
*
|
|
168
|
+
* A spinner turning in one cell says "alive". A band of light running along a
|
|
169
|
+
* track says "alive, and going somewhere", which is the honest amount of
|
|
170
|
+
* progress an agent can report — it does not know how many steps are left, so
|
|
171
|
+
* a filling bar would be a lie and an indeterminate one is not.
|
|
172
|
+
*
|
|
173
|
+
* The band runs off both ends rather than bouncing inside the track: a bounce
|
|
174
|
+
* draws the eye to the turn, where a pass reads as something continuous going
|
|
175
|
+
* by. Below 256 colours there is nothing to fade through, so the track is
|
|
176
|
+
* simply dim and still, and the spinner beside it carries the motion.
|
|
177
|
+
*/
|
|
178
|
+
export const BAR_CELLS = 12;
|
|
179
|
+
|
|
180
|
+
/** The unlit track, and the crest of the band running along it. */
|
|
181
|
+
const TRACK_RGB = [0x24, 0x34, 0x4f];
|
|
182
|
+
const CREST_RGB = [0x8f, 0xbc, 0xff];
|
|
183
|
+
|
|
184
|
+
/** One pass of the band, in milliseconds. */
|
|
185
|
+
const BAR_PERIOD = 1400;
|
|
186
|
+
|
|
187
|
+
/** Half the width of the band, in cells. */
|
|
188
|
+
const BAR_BAND = 3.5;
|
|
189
|
+
|
|
190
|
+
export function indeterminate(cells, t, { level = chalk.level } = {}) {
|
|
191
|
+
const width = Math.max(0, Math.floor(cells));
|
|
192
|
+
if (!width) return '';
|
|
193
|
+
const track = '━'.repeat(width);
|
|
194
|
+
if (level < 2) return dim(track);
|
|
195
|
+
|
|
196
|
+
const centre = ((Math.max(0, t) % BAR_PERIOD) / BAR_PERIOD) * (width + BAR_BAND * 2) - BAR_BAND;
|
|
197
|
+
|
|
198
|
+
let out = '';
|
|
199
|
+
let run = '';
|
|
200
|
+
let runStep = -1;
|
|
201
|
+
const flush = () => {
|
|
202
|
+
if (!run) return;
|
|
203
|
+
const [r, g, b] = mix(TRACK_RGB, CREST_RGB, runStep / STEPS);
|
|
204
|
+
out += painter(level).rgb(r, g, b)(run);
|
|
205
|
+
run = '';
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
for (let i = 0; i < width; i++) {
|
|
209
|
+
const distance = Math.abs(i - centre);
|
|
210
|
+
const k = distance < BAR_BAND ? (Math.cos((Math.PI * distance) / BAR_BAND) + 1) / 2 : 0;
|
|
211
|
+
const step = Math.round(k * STEPS);
|
|
212
|
+
if (step !== runStep) { flush(); runStep = step; }
|
|
213
|
+
run += track[i];
|
|
214
|
+
}
|
|
215
|
+
flush();
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The whole live line: what is happening, that it is still happening, how long
|
|
221
|
+
* it has been happening, and how to stop it.
|
|
222
|
+
*
|
|
223
|
+
* This used to be squeezed into whatever the status row had spare between the
|
|
224
|
+
* model name and the percentage, which is why the label shimmered — it was the
|
|
225
|
+
* only way to look alive in twenty columns. With a row of its own the motion
|
|
226
|
+
* moves to the bar and the label can simply be read.
|
|
227
|
+
*
|
|
228
|
+
* Things are given up from the least useful end as the terminal narrows: the
|
|
229
|
+
* hint first, then the bar shortens, then it goes, then the label is clipped.
|
|
230
|
+
* The spinner is the last thing standing, because a line with nothing moving
|
|
231
|
+
* on it says the program has hung.
|
|
232
|
+
*/
|
|
233
|
+
export function workingLine({
|
|
234
|
+
glyph, label = '', elapsed = '', hint = 'esc to stop', room, t = 0, level = chalk.level,
|
|
235
|
+
} = {}) {
|
|
236
|
+
const text = String(label ?? '');
|
|
237
|
+
if (room < 3) return glyph;
|
|
238
|
+
|
|
239
|
+
const tail = [elapsed, hint].filter(Boolean).join(' ');
|
|
240
|
+
const shortTail = elapsed || '';
|
|
241
|
+
|
|
242
|
+
const draw = (cells, tailText, labelRoom) => {
|
|
243
|
+
let out = `${glyph} ${sky(clip(text, labelRoom))}`;
|
|
244
|
+
if (cells) out += ` ${indeterminate(cells, t, { level })}`;
|
|
245
|
+
if (tailText) out += ` ${dim(tailText)}`;
|
|
246
|
+
return out;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
for (const [cells, tailText] of [
|
|
250
|
+
[BAR_CELLS, tail], [BAR_CELLS, shortTail], [8, shortTail], [0, shortTail], [0, ''],
|
|
251
|
+
]) {
|
|
252
|
+
const fixed = 2 + (cells ? cells + 3 : 0) + (tailText ? tailText.length + 3 : 0);
|
|
253
|
+
const labelRoom = room - fixed;
|
|
254
|
+
if (labelRoom >= Math.min(MIN_LABEL, text.length)) return draw(cells, tailText, labelRoom);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return `${glyph} ${sky(clip(text, Math.max(1, room - 2)))}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
164
260
|
/**
|
|
165
261
|
* The spinner glyph for a frame, breathing slowly between two blues.
|
|
166
262
|
*
|
|
@@ -170,8 +266,8 @@ export function bannerSweep(line, row, rows, elapsed, { level = chalk.level } =
|
|
|
170
266
|
export function spinnerGlyph(frame, t, { level = chalk.level } = {}) {
|
|
171
267
|
const glyph = SPINNER[((frame % SPINNER.length) + SPINNER.length) % SPINNER.length];
|
|
172
268
|
if (level < 2) return theme.blue(glyph);
|
|
173
|
-
const k = (Math.sin((Math.max(0, t) /
|
|
174
|
-
const [r, g, b] = mix([
|
|
269
|
+
const k = (Math.sin((Math.max(0, t) / 1000) * Math.PI * 2) + 1) / 2;
|
|
270
|
+
const [r, g, b] = mix([0x2f, 0x6f, 0xe0], [0x9f, 0xc6, 0xff], k);
|
|
175
271
|
return painter(level).rgb(r, g, b)(glyph);
|
|
176
272
|
}
|
|
177
273
|
|
package/src/ui/screen.js
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
boxTop, boxBottom, boxRow, visLen, padVis, clip, wrapAnsi,
|
|
42
42
|
shortenPath, asLabel, ensureColour, planLine, bare, narration, narrationMark, groupKind, groupLabel, groupTarget, runLine, planRows, tidyReply, trimAnswer,
|
|
43
43
|
bannerPaint, RAIL, modeChip } from './theme.js';
|
|
44
|
-
import { FRAME_MS,
|
|
44
|
+
import { FRAME_MS, spinnerGlyph, formatDuration, doneLine, workingLine, bannerSweep, SWEEP_MS } from './activity.js';
|
|
45
45
|
import { gitBranch } from '../core/git.js';
|
|
46
46
|
import { renderer, render, polish } from './markdown.js';
|
|
47
47
|
import { VERSION } from '../core/version.js';
|
|
@@ -100,10 +100,16 @@ const at = (row, col) => `${ESC}[${row};${col}H`;
|
|
|
100
100
|
const title = (t) => `${ESC}]0;${t}\x07`;
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
|
-
* Fixed rows below the header: the gap under it, the
|
|
104
|
-
* the input box's two borders, the blank row inside it, and the
|
|
103
|
+
* Fixed rows below the header: the gap under it, the live line, the gap above
|
|
104
|
+
* the input box, the box's two borders, the blank row inside it, and the
|
|
105
|
+
* status row.
|
|
106
|
+
*
|
|
107
|
+
* The live line's row is held whether anything is running or not. Adding it
|
|
108
|
+
* only while busy would move the whole transcript up a row at the start of
|
|
109
|
+
* every turn and back down at the end of it, which reads as the screen
|
|
110
|
+
* flinching each time you press return.
|
|
105
111
|
*/
|
|
106
|
-
const CHROME_BELOW =
|
|
112
|
+
const CHROME_BELOW = 7;
|
|
107
113
|
|
|
108
114
|
/** How long one sentence of reasoning holds the line before the next takes it. */
|
|
109
115
|
const THOUGHT_HOLD_MS = 1100;
|
|
@@ -186,6 +192,7 @@ export class Screen {
|
|
|
186
192
|
this.intro = 0; // when the launch sweep began, 0 once it is over
|
|
187
193
|
this.introTimer = null;
|
|
188
194
|
this.pendingPrompt = null;
|
|
195
|
+
this.lastPrompt = ''; // the last thing the user said, for the echo check
|
|
189
196
|
this.facts.branch = gitBranch(cwd);
|
|
190
197
|
|
|
191
198
|
this.cols = output.columns || 80;
|
|
@@ -340,7 +347,8 @@ export class Screen {
|
|
|
340
347
|
*/
|
|
341
348
|
assistant(text, { closing = false } = {}) {
|
|
342
349
|
if (!text?.trim()) return;
|
|
343
|
-
const
|
|
350
|
+
const tidy = tidyReply(text, 4, this.lastPrompt);
|
|
351
|
+
const body = closing ? trimAnswer(tidy) : tidy;
|
|
344
352
|
if (!body.trim()) return;
|
|
345
353
|
this.endRun();
|
|
346
354
|
this.add('');
|
|
@@ -367,6 +375,9 @@ export class Screen {
|
|
|
367
375
|
* loud as the input box, none of them saying anything the rail does not.
|
|
368
376
|
*/
|
|
369
377
|
userMessage(text) {
|
|
378
|
+
// Kept so the reply can be checked against it: an answer that opens by
|
|
379
|
+
// saying the request back is repeating the line directly above it.
|
|
380
|
+
this.lastPrompt = String(text ?? '');
|
|
370
381
|
const room = Math.max(8, this.width() - 2); // the rail and the space after it
|
|
371
382
|
|
|
372
383
|
const rows = [];
|
|
@@ -730,14 +741,21 @@ export class Screen {
|
|
|
730
741
|
const room = Math.max(8, inner - BANNER_WIDTH - 6);
|
|
731
742
|
const value = (v) => clip(String(v), Math.max(4, room - 10));
|
|
732
743
|
const branch = this.facts.branch;
|
|
744
|
+
|
|
745
|
+
// The facts run together from the top with nothing between them, the credit
|
|
746
|
+
// sits on the last row, and whatever is left over is the gap between the
|
|
747
|
+
// two. Holding a row empty in the middle of the list — which is what a
|
|
748
|
+
// fixed six-row layout did when a fact was missing — reads as a line that
|
|
749
|
+
// failed to draw rather than as spacing.
|
|
733
750
|
const facts = [
|
|
734
751
|
['dir', value(shortenPath(this.facts.cwd ?? this.cwd, room - 10))],
|
|
735
|
-
branch
|
|
736
|
-
VERSION
|
|
737
|
-
['', ''],
|
|
752
|
+
branch && ['branch', value(branch)],
|
|
753
|
+
VERSION && ['version', value(this.facts.update ? `${VERSION} → ${this.facts.update} next start` : VERSION)],
|
|
738
754
|
['keys', value('/help · esc interrupts · ctrl+b plan')],
|
|
739
|
-
|
|
740
|
-
|
|
755
|
+
].filter(Boolean).slice(0, BANNER.length - 1);
|
|
756
|
+
|
|
757
|
+
while (facts.length < BANNER.length - 1) facts.push(['', '']);
|
|
758
|
+
facts.push(['', 'made with ❤️ by om dixit']);
|
|
741
759
|
|
|
742
760
|
const rows = BANNER.map((art, i) => {
|
|
743
761
|
const [label, text] = facts[i] ?? ['', ''];
|
|
@@ -903,8 +921,10 @@ export class Screen {
|
|
|
903
921
|
* the same on every line of every session, so it was decoration that had to
|
|
904
922
|
* be read past to reach the two things that do change.
|
|
905
923
|
*
|
|
906
|
-
* The
|
|
907
|
-
*
|
|
924
|
+
* The spinner used to borrow the middle of this row while something ran. It
|
|
925
|
+
* has a row of its own outside the box now, so the only thing that ever
|
|
926
|
+
* appears between the model and the percentage is a flash — a reply to
|
|
927
|
+
* something you just pressed, gone a moment later.
|
|
908
928
|
*/
|
|
909
929
|
statusRow(width = this.width()) {
|
|
910
930
|
const inner = width - 2; // the space between the two borders
|
|
@@ -916,41 +936,44 @@ export class Screen {
|
|
|
916
936
|
this.chipTo = 2 + visLen(chip);
|
|
917
937
|
|
|
918
938
|
const between = Math.max(1, inner - visLen(left) - visLen(right));
|
|
939
|
+
const middle = this.flashText ? dim(clip(this.flashText, between - 2)) : '';
|
|
919
940
|
|
|
920
|
-
let middle = '';
|
|
921
|
-
if (this.flashText) {
|
|
922
|
-
middle = dim(clip(this.flashText, between - 2));
|
|
923
|
-
} else if (this.status.busy || this.activity) {
|
|
924
|
-
// The whole turn, not just the current tool: the timer and step count
|
|
925
|
-
// keep going through the gaps between calls, so a long build never
|
|
926
|
-
// looks like it has stopped.
|
|
927
|
-
const now = Date.now();
|
|
928
|
-
const a = this.activity;
|
|
929
|
-
const since = a?.start ?? this.status.since ?? now;
|
|
930
|
-
const meta = [];
|
|
931
|
-
// No step count. It measures how much machinery ran, which is not
|
|
932
|
-
// something the person waiting has any use for; the elapsed time is.
|
|
933
|
-
void stepPaint;
|
|
934
|
-
if (now - since >= 1000) meta.push({ text: formatDuration(now - since), keep: true });
|
|
935
|
-
middle = fitActivity({
|
|
936
|
-
glyph: spinnerGlyph(this.tick, now),
|
|
937
|
-
label: this.status.busy ? this.status.text : 'working',
|
|
938
|
-
meta,
|
|
939
|
-
hint: 'esc to stop',
|
|
940
|
-
paint: (s) => shimmer(s, now),
|
|
941
|
-
}, between - 3);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
// The percentage is pinned to the right border whatever is in the middle,
|
|
945
|
-
// with a gap kept in front of it so a long spinner label cannot run into
|
|
946
|
-
// the number and read as part of it.
|
|
947
941
|
const tail = middle ? `${middle} ` : '';
|
|
948
942
|
const pad = Math.max(1, inner - visLen(left) - visLen(tail) - visLen(right));
|
|
949
943
|
return padVis(left + ' '.repeat(pad) + tail + right, inner);
|
|
950
944
|
}
|
|
951
945
|
|
|
952
946
|
/**
|
|
953
|
-
*
|
|
947
|
+
* What is happening right now: the spinner, what it is doing, how long it has
|
|
948
|
+
* been doing it, and the way out.
|
|
949
|
+
*
|
|
950
|
+
* It used to live in whatever space the status row had spare between the
|
|
951
|
+
* model name and the percentage — inside the box you type into, which is the
|
|
952
|
+
* one place on screen that is about you rather than about the agent. Out
|
|
953
|
+
* here it sits directly under the steps it belongs to, in the same column,
|
|
954
|
+
* and has room for a bar instead of a shimmer.
|
|
955
|
+
*/
|
|
956
|
+
activityLine(width = this.width()) {
|
|
957
|
+
if (!this.busy()) return '';
|
|
958
|
+
const now = Date.now();
|
|
959
|
+
const since = this.activity?.start ?? this.status.since ?? now;
|
|
960
|
+
return workingLine({
|
|
961
|
+
glyph: spinnerGlyph(this.tick, now),
|
|
962
|
+
label: this.status.busy ? this.status.text : 'working',
|
|
963
|
+
elapsed: now - since >= 1000 ? formatDuration(now - since) : '',
|
|
964
|
+
hint: 'esc to stop',
|
|
965
|
+
room: Math.max(4, width),
|
|
966
|
+
t: now,
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/** Which row the live line is painted on, 1-based. */
|
|
971
|
+
activityRowAt() {
|
|
972
|
+
return this.rows - this.inputLines().rows.length - 5;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Repaint only the moving rows, leaving the caret where the user left it.
|
|
954
977
|
*
|
|
955
978
|
* It is the second row from the bottom now — the box's own border is below
|
|
956
979
|
* it — so the row is written with its borders rather than as a bare line.
|
|
@@ -964,9 +987,11 @@ export class Screen {
|
|
|
964
987
|
return;
|
|
965
988
|
}
|
|
966
989
|
const [row, col] = this.caret();
|
|
990
|
+
const width = this.width();
|
|
967
991
|
this.output.write(
|
|
968
992
|
HIDE +
|
|
969
|
-
at(this.
|
|
993
|
+
at(this.activityRowAt(), 1) + CLEAR_LINE + padVis(this.activityLine(width), width) +
|
|
994
|
+
at(this.rows - 1, 1) + CLEAR_LINE + boxRow(this.statusRow(width), width, this.borderPaint()) +
|
|
970
995
|
at(row, col) + SHOW
|
|
971
996
|
);
|
|
972
997
|
}
|
|
@@ -1465,6 +1490,9 @@ export class Screen {
|
|
|
1465
1490
|
...this.headerLines(),
|
|
1466
1491
|
'',
|
|
1467
1492
|
...window,
|
|
1493
|
+
// What is happening right now, in the same column as the steps above it,
|
|
1494
|
+
// because it is the next one of those. Empty between turns.
|
|
1495
|
+
this.activityLine(width),
|
|
1468
1496
|
// Always one clear row between the last thing said and the box you type
|
|
1469
1497
|
// in. Without it the newest line of output sits against the border and
|
|
1470
1498
|
// reads as part of the input rather than as the answer above it.
|
package/src/ui/theme.js
CHANGED
|
@@ -535,7 +535,7 @@ export function withoutCodeBlocks(text, keepLines = 4) {
|
|
|
535
535
|
* colon pointing at nothing reads as the reply having been cut off. A short
|
|
536
536
|
* block stays: three lines showing a command to run belong in an answer.
|
|
537
537
|
*/
|
|
538
|
-
export function tidyReply(text, keepLines = 4) {
|
|
538
|
+
export function tidyReply(text, keepLines = 4, prompt = '') {
|
|
539
539
|
const MARK = "\u0000CUT\u0000";
|
|
540
540
|
const FENCE = new RegExp("```([A-Za-z0-9+-]*)\\n([\\s\\S]*?)```", "g");
|
|
541
541
|
|
|
@@ -545,11 +545,76 @@ export function tidyReply(text, keepLines = 4) {
|
|
|
545
545
|
});
|
|
546
546
|
|
|
547
547
|
const leadIn = new RegExp("(?:^|\\n)[^\\n]{0,80}:[ \t]*\\n+" + MARK, "g");
|
|
548
|
-
|
|
548
|
+
const body = marked
|
|
549
549
|
.replace(leadIn, "\n")
|
|
550
550
|
.split(MARK).join("")
|
|
551
551
|
.replace(new RegExp("\\n{3,}", "g"), "\n\n")
|
|
552
552
|
.trim();
|
|
553
|
+
return withoutRestatement(body, prompt);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* The words of a line worth comparing: lowercase, no punctuation, and nothing
|
|
558
|
+
* short enough to turn up in any sentence at all.
|
|
559
|
+
*/
|
|
560
|
+
const significant = (s) => String(s ?? '')
|
|
561
|
+
.toLowerCase()
|
|
562
|
+
.replace(/[^a-z0-9\s]+/g, ' ')
|
|
563
|
+
.split(/\s+/)
|
|
564
|
+
.filter((w) => w.length >= 3);
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Is this line the request handed back?
|
|
568
|
+
*
|
|
569
|
+
* A list of phrases catches the openings a model reaches for out of habit, but
|
|
570
|
+
* the commonest way of repeating a request is simply saying it again in the
|
|
571
|
+
* asker's own words — "A Next.js habit tracker with a clean dashboard, coming
|
|
572
|
+
* right up" — and no list will ever match that. So the line is compared with
|
|
573
|
+
* what was actually typed.
|
|
574
|
+
*
|
|
575
|
+
* Three guards keep it off real answers. A prompt of four significant words or
|
|
576
|
+
* fewer is never matched, because at that length an overlap means nothing. A
|
|
577
|
+
* line much longer than the prompt is saying more than the prompt did, so it
|
|
578
|
+
* is content. And the bar is four fifths of the prompt's words rather than a
|
|
579
|
+
* majority: an answer naturally shares nouns with the request that prompted
|
|
580
|
+
* it, and only something repeating nearly all of it is a repetition.
|
|
581
|
+
*/
|
|
582
|
+
export function echoesPrompt(line, prompt) {
|
|
583
|
+
const want = [...new Set(significant(prompt))];
|
|
584
|
+
if (want.length < 4) return false;
|
|
585
|
+
|
|
586
|
+
const text = String(line ?? '');
|
|
587
|
+
if (text.length > String(prompt ?? '').length * 2.5) return false;
|
|
588
|
+
|
|
589
|
+
const have = new Set(significant(text));
|
|
590
|
+
return want.filter((w) => have.has(w)).length / want.length >= 0.8;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* The reply with any opening that reads the request back taken off the front.
|
|
595
|
+
*
|
|
596
|
+
* This ran only on the closing message before, and it belongs on every one. A
|
|
597
|
+
* model that answers "You asked me to add a dark mode toggle — done" has spent
|
|
598
|
+
* its first line telling someone something they typed themselves, and the line
|
|
599
|
+
* directly above it on screen is already their own message, in their own
|
|
600
|
+
* words, against a rail. Two copies of the request and one of the answer is
|
|
601
|
+
* the wrong ratio.
|
|
602
|
+
*
|
|
603
|
+
* It never returns nothing. A reply that is only a restatement is still the
|
|
604
|
+
* whole of the reply, and an empty answer on screen reads as a crash.
|
|
605
|
+
*/
|
|
606
|
+
export function withoutRestatement(text, prompt = '') {
|
|
607
|
+
const rows = String(text ?? '').replace(/\r/g, '').split('\n');
|
|
608
|
+
|
|
609
|
+
let start = 0;
|
|
610
|
+
while (start < rows.length) {
|
|
611
|
+
const line = rows[start].trim();
|
|
612
|
+
if (!line) { start++; continue; }
|
|
613
|
+
if (!RESTATED.test(line) && !echoesPrompt(line, prompt)) break;
|
|
614
|
+
start++;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return rows.slice(start).join('\n').trim() || String(text ?? '').trim();
|
|
553
618
|
}
|
|
554
619
|
|
|
555
620
|
/**
|
|
@@ -568,7 +633,7 @@ export function tidyReply(text, keepLines = 4) {
|
|
|
568
633
|
export const ANSWER_LINES = 8;
|
|
569
634
|
const ANSWER_ROOM = 600; // eight wrapped lines of prose, for a reply with no line breaks in it
|
|
570
635
|
|
|
571
|
-
const RESTATED = /^(?:you (?:asked|wanted|requested|said)\b|the (?:request|task|ask)\b|as (?:you )?requested\b|here(?:'s| is) what (?:you asked|i)\b|to (?:summarise|summarize|recap)\b|(?:request|task|summary|recap|overview)\s*:)/i;
|
|
636
|
+
export const RESTATED = /^(?:(?:sure|ok|okay|got it|understood|alright|right)\b[\s,!.—-]*)?(?:you(?:'ve| have)? (?:asked|want|wanted|requested|said|would like|need)\b|the (?:request|task|ask)\b|as (?:you )?requested\b|here(?:'s| is) what (?:you asked|i)\b|i(?:'ll| will|'m going to| am going to) (?:build|create|make|add|write|implement)\b|let(?:'s| us) (?:build|create|make|add|write|implement)\b|to (?:summarise|summarize|recap)\b|(?:request|task|summary|recap|overview)\s*:)/i;
|
|
572
637
|
const CAVEAT = /\b(?:however|failed|couldn't|could not|cannot|can't|didn't|did not|isn't|is not|doesn't|does not|not (?:yet|wired|working|done|implemented)|missing|unfinished|except)\b/i;
|
|
573
638
|
const ACTIONABLE = /\b(?:open|run|serve|visit|try|start|npm|npx|node|pnpm|yarn)\b|https?:\/\/|\.(?:html?|css|jsx?|tsx?|md|json|py|rs|go)\b/i;
|
|
574
639
|
const BULLET = /^\s*(?:[-*•>]|\d+[.)]|[✓✔✅☑])\s+/;
|