# adom-desktop v1.4.10 follow-ups — two remaining v1.4.9 issues

Paste into Claude on the desktop. v1.4.9 ships smart-pick refinement + `browser_fetch_url` saveTo split + tab_not_found error. Verified on container side:

- ✅ **`browser_fetch_url` saveTo:container** works perfectly (TI INA226 datasheet, 1.5 MB, valid PDF, lands at the path stated, `savedToFilesystem:"container"` flag set). 🎉
- ⚠️ **modal-scoped smart-pick** has a false-positive on Vue-Toastification toast containers
- ❌ **`tab_not_found` error** is not actually implemented — `browser_eval` still silently falls back to the active tab

Concrete repros for both below.

---

## Item 1 — modal detection false-positives on Vue-Toastification (and similar empty toast containers)

### The bug

WAGO's product page (`https://www.wago.com/global/p/2601-3105`) ships Vue-Toastification. Even with no toast active, four full-page-height toast containers exist in DOM, each `<div class="Vue-Toastification__container ...">` with `position:fixed; z-index:9999; height:100vh`. They cover >25% viewport, beat the v1.4.9 modal-detection heuristic, and become the "modal root."

The actual Generate-Datasheet modal is a sibling `<div>` rendered by Vue Teleport at `body > div`, with no `[role=dialog]`, no `aria-modal`, lower z-index than the toast container. So smart-pick scopes to the wrong "modal" and the Generate button matches **outside** it.

```bash
adom-desktop browser_input_dispatch '{
  "sessionId":"chip-fetcher",
  "type":"click",
  "selector":"button.wg-button--primary"
}'
# → matchedCount: 3
#   modalDetected: true
#   insideModal: false
#   pickStrategy: "visible-text-largest-no-modal-match"
#   clickedText: "Add to shopping cart"   ← wrong; Generate is in the real modal
```

DOM proof:

```js
// Run in pup eval against an open WAGO modal
JSON.stringify({
  fixedOver25pct: Array.from(document.querySelectorAll('*'))
    .filter(el => {
      const cs = getComputedStyle(el);
      if (!['fixed','absolute'].includes(cs.position)) return false;
      const r = el.getBoundingClientRect();
      return r.width * r.height / (innerWidth * innerHeight) > 0.25;
    })
    .map(el => ({
      tag: el.tagName,
      cls: (el.className || '').toString().slice(0,40),
      z: getComputedStyle(el).zIndex,
      hasInteractiveChild: !!el.querySelector('button, input, a, [role=button]'),
      childTextLen: (el.textContent || '').trim().length
    }))
});
// Returns 5 entries:
//   - wg-nav-overlay (z=-1, hidden behind page)
//   - 4× Vue-Toastification__container (z=9999, hasInteractiveChild=false, childTextLen=0)
// The actual Generate modal is missed because it's a body-level <div> not in
// the >25%-viewport-fixed-or-absolute set.
```

### What I want

The detection heuristic should require the candidate to **contain at least one visible interactive element with non-empty text**. Empty toast containers fail that filter. The actual modal (which has the Generate button + cancel link + checkbox grid) passes it.

```js
// In pickElementForInputDispatch's modal-root detection:
const isPlausibleModal = (el) => {
  // Must contain at least one visible button/link/input with non-empty text
  const interactives = Array.from(el.querySelectorAll('button, a, input, [role=button]'));
  return interactives.some(child => {
    const cs = getComputedStyle(child);
    if (cs.display === 'none' || cs.visibility === 'hidden') return false;
    if ((child.textContent || '').trim().length === 0 && child.tagName !== 'INPUT') return false;
    return true;
  });
};
const candidates = fixedOver25pct.filter(isPlausibleModal);
```

This change should be small — fixedOver25pct list is already computed; just add the post-filter.

