portacode 1.3.30__py3-none-any.whl → 1.3.32__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 +2 -2
- portacode/connection/handlers/project_state/file_system_watcher.py +24 -10
- portacode/connection/handlers/project_state/git_manager.py +11 -3
- portacode/connection/handlers/project_state/handlers.py +9 -9
- portacode/connection/handlers/project_state/manager.py +25 -11
- {portacode-1.3.30.dist-info → portacode-1.3.32.dist-info}/METADATA +1 -1
- {portacode-1.3.30.dist-info → portacode-1.3.32.dist-info}/RECORD +11 -11
- {portacode-1.3.30.dist-info → portacode-1.3.32.dist-info}/WHEEL +0 -0
- {portacode-1.3.30.dist-info → portacode-1.3.32.dist-info}/entry_points.txt +0 -0
- {portacode-1.3.30.dist-info → portacode-1.3.32.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.3.30.dist-info → portacode-1.3.32.dist-info}/top_level.txt +0 -0
portacode/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.3.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 3,
|
|
31
|
+
__version__ = version = '1.3.32'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 3, 32)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -28,12 +28,13 @@ except ImportError:
|
|
|
28
28
|
|
|
29
29
|
class FileSystemWatcher:
|
|
30
30
|
"""Watches file system changes for project folders."""
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
def __init__(self, project_manager):
|
|
33
33
|
self.project_manager = project_manager # Reference to ProjectStateManager
|
|
34
34
|
self.observer: Optional[Observer] = None
|
|
35
35
|
self.event_handler: Optional[FileSystemEventHandler] = None
|
|
36
36
|
self.watched_paths: Set[str] = set()
|
|
37
|
+
self.watch_handles: dict = {} # Map path -> watch handle for proper cleanup
|
|
37
38
|
# Store reference to the event loop for thread-safe async task creation
|
|
38
39
|
try:
|
|
39
40
|
self.event_loop = asyncio.get_running_loop()
|
|
@@ -140,14 +141,15 @@ class FileSystemWatcher:
|
|
|
140
141
|
if not WATCHDOG_AVAILABLE or not self.observer:
|
|
141
142
|
logger.warning("Watchdog not available, cannot start watching: %s", path)
|
|
142
143
|
return
|
|
143
|
-
|
|
144
|
+
|
|
144
145
|
if path not in self.watched_paths:
|
|
145
146
|
try:
|
|
146
147
|
# Use recursive=False to watch only direct contents of each folder
|
|
147
|
-
self.observer.schedule(self.event_handler, path, recursive=False)
|
|
148
|
+
watch_handle = self.observer.schedule(self.event_handler, path, recursive=False)
|
|
148
149
|
self.watched_paths.add(path)
|
|
150
|
+
self.watch_handles[path] = watch_handle # Store handle for cleanup
|
|
149
151
|
logger.info("Started watching path (non-recursive): %s", path)
|
|
150
|
-
|
|
152
|
+
|
|
151
153
|
if not self.observer.is_alive():
|
|
152
154
|
self.observer.start()
|
|
153
155
|
logger.info("Started file system observer")
|
|
@@ -161,14 +163,15 @@ class FileSystemWatcher:
|
|
|
161
163
|
if not WATCHDOG_AVAILABLE or not self.observer:
|
|
162
164
|
logger.warning("Watchdog not available, cannot start watching git directory: %s", git_path)
|
|
163
165
|
return
|
|
164
|
-
|
|
166
|
+
|
|
165
167
|
if git_path not in self.watched_paths:
|
|
166
168
|
try:
|
|
167
169
|
# Watch .git directory recursively to catch changes in refs/, logs/, etc.
|
|
168
|
-
self.observer.schedule(self.event_handler, git_path, recursive=True)
|
|
170
|
+
watch_handle = self.observer.schedule(self.event_handler, git_path, recursive=True)
|
|
169
171
|
self.watched_paths.add(git_path)
|
|
172
|
+
self.watch_handles[git_path] = watch_handle # Store handle for cleanup
|
|
170
173
|
logger.info("Started watching git directory (recursive): %s", git_path)
|
|
171
|
-
|
|
174
|
+
|
|
172
175
|
if not self.observer.is_alive():
|
|
173
176
|
self.observer.start()
|
|
174
177
|
logger.info("Started file system observer")
|
|
@@ -181,9 +184,19 @@ class FileSystemWatcher:
|
|
|
181
184
|
"""Stop watching a specific path."""
|
|
182
185
|
if not WATCHDOG_AVAILABLE or not self.observer:
|
|
183
186
|
return
|
|
184
|
-
|
|
187
|
+
|
|
185
188
|
if path in self.watched_paths:
|
|
186
|
-
#
|
|
189
|
+
# Actually unschedule the watch using stored handle
|
|
190
|
+
watch_handle = self.watch_handles.get(path)
|
|
191
|
+
if watch_handle:
|
|
192
|
+
try:
|
|
193
|
+
self.observer.unschedule(watch_handle)
|
|
194
|
+
logger.info("Successfully unscheduled watch for: %s", path)
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.error("Error unscheduling watch for %s: %s", path, e)
|
|
197
|
+
finally:
|
|
198
|
+
self.watch_handles.pop(path, None)
|
|
199
|
+
|
|
187
200
|
self.watched_paths.discard(path)
|
|
188
201
|
logger.debug("Stopped watching path: %s", path)
|
|
189
202
|
|
|
@@ -192,4 +205,5 @@ class FileSystemWatcher:
|
|
|
192
205
|
if self.observer and self.observer.is_alive():
|
|
193
206
|
self.observer.stop()
|
|
194
207
|
self.observer.join()
|
|
195
|
-
self.watched_paths.clear()
|
|
208
|
+
self.watched_paths.clear()
|
|
209
|
+
self.watch_handles.clear()
|
|
@@ -350,9 +350,18 @@ class GitManager:
|
|
|
350
350
|
status_output = self.repo.git.status(*rel_paths, porcelain=True)
|
|
351
351
|
if status_output.strip():
|
|
352
352
|
for line in status_output.strip().split('\n'):
|
|
353
|
+
# Git porcelain format: XY path (X=index, Y=worktree, then space, then path)
|
|
354
|
+
# Some files may have renamed format: XY path -> new_path
|
|
353
355
|
if len(line) >= 3:
|
|
354
|
-
|
|
355
|
-
|
|
356
|
+
# Skip first 3 characters (2 status + 1 space) to get the file path
|
|
357
|
+
# But git uses exactly 2 chars for status then space, so position 3 onwards is path
|
|
358
|
+
parts = line.split(None, 1) # Split on first whitespace to separate status from path
|
|
359
|
+
if len(parts) >= 2:
|
|
360
|
+
file_path_from_status = parts[1]
|
|
361
|
+
# Handle renames (format: "old_path -> new_path")
|
|
362
|
+
if ' -> ' in file_path_from_status:
|
|
363
|
+
file_path_from_status = file_path_from_status.split(' -> ')[1]
|
|
364
|
+
status_map[file_path_from_status] = line
|
|
356
365
|
except Exception as e:
|
|
357
366
|
logger.debug("Error getting batch status: %s", e)
|
|
358
367
|
|
|
@@ -469,7 +478,6 @@ class GitManager:
|
|
|
469
478
|
elif index_status == 'D' or worktree_status == 'D':
|
|
470
479
|
has_deleted = True
|
|
471
480
|
|
|
472
|
-
# Priority order: untracked > modified/deleted > clean
|
|
473
481
|
if has_untracked:
|
|
474
482
|
return {"is_tracked": False, "status": "untracked", "is_ignored": False, "is_staged": is_staged}
|
|
475
483
|
elif has_deleted:
|
|
@@ -408,10 +408,10 @@ class ProjectStateGitStageHandler(AsyncHandler):
|
|
|
408
408
|
success = git_manager.stage_file(file_paths_to_stage[0])
|
|
409
409
|
else:
|
|
410
410
|
success = git_manager.stage_files(file_paths_to_stage)
|
|
411
|
-
|
|
411
|
+
|
|
412
412
|
if success:
|
|
413
|
-
# Refresh
|
|
414
|
-
await manager._refresh_project_state(source_client_session)
|
|
413
|
+
# Refresh git status only (no filesystem changes from staging)
|
|
414
|
+
await manager._refresh_project_state(source_client_session, git_only=True)
|
|
415
415
|
|
|
416
416
|
# Build response
|
|
417
417
|
response = {
|
|
@@ -482,10 +482,10 @@ class ProjectStateGitUnstageHandler(AsyncHandler):
|
|
|
482
482
|
success = git_manager.unstage_file(file_paths_to_unstage[0])
|
|
483
483
|
else:
|
|
484
484
|
success = git_manager.unstage_files(file_paths_to_unstage)
|
|
485
|
-
|
|
485
|
+
|
|
486
486
|
if success:
|
|
487
|
-
# Refresh
|
|
488
|
-
await manager._refresh_project_state(source_client_session)
|
|
487
|
+
# Refresh git status only (no filesystem changes from unstaging)
|
|
488
|
+
await manager._refresh_project_state(source_client_session, git_only=True)
|
|
489
489
|
|
|
490
490
|
# Build response
|
|
491
491
|
response = {
|
|
@@ -620,9 +620,9 @@ class ProjectStateGitCommitHandler(AsyncHandler):
|
|
|
620
620
|
if success:
|
|
621
621
|
# Get the commit hash of the new commit
|
|
622
622
|
commit_hash = git_manager.get_head_commit_hash()
|
|
623
|
-
|
|
624
|
-
# Refresh
|
|
625
|
-
await manager._refresh_project_state(source_client_session)
|
|
623
|
+
|
|
624
|
+
# Refresh git status only (no filesystem changes from commit)
|
|
625
|
+
await manager._refresh_project_state(source_client_session, git_only=True)
|
|
626
626
|
except Exception as e:
|
|
627
627
|
error_message = str(e)
|
|
628
628
|
logger.error("Error during commit: %s", error_message)
|
|
@@ -152,7 +152,8 @@ class ProjectStateManager:
|
|
|
152
152
|
async def git_change_callback():
|
|
153
153
|
"""Callback when git status changes are detected."""
|
|
154
154
|
logger.debug("Git change detected, refreshing project state for %s", client_session_id)
|
|
155
|
-
|
|
155
|
+
# Git directory changes only affect git status, not filesystem
|
|
156
|
+
await self._refresh_project_state(client_session_id, git_only=True)
|
|
156
157
|
|
|
157
158
|
git_manager = GitManager(project_folder_path, change_callback=git_change_callback)
|
|
158
159
|
self.git_managers[client_session_id] = git_manager
|
|
@@ -877,9 +878,17 @@ class ProjectStateManager:
|
|
|
877
878
|
self._pending_changes.clear()
|
|
878
879
|
logger.debug("🔍 [TRACE] ✅ Finished processing file changes")
|
|
879
880
|
|
|
880
|
-
async def _refresh_project_state(self, client_session_id: str):
|
|
881
|
-
"""Refresh project state after file changes.
|
|
882
|
-
|
|
881
|
+
async def _refresh_project_state(self, client_session_id: str, git_only: bool = False):
|
|
882
|
+
"""Refresh project state after file changes.
|
|
883
|
+
|
|
884
|
+
Args:
|
|
885
|
+
client_session_id: The client session ID
|
|
886
|
+
git_only: If True, only git status changed (skip filesystem operations like
|
|
887
|
+
detecting new directories and syncing file state). Use this for
|
|
888
|
+
git operations (stage, unstage, revert) to avoid unnecessary work.
|
|
889
|
+
"""
|
|
890
|
+
logger.debug("🔍 [TRACE] _refresh_project_state called for session: %s (git_only=%s)",
|
|
891
|
+
client_session_id, git_only)
|
|
883
892
|
|
|
884
893
|
if client_session_id not in self.projects:
|
|
885
894
|
logger.debug("🔍 [TRACE] ❌ Session not found in projects: %s", client_session_id)
|
|
@@ -930,15 +939,20 @@ class ProjectStateManager:
|
|
|
930
939
|
old_status_summary, project_state.git_status_summary)
|
|
931
940
|
else:
|
|
932
941
|
logger.debug("🔍 [TRACE] ❌ No git manager found for session: %s", client_session_id)
|
|
933
|
-
|
|
934
|
-
#
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
942
|
+
|
|
943
|
+
# For git-only operations, skip scanning for new directories
|
|
944
|
+
# but still sync items to update git attributes for UI
|
|
945
|
+
if not git_only:
|
|
946
|
+
# Detect and add new directories in expanded folders before syncing
|
|
947
|
+
logger.debug("🔍 [TRACE] Detecting and adding new directories...")
|
|
948
|
+
await self._detect_and_add_new_directories(project_state)
|
|
949
|
+
else:
|
|
950
|
+
logger.debug("🔍 [TRACE] Skipping directory detection (git_only=True)")
|
|
951
|
+
|
|
952
|
+
# Always sync state to update git attributes on items (needed for UI updates)
|
|
939
953
|
logger.debug("🔍 [TRACE] Syncing all state with monitored folders...")
|
|
940
954
|
await self._sync_all_state_with_monitored_folders(project_state)
|
|
941
|
-
|
|
955
|
+
|
|
942
956
|
# Send update to clients
|
|
943
957
|
logger.debug("🔍 [TRACE] About to send project state update...")
|
|
944
958
|
await self._send_project_state_update(project_state)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
portacode/README.md,sha256=4dKtpvR8LNgZPVz37GmkQCMWIr_u25Ao63iW56s7Ke4,775
|
|
2
2
|
portacode/__init__.py,sha256=oB3sV1wXr-um-RXio73UG8E5Xx6cF2ZVJveqjNmC-vQ,1086
|
|
3
3
|
portacode/__main__.py,sha256=jmHTGC1hzmo9iKJLv-SSYe9BSIbPPZ2IOpecI03PlTs,296
|
|
4
|
-
portacode/_version.py,sha256=
|
|
4
|
+
portacode/_version.py,sha256=d_JbLCEf2f0Y5ScYYYOP97vUKSwyr-VyTj2aQkfr7KU,706
|
|
5
5
|
portacode/cli.py,sha256=eDqcZMVFHKzqqWxedhhx8ylu5WMVCLqeJQkbPR7RcJE,16333
|
|
6
6
|
portacode/data.py,sha256=5-s291bv8J354myaHm1Y7CQZTZyRzMU3TGe5U4hb-FA,1591
|
|
7
7
|
portacode/keypair.py,sha256=PAcOYqlVLOoZTPYi6LvLjfsY6BkrWbLOhSZLb8r5sHs,3635
|
|
@@ -27,10 +27,10 @@ portacode/connection/handlers/tab_factory.py,sha256=VBZnwtxgeNJCsfBzUjkFWAAGBdij
|
|
|
27
27
|
portacode/connection/handlers/terminal_handlers.py,sha256=HRwHW1GiqG1NtHVEqXHKaYkFfQEzCDDH6YIlHcb4XD8,11866
|
|
28
28
|
portacode/connection/handlers/project_state/README.md,sha256=trdd4ig6ungmwH5SpbSLfyxbL-QgPlGNU-_XrMEiXtw,10114
|
|
29
29
|
portacode/connection/handlers/project_state/__init__.py,sha256=5ucIqk6Iclqg6bKkL8r_wVs5Tlt6B9J7yQH6yQUt7gc,2541
|
|
30
|
-
portacode/connection/handlers/project_state/file_system_watcher.py,sha256=
|
|
31
|
-
portacode/connection/handlers/project_state/git_manager.py,sha256=
|
|
32
|
-
portacode/connection/handlers/project_state/handlers.py,sha256=
|
|
33
|
-
portacode/connection/handlers/project_state/manager.py,sha256=
|
|
30
|
+
portacode/connection/handlers/project_state/file_system_watcher.py,sha256=2zingW9BoNKRijghHC2eHHdRoyDRdLmIl1yH1y-iuF8,10831
|
|
31
|
+
portacode/connection/handlers/project_state/git_manager.py,sha256=tVcuiGYKk1GJ0f7dgjoSGNL13IXCHUOtoi_VBc8ZTqQ,88682
|
|
32
|
+
portacode/connection/handlers/project_state/handlers.py,sha256=nhs-3yiENdewAzVZnSdn2Ir-e6TQ9Nz_Bxk3iiFPd9c,37985
|
|
33
|
+
portacode/connection/handlers/project_state/manager.py,sha256=XX3wMgGdPbRgBBs_R1dXtQ4D9j-itETrJR_6IfBeDU0,61296
|
|
34
34
|
portacode/connection/handlers/project_state/models.py,sha256=EZTKvxHKs8QlQUbzI0u2IqfzfRRXZixUIDBwTGCJATI,4313
|
|
35
35
|
portacode/connection/handlers/project_state/utils.py,sha256=LsbQr9TH9Bz30FqikmtTxco4PlB_n0kUIuPKQ6Fb_mo,1665
|
|
36
36
|
portacode/static/js/test-ntp-clock.html,sha256=bUow9sifIuLNPqKvuPbpQozmEE6RhdCI4Plib3CqUmw,2130
|
|
@@ -38,7 +38,7 @@ portacode/static/js/utils/ntp-clock.js,sha256=KMeHGT-IlUSlxVRZZ899z25dQCJh6EJbgX
|
|
|
38
38
|
portacode/utils/NTP_ARCHITECTURE.md,sha256=WkESTbz5SNAgdmDKk3DrHMhtYOPji_Kt3_a9arWdRig,3894
|
|
39
39
|
portacode/utils/__init__.py,sha256=NgBlWTuNJESfIYJzP_3adI1yJQJR0XJLRpSdVNaBAN0,33
|
|
40
40
|
portacode/utils/ntp_clock.py,sha256=6QJOVZr9VQuxIyJt9KNG4dR-nZ3bKNyipMxjqDWP89Y,5152
|
|
41
|
-
portacode-1.3.
|
|
41
|
+
portacode-1.3.32.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
|
|
42
42
|
test_modules/README.md,sha256=Do_agkm9WhSzueXjRAkV_xEj6Emy5zB3N3VKY5Roce8,9274
|
|
43
43
|
test_modules/__init__.py,sha256=1LcbHodIHsB0g-g4NGjSn6AMuCoGbymvXPYLOb6Z7F0,53
|
|
44
44
|
test_modules/test_device_online.py,sha256=yiSyVaMwKAugqIX_ZIxmLXiOlmA_8IRXiUp12YmpB98,1653
|
|
@@ -63,8 +63,8 @@ testing_framework/core/playwright_manager.py,sha256=8xl-19b8NQjKNdiRyDjyeXlYyKPZ
|
|
|
63
63
|
testing_framework/core/runner.py,sha256=j2QwNJmAxVBmJvcbVS7DgPJUKPNzqfLmt_4NNdaKmZU,19297
|
|
64
64
|
testing_framework/core/shared_cli_manager.py,sha256=BESSNtyQb7BOlaOvZmm04T8Uezjms4KCBs2MzTxvzYQ,8790
|
|
65
65
|
testing_framework/core/test_discovery.py,sha256=2FZ9fJ8Dp5dloA-fkgXoJ_gCMC_nYPBnA3Hs2xlagzM,4928
|
|
66
|
-
portacode-1.3.
|
|
67
|
-
portacode-1.3.
|
|
68
|
-
portacode-1.3.
|
|
69
|
-
portacode-1.3.
|
|
70
|
-
portacode-1.3.
|
|
66
|
+
portacode-1.3.32.dist-info/METADATA,sha256=rTf7KoQ6JhbUqq_aOd-8ASc7UfR53vtrxdaLQ-ANsZ8,6989
|
|
67
|
+
portacode-1.3.32.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
68
|
+
portacode-1.3.32.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
|
|
69
|
+
portacode-1.3.32.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
70
|
+
portacode-1.3.32.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|