pygpt-net 2.7.4__py3-none-any.whl → 2.7.6__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.
Files changed (159) hide show
  1. pygpt_net/CHANGELOG.txt +15 -0
  2. pygpt_net/__init__.py +4 -4
  3. pygpt_net/app_core.py +4 -2
  4. pygpt_net/controller/__init__.py +5 -1
  5. pygpt_net/controller/assistant/assistant.py +1 -4
  6. pygpt_net/controller/assistant/batch.py +5 -504
  7. pygpt_net/controller/assistant/editor.py +5 -5
  8. pygpt_net/controller/assistant/files.py +16 -16
  9. pygpt_net/controller/chat/handler/google_stream.py +307 -1
  10. pygpt_net/controller/chat/handler/worker.py +10 -25
  11. pygpt_net/controller/chat/handler/xai_stream.py +621 -52
  12. pygpt_net/controller/chat/image.py +2 -2
  13. pygpt_net/controller/debug/fixtures.py +3 -2
  14. pygpt_net/controller/dialogs/confirm.py +73 -101
  15. pygpt_net/controller/files/files.py +65 -4
  16. pygpt_net/controller/lang/mapping.py +9 -9
  17. pygpt_net/controller/painter/capture.py +50 -1
  18. pygpt_net/controller/presets/presets.py +2 -1
  19. pygpt_net/controller/remote_store/__init__.py +12 -0
  20. pygpt_net/{provider/core/assistant_file/db_sqlite → controller/remote_store/google}/__init__.py +2 -2
  21. pygpt_net/controller/remote_store/google/batch.py +402 -0
  22. pygpt_net/controller/remote_store/google/store.py +615 -0
  23. pygpt_net/controller/remote_store/openai/__init__.py +12 -0
  24. pygpt_net/controller/remote_store/openai/batch.py +524 -0
  25. pygpt_net/controller/{assistant → remote_store/openai}/store.py +63 -60
  26. pygpt_net/controller/remote_store/remote_store.py +35 -0
  27. pygpt_net/controller/ui/ui.py +20 -1
  28. pygpt_net/core/assistants/assistants.py +3 -15
  29. pygpt_net/core/db/database.py +5 -3
  30. pygpt_net/core/filesystem/url.py +4 -1
  31. pygpt_net/core/locale/placeholder.py +35 -0
  32. pygpt_net/core/remote_store/__init__.py +12 -0
  33. pygpt_net/core/remote_store/google/__init__.py +11 -0
  34. pygpt_net/core/remote_store/google/files.py +224 -0
  35. pygpt_net/core/remote_store/google/store.py +248 -0
  36. pygpt_net/core/remote_store/openai/__init__.py +11 -0
  37. pygpt_net/core/{assistants → remote_store/openai}/files.py +26 -19
  38. pygpt_net/core/{assistants → remote_store/openai}/store.py +32 -15
  39. pygpt_net/core/remote_store/remote_store.py +24 -0
  40. pygpt_net/core/render/web/body.py +3 -2
  41. pygpt_net/core/types/chunk.py +27 -0
  42. pygpt_net/data/config/config.json +8 -4
  43. pygpt_net/data/config/models.json +77 -3
  44. pygpt_net/data/config/settings.json +45 -0
  45. pygpt_net/data/js/app/template.js +1 -1
  46. pygpt_net/data/js/app.min.js +2 -2
  47. pygpt_net/data/locale/locale.de.ini +44 -41
  48. pygpt_net/data/locale/locale.en.ini +56 -43
  49. pygpt_net/data/locale/locale.es.ini +44 -41
  50. pygpt_net/data/locale/locale.fr.ini +44 -41
  51. pygpt_net/data/locale/locale.it.ini +44 -41
  52. pygpt_net/data/locale/locale.pl.ini +45 -42
  53. pygpt_net/data/locale/locale.uk.ini +44 -41
  54. pygpt_net/data/locale/locale.zh.ini +44 -41
  55. pygpt_net/data/locale/plugin.cmd_history.de.ini +1 -1
  56. pygpt_net/data/locale/plugin.cmd_history.en.ini +1 -1
  57. pygpt_net/data/locale/plugin.cmd_history.es.ini +1 -1
  58. pygpt_net/data/locale/plugin.cmd_history.fr.ini +1 -1
  59. pygpt_net/data/locale/plugin.cmd_history.it.ini +1 -1
  60. pygpt_net/data/locale/plugin.cmd_history.pl.ini +1 -1
  61. pygpt_net/data/locale/plugin.cmd_history.uk.ini +1 -1
  62. pygpt_net/data/locale/plugin.cmd_history.zh.ini +1 -1
  63. pygpt_net/data/locale/plugin.cmd_mouse_control.en.ini +14 -0
  64. pygpt_net/data/locale/plugin.cmd_web.de.ini +1 -1
  65. pygpt_net/data/locale/plugin.cmd_web.en.ini +1 -1
  66. pygpt_net/data/locale/plugin.cmd_web.es.ini +1 -1
  67. pygpt_net/data/locale/plugin.cmd_web.fr.ini +1 -1
  68. pygpt_net/data/locale/plugin.cmd_web.it.ini +1 -1
  69. pygpt_net/data/locale/plugin.cmd_web.pl.ini +1 -1
  70. pygpt_net/data/locale/plugin.cmd_web.uk.ini +1 -1
  71. pygpt_net/data/locale/plugin.cmd_web.zh.ini +1 -1
  72. pygpt_net/data/locale/plugin.idx_llama_index.de.ini +2 -2
  73. pygpt_net/data/locale/plugin.idx_llama_index.en.ini +2 -2
  74. pygpt_net/data/locale/plugin.idx_llama_index.es.ini +2 -2
  75. pygpt_net/data/locale/plugin.idx_llama_index.fr.ini +2 -2
  76. pygpt_net/data/locale/plugin.idx_llama_index.it.ini +2 -2
  77. pygpt_net/data/locale/plugin.idx_llama_index.pl.ini +2 -2
  78. pygpt_net/data/locale/plugin.idx_llama_index.uk.ini +2 -2
  79. pygpt_net/data/locale/plugin.idx_llama_index.zh.ini +2 -2
  80. pygpt_net/item/assistant.py +1 -211
  81. pygpt_net/item/ctx.py +3 -3
  82. pygpt_net/item/store.py +238 -0
  83. pygpt_net/js_rc.py +2449 -2447
  84. pygpt_net/migrations/Version20260102190000.py +35 -0
  85. pygpt_net/migrations/__init__.py +3 -1
  86. pygpt_net/plugin/cmd_mouse_control/config.py +471 -1
  87. pygpt_net/plugin/cmd_mouse_control/plugin.py +487 -22
  88. pygpt_net/plugin/cmd_mouse_control/worker.py +464 -87
  89. pygpt_net/plugin/cmd_mouse_control/worker_sandbox.py +729 -0
  90. pygpt_net/plugin/idx_llama_index/config.py +2 -2
  91. pygpt_net/provider/api/anthropic/__init__.py +10 -8
  92. pygpt_net/provider/api/google/__init__.py +21 -58
  93. pygpt_net/provider/api/google/chat.py +545 -129
  94. pygpt_net/provider/api/google/computer.py +190 -0
  95. pygpt_net/provider/api/google/realtime/realtime.py +2 -2
  96. pygpt_net/provider/api/google/remote_tools.py +93 -0
  97. pygpt_net/provider/api/google/store.py +546 -0
  98. pygpt_net/provider/api/google/worker/__init__.py +0 -0
  99. pygpt_net/provider/api/google/worker/importer.py +392 -0
  100. pygpt_net/provider/api/openai/__init__.py +7 -3
  101. pygpt_net/provider/api/openai/computer.py +10 -1
  102. pygpt_net/provider/api/openai/responses.py +0 -0
  103. pygpt_net/provider/api/openai/store.py +6 -6
  104. pygpt_net/provider/api/openai/worker/importer.py +24 -24
  105. pygpt_net/provider/api/x_ai/__init__.py +10 -9
  106. pygpt_net/provider/api/x_ai/chat.py +272 -102
  107. pygpt_net/provider/core/config/patch.py +16 -1
  108. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +3 -3
  109. pygpt_net/provider/core/model/patch.py +17 -3
  110. pygpt_net/provider/core/preset/json_file.py +13 -7
  111. pygpt_net/provider/core/{assistant_file → remote_file}/__init__.py +1 -1
  112. pygpt_net/provider/core/{assistant_file → remote_file}/base.py +9 -9
  113. pygpt_net/provider/core/remote_file/db_sqlite/__init__.py +12 -0
  114. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/patch.py +1 -1
  115. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/provider.py +23 -20
  116. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/storage.py +35 -27
  117. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/utils.py +5 -4
  118. pygpt_net/provider/core/{assistant_store → remote_store}/__init__.py +1 -1
  119. pygpt_net/provider/core/{assistant_store → remote_store}/base.py +10 -10
  120. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/__init__.py +1 -1
  121. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/patch.py +1 -1
  122. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/provider.py +16 -15
  123. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/storage.py +30 -23
  124. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/utils.py +5 -4
  125. pygpt_net/provider/core/{assistant_store → remote_store}/json_file.py +9 -9
  126. pygpt_net/provider/llms/google.py +2 -2
  127. pygpt_net/tools/image_viewer/ui/dialogs.py +298 -12
  128. pygpt_net/tools/text_editor/ui/widgets.py +5 -1
  129. pygpt_net/ui/base/config_dialog.py +3 -2
  130. pygpt_net/ui/base/context_menu.py +44 -1
  131. pygpt_net/ui/dialog/assistant.py +3 -3
  132. pygpt_net/ui/dialog/plugins.py +3 -1
  133. pygpt_net/ui/dialog/remote_store_google.py +539 -0
  134. pygpt_net/ui/dialog/{assistant_store.py → remote_store_openai.py} +95 -95
  135. pygpt_net/ui/dialogs.py +5 -3
  136. pygpt_net/ui/layout/chat/attachments_uploaded.py +3 -3
  137. pygpt_net/ui/layout/toolbox/computer_env.py +26 -8
  138. pygpt_net/ui/layout/toolbox/indexes.py +22 -19
  139. pygpt_net/ui/layout/toolbox/model.py +28 -5
  140. pygpt_net/ui/menu/tools.py +13 -5
  141. pygpt_net/ui/widget/dialog/remote_store_google.py +56 -0
  142. pygpt_net/ui/widget/dialog/{assistant_store.py → remote_store_openai.py} +9 -9
  143. pygpt_net/ui/widget/element/button.py +4 -4
  144. pygpt_net/ui/widget/image/display.py +25 -8
  145. pygpt_net/ui/widget/lists/remote_store_google.py +248 -0
  146. pygpt_net/ui/widget/lists/{assistant_store.py → remote_store_openai.py} +21 -21
  147. pygpt_net/ui/widget/option/checkbox_list.py +47 -9
  148. pygpt_net/ui/widget/option/combo.py +39 -3
  149. pygpt_net/ui/widget/tabs/output.py +9 -1
  150. pygpt_net/ui/widget/textarea/editor.py +14 -1
  151. pygpt_net/ui/widget/textarea/input.py +20 -7
  152. pygpt_net/ui/widget/textarea/notepad.py +24 -1
  153. pygpt_net/ui/widget/textarea/output.py +23 -1
  154. pygpt_net/ui/widget/textarea/web.py +16 -1
  155. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.6.dist-info}/METADATA +41 -2
  156. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.6.dist-info}/RECORD +158 -132
  157. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.6.dist-info}/LICENSE +0 -0
  158. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.6.dist-info}/WHEEL +0 -0
  159. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.6.dist-info}/entry_points.txt +0 -0
