ucode-agent 1.31.0 → 1.33.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.31.0",
3
+ "version": "1.33.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",
@@ -16,7 +16,7 @@
16
16
  "scripts": {
17
17
  "start": "node ucode.js",
18
18
  "test": "node test/run.js",
19
- "prepublishOnly": "node test/run.js"
19
+ "prepublishOnly": "node scripts/no-bundled-key.js && node test/run.js"
20
20
  },
21
21
  "repository": {
22
22
  "type": "git",
@@ -26,9 +26,6 @@
26
26
  "url": "https://github.com/sppideey/ucode-agent/issues"
27
27
  },
28
28
  "homepage": "https://github.com/sppideey/ucode-agent#readme",
29
- "publishConfig": {
30
- "access": "public"
31
- },
32
29
  "engines": {
33
30
  "node": ">=22"
34
31
  },
@@ -56,5 +53,8 @@
56
53
  },
57
54
  "devDependencies": {
58
55
  "typescript": "^5.9.3"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
59
  }
60
60
  }
package/src/core/loop.js CHANGED
@@ -556,6 +556,34 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
556
556
  'already contains it. Do not re-check work the checks have already reported on.',
557
557
  'Fast is not sloppy: it is the same work with the waiting taken out.',
558
558
  '',
559
+ 'DESIGN IT BEFORE YOU TYPE IT. Fast means fewer round trips. It does not mean a',
560
+ 'default theme, and an app that goes out in the palette its starter came with is',
561
+ 'not a fast build, it is an undesigned one. There is no design pass after the',
562
+ 'create_app call, because there is no after — so the decisions happen before it.',
563
+ '',
564
+ 'Three of them, and you hold them for the whole build:',
565
+ ' - A TONE, one word you commit to: clinical, warm, editorial, technical,',
566
+ ' playful, industrial, calm, dense. "Modern and clean" is not a tone.',
567
+ ' - An ACCENT that is not the one the starter shipped with.',
568
+ ' - ONE memorable thing this app has that other apps do not: a colour, a type',
569
+ ' move, a texture, one interaction. Exactly one. It is the difference between',
570
+ ' a design and a theme.',
571
+ 'Name the tone and the accent in your opening line, so they are settled before any',
572
+ 'file exists: "I will build Tide - a tasks app in one HTML file, calm, warm grey',
573
+ 'with a single amber accent." That is not narrating a plan, that is the decision.',
574
+ '',
575
+ 'Then the tokens are the first thing in the file, in the same call as everything',
576
+ 'else: the type scale, the space scale, neutrals that carry a hue, the accent and',
577
+ 'its semantics. Nothing after that uses a raw value. IN next-shadcn THAT MEANS',
578
+ 'globals.css IS RE-TINTED IN THAT SAME create_app CALL. Shipping the palette the',
579
+ 'starter came with is the commonest way a build looks generated, and it is the',
580
+ 'first thing anyone notices. A blocked-out page in the colours this app chose',
581
+ 'beats a finished page in the ones it was handed.',
582
+ '',
583
+ 'add_block gives you structure, never a look. A block arrives with no opinion',
584
+ 'about this app and is yours to tint the moment it lands. Assembling blocks and',
585
+ 'shipping them as they came is the quick way to something nobody designed.',
586
+ '',
559
587
  'Never repeat the request back. Not as a summary, not as a restatement, not as',
560
588
  'a list of what was asked for. They wrote it and it is on the screen above you.',
561
589
  'Do not narrate your planning either - which files you will make, what order you',
@@ -665,6 +693,11 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
665
693
  ' reports - errors, layout that overflows a phone, the review points worth fixing -',
666
694
  ' in one pass, then look once more. A clean second look means it is done: report',
667
695
  ' back instead of polishing in circles. Never call an interface finished unlooked at.',
696
+ '- ANYTHING THAT NEEDS A SERVER IS LEFT RUNNING. If the app has a dev server -',
697
+ ' Next.js, Vite, anything with an npm run dev - start it and leave it up when you',
698
+ ' finish. ucode opens it in the browser for the user as soon as it is ready, so a',
699
+ ' build that ends with the server stopped ends with nothing to look at. A one-page',
700
+ ' app with no server needs none of this: the file is the app.',
668
701
  ' name, handles keys and returns the live link. Build locally first.',
669
702
  '- Nothing you run has a keyboard. Pass the non-interactive flag to anything that',
670
703
  ' would ask a question, or it fails instead of waiting: create-next-app --yes,',
