@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 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 u = await this.currentUrl();
96
- if (!/claude\.ai\/design/.test(u)) {
97
- await this.browser.open(DESIGN_HOME);
98
- await this.browser.waitLoad('networkidle').catch(() => null);
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
- throw new Error('Not signed in to claude.ai/design, or on an unrecognized page. Sign in in the CDP-attached Chrome.');
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
- appendHistory(this.key, { kind: 'prompt', prompt, suffixApplied: 'flat_layout' });
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: Array.isArray(result.files) ? result.files : [],
362
- folders: Array.isArray(result.folders) ? result.folders : [],
363
- authoritative: (result.folders?.length ?? 0) === 0
415
+ files,
416
+ folders,
417
+ authoritative: !emptyButLabelVisible && folders.length === 0
364
418
  };
365
419
  }
366
420
  async openFile(filename) {
@@ -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: {
@@ -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",
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",