@@ -5,10 +5,11 @@
5
5
  # Website: https://pygpt.net #
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
- # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.11 14:00:00 #
8
+ # Created By : Marcin Szczyglinski #
9
+ # Updated Date: 2026.01.02 02:00:00 #
10
10
  # ================================================== #
11
11
 
12
+ import sys
12
13
  import time
13
14
 
14
15
  from pynput.mouse import Button, Controller as MouseController
@@ -22,6 +23,11 @@ class WorkerSignals(BaseSignals):
22
23
 
23
24
 
24
25
  class Worker(BaseWorker):
26
+ """
27
+ Host worker: executes computer-use actions using native OS input (pynput).
28
+ It supports the full set of Computer Use commands. Each response includes "url" (empty on host).
29
+ """
30
+
25
31
  def __init__(self, *args, **kwargs):
26
32
  super(Worker, self).__init__()
27
33
  self.signals = WorkerSignals()
@@ -42,57 +48,117 @@ class Worker(BaseWorker):
42
48
  break
43
49
  response = None
44
50
  try:
45
- if item["cmd"] in self.plugin.allowed_cmds and self.plugin.has_cmd(item["cmd"]):
46
- # get mouse position
47
- if item["cmd"] == "get_mouse_position":
48
- response = self.cmd_mouse_get_pos(item)
49
-
50
- # set mouse position
51
- elif item["cmd"] == "mouse_move":
52
- if self.plugin.get_option_value("allow_mouse_move"):
53
- response = self.cmd_mouse_move(item)
54
-
55
- # drag mouse
56
- elif item["cmd"] == "mouse_drag":
57
- if self.plugin.get_option_value("allow_mouse_move"):
58
- response = self.cmd_mouse_drag(item)
59
-
60
- # mouse click
61
- elif item["cmd"] == "mouse_click":
62
- if self.plugin.get_option_value("allow_mouse_click"):
63
- response = self.cmd_mouse_click(item)
64
-
65
- # mouse scroll
66
- elif item["cmd"] == "mouse_scroll":
67
- if self.plugin.get_option_value("allow_mouse_scroll"):
68
- response = self.cmd_mouse_scroll(item)
69
-
70
- # screenshot
71
- elif item["cmd"] == "get_screenshot":
72
- if self.plugin.get_option_value("allow_screenshot"):
73
- response = self.cmd_make_screenshot(item)
74
-
75
- # keyboard key
76
- elif item["cmd"] == "keyboard_key":
77
- if self.plugin.get_option_value("allow_keyboard"):
78
- response = self.cmd_keyboard_key(item)
79
-
80
- # keyboard key
81
- elif item["cmd"] == "keyboard_keys":
82
- if self.plugin.get_option_value("allow_keyboard"):
83
- response = self.cmd_keyboard_keys(item)
84
-
85
- # keyboard type
86
- elif item["cmd"] == "keyboard_type":
87
- if self.plugin.get_option_value("allow_keyboard"):
88
- response = self.cmd_keyboard_type(item)
89
-
90
- # wait
91
- elif item["cmd"] == "wait":
92
- response = self.cmd_wait(item)
93
-
94
- if response:
95
- responses.append(response)
51
+ cmd = item.get("cmd")
52
+ if not cmd:
53
+ continue
54
+
55
+ # alias before gating
56
+ if cmd == "screenshot":
57
+ item = dict(item)
58
+ item["cmd"] = "get_screenshot"
59
+ cmd = "get_screenshot"
60
+
61
+ # allow only plugin-declared commands
62
+ allowed = getattr(self.plugin, "allowed_cmds", None)
63
+ if isinstance(allowed, (list, set, tuple)) and cmd not in allowed:
64
+ continue
65
+
66
+ # open web browser
67
+ if cmd == "open_web_browser":
68
+ response = self.cmd_open_web_browser(item)
69
+
70
+ # get mouse position
71
+ elif cmd == "get_mouse_position":
72
+ response = self.cmd_mouse_get_pos(item)
73
+
74
+ # set mouse position
75
+ elif cmd == "mouse_move":
76
+ if self.plugin.get_option_value("allow_mouse_move"):
77
+ response = self.cmd_mouse_move(item)
78
+
79
+ # drag mouse
80
+ elif cmd == "mouse_drag":
81
+ if self.plugin.get_option_value("allow_mouse_move"):
82
+ response = self.cmd_mouse_drag(item)
83
+
84
+ # mouse click
85
+ elif cmd == "mouse_click":
86
+ if self.plugin.get_option_value("allow_mouse_click"):
87
+ response = self.cmd_mouse_click(item)
88
+
89
+ # mouse scroll
90
+ elif cmd == "mouse_scroll":
91
+ if self.plugin.get_option_value("allow_mouse_scroll"):
92
+ response = self.cmd_mouse_scroll(item)
93
+
94
+ # screenshot
95
+ elif cmd == "get_screenshot":
96
+ if self.plugin.get_option_value("allow_screenshot"):
97
+ response = self.cmd_make_screenshot(item)
98
+
99
+ # keyboard key
100
+ elif cmd == "keyboard_key":
101
+ if self.plugin.get_option_value("allow_keyboard"):
102
+ response = self.cmd_keyboard_key(item)
103
+
104
+ # keyboard keys
105
+ elif cmd == "keyboard_keys":
106
+ if self.plugin.get_option_value("allow_keyboard"):
107
+ response = self.cmd_keyboard_keys(item)
108
+
109
+ # keyboard type
110
+ elif cmd == "keyboard_type":
111
+ if self.plugin.get_option_value("allow_keyboard"):
112
+ response = self.cmd_keyboard_type(item)
113
+
114
+ # wait
115
+ elif cmd == "wait":
116
+ response = self.cmd_wait(item)
117
+
118
+ # Computer Use: added commands (host-native)
119
+ elif cmd == "wait_5_seconds":
120
+ response = self.cmd_wait_5_seconds(item)
121
+ elif cmd == "go_back":
122
+ response = self.cmd_go_back(item)
123
+ elif cmd == "go_forward":
124
+ response = self.cmd_go_forward(item)
125
+ elif cmd == "search":
126
+ response = self.cmd_search(item)
127
+ elif cmd == "navigate":
128
+ response = self.cmd_navigate(item)
129
+ elif cmd == "click_at":
130
+ response = self.cmd_click_at(item)
131
+ elif cmd == "hover_at":
132
+ response = self.cmd_hover_at(item)
133
+ elif cmd == "type_text_at":
134
+ response = self.cmd_type_text_at(item)
135
+ elif cmd == "key_combination":
136
+ response = self.cmd_key_combination(item)
137
+ elif cmd == "scroll_document":
138
+ response = self.cmd_scroll_document(item)
139
+ elif cmd == "scroll_at":
140
+ response = self.cmd_scroll_at(item)
141
+ elif cmd == "drag_and_drop":
142
+ response = self.cmd_drag_and_drop(item)
143
+
144
+ # Action-style
145
+ elif cmd == "click":
146
+ response = self.cmd_click(item)
147
+ elif cmd == "double_click":
148
+ response = self.cmd_double_click(item)
149
+ elif cmd == "move":
150
+ response = self.cmd_move(item)
151
+ elif cmd == "type":
152
+ response = self.cmd_type_text(item)
153
+ elif cmd == "keypress":
154
+ response = self.cmd_keypress(item)
155
+ elif cmd == "scroll":
156
+ response = self.cmd_scroll(item)
157
+ elif cmd == "drag":
158
+ response = self.cmd_drag(item)
159
+
160
+ if response:
161
+ responses.append(response)
96
162
 
