ucode-agent 1.26.1 → 1.27.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.
@@ -0,0 +1,135 @@
1
+ # Interfaces, done properly — the short form
2
+
3
+ The house style of a language model is a centred column, a purple-to-blue
4
+ gradient, three identical cards, Inter at every size, and a lot of empty space.
5
+ Everyone has seen it a thousand times, and it reads as generated on sight.
6
+ This exists to stop you producing it.
7
+
8
+ Order: direction, structure, tokens, components, states, motion, accessibility,
9
+ look at it, report. Most bad interfaces are good CSS applied to an undecided
10
+ design.
11
+
12
+ ## 1. Direction — one line each, before any code
13
+
14
+ - **Job** — what the screen does, in a sentence a user would say.
15
+ - **Mode** — **Operate** (tasks, tools, dashboards: speed and scannability),
16
+ **Persuade** (landing, pricing: earn attention in one screen), **Read** (docs:
17
+ measure and rhythm), **Experience** (portfolio: content leads). A dashboard
18
+ stays Operate however loud the brand is.
19
+ - **Tone** — one word you commit to: clinical, warm, editorial, technical,
20
+ playful, industrial, calm, dense. "Modern and clean" is not a tone.
21
+ - **The one memorable thing** — a colour, a type move, a texture, one
22
+ interaction. Exactly one. It is the difference between a design and a theme.
23
+
24
+ ## 2. Structure — hierarchy before decoration
25
+
26
+ - Decide what the eye lands on first, second, third, and build it with size,
27
+ weight, colour and position before reaching for a box, a border or a card.
28
+ Three levels — primary, secondary, muted — is usually all a screen needs.
29
+ - **The primary thing gets disproportionate size.** If a score or a total is
30
+ the point of the screen, make it three or four times bigger, not 10%.
31
+ - Align to a grid and share edges. Ragged left edges are the single most common
32
+ reason a page feels amateur. Hold a max width (~1120px apps, 68ch prose).
33
+ - Space **inside** a group must be smaller than space **between** groups.
34
+ - One job per element. Put the primary action at the end of the flow it
35
+ completes; keep destructive actions away from safe ones.
36
+
37
+ ## 3. Tokens — set once, never use a raw value again
38
+
39
+ Define in `:root` and use nothing else afterwards: a type scale (xs → hero,
40
+ `clamp()` for the big sizes), one space scale (.25/.5/.75/1/1.5/2/3/4/6/8rem),
41
+ colour, radius, and duration/easing.
42
+
43
+ - **Neutrals carry a hue.** Flat `#808080` grey is what makes a UI look dead.
44
+ OKLCH keeps lightness steps perceptually even.
45
+ - Roles, not names: `--bg`, `--surface`, `--surface-2`, `--line`, `--ink`,
46
+ `--ink-2`, `--ink-3`, `--accent`, `--accent-ink`, `--accent-soft`, plus
47
+ good/warn/bad. One accent family plus semantics — count the hues.
48
+ - **Depth:** borders and background steps first; shadows only for things that
49
+ genuinely float (menus, popovers, modals, toasts).
50
+ - **Dark mode:** swap variables, never invert. Surfaces get *lighter* as they
51
+ rise, text 92–95% lightness rather than pure white, and every accent
52
+ re-checked for contrast — most need to get lighter.
53
+ - **Tailwind/shadcn:** re-theme the slate default in `globals.css`
54
+ (`--background`, `--foreground`, `--primary`, `--muted`, `--accent`,
55
+ `--destructive`, `--border`, `--ring`, `--radius`). Use the components for
56
+ behaviour, style them to the direction, and do not wrap every region in a
57
+ `Card`. No arbitrary values (`text-[17px]`) — that is a raw value in another
58
+ syntax. Icons: lucide, one stroke width, sized to the text beside them.
59
+
60
+ ## 4. Components — every state built in
61
+
62
+ Every interactive element needs default, hover, focus-visible, active and
63
+ disabled, plus selected, loading and error where they apply. A control with
64
+ only a default state is unfinished, not minimal.
65
+
66
+ - **Buttons:** verb labels ("Analyze label", not "Submit"), one primary per
67
+ view, 44px minimum height, loading keeps the width fixed so nothing jumps.
68
+ - **Inputs:** a visible `<label>` always — a placeholder is not a label. Help
69
+ text below, errors below with an icon, `aria-invalid` and `aria-describedby`
70
+ wired. Validate on blur, re-validate on input once an error shows, never
71
+ shout on the first keystroke.
72
+ - **Numbers and metrics:** large, with the scale ("7.4 / 10") and a label
73
+ saying what it measures. A colour band is never the only signal — pair it
74
+ with a word.
75
+ - **Lists of findings:** most severe first, each one thing/value/why in a line
76
+ or two. None? Say so once; do not render an empty section header.
77
+ - **Tables:** numbers right, text left, tabular numerals, sticky header, row
78
+ hover, a real empty state.
79
+
80
+ ## 5. States are most of the work
81
+
82
+ An interface that only handles the happy path is a mockup. Every screen and
83
+ every async action needs:
84
+
85
+ - **Empty / first run** — what this is and the one action that starts it.
86
+ Never a blank rectangle.
87
+ - **Loading** — skeletons shaped like the real content, or a spinner on the
88
+ control that was pressed; say what is happening past a second. Never blank
89
+ the page.
90
+ - **Success** — the result and the next action.
91
+ - **Error** — what failed in plain words, what to do, and the input preserved.
92
+ "You can fix this" and "we failed" need different words.
93
+ - **Partial** — show what worked.
94
+ - **Edge content** — longest realistic name, zero, missing field, a thousand
95
+ rows. Design for them rather than discovering them.
96
+
97
+ Announce async results with `aria-live="polite"`.
98
+
99
+ ## 6. Motion
100
+
101
+ One authored moment, not effects everywhere. Everything else 120–200ms,
102
+ ease-out, on `transform`, `opacity`, `filter` and colour only — never `width`,
103
+ `height`, `top` or `left`. Readable with motion off; honour
104
+ `prefers-reduced-motion` every time.
105
+
106
+ ## 7. Accessibility and performance — non-negotiable
107
+
108
+ - Contrast 4.5:1 body text and placeholders, 3:1 large text, icons and borders.
109
+ - Every control keyboard-reachable in visual order, visible focus ring, Escape
110
+ closes overlays, focus returns to the trigger.
111
+ - `<button>` for actions, `<a href>` for navigation, landmarks, one `<h1>`,
112
+ headings in order. `aria-label` on icon-only buttons, real `alt` text.
113
+ - 44×44px touch targets, viewport meta, 16px minimum input text on mobile.
114
+ - No layout shift: reserve space for images and async content,
115
+ `font-display: swap`. In Next.js, `next/image`, server components by default,
116
+ `"use client"` only where it is interactive.
117
+
118
+ ## 8. Do not
119
+
120
+ Purple-to-blue gradients, gradient text, glassmorphism as decoration, glowing
121
+ blobs. Three identical feature cards; cards inside cards. The big-number-plus-
122
+ three-stats template. Tracked uppercase eyebrows over every section; 01/02/03
123
+ numbering. Emoji as icons. Monospace as a costume. Centred long paragraphs.
124
+ The default shadcn slate theme. Lorem ipsum, "Feature 1", "John Doe",
125
+ placeholder images — write the real copy, it is part of the design. A modal
126
+ for anything that does not need to interrupt.
127
+
128
+ ## 9. Look at it, then report
129
+
130
+ Run it, then call `look_at_app` with the URL: it opens the app at 375px and
131
+ 1440px and reports console errors, failed requests, overflow, broken images and
132
+ unlabeled controls. Fix what it finds in one pass, then check 375px (no
133
+ horizontal scroll, nothing clipped), 1440px (a max width, no unreadable line
134
+ lengths), the longest realistic content, every state, keyboard only, contrast,
135
+ and the hue count.
@@ -1,151 +1,164 @@
1
- /**
2
- * context.js — what the model knows about the project before it asks.
3
- *
4
- * Two things go into the system prompt at the start of every turn:
5
- *
6
- * the project map every file, and the names each code file exports, so the
7
- * model can go straight to the right file instead of
8
- * spending round trips on list_dir and grep to find it
9
- *
10
- * project memory UCODE.md — the stack, the commands, the conventions, the
11
- * preferences — written once, remembered every session
12
- */
13
-
14
- import { promises as fs } from 'node:fs';
15
- import os from 'node:os';
16
- import path from 'node:path';
17
- import { walk } from '../tools/shared.js';
18
-
19
- export const MEMORY_FILE = 'UCODE.md';
20
- export const GLOBAL_MEMORY = path.join(os.homedir(), '.ucode', MEMORY_FILE);
21
-
22
- const MAP_FILES = 400;
23
- const MAP_CHARS = 8_000;
24
- const MEMORY_CHARS = 6_000;
25
- const SYMBOL_BYTES = 120_000;
26
- const SYMBOLS_PER_FILE = 8;
27
-
28
- const CODE = /\.(?:[cm]?[jt]sx?|py|go|rs|vue|svelte)$/i;
29
-
30
- // Files the map never needs to name.
31
- const NOISE = /(^|\/)(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb|.*\.map|.*\.min\.[jc]ss?|\.DS_Store|next-env\.d\.ts)$/i;
32
-
33
- /** Parsed symbols, kept per file until the file's mtime changes. */
34
- const symbolCache = new Map();
35
-
36
- function symbolsIn(file, text) {
37
- const found = [];
38
- const add = (name) => { if (name && !found.includes(name)) found.push(name); };
39
-
40
- if (/\.py$/i.test(file)) {
41
- for (const m of text.matchAll(/^(?:async\s+)?(?:def|class)\s+([A-Za-z_]\w*)/gm)) add(m[1]);
42
- } else if (/\.go$/i.test(file)) {
43
- for (const m of text.matchAll(/^func\s+(?:\([^)]*\)\s*)?([A-Z]\w*)/gm)) add(m[1]);
44
- for (const m of text.matchAll(/^type\s+([A-Z]\w*)/gm)) add(m[1]);
45
- } else if (/\.rs$/i.test(file)) {
46
- for (const m of text.matchAll(/^pub\s+(?:async\s+)?(?:fn|struct|enum|trait)\s+(\w+)/gm)) add(m[1]);
47
- } else {
48
- for (const m of text.matchAll(/export\s+(?:default\s+)?(?:async\s+)?(?:function\*?|class|const|let|var|interface|type|enum)\s+([A-Za-z_$][\w$]*)/g)) add(m[1]);
49
- if (/export\s+default\s+(?:async\s+)?function\s*\(/.test(text)) add('default');
50
- for (const m of text.matchAll(/export\s*\{([^}]+)\}/g)) {
51
- for (const part of m[1].split(',')) add(part.trim().split(/\s+as\s+/).pop());
52
- }
53
- }
54
-
55
- return found.slice(0, SYMBOLS_PER_FILE);
56
- }
57
-
58
- async function symbolsFor(root, rel) {
59
- const abs = path.join(root, rel);
60
- try {
61
- const stat = await fs.stat(abs);
62
- if (stat.size > SYMBOL_BYTES) return [];
63
- const cached = symbolCache.get(abs);
64
- if (cached && cached.mtime === stat.mtimeMs) return cached.symbols;
65
- const symbols = symbolsIn(rel, await fs.readFile(abs, 'utf8'));
66
- symbolCache.set(abs, { mtime: stat.mtimeMs, symbols });
67
- return symbols;
68
- } catch {
69
- return [];
70
- }
71
- }
72
-
73
- /**
74
- * A compact outline of the project: directories, their files, and what each
75
- * code file exports. Bounded, so a large repository costs a fixed amount of
76
- * context rather than all of it.
77
- */
78
- export async function projectMap(root) {
79
- const all = (await walk(root, { limit: MAP_FILES * 3 })).filter((f) => !NOISE.test(f));
80
- if (all.length === 0) return '(the folder is empty — this is a new project)';
81
-
82
- const files = all.slice(0, MAP_FILES).sort();
83
- const symbols = await Promise.all(
84
- files.map((f) => (CODE.test(f) ? symbolsFor(root, f) : Promise.resolve([])))
85
- );
86
-
87
- const byDir = new Map();
88
- files.forEach((f, i) => {
89
- const dir = path.posix.dirname(f);
90
- const name = path.posix.basename(f);
91
- const line = symbols[i].length ? `${name} · ${symbols[i].join(', ')}` : name;
92
- if (!byDir.has(dir)) byDir.set(dir, []);
93
- byDir.get(dir).push(line);
94
- });
95
-
96
- const out = [];
97
- let size = 0;
98
- let shown = 0;
99
- for (const [dir, entries] of byDir) {
100
- const block = [dir === '.' ? './' : `${dir}/`, ...entries.map((e) => ` ${e}`)].join('\n');
101
- if (size + block.length > MAP_CHARS) {
102
- out.push(`… ${files.length - shown} more files not shown`);
103
- break;
104
- }
105
- out.push(block);
106
- size += block.length;
107
- shown += entries.length;
108
- }
109
- if (all.length > MAP_FILES) out.push(`… the project has ${all.length}+ files; the rest are not listed`);
110
-
111
- return out.join('\n');
112
- }
113
-
114
- async function readCapped(file) {
115
- try {
116
- const text = (await fs.readFile(file, 'utf8')).trim();
117
- if (!text) return '';
118
- return text.length > MEMORY_CHARS ? `${text.slice(0, MEMORY_CHARS)}\n… (cut)` : text;
119
- } catch {
120
- return '';
121
- }
122
- }
123
-
124
- /**
125
- * Standing instructions: ~/.ucode/UCODE.md for how you like to work anywhere,
126
- * then <project>/UCODE.md for this project. The project file comes second so
127
- * it wins where the two disagree.
128
- */
129
- export async function loadMemory(root) {
130
- const personal = await readCapped(GLOBAL_MEMORY);
131
- const project = await readCapped(path.join(root, MEMORY_FILE));
132
- const parts = [];
133
- if (personal) parts.push(`From ~/.ucode/${MEMORY_FILE} (applies everywhere):\n${personal}`);
134
- if (project) parts.push(`From ./${MEMORY_FILE} (this project):\n${project}`);
135
- return parts.join('\n\n');
136
- }
137
-
138
- /** Append one note to this project's UCODE.md, creating it if needed. */
139
- export async function remember(root, note) {
140
- const file = path.join(root, MEMORY_FILE);
141
- let existing = '';
142
- try {
143
- existing = await fs.readFile(file, 'utf8');
144
- } catch {
145
- existing = `# Project memory\n\nucode reads this at the start of every session in this folder.\n\n`;
146
- }
147
- const line = `- ${String(note).trim().replace(/\s+/g, ' ')}\n`;
148
- const sep = existing.endsWith('\n') ? '' : '\n';
149
- await fs.writeFile(file, existing + sep + line, 'utf8');
150
- return file;
151
- }
1
+ /**
2
+ * context.js — what the model knows about the project before it asks.
3
+ *
4
+ * Two things go into the system prompt at the start of every turn:
5
+ *
6
+ * the project map every file, and the names each code file exports, so the
7
+ * model can go straight to the right file instead of
8
+ * spending round trips on list_dir and grep to find it
9
+ *
10
+ * project memory UCODE.md — the stack, the commands, the conventions, the
11
+ * preferences — written once, remembered every session
12
+ */
13
+
14
+ import { promises as fs } from 'node:fs';
15
+ import os from 'node:os';
16
+ import path from 'node:path';
17
+ import { walk } from '../tools/shared.js';
18
+
19
+ export const MEMORY_FILE = 'UCODE.md';
20
+ export const GLOBAL_MEMORY = path.join(os.homedir(), '.ucode', MEMORY_FILE);
21
+
22
+ const MAP_FILES = 400;
23
+ const MAP_CHARS = 8_000;
24
+ const MEMORY_CHARS = 6_000;
25
+ const SYMBOL_BYTES = 120_000;
26
+ const SYMBOLS_PER_FILE = 8;
27
+
28
+ const CODE = /\.(?:[cm]?[jt]sx?|py|go|rs|vue|svelte)$/i;
29
+
30
+ // Files the map never needs to name.
31
+ const NOISE = /(^|\/)(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb|.*\.map|.*\.min\.[jc]ss?|\.DS_Store|next-env\.d\.ts)$/i;
32
+
33
+ /** Parsed symbols, kept per file until the file's mtime changes. */
34
+ const symbolCache = new Map();
35
+
36
+ function symbolsIn(file, text) {
37
+ const found = [];
38
+ const add = (name) => { if (name && !found.includes(name)) found.push(name); };
39
+
40
+ if (/\.py$/i.test(file)) {
41
+ for (const m of text.matchAll(/^(?:async\s+)?(?:def|class)\s+([A-Za-z_]\w*)/gm)) add(m[1]);
42
+ } else if (/\.go$/i.test(file)) {
43
+ for (const m of text.matchAll(/^func\s+(?:\([^)]*\)\s*)?([A-Z]\w*)/gm)) add(m[1]);
44
+ for (const m of text.matchAll(/^type\s+([A-Z]\w*)/gm)) add(m[1]);
45
+ } else if (/\.rs$/i.test(file)) {
46
+ for (const m of text.matchAll(/^pub\s+(?:async\s+)?(?:fn|struct|enum|trait)\s+(\w+)/gm)) add(m[1]);
47
+ } else {
48
+ for (const m of text.matchAll(/export\s+(?:default\s+)?(?:async\s+)?(?:function\*?|class|const|let|var|interface|type|enum)\s+([A-Za-z_$][\w$]*)/g)) add(m[1]);
49
+ if (/export\s+default\s+(?:async\s+)?function\s*\(/.test(text)) add('default');
50
+ for (const m of text.matchAll(/export\s*\{([^}]+)\}/g)) {
51
+ for (const part of m[1].split(',')) add(part.trim().split(/\s+as\s+/).pop());
52
+ }
53
+ }
54
+
55
+ return found.slice(0, SYMBOLS_PER_FILE);
56
+ }
57
+
58
+ async function symbolsFor(root, rel) {
59
+ const abs = path.join(root, rel);
60
+ try {
61
+ const stat = await fs.stat(abs);
62
+ if (stat.size > SYMBOL_BYTES) return [];
63
+ const cached = symbolCache.get(abs);
64
+ if (cached && cached.mtime === stat.mtimeMs) return cached.symbols;
65
+ const symbols = symbolsIn(rel, await fs.readFile(abs, 'utf8'));
66
+ symbolCache.set(abs, { mtime: stat.mtimeMs, symbols });
67
+ return symbols;
68
+ } catch {
69
+ return [];
70
+ }
71
+ }
72
+
73
+ /**
74
+ * A compact outline of the project: directories, their files, and what each
75
+ * code file exports. Bounded, so a large repository costs a fixed amount of
76
+ * context rather than all of it.
77
+ */
78
+ export async function projectMap(root) {
79
+ const all = (await walk(root, { limit: MAP_FILES * 3 })).filter((f) => !NOISE.test(f));
80
+ if (all.length === 0) return '(the folder is empty — this is a new project)';
81
+
82
+ const files = all.slice(0, MAP_FILES).sort();
83
+ const symbols = await Promise.all(
84
+ files.map((f) => (CODE.test(f) ? symbolsFor(root, f) : Promise.resolve([])))
85
+ );
86
+
87
+ const byDir = new Map();
88
+ files.forEach((f, i) => {
89
+ const dir = path.posix.dirname(f);
90
+ const name = path.posix.basename(f);
91
+ const line = symbols[i].length ? `${name} · ${symbols[i].join(', ')}` : name;
92
+ if (!byDir.has(dir)) byDir.set(dir, []);
93
+ byDir.get(dir).push(line);
94
+ });
95
+
96
+ const out = [];
97
+ let size = 0;
98
+ let shown = 0;
99
+ for (const [dir, entries] of byDir) {
100
+ const block = [dir === '.' ? './' : `${dir}/`, ...entries.map((e) => ` ${e}`)].join('\n');
101
+ if (size + block.length > MAP_CHARS) {
102
+ out.push(`… ${files.length - shown} more files not shown`);
103
+ break;
104
+ }
105
+ out.push(block);
106
+ size += block.length;
107
+ shown += entries.length;
108
+ }
109
+ if (all.length > MAP_FILES) out.push(`… the project has ${all.length}+ files; the rest are not listed`);
110
+
111
+ return out.join('\n');
112
+ }
113
+
114
+ /**
115
+ * Is there any code here yet?
116
+ *
117
+ * Answered from the map that has just been built rather than by walking the
118
+ * tree a second time. A folder with no code file in it has nothing to look up,
119
+ * rename or type-check, so the tools that do those things are dead weight in
120
+ * every request of the turn — and the whole tool list is re-read by the
121
+ * provider on every step.
122
+ */
123
+ export function hasCode(map) {
124
+ return /\.(?:[cm]?[jt]sx?|py|go|rs|vue|svelte)(?![\w-])/i.test(String(map ?? ''));
125
+ }
126
+
127
+ async function readCapped(file) {
128
+ try {
129
+ const text = (await fs.readFile(file, 'utf8')).trim();
130
+ if (!text) return '';
131
+ return text.length > MEMORY_CHARS ? `${text.slice(0, MEMORY_CHARS)}\n… (cut)` : text;
132
+ } catch {
133
+ return '';
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Standing instructions: ~/.ucode/UCODE.md for how you like to work anywhere,
139
+ * then <project>/UCODE.md for this project. The project file comes second so
140
+ * it wins where the two disagree.
141
+ */
142
+ export async function loadMemory(root) {
143
+ const personal = await readCapped(GLOBAL_MEMORY);
144
+ const project = await readCapped(path.join(root, MEMORY_FILE));
145
+ const parts = [];
146
+ if (personal) parts.push(`From ~/.ucode/${MEMORY_FILE} (applies everywhere):\n${personal}`);
147
+ if (project) parts.push(`From ./${MEMORY_FILE} (this project):\n${project}`);
148
+ return parts.join('\n\n');
149
+ }
150
+
151
+ /** Append one note to this project's UCODE.md, creating it if needed. */
152
+ export async function remember(root, note) {
153
+ const file = path.join(root, MEMORY_FILE);
154
+ let existing = '';
155
+ try {
156
+ existing = await fs.readFile(file, 'utf8');
157
+ } catch {
158
+ existing = `# Project memory\n\nucode reads this at the start of every session in this folder.\n\n`;
159
+ }
160
+ const line = `- ${String(note).trim().replace(/\s+/g, ' ')}\n`;
161
+ const sep = existing.endsWith('\n') ? '' : '\n';
162
+ await fs.writeFile(file, existing + sep + line, 'utf8');
163
+ return file;
164
+ }