agentremote-cli 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentremote_cli-1.0.0.dist-info/METADATA +7 -0
- agentremote_cli-1.0.0.dist-info/RECORD +25 -0
- agentremote_cli-1.0.0.dist-info/WHEEL +5 -0
- agentremote_cli-1.0.0.dist-info/entry_points.txt +2 -0
- agentremote_cli-1.0.0.dist-info/top_level.txt +1 -0
- remotecli/__init__.py +1 -0
- remotecli/agent.py +673 -0
- remotecli/agent_cache.py +274 -0
- remotecli/agent_modes.py +5 -0
- remotecli/agent_open.py +323 -0
- remotecli/agent_projection.py +308 -0
- remotecli/agent_result.py +81 -0
- remotecli/agent_utils.py +113 -0
- remotecli/agent_window.py +249 -0
- remotecli/cli.py +635 -0
- remotecli/client.py +120 -0
- remotecli/config/agentremote.cli.toml +6 -0
- remotecli/cua.py +213 -0
- remotecli/guides/GUIDE.md +397 -0
- remotecli/guides/agent.md +60 -0
- remotecli/guides/cua.md +60 -0
- remotecli/guides/rpa.md +45 -0
- remotecli/guides/shell.md +49 -0
- remotecli/rpa.py +57 -0
- remotecli/shell.py +71 -0
remotecli/agent.py
ADDED
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent-friendly remotecli wrappers.
|
|
3
|
+
|
|
4
|
+
These commands hide mechanical details that models frequently get wrong:
|
|
5
|
+
waiting, result.output decoding, and pid/window_id discovery.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, Iterable
|
|
12
|
+
|
|
13
|
+
from .agent_cache import WindowCache, WindowRef
|
|
14
|
+
from .agent_modes import _LOCATE_MODES, _OBSERVE_MODES, _PROBE_MODES
|
|
15
|
+
from .agent_open import open_app_window
|
|
16
|
+
from .agent_projection import _compact_window_state
|
|
17
|
+
from .agent_result import _error, _normalize_task_result
|
|
18
|
+
from .agent_utils import (
|
|
19
|
+
_normalize_mode,
|
|
20
|
+
_normalize_scroll_amount,
|
|
21
|
+
_normalize_scroll_by,
|
|
22
|
+
_normalize_scroll_direction,
|
|
23
|
+
_target_texts,
|
|
24
|
+
)
|
|
25
|
+
from .agent_window import WindowResolver
|
|
26
|
+
from .client import RemoteCliClient
|
|
27
|
+
from .cua import CuaCommands
|
|
28
|
+
from .shell import ShellCommands
|
|
29
|
+
|
|
30
|
+
__all__ = ["AgentCommands", "WindowCache", "WindowRef"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AgentCommands:
|
|
34
|
+
"""High-level commands intended for AI agents."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
client: RemoteCliClient,
|
|
39
|
+
window_cache_path: str | Path | None = None,
|
|
40
|
+
default_session_id: str = "",
|
|
41
|
+
) -> None:
|
|
42
|
+
self._client = client
|
|
43
|
+
self._shell = ShellCommands(client)
|
|
44
|
+
self._cua = CuaCommands(client)
|
|
45
|
+
self._window_cache = WindowCache(window_cache_path)
|
|
46
|
+
self._window_resolver = WindowResolver(
|
|
47
|
+
client,
|
|
48
|
+
self._cua,
|
|
49
|
+
self._window_cache,
|
|
50
|
+
self._wait_and_unpack,
|
|
51
|
+
default_session_id=default_session_id,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def ps(
|
|
55
|
+
self,
|
|
56
|
+
script: str,
|
|
57
|
+
cwd: str = "",
|
|
58
|
+
timeout_sec: int = 60,
|
|
59
|
+
wait_timeout: float = 30.0,
|
|
60
|
+
poll_interval: float = 0.1,
|
|
61
|
+
) -> Dict[str, Any]:
|
|
62
|
+
submitted = self._shell.exec_script(
|
|
63
|
+
script,
|
|
64
|
+
shell="powershell",
|
|
65
|
+
cwd=cwd,
|
|
66
|
+
timeout_sec=timeout_sec,
|
|
67
|
+
)
|
|
68
|
+
return self._wait_and_unpack(submitted, "ps", wait_timeout, poll_interval)
|
|
69
|
+
|
|
70
|
+
def cmd(
|
|
71
|
+
self,
|
|
72
|
+
command: str,
|
|
73
|
+
cwd: str = "",
|
|
74
|
+
timeout_sec: int = 60,
|
|
75
|
+
wait_timeout: float = 30.0,
|
|
76
|
+
poll_interval: float = 0.1,
|
|
77
|
+
) -> Dict[str, Any]:
|
|
78
|
+
submitted = self._shell.exec(command, cwd=cwd, timeout_sec=timeout_sec)
|
|
79
|
+
return self._wait_and_unpack(submitted, "cmd", wait_timeout, poll_interval)
|
|
80
|
+
|
|
81
|
+
def open(
|
|
82
|
+
self,
|
|
83
|
+
app_query: str,
|
|
84
|
+
launch_path: str = "",
|
|
85
|
+
app_args: str = "",
|
|
86
|
+
start_if_missing: bool = True,
|
|
87
|
+
foreground: bool = True,
|
|
88
|
+
launch_timeout: int = 15,
|
|
89
|
+
wait_timeout: float = 30.0,
|
|
90
|
+
poll_interval: float = 0.1,
|
|
91
|
+
session_id: str = "",
|
|
92
|
+
use_window_cache: bool = True,
|
|
93
|
+
) -> Dict[str, Any]:
|
|
94
|
+
return open_app_window(
|
|
95
|
+
self._client,
|
|
96
|
+
self._shell,
|
|
97
|
+
self._window_cache,
|
|
98
|
+
self._wait_and_unpack,
|
|
99
|
+
app_query,
|
|
100
|
+
session_id=self._resolve_session_id(session_id),
|
|
101
|
+
use_window_cache=use_window_cache,
|
|
102
|
+
launch_path=launch_path,
|
|
103
|
+
app_args=app_args,
|
|
104
|
+
start_if_missing=start_if_missing,
|
|
105
|
+
foreground=foreground,
|
|
106
|
+
launch_timeout=launch_timeout,
|
|
107
|
+
wait_timeout=wait_timeout,
|
|
108
|
+
poll_interval=poll_interval,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def observe(
|
|
112
|
+
self,
|
|
113
|
+
app_query: str,
|
|
114
|
+
mode: str = "uia",
|
|
115
|
+
output_format: str = "compact",
|
|
116
|
+
element_limit: int = 20,
|
|
117
|
+
targets: Iterable[str] | str | None = None,
|
|
118
|
+
wait_timeout: float = 30.0,
|
|
119
|
+
poll_interval: float = 0.1,
|
|
120
|
+
session_id: str = "",
|
|
121
|
+
use_window_cache: bool = True,
|
|
122
|
+
refresh_window: bool = False,
|
|
123
|
+
) -> Dict[str, Any]:
|
|
124
|
+
output_format = output_format.strip().lower()
|
|
125
|
+
if output_format not in {"compact", "full"}:
|
|
126
|
+
return _error(
|
|
127
|
+
"observe",
|
|
128
|
+
f"unsupported observe format: {output_format}",
|
|
129
|
+
supported_formats=["compact", "full"],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
mode = _normalize_mode(mode, default="uia")
|
|
133
|
+
if mode not in _OBSERVE_MODES:
|
|
134
|
+
return _error(
|
|
135
|
+
"observe",
|
|
136
|
+
f"unsupported observe mode: {mode}",
|
|
137
|
+
supported_modes=sorted(_OBSERVE_MODES),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
ref, error = self._resolve_window(
|
|
141
|
+
app_query,
|
|
142
|
+
wait_timeout,
|
|
143
|
+
poll_interval,
|
|
144
|
+
session_id=session_id,
|
|
145
|
+
use_window_cache=use_window_cache,
|
|
146
|
+
refresh_window=refresh_window,
|
|
147
|
+
)
|
|
148
|
+
if error:
|
|
149
|
+
return error
|
|
150
|
+
|
|
151
|
+
assert ref is not None
|
|
152
|
+
self._foreground_window(ref, wait_timeout, poll_interval)
|
|
153
|
+
state = self._wait_and_unpack(
|
|
154
|
+
self._cua.get_window_state(ref.pid, ref.window_id, mode),
|
|
155
|
+
"observe",
|
|
156
|
+
wait_timeout,
|
|
157
|
+
poll_interval,
|
|
158
|
+
)
|
|
159
|
+
if self._should_refresh_cached_window(ref, state, session_id, use_window_cache):
|
|
160
|
+
ref, error = self._refresh_window(app_query, wait_timeout, poll_interval, session_id)
|
|
161
|
+
if error:
|
|
162
|
+
return error
|
|
163
|
+
assert ref is not None
|
|
164
|
+
self._foreground_window(ref, wait_timeout, poll_interval)
|
|
165
|
+
state = self._wait_and_unpack(
|
|
166
|
+
self._cua.get_window_state(ref.pid, ref.window_id, mode),
|
|
167
|
+
"observe",
|
|
168
|
+
wait_timeout,
|
|
169
|
+
poll_interval,
|
|
170
|
+
)
|
|
171
|
+
payload = state.get("json")
|
|
172
|
+
title = self._state_title(payload) or ref.title
|
|
173
|
+
if output_format == "full":
|
|
174
|
+
return {
|
|
175
|
+
"ok": bool(state.get("ok")),
|
|
176
|
+
"action": "observe",
|
|
177
|
+
"format": "full",
|
|
178
|
+
"mode": mode,
|
|
179
|
+
"app": app_query,
|
|
180
|
+
"pid": ref.pid,
|
|
181
|
+
"window_id": ref.window_id,
|
|
182
|
+
"title": title,
|
|
183
|
+
"window_cached": ref.cached,
|
|
184
|
+
"state": payload,
|
|
185
|
+
"result": state,
|
|
186
|
+
**self._window_cache_context(),
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
compact_state = _compact_window_state(
|
|
190
|
+
payload,
|
|
191
|
+
title=title,
|
|
192
|
+
trace_id=state.get("trace_id"),
|
|
193
|
+
targets=targets,
|
|
194
|
+
element_limit=element_limit,
|
|
195
|
+
)
|
|
196
|
+
return {
|
|
197
|
+
"ok": bool(state.get("ok")),
|
|
198
|
+
"action": "observe",
|
|
199
|
+
"format": "compact",
|
|
200
|
+
"mode": mode,
|
|
201
|
+
"app": app_query,
|
|
202
|
+
"pid": ref.pid,
|
|
203
|
+
"window_id": ref.window_id,
|
|
204
|
+
"title": title,
|
|
205
|
+
"window": {
|
|
206
|
+
"pid": ref.pid,
|
|
207
|
+
"window_id": ref.window_id,
|
|
208
|
+
"title": title,
|
|
209
|
+
},
|
|
210
|
+
"window_cached": ref.cached,
|
|
211
|
+
"state": compact_state,
|
|
212
|
+
"trace_id": state.get("trace_id"),
|
|
213
|
+
"status": state.get("status"),
|
|
214
|
+
"result_status": state.get("result_status"),
|
|
215
|
+
"error": state.get("error") or "",
|
|
216
|
+
**self._window_cache_context(),
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
def click(
|
|
220
|
+
self,
|
|
221
|
+
app_query: str,
|
|
222
|
+
target: str,
|
|
223
|
+
locate_mode: str = "smart",
|
|
224
|
+
wait_timeout: float = 30.0,
|
|
225
|
+
poll_interval: float = 0.1,
|
|
226
|
+
session_id: str = "",
|
|
227
|
+
use_window_cache: bool = True,
|
|
228
|
+
refresh_window: bool = False,
|
|
229
|
+
) -> Dict[str, Any]:
|
|
230
|
+
locate_mode = _normalize_mode(locate_mode, default="smart")
|
|
231
|
+
if locate_mode not in _LOCATE_MODES:
|
|
232
|
+
return _error(
|
|
233
|
+
"click",
|
|
234
|
+
f"unsupported locate mode: {locate_mode}",
|
|
235
|
+
supported_modes=sorted(_LOCATE_MODES),
|
|
236
|
+
)
|
|
237
|
+
ref, error = self._resolve_window(
|
|
238
|
+
app_query,
|
|
239
|
+
wait_timeout,
|
|
240
|
+
poll_interval,
|
|
241
|
+
session_id=session_id,
|
|
242
|
+
use_window_cache=use_window_cache,
|
|
243
|
+
refresh_window=refresh_window,
|
|
244
|
+
)
|
|
245
|
+
if error:
|
|
246
|
+
return error
|
|
247
|
+
|
|
248
|
+
assert ref is not None
|
|
249
|
+
result = self._wait_and_unpack(
|
|
250
|
+
self._cua.click(ref.pid, ref.window_id, target=target, locate_mode=locate_mode),
|
|
251
|
+
"click",
|
|
252
|
+
wait_timeout,
|
|
253
|
+
poll_interval,
|
|
254
|
+
)
|
|
255
|
+
if self._should_refresh_cached_window(ref, result, session_id, use_window_cache):
|
|
256
|
+
ref, error = self._refresh_window(app_query, wait_timeout, poll_interval, session_id)
|
|
257
|
+
if error:
|
|
258
|
+
return error
|
|
259
|
+
assert ref is not None
|
|
260
|
+
result = self._wait_and_unpack(
|
|
261
|
+
self._cua.click(ref.pid, ref.window_id, target=target, locate_mode=locate_mode),
|
|
262
|
+
"click",
|
|
263
|
+
wait_timeout,
|
|
264
|
+
poll_interval,
|
|
265
|
+
)
|
|
266
|
+
extra = {"locate_mode": locate_mode} if locate_mode else {}
|
|
267
|
+
return self._with_window_context(result, "click", ref, target=target, **extra)
|
|
268
|
+
|
|
269
|
+
def probe(
|
|
270
|
+
self,
|
|
271
|
+
app_query: str,
|
|
272
|
+
targets: Iterable[str] | str,
|
|
273
|
+
mode: str = "smart",
|
|
274
|
+
wait_timeout: float = 30.0,
|
|
275
|
+
poll_interval: float = 0.1,
|
|
276
|
+
session_id: str = "",
|
|
277
|
+
use_window_cache: bool = True,
|
|
278
|
+
refresh_window: bool = False,
|
|
279
|
+
) -> Dict[str, Any]:
|
|
280
|
+
mode = _normalize_mode(mode, default="smart")
|
|
281
|
+
if mode not in _PROBE_MODES:
|
|
282
|
+
return _error(
|
|
283
|
+
"probe",
|
|
284
|
+
f"unsupported probe mode: {mode}",
|
|
285
|
+
supported_modes=sorted(_PROBE_MODES),
|
|
286
|
+
)
|
|
287
|
+
target_list = _target_texts(targets)
|
|
288
|
+
if not target_list:
|
|
289
|
+
return _error("probe", "probe requires at least one target")
|
|
290
|
+
|
|
291
|
+
ref, error = self._resolve_window(
|
|
292
|
+
app_query,
|
|
293
|
+
wait_timeout,
|
|
294
|
+
poll_interval,
|
|
295
|
+
session_id=session_id,
|
|
296
|
+
use_window_cache=use_window_cache,
|
|
297
|
+
refresh_window=refresh_window,
|
|
298
|
+
)
|
|
299
|
+
if error:
|
|
300
|
+
return error
|
|
301
|
+
|
|
302
|
+
assert ref is not None
|
|
303
|
+
if mode != "uia":
|
|
304
|
+
self._foreground_window(ref, wait_timeout, poll_interval)
|
|
305
|
+
result = self._wait_and_unpack(
|
|
306
|
+
self._cua.probe_window(ref.pid, ref.window_id, target_list, mode=mode),
|
|
307
|
+
"probe",
|
|
308
|
+
wait_timeout,
|
|
309
|
+
poll_interval,
|
|
310
|
+
)
|
|
311
|
+
if self._should_refresh_cached_window(ref, result, session_id, use_window_cache):
|
|
312
|
+
ref, error = self._refresh_window(app_query, wait_timeout, poll_interval, session_id)
|
|
313
|
+
if error:
|
|
314
|
+
return error
|
|
315
|
+
assert ref is not None
|
|
316
|
+
if mode != "uia":
|
|
317
|
+
self._foreground_window(ref, wait_timeout, poll_interval)
|
|
318
|
+
result = self._wait_and_unpack(
|
|
319
|
+
self._cua.probe_window(ref.pid, ref.window_id, target_list, mode=mode),
|
|
320
|
+
"probe",
|
|
321
|
+
wait_timeout,
|
|
322
|
+
poll_interval,
|
|
323
|
+
)
|
|
324
|
+
payload = result.get("json")
|
|
325
|
+
if not isinstance(payload, dict):
|
|
326
|
+
payload = result.get("output") if isinstance(result.get("output"), dict) else {}
|
|
327
|
+
matches = payload.get("matches", []) if isinstance(payload.get("matches"), list) else []
|
|
328
|
+
missing = payload.get("missing", target_list) if isinstance(payload.get("missing"), list) else target_list
|
|
329
|
+
return {
|
|
330
|
+
"ok": bool(result.get("ok")),
|
|
331
|
+
"action": "probe",
|
|
332
|
+
"app": app_query,
|
|
333
|
+
"pid": ref.pid,
|
|
334
|
+
"window_id": ref.window_id,
|
|
335
|
+
"title": ref.title,
|
|
336
|
+
"window_cached": ref.cached,
|
|
337
|
+
"mode": payload.get("mode", mode),
|
|
338
|
+
"targets": payload.get("targets", target_list),
|
|
339
|
+
"matches": matches,
|
|
340
|
+
"missing": missing,
|
|
341
|
+
"any_found": bool(payload.get("any_found", bool(matches))),
|
|
342
|
+
"all_found": bool(payload.get("all_found", not missing)),
|
|
343
|
+
"probes": payload.get("probes", []),
|
|
344
|
+
"trace_id": result.get("trace_id"),
|
|
345
|
+
"status": result.get("status"),
|
|
346
|
+
"result_status": result.get("result_status"),
|
|
347
|
+
"error": result.get("error") or "",
|
|
348
|
+
**self._window_cache_context(),
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
def type_text(
|
|
352
|
+
self,
|
|
353
|
+
app_query: str,
|
|
354
|
+
target: str,
|
|
355
|
+
text: str,
|
|
356
|
+
locate_mode: str = "smart",
|
|
357
|
+
wait_timeout: float = 30.0,
|
|
358
|
+
poll_interval: float = 0.1,
|
|
359
|
+
session_id: str = "",
|
|
360
|
+
use_window_cache: bool = True,
|
|
361
|
+
refresh_window: bool = False,
|
|
362
|
+
) -> Dict[str, Any]:
|
|
363
|
+
locate_mode = _normalize_mode(locate_mode, default="smart")
|
|
364
|
+
if locate_mode not in _LOCATE_MODES:
|
|
365
|
+
return _error(
|
|
366
|
+
"type",
|
|
367
|
+
f"unsupported locate mode: {locate_mode}",
|
|
368
|
+
supported_modes=sorted(_LOCATE_MODES),
|
|
369
|
+
)
|
|
370
|
+
ref, error = self._resolve_window(
|
|
371
|
+
app_query,
|
|
372
|
+
wait_timeout,
|
|
373
|
+
poll_interval,
|
|
374
|
+
session_id=session_id,
|
|
375
|
+
use_window_cache=use_window_cache,
|
|
376
|
+
refresh_window=refresh_window,
|
|
377
|
+
)
|
|
378
|
+
if error:
|
|
379
|
+
return error
|
|
380
|
+
|
|
381
|
+
assert ref is not None
|
|
382
|
+
result = self._wait_and_unpack(
|
|
383
|
+
self._cua.type_text(
|
|
384
|
+
ref.pid,
|
|
385
|
+
target,
|
|
386
|
+
text,
|
|
387
|
+
window_id=ref.window_id,
|
|
388
|
+
locate_mode=locate_mode,
|
|
389
|
+
),
|
|
390
|
+
"type",
|
|
391
|
+
wait_timeout,
|
|
392
|
+
poll_interval,
|
|
393
|
+
)
|
|
394
|
+
if self._should_refresh_cached_window(ref, result, session_id, use_window_cache):
|
|
395
|
+
ref, error = self._refresh_window(app_query, wait_timeout, poll_interval, session_id)
|
|
396
|
+
if error:
|
|
397
|
+
return error
|
|
398
|
+
assert ref is not None
|
|
399
|
+
result = self._wait_and_unpack(
|
|
400
|
+
self._cua.type_text(
|
|
401
|
+
ref.pid,
|
|
402
|
+
target,
|
|
403
|
+
text,
|
|
404
|
+
window_id=ref.window_id,
|
|
405
|
+
locate_mode=locate_mode,
|
|
406
|
+
),
|
|
407
|
+
"type",
|
|
408
|
+
wait_timeout,
|
|
409
|
+
poll_interval,
|
|
410
|
+
)
|
|
411
|
+
extra = {"locate_mode": locate_mode} if locate_mode else {}
|
|
412
|
+
return self._with_window_context(result, "type", ref, target=target, **extra)
|
|
413
|
+
|
|
414
|
+
def key(
|
|
415
|
+
self,
|
|
416
|
+
app_query: str,
|
|
417
|
+
key: str,
|
|
418
|
+
wait_timeout: float = 30.0,
|
|
419
|
+
poll_interval: float = 0.1,
|
|
420
|
+
session_id: str = "",
|
|
421
|
+
use_window_cache: bool = True,
|
|
422
|
+
refresh_window: bool = False,
|
|
423
|
+
) -> Dict[str, Any]:
|
|
424
|
+
ref, error = self._resolve_window(
|
|
425
|
+
app_query,
|
|
426
|
+
wait_timeout,
|
|
427
|
+
poll_interval,
|
|
428
|
+
session_id=session_id,
|
|
429
|
+
use_window_cache=use_window_cache,
|
|
430
|
+
refresh_window=refresh_window,
|
|
431
|
+
)
|
|
432
|
+
if error:
|
|
433
|
+
return error
|
|
434
|
+
|
|
435
|
+
assert ref is not None
|
|
436
|
+
result = self._wait_and_unpack(
|
|
437
|
+
self._cua.press_key(ref.pid, key, window_id=ref.window_id),
|
|
438
|
+
"key",
|
|
439
|
+
wait_timeout,
|
|
440
|
+
poll_interval,
|
|
441
|
+
)
|
|
442
|
+
if self._should_refresh_cached_window(ref, result, session_id, use_window_cache):
|
|
443
|
+
ref, error = self._refresh_window(app_query, wait_timeout, poll_interval, session_id)
|
|
444
|
+
if error:
|
|
445
|
+
return error
|
|
446
|
+
assert ref is not None
|
|
447
|
+
result = self._wait_and_unpack(
|
|
448
|
+
self._cua.press_key(ref.pid, key, window_id=ref.window_id),
|
|
449
|
+
"key",
|
|
450
|
+
wait_timeout,
|
|
451
|
+
poll_interval,
|
|
452
|
+
)
|
|
453
|
+
return self._with_window_context(result, "key", ref, key=key)
|
|
454
|
+
|
|
455
|
+
def hotkey(
|
|
456
|
+
self,
|
|
457
|
+
app_query: str,
|
|
458
|
+
keys: str,
|
|
459
|
+
wait_timeout: float = 30.0,
|
|
460
|
+
poll_interval: float = 0.1,
|
|
461
|
+
session_id: str = "",
|
|
462
|
+
use_window_cache: bool = True,
|
|
463
|
+
refresh_window: bool = False,
|
|
464
|
+
) -> Dict[str, Any]:
|
|
465
|
+
ref, error = self._resolve_window(
|
|
466
|
+
app_query,
|
|
467
|
+
wait_timeout,
|
|
468
|
+
poll_interval,
|
|
469
|
+
session_id=session_id,
|
|
470
|
+
use_window_cache=use_window_cache,
|
|
471
|
+
refresh_window=refresh_window,
|
|
472
|
+
)
|
|
473
|
+
if error:
|
|
474
|
+
return error
|
|
475
|
+
|
|
476
|
+
assert ref is not None
|
|
477
|
+
result = self._wait_and_unpack(
|
|
478
|
+
self._cua.hotkey(ref.pid, keys, window_id=ref.window_id),
|
|
479
|
+
"hotkey",
|
|
480
|
+
wait_timeout,
|
|
481
|
+
poll_interval,
|
|
482
|
+
)
|
|
483
|
+
if self._should_refresh_cached_window(ref, result, session_id, use_window_cache):
|
|
484
|
+
ref, error = self._refresh_window(app_query, wait_timeout, poll_interval, session_id)
|
|
485
|
+
if error:
|
|
486
|
+
return error
|
|
487
|
+
assert ref is not None
|
|
488
|
+
result = self._wait_and_unpack(
|
|
489
|
+
self._cua.hotkey(ref.pid, keys, window_id=ref.window_id),
|
|
490
|
+
"hotkey",
|
|
491
|
+
wait_timeout,
|
|
492
|
+
poll_interval,
|
|
493
|
+
)
|
|
494
|
+
return self._with_window_context(result, "hotkey", ref, keys=keys)
|
|
495
|
+
|
|
496
|
+
def scroll(
|
|
497
|
+
self,
|
|
498
|
+
app_query: str,
|
|
499
|
+
direction: str = "down",
|
|
500
|
+
amount: int = 3,
|
|
501
|
+
by: str = "line",
|
|
502
|
+
dispatch: str = "",
|
|
503
|
+
wait_timeout: float = 30.0,
|
|
504
|
+
poll_interval: float = 0.1,
|
|
505
|
+
session_id: str = "",
|
|
506
|
+
use_window_cache: bool = True,
|
|
507
|
+
refresh_window: bool = False,
|
|
508
|
+
) -> Dict[str, Any]:
|
|
509
|
+
ref, error = self._resolve_window(
|
|
510
|
+
app_query,
|
|
511
|
+
wait_timeout,
|
|
512
|
+
poll_interval,
|
|
513
|
+
session_id=session_id,
|
|
514
|
+
use_window_cache=use_window_cache,
|
|
515
|
+
refresh_window=refresh_window,
|
|
516
|
+
)
|
|
517
|
+
if error:
|
|
518
|
+
return error
|
|
519
|
+
|
|
520
|
+
assert ref is not None
|
|
521
|
+
direction = _normalize_scroll_direction(direction)
|
|
522
|
+
amount = _normalize_scroll_amount(amount)
|
|
523
|
+
by = _normalize_scroll_by(by)
|
|
524
|
+
result = self._wait_and_unpack(
|
|
525
|
+
self._cua.scroll(
|
|
526
|
+
ref.pid,
|
|
527
|
+
ref.window_id,
|
|
528
|
+
direction=direction,
|
|
529
|
+
amount=amount,
|
|
530
|
+
by=by,
|
|
531
|
+
dispatch=dispatch,
|
|
532
|
+
),
|
|
533
|
+
"scroll",
|
|
534
|
+
wait_timeout,
|
|
535
|
+
poll_interval,
|
|
536
|
+
)
|
|
537
|
+
if self._should_refresh_cached_window(ref, result, session_id, use_window_cache):
|
|
538
|
+
ref, error = self._refresh_window(app_query, wait_timeout, poll_interval, session_id)
|
|
539
|
+
if error:
|
|
540
|
+
return error
|
|
541
|
+
assert ref is not None
|
|
542
|
+
result = self._wait_and_unpack(
|
|
543
|
+
self._cua.scroll(
|
|
544
|
+
ref.pid,
|
|
545
|
+
ref.window_id,
|
|
546
|
+
direction=direction,
|
|
547
|
+
amount=amount,
|
|
548
|
+
by=by,
|
|
549
|
+
dispatch=dispatch,
|
|
550
|
+
),
|
|
551
|
+
"scroll",
|
|
552
|
+
wait_timeout,
|
|
553
|
+
poll_interval,
|
|
554
|
+
)
|
|
555
|
+
return self._with_window_context(
|
|
556
|
+
result,
|
|
557
|
+
"scroll",
|
|
558
|
+
ref,
|
|
559
|
+
direction=direction,
|
|
560
|
+
amount=amount,
|
|
561
|
+
by=by,
|
|
562
|
+
**({"dispatch": dispatch} if dispatch else {}),
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
def clear_session(self, session_id: str = "") -> Dict[str, Any]:
|
|
566
|
+
session = self._resolve_session_id(session_id)
|
|
567
|
+
if not session:
|
|
568
|
+
return _error("session-clear", "session is required")
|
|
569
|
+
self._window_cache.clear_session(self._client, session)
|
|
570
|
+
return {
|
|
571
|
+
"ok": True,
|
|
572
|
+
"action": "session-clear",
|
|
573
|
+
"session": session,
|
|
574
|
+
**self._window_cache_context(),
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
def _resolve_window(
|
|
578
|
+
self,
|
|
579
|
+
app_query: str,
|
|
580
|
+
wait_timeout: float,
|
|
581
|
+
poll_interval: float,
|
|
582
|
+
session_id: str = "",
|
|
583
|
+
use_window_cache: bool = True,
|
|
584
|
+
refresh_window: bool = False,
|
|
585
|
+
) -> tuple[WindowRef | None, Dict[str, Any] | None]:
|
|
586
|
+
return self._window_resolver.resolve_window(
|
|
587
|
+
app_query,
|
|
588
|
+
wait_timeout,
|
|
589
|
+
poll_interval,
|
|
590
|
+
session_id=session_id,
|
|
591
|
+
use_window_cache=use_window_cache,
|
|
592
|
+
refresh_window=refresh_window,
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
def _foreground_window(
|
|
596
|
+
self,
|
|
597
|
+
ref: WindowRef,
|
|
598
|
+
wait_timeout: float,
|
|
599
|
+
poll_interval: float,
|
|
600
|
+
) -> None:
|
|
601
|
+
self._window_resolver.foreground_window(ref, wait_timeout, poll_interval)
|
|
602
|
+
|
|
603
|
+
def _refresh_window(
|
|
604
|
+
self,
|
|
605
|
+
app_query: str,
|
|
606
|
+
wait_timeout: float,
|
|
607
|
+
poll_interval: float,
|
|
608
|
+
session_id: str,
|
|
609
|
+
) -> tuple[WindowRef | None, Dict[str, Any] | None]:
|
|
610
|
+
return self._window_resolver.refresh_window(
|
|
611
|
+
app_query,
|
|
612
|
+
wait_timeout,
|
|
613
|
+
poll_interval,
|
|
614
|
+
session_id,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
def _should_refresh_cached_window(
|
|
618
|
+
self,
|
|
619
|
+
ref: WindowRef,
|
|
620
|
+
result: Dict[str, Any],
|
|
621
|
+
session_id: str,
|
|
622
|
+
use_window_cache: bool,
|
|
623
|
+
) -> bool:
|
|
624
|
+
return self._window_resolver.should_refresh_cached_window(
|
|
625
|
+
ref,
|
|
626
|
+
result,
|
|
627
|
+
session_id,
|
|
628
|
+
use_window_cache,
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
def _resolve_session_id(self, session_id: str = "") -> str:
|
|
632
|
+
return self._window_resolver.resolve_session_id(session_id)
|
|
633
|
+
|
|
634
|
+
def _window_cache_context(self) -> Dict[str, Any]:
|
|
635
|
+
return self._window_resolver.cache_context()
|
|
636
|
+
|
|
637
|
+
def _wait_and_unpack(
|
|
638
|
+
self,
|
|
639
|
+
submitted: Dict[str, Any],
|
|
640
|
+
action: str,
|
|
641
|
+
wait_timeout: float,
|
|
642
|
+
poll_interval: float,
|
|
643
|
+
) -> Dict[str, Any]:
|
|
644
|
+
trace_id = submitted.get("trace_id")
|
|
645
|
+
if not trace_id:
|
|
646
|
+
return _normalize_task_result(submitted, action)
|
|
647
|
+
data = self._client.poll_result(trace_id, timeout_sec=wait_timeout, interval=poll_interval)
|
|
648
|
+
return _normalize_task_result(data, action)
|
|
649
|
+
|
|
650
|
+
@staticmethod
|
|
651
|
+
def _state_title(payload: Any) -> str:
|
|
652
|
+
if isinstance(payload, dict):
|
|
653
|
+
return str(payload.get("title") or payload.get("window_title") or "")
|
|
654
|
+
return ""
|
|
655
|
+
|
|
656
|
+
def _with_window_context(
|
|
657
|
+
self,
|
|
658
|
+
result: Dict[str, Any],
|
|
659
|
+
action: str,
|
|
660
|
+
ref: WindowRef,
|
|
661
|
+
**extra: Any,
|
|
662
|
+
) -> Dict[str, Any]:
|
|
663
|
+
return {
|
|
664
|
+
"ok": bool(result.get("ok")),
|
|
665
|
+
"action": action,
|
|
666
|
+
"pid": ref.pid,
|
|
667
|
+
"window_id": ref.window_id,
|
|
668
|
+
"title": ref.title,
|
|
669
|
+
"window_cached": ref.cached,
|
|
670
|
+
**extra,
|
|
671
|
+
"result": result,
|
|
672
|
+
**self._window_cache_context(),
|
|
673
|
+
}
|