portacode 1.3.32__py3-none-any.whl → 1.4.11.dev0__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.

Potentially problematic release.


This version of portacode might be problematic. Click here for more details.

Files changed (56) hide show
  1. portacode/_version.py +2 -2
  2. portacode/cli.py +119 -14
  3. portacode/connection/client.py +127 -8
  4. portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +301 -4
  5. portacode/connection/handlers/__init__.py +10 -1
  6. portacode/connection/handlers/diff_handlers.py +603 -0
  7. portacode/connection/handlers/file_handlers.py +674 -17
  8. portacode/connection/handlers/project_aware_file_handlers.py +11 -0
  9. portacode/connection/handlers/project_state/file_system_watcher.py +31 -61
  10. portacode/connection/handlers/project_state/git_manager.py +139 -572
  11. portacode/connection/handlers/project_state/handlers.py +28 -14
  12. portacode/connection/handlers/project_state/manager.py +226 -101
  13. portacode/connection/handlers/proxmox_infra.py +307 -0
  14. portacode/connection/handlers/session.py +465 -84
  15. portacode/connection/handlers/system_handlers.py +140 -8
  16. portacode/connection/handlers/tab_factory.py +1 -47
  17. portacode/connection/handlers/update_handler.py +61 -0
  18. portacode/connection/terminal.py +51 -10
  19. portacode/keypair.py +63 -1
  20. portacode/link_capture/__init__.py +38 -0
  21. portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
  22. portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
  23. portacode/link_capture/bin/elinks +3 -0
  24. portacode/link_capture/bin/gio-open +3 -0
  25. portacode/link_capture/bin/gnome-open +3 -0
  26. portacode/link_capture/bin/gvfs-open +3 -0
  27. portacode/link_capture/bin/kde-open +3 -0
  28. portacode/link_capture/bin/kfmclient +3 -0
  29. portacode/link_capture/bin/link_capture_exec.sh +11 -0
  30. portacode/link_capture/bin/link_capture_wrapper.py +75 -0
  31. portacode/link_capture/bin/links +3 -0
  32. portacode/link_capture/bin/links2 +3 -0
  33. portacode/link_capture/bin/lynx +3 -0
  34. portacode/link_capture/bin/mate-open +3 -0
  35. portacode/link_capture/bin/netsurf +3 -0
  36. portacode/link_capture/bin/sensible-browser +3 -0
  37. portacode/link_capture/bin/w3m +3 -0
  38. portacode/link_capture/bin/x-www-browser +3 -0
  39. portacode/link_capture/bin/xdg-open +3 -0
  40. portacode/pairing.py +103 -0
  41. portacode/static/js/utils/ntp-clock.js +170 -79
  42. portacode/utils/diff_apply.py +456 -0
  43. portacode/utils/diff_renderer.py +371 -0
  44. portacode/utils/ntp_clock.py +45 -131
  45. {portacode-1.3.32.dist-info → portacode-1.4.11.dev0.dist-info}/METADATA +71 -3
  46. portacode-1.4.11.dev0.dist-info/RECORD +97 -0
  47. test_modules/test_device_online.py +1 -1
  48. test_modules/test_login_flow.py +8 -4
  49. test_modules/test_play_store_screenshots.py +294 -0
  50. testing_framework/.env.example +4 -1
  51. testing_framework/core/playwright_manager.py +63 -9
  52. portacode-1.3.32.dist-info/RECORD +0 -70
  53. {portacode-1.3.32.dist-info → portacode-1.4.11.dev0.dist-info}/WHEEL +0 -0
  54. {portacode-1.3.32.dist-info → portacode-1.4.11.dev0.dist-info}/entry_points.txt +0 -0
  55. {portacode-1.3.32.dist-info → portacode-1.4.11.dev0.dist-info}/licenses/LICENSE +0 -0
  56. {portacode-1.3.32.dist-info → portacode-1.4.11.dev0.dist-info}/top_level.txt +0 -0
@@ -22,6 +22,17 @@ class ProjectAwareFileWriteHandler(SyncHandler):
22
22
  """Write file contents and update project state tabs."""
23
23
  file_path = message.get("path")
24
24
  content = message.get("content", "")
25
+ # Optimistic lock: ensure the client saw the correct file state
26
+ expected_mtime = message.get("expected_mtime")
27
+ if expected_mtime is not None:
28
+ try:
29
+ current_mtime = os.path.getmtime(file_path)
30
+ except FileNotFoundError:
31
+ raise ValueError(f"File not found: {file_path}")
32
+ if current_mtime != expected_mtime:
33
+ raise ValueError(
34
+ f"File was modified on disk (current {current_mtime} != expected {expected_mtime})"
35
+ )
25
36
 
26
37
  if not file_path:
27
38
  raise ValueError("path parameter is required")
@@ -71,50 +71,29 @@ class FileSystemWatcher:
71
71
  return
72
72
 
73
73
  # Only process events that represent actual content changes
74
- # Skip opened/closed events that don't indicate file modifications
75
- if event.event_type in ('opened', 'closed'):
76
- logger.debug("🔍 [TRACE] Skipping opened/closed event: %s", event.event_type)
74
+ # Skip events that indicate read-only access or metadata churn
75
+ significant_event_types = {'modified', 'created', 'deleted', 'moved', 'closed_write'}
76
+ if event.event_type not in significant_event_types:
77
+ logger.debug("🔍 [TRACE] Skipping non-content event: %s", event.event_type)
78
+ return
79
+
80
+ # Reading directory contents during refresh generates metadata-only "modified" events.
81
+ if event.is_directory and event.event_type == 'modified':
82
+ logger.debug("🔍 [TRACE] Skipping directory metadata change: %s", event.src_path)
77
83
  return
