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.
Files changed (71) hide show
  1. package/README.md +399 -327
  2. package/package.json +6 -1
  3. package/skills/ui-ux/SKILL.md +2 -2
  4. package/src/core/doctor.js +122 -0
  5. package/src/core/livelog.js +113 -0
  6. package/src/core/loop.js +2105 -1659
  7. package/src/core/provider.js +93 -10
  8. package/src/core/stuck.js +269 -0
  9. package/src/core/tests.js +86 -0
  10. package/src/tools/blocks.js +117 -0
  11. package/src/tools/browser.js +121 -59
  12. package/src/tools/cache.js +105 -0
  13. package/src/tools/deploy.js +283 -0
  14. package/src/tools/files.js +91 -8
  15. package/src/tools/index.js +634 -495
  16. package/src/tools/rename.js +157 -0
  17. package/src/tools/scaffold.js +85 -6
  18. package/src/tools/shell.js +799 -701
  19. package/src/tools/symbols.js +218 -0
  20. package/src/tools/types.js +179 -0
  21. package/src/ui/activity.js +203 -0
  22. package/src/ui/plain.js +22 -3
  23. package/src/ui/screen.js +65 -19
  24. package/src/ui/theme.js +5 -1
  25. package/templates/blocks/app-shell.tsx +81 -0
  26. package/templates/blocks/data-table.tsx +117 -0
  27. package/templates/blocks/empty-state.tsx +41 -0
  28. package/templates/blocks/page-header.tsx +27 -0
  29. package/templates/blocks/stat-cards.tsx +46 -0
  30. package/templates/next-shadcn/TEMPLATE.md +53 -9
  31. package/templates/next-shadcn/_package-lock.json +1335 -148
  32. package/templates/next-shadcn/components.json +1 -1
  33. package/templates/next-shadcn/next.config.ts +2 -1
  34. package/templates/next-shadcn/package.json +4 -2
  35. package/templates/next-shadcn/presets/citrus.json +77 -0
  36. package/templates/next-shadcn/presets/graphite.json +77 -0
  37. package/templates/next-shadcn/presets/grove.json +77 -0
  38. package/templates/next-shadcn/presets/ocean.json +78 -0
  39. package/templates/next-shadcn/presets/sunset.json +77 -0
  40. package/templates/next-shadcn/presets/violet.json +77 -0
  41. package/templates/next-shadcn/src/components/ui/accordion.tsx +80 -0
  42. package/templates/next-shadcn/src/components/ui/alert-dialog.tsx +34 -22
  43. package/templates/next-shadcn/src/components/ui/avatar.tsx +7 -4
  44. package/templates/next-shadcn/src/components/ui/badge.tsx +15 -18
  45. package/templates/next-shadcn/src/components/ui/button.tsx +12 -3
  46. package/templates/next-shadcn/src/components/ui/calendar.tsx +1 -0
  47. package/templates/next-shadcn/src/components/ui/checkbox.tsx +6 -2
  48. package/templates/next-shadcn/src/components/ui/collapsible.tsx +33 -0
  49. package/templates/next-shadcn/src/components/ui/command.tsx +1 -2
  50. package/templates/next-shadcn/src/components/ui/dialog.tsx +34 -26
  51. package/templates/next-shadcn/src/components/ui/dropdown-menu.tsx +115 -114
  52. package/templates/next-shadcn/src/components/ui/hover-card.tsx +43 -0
  53. package/templates/next-shadcn/src/components/ui/input-group.tsx +2 -4
  54. package/templates/next-shadcn/src/components/ui/input.tsx +1 -2
  55. package/templates/next-shadcn/src/components/ui/label.tsx +6 -2
  56. package/templates/next-shadcn/src/components/ui/popover.tsx +27 -28
  57. package/templates/next-shadcn/src/components/ui/progress.tsx +11 -63
  58. package/templates/next-shadcn/src/components/ui/radio-group.tsx +43 -0
  59. package/templates/next-shadcn/src/components/ui/scroll-area.tsx +6 -6
  60. package/templates/next-shadcn/src/components/ui/select.tsx +55 -64
  61. package/templates/next-shadcn/src/components/ui/separator.tsx +6 -3
  62. package/templates/next-shadcn/src/components/ui/sheet.tsx +35 -26
  63. package/templates/next-shadcn/src/components/ui/slider.tsx +58 -0
  64. package/templates/next-shadcn/src/components/ui/switch.tsx +3 -2
  65. package/templates/next-shadcn/src/components/ui/table.tsx +115 -0
  66. package/templates/next-shadcn/src/components/ui/tabs.tsx +16 -8
  67. package/templates/next-shadcn/src/components/ui/toggle-group.tsx +89 -0
  68. package/templates/next-shadcn/src/components/ui/toggle.tsx +46 -0
  69. package/templates/next-shadcn/src/components/ui/tooltip.tsx +24 -33
  70. package/templates/next-shadcn/src/lib/utils.ts +6 -1
  71. package/ucode.js +8 -1
@@ -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(`${written.line}.`, `${written.existed ? 'overwrote' : 'created'} · ${written.lineCount} lines`);
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
- `1 change at line ${at} · ${change}${loose ? ' · whitespace-tolerant' : ''}`
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
- `${edits.length} edits · ${change}`
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) => `Edited ${p.target.show} (${p.count} change${p.count === 1 ? '' : 's'})`).join('\n'),
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);