@usercase/mcp 0.2.11-next.2 → 0.2.11-next.3

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/mcp/hub.js CHANGED
@@ -450,7 +450,12 @@ class Hub {
450
450
  external,
451
451
  study: research
452
452
  ? {
453
- press: (k) => study.press(page, k),
453
+ // 패널 옮기기는 막혔던 자리에서 비키게 끈다. 옮기고 나면 그 항목은 목록에서 내린다
454
+ press: async (k) => {
455
+ if (k !== "move") return study.press(page, k);
456
+ await study.movePanel(page, rec.panelInWay);
457
+ rec.panelInWay = null;
458
+ },
454
459
  // 과제 질문·시간 초과 창이 페이지를 덮고 있나 — 그 사이 조작은 페이지에 닿지 않는다
455
460
  blocking: async () => {
456
461
  const w = study.isSurveyUrl(page.url()) ? null : await study.readStudy(page);
@@ -505,6 +510,9 @@ class Hub {
505
510
  if (look.ended) return "This visit has ended.";
506
511
  if (look.card) return renderCard(look.card);
507
512
  // 과제 건너뛰기는 위젯 단추다. 화면 목록 끝에 붙인다 — 누르면 Visit 이 위젯을 누른다
513
+ // 우리 패널이 누르려던 것을 덮었다 — 사람은 패널을 끌어 비킨다(11/04 실험실 covered_by_panel 3건: 캘린더 Week·Month)
514
+ if (st && rec.panelInWay && !look.view.dialog)
515
+ look.view.items.push({ i: look.view.items.length, label: "Drag the study panel aside (it is covering the page)", where: "study panel, by its top bar", study: "move" });
508
516
  if (st && st.skip && !look.view.dialog) look.view.items.push({ i: look.view.items.length, label: `${st.skip} (in the study panel — gives up on this task)`, where: "study panel, corner of the screen", study: "skip" });
509
517
  // 아직 건너뛸 수 없는 과제 — 예전엔 "패널 기다리기" 를 줘서 막히면 몇 분씩 아무것도 안 했다(라이브 11번 × 약 5분).
510
518
  // 사람은 막히면 계속 찾아본다. 그러다 실패 시간이 지나면 SDK 가 묻는 창을 띄우고, 다음 화면에서 그 창이 먼저 보인다
@@ -780,6 +788,7 @@ class Hub {
780
788
  if (TRACE) trace(rec, { ev: "act", action: decision.action, study: r && r.study, actMs: Date.now() - a0, waitTurnMs: v.lastWaitMs || 0, sdk: await sdkSnap(rec.page) });
781
789
  // 패널의 건너뛰기를 눌렀다 — 그 과제를 건너뛴 것으로 남긴다
782
790
  if (r && r.study === "skip" && !r.skipped && rec.study) rec.study.skipped.push(rec.study.lastTask);
791
+ if (r && r.panelAt && rec.study) rec.panelInWay = r.panelAt;
783
792
  if (r.interrupted && r.study) {
784
793
  rec.cutOff = true;
785
794
  return ["Before you could do that, the study asked you something.", "", await this.screen(rec)].join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usercase/mcp",
3
- "version": "0.2.11-next.2",
3
+ "version": "0.2.11-next.3",
4
4
  "description": "Virtual users that browse your website and tell you what they thought — an MCP server for Claude Code",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://usercase.io",
@@ -133,7 +133,8 @@ async function coveredBy(page, el) {
133
133
  )
134
134
  .catch(() => null);
135
135
  if (!hit) return null;
136
- if (hit.own === "panel") return { say: "the study panel is on top of it — scroll or move so it is not under the panel" };
136
+ // 패널은 끌어 옮길 수 있다(core Widget 알약 드래그). 막힌 자리를 같이 넘겨 hub 가 "패널 옮기기" 를 화면 목록에 붙인다
137
+ if (hit.own === "panel") return { say: "the study panel is on top of it — drag the panel aside (it is in the list), or scroll so it is not under the panel", panelAt: { x: cx, y: cy } };
137
138
  if (hit.own) return { say: "the survey card is on top of it" };
138
139
  if (hit.text) return { say: `something else on the screen is on top of it ("${hit.text}") — you cannot reach it until that is out of the way` };
139
140
  return { invisible: true, x: cx, y: cy };
@@ -387,6 +388,7 @@ async function clickLikePerson(page, state, el) {
387
388
  if (startY !== null) await page.evaluate((y) => window.scrollTo({ top: y, behavior: "instant" }), startY).catch(() => {});
388
389
  const e = new Error(cover.say);
389
390
  e.human = cover.say;
391
+ if (cover.panelAt) e.panelAt = cover.panelAt;
390
392
  throw e;
391
393
  }
392
394
  // 화면 가장자리 밖으로 잘려 나간 것. 굴려도 옆으로는 안 온다 — 사람은 "잘려 있다" 를 본다.
@@ -180,6 +180,52 @@ const PRESS = {
180
180
  submit: "#eb-plugin #eb-survey-action-btn",
181
181
  };
182
182
 
183
+ /**
184
+ * 리서치 패널을 끌어 비킨다 — 알약(#eb-pill)을 잡고 끌면 패널이 같이 움직인다(core components/widget/Widget.ts setupDrag).
185
+ * 막혔던 자리(away)의 반대쪽 가장자리로. 그래도 덮으면 위아래도 바꾼다. 고객이 끌기를 껐으면(draggable 0) 안 움직인다
186
+ */
187
+ async function movePanel(page, away) {
188
+ const vp = page.viewportSize() || { width: 1280, height: 860 };
189
+ const pill = page.locator("#eb-plugin #eb-pill").first();
190
+ const covers = (pt) =>
191
+ pt
192
+ ? page
193
+ .evaluate(([x, y]) => {
194
+ const h = document.elementFromPoint(x, y);
195
+ return !!(h && h.closest("#eb-plugin"));
196
+ }, [pt.x, pt.y])
197
+ .catch(() => false)
198
+ : Promise.resolve(false);
199
+ const drag = async (tx, ty) => {
200
+ const b = await pill.boundingBox();
201
+ if (!b) throw new Error("the study panel is not on the screen");
202
+ const sx = b.x + b.width / 2;
203
+ const sy = b.y + b.height / 2;
204
+ await page.mouse.move(sx, sy, { steps: 6 });
205
+ await sleep(120 + Math.random() * 200);
206
+ await page.mouse.down();
207
+ await page.mouse.move(sx + (tx - sx) * 0.5 + (Math.random() - 0.5) * 30, sy + (ty - sy) * 0.5 + (Math.random() - 0.5) * 30, { steps: 10 });
208
+ await page.mouse.move(tx, ty, { steps: 8 });
209
+ await sleep(80 + Math.random() * 150);
210
+ await page.mouse.up();
211
+ await sleep(300);
212
+ const a = await pill.boundingBox();
213
+ return a && Math.hypot(a.x - b.x, a.y - b.y) > 20;
214
+ };
215
+ const b = await pill.boundingBox();
216
+ if (!b) throw new Error("the study panel is not on the screen");
217
+ const half = b.width / 2;
218
+ const ax = away ? away.x : b.x + half;
219
+ const ay = away ? away.y : b.y;
220
+ // 옆으로 — 막힌 자리의 반대쪽 끝
221
+ const tx = ax < vp.width / 2 ? vp.width - half - 24 : half + 24;
222
+ if (!(await drag(tx, b.y + b.height / 2))) throw new Error("the panel did not move — it stays where it is");
223
+ if (!(await covers(away))) return;
224
+ // 패널이 길어 아직 덮는다 — 위아래도 반대쪽으로
225
+ const ty = ay < vp.height / 2 ? vp.height - b.height - 24 : 24 + b.height / 2;
226
+ await drag(tx, ty);
227
+ }
228
+
183
229
  /** 위젯 단추를 누른다 */
184
230
  async function press(page, key) {
185
231
  const sel = PRESS[key];
@@ -211,4 +257,4 @@ async function answerQuestion(page, q, answer) {
211
257
  return null;
212
258
  }
213
259
 
214
- module.exports = { readStudy, waitState, join, press, answerQuestion, isSurveyUrl };
260
+ module.exports = { readStudy, waitState, join, press, movePanel, answerQuestion, isSurveyUrl };
package/src/run/visit.js CHANGED
@@ -727,7 +727,7 @@ class Visit {
727
727
  // 리서치 위젯의 단추(과제 건너뛰기). 페이지 조작이 아니라 위젯을 누른다 — 화면 목록에 hub 가 붙여 둔 것이다
728
728
  if (decision.action === "click" && item && item.study && this.study) {
729
729
  record.study = item.study;
730
- await this.study.press(item.study).catch(() => (record.skipped = "it did not respond"));
730
+ await this.study.press(item.study).catch((e) => (record.skipped = item.study === "move" && e && e.message ? e.message : "it did not respond"));
731
731
  await sleep(1500);
732
732
  this.steps.push(record);
733
733
  this.history.push(`click "${item.label}" — ${decision.why}${record.skipped ? ` (${record.skipped})` : ""}`);
@@ -892,7 +892,7 @@ class Visit {
892
892
  let ours = false;
893
893
  const clicked = await hand
894
894
  .clickLikePerson(page, this.handState, el)
895
- .catch((e) => ((record.skipped = e.human || humanClickError(e)), (ours = /eb-insight|eb-plugin/.test(String(e.message))), { missed: false }));
895
+ .catch((e) => ((record.skipped = e.human || humanClickError(e)), (record.panelAt = e.panelAt || undefined), (ours = /eb-insight|eb-plugin/.test(String(e.message))), { missed: false }));
896
896
  // 사이트의 무언가가 덮어 못 눌렀다. 우리 카드·위젯이 덮은 것은 사이트 탓이 아니다
897
897
  if (record.skipped && /on top of it/.test(record.skipped) && !/survey card|study panel/.test(record.skipped) && !ours)
898
898
  await this.shoot(record, "covered", await boxOf(page, el));