Add a `modalRootInfo` debug field to the response so callers can see what got picked (or didn't):

```json
{
  "modalDetected": true,
  "modalRoot": { "tag": "DIV", "cls": "datasheet-modal", "z": "1000" },
  "insideModal": true,
  "pickStrategy": "modal-scoped-largest"
}
```

### Acceptance test

After fix, the WAGO recipe with `selector:"button.wg-button--primary"` should return `clickedText:"Generate data sheet"`, `pickStrategy:"modal-scoped-largest"`, `insideModal:true`, with `modalRoot` referencing a div that actually contains the modal contents (not a Vue-Toastification container).

### Workaround for now

Same as before v1.4.9: compute the button's bounding rect via `browser_eval` and pass `{x, y}` coords. Defeats the smart-pick value-add for modals.

---

## Item 2 — `tab_not_found` error path is not implemented (regression / missing implementation)

### The bug

The v1.4.9 release notes promised:

> When you pass tabId:"tab-2" but the popup has auto-closed, you no longer silently get the active tab's response.

But it still does:

```bash
adom-desktop browser_eval '{
  "sessionId":"chip-fetcher",
  "tabId":"tab-999",
  "expr":"location.href"
}'
```

```json
{
  "ok": true,
  "result": "https://www.wago.com/global/p/2601-3105",
  "sessionId": "chip-fetcher",
  "tabId": "tab-1"
}
```

`tab-999` doesn't exist (only `tab-1` is open). Eval routed to `tab-1` and reported `tabId:"tab-1"` in the response. No `errorCode`, no `currentTabs`, no `_hint`. Same silent-fallback as v1.4.7.

The bridge-side code path that was supposed to land in v1.4.9 didn't (or got reverted, or didn't make it into the build). v1.4.10 should ship it.

### What I want

The promised v1.4.9 behavior:

```json
{
  "ok": false,
  "error": "Tab \"tab-999\" not found in session \"chip-fetcher\".",
  "errorCode": "tab_not_found",
  "currentTabs": ["tab-1"],
  "_hint": "Most likely the popup auto-closed (Chrome PDF viewer with Content-Disposition: attachment closes after download). Use browser_list_tabs IMMEDIATELY after the click to capture the popup URL, then browser_fetch_url with that URL — never eval-against-popup-then-fetch."
}
```

This applies to **all** tab-aware verbs: `browser_eval`, `browser_screenshot`, `browser_errors`, `browser_reload`, `browser_navigate`, `browser_close_tab`. Each should hard-error on missing tabId rather than fall back to active.

### Acceptance test

```bash
# After fix
adom-desktop browser_eval '{"sessionId":"x","tabId":"tab-bogus","expr":"1"}'
# → {"ok":false,"errorCode":"tab_not_found",...}
adom-desktop browser_screenshot '{"sessionId":"x","tabId":"tab-bogus"}'
# → {"ok":false,"errorCode":"tab_not_found",...}
adom-desktop browser_navigate '{"sessionId":"x","tabId":"tab-bogus","url":"https://example.com"}'
# → {"ok":false,"errorCode":"tab_not_found",...}
```

### Workaround for now

Always check `browser_list_tabs` for the tab's existence before eval-ing against it. Or trust that `browser_eval` `tabId` arg silently falls back to active and never assume it actually targeted the tab you passed. Both are bad — please ship v1.4.9's promised behavior in v1.4.10.

---

## Suggested PR shape

- **Item 1** — `pickElementForInputDispatch.detectModalRoot` adds the `isPlausibleModal` filter (interactive + non-empty text child required). Returns `modalRoot` debug field. Test: WAGO scenario should land on the real modal.
- **Item 2** — for each tab-aware verb in the bridge dispatch, look up the `tabId` and return the structured `tab_not_found` error before falling back. Add the listed `_hint`. Add an integration test that opens a session, closes a tab, then attempts eval against the closed tabId and asserts `errorCode:"tab_not_found"`.

Also worth considering: **after Item 2, when the bridge gets `tab_not_found` for a popup tabId that was just listed as `opener:"popup"` (auto-closed mid-flow), include the popup's last-known URL in the error**: `lastKnownUrl: "..."`, so the caller can immediately call `browser_fetch_url` with it instead of needing to re-list_tabs (which by definition won't show the gone tab).

---

## Container-side update flow

```bash
curl -L -o /tmp/cli-fresh \
  https://wiki-ufypy5dpx93o.adom.cloud/static/apps/adom-desktop/adom-desktop-linux
sudo install -m 755 /tmp/cli-fresh /usr/local/bin/adom-desktop
adom-desktop --version
# Need 1.4.10 or newer
```

Plus the `taskkill //f //im node.exe` reminder on the desktop side (we still have to do that manually after every release; if there's a way to trigger that automatically from `adom-desktop install`, please do).

— sent from john's container running chip-fetcher v0.1.x against adom-desktop v1.4.9
