@remcp/runtime 0.2.74 → 0.2.76
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 +6 -0
- package/package.json +1 -1
- package/src/extended/browser.mjs +48 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.2.76 — 2026-09-21
|
|
6
|
+
|
|
7
|
+
- `browser_find(text=...)` ranks exact semantic targets ahead of broad ancestor text containers, prefers actionable/semantic nodes, and de-duplicates nested matches before applying the result limit.
|
|
8
|
+
- Native `h1`–`h6` elements derive the `heading` role, improving browser text targeting and follow-up actions.
|
|
9
|
+
- Lockstep patch release with the ReMCP client/server at `0.2.76`.
|
|
10
|
+
|
|
5
11
|
## 0.2.63 — 2026-09-20
|
|
6
12
|
|
|
7
13
|
- Lockstep MCP Apps production-surface release with the ReMCP client/server at `0.2.63`; device runtime tool behavior is unchanged from `0.2.62`.
|
package/package.json
CHANGED
package/src/extended/browser.mjs
CHANGED
|
@@ -447,7 +447,7 @@ export async function browserSnapshot(args) {
|
|
|
447
447
|
});
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
function findExpression(args) {
|
|
450
|
+
export function findExpression(args) {
|
|
451
451
|
const selector = optionalString(args.selector);
|
|
452
452
|
const needle = optionalString(args.text);
|
|
453
453
|
const role = optionalString(args.role);
|
|
@@ -474,6 +474,7 @@ function findExpression(args) {
|
|
|
474
474
|
const explicit = (el.getAttribute('role') || '').trim().toLowerCase();
|
|
475
475
|
if (explicit) return explicit;
|
|
476
476
|
const tag = el.tagName.toLowerCase();
|
|
477
|
+
if (/^h[1-6]$/.test(tag)) return 'heading';
|
|
477
478
|
if (tag === 'button') return 'button';
|
|
478
479
|
if (tag === 'a' && el.hasAttribute('href')) return 'link';
|
|
479
480
|
if (tag === 'textarea') return 'textbox';
|
|
@@ -495,15 +496,57 @@ function findExpression(args) {
|
|
|
495
496
|
}
|
|
496
497
|
return '';
|
|
497
498
|
}
|
|
498
|
-
|
|
499
|
+
function visibleText(el) {
|
|
500
|
+
return (el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim();
|
|
501
|
+
}
|
|
502
|
+
function textQuality(value) {
|
|
503
|
+
if (!needle) return 0;
|
|
504
|
+
const hay = value.toLowerCase();
|
|
505
|
+
const wanted = needle.toLowerCase();
|
|
506
|
+
if (hay === wanted) return 0;
|
|
507
|
+
if (hay.startsWith(wanted)) return 1;
|
|
508
|
+
return 2;
|
|
509
|
+
}
|
|
510
|
+
function depthOf(el) {
|
|
511
|
+
let depth = 0;
|
|
512
|
+
for (let current = el.parentElement; current; current = current.parentElement) depth += 1;
|
|
513
|
+
return depth;
|
|
514
|
+
}
|
|
515
|
+
const candidates = source.filter(el => {
|
|
499
516
|
const style = getComputedStyle(el);
|
|
500
517
|
const visible = style.visibility !== 'hidden' && style.display !== 'none' && el.getClientRects().length > 0;
|
|
501
|
-
const hay = (el
|
|
518
|
+
const hay = visibleText(el);
|
|
502
519
|
const r = semanticRole(el);
|
|
503
520
|
return visible && (!needle || hay.toLowerCase().includes(needle.toLowerCase())) && (!role || r === role.toLowerCase());
|
|
504
|
-
}).
|
|
521
|
+
}).map((el, index) => ({
|
|
522
|
+
el,
|
|
523
|
+
index,
|
|
524
|
+
text: visibleText(el),
|
|
525
|
+
role: semanticRole(el),
|
|
526
|
+
depth: depthOf(el),
|
|
527
|
+
}));
|
|
528
|
+
let selected = candidates;
|
|
529
|
+
if (needle && !selector) {
|
|
530
|
+
selected = [...candidates].sort((a, b) =>
|
|
531
|
+
textQuality(a.text) - textQuality(b.text)
|
|
532
|
+
|| Number(!a.role) - Number(!b.role)
|
|
533
|
+
|| a.text.length - b.text.length
|
|
534
|
+
|| b.depth - a.depth
|
|
535
|
+
|| a.index - b.index
|
|
536
|
+
);
|
|
537
|
+
const deduped = [];
|
|
538
|
+
for (const candidate of selected) {
|
|
539
|
+
if (deduped.some(existing => existing.el.contains(candidate.el) || candidate.el.contains(existing.el))) continue;
|
|
540
|
+
deduped.push(candidate);
|
|
541
|
+
if (deduped.length >= ${limit}) break;
|
|
542
|
+
}
|
|
543
|
+
selected = deduped;
|
|
544
|
+
} else {
|
|
545
|
+
selected = selected.slice(0, ${limit});
|
|
546
|
+
}
|
|
547
|
+
return selected.map(({ el }) => {
|
|
505
548
|
const b = el.getBoundingClientRect();
|
|
506
|
-
return { selector: cssPath(el), tag: el.tagName.toLowerCase(), role: semanticRole(el), name: el.getAttribute('aria-label') || el.getAttribute('name') || '', text: (el
|
|
549
|
+
return { selector: cssPath(el), tag: el.tagName.toLowerCase(), role: semanticRole(el), name: el.getAttribute('aria-label') || el.getAttribute('name') || '', text: visibleText(el).slice(0, 500), value: 'value' in el ? String(el.value).slice(0, 500) : '', x: b.x, y: b.y, width: b.width, height: b.height, disabled: Boolean(el.disabled) };
|
|
507
550
|
});
|
|
508
551
|
})()`;
|
|
509
552
|
}
|