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
@@ -6,8 +6,10 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.15 23:00:00 #
9
+ # Updated Date: 2026.01.02 02:00:00 #
10
10
  # ================================================== #
11
+ import time
12
+ import os
11
13
 
12
14
  from PySide6.QtCore import Slot, QTimer
13
15
 
@@ -31,22 +33,54 @@ class Plugin(BasePlugin):
31
33
  self.prefix = "Mouse"
32
34
  self.order = 100
33
35
  self.allowed_cmds = [
36
+ "open_web_browser",
34
37
  "get_mouse_position",
35
38
  "mouse_move",
39
+ "mouse_drag",
36
40
  "mouse_click",
37
41
  "mouse_scroll",
38
- "mouse_drag",
39
42
  "get_screenshot",
40
43
  "keyboard_key",
41
44
  "keyboard_keys",
42
45
  "keyboard_type",
43
46
  "wait",
47
+ "wait_5_seconds",
48
+ "go_back",
49
+ "go_forward",
50
+ "search",
51
+ "navigate",
52
+ "click_at",
53
+ "hover_at",
54
+ "type_text_at",
55
+ "key_combination",
56
+ "scroll_document",
57
+ "scroll_at",
58
+ "drag_and_drop",
59
+ "click",
60
+ "double_click",
61
+ "move",
62
+ "type",
63
+ "keypress",
64
+ "scroll",
65
+ "drag"
44
66
  ]
45
67
  self.use_locale = True
46
68
  self.worker = None
47
69
  self.config = Config(self)
48
70
  self.init_options()
49
71
 
72
+ # Playwright sandbox context
73
+ self.page = None
74
+ self.pw = None
75
+ self.browser = None
76
+ self.context = None
77
+ self.viewport_w = 1440
78
+ self.viewport_h = 900
79
+
80
+ # Pointer tracking for sandbox operations
81
+ self.pointer_x = 0
82
+ self.pointer_y = 0
83
+
50
84
  def init_options(self):
51
85
  """Initialize options"""
52
86
  self.config.from_defaults(self)
@@ -85,6 +119,30 @@ class Plugin(BasePlugin):
85
119
  if self.has_cmd(option):
86
120
  data['cmd'].append(self.get_cmd(option)) # append command
87
121
 
122
+ def is_sandbox(self) -> bool:
123
+ """
124
+ Check if sandbox mode is enabled
125
+
126
+ :return: True if sandbox mode is enabled
127
+ """
128
+ return bool(self.window.core.config.get("remote_tools.computer_use.sandbox", False))
129
+
130
+ def get_worker(self):
131
+ """
132
+ Get Worker instance based on sandbox option
133
+
134
+ :return: Worker instance (native or sandboxed/Playwright)
135
+ """
136
+ if self.is_sandbox():
137
+ from .worker_sandbox import Worker
138
+ worker = Worker()
139
+ worker.signals.start.connect(self.on_playwright_start)
140
+ # Connect main-thread Playwright control channel
141
+ worker.signals.call.connect(self.on_playwright_call)
142
+ return worker
143
+ from .worker import Worker
144
+ return Worker()
145
+
88
146
  def cmd(self, ctx: CtxItem, cmds: list):
89
147
  """
90
148
  Event: CMD_EXECUTE
@@ -92,8 +150,6 @@ class Plugin(BasePlugin):
92
150
  :param ctx: CtxItem
93
151
  :param cmds: commands dict
94
152
  """
95
- from .worker import Worker
96
-
97
153
  is_cmd = False
98
154
  my_commands = []
99
155
  for item in cmds:
@@ -108,7 +164,7 @@ class Plugin(BasePlugin):
108
164
  self.cmd_prepare(ctx, my_commands)
109
165
 
110
166
  try:
111
- worker = Worker()
167
+ worker = self.get_worker()
112
168
  worker.from_defaults(self)
113
169
  worker.cmds = my_commands
114
170
  worker.ctx = ctx
