ucode-agent 1.62.3 → 1.62.5

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/README.md CHANGED
@@ -27,7 +27,7 @@ corner:
27
27
  add a dark mode toggle that remembers the choice
28
28
 
29
29
 
30
- v1.62.3
30
+ v1.62.5
31
31
  ```
32
32
 
33
33
  A light crosses the wordmark once as it opens, and the three lines under the box
package/package.json CHANGED
@@ -1,63 +1,63 @@
1
- {
2
- "name": "ucode-agent",
3
- "version": "1.62.3",
4
- "description": "ucode - a terminal coding agent that reads, edits and runs your code, on Google Gemini models.",
5
- "type": "module",
6
- "main": "ucode.js",
7
- "bin": {
8
- "ucode": "ucode.js"
9
- },
10
- "files": [
11
- "ucode.js",
12
- "src/",
13
- "skills/",
14
- "templates/",
15
- "THIRD_PARTY_NOTICES.md"
16
- ],
17
- "scripts": {
18
- "start": "node ucode.js",
19
- "test": "node test/run.js",
20
- "prepublishOnly": "node scripts/no-bundled-key.js && node test/run.js",
21
- "hooks": "node scripts/install-hooks.js"
22
- },
23
- "repository": {
24
- "type": "git",
25
- "url": "git+https://github.com/sppideey/ucode-agent.git"
26
- },
27
- "bugs": {
28
- "url": "https://github.com/sppideey/ucode-agent/issues"
29
- },
30
- "homepage": "https://github.com/sppideey/ucode-agent#readme",
31
- "engines": {
32
- "node": ">=22"
33
- },
34
- "keywords": [
35
- "agent",
36
- "cli",
37
- "terminal",
38
- "coding-agent",
39
- "llm",
40
- "gemini",
41
- "google-gemini",
42
- "ai"
43
- ],
44
- "author": "om dixit",
45
- "license": "MIT",
46
- "dependencies": {
47
- "@babel/parser": "^7.29.9",
48
- "chalk": "^6.0.0",
49
- "dotenv": "^18.0.3",
50
- "jsonrepair": "^3.15.0",
51
- "marked": "^15.0.12",
52
- "marked-terminal": "^7.3.0",
53
- "openai": "^7.23.0",
54
- "playwright-core": "^1.63.0"
55
- },
56
- "devDependencies": {
57
- "@types/node": "^26.6.2",
58
- "typescript": "^5.9.3"
59
- },
60
- "publishConfig": {
61
- "access": "public"
62
- }
63
- }
1
+ {
2
+ "name": "ucode-agent",
3
+ "version": "1.62.5",
4
+ "description": "ucode - a terminal coding agent that reads, edits and runs your code, on Google Gemini models.",
5
+ "type": "module",
6
+ "main": "ucode.js",
7
+ "bin": {
8
+ "ucode": "ucode.js"
9
+ },
10
+ "files": [
11
+ "ucode.js",
12
+ "src/",
13
+ "skills/",
14
+ "templates/",
15
+ "THIRD_PARTY_NOTICES.md"
16
+ ],
17
+ "scripts": {
18
+ "start": "node ucode.js",
19
+ "test": "node test/run.js",
20
+ "prepublishOnly": "node scripts/no-bundled-key.js && node test/run.js",
21
+ "hooks": "node scripts/install-hooks.js"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/sppideey/ucode-agent.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/sppideey/ucode-agent/issues"
29
+ },
30
+ "homepage": "https://github.com/sppideey/ucode-agent#readme",
31
+ "engines": {
32
+ "node": ">=22"
33
+ },
34
+ "keywords": [
35
+ "agent",
36
+ "cli",
37
+ "terminal",
38
+ "coding-agent",
39
+ "llm",
40
+ "gemini",
41
+ "google-gemini",
42
+ "ai"
43
+ ],
44
+ "author": "om dixit",
45
+ "license": "MIT",
46
+ "dependencies": {
47
+ "@babel/parser": "^7.29.9",
48
+ "chalk": "^6.0.0",
49
+ "dotenv": "^18.0.3",
50
+ "jsonrepair": "^3.15.0",
51
+ "marked": "^15.0.12",
52
+ "marked-terminal": "^7.3.0",
53
+ "openai": "^7.23.0",
54
+ "playwright-core": "^1.63.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^26.6.2",
58
+ "typescript": "^5.9.3"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ }
63
+ }
@@ -1,145 +1,154 @@
1
- # Building something from nothing — the short form
2
-
3
- The failure mode is not bad code. It is a folder of files that has never been
4
- run, handed over as if it works.
5
-
6
- ## 1. Decide the shape before any file exists
7
-
8
- One line each: **what it does**, **the core loop** (the one path that must work
9
- perfectly), **the stack**, **the file list**.
10
-
11
- | Need | Choose |
12
- | --- | --- |
13
- | One page, no secrets, no server | `create_app` with `plain-html` |
14
- | Interactive client app, no secrets | `plain-html` still, unless it truly needs a build |
15
- | Pages plus a server, secrets, API routes, SEO | `create_app` with `next-shadcn` |
16
- | An API on its own | Node (Hono/Express) or Python (FastAPI) |
17
-
18
- Pick the smallest one that does the job and mean it: a tasks app, a
19
- calculator, a timer, a game, a visualisation — all one page. Next.js costs an
20
- install and a build, minutes the user waits through, and buys nothing an app
21
- with no server needs.
22
-
23
- ## 2. Start from the starter — and finish in the same call
24
-
25
- `create_app` takes `files`, so for a one-page app the scaffold and the whole
26
- app are one call:
27
-
28
- ```
29
- create_app({ folder: "tide", name: "Tide", files: [
30
- { path: "tide/index.html", content: "…" },
31
- { path: "tide/styles.css", content: "…" },
32
- { path: "tide/app.js", content: "…" },
33
- ]})
34
- ```
35
-
36
- Every round trip is ten to forty seconds of the user's time, so one call
37
- instead of four is most of how long the build takes.
38
-
39
- - `plain-html` is the default: three files, no install, no build. Its files
40
- come back in full inside the result — **never read them back**.
41
- - `next-shadcn` installs in the background; commands in that folder wait for
42
- it on their own, so start writing components at once. Re-tint `globals.css`
43
- for the app's direction rather than shipping the slate default.
44
- - Never run `create-next-app` or `shadcn init`. Nothing you run has a
45
- keyboard: every scaffolder needs its answers as flags up front.
46
-
47
- ## 2a. Paths in `files` are not the paths in the page
48
-
49
- The two are relative to different things, and getting them confused is the
50
- commonest way a finished build comes up as bare markup. `files` paths are
51
- relative to the **project root**, so they carry the app folder. A link inside a
52
- page is relative to **that page**, so it must not.
53
-
54
- ```
55
- create_app({ folder: "tide", name: "Tide", files: [
56
- { path: "tide/index.html", content: "... <link rel=stylesheet href=\"styles.css\">
57
- <script type=module src=\"app.js\"></script> ..." },
58
- { path: "tide/styles.css", content: "..." },
59
- { path: "tide/app.js", content: "..." },
60
- ]})
61
- ```
62
-
63
- Wrong, and it 404s: `href="tide/styles.css"` inside `tide/index.html` — the
64
- browser resolves that to `tide/tide/styles.css`, so no stylesheet and no script
65
- load, and the page is unstyled markup with dead buttons.
66
-
67
- ## 2b. Look before you create
68
-
69
- `create_app` refuses a folder that already has something in it unless you pass
70
- the app in `files` — and a folder you half-made on an earlier attempt counts.
71
- One `list_dir` in front of it costs a second and tells you whether you are
72
- starting or resuming. The same goes for any command that makes a directory.
73
-
74
- ## 2c. Do not type what already exists
75
-
76
- `add_block` has the pieces every app needs, written for whichever starter this
77
- one uses: a list you can add to, tick off, rename and remove; a filter row; a
78
- localStorage store; a dialog; toasts; a theme toggle; a table; an empty state.
79
- Call it before writing any of those by hand. Each is a hundred lines you skip,
80
- and typing is the slowest part of a build — a page assembled from blocks is
81
- done minutes before the same page typed out. Call `add_block` with no name to
82
- see what fits this app.
83
-
84
- ## 3. Structure
85
-
86
- One component per file, named for what it is, not a 600-line `page.tsx`. In
87
- Next.js: `src/app` (routes, `globals.css`, `api/<name>/route.ts`),
88
- `src/components/<feature>/`, `src/lib/` for outside services and schemas.
89
- Server components by default, `"use client"` only where it is interactive.
90
- Types at every boundary; parse external data rather than trusting its shape.
91
-
92
- ## 4. Secrets and outside services
93
-
94
- - **A key never reaches the browser.** It lives in a server route. Anything
95
- imported by a `"use client"` file ships to every visitor, including a
96
- "hardcoded for now" key — put it in a server-only module and say where.
97
- - Every outbound call gets a timeout (`AbortSignal.timeout(60_000)`), a status
98
- check, and an error that says what failed — surfaced as a real message,
99
- never a silent `catch {}`.
100
- - Calling a model: ask for JSON and parse it defensively (extract the first
101
- `{...}`, validate, clamp numbers), put the judgement rules in the prompt
102
- explicitly, and make the route timeout longer than the model takes.
103
-
104
- ## 5. Build order
105
-
106
- Skeleton and design tokens first, so everything after is styled correctly the
107
- first time; then the server route with the real integration; then the core
108
- loop UI wired to it; then every state — empty, loading, success, error,
109
- invalid input; then polish: motion, responsive, copy, title and metadata.
110
-
111
- ## 5a. It has to look finished
112
-
113
- People judge the app in the first second it is on screen, before they click
114
- anything. Before handing it over, check:
115
-
116
- - A real title and one line saying what it is for — never "Welcome to…".
117
- - The main action is visible without scrolling and is the most prominent button.
118
- - Every button and input has hover and focus states; nothing is left browser-default.
119
- - An empty list says what to do next ("No tasks yet — add one above"), never blank.
120
- - It fits a 375px phone with no sideways scroll; tap targets are at least 44px.
121
- - The number that matters (a score, a total, a timer) is big — several times body text.
122
-
123
- ## 5b. Tests, where there is a runner
124
-
125
- `next-shadcn` ships vitest and one passing test, so `npm test` works from
126
- the first minute — add cases for the core loop as you build it, not after, and
127
- ucode will run the ones that touch whatever you change. Assert behaviour: that
128
- adding an item puts it in the list, that the total is right, that an empty
129
- input is refused.
130
-
131
- `plain-html` has no runner and installs nothing, by design. Its test is the
132
- browser check: ucode opens the app, types into the first field, presses Enter
133
- and clicks the button that submits. Make sure that path is the one that works.
134
-
135
- ## 6. Prove it works, then report
136
-
137
- `npm run build` type-checks and lints — a build that fails is not done. Start
138
- it (`npm run dev` backgrounds itself and returns the URL; do not start it
139
- twice), then `look_at_app` on every page. A clean build proves it compiles,
140
- not that it works. Fix what you find and check again.
141
-
142
- Done means: the core loop works end to end, no TODO, no placeholder copy, no
143
- dead buttons, no console errors, every async action has its states, secrets
144
- server-side, build passes. Then say what you built, how to run it, and — in
145
- one sentence — anything you did not finish or could not test.
1
+ # Building something from nothing — the short form
2
+
3
+ The failure mode is not bad code. It is a folder of files that has never been
4
+ run, handed over as if it works.
5
+
6
+ ## 1. Decide the shape before any file exists
7
+
8
+ One line each: **what it does**, **the core loop** (the one path that must work
9
+ perfectly), **the stack**, **the file list**.
10
+
11
+ | Need | Choose |
12
+ | --- | --- |
13
+ | One page, no secrets, no server | `create_app` with `plain-html` |
14
+ | Interactive client app, no secrets | `plain-html` still, unless it truly needs a build |
15
+ | Pages plus a server, secrets, API routes, SEO | `create_app` with `next-shadcn` |
16
+ | An API on its own | Node (Hono/Express) or Python (FastAPI) |
17
+
18
+ **Scope: complete, not sprawling.** Build what was asked, and build all of it
19
+ well: every feature someone using that kind of app expects on first use — a
20
+ tasks app adds, edits, ticks off, deletes, filters and remembers across a
21
+ reload; a quiz has questions, scoring, feedback and a restart; a game has
22
+ rules, a score, win and lose, and play again. Nothing from a different app: no
23
+ timer, calendar or analytics dashboard bolted onto a tasks app, no accounts on
24
+ a quiz. Features nobody named cost minutes and are where builds break — if one
25
+ is worth it, offer it in one line at the end instead of building it.
26
+
27
+ Pick the smallest stack that does the job and mean it: a tasks app, a
28
+ calculator, a timer, a game, a visualisation — all one page. Next.js costs an
29
+ install and a build, minutes the user waits through, and buys nothing an app
30
+ with no server needs.
31
+
32
+ ## 2. Start from the starter — and finish in the same call
33
+
34
+ `create_app` takes `files`, so for a one-page app the scaffold and the whole
35
+ app are one call:
36
+
37
+ ```
38
+ create_app({ folder: "tide", name: "Tide", files: [
39
+ { path: "tide/index.html", content: "…" },
40
+ { path: "tide/styles.css", content: "…" },
41
+ { path: "tide/app.js", content: "…" },
42
+ ]})
43
+ ```
44
+
45
+ Every round trip is ten to forty seconds of the user's time, so one call
46
+ instead of four is most of how long the build takes.
47
+
48
+ - `plain-html` is the default: three files, no install, no build. Its files
49
+ come back in full inside the result — **never read them back**.
50
+ - `next-shadcn` installs in the background; commands in that folder wait for
51
+ it on their own, so start writing components at once. Re-tint `globals.css`
52
+ for the app's direction rather than shipping the slate default.
53
+ - Never run `create-next-app` or `shadcn init`. Nothing you run has a
54
+ keyboard: every scaffolder needs its answers as flags up front.
55
+
56
+ ## 2a. Paths in `files` are not the paths in the page
57
+
58
+ The two are relative to different things, and getting them confused is the
59
+ commonest way a finished build comes up as bare markup. `files` paths are
60
+ relative to the **project root**, so they carry the app folder. A link inside a
61
+ page is relative to **that page**, so it must not.
62
+
63
+ ```
64
+ create_app({ folder: "tide", name: "Tide", files: [
65
+ { path: "tide/index.html", content: "... <link rel=stylesheet href=\"styles.css\">
66
+ <script type=module src=\"app.js\"></script> ..." },
67
+ { path: "tide/styles.css", content: "..." },
68
+ { path: "tide/app.js", content: "..." },
69
+ ]})
70
+ ```
71
+
72
+ Wrong, and it 404s: `href="tide/styles.css"` inside `tide/index.html` — the
73
+ browser resolves that to `tide/tide/styles.css`, so no stylesheet and no script
74
+ load, and the page is unstyled markup with dead buttons.
75
+
76
+ ## 2b. Look before you create
77
+
78
+ `create_app` refuses a folder that already has something in it unless you pass
79
+ the app in `files` — and a folder you half-made on an earlier attempt counts.
80
+ One `list_dir` in front of it costs a second and tells you whether you are
81
+ starting or resuming. The same goes for any command that makes a directory.
82
+
83
+ ## 2c. Do not type what already exists
84
+
85
+ `add_block` has the pieces every app needs, written for whichever starter this
86
+ one uses: a list you can add to, tick off, rename and remove; a filter row; a
87
+ localStorage store; a dialog; toasts; a theme toggle; a table; an empty state.
88
+ Call it before writing any of those by hand. Each is a hundred lines you skip,
89
+ and typing is the slowest part of a build — a page assembled from blocks is
90
+ done minutes before the same page typed out. Call `add_block` with no name to
91
+ see what fits this app.
92
+
93
+ ## 3. Structure
94
+
95
+ One component per file, named for what it is, not a 600-line `page.tsx`. In
96
+ Next.js: `src/app` (routes, `globals.css`, `api/<name>/route.ts`),
97
+ `src/components/<feature>/`, `src/lib/` for outside services and schemas.
98
+ Server components by default, `"use client"` only where it is interactive.
99
+ Types at every boundary; parse external data rather than trusting its shape.
100
+
101
+ ## 4. Secrets and outside services
102
+
103
+ - **A key never reaches the browser.** It lives in a server route. Anything
104
+ imported by a `"use client"` file ships to every visitor, including a
105
+ "hardcoded for now" key — put it in a server-only module and say where.
106
+ - Every outbound call gets a timeout (`AbortSignal.timeout(60_000)`), a status
107
+ check, and an error that says what failed — surfaced as a real message,
108
+ never a silent `catch {}`.
109
+ - Calling a model: ask for JSON and parse it defensively (extract the first
110
+ `{...}`, validate, clamp numbers), put the judgement rules in the prompt
111
+ explicitly, and make the route timeout longer than the model takes.
112
+
113
+ ## 5. Build order
114
+
115
+ Skeleton and design tokens first, so everything after is styled correctly the
116
+ first time; then the server route with the real integration; then the core
117
+ loop UI wired to it; then every state — empty, loading, success, error,
118
+ invalid input; then polish: motion, responsive, copy, title and metadata.
119
+
120
+ ## 5a. It has to look finished
121
+
122
+ People judge the app in the first second it is on screen, before they click
123
+ anything. Before handing it over, check:
124
+
125
+ - A real title and one line saying what it is for — never "Welcome to…".
126
+ - The main action is visible without scrolling and is the most prominent button.
127
+ - Every button and input has hover and focus states; nothing is left browser-default.
128
+ - An empty list says what to do next ("No tasks yet — add one above"), never blank.
129
+ - It fits a 375px phone with no sideways scroll; tap targets are at least 44px.
130
+ - The number that matters (a score, a total, a timer) is big — several times body text.
131
+
132
+ ## 5b. Tests, where there is a runner
133
+
134
+ `next-shadcn` ships vitest and one passing test, so `npm test` works from
135
+ the first minute — add cases for the core loop as you build it, not after, and
136
+ ucode will run the ones that touch whatever you change. Assert behaviour: that
137
+ adding an item puts it in the list, that the total is right, that an empty
138
+ input is refused.
139
+
140
+ `plain-html` has no runner and installs nothing, by design. Its test is the
141
+ browser check: ucode opens the app, types into the first field, presses Enter
142
+ and clicks the button that submits. Make sure that path is the one that works.
143
+
144
+ ## 6. Prove it works, then report
145
+
146
+ `npm run build` type-checks and lints — a build that fails is not done. Start
147
+ it (`npm run dev` backgrounds itself and returns the URL; do not start it
148
+ twice), then `look_at_app` on every page. A clean build proves it compiles,
149
+ not that it works. Fix what you find and check again.
150
+
151
+ Done means: the core loop works end to end, no TODO, no placeholder copy, no
152
+ dead buttons, no console errors, every async action has its states, secrets
153
+ server-side, build passes. Then say what you built, how to run it, and — in
154
+ one sentence — anything you did not finish or could not test.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: build-app
3
3
  description: Take an app from nothing to running and finished — stack choice, non-interactive scaffolding, project structure, secrets, AI and API integration, error handling, and proving it works before saying it does.
4
- auto: scaffold, new project, from scratch, build an app, make an app, create an app, build a website, make a website, build a site, build me a, next.js app, nextjs app, next app, react app, vite app, shadcn, create-next-app, full stack, fullstack, saas, mvp
4
+ auto: scaffold, new project, from scratch, build an app, make an app, create an app, make me a, make me an, build me an, create me a, build a website, make a website, build a site, build me a, next.js app, nextjs app, next app, react app, vite app, shadcn, create-next-app, full stack, fullstack, saas, mvp
5
5
  ---
6
6
 
7
7
  # Building something from nothing
@@ -32,6 +32,15 @@ One line each:
32
32
 
33
33
  - **The file list** — the whole tree, before creating any of it.
34
34
 
35
+ **Scope: complete, not sprawling.** Build what was asked, and build all of it
36
+ well: every feature someone using that kind of app expects on first use — a
37
+ tasks app adds, edits, ticks off, deletes, filters and remembers across a
38
+ reload; a quiz has questions, scoring, feedback and a restart; a game has
39
+ rules, a score, win and lose, and play again. Nothing from a different app: no
40
+ timer, calendar or analytics dashboard bolted onto a tasks app, no accounts on
41
+ a quiz. Features nobody named cost minutes and are where builds break — if one
42
+ is worth it, offer it in one line at the end instead of building it.
43
+
35
44
  If there is a user interface, the `ui-ux` skill is already loaded. Decide the
36
45
  design direction now, not after the logic works. If the app calls a model,
37
46
  load `ai-features`; if it has accounts, keys or uploads, load `security`.
package/src/core/loop.js CHANGED
@@ -50,6 +50,9 @@ import { Failure, ToolFailure, Declined } from './failure.js';
50
50
  import { StuckWatch, eventFor, describeHit } from './stuck.js';
51
51
  import { serversReadySince } from '../tools/shell.js';
52
52
  import { formatDuration } from '../ui/activity.js';
53
+ import { MAX_FILE_OUTPUT } from '../tools/shared.js';
54
+ import { openInBrowser } from './opener.js';
55
+ import { withScope } from './scope.js';
53
56
  import { runDoctor } from './doctor.js';
54
57
  import { JS_LOGIC } from './jslogic.js';
55
58
  import { deploy } from '../tools/deploy.js';
@@ -109,6 +112,12 @@ const SILENT = new Set(['update_plan']);
109
112
  /** How many rounds of "the type check found errors, fix them" one turn may take. */
110
113
  const MAX_FIX_ROUNDS = 3;
111
114
 
115
+ /** A file up to this many lines is sent whole on its first read, whatever slice was asked for. */
116
+ const WHOLE_READ_LINES = 1500;
117
+
118
+ /** Lookups in a row, during a fix round with nothing changed, before ucode says to stop reading. */
119
+ const LOOKUP_NUDGE = 5;
120
+
112
121
  /** Files worth checking after they change. */
113
122
  /** A file with a page in it — something a browser can be pointed at. */
114
123
  const PAGE = /\.(?:html?|tsx|jsx)$/i;
@@ -621,6 +630,15 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
621
630
  'already contains it. Do not re-check work the checks have already reported on.',
622
631
  'Fast is not sloppy: it is the same work with the waiting taken out.',
623
632
  '',
633
+ 'BUILD WHAT WAS ASKED - ALL OF IT, AND NOTHING ELSE. A short request ("a tasks',
634
+ 'app", "a quiz", "a snake game") still gets a complete, well-made app: every',
635
+ 'feature someone using that kind of app expects on first use. A tasks app adds,',
636
+ 'edits, ticks off, deletes, filters and remembers across a reload. It does NOT',
637
+ 'also get a pomodoro timer, a calendar, an analytics dashboard, stats charts or a',
638
+ 'shortcuts panel - those are other apps. Extra views are where builds break, and',
639
+ 'each one costs the user minutes. One screen that does its job perfectly beats',
640
+ 'four that half work. An extra worth having: offer it in one line at the end.',
641
+ '',
624
642
  'DESIGN IT BEFORE YOU TYPE IT. Fast means fewer round trips. It does not mean a',
625
643
  'default theme, and an app that goes out in the palette its starter came with is',
626
644
  'not a fast build, it is an undesigned one. There is no design pass after the',
@@ -632,7 +650,7 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
632
650
  ' - An ACCENT that is not the one the starter shipped with.',
633
651
  ' - ONE memorable thing this app has that other apps do not: a colour, a type',
634
652
  ' move, a texture, one interaction. Exactly one. It is the difference between',
635
- ' a design and a theme.',
653
+ ' a design and a theme - a design choice, never an extra feature or view.',
636
654
  'Name the tone and the accent in your opening line, so they are settled before any',
637
655
  'file exists: "I will build Tide - a tasks app in one HTML file, calm, warm grey',
638
656
  'with a single amber accent." That is not narrating a plan, that is the decision.',
@@ -1205,6 +1223,9 @@ export class Agent {
1205
1223
  this.appName = null;
1206
1224
  this.appTemplate = null;
1207
1225
  this.reads = new Map();
1226
+ this.fixing = false;
1227
+ this.lookups = 0;
1228
+ this.nudgedAt = 0;
1208
1229
  this.declines = 0;
1209
1230
  this.apps = [];
1210
1231
  this.wrote = new Map();
@@ -1216,9 +1237,8 @@ export class Agent {
1216
1237
  // model answered: asked for a dark mode toggle on its second turn, a live
1217
1238
  // run re-read two files and repeated its first turn's summary instead.
1218
1239
  this.autoLoad(input);
1219
- this.push(images.length
1220
- ? { role: 'user', content: input, images }
1221
- : { role: 'user', content: input });
1240
+ const request = images.length ? { role: 'user', content: input, images } : { role: 'user', content: input };
1241
+ this.push(request);
1222
1242
 
1223
1243
  if (!this.session.title || this.session.title === 'Untitled') {
1224
1244
  this.session.title = titleFrom(input);
@@ -1231,6 +1251,7 @@ export class Agent {
1231
1251
  // Nothing to look up in an empty folder, so those tools do not go out with
1232
1252
  // the request. Decided per turn: the moment there is code, they are back.
1233
1253
  this.fresh = !hasCode(this.map);
1254
+ request.content = withScope(input, { fresh: this.fresh });
1234
1255
  this.wantsWeb = WANTS_WEB.test(input);
1235
1256
  await this.persist();
1236
1257
 
@@ -1486,6 +1507,7 @@ export class Agent {
1486
1507
  const problems = await this.autoCheck();
1487
1508
  if (problems) {
1488
1509
  fixRounds++;
1510
+ this.fixing = true;
1489
1511
  if (reply.text) this.push({ role: 'assistant', content: reply.text });
1490
1512
  this.push({
1491
1513
  role: 'user',
@@ -1571,6 +1593,7 @@ export class Agent {
1571
1593
  if (handed?.done) return;
1572
1594
  if (handed?.problems && fixRounds < MAX_FIX_ROUNDS) {
1573
1595
  fixRounds++;
1596
+ this.fixing = true;
1574
1597
  this.push({
1575
1598
  role: 'user',
1576
1599
  content:
@@ -1643,8 +1666,16 @@ export class Agent {
1643
1666
  const noted = (call) => {
1644
1667
  if (FILE_WRITES.has(call.name)) {
1645
1668
  for (const p of pathsOf(call)) { this.touched.add(p); this.sinceCheck.add(p); this.forgetReads(p); }
1669
+ this.lookups = 0;
1670
+ this.nudgedAt = 0;
1671
+ } else if (PARALLEL_SAFE.has(call.name)) {
1672
+ this.lookups = (this.lookups ?? 0) + 1;
1673
+ }
1674
+ if (call.name === 'run_command' || call.name === 'run_commands') {
1675
+ this.ranSomething = true;
1676
+ // A command can rewrite any file, so no whole read is still known to be current.
1677
+ for (const key of this.reads?.keys() ?? []) if (key.endsWith('#whole')) this.reads.delete(key);
1646
1678
  }
1647
- if (call.name === 'run_command' || call.name === 'run_commands') this.ranSomething = true;
1648
1679
  };
1649
1680
 
1650
1681
  if (group.length > 1) {
@@ -1753,26 +1784,10 @@ export class Agent {
1753
1784
  if (this.openInBrowser(server.url)) this.ui.note(`Opened ${server.url} in your browser`);
1754
1785
  }
1755
1786
 
1756
- /**
1757
- * Show a URL or a local page in the desktop browser. False when it did not.
1758
- *
1759
- * A file goes to explorer on Windows, not `cmd /c start`: the folder name
1760
- * comes from the model, and an & in it would be a second command to cmd.
1761
- */
1787
+ /** Show a URL or a page in the project in the desktop browser (see opener.js). UCODE_OPEN=0 turns it off. */
1762
1788
  openInBrowser(target) {
1763
1789
  if (!this.full || process.env.UCODE_OPEN === '0') return false;
1764
- const web = /^https?:\/\//.test(target);
1765
- // Only pages inside the project: a UNC path would have explorer reach out to another machine.
1766
- const rel = web ? '' : path.relative(this.cwd, target);
1767
- if (!web && (!rel || rel.startsWith('..') || path.isAbsolute(rel))) return false;
1768
- const [cmd, args] = process.platform === 'win32'
1769
- ? (web ? ['cmd', ['/c', 'start', '', target]] : ['explorer.exe', [target]])
1770
- : [process.platform === 'darwin' ? 'open' : 'xdg-open', [target]];
1771
- try {
1772
- // A missing opener (no xdg-open) fails later, as an event; unheard, it would crash ucode.
1773
- spawn(cmd, args, { stdio: 'ignore', detached: true, windowsHide: true }).on('error', () => {}).unref();
1774
- return true;
1775
- } catch { return false; /* no browser to open — the link is in the answer */ }
1790
+ return openInBrowser(target, { root: this.cwd });
1776
1791
  }
1777
1792
 
1778
1793
  cmdStats() {
@@ -1878,7 +1893,34 @@ export class Agent {
1878
1893
  const found = /^(\d+) problem/.exec(out.summary ?? '');
1879
1894
  this.ui.runStat?.(found ? `${found[1]} to fix` : 'clean');
1880
1895
  }
1881
- this.push({ role: 'tool', toolCallId: call.id, name: call.name, content: out.content + this.stuckNote(call, { out }) });
1896
+ this.push({ role: 'tool', toolCallId: call.id, name: call.name, content: out.content + this.stuckNote(call, { out }) + this.lookupNote(call) });
1897
+ }
1898
+
1899
+ /**
1900
+ * Reading on and on while ucode's list of errors waits. A live fix round
1901
+ * read a 1000-line app.js in seventeen slices, changed nothing, and ran the
1902
+ * free key into its per-minute limit: every read resends the conversation.
1903
+ */
1904
+ lookupNote(call) {
1905
+ if (!this.fixing || !PARALLEL_SAFE.has(call.name)) return '';
1906
+ if (this.lookups < LOOKUP_NUDGE || this.lookups - this.nudgedAt < LOOKUP_NUDGE) return '';
1907
+ this.nudgedAt = this.lookups;
1908
+ return `\n\nSTOP reading - that is ${this.lookups} lookups since ucode listed the errors, and nothing has been ` +
1909
+ 'changed yet. What you need is already above. Make the fix now with edit_file or multi_edit; ucode ' +
1910
+ 'checks the app again straight after.';
1911
+ }
1912
+
1913
+ /** Lines in a file small enough to send whole in one read, or null. */
1914
+ async wholeSize(file) {
1915
+ try {
1916
+ const text = await readFile(path.resolve(this.cwd, file), 'utf8');
1917
+ if (text.includes('\0')) return null;
1918
+ const lines = text.split('\n').length;
1919
+ // The line-number gutter adds about eight characters a line to what read_file sends.
1920
+ return lines <= WHOLE_READ_LINES && text.length + lines * 8 <= MAX_FILE_OUTPUT ? { lines } : null;
1921
+ } catch {
1922
+ return null;
1923
+ }
1882
1924
  }
1883
1925
 
1884
1926
  /**
@@ -2034,6 +2076,27 @@ export class Agent {
2034
2076
  // refused as "unchanged", and a live build spent sixty steps being told so.
2035
2077
  if (call.name === 'read_file' && call.args?.path) {
2036
2078
  const file = String(call.args.path);
2079
+
2080
+ // A file that fits is sent whole the first time, and not again until
2081
+ // something writes to it. Slices were the loop: a live fix round read one
2082
+ // app.js in seventeen of them, never edited, and hit the per-minute limit.
2083
+ // The lead only — a worker has its own conversation, without the lead's reads.
2084
+ if (offered === this.offering) {
2085
+ const whole = `${fileKey(file)}#whole`;
2086
+ if (this.reads.has(whole)) {
2087
+ return {
2088
+ content: `All of ${file} is already above: you read the whole file and nothing has written to it ` +
2089
+ 'since. Use that text instead of reading it again. If something in it needs changing, change it ' +
2090
+ 'now with edit_file or multi_edit.',
2091
+ summary: 'unchanged, already read in full',
2092
+ };
2093
+ }
2094
+ // Marked before the await, so two reads of one file in a parallel batch send it once.
2095
+ this.reads.set(whole, 1);
2096
+ const size = await this.wholeSize(file);
2097
+ if (size) call.args = { ...call.args, offset: 1, limit: size.lines };
2098
+ else this.reads.delete(whole);
2099
+ }
2037
2100
  const key = `${fileKey(file)}#${Number(call.args.offset) || 1}:${Number(call.args.limit) || 0}`;
