ucode-agent 1.37.0 → 1.38.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.37.0",
3
+ "version": "1.38.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
@@ -19,6 +19,7 @@ import { testRunnerFor, relatedCommand, summariseFailures } from './tests.js';
19
19
  import { LogWatch } from './livelog.js';
20
20
  import { checkHtml } from './htmlcheck.js';
21
21
  import { runningServers } from '../tools/shell.js';
22
+ import { unprefixOwnFolder } from './relink.js';
22
23
  import { beginTurn, undoTurn, changedCount } from './undo.js';
23
24
  import { spawn } from 'node:child_process';
24
25
 
@@ -1912,6 +1913,17 @@ export class Agent {
1912
1913
  if (!changed.length) return null;
1913
1914
 
1914
1915
  const root = path.resolve(this.cwd);
1916
+
1917
+ // A page that links to its own folder, whoever wrote it. create_app fixes
1918
+ // the files it is handed, but an app written across several calls — a
1919
+ // scaffold, then two edits — reintroduces the prefix on the next write,
1920
+ // and nothing was watching after the first one. The page then loads no
1921
+ // stylesheet and no script, which is the difference between an app and a
1922
+ // wall of unstyled markup, so it is corrected wherever it turns up.
1923
+ const relinked = await unprefixOwnFolder(root, changed);
1924
+ if (relinked.length) {
1925
+ this.ui.toolCall(`Fixing self-referencing links in ${relinked.join(', ')}`);
1926
+ }
1915
1927
  const tsRoots = new Set();
1916
1928
  const singles = [];
1917
1929
  const problems = [];
@@ -0,0 +1,62 @@
1
+ /**
2
+ * relink.js — taking an app's own folder back out of its own links.
3
+ *
4
+ * create_app wants paths relative to the project root — "tide/index.html" —
5
+ * because that is where every file tool works from. The model then carries the
6
+ * same prefix into the markup and writes `<link href="tide/styles.css">` inside
7
+ * tide/index.html, where it resolves to tide/tide/styles.css and 404s. The page
8
+ * comes up as bare markup: no stylesheet, no script, dead buttons. It is the
9
+ * commonest way a finished build looks like nothing was built.
10
+ *
11
+ * It lived inside create_app first, which covered only the files that call was
12
+ * handed. An app written across a scaffold and two edits put the prefix back on
13
+ * the next write and nothing was watching, so it runs over every changed page
14
+ * now, on every turn, wherever the page came from.
15
+ */
16
+
17
+ import { promises as fs } from 'node:fs';
18
+ import path from 'node:path';
19
+
20
+ /** Relative href/src only — absolute paths, URLs, data: and anchors are left alone. */
21
+ const ASSET_REF = /\b(href|src)=("|')(?!https?:|\/\/|\/|data:|#|mailto:|tel:)([^"']+)\2/g;
22
+
23
+ /**
24
+ * Strip a page's own folder from its relative links.
25
+ *
26
+ * `files` are paths relative to `root`. A page at "tide/index.html" has "tide/"
27
+ * taken off its links, because inside that folder the prefix is always wrong.
28
+ * A page sitting directly in the root has no folder to strip and is skipped.
29
+ *
30
+ * Returns the pages it changed, for reporting. Never throws: a file that cannot
31
+ * be read is a file that is left exactly as it is.
32
+ */
33
+ export async function unprefixOwnFolder(root, files) {
34
+ const fixed = [];
35
+
36
+ for (const rel of files) {
37
+ if (!/\.html?$/i.test(rel)) continue;
38
+
39
+ const abs = path.resolve(root, rel);
40
+ const folder = path.basename(path.dirname(abs));
41
+ if (!folder || path.dirname(abs) === path.resolve(root)) continue;
42
+
43
+ const text = await fs.readFile(abs, 'utf8').catch(() => null);
44
+ if (text === null) continue;
45
+
46
+ const prefix = `${folder}/`;
47
+ let hits = 0;
48
+ const next = text.replace(ASSET_REF, (all, attr, quote, value) => {
49
+ if (!value.startsWith(prefix)) return all;
50
+ hits++;
51
+ return `${attr}=${quote}${value.slice(prefix.length)}${quote}`;
52
+ });
53
+
54
+ if (!hits) continue;
55
+ try {
56
+ await fs.writeFile(abs, next, 'utf8');
57
+ fixed.push(`${rel} (${hits})`);
58
+ } catch { /* unwritable: leave it, and say nothing that is not true */ }
59
+ }
60
+
61
+ return fixed;
62
+ }