ucode-agent 1.12.1 → 1.13.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.12.1",
3
+ "version": "1.13.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,10 +434,12 @@ 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, in one short line, before you do it — "now the',
438
- 'tests", "wiring this into the loop". A line like that before a group of actions is',
439
- 'what makes the work readable. Keep it to a sentence: the narration is not the',
440
- 'answer, and three sentences of intent before every step reads as stalling.',
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.',
441
443
  '',
442
444
  'Before you guess at an API, ask: type_of gives the exact signature from the',
443
445
  'TypeScript this project has installed, and find_symbol says where something is declared without',
@@ -493,7 +495,8 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
493
495
  ' writing files. Running the install yourself afterwards just waits for that one.',
494
496
  '- When you finish, ucode type-checks what you changed and hands you the errors, so',
495
497
  ' there is no need to run tsc yourself.',
496
- '- Once the dev server is ready, run look_at_app on the pages you built. Fix what it',
498
+ '- Do not open or drive a browser. Checking the page in one is something the user',
499
+ ' asks for with /look; your job is to leave the app in a state worth looking at.',
497
500
  ' reports - errors, layout that overflows a phone, the review points worth fixing -',
498
501
  ' in one pass, then look once more. A clean second look means it is done: report',
499
502
  ' back instead of polishing in circles. Never call an interface finished unlooked at.',
@@ -1815,6 +1818,7 @@ export class Agent {
1815
1818
  case '/copy': return this.cmdCopy();
1816
1819
  case '/stats': return this.cmdStats();
1817
1820
  case '/doctor': return this.cmdDoctor();
1821
+ case '/look': return this.cmdLook(arg);
1818
1822
  case '/deploy': return this.cmdDeploy(arg);
1819
1823
  case '/exit':
1820
1824
  case '/quit': return 'exit';
@@ -1825,11 +1829,42 @@ export class Agent {
1825
1829
  }
1826
1830
  }
1827
1831
 