@@ -129,15 +185,27 @@ class Plugin(BasePlugin):
129
185
  :param item: command item to execute
130
186
  :return:
131
187
  """
132
- from .worker import Worker
133
-
134
188
  item["params"]["no_screenshot"] = True # do not take screenshot for single command call
135
- worker = Worker()
189
+ worker = self.get_worker()
136
190
  worker.from_defaults(self)
137
191
  worker.cmds = [item]
138
192
  worker.ctx = CtxItem()
139
193
  worker.run() # sync
140
194
 
195
+ def is_google(self, ctx: CtxItem) -> bool:
196
+ """
197
+ Check if full response is required based on model provider
198
+
199
+ :param ctx: context (CtxItem)
200
+ :return: True if full response is required, False otherwise
201
+ """
202
+ model_id = ctx.model
203
+ model_data = self.window.core.models.get(model_id) if model_id else None
204
+ if model_data is not None:
205
+ if model_data.provider == "google":
206
+ return True
207
+ return False
208
+
141
209
  @Slot(list, object, dict)
142
210
  def handle_finished_more(self, responses: list, ctx: CtxItem = None, extra_data: dict = None):
143
211
  """
@@ -155,7 +223,7 @@ class Plugin(BasePlugin):
155
223
  and response["result"]["no_screenshot"]):
156
224
  with_screenshot = False
157
225
  if ctx is not None:
158
- ctx.results.append(response)
226
+ self.prepare_reply_ctx(response, ctx)
159
227
  ctx.reply = True
160
228
  self.handle_delayed(ctx, with_screenshot)
161
229
 
@@ -173,9 +241,11 @@ class Plugin(BasePlugin):
173
241
 
174
242
  context = BridgeContext()
175
243
  context.ctx = ctx
244
+ extra = {}
245
+ extra["response_type"] = "multiple"
176
246
  event = KernelEvent(KernelEvent.REPLY_ADD, {
177
247
  'context': context,
178
- 'extra': {},
248
+ 'extra': extra,
179
249
  })
180
250
  self.window.dispatch(event)
181
251
 
@@ -186,18 +256,413 @@ class Plugin(BasePlugin):
186
256
  :param ctx: context (CtxItem)
