pawbrowse 0.6.4 → 0.6.5

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.
@@ -865,14 +865,41 @@ const SNAPSHOT = `(function(seed, opts){
865
865
  if(dropH.size) actions=actions.filter(function(a){ return !dropH.has(a); });
866
866
  }catch(_){}
867
867
  // The nearest ancestor text that isn't just the label itself: which row/item a control is in.
868
+ // avoid, when given, skips past an ancestor whose stripped text matches it — used below when
869
+ // that text turned out to be shared by another same-label control too (not actually distinguishing).
868
870
  cache.ctxOf=function(el, lab){ return ctxOf(el, lab); };
869
- function ctxOf(el, lab){
871
+ function ctxOf(el, lab, avoid){
870
872
  for(var p=el&&el.parentElement, g=0; p && g<6 && p.tagName!=='BODY'; p=p.parentElement, g++){
871
873
  var tx=clean(p.innerText,400); if(!tx || tx===lab) continue;
872
- var rest=clean(tx.split(lab).join(' '),40); if(rest) return rest;
874
+ var rest=clean(tx.split(lab).join(' '),40); if(rest && rest!==avoid) return rest;
873
875
  }
874
876
  return '';
875
877
  }
878
+ // Two same-label controls can also share their nearest row text (an identical filter chip mirrored
879
+ // into two named panels, e.g. Booking's "Free cancellation" appearing under both "Your previous
880
+ // filters" and "Popular filters"): climb one level further, past that shared text, for the colliding
881
+ // pair only. others is the LIVE elements of every other control in the same label group, so this
882
+ // gives the identical answer whether it runs now (observe) or later against a re-scanned DOM
883
+ // (cache.get's re-render fallback below) — the same collision is always detected the same way.
884
+ function ctxOfDistinct(el, lab, others){
885
+ var cx=ctxOf(el, lab);
886
+ if(!cx) return cx;
887
+ for(var i=0;i<others.length;i++){ if(others[i]!==el && ctxOf(others[i], lab)===cx) return ctxOf(el, lab, cx) || cx; }
888
+ return cx;
889
+ }
890
+ cache.ctxOfDistinct=ctxOfDistinct;
891
+ // Every currently-visible, currently-actionable element sharing an identity (role+label): the group
892
+ // ctxOfDistinct needs to detect a collision live, on demand, from just an element and its guard —
893
+ // used both by cache.get's re-render fallback and by resolveHit's own row-context re-check below, so
894
+ // the two always agree on what counts as "still the same row" for a live element.
895
+ function poolOf(want){
896
+ var cands=collect(), out=[];
897
+ for(var i=0;i<cands.length;i++){
898
+ try{ if(cands[i].el.isConnected && cache.guard(cands[i].el)===want && cache.surface(cands[i].el)) out.push(cands[i].el); }catch(_){}
899
+ }
900
+ return out;
901
+ }
902
+ cache.ctxLive=function(el, lab, want){ return ctxOfDistinct(el, lab, poolOf(want)); };
876
903
  // Identical labels ("Delete" per row, "Edit" per user) are ambiguous to the agent: tag each with
877
904
  // the nearest ancestor text that tells them apart (e.g. the list row it lives in).
878
905
  try{
@@ -880,7 +907,8 @@ const SNAPSHOT = `(function(seed, opts){
880
907
  for(var u=0;u<actions.length;u++){ var key=actions[u].kind+'|'+actions[u].label; (byLabel[key]=byLabel[key]||[]).push(actions[u]); }
881
908
  Object.keys(byLabel).forEach(function(key){
882
909
  var grp=byLabel[key]; if(grp.length<2) return;
883
- grp.forEach(function(a){ var cx=ctxOf(cache.nodes.get(a.node), a.label); if(cx) a.ctx=cx; });
910
+ var els=grp.map(function(a){ return cache.nodes.get(a.node); });
911
+ grp.forEach(function(a, ai){ var cx=ctxOfDistinct(els[ai], a.label, els); if(cx) a.ctx=cx; });
884
912
  });
885
913
  }catch(_){}
886
914
  var focus=null;
@@ -929,12 +957,13 @@ const SNAPSHOT = `(function(seed, opts){
929
957
  var node=cache.byId[id]; if(node==null) return null;
930
958
  var e=cache.nodes.get(node); if(e && e.isConnected) return e;
931
959
  var fp=cache.fps && cache.fps[id], want=cache.guards && cache.guards[id]; if(!fp || !want || unnamed(want)) return null;
932
- var cands=collect(), hit=null, n=0;
933
- for(var k=0;k<cands.length && n<2;k++){
934
- var el=cands[k].el;
960
+ var pool=poolOf(want), hit=null, n=0;
961
+ for(var k=0;k<pool.length && n<2;k++){
962
+ var el=pool[k];
935
963
  try{
936
- if(!el.isConnected || cache.guard(el)!==want || !cache.surface(el)) continue;
937
- if(fp.ctx && ctxOf(el, fp.label)!==fp.ctx) continue;
964
+ // Same collision-aware ctx as at observe time (see ctxOfDistinct above): a shared, non-
965
+ // distinguishing row text must climb past it here too, or a stored deep ctx never matches.
966
+ if(fp.ctx && ctxOfDistinct(el, fp.label, pool)!==fp.ctx) continue;
938
967
  hit=el; n++;
939
968
  }catch(_){}
940
969
  }
@@ -1291,7 +1320,7 @@ async function resolveHit(tabId, ref, opts, _retries) {
1291
1320
  if(c.guard && c.guards && c.guards[${R}]!=null && c.guard(e)!==c.guards[${R}]) return {error:'element changed since observe (observe again)'};
1292
1321
  // Same node, same label — but a different ROW? Virtualized lists recycle row elements for other
1293
1322
  // items: "Delete" may now belong to someone else. The row context it was listed with must hold.
1294
- var fp=c.fps&&c.fps[${R}]; if(fp && fp.ctx && c.ctxOf && c.ctxOf(e, fp.label)!==fp.ctx) return {error:'the row this control belongs to changed since observe (now in "'+c.ctxOf(e, fp.label)+'"); observe again'};
1323
+ var fp=c.fps&&c.fps[${R}]; if(fp && fp.ctx && c.ctxLive){ var liveCtx=c.ctxLive(e, fp.label, c.guards[${R}]); if(liveCtx!==fp.ctx) return {error:'the row this control belongs to changed since observe (now in "'+liveCtx+'"); observe again'}; }
1295
1324
  if(e.matches(':disabled')||e.closest('[aria-disabled="true"],[inert]')) return {error:'element is disabled'};
1296
1325
  if(${forFill} && (e.readOnly||e.getAttribute('aria-readonly')==='true')) return {error:'field is read-only'};
1297
1326
  if(${forFill} && !('value' in e) && !e.isContentEditable) return {error:'not an editable field (observe again)'};
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "PawBrowse",
4
- "version": "0.6.4",
4
+ "version": "0.6.5",
5
5
  "description": "Let your AI agent (Claude Code) drive your real Chrome. Fast element-table control. Open source, no keys.",
6
6
  "minimum_chrome_version": "125",
7
7
  "permissions": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pawbrowse",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Let Claude Code drive your real, logged-in Chrome. A zero-dependency MCP server + a Chrome MV3 extension that uses element-table perception over CDP. The calling agent is the policy: no second model, no API keys, page snapshots never leave for a third party.",
5
5
  "type": "module",
6
6
  "bin": {