@pro-vi/designer 0.3.3 → 0.3.5
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/dist/browser.js +12 -1
- package/dist/cli.js +7 -4
- package/dist/designer-controller.js +70 -16
- package/dist/mcp-server.js +6 -2
- package/dist/scripts/probe.js +14 -3
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
const BIN = process.env.DESIGNER_AGENT_BROWSER_BIN || 'agent-browser';
|
|
3
3
|
const DEFAULT_SESSION = process.env.DESIGNER_SESSION_NAME || 'designer';
|
|
4
|
-
const CDP = process.env.DESIGNER_CDP
|
|
4
|
+
const CDP = process.env.DESIGNER_CDP ?? '9222';
|
|
5
5
|
export function createBrowser({ session = DEFAULT_SESSION, headed = true, timeoutMs = 30_000, cdp = CDP } = {}) {
|
|
6
6
|
const baseEnv = {
|
|
7
7
|
...process.env,
|
|
@@ -53,6 +53,17 @@ export function createBrowser({ session = DEFAULT_SESSION, headed = true, timeou
|
|
|
53
53
|
close: () => run(['close']).catch(() => null),
|
|
54
54
|
url: () => run(['get', 'url']),
|
|
55
55
|
title: () => run(['get', 'title']),
|
|
56
|
+
tabs: async () => {
|
|
57
|
+
const out = await run(['tab', 'list', '--json']);
|
|
58
|
+
const env = JSON.parse(out);
|
|
59
|
+
if (env.success === false) {
|
|
60
|
+
throw new Error(`agent-browser tab list failed: ${JSON.stringify(env.error)}`);
|
|
61
|
+
}
|
|
62
|
+
return env.data?.tabs ?? [];
|
|
63
|
+
},
|
|
64
|
+
activateTab: async (index) => {
|
|
65
|
+
await run(['tab', String(index)]);
|
|
66
|
+
},
|
|
56
67
|
snapshot: ({ interactive = true, scope } = {}) => {
|
|
57
68
|
const args = ['snapshot', '--json'];
|
|
58
69
|
if (interactive)
|
package/dist/cli.js
CHANGED
|
@@ -66,7 +66,8 @@ async function main() {
|
|
|
66
66
|
const res = await c.iterate(prompt, {
|
|
67
67
|
file: flags.file,
|
|
68
68
|
timeoutMs: flags.timeoutMs ? Number(flags.timeoutMs) : undefined,
|
|
69
|
-
stabilityMs: flags.stabilityMs ? Number(flags.stabilityMs) : undefined
|
|
69
|
+
stabilityMs: flags.stabilityMs ? Number(flags.stabilityMs) : undefined,
|
|
70
|
+
decisive: Boolean(flags.decisive)
|
|
70
71
|
});
|
|
71
72
|
if (res.url)
|
|
72
73
|
console.log(`\nTaste here: ${res.url}\n`);
|
|
@@ -343,6 +344,7 @@ Flags:
|
|
|
343
344
|
--file <f.html> switch to this file before prompting
|
|
344
345
|
--timeoutMs <n> default 20 minutes
|
|
345
346
|
--stabilityMs <n> default 4 seconds
|
|
347
|
+
--decisive tell Claude not to stop on clarifying questions; pick defaults and proceed
|
|
346
348
|
|
|
347
349
|
Output: prints 'Taste here: <url>' then JSON metadata (done, newFiles, htmlPath, screenshotPath,
|
|
348
350
|
htmlHash, chatReply). HTML is written to disk (read htmlPath if needed); it's not inline.
|
|
@@ -350,6 +352,10 @@ htmlHash, chatReply). HTML is written to disk (read htmlPath if needed); it's no
|
|
|
350
352
|
Auto-appended to every prompt: 'Keep all generated files at the project root; no subfolders.'
|
|
351
353
|
Override by explicitly contradicting it in your prompt text.
|
|
352
354
|
|
|
355
|
+
With --decisive, also append: 'If you would otherwise stop to ask clarifying questions, do not.
|
|
356
|
+
Choose the most defensible answer for each axis yourself and proceed.' Use when you want Claude
|
|
357
|
+
to commit to a direction instead of blocking on the clarifying-questions affordance.
|
|
358
|
+
|
|
353
359
|
Examples:
|
|
354
360
|
designer prompt "add a Remember-me checkbox" --key feat-x
|
|
355
361
|
designer prompt --prompt-file ./brief.md --key feat-x
|
|
@@ -517,9 +523,6 @@ async function checkAgentBrowser() {
|
|
|
517
523
|
}
|
|
518
524
|
async function checkCdp() {
|
|
519
525
|
const port = process.env.DESIGNER_CDP || '9222';
|
|
520
|
-
if (!process.env.DESIGNER_CDP) {
|
|
521
|
-
return { name: `CDP at port ${port}`, status: 'warn', detail: 'DESIGNER_CDP not set; defaulting to 9222. export DESIGNER_CDP=9222 to silence.' };
|
|
522
|
-
}
|
|
523
526
|
try {
|
|
524
527
|
const res = await fetch(`http://127.0.0.1:${port}/json/version`);
|
|
525
528
|
if (!res.ok)
|
|
@@ -10,6 +10,7 @@ import { REPO_ROOT } from "./repo-root.js";
|
|
|
10
10
|
import { ensureCdpUp } from "./cdp-ensure.js";
|
|
11
11
|
const DESIGN_HOME = 'https://claude.ai/design';
|
|
12
12
|
const FLAT_LAYOUT_SUFFIX = '\n\nFile layout: keep all generated files at the project root. No subfolders.';
|
|
13
|
+
const DECISIVE_SUFFIX = '\n\nIf you would otherwise stop to ask clarifying questions, do not. Choose the most defensible answer for each axis yourself and proceed. Note your assumption in a one-line `<!-- assumed: ... -->` comment at the top of the relevant file so I can override on the next turn.';
|
|
13
14
|
function loadSelectors() {
|
|
14
15
|
const base = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'selectors.json'), 'utf8'));
|
|
15
16
|
const overridePath = path.join(os.homedir(), '.designer', 'selectors.override.json');
|
|
@@ -59,15 +60,26 @@ export class DesignerController {
|
|
|
59
60
|
const url = await this.currentUrl();
|
|
60
61
|
const inSession = /\/design\/p\/[a-f0-9-]+/i.test(url);
|
|
61
62
|
const availableFiles = inSession ? await this.listFiles().catch(() => []) : [];
|
|
63
|
+
const awaitingClarification = inSession ? await this.detectAwaitingClarification() : false;
|
|
62
64
|
return {
|
|
63
65
|
key: this.key,
|
|
64
66
|
stored,
|
|
65
67
|
currentUrl: url,
|
|
66
68
|
inSession,
|
|
67
69
|
onHome: /\/design\/?$/.test(url) || url.endsWith('/design'),
|
|
68
|
-
availableFiles
|
|
70
|
+
availableFiles,
|
|
71
|
+
awaitingClarification
|
|
69
72
|
};
|
|
70
73
|
}
|
|
74
|
+
async detectAwaitingClarification() {
|
|
75
|
+
const turns = await this.getChatTurns().catch(() => []);
|
|
76
|
+
if (turns.length === 0)
|
|
77
|
+
return false;
|
|
78
|
+
const last = turns[turns.length - 1];
|
|
79
|
+
if (!last || last.role !== 'assistant')
|
|
80
|
+
return false;
|
|
81
|
+
return /Claude has some questions/i.test(last.text);
|
|
82
|
+
}
|
|
71
83
|
async session({ action = 'status', name, fidelity = 'wireframe' } = {}) {
|
|
72
84
|
if (action === 'status')
|
|
73
85
|
return this.getStatus();
|
|
@@ -90,19 +102,55 @@ export class DesignerController {
|
|
|
90
102
|
}
|
|
91
103
|
throw new Error(`Unknown action: ${action}`);
|
|
92
104
|
}
|
|
105
|
+
async selectMatchingTab() {
|
|
106
|
+
const tabs = await this.browser.tabs().catch(() => []);
|
|
107
|
+
if (tabs.length === 0)
|
|
108
|
+
return { matched: false, candidates: 0 };
|
|
109
|
+
const stored = getSession(this.key);
|
|
110
|
+
const targetRoot = stored?.designUrl?.split('?')[0];
|
|
111
|
+
const candidates = tabs.filter((t) => {
|
|
112
|
+
if (t.type !== 'page' || !t.url)
|
|
113
|
+
return false;
|
|
114
|
+
if (targetRoot)
|
|
115
|
+
return t.url.startsWith(targetRoot);
|
|
116
|
+
return /^https:\/\/claude\.ai\/design(\/|$|\?)/.test(t.url);
|
|
117
|
+
});
|
|
118
|
+
if (candidates.length === 0)
|
|
119
|
+
return { matched: false, candidates: 0 };
|
|
120
|
+
candidates.sort((a, b) => (Number(b.active) - Number(a.active)) || (a.index - b.index));
|
|
121
|
+
for (const cand of candidates) {
|
|
122
|
+
await this.browser.activateTab(cand.index).catch(() => null);
|
|
123
|
+
const composerOk = await this.browser.isVisible(this.selectors.composer.promptTextarea).catch(() => false);
|
|
124
|
+
const homeOk = this.selectors.login.signedInIndicator
|
|
125
|
+
? await this.browser.isVisible(this.selectors.login.signedInIndicator).catch(() => false)
|
|
126
|
+
: false;
|
|
127
|
+
if (composerOk || homeOk) {
|
|
128
|
+
upsertSession(this.key, { lastUrl: await this.currentUrl() });
|
|
129
|
+
return { matched: true, candidates: candidates.length };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { matched: false, candidates: candidates.length };
|
|
133
|
+
}
|
|
93
134
|
async ensureReady() {
|
|
94
135
|
await ensureCdpUp();
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
await this.
|
|
98
|
-
|
|
136
|
+
const picked = await this.selectMatchingTab();
|
|
137
|
+
if (picked.matched) {
|
|
138
|
+
return { ok: true, url: await this.currentUrl(), inSession: await this.isInSession() };
|
|
139
|
+
}
|
|
140
|
+
if (picked.candidates === 0) {
|
|
141
|
+
const u = await this.currentUrl();
|
|
142
|
+
if (!/claude\.ai\/design/.test(u)) {
|
|
143
|
+
await this.browser.open(DESIGN_HOME);
|
|
144
|
+
await this.browser.waitLoad('networkidle').catch(() => null);
|
|
145
|
+
}
|
|
99
146
|
}
|
|
100
147
|
const homeOk = this.selectors.login.signedInIndicator
|
|
101
148
|
? await this.browser.isVisible(this.selectors.login.signedInIndicator).catch(() => false)
|
|
102
149
|
: false;
|
|
103
150
|
const sessionOk = await this.browser.isVisible(this.selectors.composer.promptTextarea).catch(() => false);
|
|
104
151
|
if (!homeOk && !sessionOk) {
|
|
105
|
-
|
|
152
|
+
const suffix = picked.candidates > 0 ? ` (checked ${picked.candidates} tab(s))` : '';
|
|
153
|
+
throw new Error(`Not signed in to claude.ai/design, or on an unrecognized page${suffix}. Sign in in the CDP-attached Chrome.`);
|
|
106
154
|
}
|
|
107
155
|
upsertSession(this.key, { lastUrl: await this.currentUrl() });
|
|
108
156
|
return { ok: true, url: await this.currentUrl(), inSession: await this.isInSession() };
|
|
@@ -168,12 +216,13 @@ export class DesignerController {
|
|
|
168
216
|
return true;
|
|
169
217
|
})()`);
|
|
170
218
|
}
|
|
171
|
-
async sendPrompt(prompt) {
|
|
219
|
+
async sendPrompt(prompt, { decisive = false } = {}) {
|
|
172
220
|
const before = await this.fetchServedHtml();
|
|
173
221
|
this._preSendHtml = before.html;
|
|
174
|
-
const effective = prompt + FLAT_LAYOUT_SUFFIX;
|
|
222
|
+
const effective = prompt + FLAT_LAYOUT_SUFFIX + (decisive ? DECISIVE_SUFFIX : '');
|
|
175
223
|
await this._submitPrompt(effective);
|
|
176
|
-
|
|
224
|
+
const suffixApplied = decisive ? 'flat_layout+decisive' : 'flat_layout';
|
|
225
|
+
appendHistory(this.key, { kind: 'prompt', prompt, suffixApplied });
|
|
177
226
|
return { ok: true };
|
|
178
227
|
}
|
|
179
228
|
async waitForGenerationDone({ timeoutMs = 20 * 60_000, stabilityMs = 4000, pollMs = 1500 } = {}) {
|
|
@@ -238,13 +287,13 @@ export class DesignerController {
|
|
|
238
287
|
throw new Error(`No active session for key=${this.key}. Call createSession first.`);
|
|
239
288
|
await this.resumeSession();
|
|
240
289
|
}
|
|
241
|
-
async iterate(prompt, { file, timeoutMs, stabilityMs } = {}) {
|
|
290
|
+
async iterate(prompt, { file, timeoutMs, stabilityMs, decisive } = {}) {
|
|
242
291
|
await this._ensureInSession();
|
|
243
292
|
if (file)
|
|
244
293
|
await this.openFile(file);
|
|
245
294
|
const preFiles = await this.listFiles().catch(() => []);
|
|
246
295
|
const preChatCount = (await this.getChatTurns()).length;
|
|
247
|
-
await this.sendPrompt(prompt);
|
|
296
|
+
await this.sendPrompt(prompt, { decisive });
|
|
248
297
|
const done = await this.waitForGenerationDone({ timeoutMs, stabilityMs });
|
|
249
298
|
const postFiles = await this.listFiles().catch(() => []);
|
|
250
299
|
const postTurns = await this.getChatTurns();
|
|
@@ -334,10 +383,12 @@ export class DesignerController {
|
|
|
334
383
|
// scraping misses them; text-node walking is resilient.
|
|
335
384
|
const seen = new Set();
|
|
336
385
|
const files = [];
|
|
386
|
+
let designFilesLabelVisible = false;
|
|
337
387
|
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
338
388
|
let node;
|
|
339
389
|
while ((node = walker.nextNode())) {
|
|
340
390
|
const t = (node.textContent || '').trim();
|
|
391
|
+
if (t === 'Design Files') designFilesLabelVisible = true;
|
|
341
392
|
if (!/^[A-Za-z0-9 _.()\\-]+\\.(html|js|css|jsx|tsx|ts|md|json|svg)$/i.test(t)) continue;
|
|
342
393
|
if (t.length > 80 || seen.has(t)) continue;
|
|
343
394
|
seen.add(t);
|
|
@@ -355,12 +406,15 @@ export class DesignerController {
|
|
|
355
406
|
folderSet.add(lines[0]);
|
|
356
407
|
}
|
|
357
408
|
}
|
|
358
|
-
return { files, folders: Array.from(folderSet) };
|
|
359
|
-
})()`).catch(() => ({ files: [], folders: [] }));
|
|
409
|
+
return { files, folders: Array.from(folderSet), designFilesLabelVisible };
|
|
410
|
+
})()`).catch(() => ({ files: [], folders: [], designFilesLabelVisible: false }));
|
|
411
|
+
const files = Array.isArray(result.files) ? result.files : [];
|
|
412
|
+
const folders = Array.isArray(result.folders) ? result.folders : [];
|
|
413
|
+
const emptyButLabelVisible = files.length === 0 && result.designFilesLabelVisible === true;
|
|
360
414
|
return {
|
|
361
|
-
files
|
|
362
|
-
folders
|
|
363
|
-
authoritative:
|
|
415
|
+
files,
|
|
416
|
+
folders,
|
|
417
|
+
authoritative: !emptyButLabelVisible && folders.length === 0
|
|
364
418
|
};
|
|
365
419
|
}
|
|
366
420
|
async openFile(filename) {
|
package/dist/mcp-server.js
CHANGED
|
@@ -37,9 +37,13 @@ server.registerTool('designer_prompt', {
|
|
|
37
37
|
prompt: z.string(),
|
|
38
38
|
file: z.string().optional().describe('Switch to this file before sending (targets the prompt at it).'),
|
|
39
39
|
timeoutMs: z.number().optional().describe('Default 20m. Hi-fi generations can take 15+ min; bump this for complex multi-variant prompts.'),
|
|
40
|
-
stabilityMs: z.number().optional().describe('Default 4s.')
|
|
40
|
+
stabilityMs: z.number().optional().describe('Default 4s.'),
|
|
41
|
+
decisive: z
|
|
42
|
+
.boolean()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Append a 'do not stop to ask clarifying questions' instruction. Use when you want Claude to commit to a direction (pick defensible defaults itself, document the assumption inline) instead of blocking on the ephemeral clarifying-questions affordance — which disappears on refresh and has no stable DOM contract to scrape.")
|
|
41
45
|
}
|
|
42
|
-
}, async ({ key, prompt, file, timeoutMs, stabilityMs }) => textResult(await getController(key).iterate(prompt, { file, timeoutMs, stabilityMs })));
|
|
46
|
+
}, async ({ key, prompt, file, timeoutMs, stabilityMs, decisive }) => textResult(await getController(key).iterate(prompt, { file, timeoutMs, stabilityMs, decisive })));
|
|
43
47
|
server.registerTool('designer_ask', {
|
|
44
48
|
description: "Q&A with the design assistant — text-only, doesn't change any file. Use for 'why did you choose X?', 'compare A vs B', 'suggest 3 alternatives before I commit'. Returns the assistant's reply.",
|
|
45
49
|
inputSchema: {
|
package/dist/scripts/probe.js
CHANGED
|
@@ -3,9 +3,6 @@ import { createBrowser } from "../browser.js";
|
|
|
3
3
|
const cmd = process.argv[2];
|
|
4
4
|
const arg = process.argv[3];
|
|
5
5
|
const browser = createBrowser({ headed: true });
|
|
6
|
-
if (!process.env.DESIGNER_CDP) {
|
|
7
|
-
console.error('[probe] DESIGNER_CDP not set — using agent-browser-managed session (may be blocked by Cloudflare/SSO). Prefer: export DESIGNER_CDP=9222 and relaunch Chrome with --remote-debugging-port=9222.');
|
|
8
|
-
}
|
|
9
6
|
async function main() {
|
|
10
7
|
switch (cmd) {
|
|
11
8
|
case 'login':
|
|
@@ -43,12 +40,26 @@ async function main() {
|
|
|
43
40
|
case 'close':
|
|
44
41
|
await browser.close();
|
|
45
42
|
break;
|
|
43
|
+
case 'tabs': {
|
|
44
|
+
const tabs = await browser.tabs();
|
|
45
|
+
const composer = '[data-testid="chat-composer-input"]';
|
|
46
|
+
const signedIn = '[data-testid="create-project-button"]';
|
|
47
|
+
for (const t of tabs) {
|
|
48
|
+
await browser.activateTab(t.index).catch(() => null);
|
|
49
|
+
const composerOk = await browser.isVisible(composer).catch(() => false);
|
|
50
|
+
const signedInOk = await browser.isVisible(signedIn).catch(() => false);
|
|
51
|
+
const flag = composerOk ? 'composer' : signedInOk ? 'home' : 'unrecognized';
|
|
52
|
+
console.log(`[${t.index}] active=${t.active ? 'Y' : 'N'} ${flag.padEnd(12)} ${t.url}`);
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
46
56
|
default:
|
|
47
57
|
console.log(`Usage:
|
|
48
58
|
probe.ts login open headed window for manual login
|
|
49
59
|
probe.ts open <url> navigate
|
|
50
60
|
probe.ts url print current url
|
|
51
61
|
probe.ts title print current title
|
|
62
|
+
probe.ts tabs list CDP tabs with readiness verdict
|
|
52
63
|
probe.ts snapshot [scope] interactive a11y tree (text)
|
|
53
64
|
probe.ts snapshot-json [scope] interactive a11y tree (JSON)
|
|
54
65
|
probe.ts screenshot [path] full-page screenshot
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pro-vi/designer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"type": "module",
|
|
5
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
6
|
"license": "MIT",
|