ucode-agent 1.13.0 → 1.15.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucode-agent",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "description": "ucode - a terminal coding agent that reads, edits and runs your code, on NVIDIA and Cohere models.",
5
5
  "type": "module",
6
6
  "main": "ucode.js",
package/src/core/loop.js CHANGED
@@ -434,12 +434,41 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
434
434
  '',
435
435
  '## How to work',
436
436
  '',
437
- '- Say what you are about to do before you do it, in one short line, every time you',
438
- ' pick up a new piece of work: "Right, the HTML structure first.", "Now the state',
439
- ' and the render loop.", "That is the layout done - onto the animations." Without',
440
- ' it the screen is a list of file operations and the user cannot tell what you are',
441
- ' building or why. One sentence, in your own voice, then the actions. Not three',
442
- ' sentences, and not a restatement of the request.',
437
+ 'Before building anything, turn the request into a list of what it must do every',
438
+ 'feature named, and the ones any user would expect whether or not they were named',
439
+ '(an empty state, an error state, the keyboard doing the obvious thing, working on',
440
+ 'a phone). Keep it as the plan. Build against it, then go through it one item at a',
441
+ 'time before you say a word about being finished. Most of what gets missed was',
442
+ 'never written down.',
443
+ '',
444
+ 'Do not put code in your reply. Not a snippet, not "here is the key part", not a',
445
+ 'summary of the file. It is already in the file and the user can open it; pasting',
446
+ 'it again buries the one or two sentences that actually matter. Say what it does',
447
+ 'and what to try.',
448
+ '',
449
+ 'Do not claim it is done while anything is still running or unchecked. "I have',
450
+ 'built it" said before the build finishes is worse than saying nothing: the user',
451
+ 'believes you, looks, and finds it broken. Finish, check, then say so — and if',
452
+ 'something is incomplete, say which part and why.',
453
+ '',
454
+ 'FIRST, EVERY TIME: one short line saying what you are about to do, then the tool',
455
+ 'calls. Never open a turn with a tool call and no words. "Right, the HTML',
456
+ 'structure first." / "Now the state and the render loop." / "That is the layout',
457
+ 'done - onto the animations." / "Let me see what is there." One sentence, your own',
458
+ 'voice, before the actions - not after them, not instead of them, not a',
459
+ 'restatement of the request. The user watches this scroll past, and without those',
460
+ 'lines it is a list of file operations they cannot read intent from.',
461
+ '',
462
+ 'FIRST, EVERY TIME: write one short line saying what you are about to do, then',
463
+ 'make the tool calls. Never open a turn with a tool call and no words. Examples:',
464
+ '"Right, the HTML structure first." / "Now the state and the render loop." /',
465
+ '"That is the layout done - onto the animations." / "Let me see what is there."',
466
+ 'One sentence, your own voice, before the actions - not after them, not instead',
467
+ 'of them, and not a restatement of what was asked. The user is watching this',
468
+ 'scroll past; without those lines it is a list of file operations and they cannot',
469
+ 'tell what you are building. This matters as much as the code.',
470
+ '',
471
+
443
472
  '',
444
473
  'Before you guess at an API, ask: type_of gives the exact signature from the',
445
474
  'TypeScript this project has installed, and find_symbol says where something is declared without',
@@ -974,7 +1003,7 @@ export class Agent {
974
1003
  };
975
1004
  // Only a real terminal has somewhere to stream into.
