ucode-agent 1.5.0 → 1.7.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/README.md +399 -327
- package/package.json +6 -1
- package/skills/ui-ux/SKILL.md +2 -2
- package/src/core/doctor.js +122 -0
- package/src/core/livelog.js +113 -0
- package/src/core/loop.js +2105 -1659
- package/src/core/provider.js +93 -10
- package/src/core/stuck.js +269 -0
- package/src/core/tests.js +86 -0
- package/src/tools/blocks.js +117 -0
- package/src/tools/browser.js +121 -59
- package/src/tools/cache.js +105 -0
- package/src/tools/deploy.js +283 -0
- package/src/tools/files.js +91 -8
- package/src/tools/index.js +634 -495
- package/src/tools/rename.js +157 -0
- package/src/tools/scaffold.js +85 -6
- package/src/tools/shell.js +799 -701
- package/src/tools/symbols.js +218 -0
- package/src/tools/types.js +179 -0
- package/src/ui/activity.js +203 -0
- package/src/ui/plain.js +22 -3
- package/src/ui/screen.js +65 -19
- package/src/ui/theme.js +5 -1
- package/templates/blocks/app-shell.tsx +81 -0
- package/templates/blocks/data-table.tsx +117 -0
- package/templates/blocks/empty-state.tsx +41 -0
- package/templates/blocks/page-header.tsx +27 -0
- package/templates/blocks/stat-cards.tsx +46 -0
- package/templates/next-shadcn/TEMPLATE.md +53 -9
- package/templates/next-shadcn/_package-lock.json +1335 -148
- package/templates/next-shadcn/components.json +1 -1
- package/templates/next-shadcn/next.config.ts +2 -1
- package/templates/next-shadcn/package.json +4 -2
- package/templates/next-shadcn/presets/citrus.json +77 -0
- package/templates/next-shadcn/presets/graphite.json +77 -0
- package/templates/next-shadcn/presets/grove.json +77 -0
- package/templates/next-shadcn/presets/ocean.json +78 -0
- package/templates/next-shadcn/presets/sunset.json +77 -0
- package/templates/next-shadcn/presets/violet.json +77 -0
- package/templates/next-shadcn/src/components/ui/accordion.tsx +80 -0
- package/templates/next-shadcn/src/components/ui/alert-dialog.tsx +34 -22
- package/templates/next-shadcn/src/components/ui/avatar.tsx +7 -4
- package/templates/next-shadcn/src/components/ui/badge.tsx +15 -18
- package/templates/next-shadcn/src/components/ui/button.tsx +12 -3
- package/templates/next-shadcn/src/components/ui/calendar.tsx +1 -0
- package/templates/next-shadcn/src/components/ui/checkbox.tsx +6 -2
- package/templates/next-shadcn/src/components/ui/collapsible.tsx +33 -0
- package/templates/next-shadcn/src/components/ui/command.tsx +1 -2
- package/templates/next-shadcn/src/components/ui/dialog.tsx +34 -26
- package/templates/next-shadcn/src/components/ui/dropdown-menu.tsx +115 -114
- package/templates/next-shadcn/src/components/ui/hover-card.tsx +43 -0
- package/templates/next-shadcn/src/components/ui/input-group.tsx +2 -4
- package/templates/next-shadcn/src/components/ui/input.tsx +1 -2
- package/templates/next-shadcn/src/components/ui/label.tsx +6 -2
- package/templates/next-shadcn/src/components/ui/popover.tsx +27 -28
- package/templates/next-shadcn/src/components/ui/progress.tsx +11 -63
- package/templates/next-shadcn/src/components/ui/radio-group.tsx +43 -0
- package/templates/next-shadcn/src/components/ui/scroll-area.tsx +6 -6
- package/templates/next-shadcn/src/components/ui/select.tsx +55 -64
- package/templates/next-shadcn/src/components/ui/separator.tsx +6 -3
- package/templates/next-shadcn/src/components/ui/sheet.tsx +35 -26
- package/templates/next-shadcn/src/components/ui/slider.tsx +58 -0
- package/templates/next-shadcn/src/components/ui/switch.tsx +3 -2
- package/templates/next-shadcn/src/components/ui/table.tsx +115 -0
- package/templates/next-shadcn/src/components/ui/tabs.tsx +16 -8
- package/templates/next-shadcn/src/components/ui/toggle-group.tsx +89 -0
- package/templates/next-shadcn/src/components/ui/toggle.tsx +46 -0
- package/templates/next-shadcn/src/components/ui/tooltip.tsx +24 -33
- package/templates/next-shadcn/src/lib/utils.ts +6 -1
- package/ucode.js +8 -1
package/src/tools/files.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
changedRegion, renderDiff, renderNewFile, READ_LINES, MAX_FILE_OUTPUT,
|
|
16
16
|
} from './shared.js';
|
|
17
17
|
import { packageJsonWritten } from './shell.js';
|
|
18
|
+
import { parse as parseSource } from '@babel/parser';
|
|
18
19
|
|
|
19
20
|
export async function readFile({ path: p, offset = 1, limit = READ_LINES }) {
|
|
20
21
|
const target = resolveIn(p, 'read_file');
|
|
@@ -94,6 +95,71 @@ function written(target, content) {
|
|
|
94
95
|
if (path.basename(target.abs) === 'package.json') packageJsonWritten(target.abs, content);
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
const PARSEABLE = /\.(?:[cm]?[jt]sx?)$/i;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Does this source parse? Checked the moment a file is written.
|
|
102
|
+
*
|
|
103
|
+
* Measured on a real build: a JSX typo sat unnoticed until `npm run build`,
|
|
104
|
+
* which takes up to a minute, failed on it — then the fix, then another full
|
|
105
|
+
* build. Parsing the file takes milliseconds, so the error comes back in the
|
|
106
|
+
* same step that wrote it, with the line, while the model still has the file
|
|
107
|
+
* in front of it. Syntax only: types are checked at the end of the turn.
|
|
108
|
+
*/
|
|
109
|
+
export function syntaxProblem(file, text) {
|
|
110
|
+
if (!PARSEABLE.test(file)) return null;
|
|
111
|
+
const ext = path.extname(file).toLowerCase();
|
|
112
|
+
const plugins = ext === '.tsx'
|
|
113
|
+
? ['typescript', 'jsx']
|
|
114
|
+
: /^\.[cm]?ts$/.test(ext) ? ['typescript'] : ['jsx'];
|
|
115
|
+
const where = (loc) => (loc ? `line ${loc.line}, column ${loc.column + 1}` : 'somewhere in the file');
|
|
116
|
+
const clean = (m) => String(m).replace(/\s*\(\d+:\d+\)\s*$/, '');
|
|
117
|
+
try {
|
|
118
|
+
const ast = parseSource(text, {
|
|
119
|
+
sourceType: 'unambiguous',
|
|
120
|
+
plugins: [...plugins, 'decorators-legacy'],
|
|
121
|
+
errorRecovery: true,
|
|
122
|
+
allowReturnOutsideFunction: true,
|
|
123
|
+
allowAwaitOutsideFunction: true,
|
|
124
|
+
});
|
|
125
|
+
const first = ast.errors?.[0];
|
|
126
|
+
return first ? `${where(first.loc)}: ${clean(first.message)}` : null;
|
|
127
|
+
} catch (err) {
|
|
128
|
+
return `${where(err.loc)}: ${clean(err.message)}`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The warning appended to a result when a file does not parse. */
|
|
133
|
+
const brokenNote = (show, problem) =>
|
|
134
|
+
`\n\n⚠ ${show} does not parse — ${problem}. Fix it now: the build will fail on it.`;
|
|
135
|
+
|
|
136
|
+
/** A file this short comes back whole after an edit; longer ones show the part around the change. */
|
|
137
|
+
const SHOW_WHOLE = 250;
|
|
138
|
+
const AROUND = 15;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The file as it stands after an edit, with the same line-number gutter as
|
|
142
|
+
* read_file.
|
|
143
|
+
*
|
|
144
|
+
* Measured on a real build: the model read the same component thirteen times
|
|
145
|
+
* in one turn, because an edit's result showed only the lines it replaced and
|
|
146
|
+
* the next edit needed the file as it now was. Sending the current text back
|
|
147
|
+
* with the edit costs the same tokens the re-read would have, and saves the
|
|
148
|
+
* round trip every time.
|
|
149
|
+
*/
|
|
150
|
+
function nowReads(show, text, at, span) {
|
|
151
|
+
const lines = toLines(text);
|
|
152
|
+
const whole = lines.length <= SHOW_WHOLE;
|
|
153
|
+
const from = whole ? 1 : Math.max(1, at - AROUND);
|
|
154
|
+
const to = whole ? lines.length : Math.min(lines.length, at + span + AROUND);
|
|
155
|
+
const width = String(to).length;
|
|
156
|
+
const body = lines.slice(from - 1, to).map((l, i) => `${String(from + i).padStart(width)} | ${l}`).join('\n');
|
|
157
|
+
const heading = whole
|
|
158
|
+
? `${show} now reads (all ${lines.length} lines`
|
|
159
|
+
: `${show} now reads, lines ${from}-${to} of ${lines.length}`;
|
|
160
|
+
return `\n\n${heading} — this is the current text, so there is no need to read it again):\n${body}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
97
163
|
/** At most this many files in one read_files call. */
|
|
98
164
|
const MAX_BATCH = 20;
|
|
99
165
|
|
|
@@ -216,6 +282,7 @@ async function put(target, content, { diffMax = 16 } = {}) {
|
|
|
216
282
|
existed,
|
|
217
283
|
lineCount,
|
|
218
284
|
diff,
|
|
285
|
+
problem: syntaxProblem(target.abs, content),
|
|
219
286
|
line: `${existed ? 'Overwrote' : 'Created'} ${target.show} ` +
|
|
220
287
|
`(${lineCount} lines, ${bytes(Buffer.byteLength(content))})`,
|
|
221
288
|
};
|
|
@@ -234,7 +301,10 @@ export async function writeFile({ path: p, content }) {
|
|
|
234
301
|
await guard(target, `write ${target.abs}`);
|
|
235
302
|
|
|
236
303
|
const written = await put(target, content);
|
|
237
|
-
const out = result(
|
|
304
|
+
const out = result(
|
|
305
|
+
`${written.line}.${written.problem ? brokenNote(target.show, written.problem) : ''}`,
|
|
306
|
+
`${written.existed ? 'overwrote' : 'created'} · ${written.lineCount} lines${written.problem ? ' · does not parse' : ''}`
|
|
307
|
+
);
|
|
238
308
|
out.diff = written.diff;
|
|
239
309
|
return out;
|
|
240
310
|
}
|
|
@@ -258,6 +328,7 @@ export async function batchWrite({ files }) {
|
|
|
258
328
|
const lines = [];
|
|
259
329
|
const diff = [];
|
|
260
330
|
let created = 0;
|
|
331
|
+
let broken = 0;
|
|
261
332
|
|
|
262
333
|
for (const [index, file] of files.entries()) {
|
|
263
334
|
const { path: p, content } = file ?? {};
|
|
@@ -277,13 +348,14 @@ export async function batchWrite({ files }) {
|
|
|
277
348
|
// would bury the reply under three hundred lines of gutter.
|
|
278
349
|
const written = await put(target, content, { diffMax: 6 });
|
|
279
350
|
if (!written.existed) created++;
|
|
280
|
-
lines.push(written.line);
|
|
351
|
+
lines.push(written.line + (written.problem ? brokenNote(target.show, written.problem) : ''));
|
|
352
|
+
if (written.problem) broken++;
|
|
281
353
|
diff.push(`~${target.show}`, ...written.diff);
|
|
282
354
|
}
|
|
283
355
|
|
|
284
356
|
const out = result(
|
|
285
357
|
lines.join('\n'),
|
|
286
|
-
`${files.length} file${files.length === 1 ? '' : 's'} · ${created} new`
|
|
358
|
+
`${files.length} file${files.length === 1 ? '' : 's'} · ${created} new${broken ? ` · ${broken} do not parse` : ''}`
|
|
287
359
|
);
|
|
288
360
|
out.diff = diff;
|
|
289
361
|
return out;
|
|
@@ -462,9 +534,13 @@ export async function editFile({ path: p, old_string, new_string }) {
|
|
|
462
534
|
const change = delta === 0 ? 'same line count' : `${delta > 0 ? '+' : ''}${delta} lines`;
|
|
463
535
|
const how = loose ? ', matched ignoring whitespace and re-indented to fit' : '';
|
|
464
536
|
|
|
537
|
+
const span = toLines(new_string).length;
|
|
465
538
|
const out = result(
|
|
466
|
-
`Replaced one occurrence in ${target.show} at line ${at} (${change}${how})
|
|
467
|
-
|
|
539
|
+
`Replaced one occurrence in ${target.show} at line ${at} (${change}${how}).` +
|
|
540
|
+
(syntaxProblem(target.abs, text) ? brokenNote(target.show, syntaxProblem(target.abs, text)) : '') +
|
|
541
|
+
nowReads(target.show, text, at, span),
|
|
542
|
+
`1 change at line ${at} · ${change}${loose ? ' · whitespace-tolerant' : ''}${syntaxProblem(target.abs, text) ? ' · does not parse' : ''}`,
|
|
543
|
+
MAX_FILE_OUTPUT
|
|
468
544
|
);
|
|
469
545
|
// The replacement is diffed on its own and offset to where it landed, so
|
|
470
546
|
// the gutter shows the file's line numbers rather than 1, 2, 3.
|
|
@@ -531,8 +607,11 @@ export async function multiEdit({ path: p, edits }) {
|
|
|
531
607
|
const change = delta === 0 ? 'same line count' : `${delta > 0 ? '+' : ''}${delta} lines`;
|
|
532
608
|
|
|
533
609
|
const out = result(
|
|
534
|
-
`Applied ${edits.length} edits to ${target.show} (${change})
|
|
535
|
-
|
|
610
|
+
`Applied ${edits.length} edits to ${target.show} (${change}).` +
|
|
611
|
+
(syntaxProblem(target.abs, text) ? brokenNote(target.show, syntaxProblem(target.abs, text)) : '') +
|
|
612
|
+
nowReads(target.show, text, 1, toLines(text).length),
|
|
613
|
+
`${edits.length} edits · ${change}`,
|
|
614
|
+
MAX_FILE_OUTPUT
|
|
536
615
|
);
|
|
537
616
|
out.diff = diff;
|
|
538
617
|
return out;
|
|
@@ -615,7 +694,11 @@ export async function editFiles({ files }) {
|
|
|
615
694
|
|
|
616
695
|
const edits = planned.reduce((n, p) => n + p.count, 0);
|
|
617
696
|
const out = result(
|
|
618
|
-
planned.map((p) =>
|
|
697
|
+
planned.map((p) => {
|
|
698
|
+
const problem = syntaxProblem(p.target.abs, p.text);
|
|
699
|
+
return `Edited ${p.target.show} (${p.count} change${p.count === 1 ? '' : 's'})` +
|
|
700
|
+
(problem ? brokenNote(p.target.show, problem) : '');
|
|
701
|
+
}).join('\n'),
|
|
619
702
|
`${planned.length} files · ${edits} edits`
|
|
620
703
|
);
|
|
621
704
|
out.diff = planned.flatMap((p) => p.diff);
|