pygpt-net 2.7.4__py3-none-any.whl → 2.7.5__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 (133) hide show
  1. pygpt_net/CHANGELOG.txt +7 -0
  2. pygpt_net/__init__.py +3 -3
  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 +8 -1
  11. pygpt_net/controller/chat/image.py +2 -2
  12. pygpt_net/controller/dialogs/confirm.py +73 -101
  13. pygpt_net/controller/lang/mapping.py +9 -9
  14. pygpt_net/controller/painter/capture.py +50 -1
  15. pygpt_net/controller/presets/presets.py +2 -1
  16. pygpt_net/controller/remote_store/__init__.py +12 -0
  17. pygpt_net/{provider/core/assistant_file/db_sqlite → controller/remote_store/google}/__init__.py +2 -2
  18. pygpt_net/controller/remote_store/google/batch.py +402 -0
  19. pygpt_net/controller/remote_store/google/store.py +615 -0
  20. pygpt_net/controller/remote_store/openai/__init__.py +12 -0
  21. pygpt_net/controller/remote_store/openai/batch.py +524 -0
  22. pygpt_net/controller/{assistant → remote_store/openai}/store.py +63 -60
  23. pygpt_net/controller/remote_store/remote_store.py +35 -0
  24. pygpt_net/controller/ui/ui.py +20 -1
  25. pygpt_net/core/assistants/assistants.py +3 -15
  26. pygpt_net/core/db/database.py +5 -3
  27. pygpt_net/core/locale/placeholder.py +35 -0
  28. pygpt_net/core/remote_store/__init__.py +12 -0
  29. pygpt_net/core/remote_store/google/__init__.py +11 -0
  30. pygpt_net/core/remote_store/google/files.py +224 -0
  31. pygpt_net/core/remote_store/google/store.py +248 -0
  32. pygpt_net/core/remote_store/openai/__init__.py +11 -0
  33. pygpt_net/core/{assistants → remote_store/openai}/files.py +26 -19
  34. pygpt_net/core/{assistants → remote_store/openai}/store.py +32 -15
  35. pygpt_net/core/remote_store/remote_store.py +24 -0
  36. pygpt_net/data/config/config.json +8 -4
  37. pygpt_net/data/config/models.json +77 -3
  38. pygpt_net/data/config/settings.json +45 -0
  39. pygpt_net/data/locale/locale.de.ini +41 -41
  40. pygpt_net/data/locale/locale.en.ini +53 -43
  41. pygpt_net/data/locale/locale.es.ini +41 -41
  42. pygpt_net/data/locale/locale.fr.ini +41 -41
  43. pygpt_net/data/locale/locale.it.ini +41 -41
  44. pygpt_net/data/locale/locale.pl.ini +42 -42
  45. pygpt_net/data/locale/locale.uk.ini +41 -41
  46. pygpt_net/data/locale/locale.zh.ini +41 -41
  47. pygpt_net/data/locale/plugin.cmd_history.de.ini +1 -1
  48. pygpt_net/data/locale/plugin.cmd_history.en.ini +1 -1
  49. pygpt_net/data/locale/plugin.cmd_history.es.ini +1 -1
  50. pygpt_net/data/locale/plugin.cmd_history.fr.ini +1 -1
  51. pygpt_net/data/locale/plugin.cmd_history.it.ini +1 -1
  52. pygpt_net/data/locale/plugin.cmd_history.pl.ini +1 -1
  53. pygpt_net/data/locale/plugin.cmd_history.uk.ini +1 -1
  54. pygpt_net/data/locale/plugin.cmd_history.zh.ini +1 -1
  55. pygpt_net/data/locale/plugin.cmd_mouse_control.en.ini +14 -0
  56. pygpt_net/data/locale/plugin.cmd_web.de.ini +1 -1
  57. pygpt_net/data/locale/plugin.cmd_web.en.ini +1 -1
  58. pygpt_net/data/locale/plugin.cmd_web.es.ini +1 -1
  59. pygpt_net/data/locale/plugin.cmd_web.fr.ini +1 -1
  60. pygpt_net/data/locale/plugin.cmd_web.it.ini +1 -1
  61. pygpt_net/data/locale/plugin.cmd_web.pl.ini +1 -1
  62. pygpt_net/data/locale/plugin.cmd_web.uk.ini +1 -1
  63. pygpt_net/data/locale/plugin.cmd_web.zh.ini +1 -1
  64. pygpt_net/data/locale/plugin.idx_llama_index.de.ini +2 -2
  65. pygpt_net/data/locale/plugin.idx_llama_index.en.ini +2 -2
  66. pygpt_net/data/locale/plugin.idx_llama_index.es.ini +2 -2
  67. pygpt_net/data/locale/plugin.idx_llama_index.fr.ini +2 -2
  68. pygpt_net/data/locale/plugin.idx_llama_index.it.ini +2 -2
  69. pygpt_net/data/locale/plugin.idx_llama_index.pl.ini +2 -2
  70. pygpt_net/data/locale/plugin.idx_llama_index.uk.ini +2 -2
  71. pygpt_net/data/locale/plugin.idx_llama_index.zh.ini +2 -2
  72. pygpt_net/item/assistant.py +1 -211
  73. pygpt_net/item/ctx.py +3 -1
  74. pygpt_net/item/store.py +238 -0
  75. pygpt_net/migrations/Version20260102190000.py +35 -0
  76. pygpt_net/migrations/__init__.py +3 -1
  77. pygpt_net/plugin/cmd_mouse_control/config.py +470 -1
  78. pygpt_net/plugin/cmd_mouse_control/plugin.py +488 -22
  79. pygpt_net/plugin/cmd_mouse_control/worker.py +464 -87
  80. pygpt_net/plugin/cmd_mouse_control/worker_sandbox.py +729 -0
  81. pygpt_net/plugin/idx_llama_index/config.py +2 -2
  82. pygpt_net/provider/api/google/__init__.py +16 -54
  83. pygpt_net/provider/api/google/chat.py +546 -129
  84. pygpt_net/provider/api/google/computer.py +190 -0
  85. pygpt_net/provider/api/google/realtime/realtime.py +2 -2
  86. pygpt_net/provider/api/google/remote_tools.py +93 -0
  87. pygpt_net/provider/api/google/store.py +546 -0
  88. pygpt_net/provider/api/google/worker/__init__.py +0 -0
  89. pygpt_net/provider/api/google/worker/importer.py +392 -0
  90. pygpt_net/provider/api/openai/computer.py +10 -1
  91. pygpt_net/provider/api/openai/store.py +6 -6
  92. pygpt_net/provider/api/openai/worker/importer.py +24 -24
  93. pygpt_net/provider/core/config/patch.py +16 -1
  94. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +3 -3
  95. pygpt_net/provider/core/model/patch.py +17 -3
  96. pygpt_net/provider/core/preset/json_file.py +13 -7
  97. pygpt_net/provider/core/{assistant_file → remote_file}/__init__.py +1 -1
  98. pygpt_net/provider/core/{assistant_file → remote_file}/base.py +9 -9
  99. pygpt_net/provider/core/remote_file/db_sqlite/__init__.py +12 -0
  100. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/patch.py +1 -1
  101. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/provider.py +23 -20
  102. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/storage.py +35 -27
  103. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/utils.py +5 -4
  104. pygpt_net/provider/core/{assistant_store → remote_store}/__init__.py +1 -1
  105. pygpt_net/provider/core/{assistant_store → remote_store}/base.py +10 -10
  106. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/__init__.py +1 -1
  107. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/patch.py +1 -1
  108. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/provider.py +16 -15
  109. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/storage.py +30 -23
  110. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/utils.py +5 -4
  111. pygpt_net/provider/core/{assistant_store → remote_store}/json_file.py +9 -9
  112. pygpt_net/provider/llms/google.py +2 -2
  113. pygpt_net/ui/base/config_dialog.py +3 -2
  114. pygpt_net/ui/dialog/assistant.py +3 -3
  115. pygpt_net/ui/dialog/plugins.py +3 -1
  116. pygpt_net/ui/dialog/remote_store_google.py +539 -0
  117. pygpt_net/ui/dialog/{assistant_store.py → remote_store_openai.py} +95 -95
  118. pygpt_net/ui/dialogs.py +5 -3
  119. pygpt_net/ui/layout/chat/attachments_uploaded.py +3 -3
  120. pygpt_net/ui/layout/toolbox/computer_env.py +26 -8
  121. pygpt_net/ui/menu/tools.py +13 -5
  122. pygpt_net/ui/widget/dialog/remote_store_google.py +56 -0
  123. pygpt_net/ui/widget/dialog/{assistant_store.py → remote_store_openai.py} +9 -9
  124. pygpt_net/ui/widget/element/button.py +4 -4
  125. pygpt_net/ui/widget/lists/remote_store_google.py +248 -0
  126. pygpt_net/ui/widget/lists/{assistant_store.py → remote_store_openai.py} +21 -21
  127. pygpt_net/ui/widget/option/checkbox_list.py +47 -9
  128. pygpt_net/ui/widget/option/combo.py +39 -3
  129. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.5.dist-info}/METADATA +33 -2
  130. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.5.dist-info}/RECORD +133 -108
  131. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.5.dist-info}/LICENSE +0 -0
  132. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.5.dist-info}/WHEEL +0 -0
  133. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.5.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,8 @@ 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