78
84
 
79
85
  # Handle .git folder events separately for git status monitoring
80
86
  path_parts = Path(event.src_path).parts
81
87
  if '.git' in path_parts:
82
- logger.debug("🔍 [TRACE] Processing .git folder event: %s", event.src_path)
83
- # Get the relative path within .git directory
84
- try:
85
- git_index = path_parts.index('.git')
86
- git_relative_path = '/'.join(path_parts[git_index + 1:])
87
- git_file = Path(event.src_path).name
88
-
89
- logger.debug("🔍 [TRACE] Git file details - relative_path: %s, file: %s", git_relative_path, git_file)
90
-
91
- # Monitor git files that indicate repository state changes
92
- should_monitor_git_file = (
93
- git_file == 'index' or # Staging area changes
94
- git_file == 'index.lock' or # Staging operations in progress
95
- git_file == 'HEAD' or # Branch switches
96
- git_relative_path.startswith('refs/heads/') or # Branch updates
97
- git_relative_path.startswith('refs/remotes/') or # Remote tracking branches
98
- git_relative_path.startswith('logs/refs/heads/') or # Branch history
99
- git_relative_path.startswith('logs/HEAD') or # HEAD history
100
- git_relative_path.startswith('objects/') and event.event_type in ('created', 'modified') # New objects (commits, blobs)
101
- )
102
-
103
- if should_monitor_git_file:
104
- logger.debug("🔍 [TRACE] ✅ Git file matches monitoring criteria: %s", event.src_path)
105
- else:
106
- logger.debug("🔍 [TRACE] ❌ Git file does NOT match monitoring criteria - SKIPPING: %s", event.src_path)
107
- return # Skip other .git files
108
- except (ValueError, IndexError):
109
- logger.debug("🔍 [TRACE] ❌ Could not parse .git path - SKIPPING: %s", event.src_path)
110
- return # Skip if can't parse .git path
88
+ logger.debug("🔍 [TRACE] Skipping .git folder event entirely: %s", event.src_path)
89
+ return
90
+
91
+ logger.debug("🔍 [TRACE] Processing non-git file event: %s", event.src_path)
92
+ # Only log significant file changes, not every single event
93
+ if event.event_type in ['created', 'deleted'] or event.src_path.endswith(('.py', '.js', '.html', '.css', '.json', '.md')):
94
+ logger.debug("File system event: %s - %s", event.event_type, os.path.basename(event.src_path))
111
95
  else:
112
- logger.debug("🔍 [TRACE] Processing non-git file event: %s", event.src_path)
113
- # Only log significant file changes, not every single event
114
- if event.event_type in ['created', 'deleted'] or event.src_path.endswith(('.py', '.js', '.html', '.css', '.json', '.md')):
115
- logger.debug("File system event: %s - %s", event.event_type, os.path.basename(event.src_path))
116
- else:
117
- logger.debug("File event: %s", os.path.basename(event.src_path))
96
+ logger.debug("File event: %s", os.path.basename(event.src_path))
118
97
 
119
98
  # Schedule async task in the main event loop from this watchdog thread
120
99
  logger.debug("🔍 [TRACE] About to schedule async handler - event_loop exists: %s, closed: %s",
@@ -142,6 +121,11 @@ class FileSystemWatcher:
142
121
  logger.warning("Watchdog not available, cannot start watching: %s", path)
143
122
  return
144
123
 
124
+ normalized_name = Path(path).name
125
+ if normalized_name == '.git':
126
+ logger.debug("Skipping watch for .git path: %s", path)
127
+ return
128
+
145
129
  if path not in self.watched_paths:
146
130
  try:
147
131
  # Use recursive=False to watch only direct contents of each folder
@@ -158,28 +142,6 @@ class FileSystemWatcher:
158
142
  else:
159
143
  logger.debug("Path already being watched: %s", path)
160
144
 
161
- def start_watching_git_directory(self, git_path: str):
162
- """Start watching a .git directory for git status changes."""
163
- if not WATCHDOG_AVAILABLE or not self.observer:
164
- logger.warning("Watchdog not available, cannot start watching git directory: %s", git_path)
165
- return
166
-
167
- if git_path not in self.watched_paths:
168
- try:
169
- # Watch .git directory recursively to catch changes in refs/, logs/, etc.
170
- watch_handle = self.observer.schedule(self.event_handler, git_path, recursive=True)
171
- self.watched_paths.add(git_path)
172
- self.watch_handles[git_path] = watch_handle # Store handle for cleanup
173
- logger.info("Started watching git directory (recursive): %s", git_path)
174
-
175
- if not self.observer.is_alive():
176
- self.observer.start()
177
- logger.info("Started file system observer")
178
- except Exception as e:
179
- logger.error("Error starting git directory watcher for %s: %s", git_path, e)
180
- else:
181
- logger.debug("Git directory already being watched: %s", git_path)
182
-
183
145
  def stop_watching(self, path: str):
184
146
  """Stop watching a specific path."""
185
147
  if not WATCHDOG_AVAILABLE or not self.observer:
@@ -206,4 +168,12 @@ class FileSystemWatcher:
206
168
  self.observer.stop()
207
169
  self.observer.join()
208
170
  self.watched_paths.clear()
209
- self.watch_handles.clear()
171
+ self.watch_handles.clear()
172
+
173
+ def get_diagnostics(self) -> dict:
174
+ """Return lightweight stats for health monitoring."""
175
+ return {
176
+ "watched_paths": len(self.watched_paths),
177
+ "git_watched_paths": len([path for path in self.watched_paths if path.endswith(".git")]),
178
+ "observer_alive": bool(self.observer and self.observer.is_alive()),
179
+ }