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.
@@ -0,0 +1,249 @@
1
+ """Window lookup and foreground helpers for agent commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any, Callable, Dict, Iterable
7
+
8
+ from .agent_cache import WindowCache, WindowRef
9
+ from .agent_result import _error
10
+ from .agent_utils import _extract_int, _first_present, _first_text, _truthy
11
+
12
+
13
+ WaitAndUnpack = Callable[[Dict[str, Any], str, float, float], Dict[str, Any]]
14
+
15
+
16
+ class WindowResolver:
17
+ """Resolve app queries into concrete pid/window_id pairs."""
18
+
19
+ def __init__(
20
+ self,
21
+ client: Any,
22
+ cua: Any,
23
+ window_cache: WindowCache,
24
+ wait_and_unpack: WaitAndUnpack,
25
+ default_session_id: str = "",
26
+ ) -> None:
27
+ self._client = client
28
+ self._cua = cua
29
+ self._window_cache = window_cache
30
+ self._wait_and_unpack = wait_and_unpack
31
+ self._default_session_id = default_session_id
32
+
33
+ def resolve_window(
34
+ self,
35
+ app_query: str,
36
+ wait_timeout: float,
37
+ poll_interval: float,
38
+ session_id: str = "",
39
+ use_window_cache: bool = True,
40
+ refresh_window: bool = False,
41
+ ) -> tuple[WindowRef | None, Dict[str, Any] | None]:
42
+ session = self.resolve_session_id(session_id)
43
+ if use_window_cache and session and not refresh_window:
44
+ cached_ref = self._window_cache.get(self._client, session, app_query)
45
+ if cached_ref:
46
+ return cached_ref, None
47
+
48
+ apps_result = self._wait_and_unpack(
49
+ self._cua.list_apps(app_query, max_results=5),
50
+ "list_apps",
51
+ wait_timeout,
52
+ poll_interval,
53
+ )
54
+ if not apps_result.get("ok"):
55
+ apps_result["action"] = "resolve_window"
56
+ return None, apps_result
57
+
58
+ apps = list(_extract_apps(apps_result.get("json")))
59
+ if not apps:
60
+ return None, _error("resolve_window", f"no app matched: {app_query}", app=app_query)
61
+
62
+ last_error: Dict[str, Any] | None = None
63
+ for app in _ranked_apps(apps, app_query):
64
+ pid = _extract_int(app, "pid", "process_id", "processId", "id")
65
+ if not pid:
66
+ last_error = _error("resolve_window", f"matched app has no pid: {app_query}", app=app)
67
+ continue
68
+
69
+ windows_result = self._wait_and_unpack(
70
+ self._cua.list_windows(pid),
71
+ "list_windows",
72
+ wait_timeout,
73
+ poll_interval,
74
+ )
75
+ if not windows_result.get("ok"):
76
+ windows_result["action"] = "resolve_window"
77
+ last_error = windows_result
78
+ continue
79
+
80
+ windows = list(_extract_windows(windows_result.get("json")))
81
+ if not windows:
82
+ last_error = _error("resolve_window", f"no windows for pid: {pid}", pid=pid, app=app)
83
+ continue
84
+
85
+ window = _select_window(windows)
86
+ window_id = _extract_int(window, "window_id", "windowId", "id", "hwnd", "handle")
87
+ if not window_id:
88
+ last_error = _error("resolve_window", f"matched window has no window_id: {pid}", window=window)
89
+ continue
90
+
91
+ ref = WindowRef(
92
+ pid=pid,
93
+ window_id=window_id,
94
+ title=str(window.get("title") or window.get("name") or ""),
95
+ app=app,
96
+ window=window,
97
+ )
98
+ if use_window_cache and session:
99
+ self._window_cache.put(self._client, session, app_query, ref)
100
+ return ref, None
101
+
102
+ return None, last_error or _error(
103
+ "resolve_window",
104
+ f"no usable app window matched: {app_query}",
105
+ app=app_query,
106
+ )
107
+
108
+ def foreground_window(
109
+ self,
110
+ ref: WindowRef,
111
+ wait_timeout: float,
112
+ poll_interval: float,
113
+ ) -> None:
114
+ self._wait_and_unpack(
115
+ self._cua.bring_to_front(ref.pid, ref.window_id),
116
+ "bring_to_front",
117
+ wait_timeout,
118
+ poll_interval,
119
+ )
120
+
121
+ def refresh_window(
122
+ self,
123
+ app_query: str,
124
+ wait_timeout: float,
125
+ poll_interval: float,
126
+ session_id: str,
127
+ ) -> tuple[WindowRef | None, Dict[str, Any] | None]:
128
+ session = self.resolve_session_id(session_id)
129
+ if session:
130
+ self._window_cache.remove_app(self._client, session, app_query)
131
+ return self.resolve_window(
132
+ app_query,
133
+ wait_timeout,
134
+ poll_interval,
135
+ session_id=session,
136
+ use_window_cache=True,
137
+ refresh_window=True,
138
+ )
139
+
140
+ def should_refresh_cached_window(
141
+ self,
142
+ ref: WindowRef,
143
+ result: Dict[str, Any],
144
+ session_id: str,
145
+ use_window_cache: bool,
146
+ ) -> bool:
147
+ return bool(
148
+ ref.cached
149
+ and use_window_cache
150
+ and self.resolve_session_id(session_id)
151
+ and not result.get("ok")
152
+ )
153
+
154
+ def resolve_session_id(self, session_id: str = "") -> str:
155
+ return (
156
+ session_id
157
+ or self._default_session_id
158
+ or os.environ.get("REMOTECLI_SESSION", "")
159
+ ).strip()
160
+
161
+ def cache_context(self) -> Dict[str, Any]:
162
+ if not self._window_cache.last_error:
163
+ return {}
164
+ return {"window_cache_error": self._window_cache.last_error}
165
+
166
+
167
+ def _extract_apps(payload: Any) -> Iterable[Dict[str, Any]]:
168
+ if isinstance(payload, list):
169
+ return [item for item in payload if isinstance(item, dict)]
170
+ if not isinstance(payload, dict):
171
+ return []
172
+ for key in ("matches", "apps", "applications", "items", "processes"):
173
+ value = payload.get(key)
174
+ if isinstance(value, list):
175
+ return [item for item in value if isinstance(item, dict)]
176
+ return [payload] if _extract_int(payload, "pid", "process_id", "id") else []
177
+
178
+
179
+ def _ranked_apps(apps: list[Dict[str, Any]], query: str) -> list[Dict[str, Any]]:
180
+ indexed = list(enumerate(apps))
181
+ ranked = sorted(
182
+ indexed,
183
+ key=lambda item: (_app_score(item[1], query), -item[0]),
184
+ reverse=True,
185
+ )
186
+ return [app for _index, app in ranked]
187
+
188
+
189
+ def _app_score(app: Dict[str, Any], query: str) -> int:
190
+ score = 0
191
+ pid = _extract_int(app, "pid", "process_id", "processId", "id")
192
+ if pid:
193
+ score += 1000
194
+ else:
195
+ score -= 500
196
+
197
+ if _truthy(_first_present(app, "running", "is_running", "isRunning")):
198
+ score += 300
199
+ if _truthy(_first_present(app, "active", "is_active", "foreground", "is_foreground")):
200
+ score += 120
201
+
202
+ windows = app.get("windows")
203
+ if isinstance(windows, list) and windows:
204
+ score += 80
205
+
206
+ kind = _first_text(app, "kind", "type", "app_type", "appType", "source") or ""
207
+ kind_text = kind.lower()
208
+ if any(term in kind_text for term in ("desktop", "win32", "process")):
209
+ score += 80
210
+ if any(term in kind_text for term in ("uwp", "share", "appx")):
211
+ score -= 80
212
+
213
+ query_text = " ".join(str(query or "").lower().split())
214
+ identity = " ".join(
215
+ str(_first_present(app, key) or "")
216
+ for key in (
217
+ "name",
218
+ "title",
219
+ "display_name",
220
+ "process_name",
221
+ "app_id",
222
+ "appId",
223
+ "path",
224
+ )
225
+ ).lower()
226
+ if query_text and query_text in " ".join(identity.split()):
227
+ score += 50
228
+ if query_text and query_text == " ".join(identity.split()):
229
+ score += 50
230
+ return score
231
+
232
+
233
+ def _extract_windows(payload: Any) -> Iterable[Dict[str, Any]]:
234
+ if isinstance(payload, list):
235
+ return [item for item in payload if isinstance(item, dict)]
236
+ if not isinstance(payload, dict):
237
+ return []
238
+ for key in ("windows", "items"):
239
+ value = payload.get(key)
240
+ if isinstance(value, list):
241
+ return [item for item in value if isinstance(item, dict)]
242
+ return [payload] if _extract_int(payload, "window_id", "id", "hwnd", "handle") else []
243
+
244
+
245
+ def _select_window(windows: list[Dict[str, Any]]) -> Dict[str, Any]:
246
+ for window in windows:
247
+ if str(window.get("title") or window.get("name") or "").strip():
248
+ return window
249
+ return windows[0]