@@ -1540,15 +1573,19 @@ export class Agent {
1540
1573
 
1541
1574
  /** A dev server came up during this turn: open it in the browser, once. */
1542
1575
  /**
1543
- * Open the running app in a browser — only when asked.
1576
+ * Open the running app in a browser as soon as a dev server is up.
1577
+ *
1578
+ * This was turned off once, on the grounds that a window seizing the screen
1579
+ * mid-thought is startling and worse during a demo. It only fires at the end
1580
+ * of a finished turn, though, not mid-thought — and the thing the user asked
1581
+ * for is a running app, not a URL they then have to go and click. Being
1582
+ * handed a link to the thing you asked to be built is the last step of the
1583
+ * job left undone.
1544
1584
  *
1545
- * This used to happen on its own whenever a dev server came up. Something
1546
- * seizing the screen mid-thought is startling at the best of times, and
1547
- * during a demo it is worse. UCODE_OPEN=1 brings the old behaviour back for
1548
- * anyone who liked it; otherwise the URL is on screen to click.
1585
+ * UCODE_OPEN=0 turns it off for anyone who wants the link and nothing else.
1549
1586
  */
1550
1587
  openWhenReady(since) {
1551
- if (!this.full || process.env.UCODE_OPEN !== '1') return;
1588
+ if (!this.full || process.env.UCODE_OPEN === '0') return;
1552
1589
  const server = serversReadySince(since).at(-1);
1553
1590
  if (!server || (this.opened ??= new Set()).has(server.url)) return;
1554
1591
  this.opened.add(server.url);
@@ -107,6 +107,18 @@ export const FALLBACKS = [
107
107
 
108
108
  /** The next model to try after `id`, skipping any already tried this round. */
109
109
  export function fallbackFor(id, tried = new Set()) {
110
+ // Off unless UCODE_FALLBACK=1. A build that starts on one model and finishes
111
+ // on another finishes to a different standard, and the swap lands exactly
112
+ // when the user is least placed to work out why the output changed —
113
+ // mid-build, behind a note that scrolls past. Which model to run is the one
114
+ // decision they made before starting; it is not one to take back for them.
115
+ //
116
+ // Every caller already reads "no fallback" as "wait, then try this one
117
+ // again": failover waits a minute and returns to the same model, the
118
+ // stuck-detector simply does not switch, and a worker retries its own. That
119
+ // is why this can be a single gate rather than four.
120
+ if (process.env.UCODE_FALLBACK !== '1') return null;
121
+
110
122
  const start = Math.max(0, FALLBACKS.indexOf(id));
111
123
  for (let i = 1; i <= FALLBACKS.length; i++) {
112
124
  const next = FALLBACKS[(start + i) % FALLBACKS.length];
@@ -85,7 +85,11 @@ export const tools = [
85
85
  'Start a new app AND write it, in one call. Pass "files" with the whole app and this ' +
86
86
  'is the only call the build needs: the starter lands, your files are written over it, ' +
87
87
  'and the result comes back with everything. Two starters. "plain-html" (the default): ' +
88
- 'one index.html, one stylesheet, one ES module — nothing to install, nothing to build, ' +
88
+ 'one index.html, one stylesheet, one ES module — nothing to install, nothing to build, ' +
89
+ 'and the stylesheet is a design system already: a palette, a spacing scale, radii, ' +
90
+ 'motion timings, focus rings and a breakpoint. Re-tint those tokens to suit the app ' +
91
+ 'and compose every rule from them. Replacing it with raw pixel values is what an ' +
92
+ 'undesigned page is made of. ' +
89
93
  'opens straight in a browser, and its three files come back inside this result so there ' +
90
94
  'is never a reason to read them. Use it for anything that is one page: a tasks app, a ' +
91
95
  'toy, a game, a visualisation, a calculator, a timer. "next-shadcn": Next.js 16, ' +
@@ -108,6 +108,46 @@ async function copyTree(from, to, fill) {
108
108
  * globals.css and layout.tsx. Apps stop looking like the same default blue.
109
109
  * Returns the preset used, or null when the starter has none.
110
110
  */
111
+ /**
112
+ * The file in each starter that carries the design, not just some of the rules.
113
+ *
114
+ * next-shadcn has globals.css, which applyDesign re-tints. plain-html has one
115
+ * stylesheet and it is the whole design system: the palette, a spacing scale,
116
+ * radii, motion timings, focus rings, a reduced-motion rule and a breakpoint.
117
+ */
118
+ const TOKEN_FILE = { 'plain-html': 'styles.css' };
119
+
120
+ /**
121
+ * Put the starter's token block back when the app wrote over it without one.
122
+ *
123
+ * The stylesheet in plain-html is a design, not a placeholder — but `files`
124
+ * lands straight on top of the starter, so a model passing its own styles.css
125
+ * replaces the scale, the palette and the timings with whatever it typed. What
126
+ * comes back is hand-rolled CSS with no system behind it, and an app built on
127
+ * raw pixel values has uneven spacing everywhere for the rest of its life.
128
+ *
129
+ * A replacement that declares its own custom properties is left alone: that is
130
+ * a model doing the job properly, and second-guessing it would be worse. Only
131
+ * a stylesheet with no :root variables at all gets the starter's block put
132
+ * back above it, where every rule underneath can reach it.
133
+ */
134
+ async function keepDesignTokens(appDir, template, written, starterCss) {
135
+ const rel = TOKEN_FILE[template];
136
+ if (!rel || !starterCss) return null;
137
+
138
+ const abs = path.join(appDir, rel);
139
+ if (!written.has(abs)) return null; // never overwritten
140
+
141
+ const now = await fs.readFile(abs, 'utf8').catch(() => null);
142
+ if (now === null || /:root\s*\{[^}]*--/.test(now)) return null;
143
+
144
+ const block = /:root\s*\{[\s\S]*?\n\}/.exec(starterCss);
145
+ if (!block) return null;
146
+
147
+ await fs.writeFile(abs, `${block[0]}\n\n${now}`, 'utf8');
148
+ return rel;
149
+ }
150
+
111
151
  export async function applyDesign(appDir, design) {
112
152
  const dir = path.join(appDir, 'presets');
113
153
  const names = (await fs.readdir(dir).catch(() => [])).filter((f) => f.endsWith('.json'));
@@ -242,7 +282,13 @@ export async function createApp({ folder, name, description, template = 'plain-h
242
282
  // Next.js serves static files from public/; a plain page has no such place
243
283
  // and an empty folder in a three-file app is clutter.
244
284
  if (template !== 'plain-html') await fs.mkdir(path.join(target.abs, 'public'), { recursive: true });
245
- const look = await applyDesign(target.abs, design);
285
+ const look = await applyDesign(target.abs, design);
286
+
287
+ // Read before the app's own files land on top of it, so the tokens can be
288
+ // put back if the replacement arrives without any.
289
+ const starterCss = TOKEN_FILE[template]
290
+ ? await fs.readFile(path.join(target.abs, TOKEN_FILE[template]), 'utf8').catch(() => null)
291
+ : null;
246
292
 
247
293
  // The starter has been installed on this machine before: hard-link that
248
294
  // tree in, which is seconds where npm is a minute. Otherwise install as
@@ -271,7 +317,8 @@ export async function createApp({ folder, name, description, template = 'plain-h
271
317
  // one, and round trips are nearly all of the time a build takes.
272
318
  const mine = given;
273
319
  const wrote = mine.length ? await batchWrite({ files: mine }) : null;
274
- const written = new Set(mine.map((f) => resolveIn(f.path, 'create_app', 'files').abs));
320
+ const written = new Set(mine.map((f) => resolveIn(f.path, 'create_app', 'files').abs));
321
+ const keptTokens = await keepDesignTokens(target.abs, template, written, starterCss);
275
322
 
276
323
  // The starter's own files, in full, so there is never a reason to read them
277
324
  // back — and only the ones this call did not already write over. A read is
@@ -287,7 +334,14 @@ export async function createApp({ folder, name, description, template = 'plain-h
287
334
  const out = result(
288
335
  `Created ${target.show} from the ${template} starter — ${copied.length} files, already known to build.\n` +
289
336
  (look ? `Design: the ${look.name} preset (${look.summary}), font ${look.fonts?.sans ?? 'Geist'}.\n` : '') +
290
- (wrote ? `\nYour ${mine.length} file${mine.length === 1 ? '' : 's'}:\n${wrote.content}\n` : '') +
337
+ (wrote ? `\nYour ${mine.length} file${mine.length === 1 ? '' : 's'}:\n${wrote.content}\n` : '') +
338
+ (keptTokens
339
+ ? `\nYour ${keptTokens} arrived with no :root block, so the starter's was kept above it — ` +
340
+ 'the palette, the spacing scale (--s1 to --s5), the radius and the motion timings. ' +
341
+ 'Build the rest of the stylesheet out of those variables: spacing that comes from a ' +
342
+ 'scale is the difference between a designed page and an arranged one. Re-tint the ' +
343
+ 'values to suit this app; do not go back to raw pixels.\n'
344
+ : '') +
291
345
  (linked
292
346
  ? `Its packages are already in place (${linked.toLocaleString()} files, linked from the starter cache) — ` +
293
347
  'nothing to install: build and run straight away.\n'