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.
- package/README.md +36 -7
- package/package.json +1 -1
- package/skills/build-app/DIGEST.md +94 -0
- package/skills/build-app/SKILL.md +222 -181
- package/skills/ui-ux/DIGEST.md +135 -0
- package/src/core/context.js +164 -151
- package/src/core/loop.js +209 -39
- package/src/core/provider.js +874 -868
- package/src/core/skills.js +189 -165
- package/src/core/window.js +27 -2
- package/src/tools/blocks.js +117 -27
- package/src/tools/index.js +71 -23
- package/src/tools/scaffold.js +104 -14
- package/src/ui/plain.js +4 -2
- package/src/ui/screen.js +14 -5
- package/src/ui/theme.js +100 -24
- package/templates/blocks/plain/filter-bar.js +133 -0
- package/templates/blocks/plain/item-list.js +249 -0
- package/templates/blocks/plain/modal.js +141 -0
- package/templates/blocks/plain/store.js +93 -0
- package/templates/blocks/plain/theme-toggle.js +116 -0
- package/templates/blocks/plain/toast.js +107 -0
- package/templates/plain-html/styles.css +4 -0
|
@@ -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.
|
package/src/core/context.js
CHANGED
|
@@ -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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
+
}
|