fix: stale session client ref and CUA smoke test improvements (#30)
* fix(sessions): use latestConnState.client to avoid stale reference after reconnect Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JKGMRpgihA4io2frodqLjt * fix(cua): lru_cache get_screen_size, remove redundant if-matches guard, drop inner import re Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JKGMRpgihA4io2frodqLjt * fix(cua): use center-x comparator for send button, defer screen_w fetch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JKGMRpgihA4io2frodqLjt --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,7 @@ import time
|
|||||||
import threading
|
import threading
|
||||||
import re
|
import re
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -361,20 +362,20 @@ def execute_action(action: dict) -> str:
|
|||||||
# Threshold is screen-relative (bottom 25%) so it works on any emulator
|
# Threshold is screen-relative (bottom 25%) so it works on any emulator
|
||||||
# resolution — API 30 default profile is 1080x1920, not the 2400-tall pixel
|
# resolution — API 30 default profile is 1080x1920, not the 2400-tall pixel
|
||||||
# we previously hardcoded against.
|
# we previously hardcoded against.
|
||||||
screen_w, screen_h = get_screen_size()
|
_, screen_h = get_screen_size()
|
||||||
bottom_threshold = int(screen_h * 0.75)
|
bottom_threshold = int(screen_h * 0.75)
|
||||||
xml = ui_dump()
|
xml = ui_dump()
|
||||||
matches = re.findall(r'clickable="true"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"', xml)
|
matches = re.findall(r'clickable="true"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"', xml)
|
||||||
if matches:
|
bottom_buttons = [(int(x1), int(y1), int(x2), int(y2)) for x1, y1, x2, y2 in matches if int(y1) > bottom_threshold]
|
||||||
bottom_buttons = [(int(x1), int(y1), int(x2), int(y2)) for x1, y1, x2, y2 in matches if int(y1) > bottom_threshold]
|
if bottom_buttons:
|
||||||
if bottom_buttons:
|
# Rightmost = highest center-x (not left-edge x1, which misidentifies wide buttons)
|
||||||
# Rightmost = highest x1
|
send_btn = max(bottom_buttons, key=lambda b: (b[0] + b[2]) // 2)
|
||||||
send_btn = max(bottom_buttons, key=lambda b: b[0])
|
cx = (send_btn[0] + send_btn[2]) // 2
|
||||||
cx = (send_btn[0] + send_btn[2]) // 2
|
cy = (send_btn[1] + send_btn[3]) // 2
|
||||||
cy = (send_btn[1] + send_btn[3]) // 2
|
adb("shell", "input", "tap", str(cx), str(cy))
|
||||||
adb("shell", "input", "tap", str(cx), str(cy))
|
return f"send button tapped ({cx}, {cy})"
|
||||||
return f"send button tapped ({cx}, {cy})"
|
|
||||||
# Fallback: tap bottom-right corner of the screen, offset slightly inward
|
# Fallback: tap bottom-right corner of the screen, offset slightly inward
|
||||||
|
screen_w, _ = get_screen_size()
|
||||||
fx = screen_w - 80
|
fx = screen_w - 80
|
||||||
fy = screen_h - 120
|
fy = screen_h - 120
|
||||||
adb("shell", "input", "tap", str(fx), str(fy))
|
adb("shell", "input", "tap", str(fx), str(fy))
|
||||||
@@ -476,8 +477,14 @@ def make_client(model: str):
|
|||||||
sys.exit("Set AZURE_OPENAI_API_KEY, AZURE_DEV_AI_API_KEY, OPENAI_API_KEY, XAI_API_KEY, or GEMINI_API_KEY")
|
sys.exit("Set AZURE_OPENAI_API_KEY, AZURE_DEV_AI_API_KEY, OPENAI_API_KEY, XAI_API_KEY, or GEMINI_API_KEY")
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
def get_screen_size() -> tuple[int, int]:
|
def get_screen_size() -> tuple[int, int]:
|
||||||
"""Return (width, height) of the connected device screen."""
|
"""Return (width, height) of the connected device screen.
|
||||||
|
|
||||||
|
Cached after the first call — screen dimensions are stable for the
|
||||||
|
lifetime of a test run, and caching avoids a redundant ADB round-trip
|
||||||
|
on every `send` action.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
out = adb("shell", "wm", "size")
|
out = adb("shell", "wm", "size")
|
||||||
# "Physical size: 1080x1920" or "Override size: 1080x1920"
|
# "Physical size: 1080x1920" or "Override size: 1080x1920"
|
||||||
@@ -528,8 +535,7 @@ def run_cua(goal: str, max_steps: int = 30, model: str = "gpt-4o",
|
|||||||
if clean.startswith("```"):
|
if clean.startswith("```"):
|
||||||
clean = clean.split("\n", 1)[1].rsplit("```", 1)[0].strip()
|
clean = clean.split("\n", 1)[1].rsplit("```", 1)[0].strip()
|
||||||
# If model returned multiple JSON objects, take the first
|
# If model returned multiple JSON objects, take the first
|
||||||
import re as _re
|
m = re.search(r'\{[^{}]*\}', clean)
|
||||||
m = _re.search(r'\{[^{}]*\}', clean)
|
|
||||||
if m:
|
if m:
|
||||||
action = json.loads(m.group(0))
|
action = json.loads(m.group(0))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export const useSessions = create<SessionsState>((set, get) => ({
|
|||||||
const scopeDir = sessionScopeDirectory(Boolean(latestConnState.activeConnection?.directory), home)
|
const scopeDir = sessionScopeDirectory(Boolean(latestConnState.activeConnection?.directory), home)
|
||||||
const listClient = scopeDir ? latestConnState.clientForDirectory(scopeDir) : latestConnState.client
|
const listClient = scopeDir ? latestConnState.clientForDirectory(scopeDir) : latestConnState.client
|
||||||
|
|
||||||
const sessions = await (listClient || connState.client).session.list({ roots: true, limit: 50 })
|
const sessions = await (listClient || latestConnState.client!).session.list({ roots: true, limit: 50 })
|
||||||
set({ sessions, isLoading: false })
|
set({ sessions, isLoading: false })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({ error: "Failed to load sessions", isLoading: false })
|
set({ error: "Failed to load sessions", isLoading: false })
|
||||||
|
|||||||
Reference in New Issue
Block a user