ucode-agent 1.0.0 → 1.1.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/package.json +1 -1
- package/skills/build-app/SKILL.md +136 -57
- package/skills/code-review/SKILL.md +69 -24
- package/skills/debug/SKILL.md +73 -31
- package/skills/ui-ux/SKILL.md +256 -193
- package/skills/write-tests/SKILL.md +72 -34
- package/src/core/loop.js +3 -2
- package/src/ui/screen.js +31 -4
package/package.json
CHANGED
|
@@ -1,81 +1,160 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: build-app
|
|
3
|
-
description: Take
|
|
4
|
-
auto: scaffold, new project, from scratch, build an app, make an app, create an app, build a website, make a website, build a site, build me a, next.js app, nextjs app, next app, react app, vite app, shadcn, create-next-app
|
|
3
|
+
description: Take an app from nothing to running and finished — stack choice, non-interactive scaffolding, project structure, secrets, AI and API integration, error handling, and proving it works before saying it does.
|
|
4
|
+
auto: scaffold, new project, from scratch, build an app, make an app, create an app, build a website, make a website, build a site, build me a, next.js app, nextjs app, next app, react app, vite app, shadcn, create-next-app, full stack, fullstack, saas, mvp
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Building something from nothing
|
|
8
8
|
|
|
9
|
-
The failure mode
|
|
10
|
-
|
|
9
|
+
The failure mode is not bad code. It is a folder of files that has never been
|
|
10
|
+
run, handed over as if it works. Everything here is ordered to prevent that.
|
|
11
11
|
|
|
12
|
-
## Decide the shape before
|
|
12
|
+
## 1. Decide the shape, out loud, before any file exists
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
One line each:
|
|
15
15
|
|
|
16
|
-
- **What it does** — the
|
|
17
|
-
- **The
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
genuinely earns it, not because the project sounds serious.
|
|
21
|
-
- **The files** — the whole list, before you create any of them.
|
|
16
|
+
- **What it does** — the sentence a user would say.
|
|
17
|
+
- **The core loop** — the one path through it that must work perfectly
|
|
18
|
+
(e.g. upload a photo → analysed → see a score and the problems).
|
|
19
|
+
- **The stack, and why** — the smallest thing that does the job:
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
| Need | Choose |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| One page, no secrets, no server | a single `index.html`, no build step |
|
|
24
|
+
| Interactive client app, no secrets | Vite + React + TypeScript |
|
|
25
|
+
| Pages plus a server, secrets, API routes, SEO | Next.js App Router + TypeScript |
|
|
26
|
+
| An API on its own | Node (Hono/Express) or Python (FastAPI) |
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
- **The file list** — the whole tree, before creating any of it.
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
If there is a user interface, the `ui-ux` skill is already loaded. Decide the
|
|
31
|
+
design direction now, not after the logic works.
|
|
32
|
+
|
|
33
|
+
## 2. Scaffold without being asked questions
|
|
34
|
+
|
|
35
|
+
Nothing you run has a keyboard. A scaffolder that asks "Would you like to use
|
|
36
|
+
TypeScript?" gets no answer and fails, so give it every answer up front:
|
|
30
37
|
|
|
31
38
|
```bash
|
|
32
|
-
# Next.js
|
|
39
|
+
# Next.js into ./my-app (use . to fill the current folder — it must be empty)
|
|
33
40
|
npx create-next-app@latest my-app --ts --tailwind --eslint --app --src-dir --import-alias "@/*" --use-npm --yes
|
|
34
41
|
|
|
35
|
-
# shadcn/ui, from inside the
|
|
42
|
+
# shadcn/ui, from inside the project — every component you need, in one add
|
|
36
43
|
npx shadcn@latest init -d -y
|
|
37
|
-
npx shadcn@latest add button card input label badge progress separator skeleton sonner -y
|
|
44
|
+
npx shadcn@latest add button card input label badge progress separator skeleton sonner tooltip -y
|
|
38
45
|
```
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
Add every shadcn component you will need in one `add` call, not one per call.
|
|
48
|
-
|
|
49
|
-
## Lay it out in one pass
|
|
50
|
-
|
|
51
|
-
Use `batch_write` for the whole skeleton rather than `write_file` twenty times.
|
|
52
|
-
One call, every file, in dependency order. Then `run_command` the install, and
|
|
53
|
-
`run_commands` for anything independent that can happen at the same time.
|
|
47
|
+
- `create-next-app` refuses a folder that already has files. If the current
|
|
48
|
+
folder is not empty, scaffold into a named subfolder and pass it as `cwd` to
|
|
49
|
+
every later command.
|
|
50
|
+
- For any other scaffolder, find the flag for every question (`--help`) first.
|
|
51
|
+
- Install dependencies once, all together: `npm i zod lucide-react` — not one
|
|
52
|
+
`npm i` per package.
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
function are all the same thing: a promise you did not keep, in a file the user
|
|
57
|
-
now has to find.
|
|
54
|
+
## 3. Structure it like a real project
|
|
58
55
|
|
|
59
|
-
|
|
56
|
+
For Next.js App Router:
|
|
60
57
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
58
|
+
```
|
|
59
|
+
src/
|
|
60
|
+
app/
|
|
61
|
+
layout.tsx fonts, metadata, <body> shell, Toaster
|
|
62
|
+
page.tsx the screen — composes components, holds little logic
|
|
63
|
+
globals.css design tokens and the shadcn theme variables
|
|
64
|
+
api/<name>/route.ts server-only endpoints; the only place secrets live
|
|
65
|
+
components/
|
|
66
|
+
<feature>/ one folder per feature: its pieces, split by job
|
|
67
|
+
ui/ shadcn components (generated — edit via the theme)
|
|
68
|
+
lib/
|
|
69
|
+
<service>.ts calls to outside services, typed in and out
|
|
70
|
+
schemas.ts zod schemas shared by client and server
|
|
71
|
+
utils.ts
|
|
72
|
+
types/ shared TypeScript types, if lib/ does not own them
|
|
73
|
+
```
|
|
76
74
|
|
|
77
|
-
|
|
75
|
+
- **One component per file**, named for what it is (`ScoreDial.tsx`,
|
|
76
|
+
`NutrientFindings.tsx`, `LabelUpload.tsx`), not a 600-line `page.tsx`.
|
|
77
|
+
- Server components by default; `"use client"` only on the interactive parts.
|
|
78
|
+
- Types at every boundary. Parse external data with zod rather than trusting
|
|
79
|
+
its shape.
|
|
80
|
+
|
|
81
|
+
## 4. Secrets and outside services
|
|
82
|
+
|
|
83
|
+
- **A key never reaches the browser.** It lives in a server route or server
|
|
84
|
+
action. Anything imported by a `"use client"` file ships to every visitor —
|
|
85
|
+
including a "hardcoded for now" key. If the user asks to hardcode one, put it
|
|
86
|
+
in a server-only module (`lib/server/*.ts`, or `import 'server-only'`) and
|
|
87
|
+
say where it is so they can move it to `.env.local` later.
|
|
88
|
+
- Every outbound call gets: a timeout (`AbortSignal.timeout(60_000)`), a check
|
|
89
|
+
of the response status, and an error that says what failed — surfaced to the
|
|
90
|
+
UI as a real message, never a silent `catch {}`.
|
|
91
|
+
|
|
92
|
+
### Calling an AI model (OpenRouter or any OpenAI-compatible API)
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// src/app/api/analyze/route.ts — runs on the server only
|
|
96
|
+
export const runtime = 'nodejs';
|
|
97
|
+
export const maxDuration = 60;
|
|
98
|
+
|
|
99
|
+
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
model: 'provider/model-id',
|
|
104
|
+
messages: [
|
|
105
|
+
{ role: 'system', content: 'Reply with JSON only, matching this shape: {...}' },
|
|
106
|
+
{ role: 'user', content: [
|
|
107
|
+
{ type: 'text', text: 'Analyse this nutrition label.' },
|
|
108
|
+
{ type: 'image_url', image_url: { url: dataUrl } }, // data:image/jpeg;base64,...
|
|
109
|
+
] },
|
|
110
|
+
],
|
|
111
|
+
}),
|
|
112
|
+
signal: AbortSignal.timeout(60_000),
|
|
113
|
+
});
|
|
114
|
+
```
|
|
78
115
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
116
|
+
- **Ask for JSON and parse it defensively.** Models wrap JSON in prose or code
|
|
117
|
+
fences: extract the first `{...}` block, `JSON.parse` it, validate with zod,
|
|
118
|
+
and on failure return a clear "could not read the result" error rather than
|
|
119
|
+
crashing. Clamp numbers to their range.
|
|
120
|
+
- **Put the judgement rules in the prompt, explicitly** — thresholds, what
|
|
121
|
+
counts as "too much", what to omit. A vague prompt gives a different answer
|
|
122
|
+
every time; a specific one gives the product its consistency.
|
|
123
|
+
- **Images:** check type and size on the client (e.g. ≤ 5 MB, jpeg/png/webp),
|
|
124
|
+
downscale large photos in a canvas before upload, send as a base64 data URL.
|
|
125
|
+
- Reasoning models may take 10–60s. Show progress, and make the route's
|
|
126
|
+
timeout longer than the model's.
|
|
127
|
+
|
|
128
|
+
## 5. Build order
|
|
129
|
+
|
|
130
|
+
1. Skeleton and design tokens, so every later piece is styled correctly first time.
|
|
131
|
+
2. The server route with the real integration, tested with `curl` before any UI.
|
|
132
|
+
3. The core loop UI, wired to the real route.
|
|
133
|
+
4. Every state: empty, loading, success, error, and invalid input.
|
|
134
|
+
5. Polish: motion, responsive, copy, favicon, page title and metadata.
|
|
135
|
+
|
|
136
|
+
Use `batch_write` for the skeleton — one call, every file.
|
|
137
|
+
|
|
138
|
+
## 6. Prove it works
|
|
139
|
+
|
|
140
|
+
- `npm run build` — it type-checks and lints; a build that fails is not done.
|
|
141
|
+
- Start it: `npm run dev` goes to the background on its own and comes back with
|
|
142
|
+
the URL once ready. Do not start it twice.
|
|
143
|
+
- Exercise it: `curl` the API route with real input, load the page, check the
|
|
144
|
+
core loop end to end. A clean build proves it compiles, not that it works.
|
|
145
|
+
- Fix what you find and check again.
|
|
146
|
+
|
|
147
|
+
## 7. Definition of done
|
|
148
|
+
|
|
149
|
+
- The core loop works end to end against the real service.
|
|
150
|
+
- No `TODO`, no placeholder copy, no dead buttons, no console errors.
|
|
151
|
+
- Every async action has loading, success and error states.
|
|
152
|
+
- Invalid input is caught with a useful message before it reaches the server.
|
|
153
|
+
- Secrets only on the server.
|
|
154
|
+
- `npm run build` passes.
|
|
155
|
+
|
|
156
|
+
## 8. Report
|
|
157
|
+
|
|
158
|
+
What you built, the URL, how to start it again, and what you checked. If
|
|
159
|
+
anything is untested or unfinished, name it — one sentence of honesty saves the
|
|
160
|
+
user an hour of finding out on their own.
|
|
@@ -1,36 +1,81 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: code-review
|
|
3
|
-
description: Review
|
|
3
|
+
description: Review code the way a senior engineer would — correctness first, then security, failure handling, contracts, performance and tests, with every finding ranked, located and backed by a concrete failing scenario.
|
|
4
|
+
auto: review, code review, review my, review this, audit this, look over, check my code, pr review, pull request
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# Reviewing code
|
|
7
8
|
|
|
8
|
-
A review that lists everything is
|
|
9
|
-
|
|
9
|
+
A review that lists everything is a linter with opinions. The value is in
|
|
10
|
+
finding what will actually break, ranking it, and being honest about what you
|
|
11
|
+
verified versus what you suspect.
|
|
10
12
|
|
|
11
|
-
##
|
|
13
|
+
## 1. Understand before judging
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Secrets in the source. Credentials in a log line.
|
|
19
|
-
4. **Contracts.** Did a signature, a return shape or a stored format change
|
|
20
|
-
without every caller and every existing row being accounted for?
|
|
21
|
-
5. **Clarity.** Names that say what the thing is. Comments that explain why,
|
|
22
|
-
never what. A function that fits in your head.
|
|
23
|
-
6. **Tests.** Does the test actually fail when the code is wrong? A test that
|
|
24
|
-
asserts a mock was called proves nothing about behaviour.
|
|
15
|
+
- What is the change trying to do? Read the description, the diff, and enough of
|
|
16
|
+
the surrounding code to know the callers and the data it touches.
|
|
17
|
+
- Get the diff: `git diff`, `git diff main...HEAD`, or the files named. Read
|
|
18
|
+
every changed file in full, not only the hunks — context is where bugs hide.
|
|
19
|
+
- If there are tests, run them. If it runs, run it.
|
|
25
20
|
|
|
26
|
-
##
|
|
21
|
+
## 2. Passes, in order of what hurts most
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
1. **Correctness** — does it do what it claims for every input? Walk: empty,
|
|
24
|
+
one, many, null/undefined, the boundary value, duplicates, the second call,
|
|
25
|
+
concurrent calls, a slow or failing dependency.
|
|
26
|
+
2. **Security** — untrusted input reaching SQL, a shell, a file path, a URL
|
|
27
|
+
fetch (SSRF), `innerHTML`/`dangerouslySetInnerHTML`, `eval`, a redirect.
|
|
28
|
+
Secrets in source, in client bundles, or in logs. Missing auth or
|
|
29
|
+
authorization checks on a route. IDs a user can change to see someone
|
|
30
|
+
else's data.
|
|
31
|
+
3. **Failure handling** — errors swallowed (`catch {}`), a fallback that hides
|
|
32
|
+
a real failure, no timeout on an outbound call, a partial write left behind,
|
|
33
|
+
an error message that leaks internals.
|
|
34
|
+
4. **Contracts** — a changed signature, return shape, API response, or stored
|
|
35
|
+
format without every caller and every existing record accounted for.
|
|
36
|
+
Migrations that are not reversible or not safe on live data.
|
|
37
|
+
5. **Performance** — N+1 queries, unbounded loops over user data, missing
|
|
38
|
+
pagination, work in a render loop, a huge dependency for a small job,
|
|
39
|
+
blocking I/O on a hot path.
|
|
40
|
+
6. **Concurrency and state** — shared mutable state, race conditions,
|
|
41
|
+
stale caches, React effects with missing or wrong dependencies.
|
|
42
|
+
7. **Tests** — do they fail when the code is wrong? A test asserting a mock was
|
|
43
|
+
called proves nothing about behaviour. Are the risky paths covered?
|
|
44
|
+
8. **Clarity** — names that say what things are, functions that fit in your
|
|
45
|
+
head, comments that explain *why*. Only flag this when it will cause a real
|
|
46
|
+
misunderstanding.
|
|
31
47
|
|
|
32
|
-
|
|
33
|
-
did not run it, do not describe the behaviour as if you watched it happen.
|
|
48
|
+
## 3. Verify before you report
|
|
34
49
|
|
|
35
|
-
|
|
36
|
-
|
|
50
|
+
For each suspected issue, check it: read the caller, trace the value, run the
|
|
51
|
+
case if you can. Drop anything you cannot substantiate, or label it clearly as
|
|
52
|
+
a question rather than a finding.
|
|
53
|
+
|
|
54
|
+
## 4. Write findings that can be acted on
|
|
55
|
+
|
|
56
|
+
Rank by severity:
|
|
57
|
+
|
|
58
|
+
- **Blocker** — wrong results, data loss, a security hole, a crash on a normal path.
|
|
59
|
+
- **Major** — breaks on a realistic edge case, or a failure that will be hard to diagnose.
|
|
60
|
+
- **Minor** — a real but small risk, or a clear maintainability cost.
|
|
61
|
+
- **Nit** — style and preference. Keep these few, or leave them out.
|
|
62
|
+
|
|
63
|
+
Each finding: **where** (`path:line`), **what breaks**, **the input or sequence
|
|
64
|
+
that breaks it**, and **the fix**. For example:
|
|
65
|
+
|
|
66
|
+
> **Major** — `src/app/api/analyze/route.ts:42` — `JSON.parse(text)` throws when
|
|
67
|
+
> the model wraps its reply in a code fence, which it does intermittently. The
|
|
68
|
+
> route then returns a 500 with no message. Extract the first `{…}` block and
|
|
69
|
+
> validate it with the zod schema; return a 422 with "could not read the label"
|
|
70
|
+
> on failure.
|
|
71
|
+
|
|
72
|
+
"This could be cleaner" is not a finding.
|
|
73
|
+
|
|
74
|
+
## 5. Close out
|
|
75
|
+
|
|
76
|
+
Start with a one-line verdict (ship / ship after fixes / needs rework), then
|
|
77
|
+
the findings, most severe first. Mention what is genuinely good only where it
|
|
78
|
+
is worth copying. State what you did not review or could not run.
|
|
79
|
+
|
|
80
|
+
If asked to fix the findings, fix blockers and majors first, re-run the tests,
|
|
81
|
+
and report what changed.
|
package/skills/debug/SKILL.md
CHANGED
|
@@ -1,47 +1,89 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: debug
|
|
3
|
-
description: Find the
|
|
3
|
+
description: Find and fix the real cause of a bug — reproduce it, narrow it down with evidence, prove the fix, guard it with a test, and check for the same bug elsewhere.
|
|
4
|
+
auto: bug, crash, crashes, crashing, broken, not working, doesn't work, does not work, stack trace, exception, traceback, throws, failing, fails, regression, undefined is not, cannot read properties, 500 error, blank page, hangs, freezes
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# Debugging
|
|
7
8
|
|
|
8
|
-
The temptation is to read the code, form a theory, change something, and
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
The temptation is to read the code, form a theory, change something, and call
|
|
10
|
+
it fixed when the symptom goes away. That moves bugs rather than fixing them.
|
|
11
|
+
Work from evidence, in this order.
|
|
11
12
|
|
|
12
|
-
## Reproduce it
|
|
13
|
+
## 1. Reproduce it yourself
|
|
13
14
|
|
|
14
|
-
Do not
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
version, the full stack. Guessing from a paraphrase wastes everyone's turn.
|
|
15
|
+
Do not debug from the description. Run it and see it fail: the exact command,
|
|
16
|
+
the input, the full error, the line it comes from. Write down the reproduction
|
|
17
|
+
as a single command or a few steps — you will run it again at the end.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
If you cannot reproduce it, say so and ask for exactly what is missing: the
|
|
20
|
+
input, the environment, the version, the full output. Guessing from a
|
|
21
|
+
paraphrase wastes everyone's turn.
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
The top frame is where it surfaced, not necessarily where it went wrong.
|
|
23
|
-
- `grep` for the message text to find where it is produced.
|
|
24
|
-
- Cut the search space in half at a time: does the smaller input fail? Does it
|
|
25
|
-
fail on the previous commit? Does the layer below get the right value?
|
|
26
|
-
- Print or log the values at the boundary rather than reasoning about what they
|
|
27
|
-
should be. What you believe is in that variable is the thing under suspicion.
|
|
23
|
+
## 2. Read the whole error
|
|
28
24
|
|
|
29
|
-
|
|
25
|
+
- The whole stack trace, including frames you assume are irrelevant. The top
|
|
26
|
+
frame is where it surfaced, not necessarily where it went wrong.
|
|
27
|
+
- The first error, not the last. Later errors are often consequences.
|
|
28
|
+
- `grep` for the exact message text to find where it is produced.
|
|
29
|
+
- Check the obvious before the clever: is the file saved, the server restarted,
|
|
30
|
+
the right branch checked out, the env var set, the dependency installed, the
|
|
31
|
+
cache cleared (`.next`, `node_modules/.vite`, `__pycache__`)?
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
undefined here because the caller only sets it on the success path*. If you
|
|
33
|
-
cannot write that sentence, you have not found it yet.
|
|
33
|
+
## 3. Narrow it down
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
the real defect in place, with one more layer over it.
|
|
35
|
+
Cut the search space in half each step:
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
- **Input:** does a smaller or simpler input still fail? Find the smallest one
|
|
38
|
+
that does.
|
|
39
|
+
- **Code:** comment out or bypass half the path. Does it still fail?
|
|
40
|
+
- **Time:** did it work before? `git log` / `git diff` since then, or
|
|
41
|
+
`git bisect` between a good and a bad commit.
|
|
42
|
+
- **Layer:** is the value right when it enters the function? When it leaves?
|
|
43
|
+
At the API boundary? In the database? Log it at each boundary and look,
|
|
44
|
+
rather than reasoning about what it "should" be.
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
- Run the rest of the tests. A fix that breaks two other things is a trade,
|
|
42
|
-
and the user gets to make it, not you.
|
|
43
|
-
- Write a test that fails without your fix. A bug with no regression test comes
|
|
44
|
-
back.
|
|
46
|
+
The value you are sure about is the one under suspicion. Print it.
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
## 4. Know where bugs usually live
|
|
49
|
+
|
|
50
|
+
- **Async:** a missing `await`, a race between two requests, state read before
|
|
51
|
+
it is set, a promise rejection nobody catches.
|
|
52
|
+
- **State:** stale closures in React effects, mutation of shared objects, a
|
|
53
|
+
cache that was never invalidated.
|
|
54
|
+
- **Boundaries:** off-by-one, empty arrays, `null` vs `undefined` vs `''`,
|
|
55
|
+
timezones, number parsing (`'10' + 1`), float rounding.
|
|
56
|
+
- **Data shape:** the API returned something different from the type — an
|
|
57
|
+
error object, a wrapped payload, a string instead of JSON.
|
|
58
|
+
- **Environment:** missing env var, wrong Node version, path case sensitivity,
|
|
59
|
+
Windows vs POSIX paths and line endings, a port already in use.
|
|
60
|
+
- **Build tooling:** a stale build cache, a server/client boundary violation in
|
|
61
|
+
Next.js, a default vs named export mismatch, ESM vs CommonJS.
|
|
62
|
+
|
|
63
|
+
## 5. State the cause before fixing it
|
|
64
|
+
|
|
65
|
+
Write it in one sentence: *"`score` is `undefined` here because the parser
|
|
66
|
+
returns `{ data: {...} }` and the component reads `result.score`."* If you
|
|
67
|
+
cannot write that sentence, you have not found the cause yet — keep narrowing.
|
|
68
|
+
|
|
69
|
+
## 6. Fix the cause, not the symptom
|
|
70
|
+
|
|
71
|
+
A `?.` or a `try/catch` that hides the failure leaves the defect in place under
|
|
72
|
+
one more layer. Fix it where it originates. Keep the change as small as the
|
|
73
|
+
cause allows, and do not refactor unrelated code in the same change.
|
|
74
|
+
|
|
75
|
+
## 7. Prove it
|
|
76
|
+
|
|
77
|
+
- Run the original reproduction. It must pass now.
|
|
78
|
+
- Run the whole test suite. A fix that breaks two other things is a trade the
|
|
79
|
+
user gets to decide on, not you.
|
|
80
|
+
- Add a regression test that fails without the fix and passes with it — then
|
|
81
|
+
briefly revert the fix to confirm the test really catches it.
|
|
82
|
+
- Look for the same mistake elsewhere: `grep` for the same pattern, call, or
|
|
83
|
+
assumption. Bugs come in families.
|
|
84
|
+
|
|
85
|
+
## 8. Report
|
|
86
|
+
|
|
87
|
+
The cause in one or two sentences, the fix, how you verified it, and anything
|
|
88
|
+
adjacent you noticed but did not change. If you could not fully confirm it,
|
|
89
|
+
say what is still uncertain.
|
package/skills/ui-ux/SKILL.md
CHANGED
|
@@ -1,237 +1,300 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ui-ux
|
|
3
|
-
description:
|
|
4
|
-
auto: app, apps, ui, ux, website, web app, webapp, web page, webpage, landing page, dashboard, frontend, front-end, interface, css, tailwind, html, react, vue, svelte, nextjs, next.js, redesign, restyle, responsive, dark mode, ugly, styling, stylesheet, mockup, prototype
|
|
3
|
+
description: Design and build interfaces at the level of a senior product designer who also ships the code — direction, type, colour, layout, components, states, motion, accessibility and performance, verified on screen. Loads itself for any work with a user interface in it.
|
|
4
|
+
auto: app, apps, ui, ux, website, web app, webapp, web page, webpage, landing page, dashboard, frontend, front-end, interface, css, tailwind, html, react, vue, svelte, nextjs, next.js, shadcn, redesign, restyle, responsive, dark mode, ugly, styling, stylesheet, mockup, prototype, component, components, polished, beautiful, good looking, visually
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# Interfaces
|
|
7
|
+
# Interfaces, done properly
|
|
8
8
|
|
|
9
9
|
The house style of a language model is a centred column, a purple-to-blue
|
|
10
|
-
gradient, three
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
gradient, three identical cards, Inter at every size, and a lot of empty space.
|
|
11
|
+
Everyone has seen it a thousand times, and it reads as generated on sight.
|
|
12
|
+
This skill exists to stop you producing it.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
Work in this order and do not skip ahead. Most bad interfaces are good CSS
|
|
15
|
+
applied to an undecided design.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
1. Direction 2. Structure 3. Tokens 4. Components 5. States 6. Motion
|
|
18
|
+
7. Accessibility and performance 8. Look at it 9. Report
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
1. **Job** — what does this screen actually do?
|
|
22
|
-
2. **Who** — who opens it, how often, and what do they need first?
|
|
23
|
-
3. **Tone** — pick one and commit: utilitarian, editorial, technical, playful,
|
|
24
|
-
industrial, calm, dense. "Modern and clean" is not a tone, it is a way of
|
|
25
|
-
avoiding the question.
|
|
26
|
-
4. **One memorable detail** — a colour, a texture, a typographic move, a single
|
|
27
|
-
interaction. Exactly one. It is the difference between a design and a
|
|
28
|
-
template.
|
|
29
|
-
|
|
30
|
-
## 2. Pick the mode from the surface, not the product
|
|
31
|
-
|
|
32
|
-
The mode names what success looks like for the person in front of it. It
|
|
33
|
-
decides how much the interface is allowed to perform.
|
|
34
|
-
|
|
35
|
-
- **Operate** — they are completing a task. App UI, dashboards, editors,
|
|
36
|
-
admin, settings, tools. Scannability, consistency and speed beat expression
|
|
37
|
-
every time. The personality lives in precise details, not in the hero.
|
|
38
|
-
- **Persuade** — they are deciding whether to act. Landing pages, pricing,
|
|
39
|
-
marketing. Here the design *is* the product; earn the attention.
|
|
40
|
-
- **Read** — they are trying to understand something. Docs, articles, guides.
|
|
41
|
-
Structure for comprehension first, then make reading pleasant enough to stay.
|
|
42
|
-
- **Experience** — they are looking at the work itself. Portfolios, galleries.
|
|
43
|
-
The artifact leads from the first screen and the interface gets out of the way.
|
|
20
|
+
---
|
|
44
21
|
|
|
45
|
-
|
|
46
|
-
|
|
22
|
+
## 1. Decide the direction before any code
|
|
23
|
+
|
|
24
|
+
Write these down in one line each, then build to them:
|
|
25
|
+
|
|
26
|
+
- **Job** — what does this screen do, in one sentence a user would say?
|
|
27
|
+
- **Who and when** — who opens it, how often, on what device, in what light?
|
|
28
|
+
A tool opened forty times a day and a page seen once need opposite things.
|
|
29
|
+
- **Mode** — pick one:
|
|
30
|
+
- **Operate**: completing a task (apps, dashboards, tools, settings). Speed,
|
|
31
|
+
scannability and consistency beat expression. Personality lives in details.
|
|
32
|
+
- **Persuade**: deciding whether to act (landing, pricing, marketing). The
|
|
33
|
+
design is the product; it has to earn attention in one screen.
|
|
34
|
+
- **Read**: understanding something (docs, articles). Measure, rhythm and
|
|
35
|
+
hierarchy first; decoration last.
|
|
36
|
+
- **Experience**: looking at the work itself (portfolio, gallery). The
|
|
37
|
+
content leads; the interface gets out of the way.
|
|
38
|
+
- **Tone** — one word you commit to: clinical, warm, editorial, technical,
|
|
39
|
+
playful, industrial, calm, dense. "Modern and clean" is not a tone.
|
|
40
|
+
- **The one memorable thing** — a colour, a type move, a texture, a single
|
|
41
|
+
interaction. Exactly one. It is the difference between a design and a theme.
|
|
42
|
+
|
|
43
|
+
A dashboard stays Operate however loud the brand is. A tool's landing page is
|
|
44
|
+
still Persuade. Never put a marketing hero on top of a working tool.
|
|
45
|
+
|
|
46
|
+
## 2. Structure: hierarchy before decoration
|
|
47
|
+
|
|
48
|
+
- Decide what the eye lands on first, second, third. Build that with **size,
|
|
49
|
+
weight, colour and position** before reaching for a box, a border or a card.
|
|
50
|
+
Three levels — primary, secondary, muted — is usually all a screen needs.
|
|
51
|
+
- **The primary thing gets disproportionate size.** If a score, a total or a
|
|
52
|
+
status is the point of the screen, make it unmistakably larger than
|
|
53
|
+
everything around it — not 10% bigger, three or four times bigger.
|
|
54
|
+
- Align to a grid and share edges. Ragged left edges are the single most common
|
|
55
|
+
reason a page feels amateur. Pick a max content width (e.g. 1120px for apps,
|
|
56
|
+
68ch for prose) and hold it.
|
|
57
|
+
- Group by proximity: space **inside** a group must be smaller than space
|
|
58
|
+
**between** groups, or the grouping reads wrong however good the rest is.
|
|
59
|
+
- One job per element. A card that is a link, a form and a menu is three cards.
|
|
60
|
+
- Put actions where the eye already is: primary action at the end of the flow it
|
|
61
|
+
completes, destructive actions separated from safe ones.
|
|
47
62
|
|
|
48
|
-
## 3. Tokens
|
|
63
|
+
## 3. Tokens: set them once, never use a raw value again
|
|
49
64
|
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
Everything below is a starting point to adjust, not a look to ship unchanged.
|
|
66
|
+
The palette in particular must be re-picked for the product's tone.
|
|
52
67
|
|
|
53
68
|
```css
|
|
54
69
|
:root {
|
|
55
|
-
|
|
56
|
-
--
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
--text-
|
|
61
|
-
|
|
62
|
-
--
|
|
63
|
-
--
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
--
|
|
67
|
-
--
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
--
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
/* Type — a real scale, fluid between mobile and desktop. */
|
|
71
|
+
--font-sans: "Inter Tight", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
72
|
+
--font-display: var(--font-sans);
|
|
73
|
+
--font-mono: ui-monospace, "Cascadia Code", "JetBrains Mono", Consolas, monospace;
|
|
74
|
+
|
|
75
|
+
--text-xs: .75rem;
|
|
76
|
+
--text-sm: .875rem;
|
|
77
|
+
--text-md: 1rem;
|
|
78
|
+
--text-lg: clamp(1.125rem, 1rem + .4vw, 1.25rem);
|
|
79
|
+
--text-xl: clamp(1.375rem, 1.1rem + .9vw, 1.75rem);
|
|
80
|
+
--text-2xl: clamp(1.75rem, 1.3rem + 1.6vw, 2.5rem);
|
|
81
|
+
--text-3xl: clamp(2.25rem, 1.5rem + 3vw, 3.75rem);
|
|
82
|
+
--text-hero: clamp(3rem, 2rem + 5vw, 6rem);
|
|
83
|
+
|
|
84
|
+
/* Space — one scale, nothing in between. */
|
|
85
|
+
--s-1: .25rem; --s-2: .5rem; --s-3: .75rem; --s-4: 1rem; --s-5: 1.5rem;
|
|
86
|
+
--s-6: 2rem; --s-7: 3rem; --s-8: 4rem; --s-9: 6rem; --s-10: 8rem;
|
|
87
|
+
|
|
88
|
+
/* Colour — neutrals carry a hue; flat #808080 grey is what makes a UI look dead.
|
|
89
|
+
OKLCH so lightness steps are perceptually even. */
|
|
90
|
+
--bg: oklch(98.5% .004 90);
|
|
91
|
+
--surface: oklch(100% 0 0);
|
|
92
|
+
--surface-2: oklch(96.5% .006 90);
|
|
93
|
+
--line: oklch(90% .008 90);
|
|
94
|
+
--ink: oklch(22% .01 90);
|
|
95
|
+
--ink-2: oklch(45% .012 90);
|
|
96
|
+
--ink-3: oklch(60% .01 90);
|
|
97
|
+
|
|
98
|
+
--accent: oklch(58% .19 255);
|
|
99
|
+
--accent-ink: oklch(99% 0 0);
|
|
100
|
+
--accent-soft: oklch(95% .03 255);
|
|
101
|
+
|
|
102
|
+
--good: oklch(62% .16 150); --good-soft: oklch(95% .04 150);
|
|
103
|
+
--warn: oklch(72% .16 70); --warn-soft: oklch(96% .05 80);
|
|
104
|
+
--bad: oklch(58% .21 25); --bad-soft: oklch(95% .04 25);
|
|
105
|
+
|
|
106
|
+
--radius-sm: 6px; --radius: 10px; --radius-lg: 16px; --radius-full: 999px;
|
|
107
|
+
--shadow-sm: 0 1px 2px oklch(20% .01 90 / .06);
|
|
108
|
+
--shadow: 0 1px 2px oklch(20% .01 90 / .05), 0 8px 24px oklch(20% .01 90 / .08);
|
|
109
|
+
--shadow-lg: 0 2px 4px oklch(20% .01 90 / .06), 0 24px 48px oklch(20% .01 90 / .14);
|
|
74
110
|
--focus: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent);
|
|
111
|
+
|
|
112
|
+
--ease-out: cubic-bezier(.22, 1, .36, 1);
|
|
113
|
+
--dur-1: 120ms; --dur-2: 200ms; --dur-3: 320ms;
|
|
75
114
|
}
|
|
76
115
|
|
|
77
116
|
@media (prefers-color-scheme: dark) {
|
|
78
117
|
:root {
|
|
79
|
-
--bg:
|
|
80
|
-
--
|
|
81
|
-
--
|
|
118
|
+
--bg: oklch(16% .008 260); --surface: oklch(20% .01 260);
|
|
119
|
+
--surface-2: oklch(24% .012 260); --line: oklch(30% .012 260);
|
|
120
|
+
--ink: oklch(95% .005 260); --ink-2: oklch(76% .01 260); --ink-3: oklch(60% .01 260);
|
|
121
|
+
--accent: oklch(70% .16 255); --accent-ink: oklch(18% .02 260); --accent-soft: oklch(28% .06 255);
|
|
82
122
|
}
|
|
83
123
|
}
|
|
84
124
|
|
|
85
|
-
|
|
125
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
126
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
86
127
|
body {
|
|
87
128
|
margin: 0; background: var(--bg); color: var(--ink);
|
|
88
|
-
font: var(--text-
|
|
89
|
-
-webkit-font-smoothing: antialiased;
|
|
129
|
+
font: var(--text-md)/1.6 var(--font-sans);
|
|
130
|
+
-webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
|
|
90
131
|
}
|
|
91
|
-
h1, h2, h3 { margin: 0; line-height: 1.
|
|
92
|
-
|
|
132
|
+
h1, h2, h3 { margin: 0; line-height: 1.1; letter-spacing: -.02em; text-wrap: balance; }
|
|
133
|
+
p { text-wrap: pretty; }
|
|
134
|
+
:focus-visible { outline: none; box-shadow: var(--focus); border-radius: var(--radius-sm); }
|
|
135
|
+
.num { font-variant-numeric: tabular-nums; }
|
|
93
136
|
@media (prefers-reduced-motion: reduce) {
|
|
94
|
-
*, *::before, *::after { animation:
|
|
137
|
+
*, *::before, *::after { animation-duration: 1ms !important; transition-duration: 1ms !important; }
|
|
95
138
|
}
|
|
96
139
|
```
|
|
97
140
|
|
|
98
|
-
**
|
|
99
|
-
|
|
100
|
-
- **
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
**
|
|
111
|
-
|
|
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
|
-
- **Loading** — skeletons shaped like the real content, or a spinner on the
|
|
145
|
-
control that was pressed. Do not blank the page.
|
|
146
|
-
- **Error** — what failed and what they can do about it. Keep their input.
|
|
147
|
-
- **Partial** — one row failed and the rest loaded.
|
|
148
|
-
|
|
149
|
-
And for every interactive element: `:hover`, `:focus-visible`, `:active`,
|
|
150
|
-
`:disabled`, and the selected state. A control with only a default state is
|
|
151
|
-
unfinished, not minimal. `outline: none` with no replacement focus ring is a
|
|
152
|
-
bug, not a style choice.
|
|
141
|
+
**The rules behind the tokens**, for when you need a value that is not there:
|
|
142
|
+
|
|
143
|
+
- **Type.** One family for UI, at most one more for display. Body 15–17px at
|
|
144
|
+
1.5–1.65 line height, measure 60–75ch. Headings 1.05–1.2 line height with
|
|
145
|
+
negative tracking above 28px. Weight does more than size for mid-level
|
|
146
|
+
hierarchy: 600 for labels that matter, 400 for body, 500 for UI controls.
|
|
147
|
+
Use `tabular-nums` anywhere numbers line up or change in place.
|
|
148
|
+
- **Pick a typeface with intent.** Inter everywhere is the generated look. Good
|
|
149
|
+
free choices through `next/font/google` or Google Fonts: *Inter Tight,
|
|
150
|
+
Geist, Manrope, DM Sans, Plus Jakarta Sans, Instrument Sans, Space Grotesk,
|
|
151
|
+
IBM Plex Sans* for UI; *Fraunces, Instrument Serif, Bricolage Grotesque,
|
|
152
|
+
Newsreader* for display. Load only the weights you use, `display: swap`.
|
|
153
|
+
- **Colour.** One accent hue, one neutral ramp tinted slightly toward it, and
|
|
154
|
+
semantic good/warn/bad that mean one thing each. Colour is for meaning and
|
|
155
|
+
emphasis, not decoration. Never pure `#000` on pure `#fff`. Count the hue
|
|
156
|
+
families at the end: more than one accent plus the semantics means the
|
|
157
|
+
palette got away from you.
|
|
158
|
+
- **Radius.** Pick one base and derive: controls 6–8, cards 10–16, pills full.
|
|
159
|
+
Four unrelated radii look like an accident.
|
|
160
|
+
- **Depth.** Borders and background steps first; shadows only for things that
|
|
161
|
+
genuinely float (menus, popovers, modals, toasts). A shadow on every card
|
|
162
|
+
flattens the hierarchy it was meant to create.
|
|
163
|
+
- **Dark mode.** Swap variables, never invert. Surfaces get *lighter* as they
|
|
164
|
+
rise. Text around 92–95% lightness, not pure white. Re-check every accent for
|
|
165
|
+
contrast in dark — most need to get lighter.
|
|
166
|
+
|
|
167
|
+
### With Tailwind and shadcn/ui
|
|
168
|
+
|
|
169
|
+
- shadcn ships with a neutral slate look. **Re-theme it** in `globals.css` by
|
|
170
|
+
setting its CSS variables (`--background`, `--foreground`, `--primary`,
|
|
171
|
+
`--muted`, `--accent`, `--destructive`, `--border`, `--ring`, `--radius`) to
|
|
172
|
+
your palette. Shipping the default theme is shipping someone else's design.
|
|
173
|
+
- Use components for behaviour and accessibility (Dialog, Popover, Tabs,
|
|
174
|
+
Select, Tooltip, Toast/Sonner), then style them to the direction. Do not wrap
|
|
175
|
+
every region of the page in a `Card` — that is the three-card look again.
|
|
176
|
+
- Extend the Tailwind theme with your tokens rather than scattering arbitrary
|
|
177
|
+
values (`text-[17px]`, `mt-[13px]`). Arbitrary values are the raw values this
|
|
178
|
+
section forbids, in a different syntax.
|
|
179
|
+
- Icons: `lucide-react`, one stroke width throughout, sized to the text beside
|
|
180
|
+
them (16px with 14–15px text, 20px with 16–18px). Never emoji as UI icons.
|
|
181
|
+
|
|
182
|
+
## 4. Components, with every state built in
|
|
183
|
+
|
|
184
|
+
Every interactive element needs **default, hover, focus-visible, active,
|
|
185
|
+
disabled**, and where it applies **selected, loading, error**. A control with
|
|
186
|
+
only a default state is unfinished, not minimal.
|
|
153
187
|
|
|
154
188
|
```css
|
|
155
189
|
.btn {
|
|
156
|
-
|
|
157
|
-
padding:
|
|
190
|
+
display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
|
|
191
|
+
min-height: 44px; padding: 0 var(--s-5);
|
|
192
|
+
font: 500 var(--text-md)/1 var(--font-sans);
|
|
158
193
|
border: 1px solid transparent; border-radius: var(--radius-sm);
|
|
159
194
|
background: var(--accent); color: var(--accent-ink);
|
|
160
|
-
cursor: pointer; transition: filter
|
|
161
|
-
}
|
|
162
|
-
.btn:hover { filter: brightness(1.08); }
|
|
163
|
-
.btn:active { transform: translateY(1px); }
|
|
164
|
-
.btn:disabled { opacity: .45; cursor: not-allowed; filter: none; }
|
|
165
|
-
.btn--quiet { background: transparent; color: var(--ink); border-color: var(--line); }
|
|
166
|
-
.btn--quiet:hover { background: var(--accent-soft); }
|
|
167
|
-
|
|
168
|
-
.input {
|
|
169
|
-
width: 100%; padding: var(--s3) var(--s4);
|
|
170
|
-
font: var(--text-0) var(--font); color: var(--ink);
|
|
171
|
-
background: var(--surface);
|
|
172
|
-
border: 1px solid var(--line); border-radius: var(--radius-sm);
|
|
195
|
+
cursor: pointer; transition: filter var(--dur-1), transform var(--dur-1) var(--ease-out);
|
|
173
196
|
}
|
|
174
|
-
.
|
|
197
|
+
.btn:hover { filter: brightness(1.07); }
|
|
198
|
+
.btn:active { transform: translateY(1px) scale(.99); }
|
|
199
|
+
.btn:disabled, .btn[aria-busy="true"] { opacity: .5; cursor: not-allowed; filter: none; }
|
|
200
|
+
.btn--quiet { background: transparent; color: var(--ink); border-color: var(--line); }
|
|
201
|
+
.btn--quiet:hover { background: var(--surface-2); }
|
|
175
202
|
```
|
|
176
203
|
|
|
177
|
-
|
|
204
|
+
- **Buttons:** verb labels ("Analyze label", not "Submit"). One primary per
|
|
205
|
+
view. Loading state replaces the label's icon with a spinner and keeps the
|
|
206
|
+
width fixed so nothing jumps.
|
|
207
|
+
- **Inputs:** a visible `<label>` always — a placeholder is not a label. Help
|
|
208
|
+
text below, errors below in `--bad` with an icon, `aria-invalid` and
|
|
209
|
+
`aria-describedby` wired up. Validate on blur, re-validate on input once
|
|
210
|
+
an error is showing, never shout on the first keystroke.
|
|
211
|
+
- **File upload:** a real drop zone *and* a click target, keyboard operable,
|
|
212
|
+
showing accepted types and max size before the user tries. Show the chosen
|
|
213
|
+
file (thumbnail for images), let them replace or remove it, validate type and
|
|
214
|
+
size on the client before sending anything.
|
|
215
|
+
- **Scores and metrics:** the number large, with its scale ("7.4 / 10"), a
|
|
216
|
+
label that says what it measures, and a colour band (good/warn/bad) that is
|
|
217
|
+
never the *only* signal — pair it with a word ("Good", "Moderate", "Poor").
|
|
218
|
+
- **Lists of issues or findings:** most severe first; each with the thing, the
|
|
219
|
+
value, why it matters, in one or two lines. If there are none, say so plainly
|
|
220
|
+
once — do not render an empty section header.
|
|
221
|
+
- **Tables:** right-align numbers, left-align text, tabular numerals, sticky
|
|
222
|
+
header on long tables, row hover, and a real empty state.
|
|
223
|
+
|
|
224
|
+
## 5. States are most of the work
|
|
178
225
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
`filter` and colour only — never on `height` or `width`. Ease out from a state
|
|
182
|
-
that is already visible. Honour `prefers-reduced-motion` every time.
|
|
226
|
+
An interface that only handles the happy path is a mockup. For every screen
|
|
227
|
+
and every async action:
|
|
183
228
|
|
|
184
|
-
|
|
229
|
+
- **Empty / first run** — what this is, and the one action that starts it.
|
|
230
|
+
Illustrated or typographic, never a blank rectangle.
|
|
231
|
+
- **Loading** — skeletons shaped like the real content, or a progress
|
|
232
|
+
indicator on the control that was pressed. Say what is happening if it takes
|
|
233
|
+
more than a second ("Reading the label…"). Never blank the page.
|
|
234
|
+
- **Success** — the result, with a clear next action (try another, share, copy).
|
|
235
|
+
- **Error** — what failed in plain words, what to do next, and the user's input
|
|
236
|
+
preserved. Distinguish "you can fix this" (wrong file type) from "we failed"
|
|
237
|
+
(network, model error) — they need different words and different actions.
|
|
238
|
+
- **Partial** — some parts loaded, one failed; show what worked.
|
|
239
|
+
- **Edge content** — the longest realistic name, a value of zero, a missing
|
|
240
|
+
field, a thousand rows. Design for them, do not discover them.
|
|
241
|
+
|
|
242
|
+
Announce async results to screen readers with an `aria-live="polite"` region.
|
|
243
|
+
|
|
244
|
+
## 6. Motion
|
|
185
245
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
-
|
|
195
|
-
-
|
|
246
|
+
One authored moment, not effects scattered everywhere. For a result screen that
|
|
247
|
+
might be the score counting up and the findings staggering in 40–60ms apart.
|
|
248
|
+
Everything else: 120–200ms, ease-out, on `transform`, `opacity`, `filter` and
|
|
249
|
+
colour only — never animate `width`, `height`, `top` or `left`. Content must
|
|
250
|
+
be readable with motion off; honour `prefers-reduced-motion` every time.
|
|
251
|
+
|
|
252
|
+
## 7. Accessibility and performance — non-negotiable
|
|
253
|
+
|
|
254
|
+
- Contrast: 4.5:1 body text and placeholders, 3:1 large text, icons and control
|
|
255
|
+
borders. `--ink-3` on `--bg` is for hints only.
|
|
256
|
+
- Keyboard: every control reachable in visual order, visible focus ring,
|
|
257
|
+
Escape closes overlays, focus returns to the trigger.
|
|
258
|
+
- Semantics: `<button>` for actions, `<a href>` for navigation, landmarks
|
|
259
|
+
(`header`, `main`, `nav`), one `<h1>`, headings in order.
|
|
260
|
+
- Targets 44×44px on touch. `aria-label` on icon-only buttons. Real `alt` text.
|
|
261
|
+
- Viewport meta, 16px minimum input text on mobile (iOS zooms below that).
|
|
262
|
+
- No layout shift: reserve space for images and async content, set image
|
|
263
|
+
dimensions, use `next/image` in Next.js, `font-display: swap`.
|
|
264
|
+
- Ship less JS: server components by default in Next.js, `"use client"` only on
|
|
265
|
+
the parts that are interactive.
|
|
196
266
|
|
|
197
267
|
## 8. Do not
|
|
198
268
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
- Three identical feature cards
|
|
205
|
-
|
|
206
|
-
- The hero-metric template: big number, small label, three
|
|
207
|
-
- A tracked uppercase eyebrow over every section
|
|
208
|
-
|
|
209
|
-
-
|
|
210
|
-
-
|
|
211
|
-
|
|
212
|
-
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
6. **Contrast.** `--ink-3` on `--bg` is for hints, never for anything that has
|
|
232
|
-
to be read.
|
|
233
|
-
|
|
234
|
-
Then say which of these you actually checked and what you found. Do not claim
|
|
235
|
-
it works on mobile if you never made it narrow. If it runs in a browser, start
|
|
236
|
-
it with `run_command` and `background: true` and open it before you call it
|
|
237
|
-
finished.
|
|
269
|
+
Defaults of the category. A brief can earn any of them; reaching for one
|
|
270
|
+
because it was first to hand means you were not deciding.
|
|
271
|
+
|
|
272
|
+
- Purple-to-blue gradients, gradient text, glassmorphism as decoration,
|
|
273
|
+
glowing blobs in the background.
|
|
274
|
+
- Three identical feature cards; icon + heading + sentence cards as the whole
|
|
275
|
+
page; cards inside cards.
|
|
276
|
+
- The hero-metric template: big number, small label, three stats in a row.
|
|
277
|
+
- A tracked uppercase eyebrow over every section; 01/02/03 section numbers.
|
|
278
|
+
- Emoji as icons. Monospace as a costume. Centred long paragraphs.
|
|
279
|
+
- The default shadcn slate theme, unchanged.
|
|
280
|
+
- Lorem ipsum, "Feature 1", "John Doe", placeholder images. Write the real copy
|
|
281
|
+
— it is part of the design.
|
|
282
|
+
- A modal for anything that does not need to interrupt.
|
|
283
|
+
|
|
284
|
+
## 9. Look at it, then report
|
|
285
|
+
|
|
286
|
+
Run it (`npm run dev` starts in the background and returns the URL) and check:
|
|
287
|
+
|
|
288
|
+
1. **375px wide** — no horizontal scroll, nothing overlapping or clipped. If you
|
|
289
|
+
wrote no responsive rules at all, you have not done this.
|
|
290
|
+
2. **1440px wide** — the content has a max width and does not stretch into
|
|
291
|
+
unreadable lines.
|
|
292
|
+
3. **Longest realistic content** in every label, cell and card.
|
|
293
|
+
4. **Every state** — empty, loading, success, error — reachable and designed.
|
|
294
|
+
5. **Keyboard only** — tab through everything; focus always visible.
|
|
295
|
+
6. **Contrast** of body text, muted text and the accent on its background.
|
|
296
|
+
7. **Hue count** — one accent family plus semantics.
|
|
297
|
+
8. **The first thing the eye lands on** is the thing that matters most.
|
|
298
|
+
|
|
299
|
+
Then say which of these you actually checked and what you found. Never claim it
|
|
300
|
+
works on mobile if you never made it narrow.
|
|
@@ -1,47 +1,85 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: write-tests
|
|
3
|
-
description: Write tests that
|
|
3
|
+
description: Write tests that catch real regressions — the right level for each behaviour, real edge cases, deterministic setups, mocks only at the boundaries, and proof that each test can actually fail.
|
|
4
|
+
auto: write tests, add tests, add a test, unit test, unit tests, test coverage, integration test, e2e test, end to end test, testing, vitest, jest, pytest, playwright
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# Writing tests
|
|
7
8
|
|
|
8
|
-
The test that matters is the one that fails the day someone breaks
|
|
9
|
+
The test that matters is the one that fails the day someone breaks what it
|
|
9
10
|
covers. Every other test is overhead with a green tick on it.
|
|
10
11
|
|
|
11
|
-
##
|
|
12
|
+
## 1. Find how this project tests
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
- Look for the runner and its config before writing anything: `vitest.config.*`,
|
|
15
|
+
`jest.config.*`, `playwright.config.*`, `pytest.ini`/`pyproject.toml`,
|
|
16
|
+
`go test`, the `test` script in `package.json`.
|
|
17
|
+
- Match the existing style: file location (`__tests__/`, `*.test.ts` beside the
|
|
18
|
+
source, `tests/`), naming, helpers, fixtures. Use what is there.
|
|
19
|
+
- If nothing exists, pick the standard for the stack — Vitest for Vite and
|
|
20
|
+
Next.js, pytest for Python, the built-in `go test` — and add the script.
|
|
17
21
|
|
|
18
|
-
##
|
|
22
|
+
## 2. Pick the right level for each behaviour
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
- **Unit** — pure logic: parsing, scoring, validation, formatting, reducers.
|
|
25
|
+
Fast, many, no I/O.
|
|
26
|
+
- **Integration** — a route handler with its validation and error paths, a
|
|
27
|
+
component with its real children, a module against a real temporary
|
|
28
|
+
database or filesystem.
|
|
29
|
+
- **End to end** — the one or two core user journeys, in a real browser
|
|
30
|
+
(Playwright). Few, because they are slow and brittle.
|
|
31
|
+
|
|
32
|
+
Most value per minute is in unit tests of the logic that makes decisions and
|
|
33
|
+
integration tests of the boundaries where data comes in.
|
|
34
|
+
|
|
35
|
+
## 3. Test behaviour, not implementation
|
|
36
|
+
|
|
37
|
+
Assert what a caller can observe — the return value, the rendered output, the
|
|
38
|
+
response, the state afterwards. A test that checks a private helper was called
|
|
39
|
+
breaks on every refactor while proving nothing about the feature.
|
|
40
|
+
|
|
41
|
+
For UI, query the way a user finds things: by role, label and text
|
|
42
|
+
(`getByRole('button', { name: /analyze/i })`), not by class names or test IDs
|
|
43
|
+
unless there is no accessible alternative.
|
|
44
|
+
|
|
45
|
+
## 4. Cover the cases that find bugs
|
|
46
|
+
|
|
47
|
+
For every unit, go through:
|
|
21
48
|
|
|
22
49
|
- empty, one, many
|
|
23
|
-
-
|
|
24
|
-
- null
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
- boundaries: 0, -1, the exact threshold, just over and just under it, the max
|
|
51
|
+
- missing: `null`, `undefined`, missing field, empty string, wrong type
|
|
52
|
+
- malformed input: invalid JSON, a string where a number was expected,
|
|
53
|
+
a reply wrapped in prose
|
|
54
|
+
- failure paths: the dependency throws, times out, returns an error status
|
|
55
|
+
- repetition: the second call, the same input twice, concurrent calls
|
|
56
|
+
|
|
57
|
+
A threshold rule ("sodium over 600mg is flagged") needs a test at 599, 600 and
|
|
58
|
+
601. That is where the off-by-one lives.
|
|
59
|
+
|
|
60
|
+
## 5. Keep tests deterministic and independent
|
|
61
|
+
|
|
62
|
+
- Control time (`vi.useFakeTimers()`, a fixed clock) and randomness (a seed).
|
|
63
|
+
- No real network. Mock at the boundary — the HTTP call, the SDK client — and
|
|
64
|
+
never mock the thing under test.
|
|
65
|
+
- Each test sets up what it needs and cleans up after. No reliance on order.
|
|
66
|
+
- Build test data with small factories so each test states only what matters
|
|
67
|
+
to it.
|
|
68
|
+
- One behaviour per test, named for it:
|
|
69
|
+
`flags sodium when it is over the daily threshold`.
|
|
70
|
+
- Arrange, act, assert — visibly separated.
|
|
71
|
+
- Snapshots only for stable, small output; a 400-line snapshot gets approved
|
|
72
|
+
without being read.
|
|
73
|
+
|
|
74
|
+
## 6. Prove each test can fail
|
|
75
|
+
|
|
76
|
+
Break the code on purpose — flip a comparison, delete a branch — and watch the
|
|
77
|
+
test fail, then restore it. A test that has never failed is a test you have no
|
|
78
|
+
reason to trust. Delete tests that cannot fail (asserting a constant, a mock
|
|
79
|
+
asserting itself).
|
|
80
|
+
|
|
81
|
+
## 7. Run and report
|
|
82
|
+
|
|
83
|
+
Run the whole suite, not only the new file. Report the real numbers — passed,
|
|
84
|
+
failed, skipped — including anything that was already failing before you
|
|
85
|
+
started, and anything you could not cover and why.
|
package/src/core/loop.js
CHANGED
|
@@ -1053,8 +1053,9 @@ export class Agent {
|
|
|
1053
1053
|
.slice(-count);
|
|
1054
1054
|
|
|
1055
1055
|
for (const m of tail) {
|
|
1056
|
-
if (m.role
|
|
1057
|
-
else this.ui.
|
|
1056
|
+
if (m.role !== 'user') this.ui.assistant(m.content);
|
|
1057
|
+
else if (this.ui.userMessage) this.ui.userMessage(m.content);
|
|
1058
|
+
else this.ui.write(`${blue('›')} ${dim(m.content.split('\n')[0])}`);
|
|
1058
1059
|
}
|
|
1059
1060
|
if (tail.length) this.ui.write(dim(' ── picking up here ──\n'));
|
|
1060
1061
|
}
|
package/src/ui/screen.js
CHANGED
|
@@ -240,6 +240,34 @@ export class Screen {
|
|
|
240
240
|
this.render();
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Something the user said, in the conversation, in the same blue box as the
|
|
245
|
+
* input it was typed into.
|
|
246
|
+
*
|
|
247
|
+
* A long session is mostly the agent's output — tool calls, diffs, answers.
|
|
248
|
+
* Your own messages are the landmarks you scroll back looking for, so they
|
|
249
|
+
* get the frame: every one of them is findable at a glance, and the box
|
|
250
|
+
* matches the one below so it is plain where each came from.
|
|
251
|
+
*/
|
|
252
|
+
userMessage(text) {
|
|
253
|
+
const width = this.width();
|
|
254
|
+
const room = Math.max(8, width - 6); // borders, padding, and the caret column
|
|
255
|
+
|
|
256
|
+
const rows = [];
|
|
257
|
+
for (const paragraph of String(text).replace(/\r/g, '').split('\n')) {
|
|
258
|
+
for (const line of wrapAnsi(paragraph, room)) rows.push(line);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.add('');
|
|
262
|
+
this.add(boxTop(width, edge));
|
|
263
|
+
rows.forEach((row, i) => {
|
|
264
|
+
const lead = i === 0 ? blue('›') : ' ';
|
|
265
|
+
this.add(boxRow(` ${lead} ${chalk.white(row)}`, width, edge));
|
|
266
|
+
});
|
|
267
|
+
this.add(boxBottom(width, edge));
|
|
268
|
+
this.render();
|
|
269
|
+
}
|
|
270
|
+
|
|
243
271
|
/**
|
|
244
272
|
* A tool call, as it happens: "● Listing src".
|
|
245
273
|
*
|
|
@@ -894,10 +922,9 @@ export class Screen {
|
|
|
894
922
|
this.historyIndex = -1;
|
|
895
923
|
if (text.trim()) {
|
|
896
924
|
this.history.unshift(text);
|
|
897
|
-
//
|
|
898
|
-
//
|
|
899
|
-
this.
|
|
900
|
-
this.push(`${blue('›')} ${chalk.white(text)}`);
|
|
925
|
+
// Answers to a y/N or a numbered pick are not messages, so they are
|
|
926
|
+
// not echoed: the prompt reports its own outcome.
|
|
927
|
+
if (!this.pendingPrompt) this.userMessage(text);
|
|
901
928
|
}
|
|
902
929
|
this.render();
|
|
903
930
|
this.submit(text);
|