cobro-mcp 0.10.2 → 0.10.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/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ All notable changes to this project are documented here. The format follows [Kee
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.10.3] — 2026-10-01
8
+ ### Fixed
9
+ - Markers and the working outline no longer jump to a neighbouring element with different text when the picked element is removed (e.g. a deleted table column); the element is reported as missing on Send. Elements without text (`col`, empty cells, icons) can still be matched to a neighbour.
10
+ - `open` on a localhost URL that is refused or returns an empty response now suggests trying 127.0.0.1 (localhost may resolve to another process over IPv6).
11
+ - The first letter of the toolbar status text is no longer clipped next to the status chip while the text fades in.
12
+ - Vue: picking an element inside a component-library component (Element Plus, Vuetify, …) now reports the SFC in your code that uses it, plus up to two calling SFCs, instead of only the library component name.
13
+
7
14
  ## [0.10.2] — 2026-10-01
8
15
  ### Added
9
16
  - `done` now returns a `next` hint telling the agent to call wait() again once every received batch is done.
package/README.ko.md CHANGED
@@ -144,8 +144,8 @@ claude mcp add -s user cobro -- npx -y cobro-mcp@latest
144
144
  | `elements[].rect` | `{ x, y, w, h }` 페이지 좌표(스크롤 포함), 정수 |
145
145
  | `elements[].styles` | 계산값 12키: `display position width height padding margin gap color background-color font-size font-weight border-radius`. `none`/`normal`은 생략하되 `display: none`은 "안 보임" 단서로 남긴다 |
146
146
  | `elements[].react` | React dev 빌드에서만 `{ component, source?, callers? }`. 아니면 키 자체가 없다. `source`는 소스맵이 있는 dev 서버(React 19) 또는 React 18까지에서만 온다. `source`는 찍은 요소 자신의 JSX 줄(사용자 코드일 때). 라이브러리가 그린 요소면 사용자 코드에서 가장 가까운 호출 지점. `callers`는 `source` 위의 사용자 코드 호출 지점(가까운 순, 최대 2). `source`가 한 줄 패스스루 래퍼면 보통 첫 caller가 진짜 고칠 자리. Vite·Next.js(Turbopack) dev 서버에서 동작 |
147
- | `elements[].vue` | Vue 3 dev 빌드에서 `{ component, source? }`. `source`는 SFC 경로만(행 없음). 아니면 키 생략 |
148
- | `elements[].missing` | 재주입 뒤 선택자로 못 찾으면 `true`(드묾) |
147
+ | `elements[].vue` | Vue 3 dev 빌드에서 `{ component, source?, callers? }`. `source`는 SFC 경로만(행 없음). `source`는 항상 사용자 코드의 SFC. 요소가 컴포넌트 라이브러리(Element Plus·Vuetify 등) 컴포넌트 안에서 그려지면 `component`는 그 SFC 템플릿에 직접 쓴 라이브러리 컴포넌트 이름(템플릿에서 검색 가능), 아니면 SFC 자신의 이름. `callers`는 `source` 위의 SFC(가까운 순, 최대 2). 아니면 키 생략 |
148
+ | `elements[].missing` | 선택자로 다시 찾았을 때 같은 요소로 확인되지 않으면 `true`(삭제되었거나 tag·text가 바뀜) — 나머지 값은 찍은 시점 그대로 |
149
149
  | `elements[].ref` | 패널 행의 안정 번호(`"2"`, 영역 `"1"` 안의 요소면 `"1a"`). 메모가 이 번호를 가리킨다. 초안이 사는 동안 재번호 없음 |
150
150
  | `regions[].ref` | `elements[].ref`와 같은 체계를 공유하는 안정 번호 |
151
151
  | `batches[].regions[]` | 사용자가 드래그로 그린 사각형: 그룹(안의 요소가 이 영역의 ref를 접두사로 갖는 ref를 받음)이거나 빈 곳 위 영역(요소 없음). `rect`는 페이지 좌표, `within`은 감싸는 요소의 선택자. 없으면 키 생략 |