2038
2101
  const seen = (this.reads ??= new Map()).get(key) ?? 0;
2039
2102
  if (seen >= 2) {
@@ -2809,6 +2872,8 @@ export class Agent {
2809
2872
  this.ui.stopSpinner();
2810
2873
  if (result.folded) {
2811
2874
  this.working = result.messages;
2875
+ // What was read is summarised away now, so it may be read again.
2876
+ this.reads = new Map();
2812
2877
  this.ui.note(
2813
2878
  `folded ${result.droppedCount} earlier messages into a summary ` +
2814
2879
  '(the full history is still saved in this session)'
@@ -0,0 +1,44 @@
1
+ /**
2
+ * opener.js — showing a finished page or a running server in the user's own
3
+ * browser.
4
+ *
5
+ * Kept apart from the loop because it is the one place ucode starts a program
6
+ * on a path the model chose, and that deserves to be read on its own.
7
+ */
8
+
9
+ import path from 'node:path';
10
+ import { spawn } from 'node:child_process';
11
+
12
+ /** Is this a web address rather than a file on disk? */
13
+ const isWeb = (target) => /^https?:\/\//.test(target);
14
+
15
+ /**
16
+ * Only pages inside the project are opened. A folder name comes from the
17
+ * model, and a UNC path (\\host\share) would have explorer reach out to
18
+ * another machine.
19
+ */
20
+ export function insideRoot(target, root) {
21
+ const rel = path.relative(root, target);
22
+ return Boolean(rel) && !rel.startsWith('..') && !path.isAbsolute(rel);
23
+ }
24
+
25
+ /**
26
+ * Open a URL or a page inside `root`. Returns whether an opener was started.
27
+ *
28
+ * A file goes to explorer on Windows, not `cmd /c start`: an & in a folder
29
+ * name would be a second command to cmd.
30
+ */
31
+ export function openInBrowser(target, { root }) {
32
+ const web = isWeb(target);
33
+ if (!web && !insideRoot(target, root)) return false;
34
+ const [cmd, args] = process.platform === 'win32'
35
+ ? (web ? ['cmd', ['/c', 'start', '', target]] : ['explorer.exe', [target]])
36
+ : [process.platform === 'darwin' ? 'open' : 'xdg-open', [target]];
37
+ try {
38
+ // A missing opener (no xdg-open) fails later, as an event; unheard, it would crash ucode.
39
+ spawn(cmd, args, { stdio: 'ignore', detached: true, windowsHide: true }).on('error', () => {}).unref();
40
+ return true;
41
+ } catch {
42
+ return false; // no browser to open — the link is in the answer
43
+ }
44
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * scope.js — keeping a new app to what was asked for.
3
+ *
4
+ * In the system prompt and the build-app skill alone, Flash-Lite still built
5
+ * "make me a tasks app" as a tasks app plus a pomodoro timer, a kanban board
6
+ * and analytics — twice running — and the extra views were where the builds
7
+ * broke. So the rule also rides on the request itself, the last thing the
8
+ * model reads before it starts.
9
+ */
10
+
11
+ export const SCOPE_NOTE =
12
+ '(From ucode: if this asks for a new app, build it complete and well made, but as the one app asked ' +
13
+ 'for - no extra views or tools such as timers, calendars, kanban boards, analytics, stats or an ' +
14
+ 'editor for making your own, unless the request names them.)';
15
+
16
+ /** A request that may be for a new app. Loose on purpose: the note it adds says "if". */
17
+ export const BUILD_ASK =
18
+ /\b(?:make|build|create|design|code|write|generate|develop)\b|\bi\s+(?:want|need)\b|\b(?:app|game|website|site|page|tracker|calculator|quiz)\b/i;
19
+
20
+ /** The request as the model sees it: one in an empty folder, or shaped like a build, carries the note. */
21
+ export function withScope(input, { fresh = false } = {}) {
22
+ return fresh || BUILD_ASK.test(input) ? `${input}\n\n${SCOPE_NOTE}` : input;
23
+ }
@@ -232,6 +232,63 @@ const NOT_ADD = /search|filter|find|sort|query/i;
232
232
  const LIST_HINT = /task|todo|to-do|item|note|add|new|entry|expense|habit|title|what needs|remind/i;
233
233
  const ADD_BUTTON = /^\s*(?:\+|add|new|create|save|submit|post|done)\b|\b(?:add|create|save)\s/i;
234
234
 
235
+ /** A value a form will take for a field of this type, or null to leave the field alone. */
236
+ function sampleFor(type, today) {
237
+ switch (type) {
238
+ case 'number': return '42';
239
+ case 'date': return today;
240
+ case 'time': return '12:00';
241
+ case 'datetime-local': return `${today}T12:00`;
242
+ case 'month': return today.slice(0, 7);
243
+ case 'email': return 'check@example.com';
244
+ case 'url': return 'https://example.com';
245
+ case 'tel': return '5550100';
246
+ case 'text': case 'textarea': case 'search': case '': return PROBE_TEXT;
247
+ default: return null; // checkboxes, radios, files, colours: nothing to guess
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Fill in the rest of the form the probe text went into, the way a person
253
+ * would before pressing Add.
254
+ *
255
+ * Traced: a budget tracker's form also needs an amount. Only the description
256
+ * was typed, the browser refused the submit, and a working app was reported as
257
+ * "ADDING DOES NOT WORK" — the model then spent its fix rounds on code that
258
+ * worked. Filled: required fields, empty number fields (amounts are often
259
+ * checked in script, not marked required) and dropdowns left on a blank choice.
260
+ */
261
+ async function fillTheRest(page, field) {
262
+ const marked = await field.evaluate((e) => {
263
+ if (!e.form) return false;
264
+ e.form.setAttribute('data-ucode-probe', '');
265
+ return true;
266
+ }).catch(() => false);
267
+ if (!marked) return;
268
+
269
+ const today = new Date().toISOString().slice(0, 10);
270
+ const all = page.locator('form[data-ucode-probe] :is(input, select, textarea)');
271
+ const n = Math.min(await all.count().catch(() => 0), 20);
272
+ for (let i = 0; i < n; i++) {
273
+ const el = all.nth(i);
274
+ const info = await el.evaluate((e) => ({
275
+ tag: e.tagName.toLowerCase(),
276
+ type: (e.getAttribute('type') || (e.tagName === 'TEXTAREA' ? 'textarea' : '')).toLowerCase(),
277
+ empty: !e.value,
278
+ required: e.required,
279
+ })).catch(() => null);
280
+ if (!info?.empty || !(await el.isVisible().catch(() => false))) continue;
281
+ if (info.tag === 'select') {
282
+ await el.selectOption({ index: 1 }, { timeout: 2_000 }).catch(() => {});
283
+ continue;
284
+ }
285
+ if (!info.required && info.type !== 'number') continue;
286
+ const value = sampleFor(info.type, today);
287
+ if (value !== null) await el.fill(value, { timeout: 2_000 }).catch(() => {});
288
+ }
289
+ await page.locator('form[data-ucode-probe]').evaluate((f) => f.removeAttribute('data-ucode-probe')).catch(() => {});
290
+ }
291
+
235
292
  async function tryAdd(page) {
236
293
  const shows = () => page.evaluate((t) => document.body.innerText.includes(t), PROBE_TEXT).catch(() => false);
237
294
  const settle = () => page.waitForTimeout(400);
@@ -279,6 +336,7 @@ async function tryAdd(page) {
279
336
  }
280
337
  const ok = await field.el.fill(PROBE_TEXT, { timeout: 2_000 }).then(() => true).catch(() => false);
281
338
  if (!ok) return tried.length ? { tried, added: false, listApp } : null;
339
+ await fillTheRest(page, field.el);
282
340
  await field.el.press('Enter', { timeout: 2_000 }).catch(() => {});
283
341
  await settle();
284
342
  tried.push(`typed "${PROBE_TEXT}" into ${field.hint ? `"${field.hint.slice(0, 30)}"` : 'the field'} and pressed Enter`);
@@ -99,7 +99,11 @@ export const tools = [
99
99
  'In "next-shadcn" every path in "files" goes under src/: a page is a route only at ' +
100
100
  '<app>/src/app/page.tsx or <app>/src/app/<segment>/page.tsx, an API handler only at ' +
101
101
  '<app>/src/app/api/<name>/route.ts, and components at <app>/src/components/<feature>/. ' +
102
- 'A page.tsx written anywhere else is an ordinary file the router never serves.',
102
+ 'A page.tsx written anywhere else is an ordinary file the router never serves. ' +
103
+ 'Scope: build what was asked, complete and well made - every feature someone using that kind of ' +
104
+ 'app expects on first use (a tasks app adds, edits, ticks off, deletes, filters and remembers across ' +
105
+ 'a reload) - and nothing from a different app (no timer, calendar or analytics bolted onto a tasks ' +
106
+ 'app). Features nobody named cost minutes and are where builds break.',
103
107
  parameters: {
104
108
  type: 'object',
105
109
  properties: {
@@ -57,6 +57,13 @@ const LOOKS_LIKE_INSTALL =
57
57
  /** Opening a file or URL in the desktop browser: start, open, xdg-open, explorer. */
58
58
  const OPENS_BROWSER = /^\s*(?:cd\s+[^&;|]+&&\s*)?(?:start(?:\s+"")?|open|xdg-open|explorer(?:\.exe)?|cmd(?:\.exe)?\s+\/c\s+start)\s+\S+\.html?\b|^\s*(?:start|open|xdg-open)\s+https?:\/\//i;
59
59
 
60
+ /**
61
+ * Installing a browser to test the app. ucode already drives a real one after
62
+ * every change; a live quiz build spent two minutes on `npx @puppeteer/browsers
63
+ * install chrome` before it timed out.
64
+ */
65
+ const INSTALLS_BROWSER = /@puppeteer\/browsers\s+install|\bplaywright(?:@\S+)?\s+install\b/i;
66
+
60
67
  /**
61
68
  * Any kill by program name rather than by process number.
62
69
  *
@@ -672,6 +679,14 @@ export async function runCommand({ command, cwd, timeout_ms, background }, { onO
672
679
  );
673
680
  }
674
681
 
682
+ if (INSTALLS_BROWSER.test(command)) {
683
+ return result(
684
+ 'Not run: ucode already opens the app in a real browser after every change and reports what it finds. ' +
685
+ 'Fix what that report names; there is no browser to install.',
686
+ 'not needed — ucode has a browser',
687
+ );
688
+ }
689
+
675
690
  if (KILLS_BY_NAME.test(command)) {
676
691
  const attempted = `running \`${command.trim().slice(0, 80)}\``;
677
692
  try {