ucode-agent 1.3.0 → 1.5.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.
Files changed (56) hide show
  1. package/README.md +42 -12
  2. package/package.json +5 -3
  3. package/skills/build-app/SKILL.md +23 -3
  4. package/skills/ui-ux/SKILL.md +5 -1
  5. package/src/core/loop.js +181 -20
  6. package/src/core/provider.js +40 -12
  7. package/src/core/updater.js +95 -0
  8. package/src/tools/browser.js +258 -0
  9. package/src/tools/index.js +51 -1
  10. package/src/tools/scaffold.js +130 -0
  11. package/src/tools/shell.js +8 -1
  12. package/src/ui/screen.js +20 -6
  13. package/templates/next-shadcn/AGENTS.md +9 -0
  14. package/templates/next-shadcn/README.md +9 -0
  15. package/templates/next-shadcn/TEMPLATE.md +39 -0
  16. package/templates/next-shadcn/_gitignore +41 -0
  17. package/templates/next-shadcn/_package-lock.json +10278 -0
  18. package/templates/next-shadcn/components.json +25 -0
  19. package/templates/next-shadcn/eslint.config.mjs +18 -0
  20. package/templates/next-shadcn/next-env.d.ts +5 -0
  21. package/templates/next-shadcn/next.config.ts +7 -0
  22. package/templates/next-shadcn/package.json +37 -0
  23. package/templates/next-shadcn/postcss.config.mjs +7 -0
  24. package/templates/next-shadcn/src/app/favicon.ico +0 -0
  25. package/templates/next-shadcn/src/app/globals.css +155 -0
  26. package/templates/next-shadcn/src/app/layout.tsx +42 -0
  27. package/templates/next-shadcn/src/app/page.tsx +14 -0
  28. package/templates/next-shadcn/src/components/theme-provider.tsx +7 -0
  29. package/templates/next-shadcn/src/components/theme-toggle.tsx +27 -0
  30. package/templates/next-shadcn/src/components/ui/alert-dialog.tsx +187 -0
  31. package/templates/next-shadcn/src/components/ui/avatar.tsx +108 -0
  32. package/templates/next-shadcn/src/components/ui/badge.tsx +51 -0
  33. package/templates/next-shadcn/src/components/ui/button.tsx +57 -0
  34. package/templates/next-shadcn/src/components/ui/calendar.tsx +221 -0
  35. package/templates/next-shadcn/src/components/ui/card.tsx +102 -0
  36. package/templates/next-shadcn/src/components/ui/checkbox.tsx +28 -0
  37. package/templates/next-shadcn/src/components/ui/command.tsx +196 -0
  38. package/templates/next-shadcn/src/components/ui/dialog.tsx +160 -0
  39. package/templates/next-shadcn/src/components/ui/dropdown-menu.tsx +267 -0
  40. package/templates/next-shadcn/src/components/ui/input-group.tsx +158 -0
  41. package/templates/next-shadcn/src/components/ui/input.tsx +19 -0
  42. package/templates/next-shadcn/src/components/ui/label.tsx +19 -0
  43. package/templates/next-shadcn/src/components/ui/popover.tsx +89 -0
  44. package/templates/next-shadcn/src/components/ui/progress.tsx +82 -0
  45. package/templates/next-shadcn/src/components/ui/scroll-area.tsx +54 -0
  46. package/templates/next-shadcn/src/components/ui/select.tsx +200 -0
  47. package/templates/next-shadcn/src/components/ui/separator.tsx +24 -0
  48. package/templates/next-shadcn/src/components/ui/sheet.tsx +138 -0
  49. package/templates/next-shadcn/src/components/ui/skeleton.tsx +13 -0
  50. package/templates/next-shadcn/src/components/ui/sonner.tsx +49 -0
  51. package/templates/next-shadcn/src/components/ui/switch.tsx +31 -0
  52. package/templates/next-shadcn/src/components/ui/tabs.tsx +81 -0
  53. package/templates/next-shadcn/src/components/ui/textarea.tsx +17 -0
  54. package/templates/next-shadcn/src/components/ui/tooltip.tsx +65 -0
  55. package/templates/next-shadcn/src/lib/utils.ts +1 -0
  56. package/templates/next-shadcn/tsconfig.json +34 -0
package/README.md CHANGED
@@ -19,7 +19,7 @@ corner:
19
19
  ╭──────────────────────────────────────────────────────────────────────────────╮
20
20
  │ › Ask anything… │
21
21
  │ │
22
- │ ◆ Build · Nemotron 3 Ultra 0% │
22
+ │ ◆ Build · North Mini Code 0% │
23
23
  ╰──────────────────────────────────────────────────────────────────────────────╯
24
24
 
25
25
 
@@ -44,7 +44,7 @@ the input, so your own words are easy to find in a long session:
44
44
  ╭──────────────────────────────────────────────────────────────────────────────────╮