187
257
  """
188
258
  self.window.controller.attachment.clear_silent()
189
- path = self.window.controller.painter.capture.screenshot(attach_cursor=True,
190
- silent=True) # attach screenshot
191
- img_path = self.window.core.filesystem.make_local(path)
192
- ctx.images.append(img_path)
193
- context = BridgeContext()
194
- context.ctx = ctx
195
- event = KernelEvent(KernelEvent.REPLY_ADD, {
196
- 'context': context,
197
- 'extra': {},
198
- })
199
- self.window.dispatch(event)
259
+ if self.is_sandbox():
260
+ path = self.window.controller.painter.capture.screenshot_playwright(page=self.page, silent=True) # Playwright screenshot
261
+ else:
262
+ path = self.window.controller.painter.capture.screenshot(attach_cursor=True, silent=True) # attach screenshot
263
+ if path:
264
+ img_path = self.window.core.filesystem.make_local(path)
265
+ ctx.images_before.append(img_path)
266
+ context = BridgeContext()
267
+ context.ctx = ctx
268
+ event = KernelEvent(KernelEvent.REPLY_ADD, {
269
+ 'context': context,
270
+ 'extra': {},
271
+ })
272
+ self.window.dispatch(event)
273
+
274
+ @Slot()
275
+ def on_playwright_start(self):
276
+ """
277
+ Event: PLAYWRIGHT_START
278
+ """
279
+ self._ensure_browser()
280
+
281
+ def _ensure_browser(self):
282
+ """Start Playwright browser and page if not started yet."""
283
+ if self.page:
284
+ # check if browser is still connected
285
+ try:
286
+ self.page.screenshot()
287
+ return
288
+ except Exception as e:
289
+ print(e)
290
+ if self.page:
291
+ try:
292
+ self.page.close()
293
+ except Exception:
294
+ pass
295
+ self.page = None
296
+ if self.browser:
297
+ try:
298
+ self.browser.close()
299
+ except Exception:
300
+ pass
301
+ self.browser = None
302
+ if self.pw:
303
+ try:
304
+ self.pw.stop()
305
+ except Exception:
306
+ pass
307
+ self.pw = None
308
+ if self.context:
309
+ try:
310
+ self.context.close()
311
+ except Exception:
312
+ pass
313
+ self.context = None
314
+
315
+
316
+ # Playwright is required in sandbox mode
317
+ try:
318
+ from playwright.sync_api import sync_playwright
319
+ except Exception:
320
+ sync_playwright = None
321
+
322
+ if sync_playwright is None:
323
+ raise RuntimeError("Playwright is not installed. Please install 'playwright' and the chosen browser.")
200
324
 
325
+ # Determine headless and engine from plugin options if available
326
+ headless = True
327
+ engine = "chromium"
328
+ try:
329
+ if hasattr(self, "get_option_value"):
330
+ hv = self.get_option_value("sandbox_headless")
331
+ if hv is not None:
332
+ headless = bool(hv)
333
+ ev = self.get_option_value("sandbox_browser")
334
+ if ev in ("chromium", "firefox", "webkit"):
335
+ engine = ev
336
+ vw = int(self.get_option_value("sandbox_viewport_w") or self.viewport_w)
337
+ vh = int(self.get_option_value("sandbox_viewport_h") or self.viewport_h)
338
+ self.viewport_w, self.viewport_h = vw, vh
339
+ except Exception:
340
+ pass
341
+
342
+ args = []
343
+ cfg_engine = self.get_option_value("sandbox_engine")
344
+ if cfg_engine:
345
+ engine = cfg_engine
346
+ cfg_args = self.get_option_value("sandbox_args")
347
+ if cfg_args:
348
+ args = [arg.strip() for arg in cfg_args.split(",") if arg.strip()]
349
+ cfg_path = self.get_option_value("sandbox_path")
350
+ if cfg_path:
351
+ os.environ['PLAYWRIGHT_BROWSERS_PATH'] = cfg_path
352
+ err_msg = None
353
+ if cfg_path and not (os.path.exists(cfg_path) and os.path.isdir(cfg_path)):
354
+ err_msg = (f"Playwright browsers path does not exist: {cfg_path}\n\n"
355
+ f"1) Please install Playwright browser(s) on host machine: \n\n"
356
+ f"pip install playwright && playwright install {engine}\n\n"
357
+ f"2) Set path to browsers directory in `Mouse And Keyboard` plugin settings option: "
358
+ f"`Sandbox (Playwright): Browsers directory`.")
359
+ else:
360
+ if os.environ.get("APPIMAGE") and not cfg_path: # set path is required for AppImage version
361
+ err_msg = (f"Playwright browsers path is not set:\n\n "
362
+ f"1) Please install Playwright browser(s) on host machine: \n\n"
363
+ f"pip install playwright && playwright install {engine}\n\n"
364
+ f"2) Set path to browsers directory in `Mouse And Keyboard` plugin settings option: "
365
+ f"`Sandbox (Playwright): Browsers directory`.")
366
+
367
+ if err_msg is not None:
368
+ self.error(err_msg)
369
+ raise RuntimeError(err_msg)
370
+
371
+ self.pw = sync_playwright().start()
372
+ launcher = getattr(self.pw, engine)
373
+ self.browser = launcher.launch(
374
+ headless=headless,
375
+ args=args
376
+ )
377
+ self.context = self.browser.new_context(viewport={"width": self.viewport_w, "height": self.viewport_h})
378
+ self.page = self.context.new_page()
379
+ self.page.goto(self.get_option_value("sandbox_home"))
380
+
381
+ # ========================= Playwright ops in main thread (from worker) ========================= #
382
+
383
+ @Slot(str, dict, object, object)
384
+ def on_playwright_call(self, op: str, params: dict, ret: dict, done):
385
+ """
386
+ Execute Playwright operation in the main (GUI) thread.
387
+ 'ret' is a mutable dict to fill with results. 'done' is a threading.Event to signal completion.
388
+ """
389
+ try:
390
+ # Always ensure browser exists before any operation
391
+ if op in ("ensure", "navigate", "go_back", "go_forward", "move", "click",
392
+ "scroll", "drag", "keypress", "keypress_combo", "type",
393
+ "screenshot", "click_at", "hover_at", "type_text_at"):
394
+ self._ensure_browser()
395
+
396
+ if op == "ensure":
397
+ vw, vh = self._get_viewport()
398
+ ret.update({"ok": True, "viewport_w": vw, "viewport_h": vh, "url": self._get_url(),
399
+ "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
400
+
401
+ elif op == "navigate":
402
+ url = str(params.get("url", "") or "")
403
+ if url:
404
+ self.page.goto(url, wait_until="load")
405
+ self._wait_for_load()
406
+ vw, vh = self._get_viewport()
407
+ ret.update({"ok": True, "url": self._get_url(), "viewport_w": vw, "viewport_h": vh,
408
+ "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
409
+
410
+ elif op == "go_back":
411
+ self.page.go_back(wait_until="load")
412
+ vw, vh = self._get_viewport()
413
+ self._wait_for_load()
414
+ ret.update({"ok": True, "url": self._get_url(), "viewport_w": vw, "viewport_h": vh,
415
+ "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
416
+
417
+ elif op == "go_forward":
418
+ self.page.go_forward(wait_until="load")
419
+ self._wait_for_load()
420
+ vw, vh = self._get_viewport()
421
+ ret.update({"ok": True, "url": self._get_url(), "viewport_w": vw, "viewport_h": vh,
422
+ "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
423
+
424
+ elif op == "move":
425
+ x = int(params.get("x"))
426
+ y = int(params.get("y"))
427
+ self.page.mouse.move(x, y)
428
+ self.pointer_x, self.pointer_y = x, y
429
+ self._wait_for_load()
430
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": x, "mouse_y": y})
431
+
432
+ elif op == "click":
433
+ button = str(params.get("button", "left")).lower()
434
+ count = int(params.get("count", 1))
435
+ x = params.get("x", None)
436
+ y = params.get("y", None)
437
+ if x is None or y is None:
438
+ x, y = self.pointer_x, self.pointer_y
439
+ else:
440
+ x, y = int(x), int(y)
441
+ self.page.mouse.move(x, y)
442
+ self.pointer_x, self.pointer_y = x, y
443
+ self.page.mouse.click(x, y, button=button, click_count=max(1, count))
444
+ self._wait_for_load()
445
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": x, "mouse_y": y})
446
+
447
+ elif op == "click_at":
448
+ x = int(params.get("x"))
449
+ y = int(params.get("y"))
450
+ button = str(params.get("button", "left")).lower()
451
+ count = int(params.get("count", 1))
452
+ self.page.mouse.move(x, y)
453
+ self.pointer_x, self.pointer_y = x, y
454
+ self.page.mouse.click(x, y, button=button, click_count=max(1, count))
455
+ self._wait_for_load()
456
+ print("cliecked at", x, y)
457
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": x, "mouse_y": y})
458
+
459
+ elif op == "hover_at":
460
+ x = int(params.get("x"))
461
+ y = int(params.get("y"))
462
+ self.page.mouse.move(x, y)
463
+ self.pointer_x, self.pointer_y = x, y
464
+ self._wait_for_load()
465
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": x, "mouse_y": y})
466
+
467
+ elif op == "type_text_at":
468
+ x = int(params.get("x"))
469
+ y = int(params.get("y"))
470
+ text = str(params.get("text", "") or "")
471
+ press_enter = bool(params.get("press_enter", True))
472
+ clear_before = bool(params.get("clear_before_typing", True))
473
+ # Focus target
474
+ self.page.mouse.move(x, y)
475
+ self.pointer_x, self.pointer_y = x, y
476
+ self.page.mouse.click(x, y, button="left", click_count=1)
477
+ # Optional clear
478
+ if clear_before:
479
+ # Use Control+A universally; adjust if needed for macOS
480
+ self._press_combo(["Control", "a"])
481
+ self.page.keyboard.press("Backspace")
482
+ # Type
483
+ if text:
484
+ self.page.keyboard.type(text)
485
+ if press_enter:
486
+ self.page.keyboard.press("Enter")
487
+ self._wait_for_load()
488
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
489
+
490
+ elif op == "scroll":
491
+ x = params.get("x", None)
492
+ y = params.get("y", None)
493
+ if x is not None and y is not None:
494
+ xx, yy = int(x), int(y)
495
+ self.page.mouse.move(xx, yy)
496
+ self.pointer_x, self.pointer_y = xx, yy
497
+ dx = int(params.get("dx", 0))
498
+ dy = int(params.get("dy", 0))
499
+ self.page.mouse.wheel(dx, dy)
500
+ self._wait_for_load()
501
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
502
+
503
+ elif op == "drag":
504
+ x = int(params.get("x"))
505
+ y = int(params.get("y"))
506
+ dx = int(params.get("dx"))
507
+ dy = int(params.get("dy"))
508
+ self.page.mouse.move(x, y)
509
+ self.pointer_x, self.pointer_y = x, y
510
+ self.page.mouse.down(button="left")
511
+ self.page.mouse.move(dx, dy, steps=12)
512
+ self.page.mouse.up(button="left")
513
+ self.pointer_x, self.pointer_y = dx, dy
514
+ self._wait_for_load()
515
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
516
+
517
+ elif op == "keypress":
518
+ key = str(params.get("key"))
519
+ self.page.keyboard.press(self._key_to_playwright(key))
520
+ self._wait_for_load()
521
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
522
+
523
+ elif op == "keypress_combo":
524
+ keys = params.get("keys", []) or []
525
+ if isinstance(keys, str):
526
+ keys = [p.strip() for p in keys.replace("+", " ").split() if p.strip()]
527
+ self._press_combo(keys)
528
+ self._wait_for_load()
529
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
530
+
531
+ elif op == "type":
532
+ text = str(params.get("text", "") or "")
533
+ modifier = params.get("modifier", None)
534
+ if modifier:
535
+ mod = self._key_to_playwright(modifier)
536
+ self.page.keyboard.down(mod)
537
+ try:
538
+ self.page.keyboard.type(text)
539
+ finally:
540
+ self.page.keyboard.up(mod)
541
+ else:
542
+ self.page.keyboard.type(text)
543
+ self._wait_for_load()
544
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
545
+
546
+ elif op == "screenshot":
547
+ full = bool(params.get("full_page", False))
548
+ img_bytes = self.page.screenshot(full_page=full)
549
+ vw, vh = self._get_viewport()
550
+ ret.update({"ok": True, "image": img_bytes, "url": self._get_url(),
551
+ "viewport_w": vw, "viewport_h": vh,
552
+ "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
553
+
554
+ else:
555
+ ret.update({"ok": False, "error": f"Unknown op: {op}", "url": self._get_url()})
556
+
557
+ except Exception as e:
558
+ ret.update({"ok": False, "error": str(e), "url": self._get_url()})
559
+ finally:
560
+ try:
561
+ done.set()
562
+ except Exception:
563
+ pass
564
+
565
+ def _wait_for_load(self, timeout: int = 5000):
566
+ """Wait for page load state."""
567
+ if self.page:
568
+ self.page.wait_for_load_state(timeout=timeout)
569
+ time.sleep(1)
570
+
571
+ # ========================= Helpers for Playwright ops ========================= #
572
+
573
+ def get_last_url(self):
574
+ """
575
+ Get current page URL
576
+
577
+ :return: URL string
578
+ """
579
+ return self._get_url()
580
+
581
+ def _get_url(self) -> str:
582
+ try:
583
+ if self.page:
584
+ return self.page.url or ""
585
+ except Exception:
586
+ return ""
587
+ return ""
588
+
589
+ def _get_viewport(self):
590
+ try:
591
+ if self.page:
592
+ vp = self.page.viewport_size
593
+ if callable(vp):
594
+ vp = self.page.viewport_size()
595
+ if isinstance(vp, dict) and "width" in vp and "height" in vp:
596
+ self.viewport_w = int(vp["width"])
597
+ self.viewport_h = int(vp["height"])
598
+ except Exception:
599
+ pass
600
+ return self.viewport_w, self.viewport_h
601
+
602
+ def _key_to_playwright(self, key: str) -> str:
603
+ mapping = {
604
+ "PAGEDOWN": "PageDown",
605
+ "PAGEUP": "PageUp",
606
+ "BACKSPACE": "Backspace",
607
+ "RETURN": "Enter",
608
+ "ENTER": "Enter",
609
+ "ESCAPE": "Escape",
610
+ "ESC": "Escape",
611
+ "LEFT": "ArrowLeft",
612
+ "RIGHT": "ArrowRight",
613
+ "UP": "ArrowUp",
614
+ "DOWN": "ArrowDown",
615
+ "SPACE": " ",
616
+ "TAB": "Tab",
617
+ "CTRL": "Control",
618
+ "CONTROL": "Control",
619
+ "ALT": "Alt",
620
+ "SHIFT": "Shift",
621
+ "CMD": "Meta",
622
+ "SUPER": "Meta",
623
+ "START": "Meta",
624
+ "PRINTSCREEN": "PrintScreen",
625
+ "PRINT_SCREEN": "PrintScreen",
626
+ "PRTSC": "PrintScreen",
627
+ "END": "End",
628
+ "HOME": "Home",
629
+ "DELETE": "Delete",
630
+ "INSERT": "Insert",
631
+ }
632
+ u = key.upper() if isinstance(key, str) else key
633
+ if u in mapping:
634
+ return mapping[u]
635
+ if isinstance(u, str) and len(u) == 2 and u[0] == "F" and u[1].isdigit():
636
+ return u
637
+ if isinstance(u, str) and len(u) == 3 and u[0] == "F" and u[1:].isdigit():
638
+ return u
639
+ return key
640
+
641
+ def _press_combo(self, keys, delay: float = 0.0):
642
+ if not keys:
643
+ return
644
+ # Try chord, e.g., "Control+L"
645
+ if len(keys) >= 2:
646
+ chord = "+".join(self._key_to_playwright(k) for k in keys)
647
+ try:
648
+ self.page.keyboard.press(chord, delay=delay)
649
+ return
650
+ except Exception:
651
+ pass
652
+ # Fallback: modifier down/up sequence
653
+ modifiers = {"Control", "Alt", "Shift", "Meta"}
654
+ downs = []
655
+ try:
656
+ for k in keys:
657
+ pk = self._key_to_playwright(k)
658
+ if pk in modifiers and pk not in downs:
659
+ self.page.keyboard.down(pk)
660
+ downs.append(pk)
661
+ else:
662
+ self.page.keyboard.press(pk, delay=delay)
663
+ finally:
664
+ for m in reversed(downs):
665
+ self.page.keyboard.up(m)
201
666
 
202
667
  def on_system_prompt(self, prompt: str) -> str:
203
668
  """
@@ -208,4 +673,4 @@ class Plugin(BasePlugin):
208
673
  """
209
674
  if prompt is not None and prompt.strip() != "":
210
675
  prompt += "\n\n"
211
- return prompt + self.get_option_value("prompt")
676
+ return prompt + self.get_option_value("prompt")