portacode 0.3.4.dev0__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 (93) hide show
  1. portacode/_version.py +16 -3
  2. portacode/cli.py +155 -19
  3. portacode/connection/client.py +152 -12
  4. portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +1577 -0
  5. portacode/connection/handlers/__init__.py +43 -1
  6. portacode/connection/handlers/base.py +122 -18
  7. portacode/connection/handlers/chunked_content.py +244 -0
  8. portacode/connection/handlers/diff_handlers.py +603 -0
  9. portacode/connection/handlers/file_handlers.py +902 -17
  10. portacode/connection/handlers/project_aware_file_handlers.py +226 -0
  11. portacode/connection/handlers/project_state/README.md +312 -0
  12. portacode/connection/handlers/project_state/__init__.py +92 -0
  13. portacode/connection/handlers/project_state/file_system_watcher.py +179 -0
  14. portacode/connection/handlers/project_state/git_manager.py +1502 -0
  15. portacode/connection/handlers/project_state/handlers.py +875 -0
  16. portacode/connection/handlers/project_state/manager.py +1331 -0
  17. portacode/connection/handlers/project_state/models.py +108 -0
  18. portacode/connection/handlers/project_state/utils.py +50 -0
  19. portacode/connection/handlers/project_state_handlers.py +45 -0
  20. portacode/connection/handlers/proxmox_infra.py +307 -0
  21. portacode/connection/handlers/registry.py +53 -10
  22. portacode/connection/handlers/session.py +705 -53
  23. portacode/connection/handlers/system_handlers.py +142 -8
  24. portacode/connection/handlers/tab_factory.py +389 -0
  25. portacode/connection/handlers/terminal_handlers.py +150 -11
  26. portacode/connection/handlers/update_handler.py +61 -0
  27. portacode/connection/multiplex.py +60 -2
  28. portacode/connection/terminal.py +695 -28
  29. portacode/keypair.py +63 -1
  30. portacode/link_capture/__init__.py +38 -0
  31. portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
  32. portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
  33. portacode/link_capture/bin/elinks +3 -0
  34. portacode/link_capture/bin/gio-open +3 -0
  35. portacode/link_capture/bin/gnome-open +3 -0
  36. portacode/link_capture/bin/gvfs-open +3 -0
  37. portacode/link_capture/bin/kde-open +3 -0
  38. portacode/link_capture/bin/kfmclient +3 -0
  39. portacode/link_capture/bin/link_capture_exec.sh +11 -0
  40. portacode/link_capture/bin/link_capture_wrapper.py +75 -0
  41. portacode/link_capture/bin/links +3 -0
  42. portacode/link_capture/bin/links2 +3 -0
  43. portacode/link_capture/bin/lynx +3 -0
  44. portacode/link_capture/bin/mate-open +3 -0
  45. portacode/link_capture/bin/netsurf +3 -0
  46. portacode/link_capture/bin/sensible-browser +3 -0
  47. portacode/link_capture/bin/w3m +3 -0
  48. portacode/link_capture/bin/x-www-browser +3 -0
  49. portacode/link_capture/bin/xdg-open +3 -0
  50. portacode/logging_categories.py +140 -0
  51. portacode/pairing.py +103 -0
  52. portacode/service.py +6 -0
  53. portacode/static/js/test-ntp-clock.html +63 -0
  54. portacode/static/js/utils/ntp-clock.js +232 -0
  55. portacode/utils/NTP_ARCHITECTURE.md +136 -0
  56. portacode/utils/__init__.py +1 -0
  57. portacode/utils/diff_apply.py +456 -0
  58. portacode/utils/diff_renderer.py +371 -0
  59. portacode/utils/ntp_clock.py +65 -0
  60. portacode-1.4.11.dev0.dist-info/METADATA +298 -0
  61. portacode-1.4.11.dev0.dist-info/RECORD +97 -0
  62. {portacode-0.3.4.dev0.dist-info → portacode-1.4.11.dev0.dist-info}/WHEEL +1 -1
  63. portacode-1.4.11.dev0.dist-info/top_level.txt +3 -0
  64. test_modules/README.md +296 -0
  65. test_modules/__init__.py +1 -0
  66. test_modules/test_device_online.py +44 -0
  67. test_modules/test_file_operations.py +743 -0
  68. test_modules/test_git_status_ui.py +370 -0
  69. test_modules/test_login_flow.py +50 -0
  70. test_modules/test_navigate_testing_folder.py +361 -0
  71. test_modules/test_play_store_screenshots.py +294 -0
  72. test_modules/test_terminal_buffer_performance.py +261 -0
  73. test_modules/test_terminal_interaction.py +80 -0
  74. test_modules/test_terminal_loading_race_condition.py +95 -0
  75. test_modules/test_terminal_start.py +56 -0
  76. testing_framework/.env.example +21 -0
  77. testing_framework/README.md +334 -0
  78. testing_framework/__init__.py +17 -0
  79. testing_framework/cli.py +326 -0
  80. testing_framework/core/__init__.py +1 -0
  81. testing_framework/core/base_test.py +336 -0
  82. testing_framework/core/cli_manager.py +177 -0
  83. testing_framework/core/hierarchical_runner.py +577 -0
  84. testing_framework/core/playwright_manager.py +520 -0
  85. testing_framework/core/runner.py +447 -0
  86. testing_framework/core/shared_cli_manager.py +234 -0
  87. testing_framework/core/test_discovery.py +112 -0
  88. testing_framework/requirements.txt +12 -0
  89. portacode-0.3.4.dev0.dist-info/METADATA +0 -236
  90. portacode-0.3.4.dev0.dist-info/RECORD +0 -27
  91. portacode-0.3.4.dev0.dist-info/top_level.txt +0 -1
  92. {portacode-0.3.4.dev0.dist-info → portacode-1.4.11.dev0.dist-info}/entry_points.txt +0 -0
  93. {portacode-0.3.4.dev0.dist-info → portacode-1.4.11.dev0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,179 @@
1
+ """File system monitoring for project state changes.
2
+
3
+ This module provides the FileSystemWatcher class which monitors file system
4
+ changes using the watchdog library and triggers project state updates when
5
+ files or directories are modified.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ import os
11
+ from pathlib import Path
12
+ from typing import Optional, Set
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Cross-platform file system monitoring
17
+ try:
18
+ from watchdog.observers import Observer
19
+ from watchdog.events import FileSystemEventHandler
20
+ WATCHDOG_AVAILABLE = True
21
+ logger.info("Watchdog library available for file system monitoring")
22
+ except ImportError:
23
+ WATCHDOG_AVAILABLE = False
24
+ Observer = None
25
+ FileSystemEventHandler = None
26
+ logger.warning("Watchdog library not available - file system monitoring disabled")
27
+
28
+
29
+ class FileSystemWatcher:
30
+ """Watches file system changes for project folders."""
31
+
32
+ def __init__(self, project_manager):
33
+ self.project_manager = project_manager # Reference to ProjectStateManager
34
+ self.observer: Optional[Observer] = None
35
+ self.event_handler: Optional[FileSystemEventHandler] = None
36
+ self.watched_paths: Set[str] = set()
37
+ self.watch_handles: dict = {} # Map path -> watch handle for proper cleanup
38
+ # Store reference to the event loop for thread-safe async task creation
39
+ try:
40
+ self.event_loop = asyncio.get_running_loop()
41
+ logger.debug("🔍 [TRACE] ✅ Captured event loop reference for file system watcher: %s", self.event_loop)
42
+ except RuntimeError:
43
+ self.event_loop = None
44
+ logger.debug("🔍 [TRACE] ❌ No running event loop found - file system events may not work correctly")
45
+
46
+ logger.debug("🔍 [TRACE] WATCHDOG_AVAILABLE: %s", WATCHDOG_AVAILABLE)
47
+ if WATCHDOG_AVAILABLE:
48
+ logger.debug("🔍 [TRACE] Initializing file system watcher...")
49
+ self._initialize_watcher()
50
+ else:
51
+ logger.debug("🔍 [TRACE] ❌ Watchdog not available - file monitoring disabled")
52
+
53
+ def _initialize_watcher(self):
54
+ """Initialize file system watcher."""
55
+ if not WATCHDOG_AVAILABLE:
56
+ logger.warning("Watchdog not available, file monitoring disabled")
57
+ return
58
+
59
+ class ProjectEventHandler(FileSystemEventHandler):
60
+ def __init__(self, manager, watcher):
61
+ self.manager = manager
62
+ self.watcher = watcher
63
+ super().__init__()
64
+
65
+ def on_any_event(self, event):
66
+ logger.debug("🔍 [TRACE] FileSystemWatcher detected event: %s on path: %s", event.event_type, event.src_path)
67
+
68
+ # Skip debug files to avoid feedback loops
69
+ if event.src_path.endswith('project_state_debug.json'):
70
+ logger.debug("🔍 [TRACE] Skipping debug file: %s", event.src_path)
71
+ return
72
+
73
+ # Only process events that represent actual content changes
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)
83
+ return
84
+
85
+ # Handle .git folder events separately for git status monitoring
86
+ path_parts = Path(event.src_path).parts
87
+ if '.git' in path_parts:
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))
95
+ else:
96
+ logger.debug("File event: %s", os.path.basename(event.src_path))
97
+
98
+ # Schedule async task in the main event loop from this watchdog thread
99
+ logger.debug("🔍 [TRACE] About to schedule async handler - event_loop exists: %s, closed: %s",
100
+ self.watcher.event_loop is not None,
101
+ self.watcher.event_loop.is_closed() if self.watcher.event_loop else "N/A")
102
+
103
+ if self.watcher.event_loop and not self.watcher.event_loop.is_closed():
104
+ try:
105
+ future = asyncio.run_coroutine_threadsafe(
106
+ self.manager._handle_file_change(event),
107
+ self.watcher.event_loop
108
+ )
109
+ logger.debug("🔍 [TRACE] ✅ Successfully scheduled file change handler for: %s", event.src_path)
110
+ except Exception as e:
111
+ logger.debug("🔍 [TRACE] ❌ Failed to schedule file change handler: %s", e)
112
+ else:
113
+ logger.debug("🔍 [TRACE] ❌ No event loop available to handle file change: %s", event.src_path)
114
+
115
+ self.event_handler = ProjectEventHandler(self.project_manager, self)
116
+ self.observer = Observer()
117
+
118
+ def start_watching(self, path: str):
119
+ """Start watching a specific path."""
120
+ if not WATCHDOG_AVAILABLE or not self.observer:
121
+ logger.warning("Watchdog not available, cannot start watching: %s", path)
122
+ return
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
+
129
+ if path not in self.watched_paths:
130
+ try:
131
+ # Use recursive=False to watch only direct contents of each folder
132
+ watch_handle = self.observer.schedule(self.event_handler, path, recursive=False)
133
+ self.watched_paths.add(path)
134
+ self.watch_handles[path] = watch_handle # Store handle for cleanup
135
+ logger.info("Started watching path (non-recursive): %s", path)
136
+
137
+ if not self.observer.is_alive():
138
+ self.observer.start()
139
+ logger.info("Started file system observer")
140
+ except Exception as e:
141
+ logger.error("Error starting file watcher for %s: %s", path, e)
142
+ else:
143
+ logger.debug("Path already being watched: %s", path)
144
+
145
+ def stop_watching(self, path: str):
146
+ """Stop watching a specific path."""
147
+ if not WATCHDOG_AVAILABLE or not self.observer:
148
+ return
149
+
150
+ if path in self.watched_paths:
151
+ # Actually unschedule the watch using stored handle
152
+ watch_handle = self.watch_handles.get(path)
153
+ if watch_handle:
154
+ try:
155
+ self.observer.unschedule(watch_handle)
156
+ logger.info("Successfully unscheduled watch for: %s", path)
157
+ except Exception as e:
158
+ logger.error("Error unscheduling watch for %s: %s", path, e)
159
+ finally:
160
+ self.watch_handles.pop(path, None)
161
+
162
+ self.watched_paths.discard(path)
163
+ logger.debug("Stopped watching path: %s", path)
164
+
165
+ def stop_all(self):
166
+ """Stop all file watching."""
167
+ if self.observer and self.observer.is_alive():
168
+ self.observer.stop()
169
+ self.observer.join()
170
+ self.watched_paths.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
+ }