Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions tools/computer_use/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"wait",
"list_apps",
"focus_app",
"switch_desktop",
],
Comment thread
Icather marked this conversation as resolved.
"description": (
"Which action to perform. `capture` is free (no side "
Expand Down Expand Up @@ -208,6 +209,16 @@
"matching the background co-work model."
),
},
# ── switch_desktop ──────────────────────────────────────
"direction": {
"type": "string",
"enum": ["left", "right"],
"description": (
"Only for action='switch_desktop'. Switches to the "
"adjacent virtual desktop. Requires Windows 10+ with "
"multiple virtual desktops."
),
},
# ── return shape ───────────────────────────────────────
"capture_after": {
"type": "boolean",
Expand Down
7 changes: 7 additions & 0 deletions tools/computer_use/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,13 @@ def _dispatch(backend: ComputerUseBackend, action: str, args: Dict[str, Any]) ->
res = backend.set_value(value=str(value), element=args.get("element"))
return _maybe_follow_capture(backend, res, capture_after)

if action == "switch_desktop":
direction = args.get("direction", "")
if not hasattr(backend, "switch_desktop"):
return json.dumps({"error": "switch_desktop not supported by current backend"})
res = backend.switch_desktop(str(direction))
return json.dumps({"ok": res.ok, "message": res.message})
Comment thread
Icather marked this conversation as resolved.
Outdated

return json.dumps({"error": f"unknown action {action!r}"})


Expand Down
56 changes: 56 additions & 0 deletions tools/computer_use/windows_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,45 @@ def _press_combo(vks: List[int]) -> None:
_send_inputs(seq)


def _switch_desktop_via_keybd(direction: str, overlay_client) -> bool:
"""Switch virtual desktop via Ctrl+Win+Left/Right SendInput.

The overlay subprocess (full-screen tkinter window) is killed by any
virtual-desktop transition, so we stop it before switching and restart
it on the new desktop.
"""
VK_CONTROL = 0x11
VK_LWIN = 0x5B
VK_LEFT = 0x25
VK_RIGHT = 0x27

vk_dir = VK_LEFT if direction == "left" else VK_RIGHT
Comment thread
Icather marked this conversation as resolved.

# Single-batch SendInput matching _press_combo semantics:
# hold modifiers → tap arrow → release, so the system input thread
# sees the same event order as a physical keyboard.
seq = [_key_event(VK_CONTROL, True),
_key_event(VK_LWIN, True),
_key_event(vk_dir, True),
_key_event(vk_dir, False),
_key_event(VK_LWIN, False),
_key_event(VK_CONTROL, False)]

try:
overlay_client.stop()
_send_inputs(seq)
overlay_client._dead = False
overlay_client.start()
Comment thread
Icather marked this conversation as resolved.
return True
except Exception:
try:
overlay_client._dead = False
overlay_client.start()
except Exception:
pass
return False
Comment thread
Icather marked this conversation as resolved.


def _type_unicode(text: str) -> None:
"""Type text via KEYEVENTF_UNICODE; newlines become Return taps."""
batch: List[_INPUT] = []
Expand Down Expand Up @@ -990,6 +1029,23 @@ def key(self, keys: str) -> ActionResult:
except Exception as e:
return ActionResult(ok=False, action="key", message=f"key failed: {e}")

def switch_desktop(self, direction: str) -> ActionResult:
"""Switch to adjacent virtual desktop.

Temporarily stops the overlay subprocess before switching and
restarts it on the new desktop, because any SendInput-based
virtual-desktop transition kills the full-screen tkinter window.
"""
if direction not in ("left", "right"):
return ActionResult(ok=False, action="switch_desktop",
message=f"unknown direction {direction!r}")
self._overlay.send({"cmd": "flash", "text": f"switch desktop · {direction}", "ttl": 1.0})
ok = _switch_desktop_via_keybd(direction, self._overlay)
return ActionResult(
ok=ok, action="switch_desktop",
message=(f"switched to {direction} virtual desktop"
if ok else "virtual desktop switch failed"))

# ── Native-value mutation ───────────────────────────────────────
def set_value(self, value: str, element: Optional[int] = None) -> ActionResult:
if element is None:
Expand Down