diff --git a/backend/apps/agents/browser/browser_send_script.py b/backend/apps/agents/browser/browser_send_script.py index a6c4f92e..3129d2f2 100644 --- a/backend/apps/agents/browser/browser_send_script.py +++ b/backend/apps/agents/browser/browser_send_script.py @@ -39,6 +39,16 @@ def quoted_payload(task: str) -> str: return spans.pop() if len(spans) == 1 else "" +P_OPENER_ROW_RE = re.compile(r"\[(\d+)\]\*?<\s*(?:link|button)\s+\"(Message|Reply|Compose|New message)\"", re.I) + + +def opener_index_in_state(state_text: str): + """(index, name) of the single exact-named composer OPENER, or None. Exact + names only, so an upsell like 'Send InMail' can never match.""" + hits = [(int(m.group(1)), m.group(2)) for m in P_OPENER_ROW_RE.finditer(state_text or "")] + return hits[0] if len(hits) == 1 else None + + def composer_index_in_state(state_text: str): """(index, name) of the single compose-shaped textbox, or None. Two candidates = ambiguous = model's problem.""" @@ -62,9 +72,7 @@ async def run_send_script( 'payload': str, 'log': [...], 'note': str}.""" t0 = time.monotonic() payload = quoted_payload(task) - composer = composer_index_in_state(state_text) - # No Send-button precondition: composer sites (LinkedIn) lazy-render Send only AFTER text commits, so it's resolved post-fill; never appearing = clean pre-click abort. - if not (payload and composer): + if not payload: return None log: list[dict] = [] @@ -76,6 +84,28 @@ async def run_send_script( except Exception: return "" + composer = composer_index_in_state(state_text) + if not composer: + # Reversible-opener hop: prestage often stops on the profile with the "Message" opener visible (its settle raced the overlay). Opening a composer is the allowed opener class; the irreversible bar is unchanged. + opener = opener_index_in_state(state_text) + if not opener: + return None + r_open = await execute_tool("BrowserClickIndex", {"index": opener[0]}, browser_id, tab_id) + if not (isinstance(r_open, dict) and "error" not in r_open): + return None + log.append({"tool": "BrowserClickIndex", "input": {"index": opener[0]}, "ok": True, + "result_summary": f"script opened composer via {opener[1]!r}"[:200], "elapsed_ms": 0}) + for wait_s in (0.6, 1.2): + await asyncio.sleep(wait_s) + state_text = await fresh_list() + composer = composer_index_in_state(state_text) + if composer: + break + if not composer: + logger.info("[browser-sendscript] opener clicked but no composer appeared; handing to model") + return None + # No Send-button precondition: composer sites (LinkedIn) lazy-render Send only AFTER text commits, so it's resolved post-fill; never appearing = clean pre-click abort. + # 1. fill (focused by node, the composer overlay path coordinate clicks miss) r_fill = await execute_tool("BrowserClickIndex", {"index": composer[0], "text": payload}, browser_id, tab_id) fill_ok = isinstance(r_fill, dict) and "error" not in r_fill