ucode-agent 1.4.0 → 1.6.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 (76) hide show
  1. package/README.md +64 -4
  2. package/package.json +5 -2
  3. package/skills/build-app/SKILL.md +19 -1
  4. package/skills/ui-ux/SKILL.md +2 -2
  5. package/src/core/doctor.js +122 -0
  6. package/src/core/loop.js +303 -12
  7. package/src/core/provider.js +93 -10
  8. package/src/core/stuck.js +269 -0
  9. package/src/tools/browser.js +121 -59
  10. package/src/tools/deploy.js +283 -0
  11. package/src/tools/files.js +91 -8
  12. package/src/tools/index.js +65 -8
  13. package/src/tools/scaffold.js +190 -0
  14. package/src/tools/shell.js +89 -1
  15. package/src/ui/activity.js +203 -0
  16. package/src/ui/plain.js +22 -3
  17. package/src/ui/screen.js +65 -19
  18. package/templates/next-shadcn/AGENTS.md +9 -0
  19. package/templates/next-shadcn/README.md +9 -0
  20. package/templates/next-shadcn/TEMPLATE.md +83 -0
  21. package/templates/next-shadcn/_gitignore +41 -0
  22. package/templates/next-shadcn/_package-lock.json +11465 -0
  23. package/templates/next-shadcn/components.json +25 -0
  24. package/templates/next-shadcn/eslint.config.mjs +18 -0
  25. package/templates/next-shadcn/next-env.d.ts +5 -0
  26. package/templates/next-shadcn/next.config.ts +8 -0
  27. package/templates/next-shadcn/package.json +39 -0
  28. package/templates/next-shadcn/postcss.config.mjs +7 -0
  29. package/templates/next-shadcn/presets/citrus.json +77 -0
  30. package/templates/next-shadcn/presets/graphite.json +77 -0
  31. package/templates/next-shadcn/presets/grove.json +77 -0
  32. package/templates/next-shadcn/presets/ocean.json +78 -0
  33. package/templates/next-shadcn/presets/sunset.json +77 -0
  34. package/templates/next-shadcn/presets/violet.json +77 -0
  35. package/templates/next-shadcn/src/app/favicon.ico +0 -0
  36. package/templates/next-shadcn/src/app/globals.css +155 -0
  37. package/templates/next-shadcn/src/app/layout.tsx +42 -0
  38. package/templates/next-shadcn/src/app/page.tsx +14 -0
  39. package/templates/next-shadcn/src/components/theme-provider.tsx +7 -0
  40. package/templates/next-shadcn/src/components/theme-toggle.tsx +27 -0
  41. package/templates/next-shadcn/src/components/ui/accordion.tsx +80 -0
  42. package/templates/next-shadcn/src/components/ui/alert-dialog.tsx +199 -0
  43. package/templates/next-shadcn/src/components/ui/avatar.tsx +111 -0
  44. package/templates/next-shadcn/src/components/ui/badge.tsx +48 -0
  45. package/templates/next-shadcn/src/components/ui/button.tsx +66 -0
  46. package/templates/next-shadcn/src/components/ui/calendar.tsx +222 -0
  47. package/templates/next-shadcn/src/components/ui/card.tsx +102 -0
  48. package/templates/next-shadcn/src/components/ui/checkbox.tsx +32 -0
  49. package/templates/next-shadcn/src/components/ui/collapsible.tsx +33 -0
  50. package/templates/next-shadcn/src/components/ui/command.tsx +195 -0
  51. package/templates/next-shadcn/src/components/ui/dialog.tsx +168 -0
  52. package/templates/next-shadcn/src/components/ui/dropdown-menu.tsx +268 -0
  53. package/templates/next-shadcn/src/components/ui/hover-card.tsx +43 -0
  54. package/templates/next-shadcn/src/components/ui/input-group.tsx +156 -0
  55. package/templates/next-shadcn/src/components/ui/input.tsx +18 -0
  56. package/templates/next-shadcn/src/components/ui/label.tsx +23 -0
  57. package/templates/next-shadcn/src/components/ui/popover.tsx +88 -0
  58. package/templates/next-shadcn/src/components/ui/progress.tsx +30 -0
  59. package/templates/next-shadcn/src/components/ui/radio-group.tsx +43 -0
  60. package/templates/next-shadcn/src/components/ui/scroll-area.tsx +54 -0
  61. package/templates/next-shadcn/src/components/ui/select.tsx +191 -0
  62. package/templates/next-shadcn/src/components/ui/separator.tsx +27 -0
  63. package/templates/next-shadcn/src/components/ui/sheet.tsx +147 -0
  64. package/templates/next-shadcn/src/components/ui/skeleton.tsx +13 -0
  65. package/templates/next-shadcn/src/components/ui/slider.tsx +58 -0
  66. package/templates/next-shadcn/src/components/ui/sonner.tsx +49 -0
  67. package/templates/next-shadcn/src/components/ui/switch.tsx +32 -0
  68. package/templates/next-shadcn/src/components/ui/table.tsx +115 -0
  69. package/templates/next-shadcn/src/components/ui/tabs.tsx +89 -0
  70. package/templates/next-shadcn/src/components/ui/textarea.tsx +17 -0
  71. package/templates/next-shadcn/src/components/ui/toggle-group.tsx +89 -0
  72. package/templates/next-shadcn/src/components/ui/toggle.tsx +46 -0
  73. package/templates/next-shadcn/src/components/ui/tooltip.tsx +56 -0
  74. package/templates/next-shadcn/src/lib/utils.ts +6 -0
  75. package/templates/next-shadcn/tsconfig.json +34 -0
  76. package/ucode.js +8 -1
