Skip to content

Commit 913603b

Browse files
committed
Fix coordinate scaling for gemini
1 parent 0401b41 commit 913603b

3 files changed

Lines changed: 67 additions & 40 deletions

File tree

hud/tools/computer/gemini.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,12 @@ async def __call__(
228228

229229
# Helper to finalize ContentResult: rescale if requested and ensure URL metadata
230230
async def _finalize(
231-
result: ContentResult, requested_url: str | None = None
231+
result: ContentResult,
232+
requested_url: str | None = None,
233+
output: str | None = None,
232234
) -> list[ContentBlock]:
235+
if output is not None and not result.error:
236+
result.output = output
233237
if result.base64_image and self.rescale_images:
234238
try:
235239
result.base64_image = await self._rescale_screenshot(result.base64_image)
@@ -253,14 +257,14 @@ async def _finalize(
253257
raise McpError(ErrorData(code=INVALID_PARAMS, message="x and y are required"))
254258
sx, sy = self._scale_coordinates(x, y)
255259
result = await self.executor.click(x=sx, y=sy)
256-
return await _finalize(result)
260+
return await _finalize(result, output=f"Clicked at ({x}, {y})")
257261

258262
elif action == "hover_at":
259263
if x is None or y is None:
260264
raise McpError(ErrorData(code=INVALID_PARAMS, message="x and y are required"))
261265
sx, sy = self._scale_coordinates(x, y)
262266
result = await self.executor.move(x=sx, y=sy)
263-
return await _finalize(result)
267+
return await _finalize(result, output=f"Hovered at ({x}, {y})")
264268

265269
elif action == "type_text_at":
266270
if x is None or y is None:
@@ -284,7 +288,7 @@ async def _finalize(
284288

285289
# Type (optionally press enter after)
286290
result = await self.executor.write(text=text, enter_after=bool(press_enter))
287-
return await _finalize(result)
291+
return await _finalize(result, output=f"Typed text at ({x}, {y})")
288292

289293
elif action == "scroll_document":
290294
if direction is None:
@@ -317,7 +321,7 @@ async def _finalize(
317321
ErrorData(code=INVALID_PARAMS, message=f"Invalid direction: {direction}")
318322
)
319323
result = await self.executor.scroll(scroll_x=scroll_x, scroll_y=scroll_y)
320-
return await _finalize(result)
324+
return await _finalize(result, output=f"Scrolled document {direction}")
321325

322326
elif action == "scroll_at":
323327
if direction is None:
@@ -351,7 +355,7 @@ async def _finalize(
351355
ErrorData(code=INVALID_PARAMS, message=f"Invalid direction: {direction}")
352356
)
353357
result = await self.executor.scroll(x=sx, y=sy, scroll_x=scroll_x, scroll_y=scroll_y)
354-
return await _finalize(result)
358+
return await _finalize(result, output=f"Scrolled {direction} at ({x}, {y})")
355359

356360
elif action == "wait_5_seconds":
357361
result = await self.executor.wait(time=5000)
@@ -419,7 +423,10 @@ async def _finalize(
419423
)
420424
path = self._inset_scaled_drag_path(path)
421425
result = await self.executor.drag(path=path)
422-
return await _finalize(result)
426+
return await _finalize(
427+
result,
428+
output=f"Dragged from ({x}, {y}) to ({destination_x}, {destination_y})",
429+
)
423430

424431
else:
425432
raise McpError(ErrorData(code=INVALID_PARAMS, message=f"Unknown action: {action}"))

hud/tools/computer/hud.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525

2626
class AgentCoordinate(int):
27-
"""Carry both execution and model-visible coordinate values."""
27+
"""Execution pixel coordinate with optional model-coordinate metadata."""
2828

2929
agent_value: int
3030

@@ -33,15 +33,6 @@ def __new__(cls, value: int, agent_value: int) -> Self:
3333
obj.agent_value = agent_value
3434
return obj
3535

36-
def __format__(self, format_spec: str) -> str:
37-
return format(self.agent_value, format_spec)
38-
39-
def __str__(self) -> str:
40-
return str(int(self))
41-
42-
def __repr__(self) -> str:
43-
return repr(self.agent_value)
44-
4536

4637
class HudComputerTool(BaseTool):
4738
"""

hud/tools/computer/tests/test_computer.py

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from hud.tools.computer.anthropic import AnthropicComputerTool
99
from hud.tools.computer.gemini import GeminiComputerTool
1010
from hud.tools.computer.glm import GLMComputerTool
11-
from hud.tools.computer.hud import HudComputerTool
11+
from hud.tools.computer.hud import AgentCoordinate, HudComputerTool
1212
from hud.tools.computer.openai import OpenAIComputerTool
1313
from hud.tools.computer.qwen import QwenComputerTool
1414
from hud.tools.executors.base import BaseExecutor
@@ -75,12 +75,28 @@ async def test_anthropic_computer_screenshot():
7575

7676

7777
@pytest.mark.asyncio
78-
async def test_gemini_computer_click_reports_agent_coordinates():
78+
async def test_gemini_computer_scaling_preserves_model_coordinates():
7979
comp = GeminiComputerTool()
80+
x, y = comp._scale_coordinates(214, 420)
81+
82+
assert x is not None
83+
assert y is not None
84+
assert int(x) != 214
85+
assert int(y) != 420
86+
assert getattr(x, "agent_value") == 214
87+
assert getattr(y, "agent_value") == 420
88+
89+
90+
@pytest.mark.asyncio
91+
async def test_gemini_computer_click_reports_model_coordinates():
92+
comp = GeminiComputerTool(executor=BaseExecutor())
93+
8094
blocks = await comp(action="click_at", x=214, y=420)
8195

8296
assert any(
83-
"(214, 420)" in content.text for content in blocks if isinstance(content, TextContent)
97+
"Clicked at (214, 420)" in content.text
98+
for content in blocks
99+
if isinstance(content, TextContent)
84100
)
85101

86102

@@ -125,40 +141,44 @@ async def test_anthropic_computer_zoom():
125141

126142
@pytest.mark.asyncio
127143
async def test_openai_computer_click():
128-
comp = OpenAIComputerTool()
144+
comp = OpenAIComputerTool(executor=BaseExecutor())
129145
blocks = await comp(type="click", x=5, y=5)
130146
assert blocks
131-
assert any("(5, 5)" in content.text for content in blocks if isinstance(content, TextContent))
132147

133148

134149
@pytest.mark.asyncio
135-
async def test_anthropic_computer_click_reports_agent_coordinates():
136-
comp = AnthropicComputerTool()
137-
blocks = await comp(action="left_click", coordinate=[123, 456], text=None)
150+
async def test_anthropic_computer_scaling_preserves_agent_coordinates():
151+
comp = AnthropicComputerTool(executor=BaseExecutor())
152+
x, y = comp._scale_coordinates(123, 456)
138153

139-
assert any(
140-
"(123, 456)" in content.text for content in blocks if isinstance(content, TextContent)
141-
)
154+
assert x is not None
155+
assert y is not None
156+
assert getattr(x, "agent_value") == 123
157+
assert getattr(y, "agent_value") == 456
142158

143159

144160
@pytest.mark.asyncio
145-
async def test_qwen_computer_click_reports_agent_coordinates():
146-
comp = QwenComputerTool()
147-
blocks = await comp(action="left_click", coordinate=[123, 456])
161+
async def test_qwen_computer_scaling_preserves_agent_coordinates():
162+
comp = QwenComputerTool(executor=BaseExecutor())
163+
x, y = comp._scale_coordinates(123, 456)
148164

149-
assert any(
150-
"(123, 456)" in content.text for content in blocks if isinstance(content, TextContent)
151-
)
165+
assert x is not None
166+
assert y is not None
167+
assert getattr(x, "agent_value") == 123
168+
assert getattr(y, "agent_value") == 456
152169

153170

154171
@pytest.mark.asyncio
155-
async def test_glm_computer_click_reports_agent_coordinates():
156-
comp = GLMComputerTool()
157-
blocks = await comp(action="left_click", start_box="[123,456]")
172+
async def test_glm_computer_scaling_preserves_model_coordinates():
173+
comp = GLMComputerTool(executor=BaseExecutor())
174+
x, y = comp._scale_coordinates(123, 456)
158175

159-
assert any(
160-
"(123, 456)" in content.text for content in blocks if isinstance(content, TextContent)
161-
)
176+
assert x is not None
177+
assert y is not None
178+
assert int(x) != 123
179+
assert int(y) != 456
180+
assert getattr(x, "agent_value") == 123
181+
assert getattr(y, "agent_value") == 456
162182

163183

164184
def test_normalized_coordinate_max_stays_in_display_bounds():
@@ -217,6 +237,15 @@ async def test_xdo_drag_executes_interpolated_mouse_moves():
217237
assert mouse_moves[-1] == "mousemove 120 0"
218238

219239

240+
@pytest.mark.asyncio
241+
async def test_xdo_commands_use_execution_pixels_for_agent_coordinates():
242+
executor = RecordingXDOExecutor()
243+
244+
await executor.click(x=AgentCoordinate(309, 214), y=AgentCoordinate(396, 420))
245+
246+
assert executor.commands[-1] == "mousemove 309 396 click 1"
247+
248+
220249
class TestHudComputerToolExtended:
221250
"""Extended tests for HudComputerTool covering edge cases and platform logic."""
222251

0 commit comments

Comments
 (0)