portacode 2.2.20.dev2__py3-none-any.whl → 2.2.20.dev4__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.
portacode/_version.py CHANGED
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.2.20.dev2'
22
- __version_tuple__ = version_tuple = (2, 2, 20, 'dev2')
21
+ __version__ = version = '2.2.20.dev4'
22
+ __version_tuple__ = version_tuple = (2, 2, 20, 'dev4')
23
23
 
24
24
  __commit_id__ = commit_id = None
portacode/browser_cli.py CHANGED
@@ -2,17 +2,33 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import json
5
+ import os
5
6
  import re
6
7
  import time
7
8
  from pathlib import Path
8
9
 
9
10
  import click
10
11
 
12
+ from .connection.handlers.browser_handlers import load_browser_config, update_browser_config
13
+
14
+
15
+ _MANAGED_BROWSERS_PATH = Path("/opt/ms-playwright")
16
+
17
+
18
+ def _configure_managed_browser_path() -> None:
19
+ if _MANAGED_BROWSERS_PATH.is_dir():
20
+ os.environ.setdefault("PLAYWRIGHT_BROWSERS_PATH", str(_MANAGED_BROWSERS_PATH))
21
+
11
22
 
12
23
  def run_browser_script(*, url: str, script: str, output_dir: Path, record_video: bool,
13
- width: int, height: int, timeout_ms: int) -> dict:
24
+ width: int | None, height: int | None, timeout_ms: int) -> dict:
25
+ _configure_managed_browser_path()
14
26
  from playwright.sync_api import sync_playwright
15
27
 
28
+ browser_config = load_browser_config()
29
+ width = width or browser_config["viewport"]["width"]
30
+ height = height or browser_config["viewport"]["height"]
31
+
16
32
  output_dir.mkdir(parents=True, exist_ok=True)
17
33
  shots: list[str] = []
18
34
  values: list[object] = []
