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 +1 -1
- package/src/core/loop.js +40 -5
- package/src/tools/index.js +42 -42
- package/src/ui/screen.js +10 -5
- package/src/ui/theme.js +30 -7
package/package.json
CHANGED
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,
|
|
438
|
-
'
|
|
439
|
-
'
|
|
440
|
-
'
|
|
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
|
-
'-
|
|
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'],
|
package/src/tools/index.js
CHANGED
|
@@ -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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
|
589
|
-
case 'batch_write':
|
|
590
|
-
|
|
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
|
|
598
|
+
return 'Writing app';
|
|
596
599
|
case 'multi_edit':
|
|
597
|
-
return
|
|
598
|
-
case 'edit_files':
|
|
599
|
-
|
|
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
|
|
351
|
-
if (
|
|
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
|
-
|
|
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
|
|
272
|
-
|
|
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
|
/**
|