ucode-agent 1.33.0 → 1.34.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.33.0",
3
+ "version": "1.34.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",
package/src/core/loop.js CHANGED
@@ -1931,7 +1931,12 @@ export class Agent {
1931
1931
  }
1932
1932
 
1933
1933
  const check = async (label, command, cwd) => {
1934
- this.ui.toolCall(label);
1934
+ // The spinner only, never a transcript line. This pass runs after the
1935
+ // model has finished speaking, so a step logged here is the last thing
1936
+ // left on screen — the session ends on "Checking tasker/app.js" instead
1937
+ // of on the answer, and it reads as though something was still going
1938
+ // when it stopped. A check that passes has nothing to say, and one that
1939
+ // fails goes back to the model, which says it in words.
1935
1940
  this.ui.startSpinner(label);
1936
1941
  const { out, err } = await this.execute({
1937
1942
  id: 'check', name: 'run_command',
@@ -179,6 +179,81 @@ export function forgetReviews() {
179
179
  reviews.clear();
180
180
  }
181
181
 
182
+
183
+ /**
184
+ * Text typed into the app while checking it. Distinctive enough to recognise
185
+ * in a screenshot, and obviously not something a user wrote.
186
+ */
187
+ const PROBE_TEXT = 'ucode check';
188
+
189
+ /** Did anything at all happen on the page? */
190
+ const moved = (a, b) => a.nodes !== b.nodes || a.text !== b.text || a.stored !== b.stored;
191
+
192
+ /**
193
+ * Use the app, rather than only looking at it.
194
+ *
195
+ * Everything else here is an inspection: overflow, labels, broken images,
196
+ * console errors on load. All of it passes on an app whose Add button does
197
+ * nothing, because a page with dead JavaScript still renders, still has good
198
+ * contrast and still has no console errors — it simply does not work. Nothing
199
+ * in the harness ever pressed anything, so the model was never told, and never
200
+ * fixed it.
201
+ *
202
+ * So: type into the first text field, press Enter, and if that changed nothing,
203
+ * click the first button. Then look at whether the page has more nodes, more
204
+ * text, or more in localStorage than it did. Any of those moving means the core
205
+ * loop is wired up. None of them moving, on a page that has controls to press,
206
+ * means it is not.
207
+ *
208
+ * Real keyboard and mouse input through the driver, never synthetic DOM events:
209
+ * an implicit form submit does not fire for a dispatched event, which would
210
+ * report a working form as dead.
211
+ *
212
+ * A page with nothing to press — a landing page, a chart, a page of prose — is
213
+ * not exercised and not judged. Returning null there is the difference between
214
+ * a check and a false accusation.
215
+ */
216
+ async function useTheApp(page) {
217
+ const snapshot = () => page.evaluate(() => ({
218
+ nodes: document.body.querySelectorAll('*').length,
219
+ text: document.body.innerText.replace(/\s+/g, ' ').trim().length,
220
+ stored: (() => { try { return JSON.stringify(localStorage).length; } catch { return 0; } })(),
221
+ }));
222
+
223
+ const before = await snapshot().catch(() => null);
224
+ if (!before) return null;
225
+ const tried = [];
226
+
227
+ // A field and the Enter key: the core loop of most one-page apps.
228
+ const field = page.locator('input[type="text"], input[type="search"], input:not([type]), textarea').first();
229
+ if (await field.count().catch(() => 0)) {
230
+ const ok = await field.fill(PROBE_TEXT, { timeout: 2_000 }).then(() => true).catch(() => false);
231
+ if (ok) {
232
+ await field.press('Enter', { timeout: 2_000 }).catch(() => {});
233
+ await page.waitForTimeout(300);
234
+ tried.push('typed into the first field and pressed Enter');
235
+ }
236
+ }
237
+
238
+ let after = await snapshot().catch(() => before);
239
+ if (tried.length && moved(before, after)) return { tried, worked: true };
240
+
241
+ // Nothing moved, so try the other half of the same pattern.
242
+ const button = page.locator('button:not([disabled]), input[type="submit"], [role="button"]').first();
243
+ if (await button.count().catch(() => 0)) {
244
+ const label = (await button.innerText().catch(() => '') || '').trim().replace(/\s+/g, ' ').slice(0, 24);
245
+ const ok = await button.click({ timeout: 2_000 }).then(() => true).catch(() => false);
246
+ if (ok) {
247
+ await page.waitForTimeout(300);
248
+ tried.push(`clicked ${label ? `"${label}"` : 'the first button'}`);
249
+ after = await snapshot().catch(() => after);
250
+ }
251
+ }
252
+
253
+ if (!tried.length) return null;
254
+ return { tried, worked: moved(before, after) };
255
+ }
256
+
182
257
  export async function lookAtApp({ url, paths = ['/'] }) {
183
258
  const base = String(url ?? '').trim().replace(/\/+$/, '');
184
259
  if (!LOCAL.test(`${base}/`)) {
@@ -217,6 +292,7 @@ export async function lookAtApp({ url, paths = ['/'] }) {
217
292
 
218
293
  const target = `${base}${pagePath}`;
219
294
  let loadError = null;
295
+ let used = null;
220
296
  try {
221
297
  // 'load', not 'networkidle': a dev server holds a hot-reload
222
298
  // connection open and polls, so the network may never go quiet and
@@ -246,6 +322,12 @@ export async function lookAtApp({ url, paths = ['/'] }) {
246
322
  });
247
323
  await fs.writeFile(file, buffer);
248
324
  shot = { label: `${pagePath} at ${size.width}px (${size.name})`, dataUrl: `data:image/jpeg;base64,${buffer.toString('base64')}` };
325
+
326
+ // Once per look, not once per width: pressing the same button four
327
+ // times says nothing the first press did not, and costs four seconds.
328
+ if (size.name === 'desktop' && pagePath === pages[0]) {
329
+ used = await useTheApp(page).catch(() => null);
330
+ }
249
331
  }
250
332
  } catch (err) {
251
333
  loadError = `the page broke while being checked: ${String(err.message).split('\n')[0]}`;
@@ -270,11 +352,25 @@ export async function lookAtApp({ url, paths = ['/'] }) {
270
352
  if (facts?.noAlt) lines.push(`- ${facts.noAlt} image(s) without alt text.`);
271
353
  if (facts?.tiny) lines.push(`- ${facts.tiny} tap target(s) smaller than 32px on a phone.`);
272
354
  if (facts?.smallText) lines.push(`- ${facts.smallText} text element(s) under 12px.`);
355
+ if (used && !used.worked) {
356
+ lines.push(
357
+ `- NOTHING HAPPENS WHEN YOU USE IT. I ${used.tried.join(', then ')} — and the page`,
358
+ ' gained no elements, changed no text and stored nothing. The markup and the styling',
359
+ ' are there; the behaviour is not wired to them. Find the listener that was never',
360
+ ' attached, or the handler that throws before it does anything, and fix that first:',
361
+ ' everything else on this page is decoration until it works.',
362
+ );
363
+ problems++;
364
+ } else if (used) {
365
+ lines.push(`- Core loop works: I ${used.tried.join(', then ')}, and the page responded.`);
366
+ }
273
367
  }
274
368
  if (errors.length) { lines.push('- Console errors:', ...[...new Set(errors)].slice(0, 6).map((e) => ` - ${e}`)); problems++; }
275
369
  if (failed.length) { lines.push('- Failed requests:', ...[...new Set(failed)].slice(0, 6).map((f) => ` - ${f}`)); problems++; }
276
370
  if (lines.length === 2 && !loadError) lines.push('- No errors, no overflow, nothing unlabeled.');
277
- const broken = Boolean(loadError || errors.length || facts?.empty);
371
+ // A dead core loop counts as broken: a screenshot of an app that does not
372
+ // work is not worth a paragraph on its typography.
373
+ const broken = Boolean(loadError || errors.length || facts?.empty || (used && !used.worked));
278
374
  return { section: lines.join('\n'), shot, problems, broken };
279
375
  })));
280
376