45
45
  │ › now add a dark mode toggle │
46
46
  │ │
47
- │ ◆ Build · Nemotron 3 Ultra 4% │
47
+ │ ◆ Build · North Mini Code 4% │
48
48
  ╰──────────────────────────────────────────────────────────────────────────────────╯
49
49
  ```
50
50
 
@@ -88,27 +88,43 @@ properly, which is the thing an agent actually depends on.
88
88
 
89
89
  | Model | Context | For |
90
90
  | --- | --- | --- |
91
- | **Nemotron 3 Ultra** | 1M | the default — deepest reasoning, slowest to first token |
91
+ | Nemotron 3 Ultra | 1M | deepest reasoning, slowest to first token |
92
92
  | Nemotron 3.5 Lightning | 1M | the same enormous window, answers much sooner |
93
93
  | Nemotron 3 Super | 262k | strong all-rounder, quick to start |
94
94
  | Nemotron 3 Nano Omni | 256k | small, fast, reasoning tuned |
95
- | **North Mini Code** ★ | 256k | code and UI specialist reach for it on frontend work |
95
+ | **North Mini Code** ★ | 256k | the defaultbuilt for code and interface work, quick to answer |
96
96
 
97
97
  `/model` shows them and switches. `ucode -m cohere/north-mini-code:free`
98
98
  starts on one.
99
99
 
100
- Ultra is the default because the work this is for read a codebase, hold it in
101
- mind, change several files consistently is what a million-token window and a
102
- long think are for. When the wait stops being worth it, switch.
100
+ North Mini Code is the default: it is built for code and interfaces, which is most
101
+ of what ucode is asked to do, and it answers far sooner than the big reasoning
102
+ models. Switch to Ultra when a problem needs the million-token window more than
103
+ the speed.
104
+
105
+ **A busy model never stops a build.** Free endpoints are shared, and "too many
106
+ requests" is routine. ucode waits it out with growing pauses, and if the model
107
+ stays busy it carries on with the next one — North Mini Code, then Nemotron 3.5
108
+ Lightning, Super, Ultra — from exactly where it was, and tells you it switched.
109
+ If every model is busy at once it waits a minute and goes round again. Your
110
+ chosen model gets another go a few minutes later.
103
111
 
104
112
  ## What it does
105
113
 
106
- **Thirteen tools.** `read_file`, `read_files`, `write_file`, `batch_write`,
107
- `edit_file`, `multi_edit`, `edit_files`, `list_dir`, `glob`, `grep`,
108
- `run_command`, `run_commands`, `web_search`. Read-only calls run in parallel,
114
+ **Fifteen tools.** `create_app`, `read_file`, `read_files`, `write_file`,
115
+ `batch_write`, `edit_file`, `multi_edit`, `edit_files`, `list_dir`, `glob`,
116
+ `grep`, `run_command`, `run_commands`, `look_at_app`, `web_search`. Read-only calls run in parallel,
109
117
  and start the moment the model finishes writing them — while the rest of its
110
118
  reply is still arriving. Anything that writes runs on its own, in order.
111
119
 
120
+ **Apps start from a ready-made starter.** Setting up Next.js and shadcn from
121
+ nothing takes about four minutes — `create-next-app` and the shadcn CLI measured
122
+ at 116s and 130s — plus a dozen model round trips. `create_app` copies ucode's
123
+ starter instead: Next.js 16, TypeScript, Tailwind 4, shadcn/ui with 25 common
124
+ components, light/dark mode, toasts and a considered theme, already known to
125
+ build. The copy takes under a second, and its install runs in the background
126
+ while the model writes the first components.
127
+
112
128
  **Parallel workers.** When a build splits into parts that touch different files
113
129
  — the API route, the upload component, the results view — the model hands them
114
130
  to up to three workers that build at the same time, each line in the transcript
@@ -119,6 +135,14 @@ written, its install starts in the background while the rest of the app is
119
135
  still being written. An install the model asks for later waits for that one
120
136
  instead of running twice, and anything run in that folder waits for it too.
121
137
 
138
+ **It looks at what it built.** `look_at_app` opens the running app in a real
139
+ browser — the Edge or Chrome already on your machine, so there is nothing extra
140
+ to download — at 375px and 1440px. It reports console errors, failed requests,
141
+ content that spills off a phone screen, broken images and unlabeled controls,
142
+ saves screenshots to `.ucode/screenshots`, and has Nemotron Nano Omni review them
143
+ the way a designer would. The model fixes what it finds before calling the app
144
+ done.
145
+
122
146
  **Errors fixed before you see them.** When the model says it is done, ucode
123
147
  type-checks every file it changed — `tsc --noEmit` for TypeScript projects,
124
148
  a syntax check for JavaScript and Python — and hands any errors back to fix,
@@ -154,7 +178,12 @@ them; the chip inside the input box says which is live.
154
178
 
155
179
  **Sessions.** Everything is on disk under `~/.ucode/sessions`, saved after every
156
180
  step. `/resume` lists them with what each one was actually about, the ones from
157
- this folder first.
181
+ this folder first. Press `d` twice on one to delete it — the list stays open, so
182
+ clearing out several is quick — or `/session delete 2,5`.
183
+
184
+ **It updates itself.** Each launch checks npm in the background and, if there is
185
+ a newer version, installs it while you work. The next launch is the new one.
186
+ Set `UCODE_NO_UPDATE=1` to turn that off.
158
187
 
159
188
  **A context window that folds rather than forgets.** Past 75% the oldest turns
160
189
  are summarised instead of dropped, never cutting between a tool call and its
@@ -207,6 +236,7 @@ Everything after the frontmatter is the instruction.
207
236
  | `/help` | the list |
208
237
  | `/model` | show the models and switch — `/models` does the same |
209
238
  | `/resume` | pick up an earlier conversation — `/session`, `/sessions` too |
239
+ | `/session delete 2,5` | delete saved conversations by number (or `d d` in the list) |
210
240
  | `/new` | save this one and start fresh |
211
241
  | `/remember <note>` | add a standing note to this project's `UCODE.md` |
212
242
  | `/skills` | what it knows how to do, and what is loaded |
@@ -245,7 +275,7 @@ ucode [options]
245
275
  Environment overrides: `UCODE_MODEL`, `UCODE_WORKER_MODEL` (a faster model for
246
276
  parallel workers), `UCODE_WORKER_STEPS`, `UCODE_MAX_CONTEXT_TOKENS`,
247
277
  `UCODE_MAX_STEPS`, `UCODE_MAX_TOOL_OUTPUT`, `UCODE_REQUEST_TIMEOUT_MS`,
248
- `UCODE_BASE_URL`.
278
+ `UCODE_BASE_URL`, `UCODE_NO_UPDATE`.
249
279
 
250
280
  Web search needs a Tavily key — free, 1000 searches a month, no card. Without
251
281
  one, ucode answers from what it knows and says that it could not check.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucode-agent",
3
- "version": "1.3.0",
3
+ "version": "1.5.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",
@@ -10,7 +10,8 @@
10
10
  "files": [
11
11
  "ucode.js",
12
12
  "src/",
13
- "skills/"
13
+ "skills/",
14
+ "templates/"
14
15
  ],
15
16
  "scripts": {
16
17
  "start": "node ucode.js",
@@ -48,6 +49,7 @@
48
49
  "dotenv": "^17.4.2",
49
50
  "marked": "^15.0.12",
50
51
  "marked-terminal": "^7.3.0",
51
- "openai": "^7.4.0"
52
+ "openai": "^7.4.0",
53
+ "playwright-core": "^1.63.0"
52
54
  }
53
55
  }
@@ -31,7 +31,25 @@ If there is a user interface, the `ui-ux` skill is already loaded. Decide the
31
31
  design direction now, not after the logic works. If the app calls a model,
32
32
  load `ai-features`; if it has accounts, keys or uploads, load `security`.
33
33
 
34
- ## 2. Scaffold without being asked questions
34
+ ## 2. Start from the starter
35
+
36
+ **For a Next.js app, call `create_app`** — one step, about a second:
37
+
38
+ ```
39
+ create_app({ folder: "my-app", name: "My App", description: "…" })
40
+ ```
41
+
42
+ It copies ucode's ready-made starter — Next.js 16, TypeScript, Tailwind 4,
43
+ shadcn/ui with 25 common components, light/dark mode, toasts, a considered
44
+ theme — which is already known to build, and starts `npm install` in the
45
+ background. Read the `TEMPLATE.md` it lists, then start writing components
46
+ straight away; commands in that folder wait for the install on their own.
47
+ Re-tint the palette in `globals.css` and swap the font for the app's direction.
48
+
49
+ Never run `create-next-app` or `shadcn init` for a Next.js app — that is
50
+ four minutes and a dozen steps the starter already did.
51
+
52
+ ### Other stacks
35
53
 
36
54
  Nothing you run has a keyboard. A scaffolder that asks "Would you like to use
37
55
  TypeScript?" gets no answer and fails, so give it every answer up front:
@@ -141,8 +159,10 @@ Use `batch_write` for the skeleton — one call, every file.
141
159
  - `npm run build` — it type-checks and lints; a build that fails is not done.
142
160
  - Start it: `npm run dev` goes to the background on its own and comes back with
143
161
  the URL once ready. Do not start it twice.
144
- - Exercise it: `curl` the API route with real input, load the page, check the
145
- core loop end to end. A clean build proves it compiles, not that it works.
162
+ - Exercise it: `curl` the API route with real input, then `look_at_app` on every
163
+ page it loads them in a real browser at phone and desktop width and reports
164
+ errors, overflow and a visual review. A clean build proves it compiles, not
165
+ that it works.
146
166
  - Fix what you find and check again.
147
167
 
148
168
  ## 7. Definition of done
@@ -283,7 +283,11 @@ because it was first to hand means you were not deciding.
283
283
 
284
284
  ## 9. Look at it, then report
285
285
 
286
- Run it (`npm run dev` starts in the background and returns the URL) and check:
286
+ Run it (`npm run dev` starts in the background and returns the URL), then call
287
+ `look_at_app` with that URL. It opens the app in a real browser at 375px and
288
+ 1440px, reports console errors, failed requests, overflow, broken images and
289
+ unlabeled controls, and returns a designer's review of the screenshots. Fix what
290
+ it finds and look again. Then check:
287
291
 
288
292
  1. **375px wide** — no horizontal scroll, nothing overlapping or clipped. If you
289
293
  wrote no responsive rules at all, you have not done this.
package/src/core/loop.js CHANGED
@@ -17,14 +17,16 @@ import { spawn } from 'node:child_process';
17
17
 
18
18
  import {
19
19
  ask, model, setModel, modelName, modelList, contextLimit, rateLimits,
20
- MODELS, DEFAULT_MODEL, PROVIDER,
20
+ MODELS, DEFAULT_MODEL, PROVIDER, fallbackFor,
21
21
  } from './provider.js';
22
22
  import {
23
23
  tools, runTool, describe, setRoot, setConfirm, PARALLEL_SAFE, WRITES, FILE_WRITES,
24
24
  } from '../tools/index.js';
25
25
  import { projectMap, loadMemory, remember, MEMORY_FILE } from './context.js';
26
+ import { autoUpdate } from './updater.js';
27
+ import { closeBrowser } from '../tools/browser.js';
26
28
  import {
27
- newSession, save, load, list, removeAll, titleFrom,
29
+ newSession, save, load, list, remove, removeAll, titleFrom,
28
30
  } from './history.js';
29
31
  import { fold, usage, tooBig, SUMMARY_PROMPT, forSummary } from './window.js';
30
32
  import { loadSkills, catalogue, findSkill, skillMessage, autoLoadFor } from './skills.js';
@@ -64,6 +66,17 @@ const MAX_FIX_ROUNDS = 3;
64
66
  /** Files worth checking after they change. */
65
67
  const CHECKABLE = /\.(?:[cm]?[jt]sx?|py)$/i;
66
68
 
69
+ /**
70
+ * Failures that are the provider's and not the model's: busy, slow, down, or
71
+ * unreachable. None of them should end a build — the turn moves to another
72
+ * model and carries on from exactly where it was.
73
+ */
74
+ const TRANSIENT = new Set(['rate_limit', 'timeout', 'server', 'network', 'no_content']);
75
+ const MAX_FAILOVERS = 8;
76
+ const COOLDOWN = 5 * 60_000;
77
+
78
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
79
+
67
80
  /** Parallel workers at once, and how many steps each may take. */
68
81
  const MAX_WORKERS = 3;
69
82
  const WORKER_STEPS = Number(process.env.UCODE_WORKER_STEPS) || 60;
@@ -129,7 +142,7 @@ const delegateTool = {
129
142
  };
130
143
 
131
144
  /** The instructions a parallel worker starts with. */
132
- function workerPrompt({ cwd, name, memory, skills }) {
145
+ function workerPrompt({ cwd, name, memory, skills, map }) {
133
146
  return [
134
147
  `You are a ucode worker called "${name}" - one of several building parts of the same project at the same time.`,
135
148
  '',
@@ -145,7 +158,11 @@ function workerPrompt({ cwd, name, memory, skills }) {
145
158
  '- Finish what you build: real content, every state handled, no TODOs.',
146
159
  '- When done, reply with two or three sentences: what you built, in which files, and',
147
160
  ' anything the lead has to wire up.',
161
+ '- The files you own may not exist yet - create them. Do not go looking for them',
162
+ ' first. The project map below shows what does exist; read only the files whose',
163
+ ' interfaces you must match, then start writing within two or three steps.',
148
164
  ...(memory ? ['', '## Project memory', '', memory] : []),
165
+ ...(map ? ['', '## Project map', '', map] : []),
149
166
  ...(skills ? ['', '## Instructions in force', '', skills] : []),
150
167
  ].join('\n');
151
168
  }
@@ -223,6 +240,8 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
223
240
  ' one at a time when you already know which ones you want.',
224
241
  '- Put independent calls in the same message — several greps, a glob and a read.',
225
242
  ' Read-only calls in one message run at the same time.',
243
+ '- New Next.js app? create_app - one step, never create-next-app or shadcn init. It',
244
+ ' copies a starter that already builds and installs it in the background.',
226
245
  '- batch_write to lay out several new files at once, multi_edit for several changes',
227
246
  ' to one file, edit_files for a change that spans several files.',
228
247
  '- For work with three or more steps, keep a short plan with update_plan - at most',
@@ -234,6 +253,9 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
234
253
  ' writing files. Running the install yourself afterwards just waits for that one.',
235
254
  '- When you finish, ucode type-checks what you changed and hands you the errors, so',
236
255
  ' there is no need to run tsc yourself.',
256
+ '- Once the dev server is ready, run look_at_app on the pages you built and fix what',
257
+ ' it reports - errors, layout that overflows a phone, and the visual review - then',
258
+ ' look again. Do not call an interface finished before it has been looked at.',
237
259
  '- Nothing you run has a keyboard. Pass the non-interactive flag to anything that',
238
260
  ' would ask a question, or it fails instead of waiting: create-next-app --yes,',
239
261
  ' npx shadcn@latest init -d -y, npx shadcn@latest add <names> -y, npm init -y.',
@@ -373,6 +395,14 @@ export class Agent {
373
395
 
374
396
  this.showHeader();
375
397
  this.installSignals();
398
+
399
+ // Checked in the background; nothing here waits on it.
400
+ autoUpdate({
401
+ onUpdated: (version) => {
402
+ this.ui.setFacts?.({ update: version });
403
+ if (!this.ui.welcoming?.()) this.ui.note(`updated to v${version} — it takes over the next time you start ucode`);
404
+ },
405
+ });
376
406
  await this.repl();
377
407
  }
378
408
 
@@ -485,6 +515,7 @@ export class Agent {
485
515
  }
486
516
 
487
517
  async shutdown() {
518
+ await closeBrowser().catch(() => {});
488
519
  this.ui.stopSpinner();
489
520
  if (this.session.messages.length) {
490
521
  await this.persist();
@@ -559,6 +590,14 @@ export class Agent {
559
590
  ]);
560
591
  await this.persist();
561
592
 
593
+ // A busy model was swapped for a fallback earlier; after a few minutes the
594
+ // one the user chose gets another go.
595
+ this.preferred ??= model();
596
+ if (model() !== this.preferred && Date.now() > (this.cooldownUntil ?? 0)) {
597
+ setModel(this.preferred);
598
+ if (this.full) this.showHeader({ clear: false });
599
+ }
600
+
562
601
  this.busy = true;
563
602
  this.abort = new AbortController();
564
603
 
@@ -591,6 +630,8 @@ export class Agent {
591
630
  let askedToVerify = false;
592
631
  let askedToSpeak = false;
593
632
  let fixRounds = 0;
633
+ this.failovers = 0;
634
+ this.tried = new Set([model()]);
594
635
 
595
636
  this.touched = new Set();
596
637
  this.sinceCheck = new Set();
@@ -668,6 +709,10 @@ export class Agent {
668
709
  });
669
710
  continue;
670
711
  }
712
+
713
+ // Busy, slow or down: move to the next model and carry on, rather
714
+ // than ending a half-built app with an error.
715
+ if (TRANSIENT.has(err.kind) && !this.abort.signal.aborted && (await this.failover(err))) continue;
671
716
  throw err;
672
717
  }
673
718
 
@@ -917,6 +962,38 @@ export class Agent {
917
962
  });
918
963
  }
919
964
 
965
+ /**
966
+ * Switch to the next model after a provider failure. Returns false once
967
+ * there is nothing sensible left to try. When every model is busy at once,
968
+ * it waits a minute and goes round again rather than giving up.
969
+ */
970
+ async failover(err) {
971
+ if (++this.failovers > MAX_FAILOVERS) return false;
972
+ const from = model();
973
+ let next = fallbackFor(from, this.tried);
974
+
975
+ if (!next) {
976
+ const until = Date.now() + 60_000;
977
+ this.ui.startSpinner('every model is busy');
978
+ while (Date.now() < until && !this.abort?.signal.aborted) {
979
+ this.ui.updateSpinner(`every model is busy — trying again in ${Math.ceil((until - Date.now()) / 1000)}s`);
980
+ await wait(1000);
981
+ }
982
+ this.ui.stopSpinner();
983
+ if (this.abort?.signal.aborted) return false;
984
+ this.tried = new Set();
985
+ next = this.preferred && this.preferred !== from ? this.preferred : fallbackFor(from, this.tried) ?? from;
986
+ }
987
+
988
+ this.tried.add(next);
989
+ setModel(next);
990
+ this.cooldownUntil = Date.now() + COOLDOWN;
991
+ const why = err.kind === 'rate_limit' ? 'busy' : err.kind === 'timeout' ? 'too slow to answer' : 'not answering';
992
+ this.ui.note(`${modelName(from)} is ${why} — carrying on with ${modelName(next)}`);
993
+ if (this.full) this.showHeader({ clear: false });
994
+ return true;
995
+ }
996
+
920
997
  /** Run a call and settle to { out } or { err } — never throws. */
921
998
  execute(call) {
922
999
  return this.dispatch(call).then((out) => ({ out }), (err) => ({ err }));
@@ -964,10 +1041,18 @@ export class Agent {
964
1041
 
965
1042
  for (const r of results) for (const f of r.touched) { this.touched.add(f); this.sinceCheck.add(f); }
966
1043
 
1044
+ // A worker that wrote nothing has not done its part, whatever it said.
1045
+ // The lead builds those itself rather than leaving holes in the app.
1046
+ const empty = results.filter((r) => !r.touched.length).map((r) => r.name);
1047
+
967
1048
  return {
968
1049
  content: results
969
1050
  .map((r) => `## ${r.name}\n${r.summary}\nFiles changed: ${r.touched.join(', ') || 'none'}`)
