ucode-agent 1.48.0 → 1.50.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.
@@ -46,11 +46,11 @@ dotenv.config({ path: join(PACKAGE_ROOT, '.env'), quiet: true });
46
46
  /**
47
47
  * The whole model list. Not a starting point — the list.
48
48
  *
49
- * ucode runs on NVIDIA, Cohere and Nex AGI only. All three serve genuinely
50
- * capable models free through OpenRouter, all handle tool calling properly,
51
- * and keeping the set to six means every one of them has been used in anger
52
- * rather than listed on the strength of a benchmark. A picker offering sixty
53
- * models is a picker nobody reads.
49
+ * ucode runs on NVIDIA, Cohere, Nex AGI, DeepSeek and Qwen only. All of them
50
+ * serve genuinely capable models free through OpenRouter, all handle tool
51
+ * calling properly, and keeping the set to eight means every one of them has
52
+ * been used in anger rather than listed on the strength of a benchmark. A
53
+ * picker offering sixty models is a picker nobody reads.
54
54
  *
55
55
  * `name` is what the status bar shows. `note` is what the picker shows.
56
56
  */
@@ -89,6 +89,18 @@ export const MODELS = {
89
89
  context: 262_144,
90
90
  note: 'new agentic coder, on trial — can stall on big builds',
91
91
  },
92
+ // Both free on OpenRouter with tool calling, added untested: on trial, not
93
+ // the default and not in FALLBACKS until a real ucode build has run on them.
94
+ 'deepseek/deepseek-v4-flash-0731:free': {
95
+ name: 'DeepSeek V4 Flash',
96
+ context: 1_048_576,
97
+ note: 'fast DeepSeek coder, 1M context — on trial',
98
+ },
99
+ 'qwen/qwen3.8-27b:free': {
100
+ name: 'Qwen 3.8',
101
+ context: 262_144,
102
+ note: 'compact Qwen coder, 262k context — on trial',
103
+ },
92
104
  };
93
105
 
94
106
  /**
@@ -173,7 +185,7 @@ export function setModel(id) {
173
185
  throw new Failure({
174
186
  kind: 'bad_model',
175
187
  attempted: `switching to "${wanted}"`,
176
- failed: 'ucode only runs NVIDIA, Cohere and Nex AGI models, and that is not one of them.',
188
+ failed: 'ucode only runs NVIDIA, Cohere, Nex AGI, DeepSeek and Qwen models, and that is not one of them.',
177
189
  fix: `Run /model to choose from: ${Object.keys(MODELS).join(', ')}`,
178
190
  });
179
191
  }
@@ -188,7 +188,11 @@ export function forgetReviews() {
188
188
  const PROBE_TEXT = 'ucode check';
189
189
 
190
190
  /** Did anything at all happen on the page? */
191
- const moved = (a, b) => a.nodes !== b.nodes || a.text !== b.text || a.stored !== b.stored;
191
+ // The markup itself, not its length: a timer going 25:00 -> 24:59, a counter
192
+ // going 0 -> 1 or a task ticked off by a class change keeps every length the
193
+ // same, and was reported as "NOTHING HAPPENS" — a false accusation the model
194
+ // then "fixed" by rewriting script that worked.
195
+ const moved = (a, b) => a.nodes !== b.nodes || a.html !== b.html || a.stored !== b.stored;
192
196
 
