@pro-vi/designer 0.3.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,182 @@
1
+ async function hasSelector(browser, sel) {
2
+ return !!(await browser
3
+ .evalValue(`!!document.querySelector(${JSON.stringify(sel)})`)
4
+ .catch(() => false));
5
+ }
6
+ async function hasButtonMatching(browser, pattern) {
7
+ return !!(await browser
8
+ .evalValue(`(() => { const re = new RegExp(${JSON.stringify(pattern.source)}, ${JSON.stringify(pattern.flags)}); return Array.from(document.querySelectorAll('button')).some(b => re.test((b.textContent || '').trim())); })()`)
9
+ .catch(() => false));
10
+ }
11
+ export const UI_ANCHORS = [
12
+ {
13
+ id: 'home.creator',
14
+ category: 'home',
15
+ description: 'project-creator container',
16
+ requires: 'home',
17
+ check: async (b) => ({ ok: await hasSelector(b, '[data-testid="project-creator"]') })
18
+ },
19
+ {
20
+ id: 'home.nameInput',
21
+ category: 'home',
22
+ description: 'project-name input',
23
+ requires: 'home',
24
+ check: async (b) => ({ ok: await hasSelector(b, 'input[placeholder="Project name"]') })
25
+ },
26
+ {
27
+ id: 'home.wireframeButton',
28
+ category: 'home',
29
+ description: 'Wireframe fidelity button',
30
+ requires: 'home',
31
+ check: async (b) => ({ ok: await hasButtonMatching(b, /^Wireframe/) })
32
+ },
33
+ {
34
+ id: 'home.highFiButton',
35
+ category: 'home',
36
+ description: 'High fidelity button',
37
+ requires: 'home',
38
+ check: async (b) => ({ ok: await hasButtonMatching(b, /^High fidelity/) })
39
+ },
40
+ {
41
+ id: 'home.createButton',
42
+ category: 'home',
43
+ description: 'create-project button',
44
+ requires: 'home',
45
+ check: async (b) => ({ ok: await hasSelector(b, '[data-testid="create-project-button"]') })
46
+ },
47
+ {
48
+ id: 'home.projectsList',
49
+ category: 'home',
50
+ description: 'projects-list container',
51
+ requires: 'home',
52
+ check: async (b) => ({ ok: await hasSelector(b, '[data-testid="projects-list"]') })
53
+ },
54
+ {
55
+ id: 'home.projectCard',
56
+ category: 'home',
57
+ description: 'project-card (at least one)',
58
+ requires: 'home',
59
+ check: async (b) => ({ ok: await hasSelector(b, '[data-testid="project-card"]') })
60
+ },
61
+ {
62
+ id: 'session.promptTextarea',
63
+ category: 'session',
64
+ description: 'chat composer textarea',
65
+ requires: 'session',
66
+ check: async (b) => ({ ok: await hasSelector(b, '[data-testid="chat-composer-input"]') })
67
+ },
68
+ {
69
+ id: 'session.sendButton',
70
+ category: 'session',
71
+ description: 'send button',
72
+ requires: 'session',
73
+ check: async (b) => ({ ok: await hasSelector(b, '[data-testid="chat-send-button"]') })
74
+ },
75
+ {
76
+ id: 'session.htmlViewerIframe',
77
+ category: 'session',
78
+ description: 'html-viewer-iframe (design preview)',
79
+ requires: 'session',
80
+ check: async (b) => ({ ok: await hasSelector(b, '[data-testid="html-viewer-iframe"]') })
81
+ },
82
+ {
83
+ id: 'session.chatMessages',
84
+ category: 'session',
85
+ description: 'chat-messages container',
86
+ requires: 'session',
87
+ check: async (b) => ({ ok: await hasSelector(b, '[data-testid="chat-messages"]') })
88
+ },
89
+ {
90
+ id: 'session.iframeSrcPattern',
91
+ category: 'pattern',
92
+ description: 'iframe src is claudeusercontent.com with signed ?t= token',
93
+ requires: 'session',
94
+ check: async (b) => {
95
+ const src = await b.evalValue(`(() => { const el = document.querySelector('[data-testid="html-viewer-iframe"]'); return (el && el.src) || ''; })()`).catch(() => '');
96
+ if (!src)
97
+ return { ok: false, detail: 'no iframe src (is a file open?)' };
98
+ const ok = /claudeusercontent\.com/.test(src) && /[?&]t=/.test(src);
99
+ return { ok, detail: ok ? undefined : `src=${src.slice(0, 120)}...` };
100
+ }
101
+ },
102
+ {
103
+ id: 'session.chatTurnPrefix',
104
+ category: 'pattern',
105
+ description: "chat turns prefixed with 'You\\n' / 'Claude\\n'",
106
+ requires: 'session',
107
+ check: async (b) => {
108
+ const sample = await b.evalValue(`(() => { const c = document.querySelector('[data-testid="chat-messages"]'); const inner = c && c.children[0]; if (!inner) return ''; return Array.from(inner.children).slice(0, 3).map(d => (d.innerText||'').slice(0, 40)).join('|'); })()`).catch(() => '');
109
+ if (!sample)
110
+ return { ok: false, detail: 'no chat turns' };
111
+ const ok = /(^|\|)(You|Claude)(\\n|\n|$)/.test(sample) || /You\n|Claude\n/.test(sample);
112
+ return { ok, detail: ok ? undefined : `first turns: ${sample.slice(0, 120)}` };
113
+ }
114
+ },
115
+ {
116
+ id: 'share.shareButton',
117
+ category: 'share',
118
+ description: 'Share button (opens the dropdown containing handoff/export actions)',
119
+ requires: 'session',
120
+ check: async (b) => ({ ok: await hasButtonMatching(b, /^Share$/) })
121
+ },
122
+ {
123
+ id: 'share.handoffMenuItem',
124
+ category: 'share',
125
+ description: 'Handoff-to-Claude-Code menu item (inside Share dropdown)',
126
+ requires: 'session',
127
+ check: async (b) => {
128
+ const opened = await b.evalValue(`(() => { const btn = Array.from(document.querySelectorAll('button')).find(x => (x.textContent||'').trim() === 'Share'); if (!btn) return false; btn.click(); return true; })()`).catch(() => false);
129
+ if (!opened)
130
+ return { ok: false, detail: 'Share button not clickable' };
131
+ await new Promise((r) => setTimeout(r, 400));
132
+ const found = await hasButtonMatching(b, /handoff to claude code/i);
133
+ await b.evalValue(`document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); true`).catch(() => null);
134
+ return { ok: found, detail: found ? undefined : 'Share opened but no Handoff-to-Claude-Code item' };
135
+ }
136
+ },
137
+ {
138
+ id: 'pattern.sessionUrl',
139
+ category: 'pattern',
140
+ description: 'session URL matches /design/p/<uuid>',
141
+ requires: 'any',
142
+ check: async (_b, url) => {
143
+ const inSession = /\/design\/p\/[a-f0-9-]+/i.test(url);
144
+ return { ok: inSession || /claude\.ai\/design\/?(\?|$)/.test(url), detail: `url=${url.slice(0, 100)}` };
145
+ }
146
+ },
147
+ {
148
+ id: 'pattern.fileQueryParam',
149
+ category: 'pattern',
150
+ description: '?file=<name> opens a specific file (URL-based file switching)',
151
+ requires: 'session',
152
+ check: async (_b, url) => {
153
+ const ok = /[?&]file=/.test(url);
154
+ return { ok: true, detail: ok ? 'file param present' : '(no file open — not a regression)' };
155
+ }
156
+ }
157
+ ];
158
+ export async function runHealth(browser, opts = {}) {
159
+ const currentUrl = (await browser.url().catch(() => '')) || '';
160
+ const inSession = /\/design\/p\/[a-f0-9-]+/i.test(currentUrl);
161
+ const onHome = /\/design\/?$/.test(currentUrl) || currentUrl.endsWith('/design');
162
+ const state = inSession ? 'session' : onHome ? 'home' : 'other';
163
+ const results = [];
164
+ for (const a of UI_ANCHORS) {
165
+ const base = { id: a.id, category: a.category, description: a.description, requires: a.requires };
166
+ const applicable = a.requires === 'any' ||
167
+ (a.requires === 'home' && state === 'home') ||
168
+ (a.requires === 'session' && state === 'session');
169
+ if (!applicable) {
170
+ results.push({ ...base, status: 'skip', detail: `needs ${a.requires} state; current=${state}` });
171
+ continue;
172
+ }
173
+ try {
174
+ const r = await a.check(browser, currentUrl);
175
+ results.push({ ...base, status: r.ok ? 'ok' : 'fail', detail: r.detail });
176
+ }
177
+ catch (e) {
178
+ results.push({ ...base, status: 'fail', detail: `threw: ${e.message}` });
179
+ }
180
+ }
181
+ return results;
182
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@pro-vi/designer",
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "description": "MCP + CLI for autonomous iteration of claude.ai/design — drives the design surface via agent-browser, downloads handoff bundles, and exposes a tasting harness for full-viewport variant comparison.",
6
+ "license": "MIT",
7
+ "author": "Provi Zhang",
8
+ "homepage": "https://github.com/pro-vi/designer",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/pro-vi/designer.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/pro-vi/designer/issues"
15
+ },
16
+ "keywords": [
17
+ "claude",
18
+ "claude-design",
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "design",
22
+ "agent-browser",
23
+ "anthropic"
24
+ ],
25
+ "bin": {
26
+ "designer": "./bin/designer"
27
+ },
28
+ "scripts": {
29
+ "mcp": "tsx mcp-server.ts",
30
+ "cli": "tsx cli.ts",
31
+ "setup": "tsx cli.ts setup",
32
+ "doctor": "tsx cli.ts doctor",
33
+ "check": "tsc --noEmit",
34
+ "build": "tsc -p tsconfig.build.json",
35
+ "prepublishOnly": "npm run check && npm run build",
36
+ "postinstall": "node -e \"if (!process.env.npm_config_global) process.exit(0); console.log('\\n designer installed. Next: run `designer setup` to launch debug Chrome, sign in, and register the MCP.\\n')\"",
37
+ "probe": "tsx scripts/probe.ts"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "^1.26.0",
41
+ "zod": "^3.23.8"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^25.6.0",
45
+ "tsx": "^4.21.0",
46
+ "typescript": "^6.0.3"
47
+ },
48
+ "engines": {
49
+ "node": ">=20"
50
+ },
51
+ "files": [
52
+ "dist/",
53
+ "bin/",
54
+ "skills/",
55
+ "scripts/designer-chrome.sh",
56
+ "selectors.json",
57
+ "README.md",
58
+ "LICENSE"
59
+ ]
60
+ }
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # Launch a Chrome instance with remote debugging enabled, in a dedicated
3
+ # user-data-dir so the default profile's debug-port lockdown (Chrome 136+)
4
+ # doesn't block us. Sign in to Claude once inside the launched window;
5
+ # the profile persists.
6
+
7
+ set -e
8
+
9
+ PORT="${DESIGNER_CDP:-9222}"
10
+ PROFILE="$HOME/.chrome-designer-profile"
11
+ CHROME="${CHROME_BIN:-/Applications/Google Chrome.app/Contents/MacOS/Google Chrome}"
12
+
13
+ if [ ! -x "$CHROME" ]; then
14
+ echo "[designer-chrome] Chrome not found at: $CHROME" >&2
15
+ echo " Set CHROME_BIN to override." >&2
16
+ exit 1
17
+ fi
18
+
19
+ if curl -fs -o /dev/null "http://127.0.0.1:$PORT/json/version"; then
20
+ echo "[designer-chrome] CDP already listening on port $PORT — nothing to do."
21
+ echo " curl http://127.0.0.1:$PORT/json/version | head"
22
+ exit 0
23
+ fi
24
+
25
+ if pgrep -f "Google Chrome" >/dev/null; then
26
+ echo "[designer-chrome] WARNING: Chrome is already running." >&2
27
+ echo " If it's NOT a debug-mode Chrome, the launched window may not get the debug port." >&2
28
+ echo " Quit existing Chrome (Cmd+Q) first, or accept the risk and continue." >&2
29
+ fi
30
+
31
+ echo "[designer-chrome] Launching: $CHROME --remote-debugging-port=$PORT --user-data-dir=$PROFILE"
32
+ echo "[designer-chrome] Sign in to claude.ai in the new window. Then navigate to https://claude.ai/design."
33
+ echo "[designer-chrome] When done, leave this window open. The CDP server runs as long as Chrome runs."
34
+
35
+ exec "$CHROME" \
36
+ --remote-debugging-port="$PORT" \
37
+ --user-data-dir="$PROFILE" \
38
+ "https://claude.ai/design"
package/selectors.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "_notes": "Captured 2026-04-17 from Chrome 147 via CDP. Override at ~/.designer/selectors.override.json. Prefer data-testid over class names — Claude's styled-components classes (sc-xxx) are regenerated on every build.",
3
+ "_urls": {
4
+ "home": "https://claude.ai/design",
5
+ "session": "https://claude.ai/design/p/{uuid}"
6
+ },
7
+ "login": {
8
+ "signedInIndicator": "[data-testid=\"create-project-button\"]"
9
+ },
10
+ "home": {
11
+ "creator": "[data-testid=\"project-creator\"]",
12
+ "nameInput": "input[placeholder=\"Project name\"]",
13
+ "wireframeButtonText": "Wireframe",
14
+ "highFiButtonText": "High fidelity",
15
+ "createButton": "[data-testid=\"create-project-button\"]",
16
+ "projectsList": "[data-testid=\"projects-list\"]",
17
+ "projectCard": "[data-testid=\"project-card\"]"
18
+ },
19
+ "composer": {
20
+ "promptTextarea": "[data-testid=\"chat-composer-input\"]",
21
+ "sendButton": "[data-testid=\"chat-send-button\"]",
22
+ "stopButton": null,
23
+ "attachButton": "button[aria-label=\"Attach file\"]",
24
+ "modelButton": "[data-testid=\"model-selector-button\"]"
25
+ },
26
+ "preview": {
27
+ "iframeOrContainer": "[data-testid=\"html-viewer-iframe\"]",
28
+ "exportButtonText": "Export",
29
+ "shareButtonText": "Share",
30
+ "emptyStateHeading": "Creations will appear here"
31
+ },
32
+ "messages": {
33
+ "chatMessagesContainer": "[data-testid=\"chat-messages\"]",
34
+ "generatingIndicator": null
35
+ }
36
+ }
@@ -0,0 +1,214 @@
1
+ ---
2
+ name: designer-loop
3
+ description: "Human-participated design iteration loop driven by claude.ai/design via the `designer` MCP. The human is the designer; AI is translation + plumbing. Human states intent, AI reads what exists and relays intent to Claude Design, human tastes the variants Claude produces, AI interprets reactions and iterates. Promotes accepted result with a decision record (the bundle's chat transcript)."
4
+ ---
5
+
6
+ # Designer Loop
7
+
8
+ The human is the designer. Claude Design has taste. The orchestrating agent is translation + plumbing — not a co-designer.
9
+
10
+ Three layers, each with its own job:
11
+
12
+ - **Human**: taste. Describes intent, reacts to variants in their own words.
13
+ - **Claude Design** (via the `designer` MCP): aesthetic judgment. Produces variants, names them, wires tweaks.
14
+ - **Orchestrator** (this agent): translates intent to a minimal faithful prompt, presents what Claude produced, interprets reactions, promotes the result to code.
15
+
16
+ Don't muddle the three.
17
+
18
+ ### When NOT to invoke this skill
19
+
20
+ For trivial mechanical changes — single token, single value, no feeling-shaped question ("set `--border-radius: 12px`", "make the bg darker by 1 stop", "add 8px of padding here") — just make the change directly. Don't boot the MCP. Don't propose variants. Show the diff and move on.
21
+
22
+ This skill is for **feeling-shaped, exploratory, or multi-dimensional** design work.
23
+
24
+ ## The Loop
25
+
26
+ ```
27
+ 1. Intent → Human describes what they want to feel/change (not specific values)
28
+ 2. Read → Agent calls designer_session; reads adjacent code/tokens if relevant
29
+ 3. Relay → Agent translates intent into a minimal prompt; sends via designer_prompt
30
+ 4. Taste → Human reacts to the variants Claude produced (in the tasting harness)
31
+ 5. Interpret → Agent translates reaction into next prompt or promotion
32
+ 6. Repeat 3-5 → Until human says "that's it"
33
+ 7. Promote → designer_handoff; bundle's chat transcript is the decision record
34
+ ```
35
+
36
+ ## Phase 1: Intent
37
+
38
+ **The human speaks in feelings, not values.**
39
+
40
+ Good intent: "The sidebar feels heavy — I want it to recede." "I want an intake screen that doesn't feel clinical."
41
+
42
+ Bad intent (if it appears): `--border-radius: 12px` — already a solution, not this skill's job.
43
+
44
+ **Interview only when intent is genuinely unclear, and only about scope — never about aesthetics.**
45
+
46
+ Scope questions (yours to ask): what data is rendered, what the primary user action is, what failure modes must be visible, what the adjacent screens are, what must NOT change.
47
+
48
+ Aesthetic questions belong to Claude Design, not you: do not ask about palette, type, layout direction, tone, hierarchy, spacing style.
49
+
50
+ ## Phase 2: Read the Room
51
+
52
+ Orient in the claude.ai/design surface:
53
+
54
+ 1. `designer_session({ key })` — returns stored state + `availableFiles`. If `stored.designUrl` exists, you're resuming; otherwise create one with a sensible default name derived from the intent (don't interview for a project name).
55
+ 2. For existing projects: `designer_snapshot({ filename })` per file of interest. You get `htmlPath` — read it only if deep inspection is warranted.
56
+ 3. If this is a frontend for an existing backend or codebase, read the backend's API shape and any existing design tokens in the target repo BEFORE prompting. Those are constraints to thread into the prompt verbatim, not aesthetic hypotheses to propose.
57
+
58
+ A one-line brief is fine ("I see X; here's the prompt I'm about to send"). Don't turn it into design-by-interview.
59
+
60
+ ## Phase 3: Relay
61
+
62
+ Claude Design has taste. **Your job is to translate the human's intent into a minimal faithful prompt and let Claude's taste work.**
63
+
64
+ Guide, don't constrain. The prompt gives Claude enough to make good decisions, not pre-makes them:
65
+
66
+ | Guide (include) | Constrain (omit) |
67
+ |---|---|
68
+ | What the product does / data it renders | Color palette (unless it's a hard brand token) |
69
+ | User's situation / primary action | Type treatment, font feel |
70
+ | Entities, field names, copy that must appear | Layout direction, whitespace rules |
71
+ | Adjacent surfaces / what must NOT change | Tone adjectives ("contemplative", "trustworthy") unless paired with a concrete lever |
72
+ | Hard brand tokens (palette, type, existing component names) as non-negotiables | Visual hierarchy choices |
73
+ | Quantity + shape of variants ("3 full-page files", "20 on a wrapping grid") | Variant names (let Claude pick) |
74
+
75
+ Do not:
76
+
77
+ - Workshop aesthetic direction in chat before sending.
78
+ - Propose variants of your own to the human. Claude Design proposes; you relay.
79
+ - Interview about taste. Scope questions only — see Phase 1.
80
+
81
+ ### Prompt discipline
82
+
83
+ - **Short and concrete over long and tasteful.** Not a hard ceiling, but over-specification is the most common failure mode.
84
+ - **Avoid ungrounded vibe adjectives.** Vibe words without a concrete surface fight Claude's style coherence. Vibe word + concrete lever is fine; vibe essay ahead of the brief is not.
85
+ - **Lock brand explicitly.** Palette, type, component names — say them. Don't hope Claude guesses.
86
+ - **Ask for tweaks** on the dimensions you expect to iterate (type, spacing, palette). Claude wires live sliders.
87
+ - **Quantities help.** "N variations", "N loaders on a grid", "N-screen onboarding."
88
+ - **Flat file layout for multi-file variants.** Tell Claude "all files at project root, no subfolders." Claude tends to organize variants under a folder (`directions/`, `variants/`) which the current MCP's file list doesn't see through — use the handoff bundle if you need nested layouts.
89
+
90
+ ### Picking the right artifact shape
91
+
92
+ The split is not canvas-vs-files. The split is **what's the unit of critique?** — pick generation AND evaluation from that.
93
+
94
+ | Unit of critique | What to generate | How to evaluate |
95
+ |---|---|---|
96
+ | Alternative treatments of one view | Canvas grid OR separate full-page files, based on scale (see below) | Canvas works for ≤300×300 cells; otherwise full-viewport with a switcher |
97
+ | A journey across screens | One routed prototype with the sequence in context | Storyboard canvas — each frame is a screen in the flow |
98
+ | Named interactive states (empty / loading / error / success) | One interactive prototype with the states as toggles | Tweak system or state buttons |
99
+ | Scroll rhythm / long-form pacing | One scrollable artifact. Don't multiply it | Scroll at real width |
100
+ | A system / shipped artifact | Routed prototype, zip, or handoff to code | `designer_handoff` |
101
+
102
+ Heuristic threshold: once **screens × meaningful states × breakpoints > ~8–10**, stop multiplying variants. Converge to one routed prototype or structured handoff.
103
+
104
+ ### Canvas at what scale
105
+
106
+ Two sub-modes:
107
+
108
+ - **Grid canvas** — many small cells, each ≤300×300. For widgets where variants read at thumbnail scale.
109
+ - **Storyboard canvas** — a few frames at real device size, arranged as a sequence. For flows.
110
+
111
+ Variants of a full-viewport surface are neither. Grid canvas mangles them (type scale, whitespace, hierarchy only read at real size). Storyboard doesn't apply. Right shape: separate full-page HTML files viewed at real size — usually via Claude's own `index.html` gallery opened at `currentUrl`. Fall back to `designer tasting --key <name>` (local full-viewport switcher) only if the IDE chrome is distracting judgment.
112
+
113
+ ### Naming variants vs locking brand
114
+
115
+ - **Brand tokens** (palette, type, component names, product language) — specify explicitly.
116
+ - **Variant names** (what each disposable exploratory branch is called) — let Claude name them from the problem domain. Exception: if the human already has review-friendly labels in mind ("single-page / wizard / dense ops"), use those.
117
+
118
+ ### Tool sequence
119
+
120
+ Common loop for any variant shape:
121
+
122
+ 1. `designer_session({ key, action: 'create', name, fidelity: 'highfi' })`.
123
+ 2. `designer_prompt({ prompt })` — terse, let-Claude-name. The MCP auto-appends a flat-file-layout instruction; if you want subfolders you must explicitly override in your prompt.
124
+ 3. **Hand the human the URL** — `designer_session({ key, action: 'status' }).currentUrl`, or the URL echoed in the prompt result. The URL is the live design in Claude's IDE: all tweak sliders work, variant switcher works, fully interactive. This is the default taste path.
125
+ 4. Repeat 2-3 as they react.
126
+ 5. When they say "that's it": `designer_handoff({ key })` — tar.gz bundle with all variants + README + chat transcript. This is non-optional; it's what the implementing agent needs to build the real thing.
127
+
128
+ ### When `designer_list({ scope: 'files' })` says `authoritative: false`
129
+
130
+ Claude Design sometimes organizes generated files under folders (`directions/`, `variants/`, `shared/`). The live file-list scrape only sees top-level files + folder names, never contents. When the result includes `folders: […]` and `authoritative: false`, the list is incomplete. Fall back to `designer_handoff` — the tar.gz bundle is always folder-aware.
131
+
132
+ ### When the URL isn't enough — full-viewport tasting
133
+
134
+ The URL shows Claude's IDE (chat panel + project tree + toolbar eating space). That's fine for most taste judgments, but breaks down when:
135
+
136
+ - You're comparing multiple screen-level variants and the IDE chrome is stealing viewport space the designs need.
137
+ - Claude didn't build an index.html switcher, so the URL only lets you see one variant at a time (navigating via `?file=`).
138
+ - You need to judge the design at real app scale with no "is this just Claude wrapping?" distraction.
139
+
140
+ In those cases, after a handoff: `designer tasting --key <name>` — builds a local full-viewport switcher over the bundle, serves on `127.0.0.1`, opens the browser. Keyboard 1/N switches. Notes field persists in localStorage.
141
+
142
+ Don't default to tasting. Reach for it when the URL framing is getting in the way.
143
+
144
+ ### Canvas-variant shape (compact widgets)
145
+
146
+ When the prompt is for a grid of small things ("20 loaders on a wrapping grid"), Claude generates one HTML file with everything on a canvas. The URL shows this perfectly — no tasting harness needed. `designer_handoff` at the end for code promotion.
147
+
148
+ ## Phase 4: Taste
149
+
150
+ Show the human the URL first. Only reach for the tasting harness when full-viewport matters more than interactivity.
151
+
152
+ **The human reacts in their own language.** Don't ask "accept or reject?" — ask "what do you think?"
153
+
154
+ Messy reactions are the point:
155
+ - "A is closer but still too stark"
156
+ - "B feels right but the gap is too much"
157
+ - "Neither — what if we kept the border but made it almost invisible?"
158
+ - "Yes. That's it."
159
+
160
+ The human can tune live via the tweak sliders Claude wired into the canvas. Those tweak positions are themselves a reaction — capture them.
161
+
162
+ ## Phase 5: Interpret
163
+
164
+ Translate reaction into the next move:
165
+
166
+ - **"Too X"** → prompt for variants that move away from X.
167
+ - **"Almost"** → narrow range, smaller adjustments.
168
+ - **"What if..."** → human is designing now — execute their idea as the next prompt.
169
+ - **"Yes"** → promote (Phase 7).
170
+ - **Silence / uncertainty** → open the tasting harness, ask what's bothering them.
171
+ - **Ambiguous** → before re-prompting, consider `designer_ask({ prompt })` to *consult* Claude: "given the human said X, what small change would address that?" — cheap (~15-30s text reply) and often surfaces the right adjustment.
172
+
173
+ Never override with "but best practice says..." — capture the tension in the decision record instead.
174
+
175
+ ## Phase 6: Promote
176
+
177
+ 1. `designer_handoff({ key, openFile: <chosen variant> })` — downloads the tar.gz bundle under `./artifacts/{key}/handoff-{ts}/`. The bundle is the decision record:
178
+ - `README.md` — handoff protocol for the implementing agent
179
+ - `chats/chat1.md` — full transcript (every prompt + reply, verbatim)
180
+ - `project/*.html`, `*.jsx`, `*.css` — all design files including the chosen variant
181
+ 2. If this is a frontend for an existing codebase, the implementing agent (this one, or Claude Code downstream) reads the bundle's README + chat transcript first, then translates the chosen variant into real code in the target repo.
182
+ 3. Append to the codebase's design decision log with a short entry citing the bundle path + the human's final reaction verbatim.
183
+
184
+ ## Guardrails
185
+
186
+ - **Read before relaying** (Phase 2). Call `designer_session` first — it returns `availableFiles`.
187
+ - **Guide, don't constrain.** Scope + data + hard brand tokens in; aesthetic choices out.
188
+ - **Lock brand explicitly.** Claude won't guess your palette.
189
+ - **Let Claude name variants** from the problem domain.
190
+ - **Capture feedback verbatim.** The bundle's chat transcript is the record — don't sanitize.
191
+ - **Direct values execute.** If the human says `--border-radius: 12px` mid-loop, just do it (don't even invoke Claude Design for that). Still note the intent.
192
+ - **Promote only after explicit "yes"** — "almost" is not "yes."
193
+
194
+ ## Anti-Patterns
195
+
196
+ - **Interviewing about aesthetics.** Scope questions are fine when intent is genuinely unclear; taste questions aren't yours to ask.
197
+ - **Proposing variants of your own.** Claude Design proposes. You relay.
198
+ - **Constraining where Claude should have room.** Brand tokens are constraints; palette feelings and layout hunches aren't.
199
+ - **Ungrounded vibe essays.** Vibe words without a concrete surface to attach to fight Claude's style coherence.
200
+ - **Variant grid for full-screen experiences.** Shrinking an intake/onboarding/dashboard into a 400px cell loses hierarchy, type scale, whitespace. Screen-level variants → separate files + tasting harness.
201
+ - **Separate files for compact widgets.** 20 loading indicators don't each need their own file. Grid canvas.
202
+ - **Variant grid for journeys/states/systems.** Multiplying screens by variants blows up fast. Converge to one routed prototype.
203
+ - **Auto-handoff.** Don't promote on every iteration. Handoff is Phase 7, not a sub-verb of `designer_prompt`.
204
+ - **Invoking this skill for mechanical changes.** Single-token, single-value, no-feeling changes don't need the loop. Just edit.
205
+
206
+ ## When the MCP is not available
207
+
208
+ Check first: `designer_*` tools should appear in the tool list. If they don't:
209
+
210
+ 1. Ask the human whether they've installed the `designer` package. If yes, the MCP may have disconnected — `claude mcp list` will show the status.
211
+ 2. If they haven't set it up: `cd ~/Development/_projs/designer && ./bin/designer setup` (or after publish: `npm i -g @pro-vi/designer && designer setup`).
212
+ 3. If setup isn't possible in this session, tell the human the skill can't run and fall back to making the change directly (no variant exploration).
213
+
214
+ The MCP auto-launches debug Chrome on first tool call if the dedicated profile exists. Re-runs of `designer setup` are idempotent.