97
163
  except Exception as e:
98
164
  responses.append(
@@ -103,7 +169,7 @@ class Worker(BaseWorker):
103
169
  )
104
170
 
105
171
  if len(responses) > 0:
106
- self.reply_more(responses) # send response
172
+ self.reply_more(responses) # send response
107
173
 
108
174
  except Exception as e:
109
175
  self.error(e)
@@ -114,6 +180,62 @@ class Worker(BaseWorker):
114
180
  """Handle destroyed event."""
115
181
  self.cleanup()
116
182
 
183
+ # ========================= Helpers ========================= #
184
+
185
+ def _get_screen_size(self) -> tuple:
186
+ screen = self.window.app.primaryScreen()
187
+ size = screen.size()
188
+ return size.width(), size.height()
189
+
190
+ def _denorm_x(self, x_norm: int) -> int:
191
+ w, _ = self._get_screen_size()
192
+ x_norm = max(0, min(999, int(x_norm)))
193
+ return int(round(x_norm / 1000.0 * w))
194
+
195
+ def _denorm_y(self, y_norm: int) -> int:
196
+ _, h = self._get_screen_size()
197
+ y_norm = max(0, min(999, int(y_norm)))
198
+ return int(round(y_norm / 1000.0 * h))
199
+
200
+ def _is_normalized_pair(self, x, y) -> bool:
201
+ try:
202
+ xi, yi = int(x), int(y)
203
+ return 0 <= xi <= 999 and 0 <= yi <= 999
204
+ except Exception:
205
+ return False
206
+
207
+ def _is_mac(self) -> bool:
208
+ return sys.platform == "darwin"
209
+
210
+ # ========================= Legacy-compatible commands ========================= #
211
+
212
+ def cmd_open_web_browser(self, item: dict) -> dict:
213
+ """
214
+ Open web browser
215
+
216
+ :param item: command item
217
+ :return: response item
218
+ """
219
+ import webbrowser
220
+
221
+ try:
222
+ self.msg = "Open web browser"
223
+ self.log(self.msg)
224
+ url = ""
225
+ if self.has_param(item, "url"):
226
+ url = self.get_param(item, "url")
227
+ webbrowser.open(url)
228
+ result = self.get_current(item)
229
+ self.log("Response: {}".format(result))
230
+ except Exception as e:
231
+ result = self.throw_error(e)
232
+
233
+ # disable returning screenshot if requested
234
+ if self.has_param(item, "no_screenshot"):
235
+ result["no_screenshot"] = True
236
+
237
+ return self.make_response(item, result)
238
+
117
239
  def cmd_wait(self, item: dict) -> dict:
118
240
  """
119
241
  Wait
@@ -121,7 +243,10 @@ class Worker(BaseWorker):
121
243
  :param item: command item
122
244
  :return: response item
123
245
  """
246
+ wait_time = 5
124
247
  try:
248
+ if self.has_param(item, "seconds"):
249
+ wait_time = int(self.get_param(item, "seconds"))
125
250
  self.msg = "Wait"
126
251
  self.log(self.msg)
127
252
  result = self.get_current(item)
@@ -133,7 +258,7 @@ class Worker(BaseWorker):
133
258
  if self.has_param(item, "no_screenshot"):
134
259
  result["no_screenshot"] = True
135
260
 
136
- time.sleep(2)
261
+ time.sleep(wait_time)
137
262
  return self.make_response(item, result)
138
263
 
139
264
  def cmd_mouse_get_pos(self, item: dict) -> dict:
@@ -178,20 +303,30 @@ class Worker(BaseWorker):
178
303
  elif self.has_param(item, "mouse_y"):
179
304
  y = self.get_param(item, "mouse_y")
180
305
 
306
+ # accept normalized 0..999
307
+ try:
308
+ if self._is_normalized_pair(x, y):
309
+ x = self._denorm_x(int(x))
310
+ y = self._denorm_y(int(y))
311
+ except Exception:
312
+ pass
313
+
181
314
  if self.has_param(item, "click"):
