@pro-vi/designer 0.3.7 → 0.3.8

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.
@@ -195,17 +195,42 @@ export class DesignerController {
195
195
  const { promptTextarea, sendButton } = this.selectors.composer;
196
196
  await this.browser.waitFor(promptTextarea);
197
197
  await this.browser.evalValue(`(() => {
198
- const ta = document.querySelector(${JSON.stringify(promptTextarea)});
199
- if (!ta) throw new Error('textarea not found');
200
- const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
201
- setter.call(ta, ${JSON.stringify(prompt)});
202
- ta.dispatchEvent(new Event('input', { bubbles: true }));
203
- ta.focus();
204
- return true;
198
+ const el = document.querySelector(${JSON.stringify(promptTextarea)});
199
+ if (!el) throw new Error('composer input not found');
200
+ const text = ${JSON.stringify(prompt)};
201
+ if (el instanceof HTMLTextAreaElement) {
202
+ // Bypass React's value ownership via the native setter, then fire a
203
+ // bubbling input event.
204
+ const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
205
+ setter.call(el, text);
206
+ el.dispatchEvent(new Event('input', { bubbles: true }));
207
+ el.focus();
208
+ return true;
209
+ }
210
+ if (el.isContentEditable) {
211
+ // Deliver the text as a synthetic paste so the editor's own paste
212
+ // pipeline updates its internal state; execCommand('insertText')
213
+ // flattens multi-line prompts into one paragraph.
214
+ el.focus();
215
+ const sel = window.getSelection();
216
+ const range = document.createRange();
217
+ range.selectNodeContents(el);
218
+ sel.removeAllRanges();
219
+ sel.addRange(range);
220
+ const dt = new DataTransfer();
221
+ dt.setData('text/plain', text);
222
+ const unhandled = el.dispatchEvent(new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true }));
223
+ if (unhandled) {
224
+ // No editor intercepted the paste — plain contenteditable fallback.
225
+ document.execCommand('insertText', false, text);
226
+ }
227
+ return true;
228
+ }
229
+ throw new Error('composer input is neither textarea nor contenteditable: ' + el.tagName);
205
230
  })()`);
206
231
  for (let i = 0; i < 30; i++) {
207
232
  await new Promise((r) => setTimeout(r, 150));
208
- const disabled = await this.browser.evalValue(`(() => { const b = document.querySelector(${JSON.stringify(sendButton)}); return !b || b.disabled; })()`);
233
+ const disabled = await this.browser.evalValue(`(() => { const b = document.querySelector(${JSON.stringify(sendButton)}); return !b || b.disabled || b.getAttribute('aria-disabled') === 'true'; })()`);
209
234
  if (!disabled)
210
235
  break;
211
236
  }
@@ -520,7 +545,9 @@ export class DesignerController {
520
545
  if (!opened)
521
546
  await this._clickButtonByText(/^Export$/);
522
547
  await new Promise((r) => setTimeout(r, 400));
523
- await this._clickButtonByText(/handoff to claude code/i);
548
+ const viaSendTo = await this._clickClaudeCodeSendTo().catch(() => false);
549
+ if (!viaSendTo)
550
+ await this._clickButtonByText(/handoff to claude code/i);
524
551
  let handoffUrl = '';
525
552
  for (let i = 0; i < 30; i++) {
526
553
  await new Promise((r) => setTimeout(r, 300));
@@ -583,6 +610,28 @@ export class DesignerController {
583
610
  return true;
584
611
  })()`);
585
612
  }
613
+ async _clickClaudeCodeSendTo() {
614
+ const tabClicked = await this.browser.evalValue(`(() => {
615
+ const tab = Array.from(document.querySelectorAll('button[role="tab"]')).find(t => /send to/i.test(t.textContent || ''));
616
+ if (!tab) return false;
617
+ tab.click();
618
+ return true;
619
+ })()`);
620
+ if (!tabClicked)
621
+ return false;
622
+ await new Promise((r) => setTimeout(r, 400));
623
+ return this.browser.evalValue(`(() => {
624
+ const sends = Array.from(document.querySelectorAll('button')).filter(b => (b.textContent || '').trim() === 'Send');
625
+ const target = sends.find(b => {
626
+ let row = b;
627
+ for (let i = 0; i < 3 && row.parentElement; i++) row = row.parentElement;
628
+ return /claude code/i.test(row.textContent || '');
629
+ });
630
+ if (!target) return false;
631
+ target.click();
632
+ return true;
633
+ })()`);
634
+ }
586
635
  async _dialogText() {
587
636
  return ((await this.browser
588
637
  .evalValue(`(() => {
@@ -70,7 +70,7 @@ export const UI_ANCHORS = [
70
70
  category: 'session',
71
71
  description: 'send button',
72
72
  requires: 'session',
73
- check: async (b) => ({ ok: await hasSelector(b, '[data-testid="chat-send-button"]') })
73
+ check: async (b) => ({ ok: await hasSelector(b, '[data-testid="chat-send-button"], button[title^="Send ("]') })
74
74
  },
75
75
  {
76
76
  id: 'session.htmlViewerIframe',
@@ -122,16 +122,30 @@ export const UI_ANCHORS = [
122
122
  {
123
123
  id: 'share.handoffMenuItem',
124
124
  category: 'share',
125
- description: 'Handoff-to-Claude-Code menu item (inside Share dropdown)',
125
+ description: 'Handoff-to-Claude-Code action (Share → Send to… tab → Claude Code row, or the legacy dropdown item)',
126
126
  requires: 'session',
127
127
  check: async (b) => {
128
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
129
  if (!opened)
130
130
  return { ok: false, detail: 'Share button not clickable' };
131
131
  await new Promise((r) => setTimeout(r, 400));
132
- const found = await hasButtonMatching(b, /handoff to claude code/i);
132
+ let found = await hasButtonMatching(b, /handoff to claude code/i);
133
+ if (!found) {
134
+ const tabClicked = await b.evalValue(`(() => { const tab = Array.from(document.querySelectorAll('button[role="tab"]')).find(t => /send to/i.test(t.textContent || '')); if (!tab) return false; tab.click(); return true; })()`).catch(() => false);
135
+ if (tabClicked) {
136
+ await new Promise((r) => setTimeout(r, 400));
137
+ found = await b.evalValue(`(() => {
138
+ const sends = Array.from(document.querySelectorAll('button')).filter(x => (x.textContent || '').trim() === 'Send');
139
+ return sends.some(x => {
140
+ let row = x;
141
+ for (let i = 0; i < 3 && row.parentElement; i++) row = row.parentElement;
142
+ return /claude code/i.test(row.textContent || '');
143
+ });
144
+ })()`).catch(() => false);
145
+ }
146
+ }
133
147
  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' };
148
+ return { ok: found, detail: found ? undefined : 'Share opened but no Claude Code handoff action (checked legacy item and Send to… tab)' };
135
149
  }
136
150
  },
137
151
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pro-vi/designer",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
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",
@@ -40,7 +40,7 @@
40
40
  "smoke": "bash scripts/install-smoke.sh"
41
41
  },
42
42
  "dependencies": {
43
- "@anthropic-ai/sdk": "^0.96.0",
43
+ "@anthropic-ai/sdk": "^0.102.0",
44
44
  "@modelcontextprotocol/sdk": "^1.26.0",
45
45
  "zod": "^4.4.3"
46
46
  },
package/selectors.json CHANGED
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "composer": {
20
20
  "promptTextarea": "[data-testid=\"chat-composer-input\"]",
21
- "sendButton": "[data-testid=\"chat-send-button\"]",
21
+ "sendButton": "[data-testid=\"chat-send-button\"], button[title^=\"Send (\"]",
22
22
  "stopButton": null,
23
23
  "attachButton": "button[aria-label=\"Attach file\"]",
24
24
  "modelButton": "[data-testid=\"model-selector-button\"]"