@remcp/runtime 0.2.75 → 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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.75",
3
+ "version": "0.2.76",
4
4
  "description": "First-party ReMCP local device runtime: filesystem, terminal, computer-use, browser, diagnostics and document tools over MCP.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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
- return source.filter(el => {
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.innerText || el.textContent || el.getAttribute('aria-label') || '').trim();
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
- }).slice(0, ${limit}).map(el => {
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.innerText || el.textContent || '').trim().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) };
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
  }