182
315
  click = self.get_param(item, "click")
183
316
  if self.has_param(item, "num_clicks"):
184
317
  num_clicks = int(self.get_param(item, "num_clicks"))
185
318
  try:
186
319
  mouse = MouseController()
187
- mouse.position = (x, y)
320
+ mouse.position = (int(x), int(y))
188
321
  if click:
189
- time.sleep(0.5) # wait for a moment before clicking
322
+ time.sleep(0.05)
190
323
  self.cmd_mouse_click({
191
324
  "cmd": "mouse_click",
192
325
  "params": {
193
326
  "button": click,
194
327
  "num_clicks": num_clicks,
328
+ "x": int(x),
329
+ "y": int(y),
195
330
  }
196
331
  })
197
332
  except Exception as e:
@@ -220,17 +355,29 @@ class Worker(BaseWorker):
220
355
  """
221
356
  button = Button.left
222
357
  num = 1
358
+ x = None
359
+ y = None
223
360
  if self.has_param(item, "button"):
224
361
  btn_name = self.get_param(item, "button")
225
- if btn_name.lower() == "middle":
226
- button = Button.middle
227
- elif btn_name.lower() == "right":
228
- button = Button.right
362
+ if isinstance(btn_name, str):
363
+ if btn_name.lower() == "middle":
364
+ button = Button.middle
365
+ elif btn_name.lower() == "right":
366
+ button = Button.right
229
367
  if self.has_param(item, "num_clicks"):
230
368
  num = int(self.get_param(item, "num_clicks"))
369
+ if self.has_param(item, "x") and self.has_param(item, "y"):
370
+ x = int(self.get_param(item, "x"))
371
+ y = int(self.get_param(item, "y"))
372
+ if self._is_normalized_pair(x, y):
373
+ x = self._denorm_x(x)
374
+ y = self._denorm_y(y)
231
375
  try:
232
376
  mouse = MouseController()
233
- mouse.click(button, num)
377
+ if x is not None and y is not None:
378
+ mouse.position = (x, y)
379
+ time.sleep(0.05)
380
+ mouse.click(button, max(1, num))
234
381
  result = self.get_current(item)
235
382
  self.log("Response: {}".format(result))
236
383
  except Exception as e:
@@ -261,9 +408,14 @@ class Worker(BaseWorker):
261
408
  y = self.get_param(item, "mouse_y")
262
409
  if x is not None and y is not None:
263
410
  try:
411
+ # accept normalized 0..999
412
+ xi, yi = int(x), int(y)
413
+ if self._is_normalized_pair(xi, yi):
414
+ xi = self._denorm_x(xi)
415
+ yi = self._denorm_y(yi)
264
416
  mouse = MouseController()
265
- mouse.position = (x, y)
266
- time.sleep(0.5) # wait for a moment before scrolling
417
+ mouse.position = (xi, yi)
418
+ time.sleep(0.05)
267
419
  except Exception as e:
268
420
  error = str(e)
269
421
  self.log("Error: {}".format(e))
@@ -273,9 +425,9 @@ class Worker(BaseWorker):
273
425
  conversion_factor = 30
274
426
  delay = 0.01
275
427
  if self.has_param(item, "dx"):
276
- dx = self.get_param(item, "dx")
428
+ dx = int(self.get_param(item, "dx"))
277
429
  if self.has_param(item, "dy"):
278
- dy = self.get_param(item, "dy")
430
+ dy = int(self.get_param(item, "dy"))
279
431
  if self.has_param(item, "unit"):
280
432
  tmp_unit = self.get_param(item, "unit")
281
433
  if tmp_unit in ["step", "px"]:
@@ -293,14 +445,14 @@ class Worker(BaseWorker):
293
445
 
294
446
  # scroll x
295
447
  for _ in range(abs(notches_x)):
296
- dx = 1 if notches_x > 0 else -1
297
- mouse.scroll(dx, 0)
448
+ sdx = 1 if notches_x > 0 else -1
449
+ mouse.scroll(sdx, 0)
298
450
  time.sleep(delay)
299
451
 
300
452
  # scroll y
301
453
  for _ in range(abs(notches_y)):
302
- dy = 1 if notches_y > 0 else -1
303
- mouse.scroll(0, dy)
454
+ sdy = 1 if notches_y > 0 else -1
455
+ mouse.scroll(0, sdy)
304
456
  time.sleep(delay)
305
457
 
306
458
  print("scrolling: dx={}, dy={}".format(dx, dy))
@@ -317,7 +469,7 @@ class Worker(BaseWorker):
317
469
 
318
470
  def cmd_mouse_drag(self, item: dict) -> dict:
319
471
  """