1832
+ /**
1833
+ * Look at the running app, because the user asked to.
1834
+ *
1835
+ * This used to happen on its own, which meant a browser being driven while
1836
+ * someone was reading, and a window taking the screen mid-thought. It is
1837
+ * the same check as before; the difference is who starts it.
1838
+ */
1839
+ async cmdLook(url) {
1840
+ const { lookAtApp } = await import('../tools/browser.js');
1841
+ const server = runningServers().at(-1);
1842
+ const at = (url ?? '').trim() || server?.url;
1843
+ if (!at) {
1844
+ this.ui.write(theme.warn(' nothing is running to look at.'));
1845
+ this.ui.note('start the app first, or pass a URL: /look http://localhost:3000');
1846
+ return;
1847
+ }
1848
+ this.ui.toolCall(`Looking at ${at}`);
1849
+ try {
1850
+ const out = await lookAtApp({ url: at });
1851
+ this.ui.write(out.content);
1852
+ // The model gets it too, so the next thing it says is about what is
1853
+ // actually on the page rather than what it believes it built.
1854
+ this.push({ role: 'user', content: `I looked at ${at}. This is what is there:
1855
+
1856
+ ${out.content}` });
1857
+ } catch (err) {
1858
+ this.ui.write(theme.error(` ${err.failed ?? err.message}`));
1859
+ }
1860
+ }
1861
+
1828
1862
  cmdHelp() {
1829
1863
  const rows = [
1830
1864
  ['/help', 'this list'],
1831
1865
  ['/stats', 'time, steps and tokens this session'],
1832
1866
  ['/doctor', 'check that everything ucode needs is working'],
1867
+ ['/look [url]', 'open the running app and report what is on the page'],
1833
1868
  ['/deploy [folder]', 'put the app online and get its link'],
1834
1869
  ['/model', 'show the models and switch between them'],
1835
1870
  ['/resume', 'pick up an earlier conversation'],
@@ -23,6 +23,37 @@ const str = (description) => ({ type: 'string', description });
23
23
  const int = (description) => ({ type: 'integer', description });
24
24
  const bool = (description) => ({ type: 'boolean', description });
25
25
 
26
+ /**
27
+ * The browser check is deliberately not in `tools`.
28
+ *
29
+ * It used to run itself whenever a dev server came up, which meant a window
30
+ * opening mid-thought and a page being driven while the user was reading. It
31
+ * is now something asked for: /look runs it, and nothing else does.
32
+ */
33
+ export const lookAtAppTool = {
34
+ name: 'look_at_app',
35
+ description:
36
+ 'Open the running app in a real browser at a phone width (375px) and a desktop width ' +
37
+ '(1440px) and report what a person would run into: console errors, failed requests, ' +
38
+ 'content that spills off the side of the screen, broken images, unlabeled buttons and ' +
39
+ 'fields. The first look at an app also brings a designer-style review of the ' +
40
+ 'screenshots; later looks re-run only the fast checks. Use it once the dev server is ' +
41
+ 'ready, fix what it reports, then look once more to confirm. Screenshots are saved ' +
42
+ 'under .ucode/screenshots.',
43
+ parameters: {
44
+ type: 'object',
45
+ properties: {
46
+ url: str('The local URL the dev server reported, e.g. http://localhost:3000'),
47
+ paths: {
48
+ type: 'array',
49
+ description: 'Pages to open, e.g. ["/", "/settings"]. Defaults to ["/"]. Up to 4.',
50
+ items: { type: 'string' },
51
+ },
52
+ },
53
+ required: ['url'],
54
+ },
55
+ };
56
+
26
57
  export const tools = [
27
58
  {
28
59
  name: 'create_app',
@@ -416,29 +447,6 @@ export const tools = [
416
447
  required: ['commands'],
417
448
  },
418
449
  },
419
- {
420
- name: 'look_at_app',
421
- description:
422
- 'Open the running app in a real browser at a phone width (375px) and a desktop width ' +
423
- '(1440px) and report what a person would run into: console errors, failed requests, ' +
424
- 'content that spills off the side of the screen, broken images, unlabeled buttons and ' +
425
- 'fields. The first look at an app also brings a designer-style review of the ' +
426
- 'screenshots; later looks re-run only the fast checks. Use it once the dev server is ' +
427
- 'ready, fix what it reports, then look once more to confirm. Screenshots are saved ' +
428
- 'under .ucode/screenshots.',
429
- parameters: {
430
- type: 'object',
431
- properties: {
432
- url: str('The local URL the dev server reported, e.g. http://localhost:3000'),
433
- paths: {
434
- type: 'array',
435
- description: 'Pages to open, e.g. ["/", "/settings"]. Defaults to ["/"]. Up to 4.',
436
- items: { type: 'string' },
437
- },
438
- },
439
- required: ['url'],
440
- },
441
- },
442
450
  {
443
451
  name: 'web_search',
444
452
  description:
@@ -578,28 +586,20 @@ export async function runTool(name, args = {}, opts = {}) {
578
586
  export function describe(name, args = {}) {
579
587
  switch (name) {
580
588
  case 'read_file':
581
- return `Reading ${clip(args.path)}${args.offset > 1 ? ` from line ${args.offset}` : ''}`;
582
- case 'read_files': {
583
- const names = (args.paths ?? []).map((p) => String(p));
584
- const joined = names.join(', ');
585
- return names.length && joined.length <= 60 ? `Reading ${joined}` : `Reading ${names.length} files`;
586
- }
589
+ case 'read_files':
590
+ // Which file is in the diff and in the result; the transcript only has to
591
+ // say what kind of work is going on, so a run of them folds into one line.
592
+ return 'Reading files';
587
593
  case 'write_file':
588
- return `Writing ${clip(args.path)}`;
589
- case 'batch_write': {
590
- const n = args.files?.length ?? 0;
591
- const first = args.files?.[0]?.path;
592
- return n === 1 && first ? `Writing ${clip(first)}` : `Writing ${n} files`;
593
- }
594
+ return 'Writing app';
595
+ case 'batch_write':
596
+ return 'Writing app';
594
597
  case 'edit_file':
595
- return `Editing ${clip(args.path)}`;
598
+ return 'Writing app';
596
599
  case 'multi_edit':
597
- return `Editing ${clip(args.path)}, ${args.edits?.length ?? 0} changes`;
598
- case 'edit_files': {
599
- const n = args.files?.length ?? 0;
600
- const first = args.files?.[0]?.path;
601
- return n === 1 && first ? `Editing ${clip(first)}` : `Editing ${n} files`;
602
- }
600
+ return 'Writing app';
601
+ case 'edit_files':
602
+ return 'Writing app';
603
603
  case 'update_plan':
604
604
  return 'Updating the plan';
605
605
  case 'delegate':
package/src/ui/screen.js CHANGED
@@ -39,7 +39,7 @@ import chalk from 'chalk';
39
39
  import {
40
40
  theme, blue, sky, deep, dim, edge, ADDED, REMOVED, BANNER, BANNER_WIDTH, SPINNER,
41
41
  boxTop, boxBottom, boxRow, visLen, padVis, clip, wrapAnsi,
42
- shortenPath, asLabel, ensureColour, planLine, bare, narration, narrationMark, groupKind, groupLabel, groupTarget, runLine } from './theme.js';
42
+ shortenPath, asLabel, ensureColour, planLine, bare, narration, narrationMark, groupKind, groupLabel, groupTarget, runLine, planRows } from './theme.js';
43
43
  import { FRAME_MS, fitActivity, shimmer, spinnerGlyph, formatDuration, doneLine, stepPaint } from './activity.js';
44
44
  import { renderer, render, polish } from './markdown.js';
45
45
  import { VERSION } from '../core/version.js';
@@ -58,7 +58,7 @@ export function isLabel(text) {
58
58
  export const COMMANDS = [
59
59
  '/help', '/model', '/models', '/session', '/sessions', '/resume',
60
60
  '/new', '/remember', '/skills', '/clear', '/search', '/copy', '/exit',
61
- '/stats', '/doctor', '/deploy',
61
+ '/stats', '/doctor', '/deploy', '/look',
62
62
  ];
63
63
 
64
64
  // ANSI ----------------------------------------------------------------------
@@ -347,8 +347,10 @@ export class Screen {
347
347
 
348
348
  /** The checklist, when the model updates it. One line, wrapped if it must. */
349
349
  plan(items) {
350
- const line = planLine(items);
351
- if (line) this.push(line);
350
+ const rows = planRows(items);
351
+ if (!rows.length) return;
352
+ this.endRun(); // a plan is not another step of whatever came before
353
+ for (const row of rows) this.push(row);
352
354
  }
353
355
 
354
356
  /**
@@ -508,7 +510,10 @@ export class Screen {
508
510
  thinkingEnd() {
509
511
  if (this.thoughtSince === undefined) return;
510
512
  const seconds = Math.round((Date.now() - this.thoughtSince) / 1000);
511
- if (seconds >= 2) this.push(dim(` ⋮ thought for ${seconds}s`));
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;
512
517
  this.thoughtSince = undefined;
513
518
  }
514
519
 
package/src/ui/theme.js CHANGED
@@ -263,18 +263,41 @@ export function asLabel(text) {
263
263
  * The model's checklist, as one short line — done ticked, the current item
264
264
  * marked, the rest dim — so progress is visible without taking over the screen.
265
265
  */
266
+ /**
267
+ * The plan, as a block rather than a sentence.
268
+ *
269
+ * Six steps joined with separators made one line far wider than any terminal,
270
+ * so it wrapped — and a wrapped checklist has its ticks in the middle of the
271
+ * text, which is unreadable. Down the page each step keeps its own row, its
272
+ * mark stays in the left column, and the eye can find the one in progress
273
+ * without reading any of the others.
274
+ *
275
+ * Returns the rows; the caller pushes them.
276
+ */
277
+ export function planRows(items) {
278
+ const list = (Array.isArray(items) ? items : []).slice(0, 8);
279
+ if (!list.length) return [];
280
+ const done = list.filter((i) => i?.done).length;
281
+ const current = list.findIndex((i) => !i?.done);
282
+
283
+ const rows = [` ${sky(`plan ${done}/${list.length}`)}`];
284
+ list.forEach((item, i) => {
285
+ const text = clip(String(item?.text ?? '').trim(), 64);
286
+ if (item?.done) rows.push(` ${theme.ok('✓')} ${dim(text)}`);
287
+ else if (i === current) rows.push(` ${blue('▸')} ${chalk.white(text)}`);
288
+ else rows.push(` ${dim('○')} ${dim(text)}`);
289
+ });
290
+ return rows;
291
+ }
292
+
293
+ /** Kept for the plain interface, which has one line to work with. */
266
294
  export function planLine(items) {
267
295
  const list = (Array.isArray(items) ? items : []).slice(0, 6);
268
296
  if (!list.length) return '';
269
297
  const done = list.filter((i) => i?.done).length;
270
298
  const current = list.findIndex((i) => !i?.done);
271
- const parts = list.map((item, i) => {
272
- const text = clip(String(item?.text ?? '').trim(), 30);
273
- if (item?.done) return `${theme.ok('✓')} ${dim(text)}`;
274
- if (i === current) return `${blue('▸')} ${chalk.white(text)}`;
275
- return dim(`○ ${text}`);
276
- });
277
- return ` ${sky(`plan ${done}/${list.length}`)} ${parts.join(dim(' · '))}`;
299
+ const now = current === -1 ? 'done' : clip(String(list[current]?.text ?? '').trim(), 40);
300
+ return ` ${sky(`plan ${done}/${list.length}`)} ${chalk.white(now)}`;
278
301
  }
279
302
 
280
303
  /**