ucode-agent 1.43.0 → 1.45.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/README.md +1 -1
- package/package.json +2 -1
- package/src/core/csscheck.js +77 -0
- package/src/core/jslogic.js +69 -0
- package/src/core/loop.js +71 -7
- package/src/tools/browser.js +546 -496
- package/src/tools/index.js +14 -7
- package/src/tools/scaffold.js +81 -3
- package/src/tools/shared.js +389 -343
- package/src/ui/plain.js +353 -353
- package/src/ui/screen.js +1699 -1692
- package/src/ui/theme.js +814 -776
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucode-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.45.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",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"playwright-core": "^1.63.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
+
"@types/node": "^26.5.1",
|
|
56
57
|
"typescript": "^5.9.3"
|
|
57
58
|
},
|
|
58
59
|
"publishConfig": {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* csscheck.js — a custom property that was never defined.
|
|
3
|
+
*
|
|
4
|
+
* `padding: var(--space-md)` where nothing ever declared `--space-md` does not
|
|
5
|
+
* warn, does not fall back and does not appear in the console. The browser
|
|
6
|
+
* throws the whole declaration away at parse time, so the padding is simply
|
|
7
|
+
* not there — and the page still renders, still looks deliberate, and is
|
|
8
|
+
* wrong in a way that reads as a design choice rather than a bug.
|
|
9
|
+
*
|
|
10
|
+
* It came out of a build here: a stylesheet that declared --space-xs, --space,
|
|
11
|
+
* --space-sm, --space-lg and --space-xl, then used --space-md four times.
|
|
12
|
+
* Nothing in the checks looked at CSS, so nothing said a word.
|
|
13
|
+
*
|
|
14
|
+
* The check is one whole app at a time rather than one file at a time,
|
|
15
|
+
* because the definition and the use are routinely in different files: a
|
|
16
|
+
* single-page app keeps its tokens in styles.css and reaches for them from an
|
|
17
|
+
* inline <style>, and reporting that as undefined would be worse than not
|
|
18
|
+
* looking. Whatever is passed in is the world; a name defined anywhere in it
|
|
19
|
+
* counts as defined everywhere.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Definitions: `--name:` at the start of a declaration. */
|
|
23
|
+
const DEFINED = /(?:^|[;{\s])(--[A-Za-z0-9_-]+)\s*:/g;
|
|
24
|
+
/** Uses: `var(--name)` — and whether a fallback follows the name. */
|
|
25
|
+
const USED = /var\(\s*(--[A-Za-z0-9_-]+)\s*([,)])/g;
|
|
26
|
+
/** A name handed to CSS from script is defined, just not in a stylesheet. */
|
|
27
|
+
const FROM_JS = /setProperty\(\s*['"`](--[A-Za-z0-9_-]+)/g;
|
|
28
|
+
/** Inline styles in a page are part of that page's CSS. */
|
|
29
|
+
const STYLE_TAGS = /<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi;
|
|
30
|
+
|
|
31
|
+
/** Comments hold examples and dead rules; neither defines anything. */
|
|
32
|
+
const uncommented = (css) => css.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
|
|
33
|
+
|
|
34
|
+
/** The CSS in a source file: a stylesheet whole, a page's <style> blocks only. */
|
|
35
|
+
function cssOf(rel, text) {
|
|
36
|
+
if (!/\.html?$/i.test(rel)) return uncommented(text);
|
|
37
|
+
// Everything outside a <style> is blanked rather than dropped, so a line
|
|
38
|
+
// number in the result is still a line number in the file.
|
|
39
|
+
let out = text.replace(/[^\n]/g, ' ');
|
|
40
|
+
for (const m of text.matchAll(STYLE_TAGS)) {
|
|
41
|
+
const at = m.index + m[0].indexOf(m[1]);
|
|
42
|
+
out = out.slice(0, at) + uncommented(m[1]) + out.slice(at + m[1].length);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Every `var(--x)` in `sources` with no `--x` defined anywhere in them and no
|
|
49
|
+
* fallback of its own.
|
|
50
|
+
*
|
|
51
|
+
* @param {{rel: string, text: string}[]} sources every file of one app that
|
|
52
|
+
* holds CSS — stylesheets, pages with inline <style>, and any script that
|
|
53
|
+
* sets a property, which defines names without declaring them.
|
|
54
|
+
* @returns {{rel: string, line: number, name: string}[]}
|
|
55
|
+
*/
|
|
56
|
+
export function checkCss(sources = []) {
|
|
57
|
+
const files = sources.map(({ rel, text }) => ({ rel, css: cssOf(rel, String(text ?? '')) }));
|
|
58
|
+
|
|
59
|
+
const defined = new Set();
|
|
60
|
+
for (const { css } of files) for (const m of css.matchAll(DEFINED)) defined.add(m[1]);
|
|
61
|
+
for (const { text } of sources) for (const m of String(text ?? '').matchAll(FROM_JS)) defined.add(m[1]);
|
|
62
|
+
|
|
63
|
+
const problems = [];
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
for (const { rel, css } of files) {
|
|
66
|
+
for (const m of css.matchAll(USED)) {
|
|
67
|
+
const [, name, next] = m;
|
|
68
|
+
if (next === ',') continue; // it carries its own fallback
|
|
69
|
+
if (defined.has(name)) continue;
|
|
70
|
+
const key = `${rel}:${name}`;
|
|
71
|
+
if (seen.has(key)) continue; // once per name per file, not once per use
|
|
72
|
+
seen.add(key);
|
|
73
|
+
problems.push({ rel, name, line: css.slice(0, m.index).split('\n').length });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return problems;
|
|
77
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jslogic.js — the logic mistakes, in the system prompt, every turn.
|
|
3
|
+
*
|
|
4
|
+
* Skills are disclosed progressively and that is right for almost everything:
|
|
5
|
+
* a body loads when the work matches it. This does not, because the failures
|
|
6
|
+
* below do not announce themselves. They arrive in the middle of a build
|
|
7
|
+
* nobody called "a JavaScript task" — an empty state that stops coming back,
|
|
8
|
+
* a listener attached to a row that has since been replaced — and by the time
|
|
9
|
+
* the work looks like it needs a guide, the guide is being read too late.
|
|
10
|
+
*
|
|
11
|
+
* So it is always on, and the price of that is length: every word here is
|
|
12
|
+
* re-read by the provider on every step of every build. It is kept to the
|
|
13
|
+
* mistakes that have actually shipped from here, in the shortest form that
|
|
14
|
+
* still says what to do instead. Anything a parser could catch belongs in a
|
|
15
|
+
* check — see htmlcheck.js — not in this list. Nothing general about
|
|
16
|
+
* JavaScript belongs here at all: the model already knows it, and a paragraph
|
|
17
|
+
* confirming what it knows costs the same as one it does not.
|
|
18
|
+
*
|
|
19
|
+
* The vanilla section is drawn from builds that came out of ucode looking
|
|
20
|
+
* finished and were not. The framework section is the same failures one level
|
|
21
|
+
* up.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export const JS_LOGIC = `## The logic mistakes that ship looking finished
|
|
25
|
+
|
|
26
|
+
Every one of these renders. None of them throws where you can see it. Check
|
|
27
|
+
each against what you are writing, before you write it.
|
|
28
|
+
|
|
29
|
+
In a page:
|
|
30
|
+
|
|
31
|
+
- **Empty is a state, not a starting position.** A render function that clears
|
|
32
|
+
its container decides what zero items looks like, in that same function.
|
|
33
|
+
Markup sitting in index.html, or one render call at start-up, survives the
|
|
34
|
+
first paint and then never comes back: add an item, delete it, and the list
|
|
35
|
+
is blank for good.
|
|
36
|
+
- **User text goes in with textContent.** Into innerHTML, a task named
|
|
37
|
+
\`<img src=x onerror=alert(1)>\` runs, and one reading \`5 < 6\` silently eats
|
|
38
|
+
the rest of the row. Build rows with createElement and set .textContent.
|
|
39
|
+
- **Every var(--x) needs a --x in :root.** An undefined custom property does
|
|
40
|
+
not fall back and does not warn — the browser discards the whole declaration,
|
|
41
|
+
so the padding is simply missing. Use only names you defined.
|
|
42
|
+
- **One listener on the container, not one per row.** Re-rendering throws the
|
|
43
|
+
rows away and their listeners with them. Listen on the list and read
|
|
44
|
+
event.target.closest('[data-id]').
|
|
45
|
+
- **An id that goes through the DOM comes back a string.** dataset.id is
|
|
46
|
+
"1738", never 1738, so === against a numeric id is always false. Make ids
|
|
47
|
+
strings when you create them.
|
|
48
|
+
- **Saved is not loaded.** Writing localStorage is half of it: read it as the
|
|
49
|
+
page boots, before the first render, and confirm a refresh keeps the state.
|
|
50
|
+
- **A missing element kills every line after it.** getElementById returns null
|
|
51
|
+
for a typo, .addEventListener on it throws, and the rest of the module never
|
|
52
|
+
runs — so every later handler in that file is silently never attached. One
|
|
53
|
+
typo, and nothing on the page works.
|
|
54
|
+
|
|
55
|
+
In React or Next, the same failures one level up:
|
|
56
|
+
|
|
57
|
+
- **Setting state does not change the variable you are holding.** Two
|
|
58
|
+
setCount(count + 1) in one handler add one. Pass a function when the next
|
|
59
|
+
value depends on the last.
|
|
60
|
+
- **State is replaced, never edited.** items.push(x) then setItems(items) is
|
|
61
|
+
the same reference, so nothing re-renders.
|
|
62
|
+
- **Hooks run every render, in order** — never inside an if, a loop, or after
|
|
63
|
+
an early return.
|
|
64
|
+
- **An effect that sets what it depends on is a loop**, and one with no
|
|
65
|
+
dependency array runs after every render.
|
|
66
|
+
- **key={index} is not identity.** On a list that reorders or filters, it moves
|
|
67
|
+
state onto the wrong row.
|
|
68
|
+
- **"use client" is per file, at the top.** State, an event handler or a
|
|
69
|
+
browser API in a server file fails at build, not at run.`;
|
package/src/core/loop.js
CHANGED
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import os from 'node:os';
|
|
16
16
|
import { appendFileSync, existsSync } from 'node:fs';
|
|
17
|
-
import { readFile, access, mkdir } from 'node:fs/promises';
|
|
17
|
+
import { readFile, readdir, access, mkdir } from 'node:fs/promises';
|
|
18
18
|
import { testRunnerFor, relatedCommand, summariseFailures } from './tests.js';
|
|
19
19
|
import { LogWatch } from './livelog.js';
|
|
20
20
|
import { checkHtml } from './htmlcheck.js';
|
|
21
|
+
import { checkCss } from './csscheck.js';
|
|
21
22
|
import { runningServers } from '../tools/shell.js';
|
|
22
23
|
import { unprefixOwnFolder } from './relink.js';
|
|
23
24
|
import { beginTurn, undoTurn, changedCount } from './undo.js';
|
|
@@ -28,7 +29,8 @@ import {
|
|
|
28
29
|
MODELS, DEFAULT_MODEL, PROVIDER, fallbackFor,
|
|
29
30
|
} from './provider.js';
|
|
30
31
|
import {
|
|
31
|
-
tools, runTool, describe, setRoot, setConfirm,
|
|
32
|
+
tools, runTool, describe, setRoot, setConfirm, setRequest,
|
|
33
|
+
PARALLEL_SAFE, WRITES, FILE_WRITES,
|
|
32
34
|
} from '../tools/index.js';
|
|
33
35
|
import { projectMap, loadMemory, hasCode, remember, MEMORY_FILE } from './context.js';
|
|
34
36
|
import { autoUpdate } from './updater.js';
|
|
@@ -38,14 +40,16 @@ import {
|
|
|
38
40
|
} from './history.js';
|
|
39
41
|
import { fold, usage, tooBig, SUMMARY_PROMPT, forSummary } from './window.js';
|
|
40
42
|
import { loadSkills, catalogue, findSkill, skillMessage, autoLoadFor } from './skills.js';
|
|
41
|
-
import { Screen
|
|
43
|
+
import { Screen } from '../ui/screen.js';
|
|
42
44
|
import { Plain } from '../ui/plain.js';
|
|
43
|
-
import { theme, blue, sky, dim, formatTokens, relativeTime, shortenPath, clip
|
|
45
|
+
import { theme, blue, sky, dim, formatTokens, relativeTime, shortenPath, clip,
|
|
46
|
+
asNarrationLine } from '../ui/theme.js';
|
|
44
47
|
import { Failure, ToolFailure, Declined } from './failure.js';
|
|
45
48
|
import { StuckWatch, eventFor, describeHit } from './stuck.js';
|
|
46
49
|
import { serversReadySince } from '../tools/shell.js';
|
|
47
50
|
import { formatDuration } from '../ui/activity.js';
|
|
48
51
|
import { runDoctor } from './doctor.js';
|
|
52
|
+
import { JS_LOGIC } from './jslogic.js';
|
|
49
53
|
import { deploy } from '../tools/deploy.js';
|
|
50
54
|
|
|
51
55
|
/**
|
|
@@ -605,13 +609,13 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
|
|
|
605
609
|
' - One line when you move between the big pieces of work: "The layout is done,',
|
|
606
610
|
' now the animations."',
|
|
607
611
|
' - One line at the end saying what it does and how to try it.',
|
|
608
|
-
'That closing line is ONE OR TWO SENTENCES, and
|
|
612
|
+
'That closing line is ONE OR TWO SENTENCES, and FIVE LINES IS THE HARD CEILING.',
|
|
609
613
|
'Never a checklist, never a feature list, never ticks or bullets walking through',
|
|
610
614
|
'the request item by item, never a list of the files you touched - the user',
|
|
611
615
|
'watched them go past. "Tide is built - open tide/index.html, or serve the folder',
|
|
612
616
|
'and visit it." Anything longer is a status report nobody asked for, and it is',
|
|
613
617
|
'the last thing on screen, so it is what the whole session looks like. ucode cuts',
|
|
614
|
-
'the closing message to
|
|
618
|
+
'the closing message to five lines before it is drawn, so anything past that is',
|
|
615
619
|
'written for nobody: say the one thing that matters and stop.',
|
|
616
620
|
'That is all. A line before every tool call is not narration, it is noise: the',
|
|
617
621
|
'steps already show on screen, and repeating them in words buries the few',
|
|
@@ -632,6 +636,13 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
|
|
|
632
636
|
'the request repeated back. Say it the way you would to someone watching over',
|
|
633
637
|
'your shoulder, who can already see the screen.',
|
|
634
638
|
'',
|
|
639
|
+
'Every one of those lines is ONE SENTENCE, because that is all that is drawn:',
|
|
640
|
+
'anything you write beside a tool call is cut to a single status line, so a',
|
|
641
|
+
'paragraph there loses everything after its first sentence. The closing message',
|
|
642
|
+
'- the one with no tool call after it - is the only prose the user reads.',
|
|
643
|
+
'',
|
|
644
|
+
JS_LOGIC,
|
|
645
|
+
'',
|
|
635
646
|
|
|
636
647
|
'',
|
|
637
648
|
'',
|
|
@@ -1107,6 +1118,7 @@ export class Agent {
|
|
|
1107
1118
|
this.declines = 0;
|
|
1108
1119
|
this.apps = [];
|
|
1109
1120
|
forgetReviews(); // a new request: its apps get a fresh design review
|
|
1121
|
+
setRequest(input); // create_app checks this before choosing a starter
|
|
1110
1122
|
const images = await this.attachImages(input);
|
|
1111
1123
|
this.push(images.length
|
|
1112
1124
|
? { role: 'user', content: input, images }
|
|
@@ -1346,7 +1358,10 @@ export class Agent {
|
|
|
1346
1358
|
// message, the last thing on screen, and it gets cut to eight lines.
|
|
1347
1359
|
const closing = !narrating && (this.touched.size > 0 || this.ranSomething);
|
|
1348
1360
|
if (streaming) this.ui.streamEnd({ asNarration: narrating, closing });
|
|
1349
|
-
|
|
1361
|
+
// Anything said beside a tool call is narration, however long it ran on:
|
|
1362
|
+
// it is cut to a status line instead of being printed as an answer. Only
|
|
1363
|
+
// the closing message — the one with no tool call after it — is prose.
|
|
1364
|
+
else if (reply.text && narrating) this.ui.narrate(asNarrationLine(reply.text));
|
|
1350
1365
|
else if (reply.text) this.ui.assistant(reply.text, { closing });
|
|
1351
1366
|
if (closing && reply.text) this.checkClaims(reply.text);
|
|
1352
1367
|
|
|
@@ -2095,6 +2110,55 @@ export class Agent {
|
|
|
2095
2110
|
else if (/\.[cm]?js$/i.test(rel)) singles.push({ abs, rel, command: `node --check "${abs}"` });
|
|
2096
2111
|
}
|
|
2097
2112
|
|
|
2113
|
+
// CSS is checked a folder at a time, not a file at a time: a token
|
|
2114
|
+
// declared in styles.css and reached for from an inline <style> is
|
|
2115
|
+
// defined, and either file read on its own would call it undefined.
|
|
2116
|
+
// Definitions are gathered from everything beside the changed files;
|
|
2117
|
+
// problems are reported only in the files this turn actually wrote, so a
|
|
2118
|
+
// build is never interrupted by something it did not touch.
|
|
2119
|
+
const rels = (abs) => path.relative(root, abs).split(path.sep).join('/');
|
|
2120
|
+
const group = (found) => {
|
|
2121
|
+
const by = new Map();
|
|
2122
|
+
for (const b of found) {
|
|
2123
|
+
if (!by.has(b.rel)) by.set(b.rel, []);
|
|
2124
|
+
by.get(b.rel).push(b);
|
|
2125
|
+
}
|
|
2126
|
+
return by;
|
|
2127
|
+
};
|
|
2128
|
+
const cssDirs = new Set();
|
|
2129
|
+
for (const rel of changed) {
|
|
2130
|
+
if (/\.(?:css|html?)$/i.test(rel)) cssDirs.add(path.dirname(path.resolve(root, rel)));
|
|
2131
|
+
}
|
|
2132
|
+
if (cssDirs.size) {
|
|
2133
|
+
const sources = [];
|
|
2134
|
+
const seen = new Set();
|
|
2135
|
+
const take = async (abs) => {
|
|
2136
|
+
if (seen.has(abs)) return;
|
|
2137
|
+
seen.add(abs);
|
|
2138
|
+
const text = await readFile(abs, 'utf8').catch(() => null);
|
|
2139
|
+
if (text !== null) sources.push({ rel: rels(abs), text });
|
|
2140
|
+
};
|
|
2141
|
+
for (const dir of cssDirs) {
|
|
2142
|
+
for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
|
|
2143
|
+
if (entry.isFile() && /\.(?:css|html?|[cm]?js)$/i.test(entry.name)) {
|
|
2144
|
+
await take(path.join(dir, entry.name));
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
const mine = new Set(changed.map((rel) => rels(path.resolve(root, rel))));
|
|
2149
|
+
for (const rel of changed) await take(path.resolve(root, rel));
|
|
2150
|
+
|
|
2151
|
+
const undefinedVars = checkCss(sources).filter((b) => mine.has(b.rel));
|
|
2152
|
+
for (const [rel, found] of group(undefinedVars)) {
|
|
2153
|
+
problems.push(
|
|
2154
|
+
`${rel} — ${found.length === 1 ? 'a custom property is' : `${found.length} custom properties are`} ` +
|
|
2155
|
+
'used and never defined. CSS does not warn about this: the browser throws away the whole ' +
|
|
2156
|
+
'declaration, so the value is silently missing and the page still renders.\n' +
|
|
2157
|
+
found.map((b) => ` line ${b.line}: var(${b.name})`).join('\n')
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2098
2162
|
const check = async (label, command, cwd) => {
|
|
2099
2163
|
// The spinner only, never a transcript line. This pass runs after the
|
|
2100
2164
|
// model has finished speaking, so a step logged here is the last thing
|