320
- Mouse scroll
472
+ Mouse drag
321
473
 
322
474
  :param item: command item
323
475
  :return: response item
@@ -325,18 +477,21 @@ class Worker(BaseWorker):
325
477
  x = None
326
478
  y = None
327
479
  if self.has_param(item, "x"):
328
- x = self.get_param(item, "x")
480
+ x = int(self.get_param(item, "x"))
329
481
  elif self.has_param(item, "mouse_x"):
330
- x = self.get_param(item, "mouse_x")
482
+ x = int(self.get_param(item, "mouse_x"))
331
483
  if self.has_param(item, "y"):
332
- y = self.get_param(item, "y")
484
+ y = int(self.get_param(item, "y"))
333
485
  elif self.has_param(item, "mouse_y"):
334
- y = self.get_param(item, "mouse_y")
486
+ y = int(self.get_param(item, "mouse_y"))
335
487
  if x is not None and y is not None:
336
488
  try:
489
+ if self._is_normalized_pair(x, y):
490
+ x = self._denorm_x(x)
491
+ y = self._denorm_y(y)
337
492
  mouse = MouseController()
338
493
  mouse.position = (x, y)
339
- time.sleep(0.5) # wait for a moment before scrolling
494
+ time.sleep(0.05)
340
495
  except Exception as e:
341
496
  error = str(e)
342
497
  self.log("Error: {}".format(e))
@@ -344,14 +499,18 @@ class Worker(BaseWorker):
344
499
  dy = 0
345
500
  delay = 0.02
346
501
  if self.has_param(item, "dx"):
347
- dx = self.get_param(item, "dx")
502
+ dx = int(self.get_param(item, "dx"))
348
503
  if self.has_param(item, "dy"):
349
- dy = self.get_param(item, "dy")
504
+ dy = int(self.get_param(item, "dy"))
350
505
  try:
506
+ # If dx,dy are normalized destination, convert
507
+ if self._is_normalized_pair(dx, dy):
508
+ dx = self._denorm_x(dx)
509
+ dy = self._denorm_y(dy)
351
510
  mouse = MouseController()
352
511
  mouse.press(Button.left)
353
512
  time.sleep(delay)
354
- mouse.position = (dx, dy) # move to the new position
513
+ mouse.position = (dx, dy) # absolute destination
355
514
  time.sleep(delay)
356
515
  mouse.release(Button.left)
357
516
  print("dragging: dx={}, dy={}".format(dx, dy))
@@ -368,10 +527,8 @@ class Worker(BaseWorker):
368
527
 
369
528
  def cmd_keyboard_keys(self, item: dict) -> dict:
370
529
  """
371
- Keyboard keys press
372
-
373
- :param item: command item
374
- :return: response item
530
+ Keyboard keys press (sequence, single modifier supported)
531
+ For multiple modifiers use key_combination.
375
532
  """
376
533
  keyboard = KeyboardController()
377
534
  error = None
@@ -384,7 +541,7 @@ class Worker(BaseWorker):
384
541
  keys = self.get_param(item, "keys")
385
542
  for key in keys:
386
543
  if isinstance(key, str):
387
- if key.lower() in modifiers_list:
544
+ if key.lower() in modifiers_list and modifier is None:
388
545
  # check if key is modifier
389
546
  if key.lower() == "ctrl" or key.lower() == "control":
390
547
  modifier = Key.ctrl
@@ -463,7 +620,7 @@ class Worker(BaseWorker):
463
620
  self.set_focus()
464
621
  time.sleep(1) # wait for a second
465
622
 
466
- if key.lower() == "super" or key.lower() == "start":
623
+ if isinstance(key, str) and (key.lower() == "super" or key.lower() == "start"):
467
624
  key = Key.cmd
468
625
 
469
626
  key = self.remap_key(key) # remap key if needed
@@ -496,7 +653,7 @@ class Worker(BaseWorker):
496
653
 
497
654
  def cmd_keyboard_type(self, item: dict) -> dict:
498
655
  """
499
- Keyboard key press
656
+ Keyboard type text
500
657
 
501
658
  :param item: command item
502
659
  :return: response item
@@ -607,7 +764,7 @@ class Worker(BaseWorker):
607
764
  "END": Key.end,
608
765
  "HOME": Key.home,
609
766
  }
610
- k = key.upper()
767
+ k = key.upper() if isinstance(key, str) else key
611
768
  return mapping.get(k, key)
612
769
 
613
770
  def get_current(self, item: dict = None) -> dict:
@@ -630,4 +787,224 @@ class Worker(BaseWorker):
630
787
  'screen_h': screen_y,
631
788
  'mouse_x': mouse_pos_x,
632
789
  'mouse_y': mouse_pos_y,
790
+ 'url': "", # host has no sandbox browser URL
633
791
  }
792
+
793
+ # ========================= New Computer Use commands (host-native) ========================= #
794
+
795
+ def cmd_wait_5_seconds(self, item: dict) -> dict:
796
+ return self.cmd_wait({"cmd": "wait", "params": {"seconds": 5}})
797
+
798
+ def cmd_go_back(self, item: dict) -> dict:
799
+ keys = ["cmd", "["] if self._is_mac() else ["alt", "left"]
800
+ return self.cmd_keyboard_keys({"cmd": "keyboard_keys", "params": {"keys": keys}})
801
+
802
+ def cmd_go_forward(self, item: dict) -> dict:
803
+ keys = ["cmd", "]"] if self._is_mac() else ["alt", "right"]
804
+ return self.cmd_keyboard_keys({"cmd": "keyboard_keys", "params": {"keys": keys}})
805
+
806
+ def cmd_search(self, item: dict) -> dict:
807
+ return self.cmd_open_web_browser({"cmd": "open_web_browser", "params": {"url": "https://www.google.com"}})
808
+
809
+ def cmd_navigate(self, item: dict) -> dict:
810
+ url = ""
811
+ if self.has_param(item, "url"):
812
+ url = self.get_param(item, "url") or ""
813
+ return self.cmd_open_web_browser({"cmd": "open_web_browser", "params": {"url": url}})
814
+
815
+ def cmd_click_at(self, item: dict) -> dict:
816
+ x = int(self.get_param(item, "x"))
817
+ y = int(self.get_param(item, "y"))
818
+ px = self._denorm_x(x)
819
+ py = self._denorm_y(y)
820
+ return self.cmd_mouse_move({"cmd": "mouse_move", "params": {"x": px, "y": py, "click": "left", "num_clicks": 1}})
821
+
822
+ def cmd_hover_at(self, item: dict) -> dict:
823
+ x = int(self.get_param(item, "x"))
824
+ y = int(self.get_param(item, "y"))
825
+ px = self._denorm_x(x)
826
+ py = self._denorm_y(y)
827
+ return self.cmd_mouse_move({"cmd": "mouse_move", "params": {"x": px, "y": py}})
828
+
829
+ def cmd_type_text_at(self, item: dict) -> dict:
830
+ x = int(self.get_param(item, "x"))
831
+ y = int(self.get_param(item, "y"))
832
+ px = self._denorm_x(x)
833
+ py = self._denorm_y(y)
834
+ text = self.get_param(item, "text", "") or ""
835
+ press_enter = bool(self.get_param(item, "press_enter", True))
836
+ clear_before = bool(self.get_param(item, "clear_before_typing", True))
837
+
838
+ # focus field
839
+ self.cmd_mouse_move({"cmd": "mouse_move", "params": {"x": px, "y": py, "click": "left", "num_clicks": 1}})
840
+ if clear_before:
841
+ keys = ["cmd", "a"] if self._is_mac() else ["ctrl", "a"]
842
+ self.cmd_keyboard_keys({"cmd": "keyboard_keys", "params": {"keys": keys}})
843
+ self.cmd_keyboard_key({"cmd": "keyboard_key", "params": {"key": "BACKSPACE"}})
844
+ self.cmd_keyboard_type({"cmd": "keyboard_type", "params": {"text": text}})
845
+ if press_enter:
846
+ self.cmd_keyboard_key({"cmd": "keyboard_key", "params": {"key": "ENTER"}})
847
+ return self.make_response(item, self.get_current(item))
848
+
849
+ def cmd_key_combination(self, item: dict) -> dict:
850
+ keyboard = KeyboardController()
851
+ error = None
852
+
853
+ # autofocus on the window
854
+ if self.plugin.get_option_value("auto_focus"):
855
+ self.set_focus()
856
+ time.sleep(1)
857
+
858
+ try:
859
+ raw = self.get_param(item, "keys", [])
860
+ if isinstance(raw, str):
861
+ parts = [p.strip() for p in raw.replace("+", " ").split() if p.strip()]
862
+ else:
863
+ parts = list(raw or [])
864
+
865
+ # Separate modifiers
866
+ mods_map = {
867
+ "ctrl": Key.ctrl, "control": Key.ctrl,
868
+ "alt": Key.alt, "shift": Key.shift,
869
+ "cmd": Key.cmd, "super": Key.cmd, "start": Key.cmd,
870
+ }
871
+ modifiers = []
872
+ keys = []
873
+ for p in parts:
874
+ pl = p.lower()
875
+ if pl in mods_map and mods_map[pl] not in modifiers:
876
+ modifiers.append(mods_map[pl])
877
+ else:
878
+ keys.append(self.remap_key(p))
879
+
880
+ for m in modifiers:
881
+ keyboard.press(m)
882
+ for k in keys:
883
+ keyboard.press(k)
884
+ keyboard.release(k)
885
+ for m in reversed(modifiers):
886
+ keyboard.release(m)
887
+
888
+ result = self.get_current(item)
889
+ except Exception as e:
890
+ result = self.throw_error(e)
891
+
892
+ return self.make_response(item, result)
893
+
894
+ def cmd_scroll_document(self, item: dict) -> dict:
895
+ direction = str(self.get_param(item, "direction", "down")).lower()
896
+ magnitude = int(self.get_param(item, "magnitude", 800))
897
+ dx, dy = 0, 0
898
+ if direction == "down":
899
+ dy = magnitude
900
+ elif direction == "up":
901
+ dy = -magnitude
902
+ elif direction == "left":
903
+ dx = -magnitude
904
+ elif direction == "right":
905
+ dx = magnitude
906
+ return self.cmd_mouse_scroll({"cmd": "mouse_scroll", "params": {"dx": dx, "dy": dy, "unit": "px"}})
907
+
908
+ def cmd_scroll_at(self, item: dict) -> dict:
909
+ direction = str(self.get_param(item, "direction", "down")).lower()
910
+ magnitude = int(self.get_param(item, "magnitude", 800))
911
+ x = self.get_param(item, "x", None)
912
+ y = self.get_param(item, "y", None)
913
+ px = self._denorm_x(int(x)) if x is not None else None
914
+ py = self._denorm_y(int(y)) if y is not None else None
915
+ dx, dy = 0, 0
916
+ if direction == "down":
917
+ dy = magnitude
918
+ elif direction == "up":
919
+ dy = -magnitude
920
+ elif direction == "left":
921
+ dx = -magnitude
922
+ elif direction == "right":
923
+ dx = magnitude
924
+ payload = {"dx": dx, "dy": dy, "unit": "px"}
925
+ if px is not None and py is not None:
926
+ payload["x"] = px
927
+ payload["y"] = py
928
+ return self.cmd_mouse_scroll({"cmd": "mouse_scroll", "params": payload})
929
+
930
+ def cmd_drag_and_drop(self, item: dict) -> dict:
931
+ x = int(self.get_param(item, "x"))
932
+ y = int(self.get_param(item, "y"))
933
+ dx = int(self.get_param(item, "destination_x"))
934
+ dy = int(self.get_param(item, "destination_y"))
935
+ return self.cmd_mouse_drag({
936
+ "cmd": "mouse_drag",
937
+ "params": {
938
+ "x": self._denorm_x(x),
939
+ "y": self._denorm_y(y),
940
+ "dx": self._denorm_x(dx),
941
+ "dy": self._denorm_y(dy),
942
+ }
943
+ })
944
+
945
+ # ========================= Action-style convenience ========================= #
946
+
947
+ def cmd_click(self, item: dict) -> dict:
948
+ p = dict(item.get("params", {}))
949
+ x = p.get("x", None)
950
+ y = p.get("y", None)
951
+ if x is not None and y is not None and self._is_normalized_pair(x, y):
952
+ p["x"] = self._denorm_x(int(x))
953
+ p["y"] = self._denorm_y(int(y))
954
+ p["num_clicks"] = int(p.get("num_clicks", 1))
955
+ return self.cmd_mouse_click({"cmd": "mouse_click", "params": p})
956
+
957
+ def cmd_double_click(self, item: dict) -> dict:
958
+ p = dict(item.get("params", {}))
959
+ p["num_clicks"] = 2
960
+ return self.cmd_click({"cmd": "click", "params": p})
961
+
962
+ def cmd_move(self, item: dict) -> dict:
963
+ p = dict(item.get("params", {}))
964
+ if "x" in p and "y" in p and self._is_normalized_pair(p["x"], p["y"]):
965
+ p["x"] = self._denorm_x(int(p["x"]))
966
+ p["y"] = self._denorm_y(int(p["y"]))
967
+ return self.cmd_mouse_move({"cmd": "mouse_move", "params": p})
968
+
969
+ def cmd_type_text(self, item: dict) -> dict:
970
+ p = dict(item.get("params", {}))
971
+ return self.cmd_keyboard_type({"cmd": "keyboard_type", "params": p})
972
+
973
+ def cmd_keypress(self, item: dict) -> dict:
974
+ p = dict(item.get("params", {}))
975
+ return self.cmd_keyboard_keys({"cmd": "keyboard_keys", "params": p})
976
+
977
+ def cmd_scroll(self, item: dict) -> dict:
978
+ p = dict(item.get("params", {}))
979
+ # accept scroll_x/scroll_y aliases
980
+ if "scroll_x" in p:
981
+ p["dx"] = int(p.get("scroll_x", 0))
982
+ if "scroll_y" in p:
983
+ p["dy"] = int(p.get("scroll_y", 0))
984
+ p["unit"] = "px"
985
+ # normalize optional pointer position
986
+ if "x" in p and "y" in p and self._is_normalized_pair(p["x"], p["y"]):
987
+ p["x"] = self._denorm_x(int(p["x"]))
988
+ p["y"] = self._denorm_y(int(p["y"]))
989
+ return self.cmd_mouse_scroll({"cmd": "mouse_scroll", "params": p})
990
+
991
+ def cmd_drag(self, item: dict) -> dict:
992
+ p = dict(item.get("params", {}))
993
+ path = p.get("path", None)
994
+ if path and isinstance(path, list) and len(path) >= 2:
995
+ x0 = int(path[0]["x"]); y0 = int(path[0]["y"])
996
+ x1 = int(path[1]["x"]); y1 = int(path[1]["y"])
997
+ if self._is_normalized_pair(x0, y0):
998
+ x0 = self._denorm_x(x0); y0 = self._denorm_y(y0)
999
+ if self._is_normalized_pair(x1, y1):
1000
+ x1 = self._denorm_x(x1); y1 = self._denorm_y(y1)
1001
+ return self.cmd_mouse_drag({"cmd": "mouse_drag", "params": {"x": x0, "y": y0, "dx": x1, "dy": y1}})
1002
+ # fallback: explicit x,y and dx,dy
1003
+ if "x" in p and "y" in p and "dx" in p and "dy" in p:
1004
+ x0 = int(p["x"]); y0 = int(p["y"]); x1 = int(p["dx"]); y1 = int(p["dy"])
1005
+ if self._is_normalized_pair(x0, y0):
1006
+ x0 = self._denorm_x(x0); y0 = self._denorm_y(y0)
1007
+ if self._is_normalized_pair(x1, y1):
1008
+ x1 = self._denorm_x(x1); y1 = self._denorm_y(y1)
1009
+ return self.cmd_mouse_drag({"cmd": "mouse_drag", "params": {"x": x0, "y": y0, "dx": x1, "dy": y1}})
1010
+ return self.make_response(item, self.get_current(item))