[eric] browser: restore the Send-button handoff (points the model at the Send index after a fill); deleting it brought the Send-hunt back, it was NOT redundant with the wait

This commit is contained in:
ciregenz
2026-06-08 00:58:38 -07:00
parent bf5f95573a
commit 8d9c75803c
2 changed files with 40 additions and 1 deletions
+29 -1
View File
@@ -196,6 +196,22 @@ def _delta_state(text: str, seen_lines: set[str]) -> str:
)
# A button row whose name is exactly a Send control (not "Send InMail credit" or
# "Send a message to X"); used to hand the model the Send button after it types,
# so it never burns turns hunting a button that's right there.
_SEND_ROW_RE = re.compile(r'\[(\d+)\]\*?<\s*button\s+"([^"]*)"', re.I)
def _send_index_in_state(state_text: str):
"""(index, name) of a real Send button in an interactives list, or None.
Strict exact match so it never grabs an upsell or a profile 'Send a message' link."""
for line in (state_text or "").splitlines():
m = _SEND_ROW_RE.search(line)
if m and m.group(2).strip().lower() in ("send", "send now", "send message"):
return int(m.group(1)), m.group(2)
return None
def _is_composer_fill(tool_name: str, tool_input: dict) -> bool:
"""True if this action typed a message into a composer (the moment the Send
button is about to matter). Covers the solo fill, BrowserType, and a batched
@@ -251,7 +267,19 @@ async def _post_action_state(
if not isinstance(lst, dict) or "error" in lst or not lst.get("text"):
return ""
state = lst["text"] if seen_lines is None else _delta_state(lst["text"], seen_lines)
return f"\n\n{PAGE_STATE_MARKER}\n{_truncate_state(state)}"
out = f"\n\n{PAGE_STATE_MARKER}\n{_truncate_state(state)}"
# Hand the Send button's index over after a composer fill so the model clicks it
# directly instead of scanning the list or hunting via CSS/JS/screenshots. This is
# the POINTER; the wait above is what makes Send actually present to point at, the
# two work together (removing this brought the Send-hunt back, observed live).
if _composer_fill:
_si = _send_index_in_state(lst["text"])
if _si:
out = (f"\n\n[send-ready] Your message is typed and the Send button is index "
f"{_si[0]} below. To deliver, click it SOLO with BrowserClickIndex + an "
f"`expect` proof. Do NOT hunt for it with CSS/JS/screenshots, it is right here."
) + out
return out
async def _request_browser_approval(
+11
View File
@@ -1440,6 +1440,17 @@ def test_composer_fill_detection():
assert not _is_composer_fill("BrowserScroll", {})
def test_send_index_handoff_points_only_at_a_real_send_button():
# after a composer fill we hand the model the Send button's index so it clicks
# it directly instead of hunting; must never mistake an upsell/profile link for it
from backend.apps.agents.browser.browser_agent import _send_index_in_state
page = '[1]<link "Tyler Chen">\n[33]<textbox "Write a message">\n[44]<button "Send">'
assert _send_index_in_state(page) == (44, "Send")
assert _send_index_in_state('[12]<button "Send InMail credit">') is None
assert _send_index_in_state('[5]<button "Send a message to Maya">') is None
assert _send_index_in_state("") is None
def test_strip_lone_surrogates():
from backend.apps.agents.browser.browser_agent import _strip_lone_surrogates, _format_tool_result
# an orphan UTF-16 surrogate (half an emoji from the webview) is what crashes