193
197
  /**
194
198
  * Use the app, rather than only looking at it.
@@ -218,6 +222,7 @@ async function useTheApp(page) {
218
222
  const snapshot = () => page.evaluate(() => ({
219
223
  nodes: document.body.querySelectorAll('*').length,
220
224
  text: document.body.innerText.replace(/\s+/g, ' ').trim().length,
225
+ html: document.body.innerHTML,
221
226
  stored: (() => { try { return JSON.stringify(localStorage).length; } catch { return 0; } })(),
222
227
  }));
223
228
 
@@ -326,7 +331,7 @@ export async function withStaticServer(dir, fn) {
326
331
  }
327
332
  }
328
333
 
329
- export async function lookAtApp({ url, paths = ['/'] }) {
334
+ export async function lookAtApp({ url, paths = ['/'], review: withReview = true }) {
330
335
  const base = String(url ?? '').trim().replace(/\/+$/, '');
331
336
  if (!LOCAL.test(`${base}/`)) {
332
337
  throw new ToolFailure({
@@ -355,6 +360,10 @@ export async function lookAtApp({ url, paths = ['/'] }) {
355
360
  const errors = [];
356
361
  const failed = [];
357
362
  page.on('console', (m) => {
363
+ // The browser asks for /favicon.ico on its own. A page without one is
364
+ // not broken, and counting that 404 sent every plain-html build round
365
+ // a fix loop it did not need.
366
+ if (/\/favicon\.ico$/i.test(m.location()?.url ?? '')) return;
358
367
  if (m.type() === 'error' && !/devtools|download the react/i.test(m.text())) errors.push(m.text().slice(0, 200));
359
368
  });
360
369
  page.on('pageerror', (e) => errors.push(`uncaught: ${String(e.message).slice(0, 200)}`));
@@ -513,7 +522,7 @@ export async function lookAtApp({ url, paths = ['/'] }) {
513
522
  // error overlay is a minute spent on nothing.
514
523
  let critique = '';
515
524
  const state = reviews.get(base) ?? { done: false, tries: 0 };
516
- if (!broken && !state.done && state.tries < 2 && toReview.length) {
525
+ if (withReview && !broken && !state.done && state.tries < 2 && toReview.length) {
517
526
  state.tries++;
518
527
  reviews.set(base, state);
519
528
  try {
@@ -530,7 +539,7 @@ export async function lookAtApp({ url, paths = ['/'] }) {
530
539
  critique ? `## Visual review\n${critique}` : '',
531
540
  '',
532
541
  problems
533
- ? 'Fix the problems above, then look again to confirm.'
542
+ ? 'Fix the problems above - only those; leave everything that works as it is.'
534
543
  : state.done && critique
535
544
  ? 'The automatic checks found nothing. Weigh the visual review, fix what is worth fixing - ' +
536
545
  'the next look re-runs only the fast checks.'
@@ -12,7 +12,7 @@ import { addBlock, BLOCK_NAMES, PLAIN_BLOCK_NAMES, ALL_BLOCK_NAMES } from './blo
12
12
  import { typeOf } from './types.js';
13
13
  import { runCommand, runCommands } from './shell.js';
14
14
  import { webSearch } from './web.js';
15
- import { createApp, TEMPLATE_NAMES, TEMPLATE_NOTES } from './scaffold.js';
15
+ import { createApp, normaliseFiles, TEMPLATE_NAMES, TEMPLATE_NOTES } from './scaffold.js';
16
16
  import { deploy } from './deploy.js';
17
17
  import { clip, READ_LINES } from './shared.js';
18
18
 
@@ -702,13 +702,15 @@ export function describe(name, args = {}) {
702
702
  return `Running ${args.commands?.length ?? 0} commands together`;
703
703
  case 'deploy':
704
704
  return `Deploying ${clip(args.folder || '.', 30)} to Vercel`;
705
- case 'create_app':
705
+ case 'create_app': {
706
+ const files = normaliseFiles(args.files).length;
706
707
  // Say which starter it actually is. Hardcoding one of them meant a plain
707
708
  // HTML app announced itself as Next.js, which is a line that is simply
708
709
  // untrue on screen while the opposite happens on disk.
709
710
  return `Creating ${clip(args.name || args.folder, 30)} from the ` +
710
711
  `${args.template === 'next-shadcn' ? 'Next.js' : 'HTML'} starter` +
711
- `${args.files?.length ? ` with ${args.files.length} file${args.files.length === 1 ? '' : 's'}` : ''}`;
712
+ `${files ? ` with ${files} file${files === 1 ? '' : 's'}` : ''}`;
713
+ }
712
714
  case 'look_at_app':
713
715
  return `Looking at ${clip(args.url, 40)} on a phone and a desktop`;
714
716
  case 'web_search':
@@ -48,7 +48,7 @@ const SHOW_BACK = { 'plain-html': ['index.html', 'styles.css', 'app.js'] };
48
48
  * something adjacent. Each of those, refused, is a round trip spent being
49
49
  * told what could have been read — so they are all read.
50
50
  */
51
- function normaliseFiles(files) {
51
+ export function normaliseFiles(files) {
52
52
  let value = files;
53
53
  if (typeof value === 'string') {
54
54
  try { value = JSON.parse(value); } catch { return []; }