package/README.md CHANGED
@@ -144,8 +144,8 @@ If `wait` returns `pending`, call it again (not an error). If `browserGone: true
144
144
  | `elements[].rect` | `{ x, y, w, h }` in page coordinates (scroll included), integers |
145
145
  | `elements[].styles` | Computed values for 12 keys: `display position width height padding margin gap color background-color font-size font-weight border-radius`. `none`/`normal` are dropped, except `display: none` which is kept as a "not visible" clue |
146
146
  | `elements[].react` | `{ component, source?, callers? }` from React dev builds only; key omitted otherwise. `source` needs a dev server with source maps (React 19) or React ≤ 18. `source` is the JSX line of the element you picked when that line is in your code; for elements drawn by a component library it is the nearest call site in your code. `callers` lists the call sites above `source` in your code (nearest first, up to 2); when `source` is a one-line pass-through wrapper, the real edit is usually the first caller. Works with Vite and Next.js (Turbopack) dev servers. |
147
- | `elements[].vue` | `{ component, source? }` from Vue 3 dev builds; `source` is the SFC path only (no line). Omitted otherwise |
148
- | `elements[].missing` | `true` when the selector no longer matches after re-injection (rare) |
147
+ | `elements[].vue` | `{ component, source?, callers? }` from Vue 3 dev builds; `source` is the SFC path only (no line). `source` is always an SFC in your code. When the element is drawn by a component-library component (Element Plus, Vuetify, …), `component` is the library component you wrote in that SFC's template (its name is searchable there) and `source` is that SFC; otherwise `component` is the SFC's own name. `callers` lists the SFCs above `source` (nearest first, up to 2). Omitted otherwise |
148
+ | `elements[].missing` | `true` when re-finding the element by its selector does not confirm it is the same element (it was removed, or its tag or text changed) — the other values are as captured |
149
149
  | `elements[].ref` | Stable number of the row in the panel (`"2"`, or `"1a"` for an element inside region `"1"`); notes refer to it. Never renumbered while the draft lives |
150
150
  | `regions[].ref` | Same stable numbering, shared with `elements[].ref` |
151
151
  | `batches[].regions[]` | Rectangles the user dragged: either a group (elements inside carry refs prefixed with the region's ref) or an area on empty space (no elements). `rect` in page coordinates, `within` = selector of the enclosing element. Omitted when none |
package/dist/overlay.js CHANGED
@@ -25,6 +25,24 @@
25
25
  // src/overlay/refs.ts
26
26
  var isChildRef = (ref, g) => !!ref && new RegExp("^" + g + "[a-l]$").test(ref);
27
27
 
28
+ // src/overlay/resolve.ts
29
+ var normalizeText = (s) => (s || "").trim().replace(/\s+/g, " ").slice(0, 40);
30
+ function resolveEl(e) {
31
+ let found = null;
32
+ try {
33
+ found = document.querySelector(e.selector);
34
+ } catch {
35
+ }
36
+ if (!found || found.tagName.toLowerCase() !== e.tag) return null;
37
+ if (e.text && normalizeText(found.textContent) !== e.text) return null;
38
+ return found;
39
+ }
40
+ function groupAlive(kids) {
41
+ if (kids.length === 0) return true;
42
+ const verifiable = kids.filter((k) => k.text);
43
+ return (verifiable.length > 0 ? verifiable : kids).some((k) => resolveEl(k) !== null);
44
+ }
45
+
28
46
  // src/core/page.ts
29
47
  function samePage(a, b) {
30
48
  try {
@@ -170,7 +188,7 @@ textarea:disabled{opacity:.6;cursor:not-allowed}
170
188
  .status-in.scroll{max-width:none;overflow:visible;text-overflow:clip}
171
189
  .status-in.enter{animation:cobroin .15s ease-out}
172
190
  .status-in a.goto{color:inherit;text-decoration:underline;cursor:pointer}
173
- @keyframes cobroin{from{opacity:.4;transform:translateX(-6px)}to{opacity:1;transform:translateX(0)}}
191
+ @keyframes cobroin{from{opacity:.4}to{opacity:1}}
174
192
  @media (prefers-reduced-motion: reduce){.status-in{transition:none}.status-in.enter{animation:none}}
175
193
  .wbox{position:fixed;border:1.5px solid var(--info);border-radius:2px;pointer-events:none;box-sizing:border-box;animation:cobrobreathe 2.4s ease-in-out infinite}
176
194
  .wbox.region{border-style:dashed;background:color-mix(in srgb, var(--info) 6%, transparent)}
@@ -930,16 +948,13 @@ div.queue{display:flex;flex-direction:column;gap:8px;max-height:min(45vh,320px);
930
948
  const isChild = /[a-z]$/.test(e.ref ?? "");
931
949
  const groupRef = isChild ? e.ref.slice(0, -1) : null;
932
950
  if (isChild && vm.expanded !== groupRef) return;
933
- let target = null;
934
- try {
935
- target = document.querySelector(e.selector);
936
- } catch {
937
- }
951
+ const target = resolveEl(e);
938
952
  if (!target) return;
939
953
  const r = target.getBoundingClientRect();
940
954
  place(el("div", isChild ? "marker child" : "marker"), r.left - 2, r.top - 2, r.width + 4, r.height + 4, e.ref ?? "");
941
955
  });
942
956
  cur.regions?.forEach((r) => {
957
+ if (!groupAlive(cur.elements.filter((e) => isChildRef(e.ref, r.ref ?? "")))) return;
943
958
  place(el("div", "marker region"), r.rect.x - scrollX, r.rect.y - scrollY, r.rect.w, r.rect.h, r.ref ?? "");
944
959
  });
945
960
  }
@@ -959,16 +974,13 @@ div.queue{display:flex;flex-direction:column;gap:8px;max-height:min(45vh,320px);
959
974
  b.elements.forEach((e) => {
960
975
  if (e.missing) return;
961
976
  if (/[a-z]$/.test(e.ref ?? "")) return;
962
- let target = null;
963
- try {
964
- target = document.querySelector(e.selector);
965
- } catch {
966
- }
977
+ const target = resolveEl(e);
967
978
  if (!target) return;
968
979
  const r = target.getBoundingClientRect();
969
980
  place(wbox("wbox"), r.left - 2, r.top - 2, r.width + 4, r.height + 4, e.ref ?? "");
970
981
  });
971
982
  b.regions?.forEach((r) => {
983
+ if (!groupAlive(b.elements.filter((e) => isChildRef(e.ref, r.ref ?? "")))) return;
972
984
  place(wbox("wbox region"), r.rect.x - scrollX, r.rect.y - scrollY, r.rect.w, r.rect.h, r.ref ?? "");
973
985
  });
974
986
  }
@@ -1325,17 +1337,43 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1325
1337
  const segs = norm.split("/").filter(Boolean);
1326
1338
  return segs.slice(-2).join("/");
1327
1339
  }
1340
+ function fileBase(file) {
1341
+ return (file.replace(/\\/g, "/").split("/").pop() ?? "").replace(/\.[^.]+$/, "");
1342
+ }
1343
+ var BUILTIN = /* @__PURE__ */ new Set(["BaseTransition", "KeepAlive", "TransitionGroup"]);
1328
1344
  function vueInfo(el, root) {
1329
1345
  let c = el["__vueParentComponent"] ?? null;
1330
- for (let i = 0; c && i < 10; i++, c = c.parent ?? null) {
1346
+ let fallback;
1347
+ let fallbackBuiltin;
1348
+ let libName;
1349
+ let hit;
1350
+ const callers = [];
1351
+ for (let i = 0; c && i < 15 && callers.length < 2; i++, c = c.parent ?? null) {
1331
1352
  const name = c.type?.name || c.type?.__name;
1353
+ const builtin = !!name && BUILTIN.has(name);
1332
1354
  if (name) {
1333
- const file = c.type?.__file;
1334
- const source = file ? normalizeVueSource(file, root) : void 0;
1335
- return source ? { component: name, source } : { component: name };
1355
+ if (builtin) fallbackBuiltin ??= name;
1356
+ else fallback ??= name;
1336
1357
  }
1358
+ if (builtin) continue;
1359
+ const file = c.type?.__file;
1360
+ const source = file ? normalizeVueSource(file, root) : void 0;
1361
+ if (!source) {
1362
+ if (!hit) {
1363
+ const n = name || (file ? fileBase(file) : "");
1364
+ if (n) libName = n;
1365
+ }
1366
+ continue;
1367
+ }
1368
+ if (!hit) hit = { component: libName ?? (name || fileBase(file)), source };
1369
+ else if (source !== hit.source && !callers.includes(source)) callers.push(source);
1337
1370
  }
1338
- return void 0;
1371
+ if (!hit) {
1372
+ const n = fallback ?? fallbackBuiltin;
1373
+ return n ? { component: n } : void 0;
1374
+ }
1375
+ if (callers.length) hit.callers = callers;
1376
+ return hit;
1339
1377
  }
1340
1378
  var vue = { key: "vue", detect: vueInfo };
1341
1379
 
@@ -1412,7 +1450,7 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1412
1450
  selector: uniqueSelector(el),
1413
1451
  tag: el.tagName.toLowerCase(),
1414
1452
  classes: [...el.classList],
1415
- text: (el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 40),
1453
+ text: normalizeText(el.textContent),
1416
1454
  rect: { x: Math.round(r.left + window.scrollX), y: Math.round(r.top + window.scrollY), w: Math.round(r.width), h: Math.round(r.height) },
1417
1455
  styles
1418
1456
  };
@@ -1597,12 +1635,7 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1597
1635
  const resolveDraft = (b) => {
1598
1636
  ensureRefs(b);
1599
1637
  return { ...b, status: "draft", elements: b.elements.map((e, i) => {
1600
- let found = null;
1601
- try {
1602
- found = document.querySelector(e.selector);
1603
- } catch {
1604
- }
1605
- const missing = !found;
1638
+ const missing = !resolveEl(e);
1606
1639
  if (missing !== !!e.missing) chan.send({ type: "resolved", batchId: b.id, index: i, missing });
1607
1640
  return { ...e, missing };
1608
1641
  }) };
@@ -1647,6 +1680,10 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
1647
1680
  ui.focusNote();
1648
1681
  return;
1649
1682
  }
1683
+ for (const b of ready) for (const e of b.elements) {
1684
+ const missing = !resolveEl(e);
1685
+ if (missing !== !!e.missing) e.missing = missing;
1686
+ }
1650
1687
  flushDraft();
1651
1688
  chan.send({ type: "send", batchIds: ready.map((b) => b.id), page: pageInfo() });
1652
1689
  drafts = (drafts ?? []).filter((b) => !ready.includes(b));
package/dist/server.js CHANGED
@@ -824,6 +824,17 @@ async function fetchTextGuarded(request, url, maxBytes = 8 * 1024 * 1024, timeou
824
824
  function classifyLaunchFailure(tried, lockExists) {
825
825
  return tried.every((t) => t.includes("has been closed")) && lockExists ? "profile-locked" : "not-found";
826
826
  }
827
+ var LOCALHOST_FAILURE = /ERR_EMPTY_RESPONSE|ERR_CONNECTION_REFUSED|NS_ERROR_CONNECTION_REFUSED|Could not connect to server|Server returned nothing/;
828
+ function withLocalhostHint(url, message) {
829
+ let host = "";
830
+ try {
831
+ host = new URL(url).hostname;
832
+ } catch {
833
+ return message;
834
+ }
835
+ if (host !== "localhost" || !LOCALHOST_FAILURE.test(message)) return message;
836
+ return `${message} \u2014 localhost may resolve to another process (IPv6 ::1); try 127.0.0.1 instead.`;
837
+ }
827
838
  function parseEngine(v) {
828
839
  return v === "webkit" || v === "firefox" || v === "chromium" ? v : "chromium";
829
840
  }
@@ -936,7 +947,11 @@ Close the other session's cobro browser (close), or set COBRO_PROFILE_DIR to a d
936
947
  await this.launch();
937
948
  this.launchedOnce = true;
938
949
  }
939
- await this.page.goto(url, { waitUntil: "load" });
950
+ try {
951
+ await this.page.goto(url, { waitUntil: "load" });
952
+ } catch (e) {
953
+ throw new Error(withLocalhostHint(url, e.message), { cause: e });
954
+ }
940
955
  return { title: await this.page.title(), restarted };
941
956
  }
942
957
  async rectOfSelector(selector) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cobro-mcp",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "description": "Co-browse for coding agents: pick elements in the browser, write a note, Send — the context lands in the agent's chat. MCP server with 6 tools.",
5
5
  "keywords": [
6
6
  "mcp",