970
- .join('\n\n'),
1051
+ .join('\n\n') +
1052
+ (empty.length
1053
+ ? `\n\n${empty.join(', ')} wrote no files. Build ${empty.length === 1 ? 'that part' : 'those parts'} ` +
1054
+ 'yourself now, directly - do not delegate them again.'
1055
+ : ''),
971
1056
  summary: results.map((r) => `${r.name} · ${r.touched.length} file${r.touched.length === 1 ? '' : 's'}`).join(' '),
972
1057
  };
973
1058
  }
@@ -980,16 +1065,39 @@ export class Agent {
980
1065
  .map((s) => `--- ${s.name} ---\n${s.body}`)
981
1066
  .join('\n\n');
982
1067
  const messages = [
983
- { role: 'system', content: workerPrompt({ cwd: this.cwd, name, memory: this.memory, skills }) },
1068
+ { role: 'system', content: workerPrompt({ cwd: this.cwd, name, memory: this.memory, skills, map: this.map }) },
984
1069
  { role: 'user', content: String(task.instructions) },
985
1070
  ];
986
1071
  const available = this.toolsNow().filter((t) => !WORKER_EXCLUDED.has(t.name));
987
1072
  const wanted = process.env.UCODE_WORKER_MODEL;
988
- const workerModel = wanted && MODELS[wanted] ? wanted : model();
1073
+ let workerModel = wanted && MODELS[wanted] ? wanted : model();
1074
+ const tried = new Set([workerModel]);
1075
+ let failovers = 0;
1076
+
1077
+ // Start a moment apart. Three requests in the same instant is exactly what
1078
+ // trips a free endpoint's rate limit, and the stagger costs a second or two.
1079
+ if (index) await wait(index * 1500);
989
1080
 
990
1081
  for (let step = 0; step < WORKER_STEPS; step++) {
991
1082
  if (this.abort?.signal.aborted) break;
992
- const reply = await ask(messages, available, { signal: this.abort?.signal, model: workerModel });
1083
+
1084
+ let reply;
1085
+ try {
1086
+ reply = await ask(messages, available, { signal: this.abort?.signal, model: workerModel });
1087
+ } catch (err) {
1088
+ // Same rule as the lead: a busy model is swapped, not a reason to stop.
1089
+ if (TRANSIENT.has(err.kind) && failovers < 6 && !this.abort?.signal.aborted) {
1090
+ failovers++;
1091
+ let next = fallbackFor(workerModel, tried);
1092
+ if (!next) { tried.clear(); await wait(20_000); next = fallbackFor(workerModel, tried) ?? workerModel; }
1093
+ tried.add(next);
1094
+ this.ui.note(`${name}: ${modelName(workerModel)} is busy — switching to ${modelName(next)}`);
1095
+ workerModel = next;
1096
+ step--;
1097
+ continue;
1098
+ }
1099
+ throw err;
1100
+ }
993
1101
  this.record(reply.usage);
994
1102
 
995
1103
  if (!reply.toolCalls.length) {
@@ -1220,6 +1328,7 @@ export class Agent {
1220
1328
  if (arg) {
1221
1329
  try {
1222
1330
  setModel(arg);
1331
+ this.preferred = model();
1223
1332
  } catch (err) {
1224
1333
  this.ui.error(err, { debug: this.debug });
1225
1334
  return;
@@ -1247,6 +1356,7 @@ export class Agent {
1247
1356
  if (chosen === null) return;
1248
1357
 
1249
1358
  setModel(all[chosen].id);
1359
+ this.preferred = model();
1250
1360
  this.session.model = model();
1251
1361
  this.ui.note(`now using ${modelName()}`);
1252
1362
  this.showHeader({ clear: false });
@@ -1285,7 +1395,7 @@ export class Agent {
1285
1395
  * and how far it got, and the ones from this folder are marked, because that
1286
1396
  * is nearly always the one being looked for.
1287
1397
  */
1288
- describeSession(s, width) {
1398
+ describeSession(s, width, i) {
1289
1399
  const room = Math.max(24, Math.min(46, width - 34));
1290
1400
  const mark = s.mine ? blue('●') : dim('○');
1291
1401
  const when = relativeTime(s.updatedAt).padEnd(9);
@@ -1293,7 +1403,8 @@ export class Agent {
1293
1403
  const where = s.mine ? 'here' : shortenPath(s.cwd, 26);
1294
1404
 
1295
1405
  return {
1296
- label: `${mark} ${clip(s.title, room).padEnd(room)} ${dim(when)}${dim(turns)}${dim(where)}`,
1406
+ // Numbered in the picker, so /session delete 3 has something to point at.
1407
+ label: `${i === undefined ? '' : `${dim(String(i + 1).padStart(2))} `}${mark} ${clip(s.title, room).padEnd(room)} ${dim(when)}${dim(turns)}${dim(where)}`,
1297
1408
  sub: s.preview ? dim(` ${clip(s.preview, width - 10)}`) : '',
1298
1409
  };
1299
1410
  }
@@ -1314,6 +1425,10 @@ export class Agent {
1314
1425
  return;
1315
1426
  }
1316
1427
 
1428
+ // /session delete 3 or /session delete 2,5,7
1429
+ const del = /^(?:delete|del|rm|remove)\b\s*(.*)$/i.exec(arg ?? '');
1430
+ if (del) return this.deleteSessions(del[1]);
1431
+
1317
1432
  const sessions = await list({ cwd: this.cwd });
1318
1433
  if (!sessions.length) {
1319
1434
  this.ui.note('no saved conversations yet');
@@ -1336,22 +1451,48 @@ export class Agent {
1336
1451
  }
1337
1452
  index = n - 1;
1338
1453
  } else if (this.ui.pick) {
1339
- const here = shown.filter((s) => s.mine).length;
1340
- index = await this.ui.pick(
1341
- shown.map((s) => this.describeSession(s, width)),
1342
- {
1343
- hint:
1344
- `↑↓ move · enter to continue · esc to cancel` +
1345
- (here ? ` — ${here} from this folder` : '') +
1346
- (sessions.length > shown.length ? ` · ${sessions.length - shown.length} older not shown` : ''),
1454
+ // The picker stays open while you delete, so clearing out several old
1455
+ // conversations is d d, d d, d d — then Enter on the one you want.
1456
+ let active = 0;
1457
+ for (;;) {
1458
+ const here = shown.filter((s) => s.mine).length;
1459
+ const picked = await this.ui.pick(
1460
+ shown.map((s, i) => this.describeSession(s, width, i)),
1461
+ {
1462
+ active,
1463
+ deletable: true,
1464
+ hint:
1465
+ `↑↓ move · enter to continue · d twice to delete · esc to cancel` +
1466
+ (here ? ` — ${here} from this folder` : ''),
1467
+ }
1468
+ );
1469
+ if (picked === null) return;
1470
+ if (typeof picked === 'object' && picked.delete !== undefined) {
1471
+ const doomed = shown[picked.delete];
1472
+ active = picked.delete;
1473
+ if (doomed.id === this.session.id) {
1474
+ this.ui.flash?.('that is the conversation you are in — /new first, then delete it');
1475
+ continue;
1476
+ }
1477
+ await remove(doomed.id);
1478
+ shown.splice(picked.delete, 1);
1479
+ this.ui.flash?.(`deleted · ${clip(doomed.title, 50)}`);
1480
+ if (!shown.length) {
1481
+ this.ui.note('no saved conversations left');
1482
+ return;
1483
+ }
1484
+ active = Math.min(active, shown.length - 1);
1485
+ continue;
1347
1486
  }
1348
- );
1349
- if (index === null) return;
1487
+ index = picked;
1488
+ break;
1489
+ }
1350
1490
  } else {
1351
1491
  this.ui.blank();
1492
+ this.ui.note('/session delete <number> removes one, or several: /session delete 2,5');
1352
1493
  index = await this.ui.choose(
1353
1494
  'continue which?',
1354
- shown.map((s) => this.describeSession(s, width).label)
1495
+ shown.map((s, i) => this.describeSession(s, width, i).label)
1355
1496
  );
1356
1497
  if (index === null) return;
1357
1498
  }
@@ -1363,6 +1504,26 @@ export class Agent {
1363
1504
  }
1364
1505
  }
1365
1506
 
1507
+ /** /session delete 3, or 2,5,7 — numbers as the session list shows them. */
1508
+ async deleteSessions(spec) {
1509
+ const sessions = (await list({ cwd: this.cwd })).slice(0, 25);
1510
+ const numbers = [...new Set(String(spec).split(/[\s,]+/).filter(Boolean).map(Number))];
1511
+ const bad = numbers.filter((n) => !Number.isInteger(n) || n < 1 || n > sessions.length);
1512
+ if (!numbers.length || bad.length) {
1513
+ this.ui.write(theme.warn(` usage: /session delete <number>[,<number>…] — numbers from 1 to ${sessions.length}`));
1514
+ return;
1515
+ }
1516
+ for (const n of numbers) {
1517
+ const s = sessions[n - 1];
1518
+ if (s.id === this.session.id) {
1519
+ this.ui.note(`skipped ${n} — that is the conversation you are in`);
1520
+ continue;
1521
+ }
1522
+ await remove(s.id);
1523
+ this.ui.note(`deleted ${n} · ${s.title}`);
1524
+ }
1525
+ }
1526
+
1366
1527
  async resume(id) {
1367
1528
  try {
1368
1529
  const loaded = await load(id);
@@ -58,7 +58,7 @@ export const MODELS = {
58
58
  name: 'Nemotron 3 Ultra',
59
59
  context: 1_000_000,
60
60
  star: true,
61
- note: 'deepest reasoning, 1M context — the default',
61
+ note: 'deepest reasoning, 1M context — slowest to answer',
62
62
  },
63
63
  'nvidia/nemotron-3.5-lightning:free': {
64
64
  name: 'Nemotron 3.5 Lightning',
@@ -79,18 +79,43 @@ export const MODELS = {
79
79
  name: 'North Mini Code',
80
80
  context: 256_000,
81
81
  star: true,
82
- note: 'code and UI specialist reach for it on frontend work',
82
+ note: 'the defaultbuilt for code and interface work, quick to answer',
83
83
  },
84
84
  };
85
85
 
86
86
  /**
87
- * Nemotron 3 Ultra is the default because the work ucode is for read a
88
- * codebase, hold it in mind, change several files consistently is exactly
89
- * what a million-token window and a long think buy you. It is slower to the
90
- * first token than the others and that is the trade being made. /model swaps
91
- * to Lightning or North Mini Code when the wait stops being worth it.
87
+ * North Mini Code is the default: it is built for code and interface work,
88
+ * which is what ucode is mostly asked to do, and it answers far sooner than
89
+ * the big reasoning models. /model moves to Ultra when a problem needs the
90
+ * million-token window and the long think more than it needs the speed.
92
91
  */
93
- export const DEFAULT_MODEL = 'nvidia/nemotron-3-ultra-550b-a55b:free';
92
+ export const DEFAULT_MODEL = 'cohere/north-mini-code:free';
93
+
94
+ /**
95
+ * Where to go when a model is busy, in order of preference. Each is served by
96
+ * a different upstream, so a rate limit on one rarely means a limit on the
97
+ * next — which is what lets a long build keep going instead of stopping at
98
+ * the first "too many requests".
99
+ */
100
+ export const FALLBACKS = [
101
+ 'cohere/north-mini-code:free',
102
+ 'nvidia/nemotron-3.5-lightning:free',
103
+ 'nvidia/nemotron-3-super-120b-a12b:free',
104
+ 'nvidia/nemotron-3-ultra-550b-a55b:free',
105
+ ];
106
+
107
+ /** The next model to try after `id`, skipping any already tried this round. */
108
+ export function fallbackFor(id, tried = new Set()) {
109
+ const start = Math.max(0, FALLBACKS.indexOf(id));
110
+ for (let i = 1; i <= FALLBACKS.length; i++) {
111
+ const next = FALLBACKS[(start + i) % FALLBACKS.length];
112
+ if (next !== id && !tried.has(next)) return next;
113
+ }
114
+ return null;
115
+ }
116
+
117
+ /** Seconds to wait on successive rate limits that come with no retry-after. */
118
+ const RATE_LIMIT_BACKOFF = [5, 10, 20];
94
119
 
95
120
  let current = process.env.UCODE_MODEL || DEFAULT_MODEL;
96
121
  let client = null;
@@ -578,12 +603,15 @@ export async function ask(messages, tools = [], opts = {}) {
578
603
  problem = explain(err, id);
579
604
 
580
605
  // A per-minute limit is a wait, not a failure. Sit it out rather than
581
- // making the user retype their message.
582
- const wait = problem.detail?.retryAfter;
606
+ // making the user retype their message. Free endpoints often refuse
607
+ // without saying how long to wait, so when there is no retry-after the
608
+ // pauses grow on their own — 5s, 10s, 20s — and only then does the
609
+ // error go up to the loop, which moves to another model.
610
+ const told = problem.detail?.retryAfter;
611
+ const wait = Number.isFinite(told) && told > 0 && told <= 90 ? told : RATE_LIMIT_BACKOFF[attempt - 1];
583
612
  if (
584
613
  problem.kind === 'rate_limit' && !problem.detail?.daily &&
585
- Number.isFinite(wait) && wait > 0 && wait <= 90 &&
586
- attempt < attempts && !opts.signal?.aborted
614
+ wait && attempt < attempts && printed === 0 && !opts.signal?.aborted
587
615
  ) {
588
616
  const until = Date.now() + wait * 1000;
589
617
  while (Date.now() < until && !opts.signal?.aborted) {