ucode-agent 1.19.1 → 1.20.1

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.19.1",
3
+ "version": "1.20.1",
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",
@@ -0,0 +1,59 @@
1
+ /**
2
+ * htmlcheck.js — the JavaScript inside an HTML file is still JavaScript.
3
+ *
4
+ * A single-file app keeps everything in one <script>, and nothing was looking
5
+ * at it: the checks run `node --check` on .js files and tsc on TypeScript
6
+ * projects, so an index.html whose script does not parse passed every one of
7
+ * them. The page renders, the CSS is right, and not one button works.
8
+ *
9
+ * The failure that prompted this: a model writing an HTML-escaping map inside
10
+ * an HTML file produced `"'":'''` — three quotes, a syntax error, the whole
11
+ * script dead. Everything looked finished.
12
+ *
13
+ * Parsing is Babel's, which ucode already carries. Only syntax is checked:
14
+ * an undefined variable is a runtime problem and this is not the place for it.
15
+ */
16
+
17
+ import { parse } from '@babel/parser';
18
+
19
+ /** Inline scripts worth parsing: not src=, not JSON, not a template. */
20
+ const SCRIPTS = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi;
21
+
22
+ const runnable = (attrs) => {
23
+ if (/\bsrc\s*=/i.test(attrs)) return false; // a separate file, checked on its own
24
+ const type = /\btype\s*=\s*["']?([^"'\s>]+)/i.exec(attrs)?.[1]?.toLowerCase();
25
+ if (!type) return true;
26
+ return type === 'module' || type === 'text/javascript' || type === 'application/javascript';
27
+ };
28
+
29
+ /**
30
+ * Syntax errors in a file's inline scripts, with lines counted in the HTML
31
+ * rather than in the extracted fragment — a line number that does not match
32
+ * the file is worse than none.
33
+ */
34
+ export function checkHtml(text) {
35
+ const html = String(text ?? '');
36
+ const problems = [];
37
+
38
+ for (const match of html.matchAll(SCRIPTS)) {
39
+ const [whole, attrs, body] = match;
40
+ if (!runnable(attrs) || !body.trim()) continue;
41
+
42
+ const before = html.slice(0, match.index + whole.indexOf(body));
43
+ const offset = before.split('\n').length - 1;
44
+
45
+ try {
46
+ parse(body, {
47
+ sourceType: 'module', // accepts a plain script too
48
+ allowReturnOutsideFunction: true,
49
+ errorRecovery: false,
50
+ plugins: ['topLevelAwait'],
51
+ });
52
+ } catch (err) {
53
+ const line = (err.loc?.line ?? 1) + offset;
54
+ problems.push({ line, message: String(err.message ?? err).replace(/\s*\(\d+:\d+\)$/, '') });
55
+ }
56
+ }
57
+
58
+ return problems;
59
+ }
package/src/core/loop.js CHANGED
@@ -17,6 +17,7 @@ import { appendFileSync } from 'node:fs';
17
17
  import { readFile, access, mkdir } from 'node:fs/promises';
18
18
  import { testRunnerFor, relatedCommand, summariseFailures } from './tests.js';
19
19
  import { LogWatch } from './livelog.js';
20
+ import { checkHtml } from './htmlcheck.js';
20
21
  import { runningServers } from '../tools/shell.js';
21
22
  import { spawn } from 'node:child_process';
22
23
 
@@ -100,7 +101,7 @@ const SILENT = new Set(['update_plan']);
100
101
  const MAX_FIX_ROUNDS = 3;
101
102
 
102
103
  /** Files worth checking after they change. */
103
- const CHECKABLE = /\.(?:[cm]?[jt]sx?|py)$/i;
104
+ const CHECKABLE = /\.(?:[cm]?[jt]sx?|py|html?)$/i;
104
105
 
105
106
  /** Where TypeScript keeps what it learned, so the next check is a quick one. */
106
107
  export const TSBUILDINFO = 'node_modules/.cache/ucode/types.tsbuildinfo';
@@ -468,6 +469,11 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
468
469
  ' - One line when you move between the big pieces of work: "The layout is done,',
469
470
  ' now the animations."',
470
471
  ' - One line at the end saying what it does and how to try it.',
472
+ 'That closing line is ONE OR TWO SENTENCES. Never a checklist, never a feature',
473
+ 'list, never ticks or bullets walking through the request item by item. "Tide is',
474
+ 'built - open tide/index.html, or serve the folder and visit it." Anything longer',
475
+ 'is a status report nobody asked for, and it is the last thing on screen, so it',
476
+ 'is what the whole session looks like.',
471
477
  'That is all. A line before every tool call is not narration, it is noise: the',
472
478
  'steps already show on screen, and repeating them in words buries the few',
473
479
  'sentences worth reading.',
@@ -1656,11 +1662,23 @@ export class Agent {
1656
1662
  const root = path.resolve(this.cwd);
1657
1663
  const tsRoots = new Set();
1658
1664
  const singles = [];
1665
+ const problems = [];
1659
1666
 
1660
1667
  for (const rel of changed) {
1661
1668
  const abs = path.resolve(root, rel);
1662
1669
  if (!(await exists(abs))) continue;
1663
1670
  if (/\.py$/i.test(rel)) { singles.push({ abs, rel, command: `python -m py_compile "${abs}"` }); continue; }
1671
+ // A single-file app keeps all its logic in an inline <script>, which no
1672
+ // other check here ever looks at.
1673
+ if (/\.html?$/i.test(rel)) {
1674
+ const text = await readFile(abs, 'utf8').catch(() => null);
1675
+ const bad = text === null ? [] : checkHtml(text);
1676
+ if (bad.length) {
1677
+ problems.push(`${rel} — the script in this page does not parse, so none of it runs:\n` +
1678
+ bad.map((b) => ` line ${b.line}: ${b.message}`).join('\n'));
1679
+ }
1680
+ continue;
1681
+ }
1664
1682
  let dir = path.dirname(abs);
1665
1683
  let owner = null;
1666
1684
  while (dir.startsWith(root)) {
@@ -1673,7 +1691,6 @@ export class Agent {
1673
1691
  else if (/\.[cm]?js$/i.test(rel)) singles.push({ abs, rel, command: `node --check "${abs}"` });
1674
1692
  }
1675
1693
 
1676
- const problems = [];
1677
1694
  const check = async (label, command, cwd) => {
1678
1695
  this.ui.toolCall(label);
1679
1696
  this.ui.startSpinner(label);