+ print("APPEND RESPONSE", response)
227
+ self.prepare_reply_ctx(response, ctx)
159
228
  ctx.reply = True
160
229
  self.handle_delayed(ctx, with_screenshot)
161
230
 
@@ -173,9 +242,11 @@ class Plugin(BasePlugin):
173
242
 
174
243
  context = BridgeContext()
175
244
  context.ctx = ctx
245
+ extra = {}
246
+ extra["response_type"] = "multiple"
176
247
  event = KernelEvent(KernelEvent.REPLY_ADD, {
177
248
  'context': context,
178
- 'extra': {},
249
+ 'extra': extra,
179
250
  })
180
251
  self.window.dispatch(event)
181
252
 
@@ -186,18 +257,413 @@ class Plugin(BasePlugin):
186
257
  :param ctx: context (CtxItem)
187
258
  """
188
259
  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)
260
+ if self.is_sandbox():
261
+ path = self.window.controller.painter.capture.screenshot_playwright(page=self.page, silent=True) # Playwright screenshot
262
+ else:
263
+ path = self.window.controller.painter.capture.screenshot(attach_cursor=True, silent=True) # attach screenshot
264
+ if path:
265
+ img_path = self.window.core.filesystem.make_local(path)
266
+ ctx.images_before.append(img_path)
267
+ context = BridgeContext()
268
+ context.ctx = ctx
269
+ event = KernelEvent(KernelEvent.REPLY_ADD, {
270
+ 'context': context,
271
+ 'extra': {},
272
+ })
273
+ self.window.dispatch(event)
274
+
275
+ @Slot()
276
+ def on_playwright_start(self):
277
+ """
278
+ Event: PLAYWRIGHT_START
279
+ """
280
+ self._ensure_browser()
281
+
282
+ def _ensure_browser(self):
283
+ """Start Playwright browser and page if not started yet."""
284
+ if self.page:
285
+ # check if browser is still connected
286
+ try:
287
+ self.page.screenshot()
288
+ return
289
+ except Exception as e:
290
+ print(e)
291
+ if self.page:
292
+ try:
293
+ self.page.close()
294
+ except Exception:
295
+ pass
296
+ self.page = None
297
+ if self.browser:
298
+ try:
299
+ self.browser.close()
300
+ except Exception:
301
+ pass
302
+ self.browser = None
303
+ if self.pw:
304
+ try:
305
+ self.pw.stop()
306
+ except Exception:
307
+ pass
308
+ self.pw = None
309
+ if self.context:
310
+ try:
311
+ self.context.close()
312
+ except Exception:
313
+ pass
314
+ self.context = None
315
+
316
+
317
+ # Playwright is required in sandbox mode
318
+ try:
319
+ from playwright.sync_api import sync_playwright
320
+ except Exception:
321
+ sync_playwright = None
322
+
323
+ if sync_playwright is None:
324
+ raise RuntimeError("Playwright is not installed. Please install 'playwright' and the chosen browser.")
200
325
 
326
+ # Determine headless and engine from plugin options if available
327
+ headless = True
328
+ engine = "chromium"
329
+ try:
330
+ if hasattr(self, "get_option_value"):
331
+ hv = self.get_option_value("sandbox_headless")
332
+ if hv is not None:
333
+ headless = bool(hv)
334
+ ev = self.get_option_value("sandbox_browser")
335
+ if ev in ("chromium", "firefox", "webkit"):
336
+ engine = ev
337
+ vw = int(self.get_option_value("sandbox_viewport_w") or self.viewport_w)
338
+ vh = int(self.get_option_value("sandbox_viewport_h") or self.viewport_h)
339
+ self.viewport_w, self.viewport_h = vw, vh
340
+ except Exception:
341
+ pass
342
+
343
+ args = []
344
+ cfg_engine = self.get_option_value("sandbox_engine")
345
+ if cfg_engine:
346
+ engine = cfg_engine
347
+ cfg_args = self.get_option_value("sandbox_args")
348
+ if cfg_args:
349
+ args = [arg.strip() for arg in cfg_args.split(",") if arg.strip()]
350
+ cfg_path = self.get_option_value("sandbox_path")
351
+ if cfg_path:
352
+ os.environ['PLAYWRIGHT_BROWSERS_PATH'] = cfg_path
353
+ err_msg = None
354
+ if cfg_path and not (os.path.exists(cfg_path) and os.path.isdir(cfg_path)):
355
+ err_msg = (f"Playwright browsers path does not exist: {cfg_path}\n\n"
356
+ f"1) Please install Playwright browser(s) on host machine: \n\n"
357
+ f"pip install playwright && playwright install {engine}\n\n"
358
+ f"2) Set path to browsers directory in `Mouse And Keyboard` plugin settings option: "
359
+ f"`Sandbox: Playwright browsers path`.")
360
+ else:
361
+ if os.environ.get("APPIMAGE") and not cfg_path: # set path is required for AppImage version
362
+ err_msg = (f"Playwright browsers path is not set - "
363
+ f"1) Please install Playwright browser(s) on host machine: \n\n"
364
+ f"pip install playwright && playwright install {engine}\n\n"
365
+ f"2) Set path to browsers directory in `Mouse And Keyboard` plugin settings option: "
366
+ f"`Sandbox: Playwright browsers path`.")
367
+
368
+ if err_msg is not None:
369
+ self.error(err_msg)
370
+ raise RuntimeError(err_msg)
371
+
372
+ self.pw = sync_playwright().start()
373
+ launcher = getattr(self.pw, engine)
374
+ self.browser = launcher.launch(
375
+ headless=headless,
376
+ args=args
377
+ )
378
+ self.context = self.browser.new_context(viewport={"width": self.viewport_w, "height": self.viewport_h})
379
+ self.page = self.context.new_page()
380
+ self.page.goto(self.get_option_value("sandbox_home"))
381
+
382
+ # ========================= Playwright ops in main thread (from worker) ========================= #
383
+
384
+ @Slot(str, dict, object, object)
385
+ def on_playwright_call(self, op: str, params: dict, ret: dict, done):
386
+ """
387
+ Execute Playwright operation in the main (GUI) thread.
388
+ 'ret' is a mutable dict to fill with results. 'done' is a threading.Event to signal completion.
389
+ """
390
+ try:
391
+ # Always ensure browser exists before any operation
392
+ if op in ("ensure", "navigate", "go_back", "go_forward", "move", "click",
393
+ "scroll", "drag", "keypress", "keypress_combo", "type",
394
+ "screenshot", "click_at", "hover_at", "type_text_at"):
395
+ self._ensure_browser()
396
+
397
+ if op == "ensure":
398
+ vw, vh = self._get_viewport()
399
+ ret.update({"ok": True, "viewport_w": vw, "viewport_h": vh, "url": self._get_url(),
400
+ "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
401
+
402
+ elif op == "navigate":
403
+ url = str(params.get("url", "") or "")
404
+ if url:
405
+ self.page.goto(url, wait_until="load")
406
+ self._wait_for_load()
407
+ vw, vh = self._get_viewport()
408
+ ret.update({"ok": True, "url": self._get_url(), "viewport_w": vw, "viewport_h": vh,
409
+ "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
410
+
411
+ elif op == "go_back":
412
+ self.page.go_back(wait_until="load")
413
+ vw, vh = self._get_viewport()
414
+ self._wait_for_load()
415
+ ret.update({"ok": True, "url": self._get_url(), "viewport_w": vw, "viewport_h": vh,
416
+ "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
417
+
418
+ elif op == "go_forward":
419
+ self.page.go_forward(wait_until="load")
420
+ self._wait_for_load()
421
+ vw, vh = self._get_viewport()
422
+ ret.update({"ok": True, "url": self._get_url(), "viewport_w": vw, "viewport_h": vh,
423
+ "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
424
+
425
+ elif op == "move":
426
+ x = int(params.get("x"))
427
+ y = int(params.get("y"))
428
+ self.page.mouse.move(x, y)
429
+ self.pointer_x, self.pointer_y = x, y
430
+ self._wait_for_load()
431
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": x, "mouse_y": y})
432
+
433
+ elif op == "click":
434
+ button = str(params.get("button", "left")).lower()
435
+ count = int(params.get("count", 1))
436
+ x = params.get("x", None)
437
+ y = params.get("y", None)
438
+ if x is None or y is None:
439
+ x, y = self.pointer_x, self.pointer_y
440
+ else:
441
+ x, y = int(x), int(y)
442
+ self.page.mouse.move(x, y)
443
+ self.pointer_x, self.pointer_y = x, y
444
+ self.page.mouse.click(x, y, button=button, click_count=max(1, count))
445
+ self._wait_for_load()
446
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": x, "mouse_y": y})
447
+
448
+ elif op == "click_at":
449
+ x = int(params.get("x"))
450
+ y = int(params.get("y"))
451
+ button = str(params.get("button", "left")).lower()
452
+ count = int(params.get("count", 1))
453
+ self.page.mouse.move(x, y)
454
+ self.pointer_x, self.pointer_y = x, y
455
+ self.page.mouse.click(x, y, button=button, click_count=max(1, count))
456
+ self._wait_for_load()
457
+ print("cliecked at", x, y)
458
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": x, "mouse_y": y})
459
+
460
+ elif op == "hover_at":
461
+ x = int(params.get("x"))
462
+ y = int(params.get("y"))
463
+ self.page.mouse.move(x, y)
464
+ self.pointer_x, self.pointer_y = x, y
465
+ self._wait_for_load()
466
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": x, "mouse_y": y})
467
+
468
+ elif op == "type_text_at":
469
+ x = int(params.get("x"))
470
+ y = int(params.get("y"))
471
+ text = str(params.get("text", "") or "")
472
+ press_enter = bool(params.get("press_enter", True))
473
+ clear_before = bool(params.get("clear_before_typing", True))
474
+ # Focus target
475
+ self.page.mouse.move(x, y)
476
+ self.pointer_x, self.pointer_y = x, y
477
+ self.page.mouse.click(x, y, button="left", click_count=1)
478
+ # Optional clear
479
+ if clear_before:
480
+ # Use Control+A universally; adjust if needed for macOS
481
+ self._press_combo(["Control", "a"])
482
+ self.page.keyboard.press("Backspace")
483
+ # Type
484
+ if text:
485
+ self.page.keyboard.type(text)
486
+ if press_enter:
487
+ self.page.keyboard.press("Enter")
488
+ self._wait_for_load()
489
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
490
+
491
+ elif op == "scroll":
492
+ x = params.get("x", None)
493
+ y = params.get("y", None)
494
+ if x is not None and y is not None:
495
+ xx, yy = int(x), int(y)
496
+ self.page.mouse.move(xx, yy)
497
+ self.pointer_x, self.pointer_y = xx, yy
498
+ dx = int(params.get("dx", 0))
499
+ dy = int(params.get("dy", 0))
500
+ self.page.mouse.wheel(dx, dy)
501
+ self._wait_for_load()
502
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
503
+
504
+ elif op == "drag":
505
+ x = int(params.get("x"))
506
+ y = int(params.get("y"))
507
+ dx = int(params.get("dx"))
508
+ dy = int(params.get("dy"))
509
+ self.page.mouse.move(x, y)
510
+ self.pointer_x, self.pointer_y = x, y
511
+ self.page.mouse.down(button="left")
512
+ self.page.mouse.move(dx, dy, steps=12)
513
+ self.page.mouse.up(button="left")
514
+ self.pointer_x, self.pointer_y = dx, dy
515
+ self._wait_for_load()
516
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
517
+
518
+ elif op == "keypress":
519
+ key = str(params.get("key"))
520
+ self.page.keyboard.press(self._key_to_playwright(key))
521
+ self._wait_for_load()
522
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
523
+
524
+ elif op == "keypress_combo":
525
+ keys = params.get("keys", []) or []
526
+ if isinstance(keys, str):
527
+ keys = [p.strip() for p in keys.replace("+", " ").split() if p.strip()]
528
+ self._press_combo(keys)
529
+ self._wait_for_load()
530
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
531
+
532
+ elif op == "type":
533
+ text = str(params.get("text", "") or "")
534
+ modifier = params.get("modifier", None)
535
+ if modifier:
536
+ mod = self._key_to_playwright(modifier)
537
+ self.page.keyboard.down(mod)
538
+ try:
539
+ self.page.keyboard.type(text)
540
+ finally:
541
+ self.page.keyboard.up(mod)
542
+ else:
543
+ self.page.keyboard.type(text)
544
+ self._wait_for_load()
545
+ ret.update({"ok": True, "url": self._get_url(), "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
546
+
547
+ elif op == "screenshot":
548
+ full = bool(params.get("full_page", False))
549
+ img_bytes = self.page.screenshot(full_page=full)
550
+ vw, vh = self._get_viewport()
551
+ ret.update({"ok": True, "image": img_bytes, "url": self._get_url(),
552
+ "viewport_w": vw, "viewport_h": vh,
553
+ "mouse_x": self.pointer_x, "mouse_y": self.pointer_y})
554
+
555
+ else:
556
+ ret.update({"ok": False, "error": f"Unknown op: {op}", "url": self._get_url()})
557
+
558
+ except Exception as e:
559
+ ret.update({"ok": False, "error": str(e), "url": self._get_url()})
560
+ finally:
561
+ try:
562
+ done.set()
563
+ except Exception:
564
+ pass
565
+
566
+ def _wait_for_load(self, timeout: int = 5000):
567
+ """Wait for page load state."""
568
+ if self.page:
569
+ self.page.wait_for_load_state(timeout=timeout)
570
+ time.sleep(1)
571
+
572
+ # ========================= Helpers for Playwright ops ========================= #
573
+
574
+ def get_last_url(self):
575
+ """
576
+ Get current page URL
577
+
578
+ :return: URL string
579
+ """
580
+ return self._get_url()
581
+
582
+ def _get_url(self) -> str:
583
+ try:
584
+ if self.page:
585
+ return self.page.url or ""
586
+ except Exception:
587
+ return ""
588
+ return ""
589
+
590
+ def _get_viewport(self):
591
+ try:
592
+ if self.page:
593
+ vp = self.page.viewport_size
594
+ if callable(vp):
595
+ vp = self.page.viewport_size()
596
+ if isinstance(vp, dict) and "width" in vp and "height" in vp:
597
+ self.viewport_w = int(vp["width"])
598
+ self.viewport_h = int(vp["height"])
599
+ except Exception:
600
+ pass
601
+ return self.viewport_w, self.viewport_h
602
+
603
+ def _key_to_playwright(self, key: str) -> str:
604
+ mapping = {
605
+ "PAGEDOWN": "PageDown",
606
+ "PAGEUP": "PageUp",
607
+ "BACKSPACE": "Backspace",
608
+ "RETURN": "Enter",
609
+ "ENTER": "Enter",
610
+ "ESCAPE": "Escape",
611
+ "ESC": "Escape",
612
+ "LEFT": "ArrowLeft",
613
+ "RIGHT": "ArrowRight",
614
+ "UP": "ArrowUp",
615
+ "DOWN": "ArrowDown",
616
+ "SPACE": " ",
617
+ "TAB": "Tab",
618
+ "CTRL": "Control",
619
+ "CONTROL": "Control",
620
+ "ALT": "Alt",
621
+ "SHIFT": "Shift",
622
+ "CMD": "Meta",
623
+ "SUPER": "Meta",
624
+ "START": "Meta",
625
+ "PRINTSCREEN": "PrintScreen",
626
+ "PRINT_SCREEN": "PrintScreen",
627
+ "PRTSC": "PrintScreen",
628
+ "END": "End",
629
+ "HOME": "Home",
630
+ "DELETE": "Delete",
631
+ "INSERT": "Insert",
632
+ }
633
+ u = key.upper() if isinstance(key, str) else key
634
+ if u in mapping:
635
+ return mapping[u]
636
+ if isinstance(u, str) and len(u) == 2 and u[0] == "F" and u[1].isdigit():
637
+ return u
638
+ if isinstance(u, str) and len(u) == 3 and u[0] == "F" and u[1:].isdigit():
639
+ return u
640
+ return key
641
+
642
+ def _press_combo(self, keys, delay: float = 0.0):
643
+ if not keys:
644
+ return
645
+ # Try chord, e.g., "Control+L"
646
+ if len(keys) >= 2:
647
+ chord = "+".join(self._key_to_playwright(k) for k in keys)
648
+ try:
649
+ self.page.keyboard.press(chord, delay=delay)
650
+ return
651
+ except Exception:
652
+ pass
653
+ # Fallback: modifier down/up sequence
654
+ modifiers = {"Control", "Alt", "Shift", "Meta"}
655
+ downs = []
656
+ try:
657
+ for k in keys:
658
+ pk = self._key_to_playwright(k)
659
+ if pk in modifiers and pk not in downs:
660
+ self.page.keyboard.down(pk)
661
+ downs.append(pk)
662
+ else:
663
+ self.page.keyboard.press(pk, delay=delay)
664
+ finally:
665
+ for m in reversed(downs):
666
+ self.page.keyboard.up(m)
201
667
 
202
668
  def on_system_prompt(self, prompt: str) -> str:
203
669
  """
@@ -208,4 +674,4 @@ class Plugin(BasePlugin):
208
674
  """
209
675
  if prompt is not None and prompt.strip() != "":
210
676
  prompt += "\n\n"
211
- return prompt + self.get_option_value("prompt")
677
+ return prompt + self.get_option_value("prompt")