ucode-agent 1.29.0 → 1.29.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/package.json +1 -1
- package/src/ui/screen.js +18 -6
- package/src/ui/theme.js +68 -3
package/package.json
CHANGED
package/src/ui/screen.js
CHANGED
|
@@ -186,6 +186,7 @@ export class Screen {
|
|
|
186
186
|
this.intro = 0; // when the launch sweep began, 0 once it is over
|
|
187
187
|
this.introTimer = null;
|
|
188
188
|
this.pendingPrompt = null;
|
|
189
|
+
this.lastPrompt = ''; // the last thing the user said, for the echo check
|
|
189
190
|
this.facts.branch = gitBranch(cwd);
|
|
190
191
|
|
|
191
192
|
this.cols = output.columns || 80;
|
|
@@ -340,7 +341,8 @@ export class Screen {
|
|
|
340
341
|
*/
|
|
341
342
|
assistant(text, { closing = false } = {}) {
|
|
342
343
|
if (!text?.trim()) return;
|
|
343
|
-
const
|
|
344
|
+
const tidy = tidyReply(text, 4, this.lastPrompt);
|
|
345
|
+
const body = closing ? trimAnswer(tidy) : tidy;
|
|
344
346
|
if (!body.trim()) return;
|
|
345
347
|
this.endRun();
|
|
346
348
|
this.add('');
|
|
@@ -367,6 +369,9 @@ export class Screen {
|
|
|
367
369
|
* loud as the input box, none of them saying anything the rail does not.
|
|
368
370
|
*/
|
|
369
371
|
userMessage(text) {
|
|
372
|
+
// Kept so the reply can be checked against it: an answer that opens by
|
|
373
|
+
// saying the request back is repeating the line directly above it.
|
|
374
|
+
this.lastPrompt = String(text ?? '');
|
|
370
375
|
const room = Math.max(8, this.width() - 2); // the rail and the space after it
|
|
371
376
|
|
|
372
377
|
const rows = [];
|
|
@@ -730,14 +735,21 @@ export class Screen {
|
|
|
730
735
|
const room = Math.max(8, inner - BANNER_WIDTH - 6);
|
|
731
736
|
const value = (v) => clip(String(v), Math.max(4, room - 10));
|
|
732
737
|
const branch = this.facts.branch;
|
|
738
|
+
|
|
739
|
+
// The facts run together from the top with nothing between them, the credit
|
|
740
|
+
// sits on the last row, and whatever is left over is the gap between the
|
|
741
|
+
// two. Holding a row empty in the middle of the list — which is what a
|
|
742
|
+
// fixed six-row layout did when a fact was missing — reads as a line that
|
|
743
|
+
// failed to draw rather than as spacing.
|
|
733
744
|
const facts = [
|
|
734
745
|
['dir', value(shortenPath(this.facts.cwd ?? this.cwd, room - 10))],
|
|
735
|
-
branch
|
|
736
|
-
VERSION
|
|
737
|
-
['', ''],
|
|
746
|
+
branch && ['branch', value(branch)],
|
|
747
|
+
VERSION && ['version', value(this.facts.update ? `${VERSION} → ${this.facts.update} next start` : VERSION)],
|
|
738
748
|
['keys', value('/help · esc interrupts · ctrl+b plan')],
|
|
739
|
-
|
|
740
|
-
|
|
749
|
+
].filter(Boolean).slice(0, BANNER.length - 1);
|
|
750
|
+
|
|
751
|
+
while (facts.length < BANNER.length - 1) facts.push(['', '']);
|
|
752
|
+
facts.push(['', 'made with ❤️ by om dixit']);
|
|
741
753
|
|
|
742
754
|
const rows = BANNER.map((art, i) => {
|
|
743
755
|
const [label, text] = facts[i] ?? ['', ''];
|
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+/;
|