feat: UI feedback on actions + annotated screenshots
This commit is contained in:
parent
74a055a4e7
commit
c9f9fdb6b0
@ -160,6 +160,46 @@ async def tool_list_windows(args: dict[str, Any]) -> dict[str, Any]:
|
|||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_ui_feedback() -> dict[str, Any]:
|
||||||
|
"""Collect UI state feedback after an action: cursor position, focused element, active window."""
|
||||||
|
feedback: dict[str, Any] = {}
|
||||||
|
try:
|
||||||
|
import pyautogui
|
||||||
|
mx, my = pyautogui.position()
|
||||||
|
feedback["cursor"] = {"x": mx, "y": my}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
import uiautomation as auto
|
||||||
|
fw = auto.GetForegroundControl()
|
||||||
|
if fw:
|
||||||
|
feedback["active_window"] = fw.Name or ""
|
||||||
|
focused = auto.GetFocusedControl()
|
||||||
|
if focused:
|
||||||
|
fe: dict[str, Any] = {
|
||||||
|
"type": focused.ControlTypeName or "",
|
||||||
|
"name": focused.Name or "",
|
||||||
|
}
|
||||||
|
if focused.AutomationId:
|
||||||
|
fe["automation_id"] = focused.AutomationId
|
||||||
|
try:
|
||||||
|
vp = focused.GetValuePattern()
|
||||||
|
if vp:
|
||||||
|
fe["value"] = vp.Value
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
rect = focused.BoundingRectangle
|
||||||
|
if rect and rect.width() > 0:
|
||||||
|
fe["rect"] = {"x": rect.left, "y": rect.top, "w": rect.width(), "h": rect.height()}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
feedback["focused_element"] = fe
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return feedback
|
||||||
|
|
||||||
|
|
||||||
async def tool_click_element(args: dict[str, Any]) -> dict[str, Any]:
|
async def tool_click_element(args: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Click a UI element by name, automation_id, or coordinates."""
|
"""Click a UI element by name, automation_id, or coordinates."""
|
||||||
name = args.get("name")
|
name = args.get("name")
|
||||||
@ -178,7 +218,8 @@ async def tool_click_element(args: dict[str, Any]) -> dict[str, Any]:
|
|||||||
pyautogui.rightClick(x, y)
|
pyautogui.rightClick(x, y)
|
||||||
else:
|
else:
|
||||||
pyautogui.click(x, y)
|
pyautogui.click(x, y)
|
||||||
return {"success": True, "method": "coordinates", "x": x, "y": y}
|
await asyncio.sleep(0.3)
|
||||||
|
return {"success": True, "method": "coordinates", "x": x, "y": y, **_get_ui_feedback()}
|
||||||
|
|
||||||
import uiautomation as auto
|
import uiautomation as auto
|
||||||
ctrl = None
|
ctrl = None
|
||||||
@ -192,7 +233,8 @@ async def tool_click_element(args: dict[str, Any]) -> dict[str, Any]:
|
|||||||
ctrl.DoubleClick()
|
ctrl.DoubleClick()
|
||||||
else:
|
else:
|
||||||
ctrl.Click()
|
ctrl.Click()
|
||||||
return {"success": True, "method": "ui_automation", "element": ctrl.Name}
|
await asyncio.sleep(0.3)
|
||||||
|
return {"success": True, "method": "ui_automation", "element": ctrl.Name, **_get_ui_feedback()}
|
||||||
else:
|
else:
|
||||||
return {"error": f"Element not found: name={name}, automation_id={auto_id}"}
|
return {"error": f"Element not found: name={name}, automation_id={auto_id}"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -203,6 +245,7 @@ async def tool_type_text(args: dict[str, Any]) -> dict[str, Any]:
|
|||||||
"""Type text, optionally into a specific element."""
|
"""Type text, optionally into a specific element."""
|
||||||
text = args.get("text", "")
|
text = args.get("text", "")
|
||||||
element_name = args.get("element_name")
|
element_name = args.get("element_name")
|
||||||
|
clear_first = args.get("clear_first", False)
|
||||||
|
|
||||||
if not text:
|
if not text:
|
||||||
return {"error": "No text provided"}
|
return {"error": "No text provided"}
|
||||||
@ -213,15 +256,21 @@ async def tool_type_text(args: dict[str, Any]) -> dict[str, Any]:
|
|||||||
ctrl = auto.Control(searchDepth=8, Name=element_name)
|
ctrl = auto.Control(searchDepth=8, Name=element_name)
|
||||||
if ctrl.Exists(maxSearchSeconds=3):
|
if ctrl.Exists(maxSearchSeconds=3):
|
||||||
try:
|
try:
|
||||||
|
if clear_first:
|
||||||
|
ctrl.GetValuePattern().SetValue("")
|
||||||
ctrl.GetValuePattern().SetValue(text)
|
ctrl.GetValuePattern().SetValue(text)
|
||||||
return {"success": True, "method": "set_value", "element": element_name}
|
return {"success": True, "method": "set_value", "element": element_name, **_get_ui_feedback()}
|
||||||
except Exception:
|
except Exception:
|
||||||
ctrl.Click()
|
ctrl.Click()
|
||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
|
|
||||||
import pyautogui
|
import pyautogui
|
||||||
|
if clear_first:
|
||||||
|
pyautogui.hotkey("ctrl", "a")
|
||||||
|
await asyncio.sleep(0.05)
|
||||||
pyautogui.typewrite(text, interval=0.02) if text.isascii() else pyautogui.write(text)
|
pyautogui.typewrite(text, interval=0.02) if text.isascii() else pyautogui.write(text)
|
||||||
return {"success": True, "method": "keyboard"}
|
await asyncio.sleep(0.1)
|
||||||
|
return {"success": True, "method": "keyboard", **_get_ui_feedback()}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@ -239,14 +288,15 @@ async def tool_press_keys(args: dict[str, Any]) -> dict[str, Any]:
|
|||||||
pyautogui.hotkey(*parts)
|
pyautogui.hotkey(*parts)
|
||||||
else:
|
else:
|
||||||
pyautogui.press(parts[0])
|
pyautogui.press(parts[0])
|
||||||
return {"success": True, "keys": keys}
|
await asyncio.sleep(0.3)
|
||||||
|
return {"success": True, "keys": keys, **_get_ui_feedback()}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
async def tool_screenshot(args: dict[str, Any]) -> dict[str, Any]:
|
async def tool_screenshot(args: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Take a screenshot and return as base64 PNG."""
|
"""Take a screenshot with UI annotations: cursor marker + focused element highlight."""
|
||||||
from PIL import ImageGrab
|
from PIL import ImageGrab, ImageDraw
|
||||||
|
|
||||||
try:
|
try:
|
||||||
region = args.get("region")
|
region = args.get("region")
|
||||||
@ -256,6 +306,43 @@ async def tool_screenshot(args: dict[str, Any]) -> dict[str, Any]:
|
|||||||
else:
|
else:
|
||||||
img = ImageGrab.grab()
|
img = ImageGrab.grab()
|
||||||
|
|
||||||
|
# Draw annotations on the screenshot
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
# 1. Draw cursor position (red crosshair)
|
||||||
|
try:
|
||||||
|
import pyautogui
|
||||||
|
mx, my = pyautogui.position()
|
||||||
|
# Adjust if region capture
|
||||||
|
if region:
|
||||||
|
mx -= region["x"]
|
||||||
|
my -= region["y"]
|
||||||
|
r = 12
|
||||||
|
draw.ellipse([mx - r, my - r, mx + r, my + r], outline="red", width=3)
|
||||||
|
draw.line([mx - r - 4, my, mx + r + 4, my], fill="red", width=2)
|
||||||
|
draw.line([mx, my - r - 4, mx, my + r + 4], fill="red", width=2)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 2. Highlight focused element (green rectangle + label)
|
||||||
|
try:
|
||||||
|
import uiautomation as auto
|
||||||
|
focused = auto.GetFocusedControl()
|
||||||
|
if focused:
|
||||||
|
rect = focused.BoundingRectangle
|
||||||
|
if rect and rect.width() > 0:
|
||||||
|
fx, fy = rect.left, rect.top
|
||||||
|
fw, fh = rect.width(), rect.height()
|
||||||
|
if region:
|
||||||
|
fx -= region["x"]
|
||||||
|
fy -= region["y"]
|
||||||
|
draw.rectangle([fx, fy, fx + fw, fy + fh], outline="lime", width=2)
|
||||||
|
label = focused.Name or focused.ControlTypeName or ""
|
||||||
|
if label:
|
||||||
|
draw.text((fx + 2, fy - 14), label[:40], fill="lime")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Resize for efficiency (max 1280px wide)
|
# Resize for efficiency (max 1280px wide)
|
||||||
if img.width > 1280:
|
if img.width > 1280:
|
||||||
ratio = 1280 / img.width
|
ratio = 1280 / img.width
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user