976
1005
  if (this.full) {
977
- opts.onThinking = () => this.ui.thinkingDelta();
1006
+ opts.onThinking = (delta) => this.ui.thinkingDelta(delta);
978
1007
  opts.onText = (delta) => {
979
1008
  if (!streaming) {
980
1009
  streaming = true;
@@ -1370,9 +1399,13 @@ export class Agent {
1370
1399
  reportFailure(call, err) {
1371
1400
  if (!(err instanceof ToolFailure)) throw err;
1372
1401
 
1373
- this.ui.toolFailed(
1374
- err instanceof Declined ? 'declined' : `${err.kind}: ${err.failed}`
1375
- );
1402
+ // Bad arguments are the model talking to itself. "old_string and new_string
1403
+ // are identical" is a correction it will make on the next step, and it means
1404
+ // nothing to whoever is watching except that something went wrong. It goes
1405
+ // to the model, which can act on it, and not to the screen. A refusal the
1406
+ // user made, and anything that actually failed, still shows.
1407
+ if (err instanceof Declined) this.ui.toolFailed('declined');
1408
+ else if (err.kind !== 'bad_args') this.ui.toolFailed(`${err.kind}: ${err.failed}`);
1376
1409
  this.push({
1377
1410
  role: 'tool',
1378
1411
  toolCallId: call.id,
package/src/ui/screen.js CHANGED
@@ -297,40 +297,66 @@ export class Screen {
297
297
  // U+25CF, not U+23FA: the latter carries emoji presentation, which Windows
298
298
  // Terminal draws as a white circle on a blue tile.
299
299
  const kind = groupKind(label);
300
- const run = this.run;
301
- // The run's own line is either the last one, or the last but one with its
302
- // result underneath. Anything further down means something else was said
303
- // in between, and the run is over.
304
- const gap = run ? this.lines.length - 1 - run.at : Infinity;
305
-
306
- // A second step of the same kind rewrites the line the first one wrote,
307
- // rather than adding another almost-identical one beneath it.
308
- if (run && run.kind === kind && gap <= 1) {
309
- if (gap === 1) this.lines.pop(); // its single result line, now counted
300
+ // One line per kind of work for as long as the model is working on one
301
+ // thing. Reading, writing and reading again used to draw six lines that
302
+ // said three things; now the "Reading files" line it already has is the
303
+ // one that counts up, wherever it sits.
304
+ const run = (this.segment ??= new Map()).get(kind);
305
+
306
+ if (run && this.lines[run.at] !== undefined) {
310
307
  run.count++;
311
308
  run.label = label;
312
309
  run.targets.push(groupTarget(label));
313
- this.paintRun();
310
+ this.run = run;
311
+ this.paintRun({ live: true });
314
312
  } else {
315
313
  this.push(`${narrationMark()} ${narration(asLabel(label))}`);
316
314
  this.run = {
317
315
  kind, count: 1, at: this.lines.length - 1, label,
318
316
  targets: [groupTarget(label)], added: 0, removed: 0,
319
317
  };
318
+ this.segment.set(kind, this.run);
319
+ this.paintRun({ live: true });
320
320
  }
321
321
  this.updateSpinner(label);
322
322
  }
323
323
 
324
- /** Anything that is not another step of the same kind ends the run. */
325
- endRun() { this.run = null; }
324
+ /**
325
+ * The model speaking or a plan, or a failure — ends the segment.
326
+ *
327
+ * Up to that point a kind of work keeps one line and counts up on it. After
328
+ * it, the next read is a new piece of work and deserves its own line, which
329
+ * is what makes the transcript read as a sequence of things done rather
330
+ * than a set of running totals.
331
+ */
332
+ endRun() {
333
+ if (this.run) this.paintRun({ live: false });
334
+ this.run = null;
335
+ this.segment = new Map();
336
+ }
326
337
 
327
338
  /** Redraw the run's single line from what it has accumulated. */
328
- paintRun() {
339
+ /**
340
+ * Redraw the run's single line from what it has accumulated.
341
+ *
342
+ * While its step is still running the text shimmers, which is the only
343
+ * thing on screen saying "this is happening now" once the per-step result
344
+ * lines are gone. It settles to plain dim the moment the step finishes, so
345
+ * the finished ones above stay quiet.
346
+ */
347
+ paintRun({ live = this.run?.live } = {}) {
329
348
  if (!this.run) return;
330
- this.lines[this.run.at] = `${narrationMark()} ${narration(asLabel(runLine(this.run)))}`;
349
+ const text = asLabel(runLine(this.run));
350
+ this.run.live = live;
351
+ this.lines[this.run.at] = `${narrationMark()} ${live ? shimmer(text, this.tick * FRAME_MS) : narration(text)}`;
331
352
  this.render();
332
353
  }
333
354
 
355
+ /** Let the line in flight animate, one frame per tick. */
356
+ paintLiveRun() {
357
+ if (this.run?.live && this.lines[this.run.at] !== undefined) this.paintRun({ live: true });
358
+ }
359
+
334
360
  /**
335
361
  * A change, as its two numbers.
336
362
  *
@@ -361,7 +387,10 @@ export class Screen {
361
387
  * already names the step, and a change adds its numbers to that same line.
362
388
  * Only a failure earns a line of its own.
363
389
  */
364
- toolResult() {}
390
+ toolResult() {
391
+ // The step is over: the line stops moving and joins the quiet ones above.
392
+ if (this.run) this.paintRun({ live: false });
393
+ }
365
394
 
366
395
  toolFailed(summary) {
367
396
  // A failure is never folded away.
@@ -503,17 +532,54 @@ export class Screen {
503
532
  // than silence. The spinner counts the seconds so the wait is visibly alive,
504
533
  // and the transcript gets one line afterwards saying how long it took.
505
534
 
506
- thinkingDelta() {
535
+ /**
536
+ * The model's reasoning, live, one line at a time.
537
+ *
538
+ * Models reach for a tool before they say anything, so the first words of a
539
+ * step were arriving a minute after it began — while the reasoning channel
540
+ * had been streaming words the whole time and we were throwing them away to
541
+ * keep a timer. Its latest sentence now shows on one line that rewrites
542
+ * itself, which is something to read from the first second.
543
+ *
544
+ * It is scaffolding, not the answer: it shimmers while it is live and it is
545
+ * taken off the screen the moment the real reply starts.
546
+ */
547
+ thinkingDelta(text = '') {
507
548
  if (this.thoughtSince === undefined) this.thoughtSince = Date.now();
549
+ if (!text) return;
550
+
551
+ this.thought = ((this.thought ?? '') + text).slice(-2000);
552
+ // The last sentence it has finished, or what it has written of the next.
553
+ const parts = this.thought.split(/(?<=[.!?])\s+/).filter((p) => p.trim());
554
+ const latest = (parts[parts.length - 1] ?? '').replace(/\s+/g, ' ').trim();
555
+ if (!latest) return;
556
+
557
+ const line = ` ${shimmer(clip(latest, Math.max(20, this.width() - 6)), this.tick * FRAME_MS)}`;
558
+ if (this.thinkAt === undefined || this.lines[this.thinkAt] === undefined) {
559
+ this.thinkAt = this.lines.length;
560
+ this.push(line);
561
+ } else {
562
+ this.lines[this.thinkAt] = line;
563
+ this.render();
564
+ }
565
+ }
566
+
567
+ /** Keep the live thought moving between deltas. */
568
+ paintLiveThought() {
569
+ if (this.thinkAt !== undefined && this.lines[this.thinkAt] !== undefined) this.thinkingDelta('');
508
570
  }
509
571
 
510
572
  thinkingEnd() {
511
- if (this.thoughtSince === undefined) return;
512
- const seconds = Math.round((Date.now() - this.thoughtSince) / 1000);
513
- // How long it thought is not what the reader is here for, and a line of it
514
- // between every step broke every run of steps into singletons — which is
515
- // why nothing folded. The time is still on the status row while it runs.
516
- void seconds;
573
+ // The thought was the wait; once there is a reply it has nothing to add,
574
+ // so it comes off the screen rather than settling into the transcript.
575
+ if (this.thinkAt !== undefined) {
576
+ this.lines.splice(this.thinkAt, 1);
577
+ if (this.run && this.run.at > this.thinkAt) this.run.at--;
578
+ for (const run of this.segment?.values() ?? []) if (run.at > this.thinkAt) run.at--;
579
+ this.thinkAt = undefined;
580
+ this.render();
581
+ }
582
+ this.thought = '';
517
583
  this.thoughtSince = undefined;
518
584
  }
519
585
 
@@ -856,6 +922,8 @@ export class Screen {
856
922
  if (this.spinTimer) return;
857
923
  this.spinTimer = setInterval(() => {
858
924
  this.tick++;
925
+ this.paintLiveRun();
926
+ this.paintLiveThought();
859
927
  this.paintStatus();
860
928
  }, FRAME_MS);
861
929
  this.spinTimer.unref?.();