@@ -29,7 +45,8 @@ def run_browser_script(*, url: str, script: str, output_dir: Path, record_video:
29
45
  context_args = {"viewport": {"width": width, "height": height}}
30
46
  if record_video:
31
47
  context_args.update(record_video_dir=str(video_dir), record_video_size={
32
- "width": min(width, 1280), "height": min(height, 720),
48
+ "width": browser_config["video"]["width"],
49
+ "height": browser_config["video"]["height"],
33
50
  })
34
51
  context = browser.new_context(**context_args)
35
52
  page = context.new_page()
@@ -90,8 +107,8 @@ def browser_group():
90
107
  @click.option("--output-dir", type=click.Path(file_okay=False, path_type=Path),
91
108
  default=Path("./portacode-browser-artifacts"), show_default=True)
92
109
  @click.option("--record-video/--no-record-video", default=False)
93
- @click.option("--width", default=1280, type=click.IntRange(320, 1920))
94
- @click.option("--height", default=720, type=click.IntRange(240, 1080))
110
+ @click.option("--width", default=None, type=click.IntRange(240, 1920), help="Override saved viewport width.")
111
+ @click.option("--height", default=None, type=click.IntRange(240, 1920), help="Override saved viewport height.")
95
112
  @click.option("--timeout-ms", default=10000, type=click.IntRange(250, 60000))
96
113
  def browser_run(url, script, script_file, output_dir, record_video, width, height, timeout_ms):
97
114
  """Execute SCRIPT with page, context, screenshot(), dom(), and emit()."""
@@ -104,3 +121,26 @@ def browser_run(url, script, script_file, output_dir, record_video, width, heigh
104
121
  click.echo(json.dumps(result, indent=2, default=str))
105
122
  if not result["ok"]:
106
123
  raise click.exceptions.Exit(1)
124
+
125
+
126
+ @browser_group.command("config")
127
+ @click.option("--viewport-width", type=click.IntRange(240, 1920))
128
+ @click.option("--viewport-height", type=click.IntRange(240, 1920))
129
+ @click.option("--video-width", type=click.IntRange(240, 1920))
130
+ @click.option("--video-height", type=click.IntRange(240, 1920))
131
+ def browser_config(viewport_width, viewport_height, video_width, video_height):
132
+ """Show or persist default browser viewport and recording dimensions."""
133
+ updates = {
134
+ "viewport": {
135
+ key: value for key, value in {
136
+ "width": viewport_width, "height": viewport_height,
137
+ }.items() if value is not None
138
+ },
139
+ "video": {
140
+ key: value for key, value in {
141
+ "width": video_width, "height": video_height,
142
+ }.items() if value is not None
143
+ },
144
+ }
145
+ config = update_browser_config(updates) if any(updates.values()) else load_browser_config()
146
+ click.echo(json.dumps(config, indent=2))
@@ -16,7 +16,7 @@ from .terminal_handlers import (
16
16
  )
17
17
  from .git_handlers import GitActionHandler
18
18
  from .system_handlers import SystemInfoHandler
19
- from .browser_handlers import BrowserRunHandler
19
+ from .browser_handlers import BrowserRunHandler, BrowserConfigHandler
20
20
  from .update_handler import UpdatePortacodeHandler
21
21
  from .file_handlers import (
22
22
  FileReadHandler,
@@ -103,6 +103,7 @@ __all__ = [
103
103
  "GitActionHandler",
104
104
  "SystemInfoHandler",
105
105
  "BrowserRunHandler",
106
+ "BrowserConfigHandler",
106
107
  "ConfigureProxmoxInfraHandler",
107
108
  "CreateProxmoxContainerHandler",
108
109
  "ResizeProxmoxContainerHandler",
@@ -9,6 +9,7 @@ import os
9
9
  import re
10
10
  import shutil
11
11
  import sys
12
+ import tempfile
12
13
  import time
13
14
  from pathlib import Path
14
15
  from typing import Any, Dict
@@ -25,7 +26,81 @@ _NODE_PATH: str | None = None
25
26
  _NODE_PATH_LOCK: asyncio.Lock | None = None
26
27
  _WORKER_IDLE_SECONDS = 300.0
27
28
  _MAX_WORKER_RESPONSE_BYTES = 16 * 1024 * 1024
29
+ _BROWSER_CONFIG_NAME = "browser.json"
28
30
  logger = logging.getLogger(__name__)
31
+
32
+ _DEFAULT_BROWSER_CONFIG = {
33
+ "viewport": {"width": 1280, "height": 720},
34
+ "video": {"width": 1280, "height": 720},
35
+ }
36
+
37
+
38
+ def _browser_config_path(message: dict | None = None) -> Path:
39
+ home = Path(get_runtime_user_home(message or {}))
40
+ return home / ".config" / "portacode" / _BROWSER_CONFIG_NAME
41
+
42
+
43
+ def _validated_dimension(section: str, key: str, value: Any) -> int:
44
+ try:
45
+ parsed = int(value)
46
+ except (TypeError, ValueError) as exc:
47
+ raise ValueError(f"{section}.{key} must be an integer") from exc
48
+ if not 240 <= parsed <= 1920:
49
+ raise ValueError(f"{section}.{key} must be between 240 and 1920")
50
+ return parsed
51
+
52
+
53
+ def load_browser_config(message: dict | None = None) -> dict:
54
+ path = _browser_config_path(message)
55
+ config = json.loads(json.dumps(_DEFAULT_BROWSER_CONFIG))
56
+ try:
57
+ data = json.loads(path.read_text(encoding="utf-8"))
58
+ for section in ("viewport", "video"):
59
+ for key in ("width", "height"):
60
+ config[section][key] = _validated_dimension(
61
+ section, key, data.get(section, {}).get(key, config[section][key])
62
+ )
63
+ except (OSError, ValueError, TypeError, json.JSONDecodeError):
64
+ pass
65
+ return config
66
+
67
+
68
+ def save_browser_config(config: dict, message: dict | None = None) -> dict:
69
+ path = _browser_config_path(message)
70
+ path.parent.mkdir(parents=True, exist_ok=True)
71
+ fd, temp = tempfile.mkstemp(prefix="browser-", dir=path.parent)
72
+ try:
73
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
74
+ json.dump(config, handle, indent=2)
75
+ handle.write("\n")
76
+ os.replace(temp, path)
77
+ finally:
78
+ try:
79
+ os.unlink(temp)
80
+ except FileNotFoundError:
81
+ pass
82
+ return config
83
+
84
+
85
+ def update_browser_config(updates: dict, message: dict | None = None) -> dict:
86
+ if not isinstance(updates, dict):
87
+ raise ValueError("config must be an object")
88
+ unknown = set(updates) - {"viewport", "video"}
89
+ if unknown:
90
+ raise ValueError(f"unsupported browser config fields: {', '.join(sorted(unknown))}")
91
+ config = load_browser_config(message)
92
+ for section in ("viewport", "video"):
93
+ values = updates.get(section, {})
94
+ if not isinstance(values, dict):
95
+ raise ValueError(f"{section} must be an object")
96
+ unknown_dimensions = set(values) - {"width", "height"}
97
+ if unknown_dimensions:
98
+ raise ValueError(f"unsupported {section} fields: {', '.join(sorted(unknown_dimensions))}")
99
+ for key, value in values.items():
100
+ config[section][key] = _validated_dimension(section, key, value)
101
+ return save_browser_config(config, message)
102
+
103
+
29
104
  _RUNNER = r"""
30
105
  const fs = require('fs');
31
106
  const readline = require('readline');
@@ -293,6 +368,19 @@ async def _worker_for(*, user: str, session_id: str, argv: list[str], env: dict[
293
368
  return worker
294
369
 
295
370
 
371
+ class BrowserConfigHandler(AsyncHandler):
372
+ @property
373
+ def command_name(self) -> str:
374
+ return "browser_config"
375
+
376
+ async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
377
+ config = load_browser_config(message)
378
+ updates = message.get("config")
379
+ if updates is not None:
380
+ config = update_browser_config(updates, message)
381
+ return {"event": "browser_config_response", "config": config}
382
+
383
+
296
384
  class BrowserRunHandler(AsyncHandler):
297
385
  @property
298
386
  def command_name(self) -> str:
@@ -330,9 +418,14 @@ class BrowserRunHandler(AsyncHandler):
330
418
  raise ValueError("browser action payload is too large")
331
419
  if sum(1 for step in steps if isinstance(step, dict) and step.get("op") == "screenshot") > 4:
332
420
  raise ValueError("at most four explicit screenshots are allowed per run")
421
+ configured = load_browser_config(message)
333
422
  timeout_ms = min(max(int(message.get("timeout_ms") or 10_000), 250), 30_000)
334
- width = min(max(int((message.get("viewport") or {}).get("width", 960)), 320), 1920)
335
- height = min(max(int((message.get("viewport") or {}).get("height", 540)), 240), 1080)
423
+ viewport = {**configured["viewport"], **(message.get("viewport") or {})}
424
+ width = _validated_dimension("viewport", "width", viewport["width"])
425
+ height = _validated_dimension("viewport", "height", viewport["height"])
426
+ video = {**configured["video"], **(message.get("video") or {})}
427
+ video_width = _validated_dimension("video", "width", video["width"])
428
+ video_height = _validated_dimension("video", "height", video["height"])
336
429
  user = get_default_runtime_user(message)
337
430
  root = Path(get_runtime_user_home(message)) / ".local" / "share" / "portacode" / "browser"
338
431
  profile = root / "profiles" / session_id
@@ -345,7 +438,7 @@ class BrowserRunHandler(AsyncHandler):
345
438
  executable = "/usr/bin/chromium-browser" if Path("/usr/bin/chromium-browser").exists() else None
346
439
  config = {
347
440
  "session_id": session_id, "profile_dir": str(profile), "video_dir": str(video_dir) if video_dir else None,
348
- "video_size": {"width": min(width, 854), "height": min(height, 480)},
441
+ "video_size": {"width": video_width, "height": video_height},
349
442
  "viewport": {"width": width, "height": height}, "executable_path": executable,
350
443
  "start_url": message.get("start_url"), "steps": steps, "timeout_ms": timeout_ms,
351
444
  "deadline_ms": 45_000 if video_dir else None,
@@ -25,6 +25,7 @@ except ImportError: # pragma: no cover - py<3.8
25
25
 
26
26
  from .base import SyncHandler
27
27
  from .proxmox_infra import get_infra_snapshot
28
+ from .browser_handlers import load_browser_config
28
29
  from .runtime_user import get_default_runtime_user, get_runtime_user_home
29
30
  from portacode.tunneling.forwarding_state import load_forwarding_state
30
31
  from portacode.connection.codex_app_server import CodexAppServer
@@ -630,6 +631,7 @@ class SystemInfoHandler(SyncHandler):
630
631
  info["os_info"] = _get_os_info()
631
632
  info["user_context"] = _get_user_context()
632
633
  info["playwright"] = _get_playwright_info()
634
+ info["browser_config"] = load_browser_config()
633
635
  info["proxmox"] = _get_proxmox_info()
634
636
  info["cloudflare_tunnel"] = _get_cloudflare_tunnel_state()
635
637
  info["cloudflare_forwarding"] = _get_cloudflare_forwarding_state()
@@ -2,11 +2,12 @@ import unittest
2
2
  import asyncio
3
3
  import json
4
4
  import os
5
+ import tempfile
5
6
  from types import SimpleNamespace
6
7
  from unittest.mock import AsyncMock, Mock, patch
7
8
 
8
9
  from . import browser_handlers
9
- from .browser_handlers import BrowserRunHandler
10
+ from .browser_handlers import BrowserConfigHandler, BrowserRunHandler
10
11
 
11
12
 
12
13
  class BrowserRunHandlerTests(unittest.IsolatedAsyncioTestCase):
@@ -215,3 +216,58 @@ class BrowserRunHandlerTests(unittest.IsolatedAsyncioTestCase):
215
216
  def test_bounded_dom_operation_is_available(self):
216
217
  self.assertIn("op==='dom'", browser_handlers._RUNNER)
217
218
  self.assertIn("20000", browser_handlers._RUNNER)
219
+
220
+ async def test_persistent_mobile_config_controls_viewport_and_video(self):
221
+ worker = AsyncMock()
222
+ worker.run.return_value = {"ok": True, "steps": [], "screenshots": []}
223
+ with (
224
+ tempfile.TemporaryDirectory() as home,
225
+ patch.object(browser_handlers, "get_default_runtime_user", return_value="tester"),
226
+ patch.object(browser_handlers, "get_runtime_user_home", return_value=home),
227
+ patch.object(browser_handlers, "mkdir_with_owner"),
228
+ patch.object(browser_handlers, "wrap_argv_for_user", return_value=["node"]),
229
+ patch.object(browser_handlers, "_node_path", AsyncMock(return_value="/modules")),
230
+ patch.object(browser_handlers, "_node_executable", return_value="/bundled/node"),
231
+ patch.object(browser_handlers, "_playwright_js_package", return_value=None),
232
+ patch.object(browser_handlers, "_worker_for", AsyncMock(return_value=worker)),
233
+ ):
234
+ configured = await BrowserConfigHandler(None, {}).execute({
235
+ "config": {
236
+ "viewport": {"width": 390, "height": 844},
237
+ "video": {"width": 390, "height": 844},
238
+ }
239
+ })
240
+ await self.handler.execute({
241
+ "session_id": "mobile", "steps": [], "record_video": True,
242
+ "final_screenshot": False,
243
+ })
244
+ saved = json.loads(
245
+ open(os.path.join(home, ".config", "portacode", "browser.json"), encoding="utf-8").read()
246
+ )
247
+ self.assertEqual(configured["config"], saved)
248
+ run_config = worker.run.await_args.args[0]
249
+ self.assertEqual(run_config["viewport"], {"width": 390, "height": 844})
250
+ self.assertEqual(run_config["video_size"], {"width": 390, "height": 844})
251
+
252
+ async def test_per_run_dimensions_override_saved_config(self):
253
+ worker = AsyncMock()
254
+ worker.run.return_value = {"ok": True, "steps": [], "screenshots": []}
255
+ saved = {"viewport": {"width": 390, "height": 844}, "video": {"width": 390, "height": 844}}
256
+ with (
257
+ patch.object(browser_handlers, "load_browser_config", return_value=saved),
258
+ patch.object(browser_handlers, "get_default_runtime_user", return_value="tester"),
259
+ patch.object(browser_handlers, "get_runtime_user_home", return_value="/tmp"),
260
+ patch.object(browser_handlers, "mkdir_with_owner"),
261
+ patch.object(browser_handlers, "wrap_argv_for_user", return_value=["node"]),
262
+ patch.object(browser_handlers, "_node_path", AsyncMock(return_value="/modules")),
263
+ patch.object(browser_handlers, "_node_executable", return_value="/bundled/node"),
264
+ patch.object(browser_handlers, "_playwright_js_package", return_value=None),
265
+ patch.object(browser_handlers, "_worker_for", AsyncMock(return_value=worker)),
266
+ ):
267
+ await self.handler.execute({
268
+ "session_id": "override", "steps": [], "record_video": True,
269
+ "viewport": {"width": 430}, "video": {"height": 932},
270
+ })
271
+ run_config = worker.run.await_args.args[0]
272
+ self.assertEqual(run_config["viewport"], {"width": 430, "height": 844})
273
+ self.assertEqual(run_config["video_size"], {"width": 390, "height": 932})
@@ -46,6 +46,7 @@ from .handlers import (
46
46
  GitActionHandler,
47
47
  SystemInfoHandler,
48
48
  BrowserRunHandler,
49
+ BrowserConfigHandler,
49
50
  FileReadHandler,
50
51
  ImageReadHandler,
51
52
  DirectoryListHandler,
@@ -575,6 +576,7 @@ class TerminalManager:
575
576
  self._command_registry.register(GitActionHandler)
576
577
  self._command_registry.register(SystemInfoHandler)
577
578
  self._command_registry.register(BrowserRunHandler)
579
+ self._command_registry.register(BrowserConfigHandler)
578
580
  # File operation handlers
579
581
  self._command_registry.register(FileReadHandler)
580
582
  self._command_registry.register(ImageReadHandler)
@@ -1,10 +1,12 @@
1
1
  import json
2
+ import os
2
3
  from pathlib import Path
3
4
  from unittest.mock import patch
4
5
 
5
6
  from click.testing import CliRunner
6
7
 
7
8
  from portacode.cli import cli
9
+ from portacode import browser_cli
8
10
 
9
11
 
10
12
  def test_browser_run_accepts_inline_python_and_returns_json(tmp_path):
@@ -29,3 +31,21 @@ def test_browser_run_propagates_failure_exit_code(tmp_path):
29
31
  with patch("portacode.browser_cli.run_browser_script", return_value={"ok": False, "error": "boom"}):
30
32
  result = CliRunner().invoke(cli, ["browser", "run", "--script", "raise Exception()"])
31
33
  assert result.exit_code == 1
34
+
35
+
36
+ def test_managed_browser_cache_is_used_when_available(monkeypatch):
37
+ monkeypatch.delenv("PLAYWRIGHT_BROWSERS_PATH", raising=False)
38
+ monkeypatch.setattr(Path, "is_dir", lambda self: True)
39
+
40
+ browser_cli._configure_managed_browser_path()
41
+
42
+ assert os.environ["PLAYWRIGHT_BROWSERS_PATH"] == "/opt/ms-playwright"
43
+
44
+
45
+ def test_explicit_browser_cache_is_preserved(monkeypatch):
46
+ monkeypatch.setenv("PLAYWRIGHT_BROWSERS_PATH", "/custom/browsers")
47
+ monkeypatch.setattr(Path, "is_dir", lambda self: True)
48
+
49
+ browser_cli._configure_managed_browser_path()
50
+
51
+ assert os.environ["PLAYWRIGHT_BROWSERS_PATH"] == "/custom/browsers"
@@ -19,3 +19,18 @@ def test_system_info_includes_codex_installation_capability(monkeypatch):
19
19
  "app_server_supported": True,
20
20
  "ready": True,
21
21
  }
22
+
23
+
24
+ def test_system_info_includes_persistent_browser_config(monkeypatch):
25
+ expected = {
26
+ "viewport": {"width": 390, "height": 844},
27
+ "video": {"width": 390, "height": 844},
28
+ }
29
+ monkeypatch.setattr(
30
+ "portacode.connection.handlers.system_handlers.load_browser_config",
31
+ lambda: expected,
32
+ )
33
+
34
+ payload = SystemInfoHandler(None, {}).execute({})
35
+
36
+ assert payload["info"]["browser_config"] == expected
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 2.2.20.dev2
3
+ Version: 2.2.20.dev4
4
4
  Summary: Device agent for the AI platform that builds, tests, deploys, and operates real software
5
5
  Home-page: https://portacode.com/
6
6
  Author: Meena Erian
@@ -1,8 +1,8 @@
1
1
  portacode/README.md,sha256=R76fnJCKOnLiBEirTtc7fj4A6j5vAN1pdYMGNGqu1cs,1215
2
2
  portacode/__init__.py,sha256=Hwk8wm5KA79qi06r0qKJKT2kjUlHrWaDtH4CyhnWFw8,1086
3
3
  portacode/__main__.py,sha256=jmHTGC1hzmo9iKJLv-SSYe9BSIbPPZ2IOpecI03PlTs,296
4
- portacode/_version.py,sha256=njgzVlo1D-wKwpkJdGZMVlkIoVhVbrYsOvlqiA3-ZZE,535
5
- portacode/browser_cli.py,sha256=jhTLbvayHCOb7rcHUQgebCbw5pfSmpqanCKUIC-DQVw,4664
4
+ portacode/_version.py,sha256=H2sn0GWHtWK4XYFCqf5LLnQNzH5UrmtTH3ai6X37v24,535
5
+ portacode/browser_cli.py,sha256=XF__clsvyLsPLRVa4MezP6rijKBYsbHl-OLBJlV_63Y,6320
6
6
  portacode/cli.py,sha256=k6wApK6PclUhyAmfOR91lHVA7zbA4IbcAsr0LSumYxk,40645
7
7
  portacode/codex_loopback_proxy.py,sha256=RJ4oATShAf-m0RYlugIe6b6MzYrpb6aQ8f-xrx2AilI,24788
8
8
  portacode/codex_prepare.py,sha256=VxIklS9cb9GF6fcFt7H_NN0AVXcnEzsGJmsaPliNXco,27595
@@ -34,17 +34,17 @@ portacode/connection/client.py,sha256=QDFRwBjATw5tp-wlhkiz5OfL_jVAwRHcVpPOYg6ntO
34
34
  portacode/connection/codex_app_server.py,sha256=Q16bD78Hf14mtiWQ4QIB8eCTEY4wZrVytrXxr36Mo2E,23245
35
35
  portacode/connection/guest_host_protocol.py,sha256=bXghGvUFuj2yg89pPljLuS4ivEYwgM3BwDK7CHIc2Dk,6199
36
36
  portacode/connection/multiplex.py,sha256=L-TxqJ_ZEbfNEfu1cwxgJ5vUdyRzZjsMy2Kx1diiZys,5237
37
- portacode/connection/terminal.py,sha256=EfoxGK_nHP5QreOWs1-B1_yigY2jPZvjwChwZ6deAEw,74050
37
+ portacode/connection/terminal.py,sha256=pVlYPzOTk12phbSZYGgd-Hqgke9nAOH9ftmZdnsMxt0,74138
38
38
  portacode/connection/test_guest_host_protocol.py,sha256=KhoXv4YMDYh6ENQrzW5rQNDZ3PMTP7oSN6iSC9KH4NE,2561
39
39
  portacode/connection/test_guest_host_terminal.py,sha256=3c54_1fBHljdKmxpj4LYudVpgMZ-W8p_wB14WYVswps,5151
40
40
  portacode/connection/test_webmin_proxy_config.py,sha256=52Qv9C0E5UzKUvQDJfREAb3RwJWs5R_CkKECrTWOsNg,1121
41
41
  portacode/connection/webmin_proxy_config.py,sha256=vxzucOUz_GgxDjZWcgNVj7ImNfbdpIRABWpG_ihcK6s,2569
42
42
  portacode/connection/handlers/README.md,sha256=hDIp55rOje2Qb55K0Np6EYZUe0vp4ATT3c4ANolwyf4,12521
43
43
  portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=y6B8VfZzXp2q3JDTcHqhDf2UegyhGx_7wQJhCeM2Iok,129814
44
- portacode/connection/handlers/__init__.py,sha256=COY6pHzc_yQm7FGQQdOPXSSlGOsgNdKD4sWFkZAreaI,5168
44
+ portacode/connection/handlers/__init__.py,sha256=Xq3bNJwmsIg9oG7O4Kgzc-fg8OKS6aagSwdUEoyTZGw,5218
45
45
  portacode/connection/handlers/automation_v2_handlers.py,sha256=eQT6XHABfxDTtI2ibczA5IdV0cBddqPe1SElv56eh_M,48950
46
46
  portacode/connection/handlers/base.py,sha256=tMsKJmnNhXA3a4YZA55a0WO2e8-4Uh4BsG9lWAr3Ris,10829
47
- portacode/connection/handlers/browser_handlers.py,sha256=9uX450FFbtQgGu_pMer63phama-sPirNrDznKwzCVbM,17552
47
+ portacode/connection/handlers/browser_handlers.py,sha256=iYnKjYX-5YZyNVo3BMTkN2X52-0BJXy2xCf8l391w7I,21090
48
48
  portacode/connection/handlers/chunked_content.py,sha256=h6hXRmxSeOgnIxoU8CkmvEf2Odv-ajPrpHIe_W3GKcA,9251
49
49
  portacode/connection/handlers/cloudflare_forwarding.py,sha256=NXDKR9PZTngQ0AaH96QLICXPTKZMy-h6RCwa7F7nhVk,46928
50
50
  portacode/connection/handlers/cloudflare_tunnel.py,sha256=qHBrm2rATxFmvfoC49YOBj0EUXjh_FETiWw66krxCIg,6655
@@ -60,10 +60,10 @@ portacode/connection/handlers/registry.py,sha256=dMv0fiCY4RFtkkyB1vmQMrrpD7zEhiP
60
60
  portacode/connection/handlers/resumable_transfer_handlers.py,sha256=p8PaRRYh45LjKdfhIdHdUqREHpTlJ90yXWxnhAdYGaE,32671
61
61
  portacode/connection/handlers/runtime_user.py,sha256=tvetxe4CKe8QzmROz9HE-dEpTlKJhg_7UjChyviOlOc,7707
62
62
  portacode/connection/handlers/session.py,sha256=xUmSorFxKoLz7y9BLxfFD4VkSHEtL0bi_qf3mP0kmcw,46784
63
- portacode/connection/handlers/system_handlers.py,sha256=WDNOYkObPXQU0gvcMYV1Drg_ZhTfbutfjokPG1nMyNI,23670
63
+ portacode/connection/handlers/system_handlers.py,sha256=-BOmH3lfhvxC8hDkiSYHbcvF1D7j2T63bAruOm7LIo8,23775
64
64
  portacode/connection/handlers/tab_factory.py,sha256=yn93h6GASjD1VpvW1oqpax3EpoT0r7r97zFXxML1wdA,16173
65
65
  portacode/connection/handlers/terminal_handlers.py,sha256=s6bNmnXkjbV7rjSoYVDbbB9uHLGj2yNCz30wM6aRaHA,18651
66
- portacode/connection/handlers/test_browser_handlers.py,sha256=xsq4TUU6XTI6DHMJP2y3Fe12G__Y6fFAlhUxWxIo860,9902
66
+ portacode/connection/handlers/test_browser_handlers.py,sha256=MtL1xc8phHPay1u-EdUd2KkaO2FU_8MYaZ2nkLVRWTE,13269
67
67
  portacode/connection/handlers/test_cloudflare_forwarding.py,sha256=0rryq_r-VhTgi_spmCymnaJeLVVemsVF2qEIgXKz_Bc,11633
68
68
  portacode/connection/handlers/test_file_move_copy.py,sha256=uXejCCcPWKs9qfyL6ckp0NxjwGYK3-Y63i3lFz-F47M,2872
69
69
  portacode/connection/handlers/test_git_handlers.py,sha256=T9gFkCjanEC2L8n_kS2Ht0e67he34vThQtJU7j68auo,3514
@@ -104,7 +104,7 @@ portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-38.pyc,sha25
104
104
  portacode/static/js/test-ntp-clock.html,sha256=bUow9sifIuLNPqKvuPbpQozmEE6RhdCI4Plib3CqUmw,2130
105
105
  portacode/static/js/utils/ntp-clock.js,sha256=t9moJyAGGU054BVtuGuSETVstlj1AoxVy44i4y9QWSs,6940
106
106
  portacode/tests/test_automation_v2_inputs.py,sha256=iySvkskbqxFm7hSz19zNDwu8wGHvJkvHE-IQ3PKc42Q,2103
107
- portacode/tests/test_browser_cli.py,sha256=xmn6znqNEnhUSOlBHwyaNQ7mrnNx4w7aBZrB5VkQ-z8,1222
107
+ portacode/tests/test_browser_cli.py,sha256=M0L4v1oRe3cD40boZqGx_gTvytAPk9nLZlCy8e4fLbo,1902
108
108
  portacode/tests/test_cli_service_logs.py,sha256=Fog68tVDMEe8zOcpfA1ukPRodhgjOwbTQ6Rm4leH6dQ,1733
109
109
  portacode/tests/test_codex_app_server.py,sha256=xUiCno_Gx3Z5KrrhoxN38c5nGJBDgJ9bT7TDvFGZDMs,13993
110
110
  portacode/tests/test_codex_handlers.py,sha256=vmwpifku1Dk7TI89H2z8nZQHueHa669NXxUbVghsreA,32083
@@ -114,7 +114,7 @@ portacode/tests/test_github_credential.py,sha256=k8n8tf26GqMjw3N_nSKztBQAv4ykQBe
114
114
  portacode/tests/test_image_cli.py,sha256=By6fVtfFFu8IH7LYJqj0l3znZ30-KX-VIZ8H2RDwuKU,1646
115
115
  portacode/tests/test_image_read_handler.py,sha256=jqSa7yBKSEvit-rqqsmig7I2--RSpsRzhgr7R1nCxfw,783
116
116
  portacode/tests/test_resumable_transfer_handlers.py,sha256=Uc3vF7MMJjsTKqNO4_FOdgd0MVeEsfu8U2gV0dtt-iw,12171
117
- portacode/tests/test_system_handlers.py,sha256=I-1b367O0X0tyJt0Oh0P5e1B_Xqni7XRYm20VRwc4Yk,678
117
+ portacode/tests/test_system_handlers.py,sha256=gbc5n6o3OxDpm-wRs4D2Y3IURfs1VesliGmRgLIMi_0,1119
118
118
  portacode/tests/test_terminal_periodic_system_info.py,sha256=cYdTnI6lEigWGNtDH_54JWP9Mo4D6xr8MFHvtE_PADA,1940
119
119
  portacode/tests/test_terminal_session_shell.py,sha256=nNM_fbT1vM8oknGDBeNk0eiRtZ8uz17dXXOB7STgGK8,2157
120
120
  portacode/tunneling/__init__.py,sha256=qOrizn5awmPfsabVF59-EkmXnvS8AogvcBtVON6UfP0,54
@@ -132,7 +132,7 @@ portacode/utils/diff_apply.py,sha256=ArkT-vEQ7iyA1Nfqufq1AiNtRLhoCMnC75FCRh2kNgI
132
132
  portacode/utils/diff_renderer.py,sha256=S76StnQ2DLfsz4Gg0m07UwPfRp8270PuzbNaQq-rmYk,13850
133
133
  portacode/utils/ntp_clock.py,sha256=VqCnWCTehCufE43W23oB-WUdAZGeCcLxkmIOPwInYHc,2499
134
134
  portacode/utils/runtime_paths.py,sha256=G9ROho5f5gnDhoQ7vpEAX2XifBzoJTvqTylkF2Hkx8E,1660
135
- portacode-2.2.20.dev2.dist-info/licenses/LICENSE,sha256=iiV44E3po4OL77NL8diCk3WtYvCNGu6gUci2TYxVUgQ,5520
135
+ portacode-2.2.20.dev4.dist-info/licenses/LICENSE,sha256=iiV44E3po4OL77NL8diCk3WtYvCNGu6gUci2TYxVUgQ,5520
136
136
  test_modules/README.md,sha256=Do_agkm9WhSzueXjRAkV_xEj6Emy5zB3N3VKY5Roce8,9274
137
137
  test_modules/__init__.py,sha256=1LcbHodIHsB0g-g4NGjSn6AMuCoGbymvXPYLOb6Z7F0,53
138
138
  test_modules/test_device_online.py,sha256=QtYq0Dq9vME8Gp2O4fGSheqVf8LUtpsSKosXXk56gGM,1654
@@ -158,8 +158,8 @@ testing_framework/core/playwright_manager.py,sha256=asMe7zQg8Kp3a71F7pv92OiFt63B
158
158
  testing_framework/core/runner.py,sha256=t3fq0mGCVUCE_TbieEPwmLKjxe44ez29SzW8G33ACME,19746
159
159
  testing_framework/core/shared_cli_manager.py,sha256=BESSNtyQb7BOlaOvZmm04T8Uezjms4KCBs2MzTxvzYQ,8790
160
160
  testing_framework/core/test_discovery.py,sha256=2FZ9fJ8Dp5dloA-fkgXoJ_gCMC_nYPBnA3Hs2xlagzM,4928
161
- portacode-2.2.20.dev2.dist-info/METADATA,sha256=Oe6GQKyYSyeWcmr5h5fK2Zmbw-cmm8idyoUPNrvQDe4,12851
162
- portacode-2.2.20.dev2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
163
- portacode-2.2.20.dev2.dist-info/entry_points.txt,sha256=0cV_SuIztVnLWACUY5KMR8K8pWdAsLiaFbLXE5He0rk,151
164
- portacode-2.2.20.dev2.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
165
- portacode-2.2.20.dev2.dist-info/RECORD,,
161
+ portacode-2.2.20.dev4.dist-info/METADATA,sha256=n9OUaHJtBXU9up4EMMHmKeJHX__JjwRXQ3p7pJ28uII,12851
162
+ portacode-2.2.20.dev4.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
163
+ portacode-2.2.20.dev4.dist-info/entry_points.txt,sha256=0cV_SuIztVnLWACUY5KMR8K8pWdAsLiaFbLXE5He0rk,151
164
+ portacode-2.2.20.dev4.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
165
+ portacode-2.2.20.dev4.dist-info/RECORD,,