package/src/ui/screen.js CHANGED
@@ -41,6 +41,7 @@ import {
41
41
  boxTop, boxBottom, boxRow, visLen, padVis, clip, wrapAnsi,
42
42
  shortenPath, asLabel, ensureColour, planLine, bare,
43
43
  } from './theme.js';
44
+ import { FRAME_MS, fitActivity, shimmer, spinnerGlyph, formatDuration, doneLine, stepPaint } from './activity.js';
44
45
  import { renderer, render, polish } from './markdown.js';
45
46
  import { VERSION } from '../core/version.js';
46
47
 
@@ -58,6 +59,7 @@ export function isLabel(text) {
58
59
  export const COMMANDS = [
59
60
  '/help', '/model', '/models', '/session', '/sessions', '/resume',
60
61
  '/new', '/remember', '/skills', '/clear', '/search', '/copy', '/exit',
62
+ '/stats', '/doctor', '/deploy',
61
63
  ];
62
64
 
63
65
  // ANSI ----------------------------------------------------------------------
@@ -135,6 +137,8 @@ export class Screen {
135
137
  this.onInterrupt = null;
136
138
  this.onModeChange = null;
137
139
  this.spinTimer = null;
140
+ this.activity = null; // the turn in flight: when it began, how many steps
141
+ this.tick = 0; // animation frames painted, for the spinner
138
142
  this.pendingPrompt = null;
139
143
 
140
144
  this.cols = output.columns || 80;
@@ -164,7 +168,9 @@ export class Screen {
164
168
  }
165
169
 
166
170
  stop() {
171
+ this.activity = null;
167
172
  this.stopSpinner();
173
+ this.stopTimer();
168
174
  this.output.off?.('resize', this.onResize);
169
175
  this.input.setRawMode?.(false);
170
176
  this.input.pause();
@@ -633,14 +639,23 @@ export class Screen {
633
639
  let middle = '';
634
640
  if (this.flashText) {
635
641
  middle = dim(clip(this.flashText, between - 2));
636
- } else if (this.status.busy) {
637
- const frame = blue(SPINNER[this.status.frame]);
638
- const secs = Math.round((Date.now() - (this.status.since || Date.now())) / 1000);
639
- const elapsed = secs >= 2 ? dim(` ${secs}s`) : '';
640
- const room = between - 20;
641
- middle = room > 8
642
- ? `${frame} ${dim(clip(this.status.text, room))}${elapsed} ${dim('esc to stop')}`
643
- : `${frame}${elapsed}`;
642
+ } else if (this.status.busy || this.activity) {
643
+ // The whole turn, not just the current tool: the timer and step count
644
+ // keep going through the gaps between calls, so a long build never
645
+ // looks like it has stopped.
646
+ const now = Date.now();
647
+ const a = this.activity;
648
+ const since = a?.start ?? this.status.since ?? now;
649
+ const meta = [];
650
+ if (a?.steps) meta.push({ text: `step ${a.steps}`, paint: stepPaint(now - a.movedAt < 900) });
651
+ if (now - since >= 1000) meta.push({ text: formatDuration(now - since), keep: true });
652
+ middle = fitActivity({
653
+ glyph: spinnerGlyph(this.tick, now),
654
+ label: this.status.busy ? this.status.text : 'working',
655
+ meta,
656
+ hint: 'esc to stop',
657
+ paint: (s) => shimmer(s, now),
658
+ }, between - 3);
644
659
  }
645
660
 
646
661
  // The percentage is pinned to the right border whatever is in the middle,
@@ -700,13 +715,7 @@ export class Screen {
700
715
  // `since` is what makes a long think legible: the label may not change for
701
716
  // a minute, so the seconds beside it are the proof it is still alive.
702
717
  this.status = { busy: true, text: asLabel(text), frame: 0, since: Date.now() };
703
- if (!this.spinTimer) {
704
- this.spinTimer = setInterval(() => {
705
- this.status.frame = (this.status.frame + 1) % SPINNER.length;
706
- this.paintStatus();
707
- }, 80);
708
- this.spinTimer.unref?.();
709
- }
718
+ this.startTimer();
710
719
  this.paintStatus();
711
720
  }
712
721
 
@@ -717,16 +726,53 @@ export class Screen {
717
726
  }
718
727
 
719
728
  stopSpinner() {
720
- if (this.spinTimer) {
721
- clearInterval(this.spinTimer);
722
- this.spinTimer = null;
723
- }
729
+ if (!this.activity) this.stopTimer();
724
730
  if (this.status.busy) {
725
731
  this.status = { busy: false, text: '', frame: 0, since: 0 };
726
732
  this.paintStatus();
727
733
  }
728
734
  }
729
735
 
736
+ // -- the turn in flight ----------------------------------------------------
737
+
738
+ /** A turn begins: the timer and step count run until turnEnd(). */
739
+ turnStart() {
740
+ this.activity = { start: Date.now(), steps: 0, movedAt: 0 };
741
+ this.startTimer();
742
+ }
743
+
744
+ /** One more model step in this turn. */
745
+ step() {
746
+ if (!this.activity) return;
747
+ this.activity.steps++;
748
+ this.activity.movedAt = Date.now();
749
+ }
750
+
751
+ /** The turn is over: leave "✓ Done in 6m 12s · 25 steps" under the answer. */
752
+ turnEnd({ ok = true } = {}) {
753
+ const a = this.activity;
754
+ this.activity = null;
755
+ if (!this.status.busy) this.stopTimer();
756
+ if (a && ok && Date.now() - a.start >= 2000) this.push(` ${doneLine(Date.now() - a.start, a.steps)}`);
757
+ this.paintStatus();
758
+ }
759
+
760
+ /** The animation clock: only the status row repaints, about twelve times a second. */
761
+ startTimer() {
762
+ if (this.spinTimer) return;
763
+ this.spinTimer = setInterval(() => {
764
+ this.tick++;
765
+ this.paintStatus();
766
+ }, FRAME_MS);
767
+ this.spinTimer.unref?.();
768
+ }
769
+
770
+ stopTimer() {
771
+ if (!this.spinTimer) return;
772
+ clearInterval(this.spinTimer);
773
+ this.spinTimer = null;
774
+ }
775
+
730
776
  // -- input ---------------------------------------------------------------
731
777
 
732
778
  nextLine() {
@@ -0,0 +1,9 @@
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+
3
+ # This is NOT the Next.js you know
4
+
5
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
6
+
7
+ This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
8
+
9
+ <!-- END:nextjs-agent-rules -->
@@ -0,0 +1,9 @@
1
+ # __APP_NAME__
2
+
3
+ Built with ucode on Next.js and shadcn/ui.
4
+
5
+ ```bash
6
+ npm run dev
7
+ ```
8
+
9
+ Then open http://localhost:3000
@@ -0,0 +1,83 @@
1
+ # What this project already has
2
+
3
+ It was created by ucode's `create_app` from a starter that is known to build.
4
+ Do not re-run create-next-app or `shadcn init` — everything below is in place.
5
+
6
+ ## Stack
7
+
8
+ - Next.js 16 (App Router), React 19, TypeScript, Tailwind CSS 4
9
+ - shadcn/ui on Radix (the standard shadcn you know — `asChild` works), lucide-react icons
10
+ - next-themes (light / dark / system), sonner toasts, date-fns, react-day-picker
11
+
12
+ Next 16 changed some APIs. `AGENTS.md` explains, and `node_modules/next/dist/docs/`
13
+ has the guides — check them before using an API you are unsure of.
14
+
15
+ ## Already wired
16
+
17
+ - `src/app/layout.tsx` — font (Geist, registered as `--font-sans`), `ThemeProvider`,
18
+ `TooltipProvider`, and `<Toaster />`.
19
+ - `src/components/theme-toggle.tsx` — a light/dark button, ready to place.
20
+ - `src/app/globals.css` — the design tokens. The palette is a starting point:
21
+ re-tint `--primary`, `--accent` and the neutrals for this app's direction.
22
+ `--success` and `--warning` exist alongside `--destructive`
23
+ (`bg-success`, `text-warning`, ...).
24
+ - `src/app/page.tsx` — a placeholder. Replace it.
25
+
26
+ `layout.tsx`, `theme-provider.tsx`, `theme-toggle.tsx`, `src/lib/utils.ts` and
27
+ everything in `src/components/ui/` are finished and build. Use them; do not
28
+ rewrite them — a rewrite from memory brings back APIs that no longer exist.
29
+
30
+ ## Components in `src/components/ui/`
31
+
32
+ accordion, alert-dialog, avatar, badge, button, calendar, card, checkbox, collapsible, command, dialog, dropdown-menu, hover-card, input-group, input, label, popover, progress, radio-group, scroll-area, select, separator, sheet, skeleton, slider, sonner, switch, table, tabs, textarea, toggle-group, toggle, tooltip
33
+
34
+ Anything else: `npx shadcn@latest add <name> -y` with cwd set to this folder.
35
+
36
+ ## APIs that are easy to get wrong
37
+
38
+ ```tsx
39
+ // Toasts — sonner. A message first, options second. There is no toast({ title }).
40
+ import { toast } from "sonner";
41
+ toast.success("Saved");
42
+ toast("Task deleted", { description: "Buy milk", action: { label: "Undo", onClick: () => restore() } });
43
+
44
+ // Slider — value is an ARRAY, even for one thumb.
45
+ <Slider value={[tip]} onValueChange={([v]) => setTip(v)} min={0} max={30} step={1} />
46
+
47
+ // Select — value is a string.
48
+ <Select value={list} onValueChange={(v: string) => setList(v)}>
49
+ <SelectTrigger><SelectValue placeholder="Pick a list" /></SelectTrigger>
50
+ <SelectContent><SelectItem value="inbox">Inbox</SelectItem></SelectContent>
51
+ </Select>
52
+
53
+ // Date picker — Calendar inside a Popover.
54
+ <Popover>
55
+ <PopoverTrigger asChild><Button variant="outline">{date ? format(date, "PPP") : "Pick a date"}</Button></PopoverTrigger>
56
+ <PopoverContent className="w-auto p-0"><Calendar mode="single" selected={date} onSelect={setDate} /></PopoverContent>
57
+ </Popover>
58
+
59
+ // Toggle group — value is a string for type="single".
60
+ <ToggleGroup type="single" value={preset} onValueChange={(v) => v && setPreset(v)}>
61
+ <ToggleGroupItem value="15">15%</ToggleGroupItem>
62
+ </ToggleGroup>
63
+
64
+ // Anything using useState, events, localStorage or browser APIs needs "use client"
65
+ // as the first line of its file.
66
+
67
+ // localStorage — read it in useEffect, never in render or a useState initializer:
68
+ // the page is also rendered on the server, where localStorage does not exist.
69
+ const [bill, setBill] = useState("");
70
+ useEffect(() => { setBill(localStorage.getItem("bill") ?? ""); }, []);
71
+ useEffect(() => { localStorage.setItem("bill", bill); }, [bill]);
72
+
73
+ // next-themes — there is no "next-themes/dist/types". Import from "next-themes".
74
+ ```
75
+
76
+ ## Conventions
77
+
78
+ - Components are named exports — `export function BillInput()` — imported with
79
+ braces: `import { BillInput } from "@/components/calculator/bill-input"`.
80
+ Only `page.tsx` and `layout.tsx` use `export default`.
81
+ - One component per file, grouped by feature: `src/components/<feature>/`.
82
+ - Shared types in `src/lib/types.ts`, helpers in `src/lib/`.
83
+ - `cn()` from `@/lib/utils` to merge class names.
@@ -0,0 +1,41 @@
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts